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 #[cfg(feature = "csharp")]
458 use crate::test_support::assert_csharp_fixture_spells;
459 use crate::test_support::{
460 assert_fixture_spells, ast_has_kind_id, check_func_space_only_shim,
461 check_metrics_only_shim, child_space, metrics_verbatim,
462 };
463 // Hand-written, not derived. A trait is used through its methods, so
464 // nothing in a test body names `ParserTrait` and
465 // `check-test-lang-gates.py` derives no gate for an import at all,
466 // so an ungated one stays compiled into builds that never call
467 // `<Lang>Parser::new`. These seven are the languages that do; a new
468 // `XParser::new` test in this module needs its feature added here,
469 // and the leg that lacks it says so as an unused import.
470 #[cfg(any(
471 feature = "csharp",
472 feature = "groovy",
473 feature = "kotlin",
474 feature = "php",
475 feature = "ruby",
476 feature = "rust",
477 feature = "typescript",
478 ))]
479 use crate::traits::ParserTrait;
480
481 use super::*;
482
483 check_metrics_only_shim!(check_metrics, Abc);
484 // Every `check_func_space` caller in this module is an ABC-versus-
485 // cyclomatic parity test (`abc.conditions() == cyclomatic() - 1` on
486 // the same space), so the func-space shim carries Cyclomatic too.
487 check_func_space_only_shim!(check_func_space, Abc, Cyclomatic);
488
489 /// `abc.conditions_sum()` of `src` under a walk restricted to ABC
490 /// and the metrics it resolves, for the cross-language parity tests
491 /// that compare one construct across sibling grammars.
492 /// `check_metrics` expands to a plain `fn` and so cannot close over
493 /// a reference value; this can. Restricted rather than
494 /// `MetricsOptions::default()` per `metrics_verbatim`'s own doc
495 /// (#1127).
496 #[cfg(any(
497 feature = "c",
498 feature = "cpp",
499 feature = "go",
500 feature = "javascript",
501 feature = "mozcpp",
502 feature = "mozjs",
503 feature = "objc",
504 feature = "perl",
505 feature = "typescript",
506 ))]
507 fn abc_conditions(lang: LANG, src: &str) -> u64 {
508 metrics_verbatim(
509 lang,
510 src.as_bytes(),
511 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
512 )
513 .abc
514 .conditions_sum()
515 }
516
517 // Recurse to the innermost function space and assert the invariant
518 // named above there. `conditions()` is that one space's own count,
519 // so it pairs with `cyclomatic()` and never `cyclomatic_sum()`,
520 // which folds in a base of 1 per nested space.
521 #[cfg(any(
522 feature = "cpp",
523 feature = "csharp",
524 feature = "groovy",
525 feature = "java",
526 feature = "perl",
527 feature = "php",
528 feature = "typescript",
529 ))]
530 fn assert_deepest_conditions_match_cyclomatic(space: &crate::FuncSpace, expected: u64) {
531 let mut deepest = space;
532 while let Some(child) = deepest.spaces.last() {
533 deepest = child;
534 }
535 let decisions = deepest.metrics.cyclomatic.cyclomatic() - 1;
536 assert_eq!(decisions, expected);
537 assert_eq!(deepest.metrics.abc.conditions(), decisions);
538 }
539
540 // The sibling form of the helper above, for a fixture whose point is
541 // that several *different* spellings of one construct agree. The
542 // deepest-space walk cannot serve those: it follows `spaces.last()`
543 // and so inspects exactly one member, which is the shape #1383's
544 // over-count hid behind — a merged root total cannot tell a
545 // relational method scoring 2 from the constant control scoring 2.
546 #[cfg(any(feature = "csharp", feature = "elixir"))]
547 fn assert_every_member_scores(
548 container: &crate::FuncSpace,
549 members: usize,
550 expected: u64,
551 why: &str,
552 ) {
553 // The count is a parameter rather than a `!is_empty()` check so
554 // a caller cannot omit it: without it a fixture that lost every
555 // member but one still satisfies "every member scores N".
556 assert_eq!(
557 container.spaces.len(),
558 members,
559 "member count changed — the fixture moved, not the metric"
560 );
561 for member in &container.spaces {
562 let name = member.name.as_deref().unwrap_or("?");
563 assert_eq!(member.metrics.abc.conditions(), expected, "{name}: {why}");
564 assert_eq!(
565 member.metrics.abc.conditions(),
566 member.metrics.cyclomatic.cyclomatic() - 1,
567 "{name}: §8 parity with the cyclomatic decision count"
568 );
569 }
570 }
571
572 // The second sibling, for a fixture whose point is that a construct
573 // moves a member *off* the value its unguarded control scores.
574 // `assert_every_member_scores` cannot serve those: it takes one
575 // expected value for the whole container, and every #1422 guard
576 // fixture deliberately pairs guarded members with an unguarded
577 // control one lower — which is the comparison the test exists to
578 // make. Both tiers per member, because a guard is a decision in each
579 // and a fix that moved only one of them would satisfy neither claim
580 // on its own, and because a member may legitimately sit *above* its
581 // decision count — an `else` arm, or a comparison nested inside
582 // another comparison, is an ABC condition with no cyclomatic
583 // decision behind it (#1421).
584 #[cfg(any(
585 feature = "csharp",
586 feature = "elixir",
587 feature = "groovy",
588 feature = "java",
589 feature = "kotlin",
590 feature = "python",
591 feature = "ruby",
592 feature = "rust",
593 ))]
594 #[track_caller]
595 fn assert_members_score(container: &crate::FuncSpace, expected: &[(&str, u64, u64)]) {
596 assert_eq!(
597 container.spaces.len(),
598 expected.len(),
599 "member count changed — the fixture moved, not the metric"
600 );
601 for (name, conditions, cyclomatic) in expected {
602 let member = child_space(container, name);
603 assert_eq!(
604 member.metrics.abc.conditions(),
605 *conditions,
606 "{name}: abc.conditions"
607 );
608 assert_eq!(
609 member.metrics.cyclomatic.cyclomatic(),
610 *cyclomatic,
611 "{name}: cyclomatic"
612 );
613 }
614 }
615
616 /// Regression for #227: a `Stats::default()` that never sees an
617 /// observation must not leak the `f64::MAX` sentinel for
618 /// `assignments_min`, `branches_min`, or `conditions_min`. All
619 /// three getters collapse the sentinel to `0.0` so JSON never
620 /// emits `1.7976931e308`.
621 #[test]
622 fn abc_empty_file_min_is_zero() {
623 let stats = Stats::default();
624 assert_eq!(stats.assignments_min(), 0);
625 assert_eq!(stats.branches_min(), 0);
626 assert_eq!(stats.conditions_min(), 0);
627 }
628
629 // The `EQ` arm of `java_count_token_assignment`: a plain `=` counts
630 // unless `java_eq_initializes_final_binding` finds it initialising a
631 // `final` binding, whether the declaration is a local or a field.
632 #[cfg(feature = "java")]
633 #[test]
634 fn java_eq_arm_counts_outside_final_declarations() {
635 check_metrics::<JavaParser>(
636 "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
637 "foo.java",
638 |metric| {
639 // `int x = 0;` is not `final`, so it counts like each
640 // `x = N;` that follows.
641 assert_eq!(metric.abc.assignments_sum(), 4);
642 },
643 );
644 }
645
646 #[cfg(feature = "java")]
647 #[test]
648 fn java_eq_arm_skips_final_initializers() {
649 check_metrics::<JavaParser>(
650 "class A {
651 final int X = 1;
652 @Deprecated private final int Y = 2;
653 void m() { final int Z = 3; }
654 }",
655 "foo.java",
656 |metric| {
657 // All three `=` tokens are `final` initializers — the
658 // second behind an annotation and an access modifier in
659 // the same `modifiers` node — so assignments are 0
660 // across all spaces.
661 assert_eq!(metric.abc.assignments_sum(), 0);
662 },
663 );
664 }
665
666 #[cfg(feature = "java")]
667 #[test]
668 fn java_final_initializer_does_not_suppress_the_assignments_inside_it() {
669 // The sentinel stack the structural predicate replaced stayed
670 // live from `final` to the next `;`, so every `=` nested in the
671 // initializer — a lambda body's, an array initializer's — was
672 // suppressed with the declarator's own. Java lambdas open no
673 // space, so nothing else separated them. Each row names the
674 // pre-fix value.
675 let cases = [
676 ("void m() { final Runnable r = () -> { x = 1; }; }", 1, 0),
677 (
678 "void m() { final Runnable r = () -> { x = 1; y = 2; }; }",
679 2,
680 1,
681 ),
682 ("void m() { final Runnable r = () -> x = 1; }", 1, 0),
683 ("void m() { final int[] a = { x = 1 }; }", 1, 0),
684 ("private final Runnable f = () -> { x = 1; };", 1, 0),
685 // The `;` that closed the leak is the reference: the same
686 // body after a `final` local counts as it always did.
687 ("void m() { final int q = 1; x = 1; }", 1, 1),
688 ];
689 let mut ran = 0;
690 for (body, expected, before) in cases {
691 let src = format!("class K {{ int x, y; {body} }}\n");
692 let assignments = metrics_verbatim(
693 LANG::Java,
694 src.as_bytes(),
695 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
696 )
697 .abc
698 .assignments_sum();
699 assert_eq!(assignments, expected, "`{body}` (pre-fix {before})");
700 ran += 1;
701 }
702 assert_eq!(ran, cases.len());
703 assert!(cases.iter().any(|&(_, now, before)| now != before));
704 }
705
706 // Constant declarations are not counted as assignments
707 #[cfg(feature = "java")]
708 #[test]
709 fn java_constant_declarations() {
710 check_metrics::<JavaParser>(
711 "class A {
712 private final int X1 = 0, Y1 = 0;
713 public final float PI = 3.14f;
714 final static String HELLO = \"Hello,\";
715 protected String world = \" world!\"; // +1a
716 public float e = 2.718f; // +1a
717 private int x2 = 1, y2 = 2; // +2a
718
719 void m() {
720 final int Z1 = 0, Z2 = 0, Z3 = 0;
721 final float T = 0.0f;
722 int z1 = 1, z2 = 2, z3 = 3; // +3a
723 float t = 60.0f; // +1a
724 }
725 }",
726 "foo.java",
727 |metric| {
728 // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
729 // space count: 3 (1 unit, 1 class and 1 method)
730 insta::assert_json_snapshot!(
731 metric.abc,
732 @r#"
733 {
734 "assignments": 8,
735 "branches": 0,
736 "conditions": 0,
737 "magnitude": 8.0,
738 "value": 0.0,
739 "assignments_average": 2.6666666666666665,
740 "branches_average": 0.0,
741 "conditions_average": 0.0,
742 "assignments_min": 0,
743 "assignments_max": 4,
744 "branches_min": 0,
745 "branches_max": 0,
746 "conditions_min": 0,
747 "conditions_max": 0
748 }
749 "#
750 );
751 },
752 );
753 }
754
755 // "In computer science, conditionals (that is, conditional statements, conditional expressions
756 // and conditional constructs,) are programming language commands for handling decisions."
757 // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
758 // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
759 // Variables, method invocations and true or false values used inside
760 // variable declarations and assignment expressions are not counted as conditions
761 #[cfg(feature = "java")]
762 #[test]
763 fn java_declarations_with_conditions() {
764 check_metrics::<JavaParser>(
765 "
766 boolean a = (1 > 2); // +1a +1c
767 boolean b = 3 > 4; // +1a +1c
768 boolean c = (1 > 2) && 3 > 4; // +1a +2c
769 boolean d = b && (x > 5) || c; // +1a +3c
770 boolean e = !d; // +1a +1c
771 boolean f = ((!false)); // +1a +1c
772 boolean g = !(!(true)); // +1a +1c
773 boolean h = true; // +1a
774 boolean i = (false); // +1a
775 boolean j = (((((true))))); // +1a
776 boolean k = (((((m()))))); // +1a +1b
777 boolean l = (((((!m()))))); // +1a +1b +1c
778 boolean m = (!(!((m())))); // +1a +1b +1c
779 List<String> n = null; // +1a (< and > used for generic types are not counted as conditions)
780 ",
781 "foo.java",
782 |metric| {
783 // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
784 // space count: 1 (1 unit)
785 insta::assert_json_snapshot!(
786 metric.abc,
787 @r#"
788 {
789 "assignments": 14,
790 "branches": 3,
791 "conditions": 12,
792 "magnitude": 18.681541692269406,
793 "value": 18.681541692269406,
794 "assignments_average": 14.0,
795 "branches_average": 3.0,
796 "conditions_average": 12.0,
797 "assignments_min": 14,
798 "assignments_max": 14,
799 "branches_min": 3,
800 "branches_max": 3,
801 "conditions_min": 12,
802 "conditions_max": 12
803 }
804 "#
805 );
806 },
807 );
808 }
809
810 // Conditions can be found in assignment expressions
811 #[cfg(feature = "java")]
812 #[test]
813 fn java_assignments_with_conditions() {
814 check_metrics::<JavaParser>(
815 "
816 a = 2 < 1; // +1a +1c
817 b = (4 >= 3) && 2 <= 1; // +1a +2c
818 c = a || (x != 10) && b; // +1a +3c
819 d = !false; // +1a +1c
820 e = (!false); // +1a +1c
821 f = !(false); // +1a +1c
822 g = (!(((true)))); // +1a +1c
823 h = ((true)); // +1a
824 i = !m(); // +1a +1b +1c
825 j = !((m())); // +1a +1b +1c
826 k = (!(m())); // +1a +1b +1c
827 l = ((!(m()))); // +1a +1b +1c
828 m = !B.<Integer>m(2); // +1a +1b +1c
829 n = !((B.<Integer>m(4))); // +1a +1b +1c
830 ",
831 "foo.java",
832 |metric| {
833 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
834 // space count: 1 (1 unit)
835 insta::assert_json_snapshot!(
836 metric.abc,
837 @r#"
838 {
839 "assignments": 14,
840 "branches": 6,
841 "conditions": 16,
842 "magnitude": 22.090722034374522,
843 "value": 22.090722034374522,
844 "assignments_average": 14.0,
845 "branches_average": 6.0,
846 "conditions_average": 16.0,
847 "assignments_min": 14,
848 "assignments_max": 14,
849 "branches_min": 6,
850 "branches_max": 6,
851 "conditions_min": 16,
852 "conditions_max": 16
853 }
854 "#
855 );
856 },
857 );
858 }
859
860 // Conditions can be found in method arguments
861 #[cfg(feature = "java")]
862 #[test]
863 fn java_methods_arguments_with_conditions() {
864 check_metrics::<JavaParser>(
865 "
866 m1(a); // +1b
867 m2(a, b); // +1b
868 m3(true, (false), (((true)))); // +1b
869 m3(m1(false), m1(true), m1(false)); // +4b
870 m1(!a); // +1b +1c
871 m2((((a))), (!b)); // +1b +1c
872 m3(!(a), b, !!!c); // +1b +2c
873 m3(a, !b, m2(!a, !m2(!b, !m1(!c)))); // +4b +6c
874 ",
875 "foo.java",
876 |metric| {
877 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
878 // space count: 1 (1 unit)
879 insta::assert_json_snapshot!(
880 metric.abc,
881 @r#"
882 {
883 "assignments": 0,
884 "branches": 14,
885 "conditions": 10,
886 "magnitude": 17.204650534085253,
887 "value": 17.204650534085253,
888 "assignments_average": 0.0,
889 "branches_average": 14.0,
890 "conditions_average": 10.0,
891 "assignments_min": 0,
892 "assignments_max": 0,
893 "branches_min": 14,
894 "branches_max": 14,
895 "conditions_min": 10,
896 "conditions_max": 10
897 }
898 "#
899 );
900 },
901 );
902 }
903
904 // "A unary conditional expression is an implicit condition that uses no relational operators."
905 // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
906 // https://www.softwarerenovation.com/Articles.aspx (page 5)
907 #[cfg(feature = "java")]
908 #[test]
909 fn java_if_single_conditions() {
910 check_metrics::<JavaParser>(
911 "
912 if ( a < 0 ) {} // +1c
913 if ( ((a != 0)) ) {} // +1c
914 if ( !(a > 0) ) {} // +1c
915 if ( !(((a == 0))) ) {} // +1c
916 if ( b.m1() ) {} // +1b +1c
917 if ( !b.m1() ) {} // +1b +1c
918 if ( !!b.m2() ) {} // +1b +1c
919 if ( (!(b.m1())) ) {} // +1b +1c
920 if ( (!(!b.m1())) ) {} // +1b +1c
921 if ( ((b.m2())) ) {} // +1b +1c
922 if ( ((b.m().m1())) ) {} // +2b +1c
923 if ( c ) {} // +1c
924 if ( !c ) {} // +1c
925 if ( !!!!!!!!!!c ) {} // +1c
926 if ( (((c))) ) {} // +1c
927 if ( (((!c))) ) {} // +1c
928 if ( ((!(c))) ) {} // +1c
929 if ( true ) {} // +1c
930 if ( !true ) {} // +1c
931 if ( ((false)) ) {} // +1c
932 if ( !(!(false)) ) {} // +1c
933 if ( !!!false ) {} // +1c
934 ",
935 "foo.java",
936 |metric| {
937 // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
938 // space count: 1 (1 unit)
939 insta::assert_json_snapshot!(
940 metric.abc,
941 @r#"
942 {
943 "assignments": 0,
944 "branches": 8,
945 "conditions": 22,
946 "magnitude": 23.40939982143925,
947 "value": 23.40939982143925,
948 "assignments_average": 0.0,
949 "branches_average": 8.0,
950 "conditions_average": 22.0,
951 "assignments_min": 0,
952 "assignments_max": 0,
953 "branches_min": 8,
954 "branches_max": 8,
955 "conditions_min": 22,
956 "conditions_max": 22
957 }
958 "#
959 );
960 },
961 );
962 }
963
964 #[cfg(feature = "java")]
965 #[test]
966 fn java_if_multiple_conditions() {
967 check_metrics::<JavaParser>(
968 "
969 if ( a || b || c || d ) {} // +4c
970 if ( a || b && c && d ) {} // +4c
971 if ( x < y && a == b ) {} // +2c
972 if ( ((z < (x + y))) ) {} // +1c
973 if ( a || ((((b))) && c) ) {} // +3c
974 if ( a && ((((a == b))) && c) ) {} // +3c
975 if ( a || ((((a == b))) || ((c))) ) {} // +3c
976 if ( x < y && B.m() ) {} // +1b +2c
977 if ( x < y && !(((B.m()))) ) {} // +1b +2c
978 if ( !(x < y) && !B.m() ) {} // +1b +2c
979 if ( !!!(!!!(a)) && B.m() || // +1b +2c
980 !B.m() && (((x > 4))) ) {} // +1b +2c
981 ",
982 "foo.java",
983 |metric| {
984 // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
985 // space count: 1 (1 unit)
986 insta::assert_json_snapshot!(
987 metric.abc,
988 @r#"
989 {
990 "assignments": 0,
991 "branches": 5,
992 "conditions": 30,
993 "magnitude": 30.4138126514911,
994 "value": 30.4138126514911,
995 "assignments_average": 0.0,
996 "branches_average": 5.0,
997 "conditions_average": 30.0,
998 "assignments_min": 0,
999 "assignments_max": 0,
1000 "branches_min": 5,
1001 "branches_max": 5,
1002 "conditions_min": 30,
1003 "conditions_max": 30
1004 }
1005 "#
1006 );
1007 },
1008 );
1009 }
1010
1011 #[cfg(feature = "java")]
1012 #[test]
1013 fn java_while_and_do_while_conditions() {
1014 check_metrics::<JavaParser>(
1015 "
1016 while ( (!(!(!(a)))) ) {} // +1c
1017 while ( b || 1 > 2 ) {} // +2c
1018 while ( x.m() && (((c))) ) {} // +1b +2c
1019 do {} while ( !!!(((!!!a))) ); // +1c
1020 do {} while ( a || (b && c) ); // +3c
1021 do {} while ( !x.m() && 1 > 2 || !true ); // +1b +3c
1022 ",
1023 "foo.java",
1024 |metric| {
1025 // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
1026 // space count: 1 (1 unit)
1027 insta::assert_json_snapshot!(
1028 metric.abc,
1029 @r#"
1030 {
1031 "assignments": 0,
1032 "branches": 2,
1033 "conditions": 12,
1034 "magnitude": 12.165525060596439,
1035 "value": 12.165525060596439,
1036 "assignments_average": 0.0,
1037 "branches_average": 2.0,
1038 "conditions_average": 12.0,
1039 "assignments_min": 0,
1040 "assignments_max": 0,
1041 "branches_min": 2,
1042 "branches_max": 2,
1043 "conditions_min": 12,
1044 "conditions_max": 12
1045 }
1046 "#
1047 );
1048 },
1049 );
1050 }
1051
1052 // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
1053 // "These are cases where a single variable/field/value is treated as a boolean value.
1054 // Examples include `if (x)` and `return !ready`."
1055 // According to this definition, unary conditional expressions are counted also in function return values.
1056 // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
1057 // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
1058 #[cfg(feature = "java")]
1059 #[test]
1060 fn java_return_with_conditions() {
1061 check_metrics::<JavaParser>(
1062 "class A {
1063 boolean m1() {
1064 return !(z >= 0); // +1c
1065 }
1066 boolean m2() {
1067 return (((!x))); // +1c
1068 }
1069 boolean m3() {
1070 return x && y; // +2c
1071 }
1072 boolean m4() {
1073 return y || (z < 0); // +2c
1074 }
1075 boolean m5() {
1076 return x || y ? // +3c (two unary conditions and one ?)
1077 true : false;
1078 }
1079 }",
1080 "foo.java",
1081 |metric| {
1082 // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
1083 // space count: 7 (1 unit, 1 class and 5 methods)
1084 insta::assert_json_snapshot!(
1085 metric.abc,
1086 @r#"
1087 {
1088 "assignments": 0,
1089 "branches": 0,
1090 "conditions": 9,
1091 "magnitude": 9.0,
1092 "value": 0.0,
1093 "assignments_average": 0.0,
1094 "branches_average": 0.0,
1095 "conditions_average": 1.2857142857142858,
1096 "assignments_min": 0,
1097 "assignments_max": 0,
1098 "branches_min": 0,
1099 "branches_max": 0,
1100 "conditions_min": 0,
1101 "conditions_max": 3
1102 }
1103 "#
1104 );
1105 },
1106 );
1107 }
1108
1109 // Variables, method invocations, and true or false values
1110 // inside return statements are not counted as conditions
1111 #[cfg(feature = "java")]
1112 #[test]
1113 fn java_return_without_conditions() {
1114 check_metrics::<JavaParser>(
1115 "class A {
1116 boolean m1() {
1117 return x;
1118 }
1119 boolean m2() {
1120 return (x);
1121 }
1122 boolean m3() {
1123 return y.m(); // +1b
1124 }
1125 boolean m4() {
1126 return false;
1127 }
1128 void m5() {
1129 return;
1130 }
1131 }",
1132 "foo.java",
1133 |metric| {
1134 // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
1135 // space count: 7 (1 unit, 1 class and 5 methods)
1136 insta::assert_json_snapshot!(
1137 metric.abc,
1138 @r#"
1139 {
1140 "assignments": 0,
1141 "branches": 1,
1142 "conditions": 0,
1143 "magnitude": 1.0,
1144 "value": 0.0,
1145 "assignments_average": 0.0,
1146 "branches_average": 0.14285714285714285,
1147 "conditions_average": 0.0,
1148 "assignments_min": 0,
1149 "assignments_max": 0,
1150 "branches_min": 0,
1151 "branches_max": 1,
1152 "conditions_min": 0,
1153 "conditions_max": 0
1154 }
1155 "#
1156 );
1157 },
1158 );
1159 }
1160
1161 // Variables, method invocations, and true or false values
1162 // in lambda expression return values are not counted as conditions
1163 #[cfg(feature = "java")]
1164 #[test]
1165 fn java_lambda_expressions_return_with_conditions() {
1166 check_metrics::<JavaParser>(
1167 "
1168 Predicate<Boolean> p1 = a -> a; // +1a
1169 Predicate<Boolean> p2 = b -> true; // +1a
1170 Predicate<Boolean> p3 = c -> m(); // +1a
1171 Predicate<Integer> p4 = d -> d > 10; // +1a +1c
1172 Predicate<Boolean> p5 = (e) -> !e; // +1a +1c
1173 Predicate<Boolean> p6 = (f) -> !((!f)); // +1a +1c
1174 Predicate<Boolean> p7 = (g) -> !g && true; // +1a +2c
1175 BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
1176 BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
1177 return j || k; // +1a +2c
1178 };
1179 ",
1180 "foo.java",
1181 |metric| {
1182 // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
1183 // space count: 1 (1 unit)
1184 insta::assert_json_snapshot!(
1185 metric.abc,
1186 @r#"
1187 {
1188 "assignments": 9,
1189 "branches": 1,
1190 "conditions": 9,
1191 "magnitude": 12.767145334803704,
1192 "value": 12.767145334803704,
1193 "assignments_average": 9.0,
1194 "branches_average": 1.0,
1195 "conditions_average": 9.0,
1196 "assignments_min": 9,
1197 "assignments_max": 9,
1198 "branches_min": 1,
1199 "branches_max": 1,
1200 "conditions_min": 9,
1201 "conditions_max": 9
1202 }
1203 "#
1204 );
1205 },
1206 );
1207 }
1208
1209 #[cfg(feature = "java")]
1210 #[test]
1211 fn java_for_with_variable_declaration() {
1212 check_metrics::<JavaParser>(
1213 "
1214 for ( int i1 = 0; !(!(!(!a))); i1++ ) {} // +2a +1c
1215 for ( int i2 = 0; !B.m(); i2++ ) {} // +2a +1b +1c
1216 for ( int i3 = 0; a || false; i3++ ) {} // +2a +2c
1217 for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {} // +2a +1b +3c
1218 for ( int i5 = 0; true; i5++ ) {} // +2a +1c
1219 ",
1220 "foo.java",
1221 |metric| {
1222 // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1223 // space count: 1 (1 unit)
1224 insta::assert_json_snapshot!(
1225 metric.abc,
1226 @r#"
1227 {
1228 "assignments": 10,
1229 "branches": 2,
1230 "conditions": 8,
1231 "magnitude": 12.96148139681572,
1232 "value": 12.96148139681572,
1233 "assignments_average": 10.0,
1234 "branches_average": 2.0,
1235 "conditions_average": 8.0,
1236 "assignments_min": 10,
1237 "assignments_max": 10,
1238 "branches_min": 2,
1239 "branches_max": 2,
1240 "conditions_min": 8,
1241 "conditions_max": 8
1242 }
1243 "#
1244 );
1245 },
1246 );
1247 }
1248
1249 #[cfg(feature = "java")]
1250 #[test]
1251 fn java_for_without_variable_declaration() {
1252 check_metrics::<JavaParser>(
1253 "class A{
1254 void m1() {
1255 for (i = 0; x < y; i++) {} // +2a +1c
1256 for (i = 0; ((x < y)); i++) {} // +2a +1c
1257 for (i = 0; !(!(x < y)); i++) {} // +2a +1c
1258 for (i = 0; true; i++) {} // +2a +1c
1259 }
1260 void m2() {
1261 for ( ; true; ) {} // +1c
1262 }
1263 void m3() {
1264 for ( ; ; ) {} // +0c — no condition to count (#1276)
1265 }
1266 }",
1267 "foo.java",
1268 |metric| {
1269 // magnitude: sqrt(64 + 0 + 25) = sqrt(89)
1270 // space count: 5 (1 unit, 1 class and 3 methods)
1271 insta::assert_json_snapshot!(
1272 metric.abc,
1273 @r#"
1274 {
1275 "assignments": 8,
1276 "branches": 0,
1277 "conditions": 5,
1278 "magnitude": 9.433981132056603,
1279 "value": 0.0,
1280 "assignments_average": 1.6,
1281 "branches_average": 0.0,
1282 "conditions_average": 1.0,
1283 "assignments_min": 0,
1284 "assignments_max": 8,
1285 "branches_min": 0,
1286 "branches_max": 0,
1287 "conditions_min": 0,
1288 "conditions_max": 4
1289 }
1290 "#
1291 );
1292 },
1293 );
1294 }
1295
1296 // Issue #1276 changed Java's answer here. `java_walk_for_statement`
1297 // used to read child(3), fall through to child(4) when that was the
1298 // `;` an expression initializer leaves behind, and count a `;` or
1299 // `)` landing there as a vacuously-true condition — so `for (;;)`
1300 // scored one. Nothing in Fitzpatrick's C dimension attributes a
1301 // condition to a `for` keyword; it counts conditional operators and
1302 // unary conditions that are present. Java and Groovy were the only
1303 // two impls disagreeing, and the field-addressed walker now reports
1304 // zero the way the C family, the JS family, PHP, C# and Go all do.
1305 #[cfg(feature = "java")]
1306 #[test]
1307 fn java_empty_for_condition_counts_nothing() {
1308 check_metrics::<JavaParser>(
1309 "class A { void m() { for (;;) { break; } } }",
1310 "foo.java",
1311 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1312 );
1313 // The other empty spelling, and the one the old cascade
1314 // actually mis-scored: with an **expression** initializer the
1315 // children are `for ( init ; ; update )`, so child(3) was the
1316 // separating `;` and child(4) the empty condition's `;`, which
1317 // the vacuous-true arm counted. The `for (int i = 0; ; i++)`
1318 // spelling scored 0 both before and after — Java's
1319 // `local_variable_declaration` swallows its own `;`, putting the
1320 // update expression at child(4), where it matched no arm — so it
1321 // would be a vacuous regression test and is deliberately not
1322 // used here.
1323 check_metrics::<JavaParser>(
1324 "class A { void m() { int i; for (i = 0; ; i++) { break; } } }",
1325 "foo.java",
1326 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1327 );
1328 }
1329
1330 // The second defect the positional cascade carried: tree-sitter
1331 // counts comments among a node's children, so a comment anywhere in
1332 // the header shifted every index and the condition went unread —
1333 // the same failure #1181 removed from `java_walk_ternary`. Reading
1334 // the `condition` field cannot shift.
1335 #[cfg(feature = "java")]
1336 #[test]
1337 fn java_for_condition_survives_a_header_comment() {
1338 check_metrics::<JavaParser>(
1339 "class A { void m(boolean a) { for (; /* n */ a; ) { break; } } }",
1340 "foo.java",
1341 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1342 );
1343 // Unchanged control: the same loop without the comment. Both
1344 // spellings must agree, which is the property the positional
1345 // form broke.
1346 check_metrics::<JavaParser>(
1347 "class A { void m(boolean a) { for (; a; ) { break; } } }",
1348 "foo.java",
1349 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1350 );
1351 }
1352
1353 // Variables, method invocations, and true or false values
1354 // in ternary expression return values are not counted as conditions
1355 #[cfg(feature = "java")]
1356 #[test]
1357 fn java_ternary_conditions() {
1358 check_metrics::<JavaParser>(
1359 "
1360 a = true; // +1a
1361 b = a ? true : false; // +1a +2c
1362 c = ((((a)))) ? !false : !b; // +1a +4c
1363 d = !this.m() ? !!a : (false); // +1a +1b +3c
1364 e = !(a) && b ? ((c)) : !d; // +1a +4c
1365 if ( this.m() ? a : !this.m() ) {} // +2b +3c
1366 if ( x > 0 ? !(false) : this.m() ) {} // +1b +3c
1367 if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {} // +5c
1368 ",
1369 "foo.java",
1370 |metric| {
1371 // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1372 // space count: 1 (1 unit)
1373 insta::assert_json_snapshot!(
1374 metric.abc,
1375 @r#"
1376 {
1377 "assignments": 5,
1378 "branches": 4,
1379 "conditions": 24,
1380 "magnitude": 24.839484696748443,
1381 "value": 24.839484696748443,
1382 "assignments_average": 5.0,
1383 "branches_average": 4.0,
1384 "conditions_average": 24.0,
1385 "assignments_min": 5,
1386 "assignments_max": 5,
1387 "branches_min": 4,
1388 "branches_max": 4,
1389 "conditions_min": 24,
1390 "conditions_max": 24
1391 }
1392 "#
1393 );
1394 },
1395 );
1396 }
1397
1398 #[cfg(feature = "bash")]
1399 #[test]
1400 fn bash_assignments_only() {
1401 check_metrics::<BashParser>(
1402 "f() {
1403 a=1
1404 b=2
1405 c+=3
1406 }",
1407 "foo.sh",
1408 |metric| {
1409 insta::assert_json_snapshot!(
1410 metric.abc,
1411 @r#"
1412 {
1413 "assignments": 3,
1414 "branches": 0,
1415 "conditions": 0,
1416 "magnitude": 3.0,
1417 "value": 0.0,
1418 "assignments_average": 1.5,
1419 "branches_average": 0.0,
1420 "conditions_average": 0.0,
1421 "assignments_min": 0,
1422 "assignments_max": 3,
1423 "branches_min": 0,
1424 "branches_max": 0,
1425 "conditions_min": 0,
1426 "conditions_max": 0
1427 }
1428 "#
1429 );
1430 },
1431 );
1432 }
1433
1434 #[cfg(feature = "bash")]
1435 #[test]
1436 fn bash_commands_only() {
1437 check_metrics::<BashParser>(
1438 "f() {
1439 echo a
1440 ls
1441 }",
1442 "foo.sh",
1443 |metric| {
1444 insta::assert_json_snapshot!(
1445 metric.abc,
1446 @r#"
1447 {
1448 "assignments": 0,
1449 "branches": 2,
1450 "conditions": 0,
1451 "magnitude": 2.0,
1452 "value": 0.0,
1453 "assignments_average": 0.0,
1454 "branches_average": 1.0,
1455 "conditions_average": 0.0,
1456 "assignments_min": 0,
1457 "assignments_max": 0,
1458 "branches_min": 0,
1459 "branches_max": 2,
1460 "conditions_min": 0,
1461 "conditions_max": 0
1462 }
1463 "#
1464 );
1465 },
1466 );
1467 }
1468
1469 #[cfg(feature = "bash")]
1470 #[test]
1471 fn bash_control_flow_counts_conditions() {
1472 // Regression for #696: Bash control-flow branches are ABC
1473 // conditions (a Bash predicate is a command, so the branch keyword
1474 // is the only condition signal). Each mirrors a cyclomatic decision.
1475 //
1476 // expected: 4 conditions — `if` (1) + `elif` (1) + `while` (1) +
1477 // the non-wildcard case arm `a)` (1). The bare-`*)` wildcard arm is
1478 // the Bash analogue of `default:` and is excluded, exactly as the
1479 // cyclomatic standard count excludes it. No comparison / test
1480 // operators appear, so every condition here is control-flow.
1481 check_metrics::<BashParser>(
1482 "f() {
1483 if cmd; then
1484 echo a
1485 elif other; then
1486 echo b
1487 fi
1488 while running; do
1489 echo c
1490 done
1491 case \"$x\" in
1492 a) echo d ;;
1493 *) echo e ;;
1494 esac
1495 }",
1496 "foo.sh",
1497 |metric| {
1498 assert_eq!(metric.abc.conditions_sum(), 4);
1499 },
1500 );
1501 }
1502
1503 #[cfg(feature = "bash")]
1504 #[test]
1505 fn bash_conditions_mix() {
1506 // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
1507 // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
1508 // inside `[ ]`. Each `if` body's `echo` contributes a branch.
1509 //
1510 // expected: 8 conditions — each of the four `if`s contributes one
1511 // for the control-flow branch (#696) plus one for its comparison /
1512 // test operator (`==`, `!=`, `<`, `-z`). 4 branches (one `echo`
1513 // each). magnitude = sqrt(4² + 8²) = sqrt(80).
1514 check_metrics::<BashParser>(
1515 "f() {
1516 if [[ \"$a\" == \"$b\" ]]; then
1517 echo eq
1518 fi
1519 if [[ \"$x\" != \"$y\" ]]; then
1520 echo ne
1521 fi
1522 if (( $a < $b )); then
1523 echo lt
1524 fi
1525 if [ -z \"$x\" ]; then
1526 echo empty
1527 fi
1528 }",
1529 "foo.sh",
1530 |metric| {
1531 assert_eq!(metric.abc.conditions_sum(), 8);
1532 assert_eq!(metric.abc.branches_sum(), 4);
1533 insta::assert_json_snapshot!(
1534 metric.abc,
1535 @r#"
1536 {
1537 "assignments": 0,
1538 "branches": 4,
1539 "conditions": 8,
1540 "magnitude": 8.94427190999916,
1541 "value": 0.0,
1542 "assignments_average": 0.0,
1543 "branches_average": 2.0,
1544 "conditions_average": 4.0,
1545 "assignments_min": 0,
1546 "assignments_max": 0,
1547 "branches_min": 0,
1548 "branches_max": 4,
1549 "conditions_min": 0,
1550 "conditions_max": 8
1551 }
1552 "#
1553 );
1554 },
1555 );
1556 }
1557
1558 #[cfg(feature = "bash")]
1559 #[test]
1560 fn bash_redirection_is_not_a_condition() {
1561 // `>` and `<` spell an I/O redirection as well as a comparison, and
1562 // the grammar parents the redirection under `file_redirect` rather
1563 // than `binary_expression`. Ungated, every redirect in a script
1564 // scored a condition: this fixture measured 2 before the parent
1565 // gate — the Bash instance of #1280's positive-parent polarity.
1566 // expected: 0 conditions — no test, no `if`, no comparison.
1567 check_metrics::<BashParser>(
1568 "f() {\n echo hi > out.txt\n read x < in.txt\n}\n",
1569 "foo.sh",
1570 |metric| {
1571 assert_eq!(metric.abc.conditions_sum(), 0);
1572 // The two `echo` / `read` commands still count as branches,
1573 // so the zero above is the gate firing rather than the walk
1574 // skipping the function body.
1575 assert_eq!(metric.abc.branches_sum(), 2);
1576 },
1577 );
1578 }
1579
1580 #[cfg(feature = "bash")]
1581 #[test]
1582 fn bash_comparison_inside_an_arithmetic_or_test_context_is_a_condition() {
1583 // The positive control for the gate above: the same tokens under a
1584 // `binary_expression` are real comparisons in both the `[[ … ]]`
1585 // test form and the `(( … ))` arithmetic form, and still count.
1586 // expected: 4 — one `if` control-flow condition and one `>` per
1587 // function.
1588 check_metrics::<BashParser>(
1589 "f() {\n if [[ $a > $b ]]; then :; fi\n}\ng() {\n if (( a > b )); then :; fi\n}\n",
1590 "foo.sh",
1591 |metric| {
1592 assert_eq!(metric.abc.conditions_sum(), 4);
1593 },
1594 );
1595 }
1596
1597 #[cfg(feature = "bash")]
1598 #[test]
1599 fn bash_arithmetic_ternary_is_a_condition() {
1600 // The ABC half of #1268. Cyclomatic and cognitive both count Bash's
1601 // only ternary form; ABC did not, so the identical construct scored
1602 // 1 here against the C family's 2.
1603 // expected: 2 — the `>` comparison and the ternary itself, matching
1604 // `int m = a > b ? a : b;` in C.
1605 check_metrics::<BashParser>(
1606 "f() {\n local m=$(( a > b ? a : b ))\n}\n",
1607 "foo.sh",
1608 |metric| {
1609 assert_eq!(metric.abc.conditions_sum(), 2);
1610 },
1611 );
1612 }
1613
1614 #[cfg(feature = "bash")]
1615 #[test]
1616 fn bash_magnitude() {
1617 // Combined assignments + branches + conditions. The single `if`
1618 // contributes two conditions (the control-flow branch, #696, plus
1619 // the `==` operator), so magnitude = sqrt(2² + 1² + 2²) = sqrt(9).
1620 check_metrics::<BashParser>(
1621 "f() {
1622 a=1
1623 b=2
1624 if [[ \"$a\" == \"$b\" ]]; then
1625 echo eq
1626 fi
1627 }",
1628 "foo.sh",
1629 |metric| {
1630 assert_eq!(metric.abc.conditions_sum(), 2);
1631 insta::assert_json_snapshot!(
1632 metric.abc,
1633 @r#"
1634 {
1635 "assignments": 2,
1636 "branches": 1,
1637 "conditions": 2,
1638 "magnitude": 3.0,
1639 "value": 0.0,
1640 "assignments_average": 1.0,
1641 "branches_average": 0.5,
1642 "conditions_average": 1.0,
1643 "assignments_min": 0,
1644 "assignments_max": 2,
1645 "branches_min": 0,
1646 "branches_max": 1,
1647 "conditions_min": 0,
1648 "conditions_max": 2
1649 }
1650 "#
1651 );
1652 },
1653 );
1654 }
1655
1656 #[cfg(feature = "java")]
1657 #[test]
1658 fn java_malformed_parenthesized_no_panic() {
1659 check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
1660 // tree-sitter emits ERROR nodes for this malformed source, so no
1661 // IfStatement, branch, or condition is recognised — all counts are 0.
1662 // Primary goal: the unwrap-free path does not panic.
1663 assert_eq!(metric.abc.assignments(), 0);
1664 assert_eq!(metric.abc.branches(), 0);
1665 assert_eq!(metric.abc.conditions(), 0);
1666 assert_eq!(metric.abc.magnitude(), 0.0);
1667 });
1668 }
1669
1670 #[cfg(feature = "java")]
1671 #[test]
1672 fn java_bool_returning_terminal_kinds_count() {
1673 // Companion to `csharp_bool_returning_terminal_kinds_count`
1674 // (issue #372 / lesson #19). Java's grammar wraps every
1675 // if/while/do condition in `parenthesized_expression`, so
1676 // the gap lived in `java_inspect_container`'s terminal-arm
1677 // recognizer: `FieldAccess` (`cfg.flag`), `CastExpression`
1678 // (`(boolean)v`), `ArrayAccess` (`flags[0]`), and
1679 // `InstanceofExpression` (`x instanceof Foo`) were never
1680 // counted. Java has no `await` or `is_pattern` analogues,
1681 // so the C# fix's five-kind set collapses to four here.
1682 //
1683 // `instanceof` has since moved out of the terminal set to an
1684 // unconditional arm (#1461), so it no longer depends on the
1685 // slot to be counted. The total is unchanged, which is the
1686 // point of that change rather than an accident of it.
1687 //
1688 // expected: 4 conditions (one per `if`), 0 assignments,
1689 // 0 branches (no invocations).
1690 check_metrics::<JavaParser>(
1691 "class Cfg { boolean flag; }
1692 class A {
1693 void m(Object v, boolean[] flags, Cfg cfg) {
1694 if (cfg.flag) { }
1695 if ((boolean) v) { }
1696 if (v instanceof Cfg) { }
1697 if (flags[0]) { }
1698 }
1699 }",
1700 "foo.java",
1701 |metric| {
1702 assert_eq!(metric.abc.conditions_sum(), 4);
1703 assert_eq!(metric.abc.assignments_sum(), 0);
1704 assert_eq!(metric.abc.branches_sum(), 0);
1705 },
1706 );
1707 }
1708
1709 // Issue #1274: a generic *declaration* is type syntax, not a
1710 // decision. `java_count_token_condition` denied only
1711 // `type_arguments`, so `class Gen<T>` and `<U> U ident(U x)` — both
1712 // `type_parameters` — each scored two conditions. The fix counts
1713 // `<` / `>` only under `binary_expression`, the polarity C / C++ /
1714 // Rust / Go already use; a `grammar.json` sweep proves the token is
1715 // emitted from exactly three productions, so the two shapes agree on
1716 // every well-formed input.
1717 //
1718 // One fixture per production the arm must ignore: the class-level
1719 // `type_parameters` (`Gen<T …>`), its `extends` bound's nested
1720 // `type_arguments` (`Comparable<T>`), the method-level
1721 // `type_parameters` (`<U>`), and a nested generic
1722 // (`Map<String, List<T>>`) whose two closing `>` lex as separate
1723 // tokens under separate `type_arguments`. Pre-fix this file scored
1724 // 4 conditions — two per `type_parameters` bracket pair; expected 0,
1725 // the file contains no conditional construct at all.
1726 #[cfg(feature = "java")]
1727 #[test]
1728 fn java_generic_declarations_are_not_conditions() {
1729 check_metrics::<JavaParser>(
1730 "class Gen<T extends Comparable<T>> {
1731 <U> U ident(U x) { return x; }
1732 List<T> pick(Map<String, List<T>> m) { return m.get(\"k\"); }
1733 }",
1734 "foo.java",
1735 |metric| {
1736 assert_eq!(metric.abc.conditions_sum(), 0);
1737 // Non-vacuity guard: 0 is also what an unparsed file
1738 // scores, so pin a value only a walked body can produce
1739 // — the `m.get("k")` invocation.
1740 assert_eq!(metric.abc.branches_sum(), 1);
1741 },
1742 );
1743 }
1744
1745 // Second half of #1274, same premise and same match: a wildcard type
1746 // argument's `?` is type syntax, not a ternary. tree-sitter-java emits
1747 // a bare `?` from exactly two productions — `ternary_expression` and
1748 // `wildcard` — so the `QMARK` arm carries the same allowlist gate the
1749 // `<` / `>` arm does.
1750 //
1751 // The body carries a real ternary over a real comparison so the
1752 // expected total is 2, not 0. Asserting 0 on a wildcard-only fixture
1753 // would have been vacuous — an unparsable file scores 0 too, so the
1754 // test could not tell "the wildcard was ignored" from "the fixture
1755 // stopped being recognised". Each failure mode now lands on its own
1756 // number: 4 if the wildcard `?` counts again (the pre-fix value), 3
1757 // if the gate is aimed at `Wildcard` instead, 1 if it swallows the
1758 // genuine ternary, 0 if parsing breaks. Two wildcards against one
1759 // ternary is deliberate — with one of each, aiming the gate at the
1760 // wrong one of the two productions still totals 2 and the test
1761 // cannot see the difference.
1762 #[cfg(feature = "java")]
1763 #[test]
1764 fn java_generic_wildcard_is_not_a_condition() {
1765 check_metrics::<JavaParser>(
1766 "class A {
1767 int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
1768 int a, int b) { return a < b ? 1 : 2; }
1769 }",
1770 "foo.java",
1771 |metric| {
1772 assert_eq!(metric.abc.conditions_sum(), 2);
1773 },
1774 );
1775 }
1776
1777 // The other half of #1274: narrowing the `<` / `>` arm must not
1778 // swallow real comparisons. `List<String>` in the signature keeps a
1779 // generic in scope so the fixture proves both directions at once,
1780 // and the assertion is the grammar-dispatch §8 pin — on the
1781 // method's own space the ABC condition count must equal the
1782 // cyclomatic decision count (`cyclomatic()` minus the per-space
1783 // base of 1). Both are 2, one per `if`.
1784 #[cfg(feature = "java")]
1785 #[test]
1786 fn java_comparison_operators_still_count_alongside_generics() {
1787 check_func_space::<JavaParser, _>(
1788 "class A {
1789 int m(List<String> xs, int a, int b) {
1790 if (a < b) { return 1; }
1791 if (a > b) { return 2; }
1792 return 0;
1793 }
1794 }",
1795 "foo.java",
1796 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
1797 );
1798 }
1799
1800 #[cfg(feature = "java")]
1801 #[test]
1802 fn java_constructor_delegation_is_a_branch() {
1803 // Regression for #1279: `super(…)` / `this(…)` parse as
1804 // `explicit_constructor_invocation`, not `method_invocation`, so
1805 // each scored zero branches while Groovy scored one for identical
1806 // source. Both constructors delegate, and neither body contains any
1807 // other call.
1808 // expected: 2 branches — one delegation each.
1809 check_metrics::<JavaParser>(
1810 "class Sub extends Base {
1811 Sub() { super(1); }
1812 Sub(int a) { this(); }
1813 }",
1814 "foo.java",
1815 |metric| {
1816 assert_eq!(metric.abc.branches_sum(), 2);
1817 },
1818 );
1819 }
1820
1821 #[cfg(feature = "java")]
1822 #[test]
1823 fn java_constructor_delegation_does_not_double_count_arguments() {
1824 // The delegation node does not wrap a `method_invocation` for the
1825 // call itself, so `super(f())` is exactly two branches — the
1826 // delegation and the argument call — not three (#1279).
1827 // expected: 2 branches.
1828 check_metrics::<JavaParser>(
1829 "class Sub extends Base {
1830 Sub() { super(f()); }
1831 }",
1832 "foo.java",
1833 |metric| {
1834 assert_eq!(metric.abc.branches_sum(), 2);
1835 },
1836 );
1837 }
1838
1839 #[cfg(feature = "csharp")]
1840 #[test]
1841 fn csharp_constructor_initializer_is_a_branch() {
1842 // C# spells the same delegation as a `constructor_initializer`
1843 // (`: base(…)` / `: this(…)`), which likewise scored zero (#1279).
1844 // expected: 2 branches — one initializer each, no other calls.
1845 check_metrics::<CsharpParser>(
1846 "class Sub : Base {
1847 Sub() : base(1) { }
1848 Sub(int a) : this() { }
1849 }",
1850 "foo.cs",
1851 |metric| {
1852 assert_eq!(metric.abc.branches_sum(), 2);
1853 },
1854 );
1855 }
1856
1857 #[cfg(feature = "csharp")]
1858 #[test]
1859 fn csharp_primary_constructor_base_call_is_a_branch() {
1860 // The C# 12 primary-constructor superclass call invokes the base
1861 // constructor exactly as the `: base(…)` above it does, and scored
1862 // zero until #1406 — the C# sibling of Kotlin's #1384.
1863 //
1864 // tree-sitter-c-sharp 0.23.5 spells the `record` and `class`
1865 // families differently, so per `.claude/rules/grammar-dispatch.md`
1866 // §11 they are two independent paths and each needs its own
1867 // fixture: the record form nests the arguments under a
1868 // `primary_constructor_base_type`, the class form hangs them
1869 // straight off the `base_list`. A record-only fixture says nothing
1870 // about the class arm, and vice versa.
1871 //
1872 // `S` and `R3` are the negatives: a base type with no argument
1873 // list is not a call, and neither spelling emits an
1874 // `argument_list` for it.
1875 let src = "class Sub(int x) : Base(x) { }
1876 record R1(int x) : Base(x);
1877 record class R2(int x) : Base(x);
1878 class G<T>(int x) : GBase<T>(x) where T : class { }
1879 class Multi(int x) : Base(x), IBase { }
1880 struct S(int x) : IBase { }
1881 record R3(int x) : Base;";
1882 // Anchors every spelling against a fixture edit: with presence
1883 // checks only, deleting `(x)` from the class form leaves the
1884 // record form satisfying the arm and this test green. The last
1885 // two counts anchor the declarations that would otherwise decay
1886 // into duplicates of `Sub` — `Multi` owns the fixture's only
1887 // comma (`Base(x), IBase`) and `G` its only type argument list
1888 // (`GBase<T>`), so trimming either back fails here by name.
1889 assert_csharp_fixture_spells(
1890 src,
1891 &[
1892 (
1893 Csharp::PrimaryConstructorBaseType as u16,
1894 2,
1895 "`record` base calls",
1896 ),
1897 (Csharp::ArgumentList as u16, 5, "base-call argument lists"),
1898 (Csharp::BaseList2 as u16, 7, "base lists"),
1899 // The §2 drift marker for the arm's other half. Both ids
1900 // render to `"base_list"`; at this pin every declaration
1901 // family above emits 252 and none emits 246, so
1902 // `BaseList` is defensive and this zero says so. A
1903 // grammar bump that starts emitting it fails here rather
1904 // than silently changing which arm does the work.
1905 (Csharp::BaseList as u16, 0, "unaliased `base_list` nodes"),
1906 (Csharp::COMMA as u16, 1, "`Multi`'s trailing interface"),
1907 (Csharp::TypeArgumentList as u16, 1, "`G`'s generic base"),
1908 ],
1909 );
1910 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
1911 for name in ["Sub", "R1", "R2", "G", "Multi"] {
1912 assert_eq!(
1913 child_space(&space, name).metrics.abc.branches(),
1914 1,
1915 "{name}: the primary-constructor base call is one branch"
1916 );
1917 }
1918 for name in ["S", "R3"] {
1919 assert_eq!(
1920 child_space(&space, name).metrics.abc.branches(),
1921 0,
1922 "{name}: a base type with no argument list is not a call"
1923 );
1924 }
1925 });
1926 }
1927
1928 #[cfg(feature = "csharp")]
1929 #[test]
1930 fn csharp_base_list_gate_excludes_other_argument_lists() {
1931 // The `base_list` parent gate on the `ArgumentList` arm is
1932 // load-bearing, not decoration: ungated it bills a branch for the
1933 // argument list of every call and allocation in the file, on top
1934 // of the `InvocationExpression` / `ObjectCreationExpression` the
1935 // arm already counts — doubling both (#1406).
1936 //
1937 // The fixture pairs one real base call with the four other
1938 // argument-carrying productions C# spells nearby: an attribute
1939 // (`attribute_argument_list`), an invocation, an object creation,
1940 // and an indexer (`bracketed_argument_list`) alongside a generic
1941 // (`type_argument_list`). Those last three are distinct kind ids
1942 // rather than `argument_list`, so they pin the §1 neighbour set
1943 // as much as the gate.
1944 let src = "[System.Obsolete(\"why\")]
1945 class A(int x) : Base(x) {
1946 void M(int y) { Helper(y); }
1947 object N() { return new Foo(1); }
1948 int P(System.Collections.Generic.List<int> l, int i) { return l[i]; }
1949 }";
1950 assert_csharp_fixture_spells(
1951 src,
1952 &[
1953 (
1954 Csharp::AttributeArgumentList as u16,
1955 1,
1956 "attribute argument lists",
1957 ),
1958 (
1959 Csharp::BracketedArgumentList as u16,
1960 1,
1961 "bracketed argument lists",
1962 ),
1963 (Csharp::TypeArgumentList as u16, 1, "type argument lists"),
1964 (Csharp::ArgumentList as u16, 3, "argument lists"),
1965 ],
1966 );
1967 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
1968 let class = child_space(&space, "A");
1969 assert_eq!(
1970 class.metrics.abc.branches(),
1971 1,
1972 "`: Base(x)` alone — the attribute's arguments are not a call"
1973 );
1974 // The controls. 2 apiece would mean the gate let the
1975 // argument list through beside the call that owns it.
1976 assert_eq!(
1977 child_space(class, "M").metrics.abc.branches(),
1978 1,
1979 "`Helper(y)` is one branch, not one per argument list"
1980 );
1981 assert_eq!(
1982 child_space(class, "N").metrics.abc.branches(),
1983 1,
1984 "`new Foo(1)` is one branch, not one per argument list"
1985 );
1986 // `P` carries the indexer and the generic deliberately, but
1987 // gets no assertion: `bracketed_argument_list` and
1988 // `type_argument_list` are distinct kind ids, so `P` reads 0
1989 // with the gate, without it, and without this change
1990 // altogether. The census above is what actually pins them —
1991 // an assertion here would read as coverage it cannot provide.
1992 });
1993 }
1994
1995 #[cfg(feature = "csharp")]
1996 #[test]
1997 fn csharp_base_call_and_constructor_initializer_do_not_double_count() {
1998 // A class can spell *both* delegations at once: the primary
1999 // constructor's `: Base(x)` and a secondary constructor's
2000 // `: this(a)`. They are separate nodes in separate spaces, so the
2001 // file scores 2 — one each. 3 would mean the #1406 arm also fired
2002 // on something `ConstructorInitializer` already owned; 1 would
2003 // mean one of the two stopped counting.
2004 //
2005 // Java has the equivalent guard
2006 // (`java_constructor_delegation_does_not_double_count_arguments`);
2007 // C# now has two delegation spellings and this is the pairing
2008 // test for them.
2009 let src = "class Both(int x) : Base(x) {
2010 public Both(int a, int b) : this(a) { }
2011 }";
2012 assert_csharp_fixture_spells(
2013 src,
2014 &[
2015 (Csharp::ConstructorInitializer as u16, 1, "`: this(a)`"),
2016 (Csharp::BaseList2 as u16, 1, "the primary-constructor base"),
2017 ],
2018 );
2019 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2020 let class = child_space(&space, "Both");
2021 assert_eq!(
2022 class.metrics.abc.branches(),
2023 1,
2024 "the class space owns `: Base(x)` and nothing else"
2025 );
2026 assert_eq!(
2027 child_space(class, "Both").metrics.abc.branches(),
2028 1,
2029 "the secondary constructor owns `: this(a)` and nothing else"
2030 );
2031 });
2032 }
2033
2034 #[cfg(feature = "kotlin")]
2035 #[test]
2036 fn kotlin_constructor_delegation_is_a_branch() {
2037 // Kotlin's secondary-constructor delegation is a
2038 // `constructor_delegation_call`, a distinct production from
2039 // `CallExpression`, so it scored zero for the same reason (#1279).
2040 // expected: 1 branch — the `: super(x)` delegation.
2041 check_metrics::<KotlinParser>(
2042 "class Sub : Base {
2043 constructor(x: Int) : super(x) { }
2044 }",
2045 "foo.kt",
2046 |metric| {
2047 assert_eq!(metric.abc.branches_sum(), 1);
2048 },
2049 );
2050 }
2051
2052 #[cfg(feature = "kotlin")]
2053 #[test]
2054 fn kotlin_primary_constructor_superclass_call_is_a_branch() {
2055 // Kotlin's *primary*-constructor superclass call is a
2056 // `constructor_invocation` under a `delegation_specifier` — a third
2057 // production, distinct from both `CallExpression` and the
2058 // `ConstructorDelegationCall` #1279 added, so it scored zero while
2059 // the secondary form beside it scored one (#1384). An object
2060 // expression's superclass call uses the same production.
2061 //
2062 // `class Plain : Marker` is the negative case: a delegation
2063 // specifier with no argument list is a plain `user_type`, so it
2064 // must stay at zero. Broadening the arm to bare
2065 // `DelegationSpecifier` fails here at `branches_sum` (5 against
2066 // 3), though not on `Plain`'s account alone: `class Classic :
2067 // Base` below is argument-less too, so deleting `Plain` still
2068 // leaves the broadening failing, at 4. What `Plain` buys is the
2069 // case stated outright rather than riding on the contrast fixture,
2070 // and since the per-space assertions below name it, trimming it
2071 // from the fixture fails in `child_space` instead of passing
2072 // silently as it did while this test read only the sum.
2073 //
2074 // `class Classic` carries the secondary spelling of the same call
2075 // alongside, which is what makes the per-space assertions below a
2076 // contrast rather than a census: a class header's superclass call
2077 // sits in `delegation_specifiers`, outside every member, so no
2078 // function space encloses it and the branch is the **class**
2079 // space's, while `constructor(x: Int) : super(x)` opens a function
2080 // space and takes the branch with it. `branches_sum` cannot see the
2081 // difference — the file totals 3 either way — so a codebase moving
2082 // to primary constructors shifts per-function `branches_max` /
2083 // `branches_average` and per-space `abc` thresholds without any
2084 // behaviour change. That is deliberate (there is no function to
2085 // attribute the call to), and C# has the identical split, pinned by
2086 // `csharp_base_call_and_constructor_initializer_do_not_double_count`
2087 // (#1456).
2088 //
2089 // expected: 3 branches — `Base(1, 2)`, `Base(3)` and `super(x)`.
2090 // Nothing else in the fixture is a call, so deleting any one
2091 // construction from the fixture moves both the total and the space
2092 // it belongs to.
2093 check_func_space::<KotlinParser, _>(
2094 "class Sub : Base(1, 2) { }
2095 class Plain : Marker { }
2096 fun make(): Any = object : Base(3) { }
2097 class Classic : Base {
2098 constructor(x: Int) : super(x) { }
2099 }",
2100 "foo.kt",
2101 |space| {
2102 assert_eq!(space.metrics.abc.branches_sum(), 3);
2103 assert_eq!(
2104 child_space(&space, "Sub").metrics.abc.branches(),
2105 1,
2106 "the primary-constructor superclass call is the class space's"
2107 );
2108 assert_eq!(
2109 child_space(&space, "Plain").metrics.abc.branches(),
2110 0,
2111 "a delegation specifier with no argument list is not a call"
2112 );
2113 // The object expression is its own Class space (#463), so
2114 // `make` holds nothing of its own — the same attribution
2115 // one level down.
2116 let make = child_space(&space, "make");
2117 assert_eq!(
2118 make.metrics.abc.branches(),
2119 0,
2120 "`make` owns no branch; `object : Base(3)` is its own space"
2121 );
2122 assert_eq!(
2123 child_space(make, "<anonymous>").metrics.abc.branches(),
2124 1,
2125 "the object expression's superclass call is that space's"
2126 );
2127 // The contrast. `Classic` spells the same call through a
2128 // secondary constructor, which *does* open a function
2129 // space, so the class owns nothing.
2130 let classic = child_space(&space, "Classic");
2131 assert_eq!(
2132 classic.metrics.abc.branches(),
2133 0,
2134 "a secondary constructor's `: super(x)` is not the class's"
2135 );
2136 assert_eq!(
2137 child_space(classic, "<anonymous>").metrics.abc.branches(),
2138 1,
2139 "it belongs to the constructor's own function space"
2140 );
2141 },
2142 );
2143 }
2144
2145 #[cfg(feature = "kotlin")]
2146 #[test]
2147 fn kotlin_annotation_arguments_are_not_a_branch() {
2148 // The parent gate on that arm is load-bearing, not decoration:
2149 // tree-sitter-kotlin-ng spells an annotation's argument list with
2150 // the *same* `constructor_invocation` production — `@Mark("a")`
2151 // parses as `annotation > constructor_invocation` and
2152 // `@file:Suppress(…)` as `file_annotation > constructor_invocation`.
2153 // Ungated, every argument-carrying annotation in a Kotlin file
2154 // would bill a branch, and annotations are everywhere (#1384).
2155 //
2156 // The fixture pairs three annotation spellings — file-level,
2157 // class-level and use-site-targeted — with one real superclass
2158 // call, so the total discriminates in both directions: 1 with the
2159 // gate, 4 without it, 0 without the arm at all.
2160 //
2161 // A correctly-excluded annotation contributes to no ABC axis, so
2162 // `branches_sum()` alone cannot notice the annotations being
2163 // trimmed out of the fixture — measured: dropping any of the three
2164 // leaves this test green, and with all three gone, removing the
2165 // production gate fails nothing. The node census below is the
2166 // anchor: it pins that the fixture really does hand the arm four
2167 // `constructor_invocation` nodes for it to score 1 out of, and
2168 // that the `file_annotation` parent — the spelling a denylist of
2169 // `Annotation` alone would have missed — is among them.
2170 //
2171 // expected: 1 branch — `Base(1)` only.
2172 let src = r#"@file:Suppress("unused")
2173 @Mark("a")
2174 class Ann : Base(1) {
2175 @get:Mark("b")
2176 val v: Int = 0
2177 }"#;
2178 let parser = KotlinParser::new(
2179 src.as_bytes().to_vec(),
2180 &std::path::PathBuf::from("foo.kt"),
2181 None,
2182 );
2183 assert_eq!(
2184 parser
2185 .root()
2186 .preorder()
2187 .filter(|n| n.kind_id() == Kotlin::ConstructorInvocation as u16)
2188 .count(),
2189 4,
2190 "fixture must keep three annotations plus the superclass call"
2191 );
2192 assert!(ast_has_kind_id(&parser, Kotlin::FileAnnotation as u16));
2193
2194 check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
2195 assert_eq!(metric.abc.branches_sum(), 1);
2196 });
2197 }
2198
2199 #[cfg(feature = "groovy")]
2200 #[test]
2201 fn groovy_constructor_delegation_is_a_branch() {
2202 // Groovy already counted this shape before #1279; the assertion
2203 // pins the JVM-family parity the Java and Kotlin fixes restore.
2204 // expected: 1 branch — the `super(1)` delegation.
2205 check_metrics::<GroovyParser>(
2206 "class Sub extends Base {
2207 Sub() { super(1) }
2208 }",
2209 "foo.groovy",
2210 |metric| {
2211 assert_eq!(metric.abc.branches_sum(), 1);
2212 },
2213 );
2214 }
2215
2216 // The three tests below are one fix seen from three grammars (#1407):
2217 // an enum constant carrying constructor arguments is an object
2218 // construction and scored zero in every JVM-family language, which
2219 // #1279 and #1384 left behind because both looked at superclass
2220 // delegation only.
2221 //
2222 // Each fixture pairs an argument-carrying constant with a bare one in
2223 // the *same* enum, because that is the only shape that shows the gate
2224 // working: a fixture of only `A(1)` scores the same whether the arm is
2225 // gated on the argument list or matches every constant
2226 // (`.claude/rules/grammar-dispatch.md` §6). Each also carries a
2227 // constant whose argument is itself a call, which must score 2 rather
2228 // than 1 (§5).
2229 //
2230 // `branches_sum()` alone cannot notice the bare constant being trimmed
2231 // out — it contributes to no axis — so every fixture is anchored on a
2232 // node census that fails by name when a spelling is edited away
2233 // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
2234 // production line"). The two are coupled in one helper for the same
2235 // reason `assert_kotlin_class_members` couples its pair: a branch
2236 // total added without the census reads as coverage and is not.
2237
2238 #[cfg(any(feature = "groovy", feature = "java", feature = "kotlin"))]
2239 fn assert_enum_branches<P: MetricSuite>(
2240 src: &str,
2241 path: &str,
2242 kinds: &[(u16, usize, &str)],
2243 branches: u64,
2244 ) {
2245 assert_fixture_spells::<P>(src, path, kinds);
2246 check_func_space::<P, _>(src, path, |space| {
2247 assert_eq!(space.metrics.abc.branches_sum(), branches);
2248 });
2249 }
2250
2251 #[cfg(feature = "java")]
2252 #[test]
2253 fn java_enum_constant_with_arguments_is_a_branch() {
2254 // `@SuppressWarnings("x") B` is the discriminating case for the
2255 // gate's *kind*, not just its presence: the annotation carries an
2256 // argument list of its own, so an arm gated on "has some argument
2257 // list anywhere below" would score it. It cannot satisfy this one
2258 // twice over — `annotation_argument_list` is a distinct kind id
2259 // from `argument_list`, and it hangs off the constant's `modifiers`
2260 // child rather than the constant itself. Both verified with `bca
2261 // dump`.
2262 //
2263 // expected: 4 branches — `A(1)`, `K(2)`, `D(…)`, and the `f()`
2264 // inside `D`'s argument list. `B` scores 0.
2265 let src = "class Outer {
2266 static int f() { return 1; }
2267 enum E {
2268 A(1),
2269 @SuppressWarnings(\"x\") B,
2270 K(2),
2271 D(f());
2272 E(int v) {}
2273 }
2274 }";
2275 assert_enum_branches::<JavaParser>(
2276 src,
2277 "foo.java",
2278 &[
2279 (Java::EnumConstant as u16, 4, "enum constants"),
2280 (Java::ArgumentList as u16, 4, "argument lists"),
2281 (
2282 Java::AnnotationArgumentList as u16,
2283 1,
2284 "`@SuppressWarnings`'s argument list",
2285 ),
2286 (Java::MethodInvocation as u16, 1, "`D`'s nested `f()` call"),
2287 ],
2288 4,
2289 );
2290 }
2291
2292 #[cfg(feature = "kotlin")]
2293 #[test]
2294 fn kotlin_enum_entry_with_arguments_is_a_branch() {
2295 // Kotlin needs a defaulted primary-constructor parameter to spell
2296 // both cases in one enum: `enum class E(val v: Int)` obliges every
2297 // entry to pass an argument, so `= 0` is what lets the bare `B`
2298 // sit beside `A(1)`. `enum class P` covers the other shape the gate
2299 // has to leave alone — an enum with no constructor at all, whose
2300 // entries can never carry an argument list.
2301 //
2302 // expected: 3 branches — `A(1)`, `C(…)`, and the `g()` inside `C`'s
2303 // argument list. `B`, `X` and `Y` score 0.
2304 let src = "fun g(): Int = 1
2305 enum class E(val v: Int = 0) { A(1), B, C(g()) }
2306 enum class P { X, Y }";
2307 assert_enum_branches::<KotlinParser>(
2308 src,
2309 "foo.kt",
2310 &[
2311 (Kotlin::EnumEntry as u16, 5, "enum entries"),
2312 (Kotlin::ValueArguments as u16, 3, "argument lists"),
2313 (Kotlin::CallExpression as u16, 1, "`C`'s nested `g()` call"),
2314 ],
2315 3,
2316 );
2317 }
2318
2319 #[cfg(feature = "groovy")]
2320 #[test]
2321 fn groovy_enum_constant_with_arguments_is_a_branch() {
2322 // Groovy's enum has no annotated-constant case to cover: the
2323 // dekobon grammar cannot parse `@Deprecated A(1)` inside an enum
2324 // body and recovers into `ERROR` nodes, emitting no `enum_constant`
2325 // at all. The remaining two cases are the ones that matter.
2326 //
2327 // expected: 3 branches — `A(1)`, `D(…)`, and the `Outer.f()` inside
2328 // `D`'s argument list. `B` scores 0.
2329 let src = "class Outer { static int f() { 1 } }
2330 enum E { A(1), B, D(Outer.f()) }";
2331 assert_enum_branches::<GroovyParser>(
2332 src,
2333 "foo.groovy",
2334 &[
2335 (Groovy::EnumConstant as u16, 3, "enum constants"),
2336 (Groovy::ArgumentList as u16, 3, "argument lists"),
2337 (
2338 Groovy::MethodInvocation as u16,
2339 1,
2340 "`D`'s nested `Outer.f()` call",
2341 ),
2342 ],
2343 3,
2344 );
2345 }
2346
2347 #[cfg(feature = "groovy")]
2348 #[test]
2349 fn groovy_no_abc() {
2350 // Comment-only file has no executable code → all-zero ABC.
2351 check_metrics::<GroovyParser>(
2352 "// just a comment, no executable code",
2353 "foo.groovy",
2354 |metric| {
2355 assert_eq!(metric.abc.assignments_sum(), 0);
2356 assert_eq!(metric.abc.branches_sum(), 0);
2357 assert_eq!(metric.abc.conditions_sum(), 0);
2358 },
2359 );
2360 }
2361
2362 #[cfg(feature = "groovy")]
2363 #[test]
2364 fn groovy_single_assignment() {
2365 // `int x = 1` is a local-variable declaration whose `=` counts
2366 // as one assignment (matches Java's semantics).
2367 check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
2368 assert_eq!(metric.abc.assignments_sum(), 1);
2369 assert_eq!(metric.abc.branches_sum(), 0);
2370 assert_eq!(metric.abc.conditions_sum(), 0);
2371 });
2372 }
2373
2374 #[cfg(feature = "groovy")]
2375 #[test]
2376 fn groovy_assignments() {
2377 check_metrics::<GroovyParser>(
2378 "void f() {
2379 int a = 1
2380 int b = 2
2381 a = 3
2382 b = 4
2383 a += 1
2384 b -= 1
2385 }",
2386 "foo.groovy",
2387 |metric| {
2388 // Six `=` tokens total. The two `Final`-less local
2389 // var-decls (`int a = 1`, `int b = 2`) and the two
2390 // bare assignments (`a = 3`, `b = 4`) each contribute
2391 // one assignment via the `EQ` arm; the `+=` / `-=`
2392 // each contribute one via the compound-assign arm.
2393 assert_eq!(metric.abc.assignments_sum(), 6);
2394 },
2395 );
2396 }
2397
2398 #[cfg(feature = "groovy")]
2399 #[test]
2400 fn groovy_branches() {
2401 check_metrics::<GroovyParser>(
2402 "void f() {
2403 doStuff()
2404 helper.invoke()
2405 new Worker()
2406 }",
2407 "foo.groovy",
2408 |metric| {
2409 // 2 method invocations + 1 object creation = 3 branches
2410 assert_eq!(metric.abc.branches_sum(), 3);
2411 },
2412 );
2413 }
2414
2415 #[cfg(feature = "groovy")]
2416 #[test]
2417 fn groovy_conditions_in_if() {
2418 check_metrics::<GroovyParser>(
2419 "void f(int a) {
2420 if (a == 0) { println(a) }
2421 if (a >= 1) { println(a) }
2422 if (a != 2) { println(a) }
2423 }",
2424 "foo.groovy",
2425 |metric| {
2426 // Three relational ops = 3 conditions
2427 assert_eq!(metric.abc.conditions_sum(), 3);
2428 },
2429 );
2430 }
2431
2432 #[cfg(feature = "groovy")]
2433 #[test]
2434 fn groovy_branches_with_juxt_call() {
2435 // Groovy's parens-less call form `println foo` must be counted
2436 // as a branch (`JuxtFunctionCall`).
2437 check_metrics::<GroovyParser>(
2438 "void f() {
2439 println 'hi'
2440 println 'bye'
2441 }",
2442 "foo.groovy",
2443 |metric| {
2444 // 2 juxt calls = 2 branches.
2445 assert_eq!(metric.abc.branches_sum(), 2);
2446 },
2447 );
2448 }
2449
2450 #[cfg(feature = "groovy")]
2451 #[test]
2452 fn groovy_try_catch_conditions() {
2453 // Each `try` and `catch` keyword token contributes +1 to
2454 // conditions (mirrors Java).
2455 check_metrics::<GroovyParser>(
2456 "void f() {
2457 try {
2458 risky()
2459 } catch (Exception e) {
2460 handle(e)
2461 }
2462 }",
2463 "foo.groovy",
2464 |metric| {
2465 // try + catch = 2 conditions
2466 assert_eq!(metric.abc.conditions_sum(), 2);
2467 },
2468 );
2469 }
2470
2471 #[cfg(feature = "groovy")]
2472 #[test]
2473 fn groovy_ternary_conditions() {
2474 check_metrics::<GroovyParser>(
2475 "void f(int x) {
2476 def y = x > 0 ? 1 : 2
2477 }",
2478 "foo.groovy",
2479 |metric| {
2480 // QMARK alone is +1 condition, plus the `>` condition = 2.
2481 assert_eq!(metric.abc.conditions_sum(), 2);
2482 },
2483 );
2484 }
2485
2486 #[cfg(feature = "groovy")]
2487 #[test]
2488 fn groovy_constant_excluded_from_assignments() {
2489 // `final` declarations are not counted as assignments
2490 // (mirrors Java's `Final` handling).
2491 check_metrics::<GroovyParser>(
2492 "class A {
2493 final int CONST = 42
2494 int field = 0
2495 }",
2496 "foo.groovy",
2497 |metric| {
2498 // The `=` on `final int CONST = 42` is a constant
2499 // initialiser (skipped). Only `field = 0` counts.
2500 assert_eq!(metric.abc.assignments_sum(), 1);
2501 },
2502 );
2503 }
2504
2505 #[cfg(feature = "groovy")]
2506 #[test]
2507 fn groovy_malformed_parenthesized_no_panic() {
2508 // Regression: malformed Groovy input must not panic the ABC
2509 // walker; the `spaces.rs` Unit fallback (lesson 9) covers
2510 // structural recovery. amaanq's grammar treats `def x = (((`
2511 // as a `local_variable_declaration` whose initialiser is the
2512 // first opening paren — the `=` still fires the assignment
2513 // arm.
2514 check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
2515 assert_eq!(metric.abc.assignments_sum(), 1);
2516 });
2517 }
2518
2519 #[cfg(feature = "groovy")]
2520 #[test]
2521 fn groovy_bool_returning_terminal_kinds_count() {
2522 // Companion to `csharp_bool_returning_terminal_kinds_count`
2523 // (issue #372 / lesson #19). The dekobon Groovy grammar
2524 // shares Java's wrapping conventions for `FieldAccess` and
2525 // `InstanceofExpression`, but it splits casts into two
2526 // distinct kinds — `cast_expression` for the Groovy-idiomatic
2527 // `v as Boolean` and `parenthesized_type_cast` for the
2528 // Java-style `(boolean) v`. The grammar has no `await` or
2529 // `array_access` analogues, so the C# fix's five-kind set
2530 // collapses to four here (with the cast slot doubled).
2531 //
2532 // `instanceof` has since moved out of the terminal set to an
2533 // unconditional arm (#1461), so it no longer depends on the
2534 // slot to be counted. The total is unchanged, which is the
2535 // point of that change rather than an accident of it.
2536 //
2537 // expected: 4 conditions (one per `if`), 0 assignments,
2538 // 0 branches (no invocations).
2539 check_metrics::<GroovyParser>(
2540 "class Cfg { boolean flag }
2541 class A {
2542 void m(Object v, Cfg cfg) {
2543 if (cfg.flag) { }
2544 if ((boolean) v) { }
2545 if (v as Boolean) { }
2546 if (v instanceof Cfg) { }
2547 }
2548 }",
2549 "foo.groovy",
2550 |metric| {
2551 assert_eq!(metric.abc.conditions_sum(), 4);
2552 assert_eq!(metric.abc.assignments_sum(), 0);
2553 assert_eq!(metric.abc.branches_sum(), 0);
2554 },
2555 );
2556 }
2557
2558 // Groovy half of #1274 — see `java_generic_declarations_are_not_conditions`.
2559 // The class-level `type_parameters` and its bound's nested
2560 // `type_arguments` mirror the Java fixture; pre-fix this file scored
2561 // 2 conditions, expected 0.
2562 #[cfg(feature = "groovy")]
2563 #[test]
2564 fn groovy_generic_declarations_are_not_conditions() {
2565 check_metrics::<GroovyParser>(
2566 "class Gen<T extends Comparable<T>> {
2567 List<T> pick(Map<String, List<T>> m) { m.get(\"k\") }
2568 }",
2569 "foo.groovy",
2570 |metric| {
2571 assert_eq!(metric.abc.conditions_sum(), 0);
2572 // Non-vacuity guard — see the Java counterpart.
2573 assert_eq!(metric.abc.branches_sum(), 1);
2574 },
2575 );
2576 }
2577
2578 // Groovy counterpart of `java_generic_wildcard_is_not_a_condition`,
2579 // including its choice of a non-zero expected total and its
2580 // two-wildcards-one-ternary shape; the dekobon grammar emits the
2581 // same `wildcard` node. Pre-fix: 4.
2582 #[cfg(feature = "groovy")]
2583 #[test]
2584 fn groovy_generic_wildcard_is_not_a_condition() {
2585 check_metrics::<GroovyParser>(
2586 "class A {
2587 int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
2588 int a, int b) { a < b ? 1 : 2 }
2589 }",
2590 "foo.groovy",
2591 |metric| {
2592 assert_eq!(metric.abc.conditions_sum(), 2);
2593 },
2594 );
2595 }
2596
2597 // A generic *method* gets its own fixture because the dekobon
2598 // grammar gives it its own production: `def <U> U ident(U x)` emits
2599 // `method_type_parameters`, not the `type_parameters` the class form
2600 // above uses. It is the fourth and last production from which that
2601 // grammar emits a bare `<` / `>` (the others being
2602 // `binary_expression`, `type_arguments` and `type_parameters`), and
2603 // the one a denylist extended only with `TypeParameters` — the fix
2604 // this issue's own plan proposed — would still miss. Revert-verified
2605 // in both directions: under that denylist this test still fails
2606 // while `groovy_generic_declarations_are_not_conditions` passes.
2607 //
2608 // The body carries a ternary over a comparison for the same
2609 // non-vacuity reason as the wildcard tests: expected 2, pre-fix 4
2610 // (the `<U>` bracket pair), 0 if the fixture stops parsing.
2611 #[cfg(feature = "groovy")]
2612 #[test]
2613 fn groovy_method_type_parameters_are_not_conditions() {
2614 check_metrics::<GroovyParser>(
2615 "class A {
2616 def <U> U ident(U x, int a, int b) { a < b ? x : null }
2617 }",
2618 "foo.groovy",
2619 |metric| {
2620 assert_eq!(metric.abc.conditions_sum(), 2);
2621 },
2622 );
2623 }
2624
2625 // Groovy counterpart of
2626 // `java_comparison_operators_still_count_alongside_generics`: the
2627 // narrowed arm keeps counting real comparisons, pinned against the
2628 // cyclomatic decision count on the method's own space (§8).
2629 #[cfg(feature = "groovy")]
2630 #[test]
2631 fn groovy_elvis_counts_one_condition_per_token() {
2632 // `a ?: c` is a short-circuit decision Groovy cyclomatic already
2633 // counts and the Kotlin ABC arm counts for its identical token;
2634 // Groovy's arm listed neither `?:` nor its `elvis_expression`, so
2635 // a method whose only branching is elvis chains reported
2636 // cyclomatic > 1 with zero conditions. One per token, so the
2637 // grammar-dispatch §8 invariant holds on the chain:
2638 // `conditions() == cyclomatic() - 1`.
2639 check_func_space::<GroovyParser, _>(
2640 "class K { def f(a, c) { return a ?: c } }",
2641 "foo.groovy",
2642 |space| assert_deepest_conditions_match_cyclomatic(&space, 1),
2643 );
2644 check_func_space::<GroovyParser, _>(
2645 "class K { def g(a, b, c) { return a ?: b ?: c } }",
2646 "foo.groovy",
2647 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2648 );
2649 }
2650
2651 #[cfg(feature = "groovy")]
2652 #[test]
2653 fn groovy_comparison_operators_still_count_alongside_generics() {
2654 check_func_space::<GroovyParser, _>(
2655 "class A {
2656 int m(List<String> xs, int a, int b) {
2657 if (a < b) { return 1 }
2658 if (a > b) { return 2 }
2659 return 0
2660 }
2661 }",
2662 "foo.groovy",
2663 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2664 );
2665 }
2666
2667 #[cfg(feature = "groovy")]
2668 #[test]
2669 fn groovy_if_multiple_conditions() {
2670 // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
2671 // and parenthesised unary forms each contribute one
2672 // condition per primitive comparison; the inspect-container
2673 // pass picks up the unary `!a` / `!b` arguments inside the
2674 // `BinaryExpression` and counts them too.
2675 check_metrics::<GroovyParser>(
2676 "void f(boolean a, boolean b, boolean c) {
2677 if (a || b || c) { println(a) }
2678 if (a && b && c) { println(a) }
2679 if (!a && !b) { println(a) }
2680 }",
2681 "foo.groovy",
2682 |metric| {
2683 // Conditions counted via the AMPAMP/PIPEPIPE arms
2684 // (one count per identifier in the chain — three
2685 // for `||`, three for `&&`, two for the unary chain)
2686 // = 8.
2687 assert_eq!(metric.abc.conditions_sum(), 8);
2688 // Three `println a` juxt calls — each is a branch.
2689 assert_eq!(metric.abc.branches_sum(), 3);
2690 },
2691 );
2692 }
2693
2694 #[cfg(feature = "groovy")]
2695 #[test]
2696 fn groovy_while_and_do_while_conditions() {
2697 // Covers the WhileStatement and DoStatement arms in
2698 // `impl Abc for GroovyCode`. Each `while` / `do-while` has
2699 // its condition inspected through `groovy_inspect_container`.
2700 check_metrics::<GroovyParser>(
2701 "void f(boolean a, boolean b) {
2702 while (a) {
2703 a = false
2704 }
2705 do {
2706 b = !b
2707 } while (b)
2708 }",
2709 "foo.groovy",
2710 |metric| {
2711 // `while(a)` + `while(b)` each contribute one condition;
2712 // the unary `!b` on the do body's right-hand side adds
2713 // one more via the assignment-arm inspection = 3.
2714 assert_eq!(metric.abc.conditions_sum(), 3);
2715 // Two assignments to existing variables (`a = false`,
2716 // `b = !b`).
2717 assert_eq!(metric.abc.assignments_sum(), 2);
2718 },
2719 );
2720 }
2721
2722 #[cfg(feature = "groovy")]
2723 #[test]
2724 fn groovy_if_while_boolean_literal_condition() {
2725 // Regression for the Groovy half of #371-class bugs: the
2726 // dekobon tree-sitter-groovy grammar wraps a bare
2727 // `true` / `false` literal used as the condition of
2728 // `if` / `while` / `do` / `?:` in a `boolean_literal` node
2729 // (`Groovy::BooleanLiteral`, kind_id 270), not the leaf
2730 // `True` / `False` keyword tokens. `groovy_count_condition`
2731 // must therefore match `BooleanLiteral` (the wrapper).
2732 // Without that, every literal-condition statement silently
2733 // scored 0 conditions. Mirror of
2734 // `csharp_if_while_boolean_literal_condition`.
2735 check_metrics::<GroovyParser>(
2736 "void m() {
2737 if (true) { println 'a' }
2738 if (false) { println 'b' }
2739 while (true) { break }
2740 int t = true ? 1 : 0
2741 }",
2742 "foo.groovy",
2743 |metric| {
2744 // Four literal-condition statements contribute 4
2745 // `BooleanLiteral` conditions (if / if / while /
2746 // ternary), plus the ternary's `?` token adds one
2747 // more via `groovy_count_token_condition` → 5
2748 // total. The `println` calls contribute 2 branches
2749 // (the `while` body's `break` is not a branch).
2750 // The `int t = …` initializer contributes 1
2751 // assignment.
2752 assert_eq!(metric.abc.conditions_sum(), 5);
2753 assert_eq!(metric.abc.branches_sum(), 2);
2754 assert_eq!(metric.abc.assignments_sum(), 1);
2755 },
2756 );
2757 }
2758
2759 #[cfg(feature = "groovy")]
2760 #[test]
2761 fn groovy_return_unary_boolean_literal() {
2762 // Companion to `groovy_if_while_boolean_literal_condition`:
2763 // a `!true` / `!false` operand inside a `return` statement
2764 // routes through `groovy_inspect_container` (via
2765 // `groovy_inspect_child(node, 1)` on the ReturnStatement).
2766 // The `!` operator establishes boolean context, then the
2767 // innermost-operand check matches `BooleanLiteral` — that
2768 // helper's `BooleanLiteral` arm must be present or the
2769 // count silently drops. Mutation-verified: removing
2770 // `BooleanLiteral` from `groovy_inspect_container` leaves
2771 // every other Groovy test passing.
2772 check_metrics::<GroovyParser>(
2773 "boolean f() {
2774 return !true
2775 }
2776 boolean g() {
2777 return !false
2778 }",
2779 "foo.groovy",
2780 |metric| {
2781 // Each `return !X` walks into
2782 // `groovy_inspect_container` with a UnaryExpression
2783 // wrapping a `BANG` + BooleanLiteral. The `!` arm
2784 // seeds `has_boolean_content = true` (ReturnStatement
2785 // is not a known-boolean parent), then the
2786 // BooleanLiteral operand contributes one condition.
2787 // Two `return !X` → 2 conditions, no branches, no
2788 // assignments.
2789 assert_eq!(metric.abc.conditions_sum(), 2);
2790 assert_eq!(metric.abc.branches_sum(), 0);
2791 assert_eq!(metric.abc.assignments_sum(), 0);
2792 },
2793 );
2794 }
2795
2796 #[cfg(feature = "groovy")]
2797 #[test]
2798 fn groovy_short_circuit_with_boolean_literal_operand() {
2799 // Companion to `groovy_if_while_boolean_literal_condition`:
2800 // a bare `true` / `false` operand of `&&` / `||` lands in
2801 // `groovy_count_unary_conditions`, which iterates the
2802 // parent BinaryExpression's children. That helper must
2803 // match the `BooleanLiteral` wrapper just like
2804 // `groovy_count_condition` does — otherwise the operand
2805 // silently scores zero. Mutation-verified: removing
2806 // `BooleanLiteral` from the `groovy_count_unary_conditions`
2807 // arm leaves every other Groovy test passing.
2808 check_metrics::<GroovyParser>(
2809 "void m(boolean x) {
2810 if (x && true) { println 'a' }
2811 if (false || x) { println 'b' }
2812 }",
2813 "foo.groovy",
2814 |metric| {
2815 // `&&` and `||` themselves are NOT in
2816 // `groovy_count_token_condition`'s match list —
2817 // they route through
2818 // `groovy_walk_for_conditions::AMPAMP|PIPEPIPE`,
2819 // which calls `groovy_count_unary_conditions` on
2820 // the parent BinaryExpression. Each invocation
2821 // counts every child that matches the terminal-
2822 // operand kinds and whose parent is a
2823 // BinaryExpression. For `x && true`: Identifier x
2824 // (+1) + BooleanLiteral true (+1) = 2. For
2825 // `false || x`: BooleanLiteral false (+1) +
2826 // Identifier x (+1) = 2. Total 4.
2827 assert_eq!(metric.abc.conditions_sum(), 4);
2828 assert_eq!(metric.abc.branches_sum(), 2);
2829 assert_eq!(metric.abc.assignments_sum(), 0);
2830 },
2831 );
2832 }
2833
2834 #[cfg(feature = "groovy")]
2835 #[test]
2836 fn groovy_methods_arguments_with_conditions() {
2837 // Mirror of `java_methods_arguments_with_conditions`: a
2838 // unary `!x` inside an argument list must count both the
2839 // method invocation as a branch AND the unary as a
2840 // condition. The `ArgumentList | ArgumentList2` arm in
2841 // `impl Abc for GroovyCode` is what exercises this.
2842 check_metrics::<GroovyParser>(
2843 "void f(boolean a, boolean b, boolean c) {
2844 m1(a)
2845 m1(!a)
2846 m2(!a, !b)
2847 }",
2848 "foo.groovy",
2849 |metric| {
2850 // 3 method invocations (m1, m1, m2) — each fires the
2851 // branches arm.
2852 assert_eq!(metric.abc.branches_sum(), 3);
2853 // Three `!` unaries — `m1(!a)` and the two args of
2854 // `m2(!a, !b)` — each contribute one condition via
2855 // the ArgumentList inspection.
2856 assert_eq!(metric.abc.conditions_sum(), 3);
2857 },
2858 );
2859 }
2860
2861 #[cfg(feature = "groovy")]
2862 #[test]
2863 fn groovy_return_with_conditions() {
2864 // Mirror of `java_return_with_conditions`: a parenthesised
2865 // or unary expression inside `return` flows through the
2866 // `ReturnStatement` arm to `groovy_inspect_container`.
2867 check_metrics::<GroovyParser>(
2868 "boolean f(boolean a) {
2869 return (a)
2870 }
2871 boolean g(boolean a) {
2872 return !a
2873 }",
2874 "foo.groovy",
2875 |metric| {
2876 // Only one of the two return forms surfaces a
2877 // condition: `return !a` hits the UnaryExpression
2878 // path and adds one; `return (a)` reaches
2879 // `groovy_inspect_container` but the inner
2880 // identifier `a` is not in a boolean-context-firing
2881 // parent, so no condition is added.
2882 assert_eq!(metric.abc.conditions_sum(), 1);
2883 },
2884 );
2885 }
2886
2887 #[cfg(feature = "groovy")]
2888 #[test]
2889 fn groovy_for_with_variable_declaration() {
2890 // Classical `for (int i = 0; cond; i++)` form. The init
2891 // slot's `int i = 0` is suppressed from assignments by the
2892 // `LocalVariableDeclaration` push/pop dance; the `i++` in
2893 // the update slot contributes one assignment via the
2894 // `PLUSPLUS` arm. The condition `i < 10` flows through the
2895 // `ForStatement` arm.
2896 check_metrics::<GroovyParser>(
2897 "void f() {
2898 for (int i = 0; i < 10; i++) {
2899 println(i)
2900 }
2901 }",
2902 "foo.groovy",
2903 |metric| {
2904 // `int i = 0` fires the EQ arm + `i++` fires the
2905 // PLUSPLUS arm = 2 assignments.
2906 assert_eq!(metric.abc.assignments_sum(), 2);
2907 // `i < 10` is one condition (the LT arm).
2908 assert_eq!(metric.abc.conditions_sum(), 1);
2909 },
2910 );
2911 }
2912
2913 /// The existing `for` test uses `i < 10`, which the `LT` token arm
2914 /// counts on its own — `groovy_walk_for_statement` never
2915 /// contributes there, so it stayed uncovered. A bare-identifier
2916 /// condition has no comparison token, so the count can only come
2917 /// from the walker.
2918 #[cfg(feature = "groovy")]
2919 #[test]
2920 fn groovy_for_with_bare_identifier_condition() {
2921 check_metrics::<GroovyParser>(
2922 "void f(boolean go) {
2923 for (int i = 0; go; i++) {
2924 println(i)
2925 }
2926 }",
2927 "foo.groovy",
2928 |metric| {
2929 // `go` is the whole condition and counts once.
2930 assert_eq!(metric.abc.conditions_sum(), 1);
2931 // `int i = 0` (EQ) + `i++` (PLUSPLUS) = 2, as in
2932 // `groovy_for_with_variable_declaration`.
2933 assert_eq!(metric.abc.assignments_sum(), 2);
2934 },
2935 );
2936 }
2937
2938 /// The same slot with the initialiser hoisted out of the header.
2939 /// Under the pre-#1276 positional cascade this was a distinct code
2940 /// path — the condition moved from child(4) to child(3) — and the
2941 /// pair is kept as a shape guard now that the walker reads the
2942 /// `condition` field and cannot see the difference.
2943 #[cfg(feature = "groovy")]
2944 #[test]
2945 fn groovy_for_with_empty_initializer_counts_the_condition() {
2946 check_metrics::<GroovyParser>(
2947 "void f(boolean go) {
2948 int i = 0
2949 for (; go; i++) {
2950 println(i)
2951 }
2952 }",
2953 "foo.groovy",
2954 |metric| {
2955 assert_eq!(metric.abc.conditions_sum(), 1);
2956 // `int i = 0` (EQ) + `i++` (PLUSPLUS), as above — the
2957 // initialiser just moved out of the loop header.
2958 assert_eq!(metric.abc.assignments_sum(), 2);
2959 },
2960 );
2961 }
2962
2963 /// Issue #1276 changed Groovy's answer here, exactly as it changed
2964 /// Java's: the positional cascade counted a `;` / `)` landing at
2965 /// child(4) as a vacuously-true condition, so `for (;;)` scored
2966 /// one. An omitted test is not a decision, and every other impl
2967 /// scores it zero. See `java_empty_for_condition_counts_nothing`.
2968 #[cfg(feature = "groovy")]
2969 #[test]
2970 fn groovy_empty_for_condition_counts_nothing() {
2971 check_metrics::<GroovyParser>("void f() { for (;;) { break } }", "foo.groovy", |metric| {
2972 assert_eq!(metric.abc.conditions_sum(), 0);
2973 });
2974 // The second spelling that moved. Unlike Java's, Groovy's
2975 // `local_variable_declaration` does not swallow its `;`, so the
2976 // old cascade found `;` at child(3) and `;` at child(4) here
2977 // too, and counted one.
2978 check_metrics::<GroovyParser>(
2979 "void f() { for (int i = 0; ; i++) { break } }",
2980 "foo.groovy",
2981 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
2982 );
2983 }
2984
2985 /// The cascade's other defect, shared with Java: a comment in the
2986 /// header shifted every child index, so the condition went unread.
2987 /// Reading the `condition` field cannot shift.
2988 #[cfg(feature = "groovy")]
2989 #[test]
2990 fn groovy_for_condition_survives_a_header_comment() {
2991 check_metrics::<GroovyParser>(
2992 "void f(boolean go) { for (; /* n */ go; ) { break } }",
2993 "foo.groovy",
2994 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
2995 );
2996 // Unchanged control: the same loop without the comment.
2997 check_metrics::<GroovyParser>(
2998 "void f(boolean go) { for (; go; ) { break } }",
2999 "foo.groovy",
3000 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
3001 );
3002 }
3003
3004 /// C#'s `csharp_walk_for_statement` reads the loop condition off
3005 /// the named `condition` field and routes a parenthesised or
3006 /// `!`-prefixed one through `csharp_inspect_container`. Every other
3007 /// C# `for` test uses a comparison (`i < n`), which the `LT` token
3008 /// arm counts without entering the walker.
3009 #[cfg(feature = "csharp")]
3010 #[test]
3011 fn csharp_for_with_negated_condition() {
3012 check_metrics::<CsharpParser>(
3013 "class A {
3014 void M(bool done) {
3015 for (int i = 0; !done; i++) { System.Console.WriteLine(i); }
3016 }
3017 }",
3018 "foo.cs",
3019 |metric| {
3020 // `!done` unwraps to the `done` terminal: one condition,
3021 // and no comparison token to double-count it.
3022 assert_eq!(metric.abc.conditions_sum(), 1);
3023 // `int i = 0` + `i++`.
3024 assert_eq!(metric.abc.assignments_sum(), 2);
3025 assert_eq!(metric.abc.branches_sum(), 1);
3026 },
3027 );
3028 }
3029
3030 #[cfg(feature = "groovy")]
3031 #[test]
3032 fn groovy_eq_arm_counts_outside_final_declarations() {
3033 // Bare reassignment of an already-declared variable: the `=`
3034 // belongs to no `final` declaration, so it counts. Mirrors
3035 // `java_eq_arm_counts_outside_final_declarations`.
3036 check_metrics::<GroovyParser>(
3037 "void f(int x) {
3038 x = 42
3039 }",
3040 "foo.groovy",
3041 |metric| {
3042 assert_eq!(metric.abc.assignments_sum(), 1);
3043 assert_eq!(metric.abc.branches_sum(), 0);
3044 assert_eq!(metric.abc.conditions_sum(), 0);
3045 },
3046 );
3047 }
3048
3049 #[cfg(feature = "groovy")]
3050 #[test]
3051 fn groovy_final_field_initializer_does_not_suppress_the_closure_body() {
3052 // The Groovy spelling of
3053 // `java_final_initializer_does_not_suppress_the_assignments_inside_it`:
3054 // a `final` field's closure body opens no space, and the sentinel
3055 // stack suppressed its `x = 1` with the declarator's own `=`.
3056 check_metrics::<GroovyParser>(
3057 "class K {
3058 int x
3059 final Closure c = { x = 1 }
3060 Closure d = { x = 2 }
3061 final int q = 3
3062 }",
3063 "foo.groovy",
3064 |metric| {
3065 // `x = 1`, `d = {…}`, `x = 2`; the two `final`
3066 // initializers are suppressed. Pre-fix: 2.
3067 assert_eq!(metric.abc.assignments_sum(), 3);
3068 },
3069 );
3070 }
3071
3072 #[cfg(feature = "groovy")]
3073 #[test]
3074 fn groovy_final_local_is_an_error_at_the_pinned_grammar() {
3075 // `groovy_eq_initializes_final_binding` lists
3076 // `local_variable_declaration` for symmetry with Java, but at
3077 // dekobon-tree-sitter-groovy 0.2.2 a `final` local never
3078 // reaches it: the parser emits an `ERROR` node for the
3079 // declaration, with or without a terminator, so both of its `=`
3080 // tokens count. This pins the grammar limitation so a bump that
3081 // starts parsing the local shows up here rather than as a silent
3082 // change in what the predicate suppresses.
3083 let source = b"class K { int x; def m() { final int a = 0; x = 1 } }";
3084 let parser = GroovyParser::new(
3085 source.to_vec(),
3086 &std::path::PathBuf::from("foo.groovy"),
3087 None,
3088 );
3089 assert!(
3090 ast_has_kind_id(&parser, u16::MAX),
3091 "a `final` local should still parse to an ERROR node; if the grammar \
3092 now accepts it, re-derive the local half of the predicate",
3093 );
3094 check_metrics::<GroovyParser>(
3095 "class K { int x; def m() { final int a = 0; x = 1 } }",
3096 "foo.groovy",
3097 |metric| {
3098 assert_eq!(metric.abc.assignments_sum(), 2);
3099 },
3100 );
3101 }
3102
3103 #[cfg(feature = "csharp")]
3104 #[test]
3105 fn csharp_const_initializer_shapes() {
3106 // `csharp_eq_initializes_const_binding` reads the `const`
3107 // through the `modifier` node one hop above the
3108 // `variable_declaration`, for a local and a field alike, behind
3109 // other modifiers; `readonly` is not `const` and its initializer
3110 // counts. A lambda's body opens its own space in C#, so unlike
3111 // Java there was never a nested `=` to leak — the row pins that
3112 // the structural rule keeps the count the sentinel gave.
3113 check_metrics::<CsharpParser>(
3114 "class K {
3115 int x;
3116 private const int Q = 1;
3117 public static readonly int R = 2;
3118 void M() { const int q = 3; System.Action a = () => { x = 4; }; }
3119 }",
3120 "foo.cs",
3121 |metric| {
3122 // `R = 2`, `a = () => …`, `x = 4`.
3123 assert_eq!(metric.abc.assignments_sum(), 3);
3124 },
3125 );
3126 }
3127
3128 #[cfg(feature = "csharp")]
3129 #[test]
3130 fn csharp_constant_declarations() {
3131 check_metrics::<CsharpParser>(
3132 "class A {
3133 private const int X1 = 0, Y1 = 0;
3134 public const float PI = 3.14f;
3135 const string HELLO = \"Hello,\";
3136 protected string world = \" world!\";
3137 public float e = 2.718f;
3138 private int x2 = 1, y2 = 2;
3139 void M() {
3140 const int Z1 = 0, Z2 = 0, Z3 = 0;
3141 const float T = 0.0f;
3142 int z1 = 1, z2 = 2, z3 = 3;
3143 }
3144 }",
3145 "foo.cs",
3146 |metric| insta::assert_json_snapshot!(metric.abc),
3147 );
3148 }
3149
3150 #[cfg(feature = "csharp")]
3151 #[test]
3152 fn csharp_declarations_with_conditions() {
3153 check_metrics::<CsharpParser>(
3154 "class A {
3155 bool a = (1 == 2);
3156 bool b = (1 < 2);
3157 bool c = !true;
3158 bool d = !false;
3159 }",
3160 "foo.cs",
3161 |metric| insta::assert_json_snapshot!(metric.abc),
3162 );
3163 }
3164
3165 #[cfg(feature = "csharp")]
3166 #[test]
3167 fn csharp_assignments_with_conditions() {
3168 check_metrics::<CsharpParser>(
3169 "class A {
3170 void M() {
3171 int a = 0;
3172 a += 1;
3173 a -= 2;
3174 a *= 3;
3175 a /= 4;
3176 a %= 5;
3177 a++;
3178 a--;
3179 }
3180 }",
3181 "foo.cs",
3182 |metric| insta::assert_json_snapshot!(metric.abc),
3183 );
3184 }
3185
3186 #[cfg(feature = "csharp")]
3187 #[test]
3188 fn csharp_methods_arguments_with_conditions() {
3189 check_metrics::<CsharpParser>(
3190 "class A {
3191 void M(int x, int y) {
3192 F(x == y, x < y, !x.Equals(y));
3193 }
3194 void F(bool a, bool b, bool c) {}
3195 }",
3196 "foo.cs",
3197 |metric| insta::assert_json_snapshot!(metric.abc),
3198 );
3199 }
3200
3201 #[cfg(feature = "csharp")]
3202 #[test]
3203 fn csharp_if_single_conditions() {
3204 check_metrics::<CsharpParser>(
3205 "class A {
3206 void M(int x) {
3207 if (x > 0) { System.Console.WriteLine(\"a\"); }
3208 if (x < 0) { System.Console.WriteLine(\"b\"); }
3209 if (x == 0) { System.Console.WriteLine(\"c\"); }
3210 }
3211 }",
3212 "foo.cs",
3213 |metric| insta::assert_json_snapshot!(metric.abc),
3214 );
3215 }
3216
3217 #[cfg(feature = "csharp")]
3218 #[test]
3219 fn csharp_if_multiple_conditions() {
3220 check_metrics::<CsharpParser>(
3221 "class A {
3222 void M(int x, int y) {
3223 if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
3224 if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
3225 }
3226 }",
3227 "foo.cs",
3228 |metric| insta::assert_json_snapshot!(metric.abc),
3229 );
3230 }
3231
3232 #[cfg(feature = "csharp")]
3233 #[test]
3234 fn csharp_while_and_do_while_conditions() {
3235 check_metrics::<CsharpParser>(
3236 "class A {
3237 void M(int x) {
3238 while (x > 0) { x--; }
3239 do { x++; } while (x < 10);
3240 }
3241 }",
3242 "foo.cs",
3243 |metric| insta::assert_json_snapshot!(metric.abc),
3244 );
3245 }
3246
3247 #[cfg(feature = "csharp")]
3248 #[test]
3249 fn csharp_return_with_conditions() {
3250 check_metrics::<CsharpParser>(
3251 "class A {
3252 bool M(int x) {
3253 return (x > 0);
3254 }
3255 bool N(int x) {
3256 return !(x < 0);
3257 }
3258 }",
3259 "foo.cs",
3260 |metric| insta::assert_json_snapshot!(metric.abc),
3261 );
3262 }
3263
3264 // C# `switch` *expression* arms scored zero ABC conditions before
3265 // #456 — they carry no `case` / `default` token, so the token-driven
3266 // `csharp_count_token_condition` never saw them, even though C#
3267 // cyclomatic counts each non-discard arm. Revert-verified: adding the
3268 // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The
3269 // bare `_ =>` discard arm is excluded (the `default:` analogue),
3270 // mirroring the cyclomatic gate (lesson 11).
3271 #[cfg(feature = "csharp")]
3272 #[test]
3273 fn csharp_switch_expression_arm_counts_condition() {
3274 check_metrics::<CsharpParser>(
3275 "class A {
3276 int M(int x) {
3277 return x switch { 1 => 10, 2 => 20, _ => 0 };
3278 }
3279 }",
3280 "foo.cs",
3281 |metric| {
3282 // arm `1 =>` (+1) + arm `2 =>` (+1) + `_ =>` discard (+0).
3283 assert_eq!(metric.abc.conditions_sum(), 2);
3284 },
3285 );
3286 }
3287
3288 // Cross-language parity (lesson 11): a C# `switch` expression and the
3289 // equivalent Java arrow-`switch` must report the same ABC condition
3290 // count on equivalent code. Both have two concrete case arms and no
3291 // fallback arm, so both must count exactly 2. `check_metrics` takes a
3292 // non-capturing `fn` pointer, so the shared expected value (2) is
3293 // asserted in each callback rather than compared across closures; the
3294 // matching constant is what enforces parity. This guards against the
3295 // C# fix drifting away from the Java arrow-case treatment.
3296 #[cfg(all(feature = "csharp", feature = "java"))]
3297 #[test]
3298 fn csharp_java_switch_arm_abc_parity() {
3299 // C# switch expression: two arms, no fallback → 2 conditions.
3300 check_metrics::<CsharpParser>(
3301 "class A {
3302 int M(int x) {
3303 return x switch { 1 => 10, 2 => 20 };
3304 }
3305 }",
3306 "foo.cs",
3307 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3308 );
3309
3310 // Equivalent Java arrow-`switch`: two case arms, no default → 2.
3311 check_metrics::<JavaParser>(
3312 "class A {
3313 int m(int x) {
3314 return switch (x) { case 1 -> 10; case 2 -> 20; };
3315 }
3316 }",
3317 "foo.java",
3318 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3319 );
3320 }
3321
3322 // Issue #469: the `default` arm of a C-family `switch` is the
3323 // unconditional fallthrough and must NOT count as an ABC condition,
3324 // mirroring cyclomatic — which counts only the `Case` arms, never
3325 // the `Default` token.
3326 //
3327 // expected: each fixture is a single function whose switch has two
3328 // concrete `case` arms plus one `default`. ABC must count exactly
3329 // the two case arms (conditions = 2), matching cyclomatic's two
3330 // case-arm decisions. Pre-fix, every language below scored 3 (the
3331 // `Default` token leaked into the condition tally) — revert-verified
3332 // against the pre-#469 condition arms. We anchor on the integer
3333 // `conditions_sum()` headline (the value the public JSON serializes;
3334 // float magnitude is bit-brittle and excluded by the snapshot
3335 // policy). The cyclomatic side is pinned separately in
3336 // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where
3337 // the per-space `cyclomatic()` decision count is isolated.
3338 #[cfg(feature = "java")]
3339 #[test]
3340 fn java_switch_default_not_a_condition() {
3341 // Classic statement `default:`.
3342 check_metrics::<JavaParser>(
3343 "class A {
3344 int m(int x) {
3345 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3346 }
3347 }",
3348 "foo.java",
3349 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3350 );
3351 // Arrow `default ->` — shares the same `Default` token.
3352 check_metrics::<JavaParser>(
3353 "class A {
3354 int m(int x) {
3355 return switch (x) { case 1 -> 1; case 2 -> 2; default -> 0; };
3356 }
3357 }",
3358 "foo.java",
3359 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3360 );
3361 }
3362
3363 // Over-exclusion guard (issue #469): a statement `switch` with two
3364 // `case` arms and NO `default` must still count both cases. This
3365 // pins that the fix excludes only the `Default` token, never a
3366 // `Case` arm — the count is identical before and after #469 (two
3367 // cases → 2), so it would catch a fix that over-eagerly dropped a
3368 // real case (e.g. treating the trailing case as a fallthrough).
3369 // expected: case 1 (+1) + case 2 (+1) = 2.
3370 #[cfg(feature = "java")]
3371 #[test]
3372 fn java_switch_without_default_counts_all_cases() {
3373 check_metrics::<JavaParser>(
3374 "class A {
3375 int m(int x) {
3376 switch (x) { case 1: return 1; case 2: return 2; }
3377 return -1;
3378 }
3379 }",
3380 "foo.java",
3381 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3382 );
3383 }
3384
3385 #[cfg(feature = "csharp")]
3386 #[test]
3387 fn csharp_switch_default_not_a_condition() {
3388 check_metrics::<CsharpParser>(
3389 "class A {
3390 int M(int x) {
3391 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3392 }
3393 }",
3394 "foo.cs",
3395 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3396 );
3397 }
3398
3399 /// Regression #1450 / #1451: `goto case 2;` spells the same `case`
3400 /// keyword token (id 114) as a real arm, from a second production
3401 /// (`goto_statement`), and scored an ABC condition it does not earn —
3402 /// an unconditional jump to an arm is not a decision.
3403 ///
3404 /// `jmp` differs from `ctl` by exactly one statement, so the
3405 /// `goto case` is the only thing that can separate their scores
3406 /// (`.claude/rules/grammar-dispatch.md` §11 — an arm and a
3407 /// `goto case` are independent paths into the same counter, and a
3408 /// fixture carrying only arms cannot show the gate works). `dfl` is
3409 /// the control that was already correct, by accident rather than
3410 /// design: `goto default;` spells `Default`, which neither metric
3411 /// counts because it is the switch's unconditional fallthrough
3412 /// (#456, #469).
3413 ///
3414 /// [`assert_every_member_scores`] covers cyclomatic as well as ABC,
3415 /// through its per-member §8 parity assertion, and would catch the
3416 /// gate being removed from *either* side alone: dropping ABC's puts
3417 /// `jmp` at conditions 3 against the asserted 2, and dropping
3418 /// cyclomatic's leaves conditions 2 against a decision count of 3.
3419 /// Removing both restores the parity at `3 == 4 - 1` and is caught by
3420 /// the conditions value, which is why 2 is named rather than derived.
3421 ///
3422 /// The `Case` anchor is the second axis. Every member scores 2 from
3423 /// its two arms alone, so deleting `goto case 2;` leaves every
3424 /// assertion below satisfied and the construct under test gone; the
3425 /// anchor's count of 7 — six arms plus the one jump — fails by name
3426 /// instead.
3427 #[cfg(feature = "csharp")]
3428 #[test]
3429 fn csharp_goto_case_is_not_a_condition() {
3430 let src = "class A {
3431 int ctl(int x) { switch (x) { case 1: return 1; case 2: return 2; default: return 0; } }
3432 int jmp(int x) { switch (x) { case 1: goto case 2; case 2: return 2; default: return 0; } }
3433 int dfl(int x) { switch (x) { case 1: goto default; case 2: return 2; default: return 0; } }
3434 }";
3435 assert_csharp_fixture_spells(
3436 src,
3437 &[
3438 (Csharp::Case as u16, 7, "`case` tokens (6 arms + 1 jump)"),
3439 (Csharp::SwitchSection as u16, 9, "switch arms"),
3440 (Csharp::GotoStatement as u16, 2, "`goto` statements"),
3441 (
3442 Csharp::Default as u16,
3443 4,
3444 "`default` tokens (3 arms + 1 jump)",
3445 ),
3446 ],
3447 );
3448 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
3449 assert_every_member_scores(
3450 &space.spaces[0],
3451 3,
3452 2,
3453 "one condition per `case` arm, none for a `goto` into one",
3454 );
3455 });
3456 }
3457
3458 #[cfg(feature = "cpp")]
3459 #[test]
3460 fn cpp_switch_default_not_a_condition() {
3461 // C++ (and plain C, which shares this grammar) already excluded
3462 // `default`; this pins the cross-language parity invariant.
3463 check_metrics::<CppParser>(
3464 "void f(int x) {
3465 switch (x) { case 1: return; case 2: return; default: return; }
3466 }",
3467 "foo.cpp",
3468 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3469 );
3470 }
3471
3472 #[cfg(feature = "objc")]
3473 #[test]
3474 fn objc_abc() {
3475 // ObjC ABC reuses the C/C++ walker with two additions: a message
3476 // send `[obj msg]` is a call (B), and `@try` / `@catch` count as
3477 // conditions (C) like C++ try/catch.
3478 // A: `int total = 0`, `int i = 0`, `i++`, `total = total + …`,
3479 // `total = -1` = 5.
3480 // B: `[self valueAt:i]`, `[self risky]` = 2 message sends.
3481 // C: `i < n`, `@try`, `@catch`, `total >= 0` = 4.
3482 check_metrics::<ObjcParser>(
3483 "@implementation Foo\n\
3484 - (int)bar:(int)n {\n\
3485 int total = 0;\n\
3486 for (int i = 0; i < n; i++) {\n\
3487 total = total + [self valueAt:i];\n\
3488 }\n\
3489 @try {\n\
3490 [self risky];\n\
3491 } @catch (NSException *e) {\n\
3492 total = -1;\n\
3493 }\n\
3494 if (total >= 0) {\n\
3495 return total;\n\
3496 }\n\
3497 return 0;\n\
3498 }\n\
3499 @end\n",
3500 "foo.m",
3501 |metric| {
3502 assert_eq!(metric.abc.assignments_sum(), 5);
3503 assert_eq!(metric.abc.branches_sum(), 2);
3504 assert_eq!(metric.abc.conditions_sum(), 4);
3505 },
3506 );
3507 }
3508
3509 #[cfg(feature = "objc")]
3510 #[test]
3511 fn objc_abc_conditions() {
3512 // Exercises the condition-slot arms shared with C/C++: a `while`
3513 // head, a `&&` chain, a `do … while` trailing condition, and a
3514 // `return <comparison>`. ObjC routes these through the same
3515 // grammar-agnostic `cpp_inspect_*` helpers.
3516 // A: `p++`, `p--` = 2. B: no calls = 0.
3517 // C: `p != 0`, `*p > 0`, `*p < 9`, `*p == 0` = 4.
3518 check_metrics::<ObjcParser>(
3519 "@implementation Foo\n\
3520 - (int)g:(int *)p {\n\
3521 while (p != 0 && *p > 0) {\n\
3522 p++;\n\
3523 }\n\
3524 do {\n\
3525 p--;\n\
3526 } while (*p < 9);\n\
3527 return *p == 0;\n\
3528 }\n\
3529 @end\n",
3530 "foo.m",
3531 |metric| {
3532 assert_eq!(metric.abc.assignments_sum(), 2);
3533 assert_eq!(metric.abc.branches_sum(), 0);
3534 assert_eq!(metric.abc.conditions_sum(), 4);
3535 },
3536 );
3537 }
3538
3539 #[cfg(feature = "objc")]
3540 #[test]
3541 fn objc_abc_message_send_unary_condition() {
3542 // A negated boolean passed as a message-send argument is a unary
3543 // condition (Fitzpatrick Rule 9), the same as in a C-call argument.
3544 // Message args are direct children of `message_expression` (no
3545 // `argument_list`), so they are inspected in the `MessageExpression`
3546 // arm. Here: `[self use:!a]` (1 call + 1 unary condition) +
3547 // `cFunc(!a)` (1 call + 1 unary condition) → B=2, C=2.
3548 check_metrics::<ObjcParser>(
3549 "@implementation Foo\n\
3550 - (void)bar:(int)a {\n\
3551 [self use:!a];\n\
3552 cFunc(!a);\n\
3553 }\n\
3554 @end\n",
3555 "foo.m",
3556 |metric| {
3557 assert_eq!(metric.abc.branches_sum(), 2);
3558 assert_eq!(metric.abc.conditions_sum(), 2);
3559 },
3560 );
3561 }
3562
3563 #[cfg(feature = "objc")]
3564 #[test]
3565 fn objc_message_send_is_a_bool_terminal_in_condition_slots() {
3566 // `[obj ok]` is Objective-C's call, so in a condition slot it is
3567 // the unary condition `ok()` is (Fitzpatrick Rule 9). Until
3568 // `message_expression` joined `cpp_bool_terminal_kinds!` every
3569 // row here scored zero conditions where its C-call twin scored
3570 // one — and #1276's `for` slot inherited the gap. Each row is a
3571 // message send as the *whole* slot (grammar-dispatch §11: a
3572 // comparison would be counted by its own operator arm anyway),
3573 // and branches are asserted beside conditions so the arm cannot
3574 // be read as counting the call twice. The last three are the
3575 // over-count guards: a value position counts nothing, as `ok()`'s
3576 // does not.
3577 let cases = [
3578 ("if ([self ok]) {}", 1, 1),
3579 ("while (![self ok]) {}", 1, 1),
3580 ("do {} while ([self ok]);", 1, 1),
3581 ("for (; [self ok]; ) {}", 1, 1),
3582 ("x = [self ok] ? 1 : 2;", 2, 1),
3583 ("if ([self ok] && [o ok]) {}", 2, 2),
3584 ("return [self ok];", 0, 1),
3585 ("[self use:[self ok]];", 0, 2),
3586 ("[self use:![self ok]];", 1, 2),
3587 ];
3588 let mut ran = 0;
3589 for (body, conditions, branches) in cases {
3590 let src = format!("@implementation Foo\n- (int)bar {{\n {body}\n}}\n@end\n");
3591 let abc = metrics_verbatim(
3592 LANG::Objc,
3593 src.as_bytes(),
3594 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
3595 )
3596 .abc;
3597 assert_eq!(abc.conditions_sum(), conditions, "`{body}` conditions");
3598 assert_eq!(abc.branches_sum(), branches, "`{body}` branches");
3599 ran += 1;
3600 }
3601 assert_eq!(ran, cases.len());
3602 // Both answers are present, so a walker stuck at 0 or at 1 cannot
3603 // pass half the table silently.
3604 assert!(cases.iter().any(|&(_, c, _)| c == 0));
3605 assert!(cases.iter().any(|&(_, c, _)| c == 2));
3606 }
3607
3608 #[cfg(feature = "objc")]
3609 #[test]
3610 fn objc_message_send_condition_agrees_with_c_call() {
3611 // The intra-ObjC parity C++ cannot express: a message send and a
3612 // C call in the same slot score alike. Non-degenerate by the
3613 // `assert_eq!(…, 1)` on the reference.
3614 for (send, call) in [
3615 ("if ([a ok]) {}", "if (ok()) {}"),
3616 ("for (; [a ok]; ) {}", "for (; ok(); ) {}"),
3617 ("x = [a ok] ? 1 : 2;", "x = ok() ? 1 : 2;"),
3618 ] {
3619 let wrap =
3620 |body: &str| format!("@implementation Foo\n- (int)bar {{\n {body}\n}}\n@end\n");
3621 let reference = abc_conditions(LANG::Objc, &wrap(call));
3622 assert!(reference >= 1, "`{call}` must count at least the slot");
3623 assert_eq!(
3624 abc_conditions(LANG::Objc, &wrap(send)),
3625 reference,
3626 "`{send}`"
3627 );
3628 }
3629 }
3630
3631 #[cfg(feature = "groovy")]
3632 #[test]
3633 fn groovy_switch_default_not_a_condition() {
3634 check_metrics::<GroovyParser>(
3635 "class A {
3636 int m(int x) {
3637 switch (x) { case 1: return 1; case 2: return 2; default: return 0 }
3638 }
3639 }",
3640 "foo.groovy",
3641 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3642 );
3643 }
3644
3645 #[cfg(feature = "javascript")]
3646 #[test]
3647 fn js_switch_default_not_a_condition() {
3648 check_metrics::<JavascriptParser>(
3649 "function f(x) {
3650 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3651 }",
3652 "foo.js",
3653 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3654 );
3655 }
3656
3657 #[cfg(feature = "typescript")]
3658 #[test]
3659 fn ts_switch_default_not_a_condition() {
3660 check_metrics::<TypescriptParser>(
3661 "function f(x: number): number {
3662 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3663 }",
3664 "foo.ts",
3665 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3666 );
3667 }
3668
3669 // Cross-language parity (lesson 11): the equivalent statement-`switch`
3670 // with a `default` arm reports the same ABC condition count across
3671 // Java / C# / C++. All three have two concrete case arms plus a
3672 // fallthrough `default`, so all three must count exactly 2 conditions
3673 // (the `default` excluded). `check_metrics` takes a non-capturing
3674 // `fn` pointer, so the shared expected value is asserted in each
3675 // callback; the matching constant is what enforces parity.
3676 #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3677 #[test]
3678 fn java_csharp_cpp_switch_default_abc_parity() {
3679 check_metrics::<JavaParser>(
3680 "class A {
3681 int m(int x) {
3682 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3683 }
3684 }",
3685 "foo.java",
3686 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3687 );
3688 check_metrics::<CsharpParser>(
3689 "class A {
3690 int M(int x) {
3691 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3692 }
3693 }",
3694 "foo.cs",
3695 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3696 );
3697 check_metrics::<CppParser>(
3698 "void f(int x) {
3699 switch (x) { case 1: return; case 2: return; default: return; }
3700 }",
3701 "foo.cpp",
3702 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3703 );
3704 }
3705
3706 // Pins the ABC-vs-cyclomatic agreement the fix is about (lesson 11):
3707 // on the method's own function space, the cyclomatic decision count
3708 // (`cyclomatic()` minus the per-space base of 1) must equal the ABC
3709 // `conditions()` for the same switch. Both must be 2 — the two case
3710 // arms — with the `default` excluded from each. Revert-verified: pre-
3711 // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2.
3712 #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3713 #[test]
3714 fn java_csharp_cpp_switch_default_cyclomatic_parity() {
3715 check_func_space::<JavaParser, _>(
3716 "class A {
3717 int m(int x) {
3718 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3719 }
3720 }",
3721 "foo.java",
3722 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3723 );
3724 check_func_space::<CsharpParser, _>(
3725 "class A {
3726 int M(int x) {
3727 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3728 }
3729 }",
3730 "foo.cs",
3731 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3732 );
3733 check_func_space::<CppParser, _>(
3734 "void f(int x) {
3735 switch (x) { case 1: return; case 2: return; default: return; }
3736 }",
3737 "foo.cpp",
3738 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3739 );
3740 }
3741
3742 // Issue #473: PHP `switch` `default:` (`DefaultStatement`) is the
3743 // unconditional fallthrough, not a condition. ABC `conditions()` must
3744 // equal the cyclomatic decision count (`cyclomatic() - 1`) on the
3745 // function's own space — both 2 for the two `case` arms, with the
3746 // `default` excluded. Revert-verified: re-adding `DefaultStatement` to
3747 // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic
3748 // stays at 2, failing the invariant.
3749 #[cfg(feature = "php")]
3750 #[test]
3751 fn php_switch_default_not_a_condition() {
3752 check_func_space::<PhpParser, _>(
3753 "<?php
3754 function f($x) {
3755 switch ($x) {
3756 case 1: return 1;
3757 case 2: return 2;
3758 default: return 0;
3759 }
3760 }",
3761 "foo.php",
3762 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3763 );
3764 }
3765
3766 // Issue #473: PHP `match` `default =>` (`MatchDefaultExpression`) is the
3767 // unconditional fallthrough, mirroring the switch `default:` case above.
3768 // ABC `conditions()` must equal the cyclomatic decision count
3769 // (`cyclomatic() - 1`) — both 2 for the two non-default match arms.
3770 // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC
3771 // condition arm makes `conditions()` 3 here while cyclomatic stays at 2.
3772 #[cfg(feature = "php")]
3773 #[test]
3774 fn php_match_default_not_a_condition() {
3775 check_func_space::<PhpParser, _>(
3776 "<?php
3777 function g($x) {
3778 return match ($x) {
3779 1 => \"a\",
3780 2 => \"b\",
3781 default => \"z\",
3782 };
3783 }",
3784 "foo.php",
3785 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3786 );
3787 }
3788
3789 #[cfg(feature = "csharp")]
3790 #[test]
3791 fn csharp_if_bare_identifier_condition() {
3792 check_metrics::<CsharpParser>(
3793 "class A {
3794 void M(bool x) {
3795 if (x) { System.Console.WriteLine(\"a\"); }
3796 }
3797 }",
3798 "foo.cs",
3799 |metric| {
3800 // `if (x)` contributes 1 condition (bare identifier).
3801 // `System.Console.WriteLine(...)` is the only call → 1 branch.
3802 // `*_sum()` is what the public JSON serializes as the
3803 // headline value (see `crate::wire::Abc`).
3804 assert_eq!(metric.abc.conditions_sum(), 1);
3805 assert_eq!(metric.abc.branches_sum(), 1);
3806 assert_eq!(metric.abc.assignments_sum(), 0);
3807 },
3808 );
3809 }
3810
3811 #[cfg(feature = "csharp")]
3812 #[test]
3813 fn csharp_while_bare_identifier_condition() {
3814 check_metrics::<CsharpParser>(
3815 "class A {
3816 void M(bool x) {
3817 while (x) { x = false; }
3818 }
3819 }",
3820 "foo.cs",
3821 |metric| {
3822 // `while (x)` contributes 1 condition; `x = false` is 1 assignment.
3823 assert_eq!(metric.abc.conditions_sum(), 1);
3824 assert_eq!(metric.abc.assignments_sum(), 1);
3825 assert_eq!(metric.abc.branches_sum(), 0);
3826 },
3827 );
3828 }
3829
3830 #[cfg(feature = "csharp")]
3831 #[test]
3832 fn csharp_do_while_bare_identifier_condition() {
3833 check_metrics::<CsharpParser>(
3834 "class A {
3835 void M(bool x) {
3836 do { x = true; } while (x);
3837 }
3838 }",
3839 "foo.cs",
3840 |metric| {
3841 // `do { ... } while (x)` contributes 1 condition;
3842 // `x = true` is 1 assignment.
3843 assert_eq!(metric.abc.conditions_sum(), 1);
3844 assert_eq!(metric.abc.assignments_sum(), 1);
3845 assert_eq!(metric.abc.branches_sum(), 0);
3846 },
3847 );
3848 }
3849
3850 #[cfg(feature = "csharp")]
3851 #[test]
3852 fn csharp_if_unary_not_condition() {
3853 // Two cases share one test:
3854 //
3855 // if (!x) { … } — IfStatement is a known-boolean parent, so
3856 // the unary `!` arm in `csharp_inspect_container` is *one of
3857 // two* ways `has_boolean_content` gets set to true (the parent
3858 // seed sets it before the `!` does). A regression that broke
3859 // only the `is_not` branch wouldn't show up here.
3860 //
3861 // return !x; — ReturnStatement is *not* in the boolean-context
3862 // seed list (BinaryExpression | IfStatement | WhileStatement |
3863 // DoStatement | ForStatement | ConditionalExpression). So the
3864 // `!` wrapper is the *only* path that sets
3865 // `has_boolean_content = true`. Asserting the `return !x;`
3866 // case isolates the unary-unwrap logic from the parent-seed
3867 // path.
3868 check_metrics::<CsharpParser>(
3869 "class A {
3870 void M(bool x) {
3871 if (!x) { System.Console.WriteLine(\"a\"); }
3872 }
3873 bool N(bool x) {
3874 return !x;
3875 }
3876 }",
3877 "foo.cs",
3878 |metric| {
3879 // `if (!x)` contributes 1 condition (PrefixUnaryExpression
3880 // path with parent IfStatement seeding has_boolean_content).
3881 // `return !x;` contributes 1 condition (parent doesn't seed
3882 // — the unary `!` is the only path that sets the flag).
3883 // → 2 conditions total. 1 branch from WriteLine().
3884 assert_eq!(metric.abc.conditions_sum(), 2);
3885 assert_eq!(metric.abc.branches_sum(), 1);
3886 assert_eq!(metric.abc.assignments_sum(), 0);
3887 },
3888 );
3889 }
3890
3891 #[cfg(feature = "csharp")]
3892 #[test]
3893 fn csharp_if_double_parenthesized_condition() {
3894 // Audit-tests follow-up: with only the
3895 // `csharp_prefix_unary_expr_kinds!()` arm covered by
3896 // `csharp_if_unary_not_condition`, the
3897 // `csharp_paren_expr_kinds!()` delegation arm in
3898 // `csharp_count_condition` was a pure dead-code candidate —
3899 // disabling it caused zero existing tests to fail (verified
3900 // 2026-05-26).
3901 //
3902 // `if ((x))` puts a `ParenthesizedExpression` at child(2) of
3903 // the IfStatement (child(1) is the literal `(`, child(2) is
3904 // the inner parenthesised expression, child(3) is the literal
3905 // `)`). `csharp_count_condition` must route that case to
3906 // `csharp_inspect_container`, which then sees parent =
3907 // IfStatement, seeds `has_boolean_content = true`, walks to
3908 // the inner Identifier, and counts it. A regression that
3909 // removed the paren arm would silently score 0.
3910 check_metrics::<CsharpParser>(
3911 "class A {
3912 void M(bool x) {
3913 if ((x)) { System.Console.WriteLine(\"a\"); }
3914 }
3915 }",
3916 "foo.cs",
3917 |metric| {
3918 assert_eq!(metric.abc.conditions_sum(), 1);
3919 assert_eq!(metric.abc.branches_sum(), 1);
3920 assert_eq!(metric.abc.assignments_sum(), 0);
3921 },
3922 );
3923 }
3924
3925 #[cfg(feature = "csharp")]
3926 #[test]
3927 fn csharp_bool_returning_terminal_kinds_count() {
3928 // Regression for issue #372 (lesson #19): before the fix,
3929 // `csharp_count_condition` / `csharp_inspect_container` only
3930 // recognised invocation / identifier / boolean literal as
3931 // terminal-bool operands, so the five idiomatic boolean
3932 // expressions in the `if (...)` slots below silently scored
3933 // zero conditions:
3934 //
3935 // - `cfg.flag` — MemberAccessExpression
3936 // - `await c.Check()` — AwaitExpression
3937 // - `(bool)v` — CastExpression
3938 // - `v is not null` — IsPatternExpression
3939 // - `flags[0]` — ElementAccessExpression
3940 //
3941 // The `is` test has since moved out of the terminal set to an
3942 // unconditional arm (#1461), so it no longer depends on the
3943 // slot to be counted. The total is unchanged, which is the
3944 // point of that change rather than an accident of it.
3945 //
3946 // expected: 5 conditions (one per `if`), 0 assignments,
3947 // 1 branch (the single `c.Check()` invocation; the other
3948 // `if`-condition expressions are not invocations).
3949 check_metrics::<CsharpParser>(
3950 "using System.Threading.Tasks;
3951 class A {
3952 async Task M(object v, bool[] flags, Cfg cfg, C c) {
3953 if (cfg.flag) { }
3954 if (await c.Check()) { }
3955 if ((bool)v) { }
3956 if (v is not null) { }
3957 if (flags[0]) { }
3958 }
3959 }
3960 class Cfg { public bool flag; }
3961 class C { public Task<bool> Check() => null; }",
3962 "foo.cs",
3963 |metric| {
3964 assert_eq!(metric.abc.conditions_sum(), 5);
3965 assert_eq!(metric.abc.assignments_sum(), 0);
3966 assert_eq!(metric.abc.branches_sum(), 1);
3967 },
3968 );
3969 }
3970
3971 #[cfg(feature = "csharp")]
3972 #[test]
3973 fn csharp_if_method_call_condition() {
3974 check_metrics::<CsharpParser>(
3975 "class A {
3976 void M(string s) {
3977 if (s.StartsWith(\"x\")) { System.Console.WriteLine(\"a\"); }
3978 }
3979 }",
3980 "foo.cs",
3981 |metric| {
3982 // `if (s.StartsWith("x"))` contributes 1 condition
3983 // (InvocationExpression) plus 1 branch for the call itself,
3984 // plus 1 branch for WriteLine.
3985 assert_eq!(metric.abc.conditions_sum(), 1);
3986 assert_eq!(metric.abc.branches_sum(), 2);
3987 assert_eq!(metric.abc.assignments_sum(), 0);
3988 },
3989 );
3990 }
3991
3992 #[cfg(feature = "csharp")]
3993 #[test]
3994 fn csharp_if_while_boolean_literal_condition() {
3995 // Regression for #371: the tree-sitter-c-sharp grammar wraps a
3996 // bare `true` / `false` literal used as the condition of
3997 // `if` / `while` / `do` / `?:` in a `boolean_literal` node,
3998 // not the leaf `true` / `false` tokens. `csharp_count_condition`
3999 // must therefore match `BooleanLiteral` (the wrapper),
4000 // mirroring the existing `csharp_walk_for_statement` arm.
4001 // Without that, every literal-condition statement scored 0
4002 // conditions. The sibling `csharp_count_unary_conditions`
4003 // arm is covered separately by
4004 // `csharp_short_circuit_with_boolean_literal_operand` and
4005 // `csharp_inspect_container` is covered by
4006 // `csharp_declarations_with_conditions` (`!true` / `!false`).
4007 check_metrics::<CsharpParser>(
4008 "class A {
4009 void M() {
4010 if (true) { System.Console.WriteLine(\"a\"); }
4011 if (false) { System.Console.WriteLine(\"b\"); }
4012 while (true) { break; }
4013 do { break; } while (false);
4014 int t = true ? 1 : 0;
4015 }
4016 }",
4017 "foo.cs",
4018 |metric| {
4019 // Five literal-condition statements contribute 5
4020 // `BooleanLiteral` conditions (one per if/if/while/
4021 // do-while/ternary), plus the ternary's `?` token
4022 // adds one more via `csharp_count_token_condition`
4023 // → 6 total. The two `System.Console.WriteLine`
4024 // calls contribute 2 branches; the `int t = …`
4025 // initializer contributes 1 assignment.
4026 assert_eq!(metric.abc.conditions_sum(), 6);
4027 assert_eq!(metric.abc.branches_sum(), 2);
4028 assert_eq!(metric.abc.assignments_sum(), 1);
4029 },
4030 );
4031 }
4032
4033 #[cfg(feature = "csharp")]
4034 #[test]
4035 fn csharp_short_circuit_with_boolean_literal_operand() {
4036 // Regression for #371 (companion to
4037 // `csharp_if_while_boolean_literal_condition`): a bare
4038 // `true` / `false` operand of `&&` / `||` lands in
4039 // `csharp_count_unary_conditions`, which iterates the parent
4040 // BinaryExpression's children. That helper must match the
4041 // `BooleanLiteral` wrapper just like `csharp_count_condition`
4042 // does — otherwise the operand silently scores zero. Mutation-
4043 // verified: removing `BooleanLiteral` from the
4044 // `csharp_count_unary_conditions` arm leaves every other test
4045 // in the suite passing, so this is the only test guarding
4046 // that helper's literal-operand path.
4047 check_metrics::<CsharpParser>(
4048 "class A {
4049 void M(bool x) {
4050 if (x && true) { System.Console.WriteLine(\"a\"); }
4051 if (false || x) { System.Console.WriteLine(\"b\"); }
4052 }
4053 }",
4054 "foo.cs",
4055 |metric| {
4056 // `&&` and `||` themselves are NOT in
4057 // `csharp_count_token_condition`'s match list — they
4058 // route through `csharp_walk_for_conditions::AMPAMP|
4059 // PIPEPIPE`, which calls
4060 // `csharp_count_unary_conditions` on the parent
4061 // BinaryExpression. Each invocation counts every
4062 // child that matches the terminal-operand kinds and
4063 // whose parent is a BinaryExpression. For
4064 // `x && true`: 1 (Identifier x) + 1 (BooleanLiteral
4065 // true) = 2. For `false || x`: 1 (BooleanLiteral
4066 // false) + 1 (Identifier x) = 2. Total 4. Without
4067 // the BooleanLiteral arm only the two Identifier
4068 // counts would land, giving 2.
4069 assert_eq!(metric.abc.conditions_sum(), 4);
4070 assert_eq!(metric.abc.branches_sum(), 2);
4071 assert_eq!(metric.abc.assignments_sum(), 0);
4072 },
4073 );
4074 }
4075
4076 #[cfg(feature = "csharp")]
4077 #[test]
4078 fn csharp_return_without_conditions() {
4079 check_metrics::<CsharpParser>(
4080 "class A {
4081 int M() { return 42; }
4082 string N() { return \"hi\"; }
4083 }",
4084 "foo.cs",
4085 |metric| insta::assert_json_snapshot!(metric.abc),
4086 );
4087 }
4088
4089 #[cfg(feature = "csharp")]
4090 #[test]
4091 fn csharp_lambda_expressions_return_with_conditions() {
4092 check_metrics::<CsharpParser>(
4093 "class A {
4094 public void M() {
4095 System.Func<int, bool> f = x => (x > 0);
4096 System.Func<int, bool> g = x => !(x < 0);
4097 }
4098 }",
4099 "foo.cs",
4100 |metric| insta::assert_json_snapshot!(metric.abc),
4101 );
4102 }
4103
4104 #[cfg(feature = "csharp")]
4105 #[test]
4106 fn csharp_for_with_variable_declaration() {
4107 check_metrics::<CsharpParser>(
4108 "class A {
4109 void M() {
4110 for (int i = 0; i < 10; i++) {
4111 System.Console.WriteLine(i);
4112 }
4113 }
4114 }",
4115 "foo.cs",
4116 |metric| insta::assert_json_snapshot!(metric.abc),
4117 );
4118 }
4119
4120 #[cfg(feature = "csharp")]
4121 #[test]
4122 fn csharp_for_without_variable_declaration() {
4123 check_metrics::<CsharpParser>(
4124 "class A {
4125 void M() {
4126 int i;
4127 for (i = 0; i < 10; i++) {
4128 System.Console.WriteLine(i);
4129 }
4130 }
4131 }",
4132 "foo.cs",
4133 |metric| insta::assert_json_snapshot!(metric.abc),
4134 );
4135 }
4136
4137 #[cfg(feature = "csharp")]
4138 #[test]
4139 fn csharp_for_identifier_condition() {
4140 check_metrics::<CsharpParser>(
4141 "class A {
4142 void M(bool ready) {
4143 for (; ready ;) { }
4144 }
4145 }",
4146 "foo.cs",
4147 |metric| {
4148 // expected: assignments=0 (no `=` / `++` / `--`),
4149 // branches=0 (no invocation / object creation),
4150 // conditions=1 (bare-identifier for-loop condition).
4151 // Averages divide by 3 spaces (top-level + class + method).
4152 insta::assert_json_snapshot!(
4153 metric.abc,
4154 @r#"
4155 {
4156 "assignments": 0,
4157 "branches": 0,
4158 "conditions": 1,
4159 "magnitude": 1.0,
4160 "value": 0.0,
4161 "assignments_average": 0.0,
4162 "branches_average": 0.0,
4163 "conditions_average": 0.3333333333333333,
4164 "assignments_min": 0,
4165 "assignments_max": 0,
4166 "branches_min": 0,
4167 "branches_max": 0,
4168 "conditions_min": 0,
4169 "conditions_max": 1
4170 }
4171 "#
4172 );
4173 },
4174 );
4175 }
4176
4177 #[cfg(feature = "csharp")]
4178 #[test]
4179 fn csharp_for_invocation_condition() {
4180 check_metrics::<CsharpParser>(
4181 "class A {
4182 bool Ok() { return true; }
4183 void M() {
4184 for (; Ok() ;) { }
4185 }
4186 }",
4187 "foo.cs",
4188 |metric| {
4189 // expected: assignments=0, branches=1 (the `Ok()` call),
4190 // conditions=1 (invocation as for-loop condition).
4191 // Averages divide by 4 spaces (top-level + class + two
4192 // methods).
4193 insta::assert_json_snapshot!(
4194 metric.abc,
4195 @r#"
4196 {
4197 "assignments": 0,
4198 "branches": 1,
4199 "conditions": 1,
4200 "magnitude": 1.4142135623730951,
4201 "value": 0.0,
4202 "assignments_average": 0.0,
4203 "branches_average": 0.25,
4204 "conditions_average": 0.25,
4205 "assignments_min": 0,
4206 "assignments_max": 0,
4207 "branches_min": 0,
4208 "branches_max": 1,
4209 "conditions_min": 0,
4210 "conditions_max": 1
4211 }
4212 "#
4213 );
4214 },
4215 );
4216 }
4217
4218 // Regression coverage for #279: the C# grammar wraps a literal
4219 // `true` / `false` for-loop condition in a `boolean_literal` node.
4220 // The `BooleanLiteral` arm in the `ForStatement` dispatch must
4221 // attribute one condition; without it, `for (; true ;)` would
4222 // contribute 0 (the bug fixed by this commit also affected this
4223 // shape).
4224 #[cfg(feature = "csharp")]
4225 #[test]
4226 fn csharp_for_boolean_literal_condition() {
4227 check_metrics::<CsharpParser>(
4228 "class A {
4229 void M() {
4230 for (; true ;) { }
4231 }
4232 }",
4233 "foo.cs",
4234 |metric| {
4235 // expected: assignments=0, branches=0,
4236 // conditions=1 (the `true` literal as condition).
4237 assert_eq!(metric.abc.conditions_sum(), 1);
4238 assert_eq!(metric.abc.assignments_sum(), 0);
4239 assert_eq!(metric.abc.branches_sum(), 0);
4240 },
4241 );
4242 }
4243
4244 // Regression coverage for #279: an empty for-loop condition such as
4245 // `for (; ;) {}` must contribute 0 to conditions — there is no
4246 // condition node to count.
4247 #[cfg(feature = "csharp")]
4248 #[test]
4249 fn csharp_for_empty_condition() {
4250 check_metrics::<CsharpParser>(
4251 "class A {
4252 void M() {
4253 for (; ;) { }
4254 }
4255 }",
4256 "foo.cs",
4257 |metric| {
4258 // expected: assignments=0, branches=0, conditions=0
4259 // (no condition expression in `for (; ;)`).
4260 insta::assert_json_snapshot!(
4261 metric.abc,
4262 @r#"
4263 {
4264 "assignments": 0,
4265 "branches": 0,
4266 "conditions": 0,
4267 "magnitude": 0.0,
4268 "value": 0.0,
4269 "assignments_average": 0.0,
4270 "branches_average": 0.0,
4271 "conditions_average": 0.0,
4272 "assignments_min": 0,
4273 "assignments_max": 0,
4274 "branches_min": 0,
4275 "branches_max": 0,
4276 "conditions_min": 0,
4277 "conditions_max": 0
4278 }
4279 "#
4280 );
4281 },
4282 );
4283 }
4284
4285 #[cfg(feature = "csharp")]
4286 #[test]
4287 fn csharp_ternary_conditions() {
4288 check_metrics::<CsharpParser>(
4289 "class A {
4290 int Sign(int x) {
4291 return (x > 0) ? 1 : (x < 0 ? -1 : 0);
4292 }
4293 }",
4294 "foo.cs",
4295 |metric| insta::assert_json_snapshot!(metric.abc),
4296 );
4297 }
4298
4299 #[cfg(feature = "csharp")]
4300 #[test]
4301 fn csharp_malformed_parenthesized_no_panic() {
4302 check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
4303 // Don't panic on malformed source.
4304 assert_eq!(metric.abc.assignments(), 0);
4305 assert_eq!(metric.abc.branches(), 0);
4306 });
4307 }
4308
4309 #[cfg(feature = "csharp")]
4310 #[test]
4311 fn csharp_function_pointer_type_no_double_count() {
4312 // EC1 extension — `<` and `>` are also parameter-list delimiters
4313 // for unsafe function-pointer types. Since #1297 the arm is an
4314 // allowlist, so `FunctionPointerType` is excluded by being
4315 // absent from it rather than by being named in a deny set;
4316 // either way these brackets must not accumulate `conditions`.
4317 check_metrics::<CsharpParser>(
4318 "unsafe class A {
4319 public delegate*<int, int, int> Adder;
4320 public delegate*<string, void> Logger;
4321 }",
4322 "foo.cs",
4323 |metric| {
4324 assert_eq!(
4325 metric.abc.conditions(),
4326 0,
4327 "function-pointer-type angle brackets must not count"
4328 );
4329 },
4330 );
4331 }
4332
4333 #[cfg(feature = "csharp")]
4334 #[test]
4335 fn csharp_generic_type_args_no_double_count() {
4336 // EC1 — `<` and `>` inside TypeArgumentList must not count as
4337 // boolean conditions.
4338 check_metrics::<CsharpParser>(
4339 "class A {
4340 void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
4341 System.Console.WriteLine(d);
4342 }
4343 }",
4344 "foo.cs",
4345 |metric| insta::assert_json_snapshot!(metric.abc),
4346 );
4347 }
4348
4349 // #1297: `operator <` names the operator a type *defines*; applying
4350 // one is what a decision looks like. tree-sitter-c-sharp spells the
4351 // declaration's operator with the same bare token as a comparison,
4352 // and the previous denylist named only `type_argument_list`,
4353 // `type_parameter_list` and `function_pointer_type`, so every
4354 // comparison-operator overload scored a condition.
4355 //
4356 // The fixture carries the two overloads plus one `a < b` inside a
4357 // `binary_expression` and one `x is > 0 ? 2 : 3`, which the grammar
4358 // parses as a `relational_pattern` whose operand is the ternary (see
4359 // the assertion). Every mis-aim lands on its own number: 6
4360 // with neither gate, 4 if `RelationalPattern` is readmitted to the
4361 // allowlist (#1383 dropped it), 2 if the gate swallows
4362 // `BinaryExpression` too (only the ternary `?` and the `is` test
4363 // survive), 0 if the fixture stops parsing. Each is one higher than
4364 // before #1461, which added the `is` test as a count no gate on the
4365 // `<` / `>` token can reach.
4366 #[cfg(feature = "csharp")]
4367 #[test]
4368 fn csharp_operator_declaration_is_not_a_condition() {
4369 check_func_space::<CsharpParser, _>(
4370 "class V {
4371 public static bool operator <(V a, V b) { return true; }
4372 public static bool operator >(V a, V b) { return true; }
4373 int m(int a, int b, int x) {
4374 if (a < b) { return 1; }
4375 return x is > 0 ? 2 : 3;
4376 }
4377 }",
4378 "foo.cs",
4379 |space| {
4380 // Assert the claim per space rather than through the
4381 // file total, which is 2 with the two overloads and 2
4382 // without them — an aggregate assertion would pass on a
4383 // fixture that had lost the very construct under test.
4384 let class = &space.spaces[0];
4385 assert_eq!(class.spaces.len(), 3, "two overloads plus `m`");
4386 for (i, member) in class.spaces.iter().enumerate().take(2) {
4387 assert_eq!(
4388 member.metrics.abc.conditions(),
4389 0,
4390 "operator declaration {i} must score no condition"
4391 );
4392 }
4393 // 3: the `a < b` comparison, the ternary `?`, and the
4394 // `is` test. tree-sitter-c-sharp 0.23.5 parses
4395 // `x is > 0 ? 2 : 3` as `x is > (0 ? 2 : 3)` — the ternary
4396 // is the pattern's operand, so the pattern sits in no
4397 // decision slot and its `>` still scores nothing (#1383),
4398 // while the ternary's condition is the literal `0`.
4399 // The third count is the enclosing `is_pattern_expression`
4400 // itself, which since #1461 scores by use rather than only
4401 // inside a boolean slot. C# itself binds the source
4402 // `(x is > 0) ? 2 : 3`; a grammar that agrees still reads
4403 // 3 here, by the same three counts in a different
4404 // arrangement.
4405 assert_eq!(class.spaces[2].metrics.abc.conditions(), 3);
4406 },
4407 );
4408 }
4409
4410 // #1420, the other four spellings of the test above. C# overloads six
4411 // comparison operators and #1297 gated two: `<=` `>=` `==` `!=` are
4412 // distinct tokens that reach a different arm of
4413 // `csharp_count_token_condition` and each still scored one spurious
4414 // condition on its declaration — measured 1 apiece against `<` / `>`'s
4415 // 0, on a class where `cyclomatic()` is 1 for all six.
4416 //
4417 // Each token gets its own member so a partial fix names the spelling
4418 // it missed: gating `EQEQ | BANGEQ` and forgetting `GTEQ | LTEQ` fails
4419 // here by operator, where the file total cannot tell the two apart —
4420 // it reads 4 before the fix and 0 after, so any two-of-four fix halves
4421 // it and still looks like movement.
4422 //
4423 // `n` is the deliberate control (§11): the same four tokens *applied*,
4424 // inside `binary_expression` parents, which must keep scoring one
4425 // condition each. Without it the gate could be satisfied by refusing
4426 // to count these tokens at all.
4427 #[cfg(feature = "csharp")]
4428 #[test]
4429 fn csharp_comparison_operator_overloads_are_not_conditions() {
4430 let src = "class V {
4431 public static bool operator <=(V a, V b) { return true; }
4432 public static bool operator >=(V a, V b) { return true; }
4433 public static bool operator ==(V a, V b) { return true; }
4434 public static bool operator !=(V a, V b) { return true; }
4435 int n(int a, int b) {
4436 if (a <= b) { return 1; }
4437 if (a >= b) { return 2; }
4438 if (a == b) { return 3; }
4439 if (a != b) { return 4; }
4440 return 0;
4441 }
4442 }";
4443 // Two of each token: one declaring, one applying. Trimming either
4444 // half out collapses this test into the other half's control and
4445 // is what this anchor exists to catch.
4446 assert_csharp_fixture_spells(
4447 src,
4448 &[
4449 (
4450 Csharp::OperatorDeclaration as u16,
4451 4,
4452 "operator declarations",
4453 ),
4454 (Csharp::LTEQ as u16, 2, "`<=`"),
4455 (Csharp::GTEQ as u16, 2, "`>=`"),
4456 (Csharp::EQEQ as u16, 2, "`==`"),
4457 (Csharp::BANGEQ as u16, 2, "`!=`"),
4458 ],
4459 );
4460 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4461 let class = &space.spaces[0];
4462 assert_eq!(class.spaces.len(), 5, "four overloads plus `n`");
4463 for (member, spelling) in class.spaces.iter().zip(["<=", ">=", "==", "!="]) {
4464 assert_eq!(
4465 member.metrics.abc.conditions(),
4466 0,
4467 "`operator {spelling}` declares an operator, it does not apply one"
4468 );
4469 assert_eq!(
4470 member.metrics.abc.conditions(),
4471 member.metrics.cyclomatic.cyclomatic() - 1,
4472 "`operator {spelling}`: §8 parity with the cyclomatic decision count"
4473 );
4474 }
4475 // The control, found by name so a reordered fixture cannot
4476 // silently re-point it at an overload. 0 here would mean the
4477 // gate swallowed the applying form along with the declaring
4478 // one.
4479 let control = child_space(class, "n");
4480 assert_eq!(
4481 control.metrics.abc.conditions(),
4482 4,
4483 "`n` applies all four tokens: one condition each"
4484 );
4485 });
4486 }
4487
4488 // The allowlist #1420 installed is the only thing keeping `#if A == B`
4489 // a condition, and it keeps it for a reason no arm states: the grammar
4490 // aliases `preproc_binary_expression` onto `binary_expression`, so the
4491 // preprocessor's `==` arrives with kind id `BinaryExpression` (369)
4492 // and not with one of its own. Before #1420 the token was ungated and
4493 // no fact about its parent mattered; now a grammar bump that gave the
4494 // alias a distinct id would stop counting every `#if` equality with
4495 // nothing red. This is that pin.
4496 //
4497 // `BinaryExpression2` (469) is the pre-alias symbol the arm also
4498 // lists, defensively, per `.claude/rules/grammar-dispatch.md` §1. §2
4499 // asks that such an entry be kept *and* have its unreachability
4500 // asserted, so that a pin promoting the symbol changes behaviour
4501 // loudly rather than invisibly.
4502 #[cfg(feature = "csharp")]
4503 #[test]
4504 fn csharp_preproc_equality_counts_through_the_binary_expression_alias() {
4505 let src = "class P {
4506 #if A == B
4507 int x;
4508 #endif
4509 #if C != D
4510 int y;
4511 #endif
4512 }";
4513 let parser = CsharpParser::new(
4514 src.as_bytes().to_vec(),
4515 std::path::Path::new("foo.cs"),
4516 None,
4517 );
4518 assert!(
4519 ast_has_kind_id(&parser, Csharp::BinaryExpression as u16),
4520 "the `#if` expressions must parse as `binary_expression` — \
4521 the alias is what the arm's allowlist matches"
4522 );
4523 assert!(
4524 !ast_has_kind_id(&parser, Csharp::BinaryExpression2 as u16),
4525 "`BinaryExpression2` is the pre-alias symbol and must stay \
4526 unreachable; if the grammar starts emitting it, re-derive the \
4527 allowlist rather than trusting this arm"
4528 );
4529 // 2, one per `#if` expression. The two directives carry no
4530 // declarations the walk would score, so nothing else contributes.
4531 check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
4532 assert_eq!(metric.abc.conditions_sum(), 2);
4533 });
4534 }
4535
4536 // #1383: a `relational_pattern`'s operator is not a condition of
4537 // its own — the `switch_expression_arm` that owns it already scores
4538 // the decision, exactly as it does for the constant arm `5 => 1`.
4539 // Counting both charged a relational arm twice, and twice C#'s own
4540 // cyclomatic decision count.
4541 //
4542 // Per `.claude/rules/grammar-dispatch.md` §11 the `operator <`
4543 // fixture above cannot prove this alone: its `is > 0` sits beside a
4544 // `binary_expression` comparison that supplies a plausible total on
4545 // its own. Here the pattern operators are the *only* `<` / `>` /
4546 // `>=` / `<=` in the file, so a readmitted `RelationalPattern`
4547 // parent is the only thing that can lift the count.
4548 //
4549 // Both spellings are covered, and asserted per member rather than
4550 // through a total, because they reached two different arms when this
4551 // was written: `n`'s `>` / `<` were gated by a `GT | LT` parent
4552 // allowlist and `g`'s `>=` / `<=` by a separate `RelationalPattern`
4553 // denial, so fixing one and not the other left the *other method* at
4554 // 4. #1420 merged all six comparison tokens onto the one allowlist,
4555 // which removes that particular way to get it half right — but the
4556 // per-member shape still earns its keep, since a total of 6 is what
4557 // both `{2, 2, 2}` and a regressed `{4, 2, 0}` report:
4558 //
4559 // | state | `n` | `g` | `c` |
4560 // |---|---|---|---|
4561 // | `relational_pattern` excluded (shipped) | 2 | 2 | 2 |
4562 // | readmitted to the allowlist | 4 | 4 | 2 |
4563 //
4564 // `c` is the constant-pattern control and reads 2 in both rows: it is
4565 // what the relational methods are supposed to agree with.
4566 #[cfg(feature = "csharp")]
4567 #[test]
4568 fn csharp_relational_pattern_does_not_double_count_its_arm() {
4569 let src = "class A {
4570 int n(int x) => x switch { > 5 => 1, < 0 => 2, _ => 3 };
4571 int g(int x) => x switch { >= 5 => 1, <= 0 => 2, _ => 3 };
4572 int c(int x) => x switch { 5 => 1, 0 => 2, _ => 3 };
4573 }";
4574 assert_csharp_fixture_spells(
4575 src,
4576 &[
4577 (Csharp::RelationalPattern as u16, 4, "relational patterns"),
4578 (Csharp::GT as u16, 1, "`>`"),
4579 (Csharp::LT as u16, 1, "`<`"),
4580 (Csharp::GTEQ as u16, 1, "`>=`"),
4581 (Csharp::LTEQ as u16, 1, "`<=`"),
4582 ],
4583 );
4584 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4585 // Per space, not through the root: the root can only see
4586 // the sum, and a `{4, 2, 0}` regression sums to the same 6
4587 // as the correct `{2, 2, 2}`. `cyclomatic()` parity is also
4588 // only expressible per space — `cyclomatic_sum()` folds in
4589 // a base of 1 for each one.
4590 assert_every_member_scores(&space.spaces[0], 3, 2, "one condition per non-discard arm");
4591 });
4592 }
4593
4594 // The `is` half of #1383, and the one whose over-count came from a
4595 // different owner: `if (x is > 0)` scored the `IfStatement`
4596 // condition slot *and* the pattern's `>`. `f`'s guard is the
4597 // deliberate control — a `binary_expression` comparison in the same
4598 // slot still scores exactly one, so the fix cannot be read as
4599 // "stop counting `if` conditions".
4600 //
4601 // The two combinator methods pin the compound forms the issue asks
4602 // about. `and` / `or` are keyword tokens C# ABC never listed and C#
4603 // cyclomatic never counts, so the arm stays the single decision
4604 // however many relational operands it carries — which is exactly
4605 // what would regress if a later fix re-derived the gate from the
4606 // operand instead of the parent.
4607 #[cfg(feature = "csharp")]
4608 #[test]
4609 fn csharp_is_pattern_and_combinators_score_one_decision() {
4610 let src = "class A {
4611 int m(int x) { if (x is > 0) { return 1; } return 0; }
4612 int f(int x) { if (x > 0) { return 1; } return 0; }
4613 int a(int x) { if (x is > 0 and < 9) { return 1; } return 0; }
4614 int o(int x) { if (x is < 0 or >= 100) { return 1; } return 0; }
4615 int n(int x) { if (x is not > 5) { return 1; } return 0; }
4616 }";
4617 assert_csharp_fixture_spells(
4618 src,
4619 &[
4620 (Csharp::RelationalPattern as u16, 6, "relational patterns"),
4621 (Csharp::IsPatternExpression as u16, 4, "`is` pattern tests"),
4622 (Csharp::AndPattern as u16, 1, "`and`"),
4623 (Csharp::OrPattern as u16, 1, "`or`"),
4624 (Csharp::NegatedPattern as u16, 1, "`not`"),
4625 (Csharp::GTEQ as u16, 1, "`>=` (only `o` carries one)"),
4626 ],
4627 );
4628 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4629 assert_every_member_scores(&space.spaces[0], 5, 1, "the `if` is the only decision");
4630 });
4631 }
4632
4633 // #1383's second, quieter effect, and the one its issue does not
4634 // mention: a relational pattern outside any decision slot went from
4635 // 1 to 0 as well, because the gate is on the operator's parent and
4636 // not on what encloses the pattern. That left `q` — the equivalent
4637 // binary comparison `x > 5` in the same slot — scoring 1 against
4638 // these four's 0, so a relational pattern read one *lower* than the
4639 // comparison it is sugar for. #1383 recorded that as a deliberate
4640 // exception to Fitzpatrick Rule 5 and kept it, because the only
4641 // route it had was to readmit the pattern's `<` / `>` token, which
4642 // would have scored the decision-slot case twice.
4643 //
4644 // #1461 closed it from the other end, and the test now pins the
4645 // agreement rather than the exception. The enclosing
4646 // `is_pattern_expression` scores by use, so `x is > 5` reads 1
4647 // wherever it is written — level with `q`, and level with the plain
4648 // type test `x is int` (`t`, whose `is_expression` moved for the
4649 // same reason). The pattern's own `>` still scores nothing, which
4650 // is what keeps the whole test at 1 rather than 2: one relational
4651 // operation, one condition.
4652 //
4653 // The four probes stay because they are four different enclosings —
4654 // a declarator, a bare `return`, an argument, a type test — and the
4655 // point was never that the pattern is special in one of them.
4656 #[cfg(feature = "csharp")]
4657 #[test]
4658 fn csharp_relational_pattern_scores_one_wherever_it_is_written() {
4659 let src = "class A {
4660 static bool M(bool b) { return b; }
4661 bool p(int x) { bool b = x is > 5; return b; }
4662 bool q(int x) { bool b = x > 5; return b; }
4663 bool r(int x) { return x is > 5; }
4664 bool s(int x) { return M(x is > 5); }
4665 bool t(object x) { bool b = x is int; return b; }
4666 }";
4667 assert_csharp_fixture_spells(
4668 src,
4669 &[
4670 (Csharp::RelationalPattern as u16, 3, "relational patterns"),
4671 (Csharp::IsPatternExpression as u16, 3, "`is` pattern tests"),
4672 (Csharp::IsExpression as u16, 1, "the `is int` control"),
4673 ],
4674 );
4675 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4676 let class = &space.spaces[0];
4677 assert_eq!(class.spaces.len(), 6, "`M` plus five probes");
4678 let by_name = |n: &str| {
4679 class
4680 .spaces
4681 .iter()
4682 .find(|s| s.name.as_deref() == Some(n))
4683 .unwrap_or_else(|| panic!("fixture lost `{n}`"))
4684 .metrics
4685 .abc
4686 .conditions()
4687 };
4688 // Named rather than indexed: the claim is about which
4689 // spelling scores what, so a reordering of the fixture must
4690 // not silently re-point the assertions.
4691 for probe in ["p", "r", "s"] {
4692 assert_eq!(by_name(probe), 1, "`{probe}`: the `is` test, once");
4693 }
4694 assert_eq!(by_name("t"), 1, "the bare type test, the same once");
4695 assert_eq!(by_name("q"), 1, "the comparison it is sugar for");
4696 assert_eq!(by_name("M"), 0, "no relational operator anywhere");
4697 });
4698 }
4699
4700 // The boundary of #1383: the pattern's operator stops counting, a
4701 // *binary* operator in the arm's `when` guard keeps counting (`w`).
4702 // Both sit under the same `switch_expression_arm`, so this is what
4703 // stops a later "patterns don't count" pass from suppressing the
4704 // guard too.
4705 //
4706 // A relational *pattern* in the guard (`g`) is the other side of that
4707 // line: the operator itself still scores nothing, because the gate is
4708 // on the operator's parent — but since #1422 the guard is a condition
4709 // slot, and an `is` test is one of `csharp_bool_terminal_kinds!()`, so
4710 // the slot pays for it. That is what closed the gap this test used to
4711 // carry a `FIXME(#1422)` for: `when n is > 5` now reads level with
4712 // `when n > 5` instead of one below it.
4713 //
4714 // §8 holds throughout, which it did not before #1422. The old table
4715 // here recorded the defect: neither metric modelled the guard, so
4716 // ABC's count was whatever operator token happened to sit inside it
4717 // and cyclomatic's was nothing at all. Both tiers measured, before
4718 // and after:
4719 //
4720 // | guard | `conditions()` | `cyclomatic() - 1` |
4721 // |---|---|---|
4722 // | `when x % 2 == 0` (this fixture) | 3 → 3 | 2 → 3 |
4723 // | `when x > 2` | 3 → 3 | 2 → 3 |
4724 // | `when IsEven(x)` | 2 → 3 | 2 → 3 |
4725 //
4726 // The `==` is the guard's own operator and the only `==` in the
4727 // file, so trimming the `when` clause out of `w` drops its count to
4728 // 2 rather than leaving the assertion satisfied by something else;
4729 // `assert_csharp_fixture_spells` pins both guards by kind against
4730 // the same decay.
4731 #[cfg(feature = "csharp")]
4732 #[test]
4733 fn csharp_switch_arm_guard_operator_still_counts() {
4734 let src = "class A {
4735 int w(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
4736 int g(int x) => x switch { int n when n is > 5 => 1, _ => 0 };
4737 }";
4738 assert_csharp_fixture_spells(
4739 src,
4740 &[
4741 (Csharp::WhenClause as u16, 2, "`when` guards"),
4742 (Csharp::EQEQ as u16, 1, "the guard's `==`"),
4743 (Csharp::RelationalPattern as u16, 3, "relational patterns"),
4744 ],
4745 );
4746 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4747 let m = &space.spaces[0].spaces[0];
4748 assert_eq!(m.name.as_deref(), Some("w"));
4749 // 4 if the guard slot started counting a `binary_expression`
4750 // on top of its `==`, 2 if the `==` were suppressed along
4751 // with the pattern operators — both are live regressions.
4752 assert_eq!(
4753 m.metrics.abc.conditions(),
4754 3,
4755 "two arms plus the guard's `==`"
4756 );
4757 assert_eq!(m.metrics.cyclomatic.cyclomatic(), 4);
4758
4759 let g = &space.spaces[0].spaces[1];
4760 assert_eq!(g.name.as_deref(), Some("g"));
4761 assert_eq!(
4762 g.metrics.abc.conditions(),
4763 2,
4764 "one arm plus the guard slot, which scores the `is` test"
4765 );
4766 assert_eq!(g.metrics.cyclomatic.cyclomatic(), 3);
4767 });
4768 }
4769
4770 // #1422's headline: four spellings of one guard, previously worth
4771 // two different ABC numbers because nothing modelled the guard and
4772 // ABC counted whatever operator token happened to sit inside it —
4773 // `when x % 2 == 0` and `when x > 2` scored 1 through the
4774 // comparison-token arm, `when IsEven(x)` scored 0. Modelled as a
4775 // condition slot each contributes exactly 1, and the cyclomatic
4776 // `WhenClause` arm lifts the decision count to match.
4777 //
4778 // `none` is the unguarded control and the anchor: a two-arm switch
4779 // over the same relational patterns carrying no guard at all, so
4780 // every member here is exactly one guard away from it. Without that
4781 // row a fix that scored the *arms* differently would satisfy the
4782 // four guarded rows alike. Its arms run `> 2` before `> 0` because
4783 // the reverse order is a C# compile error (CS8510, the later arm
4784 // subsumed) — the guarded members escape that only because a guard
4785 // can fail, which is what makes their repeated `> 0` legal.
4786 //
4787 // `paren` is not a fourth spelling for its own sake — it is the only
4788 // member reaching the `WhenClause` seed added to
4789 // `csharp_inspect_container`, since `when_clause` wraps a
4790 // parenthesised guard in a real `parenthesized_expression` rather
4791 // than the anonymous parens `catch_filter_clause` uses.
4792 //
4793 // `nullc` is #1459's row and the counter-example that falsified the
4794 // claim above when it was first written. A `??` guard is a
4795 // `binary_expression`, which the slot declines — it leaves an
4796 // operator guard to the arm that already counts the operator — and
4797 // no ABC arm counted `??`, so this one spelling read 2 where the
4798 // other four read 3. Counting the token levels it without touching
4799 // the slot; an unconditional `+1` in the slot instead would have
4800 // taken `cmp` to 4, re-creating the double count #1422 removed.
4801 //
4802 // It is the one guarded member whose cyclomatic is not 4, and that
4803 // is not a discrepancy to fix: C# cyclomatic counts `??` as a
4804 // decision *in addition to* the guard clause, so the arm below
4805 // narrows the ABC-versus-cyclomatic gap from two to one rather than
4806 // closing it. The remaining one is the slot's standing policy of
4807 // leaving a `binary_expression` to its operators, and `cmp` pays it
4808 // too — it just happens to break even there.
4809 #[cfg(feature = "csharp")]
4810 #[test]
4811 fn csharp_switch_arm_guard_scores_one_condition_however_spelled() {
4812 let src = "class A {
4813 static bool IsEven(int x) { return true; }
4814 int tok(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
4815 int cmp(int x) => x switch { > 0 when x > 2 => 1, > 0 => 2, _ => 3 };
4816 int call(int x) => x switch { > 0 when IsEven(x) => 1, > 0 => 2, _ => 3 };
4817 int paren(int x) => x switch { > 0 when (IsEven(x)) => 1, > 0 => 2, _ => 3 };
4818 int nullc(int x, bool? b) => x switch { > 0 when b ?? false => 1, > 0 => 2, _ => 3 };
4819 int none(int x) => x switch { > 2 => 1, > 0 => 2, _ => 3 };
4820 }";
4821 assert_csharp_fixture_spells(
4822 src,
4823 &[
4824 (Csharp::WhenClause as u16, 5, "`when` guards"),
4825 (Csharp::EQEQ as u16, 1, "`tok`'s `==`"),
4826 (Csharp::QMARKQMARK as u16, 1, "`nullc`'s `??`"),
4827 (
4828 Csharp::ParenthesizedExpression as u16,
4829 1,
4830 "`paren`'s parenthesised guard",
4831 ),
4832 // By the call's *argument list*, which carries no
4833 // numeric-suffix alias, rather than by
4834 // `invocation_expression`, which has three
4835 // (`InvocationExpression` / `2` / `3`) and would pin this
4836 // anchor to whichever one the pinned grammar happens to
4837 // emit. `call` and `paren` hold the only two argument
4838 // lists in the fixture — `IsEven(int x)` is a
4839 // `parameter_list`.
4840 (Csharp::ArgumentList as u16, 2, "the `IsEven` calls"),
4841 ],
4842 );
4843 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4844 assert_members_score(
4845 &space.spaces[0],
4846 &[
4847 ("IsEven", 0, 1),
4848 ("tok", 3, 4),
4849 ("cmp", 3, 4),
4850 ("call", 3, 4),
4851 ("paren", 3, 4),
4852 // Was 2 — the guard spelling nothing counted.
4853 // Cyclomatic 5 rather than 4 because `??` is a
4854 // decision there on top of the guard clause.
4855 ("nullc", 3, 5),
4856 ("none", 2, 3),
4857 ],
4858 );
4859 });
4860 }
4861
4862 // The bare type test `x is int` is `is_expression` (391); only once
4863 // a pattern is involved (`x is int y`, `x is null`) does the grammar
4864 // emit `is_pattern_expression` (392). They are distinct kinds, and
4865 // `csharp_bool_terminal_kinds!()` listed only the second, so the two
4866 // spellings of one test disagreed: `if (x is int)` scored zero
4867 // conditions against a cyclomatic decision of one while
4868 // `if (x is int y)` scored one.
4869 //
4870 // Found by the whole-branch review of this batch, and it falsified
4871 // #1422's own claim in the arm above that a guard "scores one
4872 // however it is spelled" — `when x is int` read 1 where
4873 // `when IsEven(x)` read 2, which is the spelling-dependence that
4874 // fix exists to remove. It is the C# half of the gap #1421 closed
4875 // for Kotlin by adding `IsExpression | InExpression` there. Both
4876 // languages' type tests have since moved out of the terminal set to
4877 // an unconditional arm (#1461), which leaves every number here
4878 // unchanged — the slot no longer counts the test and the arm does.
4879 //
4880 // The plain `if` members are the control that keeps this honest: a
4881 // guard-only fixture could be satisfied by a guard-specific rule,
4882 // and the defect was never guard-specific. `q` and `iq` are the
4883 // already-correct `is_pattern_expression` twins, so a regression
4884 // that reintroduced the asymmetry fails on the pair rather than on
4885 // an absolute number.
4886 #[cfg(feature = "csharp")]
4887 #[test]
4888 fn csharp_bare_is_type_test_scores_one_condition() {
4889 let src = "class A {
4890 static bool IsEven(int x) { return true; }
4891 int g(object x) => x switch { object o when x is int => 1, _ => 0 };
4892 int gq(object x) => x switch { object o when x is int y => 1, _ => 0 };
4893 int gc(object x) => x switch { object o when IsEven(1) => 1, _ => 0 };
4894 int p(object x) { if (x is int) { return 1; } return 0; }
4895 int q(object x) { if (x is int y) { return 1; } return 0; }
4896 }";
4897 assert_csharp_fixture_spells(
4898 src,
4899 &[
4900 (Csharp::IsExpression as u16, 2, "the bare `is` type tests"),
4901 (
4902 Csharp::IsPatternExpression as u16,
4903 2,
4904 "the designation-pattern twins",
4905 ),
4906 (Csharp::WhenClause as u16, 3, "`when` guards"),
4907 ],
4908 );
4909 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4910 assert_members_score(
4911 &space.spaces[0],
4912 &[
4913 ("IsEven", 0, 1),
4914 // Level with `gq` and `gc`: every guard spelling
4915 // scores one, which is what #1422 claims.
4916 ("g", 2, 3),
4917 ("gq", 2, 3),
4918 ("gc", 2, 3),
4919 // Level with `q`: the two spellings of one type
4920 // test agree outside a guard too.
4921 ("p", 1, 2),
4922 ("q", 1, 2),
4923 ],
4924 );
4925 });
4926 }
4927
4928 // The null-forgiving `b!` is transparent: it scores whatever the
4929 // operand it wraps scores (#1463).
4930 //
4931 // `postfix_unary_expression` was in neither
4932 // `csharp_bool_terminal_kinds!()` nor the wrapper peel, so every
4933 // spelling below scored **zero** conditions against a cyclomatic
4934 // decision of one, while the bare `b` control scored one. In a
4935 // codebase with nullable reference types enabled the suffix is
4936 // everywhere, so the gap is not an exotic corner.
4937 //
4938 // Both walker paths, per `.claude/rules/grammar-dispatch.md` §11:
4939 // `p*` go through the `if` predicate slot
4940 // (`csharp_count_condition`) and `c*` through the `&&` chain slot
4941 // (`csharp_count_unary_conditions`), which reach
4942 // `csharp_inspect_container` independently. A fixture of only one
4943 // reads correct with the other path dead.
4944 //
4945 // The controls are load-bearing in both directions. `p` / `c` pin
4946 // what the wrapper must score, so a regression fails on the pair
4947 // rather than on an absolute number; and the cyclomatic column pins
4948 // that only `conditions` moved — the suffix is not a decision in any
4949 // language, so a fix that moved both together would be a different
4950 // bug.
4951 #[cfg(feature = "csharp")]
4952 #[test]
4953 fn csharp_null_forgiving_operand_scores_like_its_operand() {
4954 let src = "class A {
4955 int p(bool b) { if (b) { return 1; } return 0; }
4956 int pn(bool b) { if (b!) { return 1; } return 0; }
4957 int pp(bool b) { if ((b!)) { return 1; } return 0; }
4958 int pd(bool b) { if (b!!) { return 1; } return 0; }
4959 int pb(bool b) { if (!b!) { return 1; } return 0; }
4960 int c(bool b) { if (b && b) { return 1; } return 0; }
4961 int cn(bool b) { if (b! && b) { return 1; } return 0; }
4962 int cp(bool b) { if ((b!) && b) { return 1; } return 0; }
4963 }";
4964 assert_csharp_fixture_spells(
4965 src,
4966 &[
4967 // Seven, not five: `b!!` is two nested nodes, and each
4968 // chain member carries one. Trimming a `!` out of any
4969 // member fails here by name instead of turning that
4970 // member into a silent copy of its control.
4971 (
4972 Csharp::PostfixUnaryExpression as u16,
4973 7,
4974 "the null-forgiving suffixes",
4975 ),
4976 // The `if (…)` parens are anonymous tokens rather than
4977 // nodes in this grammar, so these two are `pp`'s and
4978 // `cp`'s explicit ones — the wrappers that make the peel
4979 // chain rather than peel once.
4980 (
4981 Csharp::ParenthesizedExpression as u16,
4982 2,
4983 "the explicit parenthesised spellings",
4984 ),
4985 (Csharp::PrefixUnaryExpression as u16, 1, "`pb`'s `!`"),
4986 ],
4987 );
4988 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4989 assert_members_score(
4990 &space.spaces[0],
4991 &[
4992 ("p", 1, 2),
4993 ("pn", 1, 2),
4994 ("pp", 1, 2),
4995 ("pd", 1, 2),
4996 ("pb", 1, 2),
4997 ("c", 2, 3),
4998 ("cn", 2, 3),
4999 ("cp", 2, 3),
5000 ],
5001 );
5002 });
5003 }
5004
5005 // The null-forgiving operand is read by position, but from the end
5006 // the grammar cannot move: a `postfix_unary_expression` *starts* at
5007 // its operand, so `child(0)` is the operand whatever `extra` sits
5008 // between it and the operator. The operator is identified by
5009 // membership rather than by index for the same reason.
5010 //
5011 // The other two wrappers have no such luck and are pinned here as a
5012 // *measured* gap rather than left to be discovered: the C# grammar
5013 // gives `parenthesized_expression`, `prefix_unary_expression` and
5014 // `postfix_unary_expression` an empty `fields` map in
5015 // node-types.json, so the field read Kotlin's and Groovy's
5016 // equivalents use is unavailable and `child(1)` is a comment
5017 // whenever one is written there. That is #1455, which predates
5018 // #1463 and is a separate change; what this test adds is that the
5019 // new arm does not join it.
5020 #[cfg(feature = "csharp")]
5021 #[test]
5022 fn csharp_null_forgiving_operand_survives_an_interposed_comment() {
5023 let src = "class A {
5024 int p(bool b) { if (b!) { return 1; } return 0; }
5025 int pc(bool b) { if (b /*c*/ !) { return 1; } return 0; }
5026 int n(bool b) { if (!b) { return 1; } return 0; }
5027 int nc(bool b) { if (! /*c*/ b) { return 1; } return 0; }
5028 int r(bool b) { if ((b)) { return 1; } return 0; }
5029 int rc(bool b) { if (( /*c*/ b)) { return 1; } return 0; }
5030 }";
5031 assert_csharp_fixture_spells(
5032 src,
5033 &[
5034 (
5035 Csharp::Comment as u16,
5036 3,
5037 "the interposed comments — without them every row is its own control",
5038 ),
5039 (Csharp::PostfixUnaryExpression as u16, 2, "`p` and `pc`"),
5040 (Csharp::PrefixUnaryExpression as u16, 2, "`n` and `nc`"),
5041 (
5042 Csharp::ParenthesizedExpression as u16,
5043 2,
5044 "`r` and `rc` — the `if` parens are anonymous tokens",
5045 ),
5046 ],
5047 );
5048 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5049 assert_members_score(
5050 &space.spaces[0],
5051 &[
5052 ("p", 1, 2),
5053 // The row #1463 fixes and the only one of the three
5054 // pairs that agrees with its control.
5055 ("pc", 1, 2),
5056 ("n", 1, 2),
5057 // #1455: `child(1)` of the prefix wrapper is the
5058 // comment. Delete this expectation, not the row,
5059 // when that issue is fixed.
5060 ("nc", 0, 2),
5061 ("r", 1, 2),
5062 // #1455 again, through the paren wrapper.
5063 ("rc", 0, 2),
5064 ],
5065 );
5066 });
5067 }
5068
5069 // The statement `switch` reaches the same `when_clause` kind through
5070 // a different parent — `switch_section` rather than
5071 // `switch_expression_arm` — which makes it an independent path in
5072 // the sense of `.claude/rules/grammar-dispatch.md` §11: the arms
5073 // themselves are counted by two different owners (a `Case` token
5074 // here, the `SwitchExpressionArm` node there), so a guard rule
5075 // written against one shape could be dead for the other and every
5076 // expression-form fixture would still read correct.
5077 #[cfg(feature = "csharp")]
5078 #[test]
5079 fn csharp_statement_switch_section_guard_counts() {
5080 let src = "class A {
5081 static bool IsEven(int x) { return true; }
5082 int guarded(int x) {
5083 switch (x) {
5084 case > 0 when IsEven(x): return 1;
5085 case > 0: return 2;
5086 default: return 3;
5087 }
5088 }
5089 int plain(int x) {
5090 switch (x) {
5091 case > 2: return 1;
5092 case > 0: return 2;
5093 default: return 3;
5094 }
5095 }
5096 }";
5097 assert_csharp_fixture_spells(
5098 src,
5099 &[
5100 (Csharp::WhenClause as u16, 1, "the `case … when` guard"),
5101 (Csharp::SwitchSection as u16, 6, "switch sections"),
5102 (
5103 Csharp::SwitchExpressionArm as u16,
5104 0,
5105 "expression arms (this fixture is the statement form)",
5106 ),
5107 ],
5108 );
5109 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5110 assert_members_score(
5111 &space.spaces[0],
5112 &[("IsEven", 0, 1), ("guarded", 3, 4), ("plain", 2, 3)],
5113 );
5114 });
5115 }
5116
5117 // The sibling #1422's issue body does not mention, measured to have
5118 // the identical defect: `catch (E e) when (…)` is a second guard
5119 // spelling on its own `catch_filter_clause` kind, and it scored
5120 // conditions 3 / 2 / 2 for the comparison / call spellings and the
5121 // unfiltered control while `cyclomatic()` sat at 2 for all three.
5122 //
5123 // ABC runs one above `cyclomatic() - 1` on every row, guarded and
5124 // unguarded alike, and that offset is not this fix's: C# ABC counts
5125 // the `Try` token as a condition (Fitzpatrick Rule 5, the same
5126 // family as `else`) where cyclomatic counts only the `catch`. The
5127 // guard moves both tiers by exactly one, which is what `none`
5128 // anchors — the offset is a constant here, not something a guard
5129 // introduces.
5130 //
5131 // `paren` carries the *double* parenthesis on purpose. The grammar
5132 // spells `catch_filter_clause`'s own parentheses as anonymous tokens
5133 // the way `if_statement` does, so `when (IsEven(x))` hands the slot a
5134 // bare `invocation_expression` and never reaches the
5135 // `CatchFilterClause` seed in `csharp_inspect_container`; only a
5136 // second pair of parentheses produces a `parenthesized_expression`
5137 // there.
5138 #[cfg(feature = "csharp")]
5139 #[test]
5140 fn csharp_catch_filter_guard_scores_one_condition_however_spelled() {
5141 let src = "class A {
5142 static bool IsEven(int x) { return true; }
5143 int tok(int x) { try { return 1; } catch (System.Exception e) when (x % 2 == 0) { return 2; } }
5144 int cmp(int x) { try { return 1; } catch (System.Exception e) when (x > 0) { return 2; } }
5145 int call(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5146 int paren(int x) { try { return 1; } catch (System.Exception e) when ((IsEven(x))) { return 2; } }
5147 int none(int x) { try { return 1; } catch (System.Exception e) { return 2; } }
5148 }";
5149 assert_csharp_fixture_spells(
5150 src,
5151 &[
5152 (
5153 Csharp::CatchFilterClause as u16,
5154 4,
5155 "`catch … when` filters",
5156 ),
5157 (Csharp::CatchClause as u16, 5, "catch clauses"),
5158 (Csharp::WhenClause as u16, 0, "switch guards (none here)"),
5159 (
5160 Csharp::ParenthesizedExpression as u16,
5161 1,
5162 "`paren`'s inner parentheses",
5163 ),
5164 ],
5165 );
5166 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5167 assert_members_score(
5168 &space.spaces[0],
5169 &[
5170 ("IsEven", 0, 1),
5171 ("tok", 3, 3),
5172 ("cmp", 3, 3),
5173 ("call", 3, 3),
5174 ("paren", 3, 3),
5175 ("none", 2, 2),
5176 ],
5177 );
5178 });
5179 }
5180
5181 // A guarded discard is where the two rules meet. `_ when g =>` is
5182 // already exempt from the `default:` exclusion —
5183 // `csharp_switch_expression_arm_is_bare_discard` looks for a
5184 // `when_clause` for exactly this reason — so after #1422 it scores
5185 // twice: once as an arm that is no longer unconditional, once for
5186 // the guard that makes it conditional. `bare` is the control that
5187 // keeps the exclusion itself pinned: drop the guard and the arm goes
5188 // back to costing nothing.
5189 #[cfg(feature = "csharp")]
5190 #[test]
5191 fn csharp_guarded_discard_arm_scores_arm_and_guard() {
5192 let src = "class A {
5193 static bool IsEven(int x) { return true; }
5194 int guarded(int x) => x switch { > 0 => 1, _ when IsEven(x) => 2, _ => 3 };
5195 int bare(int x) => x switch { > 0 => 1, _ => 3 };
5196 }";
5197 assert_csharp_fixture_spells(
5198 src,
5199 &[
5200 (Csharp::WhenClause as u16, 1, "the guarded discard"),
5201 (Csharp::Discard as u16, 3, "discard patterns"),
5202 ],
5203 );
5204 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5205 assert_members_score(
5206 &space.spaces[0],
5207 &[("IsEven", 0, 1), ("guarded", 3, 4), ("bare", 1, 2)],
5208 );
5209 });
5210 }
5211
5212 // A comment inside the guard must not cost it its condition.
5213 // tree-sitter `extra`s are *named* nodes, so `comment` can be the
5214 // first named child of either guard clause — and a slot that read
5215 // only the first named child scored `when /*c*/ IsEven(x)` as 1
5216 // where `when IsEven(x)` scores 2, silently reinstating the very
5217 // spelling-dependence #1422 removes and breaking the §8 parity the
5218 // fix establishes. Found in review of #1422 before it shipped.
5219 //
5220 // This is the C# instance of the class #1181 fixed for the ternary,
5221 // where a comment between a token and its operand shifted every
5222 // positional read. Each commented member is asserted against the
5223 // same literal pair as its uncommented twin in the same table, so a
5224 // regression that moved only the commented spelling fails here; the
5225 // literals themselves are pinned by
5226 // `csharp_switch_arm_guard_scores_one_condition_however_spelled` and
5227 // its catch-filter sibling.
5228 #[cfg(feature = "csharp")]
5229 #[test]
5230 fn csharp_guard_keeps_its_condition_across_a_comment() {
5231 let src = "class A {
5232 static bool IsEven(int x) => true;
5233 int plain(int x) => x switch { > 0 when IsEven(x) => 1, _ => 0 };
5234 int cmt(int x) => x switch { > 0 when /*c*/ IsEven(x) => 1, _ => 0 };
5235 int cplain(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5236 int ccmt(int x) { try { return 1; } catch (System.Exception e) when (/*c*/ IsEven(x)) { return 2; } }
5237 }";
5238 assert_csharp_fixture_spells(
5239 src,
5240 &[
5241 (Csharp::Comment as u16, 2, "the in-guard comments"),
5242 (Csharp::WhenClause as u16, 2, "switch guards"),
5243 (Csharp::CatchFilterClause as u16, 2, "catch filters"),
5244 ],
5245 );
5246 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5247 assert_members_score(
5248 &space.spaces[0],
5249 &[
5250 ("IsEven", 0, 1),
5251 ("plain", 2, 3),
5252 ("cmt", 2, 3),
5253 ("cplain", 3, 3),
5254 ("ccmt", 3, 3),
5255 ],
5256 );
5257 });
5258 }
5259
5260 // Why the guard is a condition *slot* and not a flat +1: a compound
5261 // guard keeps its sub-structure. `when a > 1 && b < 2` scores the
5262 // two comparisons through the token arm and nothing extra from the
5263 // slot, because a `binary_expression` is not one of
5264 // `csharp_bool_terminal_kinds!()` — so it reads one above `single`'s
5265 // lone comparison rather than collapsing to the same number, and
5266 // cyclomatic agrees because it counts the `&&`.
5267 #[cfg(feature = "csharp")]
5268 #[test]
5269 fn csharp_compound_guard_keeps_its_sub_structure() {
5270 let src = "class A {
5271 int compound(int a, int b) => a switch { > 0 when a > 1 && b < 2 => 1, _ => 0 };
5272 int single(int a, int b) => a switch { > 0 when a > 1 => 1, _ => 0 };
5273 }";
5274 assert_csharp_fixture_spells(
5275 src,
5276 &[
5277 (Csharp::WhenClause as u16, 2, "`when` guards"),
5278 (Csharp::AMPAMP as u16, 1, "`compound`'s `&&`"),
5279 (Csharp::LT as u16, 1, "`compound`'s `<`"),
5280 ],
5281 );
5282 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5283 assert_members_score(&space.spaces[0], &[("compound", 3, 4), ("single", 2, 3)]);
5284 });
5285 }
5286
5287 // #1275: tree-sitter-c-sharp spells `int?`, `where T : class?`, the
5288 // ternary and `a?.b` with one and the same bare `?` token, so the
5289 // unguarded `QMARK` arm scored the two type-syntax forms as
5290 // decisions.
5291 //
5292 // The fixture carries two `nullable_type` `?`, one
5293 // `type_parameter_constraint` `?`, and one real ternary over a real
5294 // `>` comparison, so every way of getting the gate wrong lands on
5295 // its own number: 5 pre-fix, 3 if only `NullableType` is denied, 4
5296 // if only `TypeParameterConstraint` is, 1 if the gate swallows the
5297 // genuine ternary, 0 if the fixture stops parsing. Asserting 0 on a
5298 // nullable-only body would have been vacuous — an unparsable file
5299 // scores 0 too.
5300 #[cfg(feature = "csharp")]
5301 #[test]
5302 fn csharp_nullable_type_syntax_is_not_a_condition() {
5303 check_metrics::<CsharpParser>(
5304 "class A {
5305 int M<T>(int? p, int a, int b) where T : class? {
5306 int? q = null;
5307 return a > b ? 1 : 2;
5308 }
5309 }",
5310 "foo.cs",
5311 |metric| {
5312 assert_eq!(metric.abc.conditions_sum(), 2);
5313 },
5314 );
5315 }
5316
5317 // The polarity pin for #1275's C# half. `csharp_count_token_condition`
5318 // denies the two type-syntax parents rather than allowing the two
5319 // decision-bearing ones, so that `a?.b` and `a?[0]` keep counting by
5320 // construction: both spell their operator as the same bare `?` under
5321 // `conditional_access_expression`, and C# cyclomatic counts that node
5322 // (`safe_navigation_chain_parity` pins C# at one decision per
5323 // operator alongside every other safe-navigation language).
5324 //
5325 // This test is what stops a later "make all three languages
5326 // consistent" pass from flipping C# to a `ConditionalExpression`
5327 // allowlist: that would silently drop both `?` counts here while
5328 // every other ABC test still passed. The third condition is the
5329 // `??`, which #1459 added to close the gap this comment used to
5330 // record as out of scope for #1275 — so a flipped `QMARK` polarity
5331 // now reads 1 rather than 0, and the discrimination is unchanged.
5332 #[cfg(feature = "csharp")]
5333 #[test]
5334 fn csharp_conditional_access_still_counts_as_a_condition() {
5335 let src = "class A {
5336 object M(string s, int[] xs) {
5337 return s?.Length ?? xs?[0];
5338 }
5339 }";
5340 // The total is now fed by two token arms rather than one, so it
5341 // carries the per-source claim only alongside these counts: 3 is
5342 // two bare `?` plus one `??`, and without them a `QMARK` arm
5343 // that had stopped counting `a?[0]` while `??` gained a second
5344 // count elsewhere would still read 3.
5345 assert_csharp_fixture_spells(
5346 src,
5347 &[
5348 (Csharp::QMARK as u16, 2, "the `?.` and `?[` operators"),
5349 (Csharp::QMARKQMARK as u16, 1, "the `??`"),
5350 ],
5351 );
5352 check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
5353 assert_eq!(metric.abc.conditions_sum(), 3);
5354 });
5355 }
5356
5357 // The `QMARKQMARK` arm away from any guard, where nothing else can
5358 // supply the count (#1459). `coalesce`'s body holds no comparison,
5359 // no call and no control-flow keyword, so its one condition is the
5360 // `??` and nothing else; it read 0 before the arm while C#
5361 // cyclomatic scored the decision, which is the direction ABC is not
5362 // allowed to sit on.
5363 //
5364 // `plain` is the control that keeps this from being an assertion
5365 // about `return` or about the method shape: same body without the
5366 // operator, 0 conditions and 1 decision.
5367 #[cfg(feature = "csharp")]
5368 #[test]
5369 fn csharp_null_coalescing_is_a_condition() {
5370 let src = "class A {
5371 int coalesce(int? x) { return x ?? 0; }
5372 int plain(int x) { return x; }
5373 }";
5374 assert_csharp_fixture_spells(src, &[(Csharp::QMARKQMARK as u16, 1, "the `??`")]);
5375 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5376 assert_members_score(&space.spaces[0], &[("coalesce", 1, 2), ("plain", 0, 1)]);
5377 });
5378 }
5379
5380 // The other direction of #1275's C# gate, isolated: narrowing
5381 // `QMARK` must not swallow a real ternary. The fixture carries no
5382 // comparison operator at all, so the ternary is the only thing that
5383 // can put the count above the walker's own contribution — in the
5384 // test above, a surviving `>` would have masked an over-suppressed
5385 // `?` at 1 instead of 0 (grammar-dispatch §11). A nullable
5386 // parameter keeps the denied production live in the same parse, so
5387 // one fixture proves both directions: 3 pre-fix, 2 after, 1 if the
5388 // allowlist/denylist is aimed at `ConditionalExpression`.
5389 //
5390 // The 2 is the `?` token plus the ternary's condition slot, which
5391 // `csharp_walk_for_conditions` counts as a Fitzpatrick unary
5392 // condition. That is why this is not written as an
5393 // `assert_deepest_conditions_match_cyclomatic` parity pin the way
5394 // the Java `<` / `>` sibling is: C# cyclomatic scores this method 1
5395 // decision, and the divergence is the pre-existing unary-condition
5396 // rule, not the `?` gate.
5397 #[cfg(feature = "csharp")]
5398 #[test]
5399 fn csharp_ternary_still_counts_alongside_nullable_types() {
5400 check_metrics::<CsharpParser>(
5401 "class A {
5402 int M(int? n, bool c) {
5403 return c ? 1 : 2;
5404 }
5405 }",
5406 "foo.cs",
5407 |metric| {
5408 assert_eq!(metric.abc.conditions_sum(), 2);
5409 },
5410 );
5411 }
5412
5413 #[cfg(feature = "csharp")]
5414 #[test]
5415 fn csharp_aliased_invocation_expression_branches() {
5416 // Regression for issue #94 (lesson #2): the C# grammar emits three
5417 // aliased `kind_id`s for `invocation_expression`. Code that matches
5418 // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
5419 // branches whenever the AST emits an aliased variant. The three
5420 // method calls live in `M`, so the per-method maximum (visible at
5421 // the unit-space aggregate as `branches_max`) must be 3.
5422 check_metrics::<CsharpParser>(
5423 "class A {
5424 void M() {
5425 System.Console.WriteLine(1);
5426 System.Console.WriteLine(2);
5427 System.Console.WriteLine(3);
5428 }
5429 }",
5430 "foo.cs",
5431 |metric| {
5432 assert_eq!(metric.abc.branches_max(), 3);
5433 assert_eq!(metric.abc.conditions_max(), 0);
5434 },
5435 );
5436 }
5437
5438 #[cfg(feature = "php")]
5439 #[test]
5440 fn php_zero_abc() {
5441 check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
5442 assert_eq!(metric.abc.assignments_sum(), 0);
5443 assert_eq!(metric.abc.branches_sum(), 0);
5444 assert_eq!(metric.abc.conditions_sum(), 0);
5445 insta::assert_json_snapshot!(metric.abc);
5446 });
5447 }
5448
5449 #[cfg(feature = "php")]
5450 #[test]
5451 fn php_simple_assignment() {
5452 check_metrics::<PhpParser>(
5453 "<?php
5454function f(): void {
5455 $a = 1;
5456 $b = 2;
5457}",
5458 "foo.php",
5459 |metric| insta::assert_json_snapshot!(metric.abc),
5460 );
5461 }
5462
5463 #[cfg(feature = "php")]
5464 #[test]
5465 fn php_augmented_assignment() {
5466 check_metrics::<PhpParser>(
5467 "<?php
5468function f(int $x): int {
5469 $a = 0;
5470 $a += $x;
5471 $a -= 1;
5472 $a *= 2;
5473 return $a;
5474}",
5475 "foo.php",
5476 |metric| insta::assert_json_snapshot!(metric.abc),
5477 );
5478 }
5479
5480 #[cfg(feature = "php")]
5481 #[test]
5482 fn php_const_excluded() {
5483 // Constant declarations and enum cases are NOT counted as
5484 // assignments — they declare immutable values.
5485 check_metrics::<PhpParser>(
5486 "<?php
5487class A {
5488 const PI = 3.14;
5489 const E = 2.71;
5490}
5491enum Color {
5492 case Red;
5493 case Green;
5494}",
5495 "foo.php",
5496 |metric| insta::assert_json_snapshot!(metric.abc),
5497 );
5498 }
5499
5500 #[cfg(feature = "php")]
5501 #[test]
5502 fn php_function_call() {
5503 check_metrics::<PhpParser>(
5504 "<?php
5505function f(): void {
5506 foo();
5507 bar(1, 2);
5508}",
5509 "foo.php",
5510 |metric| insta::assert_json_snapshot!(metric.abc),
5511 );
5512 }
5513
5514 #[cfg(feature = "php")]
5515 #[test]
5516 fn php_method_call() {
5517 check_metrics::<PhpParser>(
5518 "<?php
5519function f($obj): void {
5520 $obj->m1();
5521 $obj->m2(1);
5522}",
5523 "foo.php",
5524 |metric| insta::assert_json_snapshot!(metric.abc),
5525 );
5526 }
5527
5528 #[cfg(feature = "php")]
5529 #[test]
5530 fn php_static_call() {
5531 check_metrics::<PhpParser>(
5532 "<?php
5533function f(): void {
5534 Foo::bar();
5535 Foo::baz(1);
5536}",
5537 "foo.php",
5538 |metric| insta::assert_json_snapshot!(metric.abc),
5539 );
5540 }
5541
5542 #[cfg(feature = "php")]
5543 #[test]
5544 fn php_nullsafe_call() {
5545 check_metrics::<PhpParser>(
5546 "<?php
5547function f($obj): void {
5548 $obj?->m1();
5549 $obj?->m2(1);
5550}",
5551 "foo.php",
5552 |metric| insta::assert_json_snapshot!(metric.abc),
5553 );
5554 }
5555
5556 #[cfg(feature = "php")]
5557 #[test]
5558 fn php_object_creation() {
5559 check_metrics::<PhpParser>(
5560 "<?php
5561function f(): void {
5562 new Foo();
5563 new Bar(1);
5564}",
5565 "foo.php",
5566 |metric| insta::assert_json_snapshot!(metric.abc),
5567 );
5568 }
5569
5570 #[cfg(feature = "php")]
5571 #[test]
5572 fn php_comparison_eq() {
5573 check_metrics::<PhpParser>(
5574 "<?php
5575function f(int $a, int $b): bool {
5576 return $a == $b || $a != $b;
5577}",
5578 "foo.php",
5579 |metric| insta::assert_json_snapshot!(metric.abc),
5580 );
5581 }
5582
5583 #[cfg(feature = "php")]
5584 #[test]
5585 fn php_comparison_strict() {
5586 check_metrics::<PhpParser>(
5587 "<?php
5588function f(int $a, int $b): bool {
5589 return $a === $b || $a !== $b;
5590}",
5591 "foo.php",
5592 |metric| insta::assert_json_snapshot!(metric.abc),
5593 );
5594 }
5595
5596 #[cfg(feature = "php")]
5597 #[test]
5598 fn php_spaceship() {
5599 check_metrics::<PhpParser>(
5600 "<?php
5601function f(int $a, int $b): int {
5602 return $a <=> $b;
5603}",
5604 "foo.php",
5605 |metric| insta::assert_json_snapshot!(metric.abc),
5606 );
5607 }
5608
5609 #[cfg(feature = "php")]
5610 #[test]
5611 fn php_instanceof() {
5612 check_metrics::<PhpParser>(
5613 "<?php
5614function f($x): bool {
5615 return $x instanceof Foo;
5616}",
5617 "foo.php",
5618 |metric| insta::assert_json_snapshot!(metric.abc),
5619 );
5620 }
5621
5622 #[cfg(feature = "php")]
5623 #[test]
5624 fn php_complex_function() {
5625 // One snippet exercising A, B, C buckets together.
5626 check_metrics::<PhpParser>(
5627 "<?php
5628function f(int $a, int $b): int {
5629 $sum = $a + $b;
5630 $prod = $a * $b;
5631 if ($sum > 0 && $prod === 0) {
5632 return foo($sum);
5633 }
5634 return bar()->double();
5635}",
5636 "foo.php",
5637 |metric| insta::assert_json_snapshot!(metric.abc),
5638 );
5639 }
5640
5641 #[cfg(feature = "php")]
5642 #[test]
5643 fn php_if_boolean_literal_condition() {
5644 check_metrics::<PhpParser>(
5645 "<?php\n\
5646 function f() {\n\
5647 \x20 if (true) {} // +1c\n\
5648 \x20 if (!false) {} // +1c\n\
5649 \x20 while (true) {} // +1c\n\
5650 \x20 do {} while (false); // +1c\n\
5651 }\n",
5652 "foo.php",
5653 |metric| {
5654 assert_eq!(metric.abc.conditions_sum(), 4);
5655 insta::assert_json_snapshot!(metric.abc);
5656 },
5657 );
5658 }
5659
5660 #[cfg(feature = "php")]
5661 #[test]
5662 fn php_methods_arguments_with_conditions() {
5663 check_metrics::<PhpParser>(
5664 "<?php\n\
5665 function f($a, $b) {\n\
5666 \x20 m($a, $b); // +1b\n\
5667 \x20 m(!$a, !$b); // +1b +2c\n\
5668 }\n",
5669 "foo.php",
5670 |metric| {
5671 assert_eq!(metric.abc.branches_sum(), 2);
5672 assert_eq!(metric.abc.conditions_sum(), 2);
5673 insta::assert_json_snapshot!(metric.abc);
5674 },
5675 );
5676 }
5677
5678 #[cfg(feature = "php")]
5679 #[test]
5680 fn php_return_with_conditions() {
5681 check_metrics::<PhpParser>(
5682 "<?php\n\
5683 function m1($z) { return !($z >= 0); }\n\
5684 function m2($x) { return (((!$x))); }\n\
5685 function m3($x, $y) { return $x && $y; }\n",
5686 "foo.php",
5687 |metric| {
5688 // m1: `>=` (1). m2: walker unwraps to $x (1).
5689 // m3: `&&` walker counts both (2). Sum: 4.
5690 assert_eq!(metric.abc.conditions_sum(), 4);
5691 insta::assert_json_snapshot!(metric.abc);
5692 },
5693 );
5694 }
5695
5696 #[cfg(feature = "php")]
5697 #[test]
5698 fn php_name2_hidden_rule_drift_marker() {
5699 // Drift marker (findings.md round-2 #3): `Php::Name2` maps
5700 // to the hidden grammar rule `_name`. At the pinned
5701 // tree-sitter-php version it is never emitted as a concrete
5702 // node — the visible `Name` (= 1) carries every name.
5703 // We list `Name2` defensively in `php_bool_terminal_kinds!()`
5704 // (lesson 34); if a future grammar bump promotes `_name`
5705 // to a visible rule, this assertion fails loudly.
5706 let src = "<?php\nfunction f($x) { if ($x) { foo($x); } }\n";
5707 let parser = PhpParser::new(
5708 src.as_bytes().to_vec(),
5709 &std::path::PathBuf::from("foo.php"),
5710 None,
5711 );
5712 assert!(!ast_has_kind_id(&parser, Php::Name2 as u16));
5713 }
5714
5715 #[cfg(feature = "php")]
5716 #[test]
5717 fn php_scoped_property_access_condition_counts() {
5718 // Regression for findings.md round-2 #1 (PHP):
5719 // `if (Config::$enabled) {}` parses with
5720 // `scoped_property_access_expression` as the condition
5721 // node (kind_id 333 at the pinned grammar version — the
5722 // `*2` alias). Pre-fix, neither `ScopedPropertyAccessExpression`
5723 // nor its alias was in `php_bool_terminal_kinds!()`. The
5724 // walker reached the access node, found it non-terminal,
5725 // and broke. Mirrors C#'s `MemberAccessExpression` rule
5726 // (lesson 19, #372).
5727 check_metrics::<PhpParser>(
5728 "<?php\n\
5729 class Config { public static $enabled = true; }\n\
5730 function f() { if (Config::$enabled) { } }\n",
5731 "foo.php",
5732 |metric| {
5733 assert_eq!(metric.abc.conditions_sum(), 1);
5734 insta::assert_json_snapshot!(metric.abc);
5735 },
5736 );
5737 }
5738
5739 #[cfg(feature = "php")]
5740 #[test]
5741 fn php_named_argument_unary_conditional_counts() {
5742 // Regression for the code-review finding: PHP 8 named-argument
5743 // syntax `m(name: !$a)` parses as `argument(name, ':',
5744 // unary_op_expression)`. Pre-fix, the count walker took
5745 // `argument.child(0)` (the name) and missed the value at the
5746 // last child. Now it picks the last named child as the value.
5747 check_metrics::<PhpParser>(
5748 "<?php\nfunction f($a) { m(name: !$a); }\n",
5749 "foo.php",
5750 |metric| {
5751 // 1 call (branch) + 1 unary-conditional named argument.
5752 assert_eq!(metric.abc.branches_sum(), 1);
5753 assert_eq!(metric.abc.conditions_sum(), 1);
5754 insta::assert_json_snapshot!(metric.abc);
5755 },
5756 );
5757 }
5758
5759 #[cfg(feature = "php")]
5760 #[test]
5761 fn php_low_precedence_keyword_logical_ops_trigger_walker() {
5762 // Regression: pre-fix, `$a or $b` reported 0 conditions
5763 // because the dispatcher only handled `AMPAMP|PIPEPIPE`,
5764 // skipping the PHP-specific `and` / `or` / `xor` keyword
5765 // forms even though they parse under the same
5766 // `binary_expression` shape.
5767 check_metrics::<PhpParser>(
5768 "<?php\n\
5769 function f($a, $b) {\n\
5770 \x20 return $a or $b;\n\
5771 }\n",
5772 "foo.php",
5773 |metric| {
5774 assert_eq!(metric.abc.conditions_sum(), 2);
5775 insta::assert_json_snapshot!(metric.abc);
5776 },
5777 );
5778 }
5779
5780 #[cfg(feature = "php")]
5781 #[test]
5782 fn php_if_multiple_conditions() {
5783 check_metrics::<PhpParser>(
5784 "<?php\n\
5785 function f($a, $b, $c, $d) {\n\
5786 \x20 if ($a || $b || $c || $d) {} // +4c\n\
5787 \x20 if ($a && $b && $c) {} // +3c\n\
5788 \x20 if (!$a && !$b) {} // +2c\n\
5789 }\n",
5790 "foo.php",
5791 |metric| {
5792 assert_eq!(metric.abc.conditions_sum(), 9);
5793 insta::assert_json_snapshot!(metric.abc);
5794 },
5795 );
5796 }
5797
5798 #[cfg(feature = "php")]
5799 #[test]
5800 fn php_while_and_do_while_conditions() {
5801 check_metrics::<PhpParser>(
5802 "<?php\n\
5803 function f($a, $b) {\n\
5804 \x20 while ($a || $b) {} // +2c\n\
5805 \x20 do {} while ($a && !$b); // +2c\n\
5806 }\n",
5807 "foo.php",
5808 |metric| {
5809 assert_eq!(metric.abc.conditions_sum(), 4);
5810 insta::assert_json_snapshot!(metric.abc);
5811 },
5812 );
5813 }
5814
5815 #[cfg(feature = "php")]
5816 #[test]
5817 fn php_short_circuit_with_boolean_literal_operand() {
5818 check_metrics::<PhpParser>(
5819 "<?php\nfunction f($a) { return $a && true; }\n",
5820 "foo.php",
5821 |metric| {
5822 assert_eq!(metric.abc.conditions_sum(), 2);
5823 insta::assert_json_snapshot!(metric.abc);
5824 },
5825 );
5826 }
5827
5828 // Issue #1102, PHP half. See
5829 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
5830 // rule. PHP's ABC dispatcher has no `?`-token arm — the grammar
5831 // does emit the token, but the `conditional_expression` node is
5832 // what carries the tally's +1 — so the arm keeps that increment and
5833 // adds the operand slots.
5834 #[cfg(feature = "php")]
5835 #[test]
5836 fn php_ternary_operand_slots_count_as_unary_conditions() {
5837 // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
5838 check_metrics::<PhpParser>(
5839 "<?php\nfunction f() { $x = $a ? !$b : !$c; }\n",
5840 "foo.php",
5841 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
5842 );
5843 // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
5844 // the fix.
5845 check_metrics::<PhpParser>(
5846 "<?php\nfunction f() { $x = ($a > 0) ? $b : -$b; }\n",
5847 "foo.php",
5848 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
5849 );
5850 // Nested (PHP 8 requires the inner ternary parenthesised): two
5851 // ternary nodes plus the two bare-variable conditions = 4.
5852 check_metrics::<PhpParser>(
5853 "<?php\nfunction f() { $x = $a ? ($b ? $c : $d) : $e; }\n",
5854 "foo.php",
5855 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
5856 );
5857 // A negated condition is the only input reaching the walker's
5858 // `else` fallback — see the C++ sibling for why. ternary (1) +
5859 // `!$a` (1) = 2.
5860 check_metrics::<PhpParser>(
5861 "<?php\nfunction f() { $x = !$a ? $b : $c; }\n",
5862 "foo.php",
5863 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
5864 );
5865 }
5866
5867 // PHP's short ternary `$a ?: $b` elides the consequence, which the
5868 // grammar names `body` (not `consequence`) and marks optional. The
5869 // alternative lands at child(3), so addressing the slot by field
5870 // name rather than a fixed child(4) is what keeps `!$b` counted.
5871 #[cfg(feature = "php")]
5872 #[test]
5873 fn php_elided_ternary_body_still_walks_the_alternative() {
5874 // ternary (1) + condition `$a` (1) + `!$b` (1) = 3.
5875 check_metrics::<PhpParser>(
5876 "<?php\nfunction f() { $x = $a ?: !$b; }\n",
5877 "foo.php",
5878 |metric| assert_eq!(metric.abc.conditions_sum(), 3),
5879 );
5880 }
5881
5882 // Issue #1276, PHP half. The `for` header's condition slot was the
5883 // one condition slot `PhpCode::compute` never dispatched, so a
5884 // bare / negated / parenthesised loop condition scored zero while
5885 // the identical predicate in an `if` header scored one. Each
5886 // fixture below is a shape only the new arm can classify — a
5887 // comparison-shaped condition proves nothing here, because the `<`
5888 // token arm counts it either way (grammar-dispatch §11).
5889 #[cfg(feature = "php")]
5890 #[test]
5891 fn php_for_condition_slot_counts_unary_conditions() {
5892 // Bare variable: the whole condition, no operator token.
5893 check_metrics::<PhpParser>(
5894 "<?php\nfunction f($a) { for (; $a; ) {} }\n",
5895 "foo.php",
5896 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
5897 );
5898 // Negation: reaches the terminal through
5899 // `php_inspect_container`'s `!` unwrap.
5900 check_metrics::<PhpParser>(
5901 "<?php\nfunction f($a) { for (; !$a; ) {} }\n",
5902 "foo.php",
5903 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
5904 );
5905 // Parentheses: counts only because the `for_statement` parent
5906 // seeds `has_boolean_content`, the seed #1276 found dead.
5907 check_metrics::<PhpParser>(
5908 "<?php\nfunction f($a) { for (; ($a); ) {} }\n",
5909 "foo.php",
5910 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
5911 );
5912 // No-double-count pin: the `<` token arm already counted this
5913 // shape before the fix, and the walker must not add a second.
5914 // Two assignments (`$i = 0`, `$i++`) confirm the header parsed
5915 // as the three-clause form rather than degenerating.
5916 check_metrics::<PhpParser>(
5917 "<?php\nfunction f($n) { for ($i = 0; $i < $n; $i++) {} }\n",
5918 "foo.php",
5919 |metric| {
5920 assert_eq!(metric.abc.conditions_sum(), 1);
5921 assert_eq!(metric.abc.assignments_sum(), 2);
5922 },
5923 );
5924 // Empty condition: no `condition` field, no decision, zero.
5925 check_metrics::<PhpParser>(
5926 "<?php\nfunction f() { for (;;) { break; } }\n",
5927 "foo.php",
5928 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
5929 );
5930 }
5931
5932 // --- Kotlin ABC tests -------------------------------------------------
5933
5934 #[cfg(feature = "kotlin")]
5935 #[test]
5936 fn kotlin_empty_class() {
5937 check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
5938 assert_eq!(metric.abc.assignments_sum(), 0);
5939 assert_eq!(metric.abc.branches_sum(), 0);
5940 assert_eq!(metric.abc.conditions_sum(), 0);
5941 insta::assert_json_snapshot!(metric.abc);
5942 });
5943 }
5944
5945 #[cfg(feature = "kotlin")]
5946 #[test]
5947 fn kotlin_val_declarations_are_not_assignments() {
5948 // `val` introduces an immutable binding — the `=` initialising it
5949 // is not an assignment in the ABC sense.
5950 check_metrics::<KotlinParser>(
5951 "class C {
5952 val a: Int = 1
5953 val b: Int = 2
5954 val c: Int = 3
5955 }",
5956 "foo.kt",
5957 |metric| {
5958 assert_eq!(metric.abc.assignments_sum(), 0);
5959 assert_eq!(metric.abc.branches_sum(), 0);
5960 insta::assert_json_snapshot!(metric.abc);
5961 },
5962 );
5963 }
5964
5965 #[cfg(feature = "kotlin")]
5966 #[test]
5967 fn kotlin_var_declarations_count_assignment() {
5968 // `var` initialisers count as assignments (mutable binding).
5969 check_metrics::<KotlinParser>(
5970 "class C {
5971 var a: Int = 1
5972 var b: Int = 2
5973 }",
5974 "foo.kt",
5975 |metric| {
5976 assert_eq!(metric.abc.assignments_sum(), 2);
5977 insta::assert_json_snapshot!(metric.abc);
5978 },
5979 );
5980 }
5981
5982 #[cfg(feature = "kotlin")]
5983 #[test]
5984 fn kotlin_val_then_assignments_count() {
5985 // Regression for #455: a `val` initialiser must not suppress the
5986 // standalone `=` assignments that follow it. tree-sitter-kotlin
5987 // emits no `SEMI` token (even for explicit semicolons), so the
5988 // pre-#455 `SEMI`-cleared declaration stack never cleared and the
5989 // immutable-`val` sentinel leaked, reporting A=0 here.
5990 check_metrics::<KotlinParser>(
5991 "fun f() {
5992 val cfg = 0
5993 a = 1
5994 b = 2
5995 }",
5996 "foo.kt",
5997 |metric| {
5998 // val initialiser suppressed; `a = 1` and `b = 2` count.
5999 assert_eq!(metric.abc.assignments_sum(), 2);
6000 insta::assert_json_snapshot!(metric.abc);
6001 },
6002 );
6003 }
6004
6005 #[cfg(feature = "kotlin")]
6006 #[test]
6007 fn kotlin_var_then_assignments_count() {
6008 // Companion to the #455 regression: a `var` declaration leaves a
6009 // mutable-binding sentinel that *permits* the `=` — this path
6010 // accidentally masked the leak (its `Var` sentinel never suppressed
6011 // anything), so it must keep counting both the initialiser and the
6012 // following standalone assignments.
6013 check_metrics::<KotlinParser>(
6014 "fun f() {
6015 var cfg = 0
6016 a = 1
6017 b = 2
6018 }",
6019 "foo.kt",
6020 |metric| {
6021 // var initialiser (+1) plus `a = 1` and `b = 2` (+2).
6022 assert_eq!(metric.abc.assignments_sum(), 3);
6023 insta::assert_json_snapshot!(metric.abc);
6024 },
6025 );
6026 }
6027
6028 #[cfg(feature = "kotlin")]
6029 #[test]
6030 fn kotlin_augmented_assignments_count() {
6031 // Augmented operators (+=, -=, etc.) and ++/-- always count.
6032 check_metrics::<KotlinParser>(
6033 "fun m() {
6034 var x = 0
6035 x += 1
6036 x -= 2
6037 x *= 3
6038 x++
6039 --x
6040 }",
6041 "foo.kt",
6042 |metric| {
6043 // var declaration (var x = 0): +1
6044 // x += 1, x -= 2, x *= 3, x++, --x: +5
6045 assert_eq!(metric.abc.assignments_sum(), 6);
6046 insta::assert_json_snapshot!(metric.abc);
6047 },
6048 );
6049 }
6050
6051 #[cfg(feature = "kotlin")]
6052 #[test]
6053 fn kotlin_branches_call_expression() {
6054 check_metrics::<KotlinParser>(
6055 "fun m() {
6056 println(\"a\")
6057 println(\"b\")
6058 println(\"c\")
6059 }",
6060 "foo.kt",
6061 |metric| {
6062 assert_eq!(metric.abc.branches_sum(), 3);
6063 insta::assert_json_snapshot!(metric.abc);
6064 },
6065 );
6066 }
6067
6068 #[cfg(feature = "kotlin")]
6069 #[test]
6070 fn kotlin_object_construction_branch() {
6071 // Kotlin's object construction is just `Foo()` — a `CallExpression`.
6072 check_metrics::<KotlinParser>(
6073 "class P(val x: Int)
6074 fun m(): P = P(1)",
6075 "foo.kt",
6076 |metric| {
6077 assert_eq!(metric.abc.branches_sum(), 1);
6078 insta::assert_json_snapshot!(metric.abc);
6079 },
6080 );
6081 }
6082
6083 #[cfg(feature = "kotlin")]
6084 #[test]
6085 fn kotlin_comparisons_count_conditions() {
6086 check_metrics::<KotlinParser>(
6087 "fun m(a: Int, b: Int): Boolean {
6088 val r1 = a < b
6089 val r2 = a > b
6090 val r3 = a <= b
6091 val r4 = a >= b
6092 val r5 = a == b
6093 val r6 = a != b
6094 return r1 || r2 || r3 || r4 || r5 || r6
6095 }",
6096 "foo.kt",
6097 |metric| {
6098 // Six comparison operators in the `val` initialisers
6099 // (<, >, <=, >=, ==, !=) → 6, plus the six bare-identifier
6100 // operands of the `r1 || … || r6` return chain, each a
6101 // Fitzpatrick Rule 9 unary condition (issue #557) → 6.
6102 // Total 12. Before the Kotlin walker was wired the chain
6103 // operands were silently dropped and this read 6.
6104 assert_eq!(metric.abc.conditions_sum(), 12);
6105 insta::assert_json_snapshot!(metric.abc);
6106 },
6107 );
6108 }
6109
6110 #[cfg(feature = "kotlin")]
6111 #[test]
6112 fn kotlin_identity_equality_conditions() {
6113 // `===` / `!==` are referential equality in Kotlin; they count too.
6114 check_metrics::<KotlinParser>(
6115 "fun m(a: Any, b: Any): Boolean {
6116 return a === b || a !== b
6117 }",
6118 "foo.kt",
6119 |metric| {
6120 assert_eq!(metric.abc.conditions_sum(), 2);
6121 insta::assert_json_snapshot!(metric.abc);
6122 },
6123 );
6124 }
6125
6126 #[cfg(feature = "kotlin")]
6127 #[test]
6128 fn kotlin_else_branch_counts() {
6129 check_metrics::<KotlinParser>(
6130 "fun m(x: Int): Int {
6131 return if (x > 0) 1 else -1
6132 }",
6133 "foo.kt",
6134 |metric| {
6135 // condition: > (1) + else (1) = 2
6136 assert_eq!(metric.abc.conditions_sum(), 2);
6137 insta::assert_json_snapshot!(metric.abc);
6138 },
6139 );
6140 }
6141
6142 #[cfg(feature = "kotlin")]
6143 #[test]
6144 fn kotlin_when_entries_count() {
6145 check_metrics::<KotlinParser>(
6146 "fun m(x: Int): Int {
6147 return when (x) {
6148 1 -> 10
6149 2 -> 20
6150 else -> 0
6151 }
6152 }",
6153 "foo.kt",
6154 |metric| {
6155 // Non-`else` WhenEntry arms count; the `else ->` fallback
6156 // arm does not (issue #456). Two case arms + zero for the
6157 // `else` arm = 2.
6158 assert_eq!(metric.abc.conditions_sum(), 2);
6159 insta::assert_json_snapshot!(metric.abc);
6160 },
6161 );
6162 }
6163
6164 // Pins the `else ->` exclusion directly: a `when` whose only fallback
6165 // is `else ->` must not count that arm. Revert-verified — gating the
6166 // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this
6167 // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate.
6168 #[cfg(feature = "kotlin")]
6169 #[test]
6170 fn kotlin_when_else_not_a_condition() {
6171 check_metrics::<KotlinParser>(
6172 "fun m(x: Int): Int {
6173 return when (x) { 1 -> 10; 2 -> 20; else -> 0 }
6174 }",
6175 "foo.kt",
6176 |metric| {
6177 // case `1 ->` (+1) + case `2 ->` (+1) + `else ->` (+0) = 2.
6178 assert_eq!(metric.abc.conditions_sum(), 2);
6179 },
6180 );
6181 }
6182
6183 // The Kotlin binding of the two assertions every #1421 fixture needs,
6184 // coupled on purpose. A per-member score table is worth only as much
6185 // as the anchor proving the fixture still spells what it claims to
6186 // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
6187 // production line"), and the `when_subject` count is the anchor no
6188 // score can stand in for: a `when` that grew a subject scores its
6189 // entries the pre-#1421 way with every other row still satisfied.
6190 // Each fixture is one class, so the members are `spaces[0]`'s.
6191 #[cfg(feature = "kotlin")]
6192 #[track_caller]
6193 fn assert_kotlin_class_members(
6194 src: &str,
6195 kinds: &[(u16, usize, &str)],
6196 expected: &[(&str, u64, u64)],
6197 ) {
6198 assert_fixture_spells::<KotlinParser>(src, "foo.kt", kinds);
6199 check_func_space::<KotlinParser, _>(src, "foo.kt", |space| {
6200 assert_members_score(&space.spaces[0], expected);
6201 });
6202 }
6203
6204 // #1421's headline. A subject-less `when` arm's condition is an
6205 // ordinary boolean expression, so its comparison operator is already
6206 // an ABC condition through the token arms; the `WhenEntry` arm added
6207 // a second one on top, and `two` reported 4 against a decision count
6208 // of 2.
6209 //
6210 // `bare` is the control that isolates the defect rather than merely
6211 // observing it: same arm count, same entry shape, no comparison
6212 // operator anywhere — so it scored the right 2 before the fix and
6213 // after it. A fix that changed both members alike would be attacking
6214 // the entry count, which was never wrong.
6215 //
6216 // `ge` and `eq` exist because the issue named `<` and `>` only. All
6217 // six comparison spellings share the defect: `>=` and `==` reach
6218 // `stats.conditions` through the `LTEQ | GTEQ | EQEQ | …` arm rather
6219 // than the parent-gated `LT | GT` arm, and both scored 2 against a
6220 // decision count of 1.
6221 //
6222 // `iff` is the member that legitimately sits *above* its decision
6223 // count, and it is here so the table cannot be read as "§8 parity
6224 // everywhere". ABC counts the `else` (Fitzpatrick Rule 5) and
6225 // cyclomatic does not, so 2 conditions against 1 decision is correct
6226 // and unchanged by this fix.
6227 //
6228 // The `when_subject` anchor is the load-bearing one: the whole test
6229 // turns on these `when`s having no subject, and nothing in the
6230 // measured numbers would notice a fixture that grew one — a
6231 // subject-ful `when (x) { … }` scores its entries the old way and
6232 // `two` would read 4 again with every other row still satisfied.
6233 #[cfg(feature = "kotlin")]
6234 #[test]
6235 fn kotlin_subjectless_when_arm_counts_its_condition_once() {
6236 let src = "class K {
6237 fun f(): Boolean = true
6238 fun two(x: Int): Int = when { x > 5 -> 1; x < 0 -> 2; else -> 0 }
6239 fun bare(x: Boolean, y: Boolean): Int = when { x -> 1; y -> 2; else -> 0 }
6240 fun ge(x: Int): Int = when { x >= 5 -> 1; else -> 0 }
6241 fun eq(x: Int): Int = when { x == 5 -> 1; else -> 0 }
6242 fun andd(a: Int, b: Int): Int = when { a > 1 && b < 2 -> 1; else -> 0 }
6243 fun call(): Int = when { f() -> 1; else -> 0 }
6244 fun iff(x: Int): Int = if (x > 5) 1 else 0
6245 }";
6246 assert_kotlin_class_members(
6247 src,
6248 &[
6249 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6250 (Kotlin::WhenEntry as u16, 14, "`when` entries"),
6251 (
6252 Kotlin::BinaryExpression as u16,
6253 8,
6254 "comparison / chain expressions",
6255 ),
6256 (Kotlin::AMPAMP as u16, 1, "`andd`'s `&&`"),
6257 ],
6258 &[
6259 // (member, abc.conditions, cyclomatic)
6260 ("f", 0, 1),
6261 // Was 4: two entries, two comparisons, counted twice.
6262 ("two", 2, 3),
6263 // The no-comparison control — 2 before and after.
6264 ("bare", 2, 3),
6265 // Was 2 each: the `GTEQ` / `EQEQ` spellings.
6266 ("ge", 1, 2),
6267 ("eq", 1, 2),
6268 // Was 3. A compound condition keeps both of its
6269 // comparisons — suppressing the operators instead
6270 // would have collapsed this one to 1.
6271 ("andd", 2, 3),
6272 // The condition slot scores a call directly, so a
6273 // spelling with no operator in it still reads 1.
6274 ("call", 1, 2),
6275 // Above its decision count on purpose: ABC counts
6276 // the `else`, cyclomatic does not.
6277 ("iff", 2, 2),
6278 ],
6279 );
6280 }
6281
6282 // #1459, the regression #1421 left behind. Turning the `when`
6283 // entry's blanket count into a condition slot moved the payment from
6284 // the entry to the condition — so every shape the slot cannot
6285 // classify went from 1 to 0, silently. Two shapes did:
6286 //
6287 // - `a!!`. `unary_expression` is one kind for *both* Kotlin unary
6288 // spellings, and the slot routed it to the peel, so this reads as
6289 // handled at every call site. The peel then looked for the operand
6290 // at child(1) — right for the prefix `!x`, wrong for a postfix
6291 // `x!!`, whose child(1) is the `!!` token — and stopped.
6292 // - `a as Boolean`. `as_expression` was not a wrapper the peel knew
6293 // at all, so the slot fell off the end of its `else if`.
6294 //
6295 // The controls are `bare` / `cmp` / `call`, one per surviving path
6296 // (terminal, comparison token, invocation): a fix that moved the
6297 // entry count back would take those to 2 and fail here, which is
6298 // what distinguishes repairing the peel from reverting #1421.
6299 //
6300 // `safe` is the §5 row and the one measurement decided rather than
6301 // taste. `as?` is already a condition *token*
6302 // (`src/metrics/abc/kotlin.rs`), so a peel that treated `as` and
6303 // `as?` alike would score it 2 — the double count that arm's own
6304 // fix was avoiding. `kotlin_wrapper_operand` excludes `as?` for
6305 // that reason, and this member is what fails if the exclusion goes:
6306 // `(a as? Boolean)!!` is valid Kotlin whose peel reaches the cast
6307 // through two other wrappers, so it needs no invalid fixture to
6308 // discriminate.
6309 //
6310 // `iff` carries the same `a!!` through the `if` slot. That half is
6311 // not #1421 fallout — `if (a!!)` has read 0 since the slot landed in
6312 // #773 — but it is the same peel and the same edit, and a fix
6313 // narrowed to `when` entries would leave it at 0.
6314 //
6315 // Every member is valid Kotlin: `a!!` on a `Boolean?`, `as` on an
6316 // `Any`, and `(a as? Boolean)!!` back to a `Boolean`. The kind
6317 // anchors are what stop a later edit from trimming a spelling out
6318 // and turning its member into a silent copy of `bare`.
6319 #[cfg(feature = "kotlin")]
6320 #[test]
6321 fn kotlin_condition_slot_peels_null_assertions_and_casts() {
6322 let src = "class K {
6323 fun g(x: Boolean): Boolean = true
6324 fun bare(a: Boolean): Int = when { a -> 1; else -> 0 }
6325 fun cmp(a: Int): Int = when { a > 5 -> 1; else -> 0 }
6326 fun call(a: Boolean): Int = when { g(a) -> 1; else -> 0 }
6327 fun bang(a: Boolean?): Int = when { a!! -> 1; else -> 0 }
6328 fun paren(a: Boolean?): Int = when { (a!!) -> 1; else -> 0 }
6329 fun cast(a: Any): Int = when { a as Boolean -> 1; else -> 0 }
6330 fun chain(a: Any?): Int = when { a!! as Boolean -> 1; else -> 0 }
6331 fun safe(a: Any): Int = when { (a as? Boolean)!! -> 1; else -> 0 }
6332 fun cmt(a: Boolean): Int = when { ! /*c*/ a -> 1; else -> 0 }
6333 fun iff(a: Boolean?): Int { if (a!!) { return 1 }; return 0 }
6334 }";
6335 assert_kotlin_class_members(
6336 src,
6337 &[
6338 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6339 (
6340 Kotlin::BANGBANG as u16,
6341 5,
6342 "the `!!` null assertions of `bang` / `paren` / `chain` / `safe` / `iff`",
6343 ),
6344 (
6345 Kotlin::AsExpression as u16,
6346 3,
6347 "the casts of `cast` / `chain` / `safe`",
6348 ),
6349 (Kotlin::AsQMARK as u16, 1, "`safe`'s safe cast"),
6350 (
6351 Kotlin::BlockComment as u16,
6352 1,
6353 "`cmt`'s interposed comment, spaces and all",
6354 ),
6355 (
6356 Kotlin::ParenthesizedExpression as u16,
6357 2,
6358 "the parenthesised operands of `paren` / `safe`",
6359 ),
6360 ],
6361 &[
6362 // (member, abc.conditions, cyclomatic)
6363 ("g", 0, 1),
6364 // The three controls, unchanged by this fix.
6365 ("bare", 1, 2),
6366 ("cmp", 1, 2),
6367 ("call", 1, 2),
6368 // Each was 0: the peel stopped on the wrapper.
6369 ("bang", 1, 2),
6370 ("paren", 1, 2),
6371 ("cast", 1, 2),
6372 ("chain", 1, 2),
6373 // 1, not 2 — the `as?` token owns this one, and the
6374 // peel declines to charge for it a second time.
6375 ("safe", 1, 2),
6376 // The peel reads its operand through the `operator` /
6377 // `argument` fields, so an `extra` between the two no
6378 // longer displaces it. The spacing is load-bearing and
6379 // must not be tidied away: `!/*c*/a` unspaced parses
6380 // with the comment somewhere a `child(1)` read still
6381 // skips, so only the spaced spelling can tell the field
6382 // read from the positional one. Measured both ways.
6383 ("cmt", 1, 2),
6384 // The `if` slot, which has read 0 since #773.
6385 ("iff", 1, 2),
6386 ],
6387 );
6388 }
6389
6390 // The other half of #1421: a subject-ful entry must not move. Its
6391 // condition is a *pattern* matched against the subject, not an
6392 // independent boolean expression, so the implicit `subject ==
6393 // pattern` is the decision and the entry itself pays for it —
6394 // `range_test`, `type_test` and a bare constant carry no token the
6395 // comparison arms would count.
6396 //
6397 // `cmp` is the interesting row and the reason `nested` sits beside
6398 // it. `when (x) { y > 5 -> … }` is legal when `x` is `Boolean`, and
6399 // it reads 2 conditions against 1 decision — the entry's implicit
6400 // equality plus the `>` inside the operand. That is not the #1421
6401 // double-count reappearing: `nested` spells the same thing as an
6402 // `if` (`x == (y > 5)`) and ABC has always scored it 2 against the
6403 // same 1 decision. A comparison nested inside a comparison is two
6404 // comparisons; only one of them is a branch.
6405 #[cfg(feature = "kotlin")]
6406 #[test]
6407 fn kotlin_subjectful_when_arms_keep_the_per_entry_count() {
6408 let src = "class K {
6409 fun inn(a: Int): Int = when (a) { in 1..2 -> 1; else -> 0 }
6410 fun notin(a: Int): Int = when (a) { !in 1..2 -> 1; else -> 0 }
6411 fun iss(a: Any): Int = when (a) { is String -> 1; else -> 0 }
6412 fun konst(a: Int): Int = when (a) { 1 -> 10; 2 -> 20; else -> 0 }
6413 fun cmp(x: Boolean, y: Int): Int = when (x) { y > 5 -> 1; else -> 0 }
6414 fun nested(x: Boolean, y: Int): Int { if (x == (y > 5)) { return 1 }; return 0 }
6415 }";
6416 assert_kotlin_class_members(
6417 src,
6418 &[
6419 (Kotlin::WhenSubject as u16, 5, "`when` subjects"),
6420 (Kotlin::WhenEntry as u16, 11, "`when` entries"),
6421 (Kotlin::RangeTest as u16, 2, "`in` / `!in` patterns"),
6422 (Kotlin::TypeTest as u16, 1, "the `is` pattern"),
6423 (
6424 Kotlin::BinaryExpression as u16,
6425 3,
6426 "`cmp`'s and `nested`'s comparisons",
6427 ),
6428 ],
6429 &[
6430 ("inn", 1, 2),
6431 ("notin", 1, 2),
6432 ("iss", 1, 2),
6433 ("konst", 2, 3),
6434 // Above its decision count, like `nested` below it
6435 // and for the same reason.
6436 ("cmp", 2, 2),
6437 ("nested", 2, 2),
6438 ],
6439 );
6440 }
6441
6442 // `kotlin_enclosing_when_has_subject` stops scanning at the `{`, so
6443 // that a subject-less `when` does not walk all N arms once per arm.
6444 // The bound has to clear the header first, and the header can carry
6445 // an `extra`: tree-sitter hangs `when /*c*/ (x)`'s comment off the
6446 // `when_expression` between the keyword and the subject, so a bound
6447 // one child tighter — `child(1)`, or a stop at the first *named*
6448 // child — reads `cmtSubject` as subject-less. It would then score
6449 // its `1` arm through the slot, where a `number_literal` is not a
6450 // Kotlin terminal, and report 0 conditions instead of 1. That is
6451 // what discriminates here; `cmtNone` is the other direction, where
6452 // the brace is all the bound has to stop on.
6453 //
6454 // The `block_comment` census is the fixture anchor: delete either
6455 // comment and the members still read 1, because a plain `when (x)`
6456 // and a plain `when {` both do. The count fails by name instead.
6457 #[cfg(feature = "kotlin")]
6458 #[test]
6459 fn kotlin_when_subject_scan_clears_a_comment_before_the_brace() {
6460 let src = "class K {
6461 fun cmtSubject(x: Int): Int = when /*c*/ (x) { 1 -> 1; else -> 0 }
6462 fun cmtNone(a: Boolean): Int = when /*c*/ { a -> 1; else -> 0 }
6463 }";
6464 assert_kotlin_class_members(
6465 src,
6466 &[
6467 (Kotlin::BlockComment as u16, 2, "the two header comments"),
6468 (Kotlin::WhenSubject as u16, 1, "`cmtSubject`'s subject"),
6469 (Kotlin::WhenEntry as u16, 4, "`when` entries"),
6470 ],
6471 &[("cmtSubject", 1, 2), ("cmtNone", 1, 2)],
6472 );
6473 }
6474
6475 // A subject-less condition reaches the slot through the same
6476 // `kotlin_inspect_container` unwrapping an `if` predicate does, which
6477 // is what separates this design from suppressing the entry's
6478 // operator. `notted` and `paren` unwrap to a bare terminal and score
6479 // through the slot; `notCmp` and `parenCmp` unwrap to a
6480 // `binary_expression`, score nothing from the slot, and take their
6481 // one condition from the `>` the token arm already owned. Both pairs
6482 // land on 1, which no blanket `+1` can produce for the second pair —
6483 // they read 2 before the fix.
6484 //
6485 // `paren` is the member that reaches the `WhenEntry` seed added to
6486 // `kotlin_inspect_container`'s boolean-context set; without it the
6487 // parenthesised operand is not in a slot the walker calls boolean and
6488 // `paren` drops to 0. `notted` cannot stand in for it — a `!`
6489 // operator proves boolean content on its own.
6490 #[cfg(feature = "kotlin")]
6491 #[test]
6492 fn kotlin_subjectless_when_condition_wrappers_count_once() {
6493 let src = "class K {
6494 fun notted(a: Boolean): Int = when { !a -> 1; else -> 0 }
6495 fun paren(a: Boolean): Int = when { (a) -> 1; else -> 0 }
6496 fun notCmp(a: Int): Int = when { !(a > 1) -> 1; else -> 0 }
6497 fun parenCmp(a: Int): Int = when { (a > 1) -> 1; else -> 0 }
6498 fun nest(x: Int, y: Int): Int =
6499 when { x > 1 -> when { y > 2 -> 1; else -> 2 }; else -> 0 }
6500 fun blockBody(x: Int): Int { when { x > 5 -> { return 1 } }; return 0 }
6501 }";
6502 assert_kotlin_class_members(
6503 src,
6504 &[
6505 // Paired, not bare: "no subjects" alone is satisfied by a
6506 // fixture that lost its `when`s altogether.
6507 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6508 (Kotlin::WhenEntry as u16, 13, "`when` entries"),
6509 (Kotlin::UnaryExpression as u16, 2, "the two `!` operands"),
6510 (
6511 Kotlin::ParenthesizedExpression as u16,
6512 3,
6513 "the three parenthesised operands",
6514 ),
6515 (Kotlin::BinaryExpression as u16, 5, "comparisons"),
6516 ],
6517 &[
6518 ("notted", 1, 2),
6519 ("paren", 1, 2),
6520 // Both were 2: the entry's blanket count plus the `>`.
6521 ("notCmp", 1, 2),
6522 ("parenCmp", 1, 2),
6523 // Was 4 — one entry and one comparison per `when`,
6524 // each counted twice.
6525 ("nest", 2, 3),
6526 // The `when`-as-statement shape: a `block` body and no
6527 // `else` arm at all. Reading the condition from the
6528 // `condition` field rather than by position is what keeps
6529 // the block out of the slot; it was 2 before the fix.
6530 ("blockBody", 1, 2),
6531 ],
6532 );
6533 }
6534
6535 // The one place this design deliberately does not reach parity, put
6536 // under test so the choice cannot drift unremarked. A `when` entry
6537 // may list alternatives (`when { a, b -> … }`, an implicit `or`), and
6538 // the `condition` field is `multiple` — but cyclomatic scores the
6539 // whole entry as one decision, so `bareAlts` is at parity only
6540 // because the slot reads the *first* alternative and stops. Routing
6541 // every alternative through the slot would push it to 2.
6542 //
6543 // `alts` is the row that cannot be brought to parity from the ABC
6544 // side at all: the comparison-token arms count `>` and `<` wherever
6545 // they appear, so it reads 2 against 1 decision no matter what the
6546 // entry slot does. It was 3 before #1421 and the remaining gap is
6547 // cyclomatic's single count per multi-alternative entry, not a
6548 // double-count here.
6549 //
6550 // `subjAlts` is the subject-ful control, unchanged and at parity: its
6551 // alternatives are constants carrying no token to count.
6552 #[cfg(feature = "kotlin")]
6553 #[test]
6554 fn kotlin_subjectless_when_multi_alternative_entry() {
6555 let src = "class K {
6556 fun alts(x: Int, y: Int): Int = when { x > 5, y < 0 -> 1; else -> 0 }
6557 fun bareAlts(x: Boolean, y: Boolean): Int = when { x, y -> 1; else -> 0 }
6558 fun subjAlts(a: Int): Int = when (a) { 1, 2 -> 10; else -> 0 }
6559 }";
6560 assert_kotlin_class_members(
6561 src,
6562 &[
6563 (Kotlin::WhenSubject as u16, 1, "`subjAlts`'s subject"),
6564 (Kotlin::WhenEntry as u16, 6, "`when` entries"),
6565 (
6566 Kotlin::BinaryExpression as u16,
6567 2,
6568 "`alts`'s two comparisons",
6569 ),
6570 ],
6571 &[
6572 // Above its decision count: cyclomatic scores the entry
6573 // once, the token arms score both comparisons. Was 3.
6574 ("alts", 2, 2),
6575 ("bareAlts", 1, 2),
6576 ("subjAlts", 1, 2),
6577 ],
6578 );
6579 }
6580
6581 // `is_expression` and `in_expression` are the two relational forms
6582 // tree-sitter-kotlin-ng spells as their own production rather than as
6583 // a `binary_expression`, so no comparison-token arm sees them and
6584 // nothing counted them: `if (a is String)` scored 0 against a
6585 // decision count of 1, and the same test in a subject-less `when`
6586 // scored 1 only because the entry's blanket `+1` happened to cover
6587 // it. Removing that blanket without counting these two somewhere
6588 // would have regressed `whenIs` and `whenIn` to 0, and `isAnd` —
6589 // where the `is` test is one operand of a `&&` chain and so reaches
6590 // the walker rather than the slot — from 2 to 1.
6591 //
6592 // The `if` members are here because the fix was never `when`-
6593 // specific: they are the call sites that prove it reaches the `if` /
6594 // `while` predicate slot too.
6595 //
6596 // #1421 counted them through `kotlin_bool_terminal_kinds!()` and
6597 // #1461 moved them to an unconditional arm, which leaves every
6598 // number below unchanged — the slot no longer counts the test and
6599 // the arm does. `kotlin_is_and_in_score_outside_a_boolean_slot` is
6600 // the half this fixture cannot see, every member of it being inside
6601 // a slot.
6602 #[cfg(feature = "kotlin")]
6603 #[test]
6604 fn kotlin_is_and_in_expressions_are_unary_conditions() {
6605 let src = "class K {
6606 fun ifIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
6607 fun ifIn(a: Int): Int { if (a in 1..2) { return 1 }; return 0 }
6608 fun whenIs(a: Any): Int = when { a is String -> 1; else -> 0 }
6609 fun whenIn(a: Int): Int = when { a in 1..2 -> 1; else -> 0 }
6610 fun isAnd(a: Any, b: Boolean): Int = when { a is String && b -> 1; else -> 0 }
6611 }";
6612 assert_kotlin_class_members(
6613 src,
6614 &[
6615 // Paired, not bare — see the wrappers test above.
6616 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6617 (Kotlin::WhenEntry as u16, 6, "`when` entries"),
6618 (Kotlin::IsExpression as u16, 3, "`is` tests"),
6619 (Kotlin::InExpression as u16, 2, "`in` tests"),
6620 (Kotlin::AMPAMP as u16, 1, "`isAnd`'s `&&`"),
6621 ],
6622 &[
6623 // Both were 0 — the predicate slot saw a kind it did
6624 // not classify and the token arms saw no operator.
6625 ("ifIs", 1, 2),
6626 ("ifIn", 1, 2),
6627 ("whenIs", 1, 2),
6628 ("whenIn", 1, 2),
6629 ("isAnd", 2, 3),
6630 ],
6631 );
6632 }
6633
6634 // Kotlin spells boolean `and` / `or` / `xor` as infix *functions*, so
6635 // `a and b` parses as `infix_expression` — not `binary_expression`,
6636 // and not any token arm. It belongs in
6637 // `kotlin_bool_terminal_kinds!()` for the same reason
6638 // `call_expression` does: `a and b` is `a.and(b)`.
6639 //
6640 // This was a regression of #1421, not a pre-existing gap, and it is
6641 // the third shape the blanket per-entry count turned out to be
6642 // propping up — after `is` / `in` and bare parens, which that fix
6643 // caught. Found by the whole-branch review. Measured on both sides:
6644 // `when { a and b -> … }` scored 1 before and 0 after, while the
6645 // `if (a and b)` it is supposed to agree with still scored 1.
6646 //
6647 // The `if` members are the anchor rather than a second `when`: the
6648 // whole premise of #1421 is that a subject-less arm scores like an
6649 // `if` predicate, so a fixture where the two disagree is the
6650 // statement of the bug. `andAmp` holds the pair level against the
6651 // `&&` spelling, which never regressed and would otherwise be the
6652 // only form under test.
6653 #[cfg(feature = "kotlin")]
6654 #[test]
6655 fn kotlin_infix_boolean_functions_are_unary_conditions() {
6656 let src = "class K {
6657 fun whenAnd(a: Boolean, b: Boolean): Int = when { a and b -> 1; else -> 0 }
6658 fun whenOr(a: Boolean, b: Boolean): Int = when { a or b -> 1; else -> 0 }
6659 fun whenXor(a: Boolean, b: Boolean): Int = when { a xor b -> 1; else -> 0 }
6660 fun whenAmp(a: Boolean, b: Boolean): Int = when { a && b -> 1; else -> 0 }
6661 fun ifAnd(a: Boolean, b: Boolean): Int { if (a and b) { return 1 }; return 0 }
6662 }";
6663 assert_kotlin_class_members(
6664 src,
6665 &[
6666 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6667 (Kotlin::InfixExpression as u16, 4, "the infix calls"),
6668 (Kotlin::AMPAMP as u16, 1, "`whenAmp`'s `&&`"),
6669 ],
6670 &[
6671 // 0 before this fix: the predicate slot saw a kind it
6672 // did not classify and no token arm matched.
6673 ("whenAnd", 1, 2),
6674 ("whenOr", 1, 2),
6675 ("whenXor", 1, 2),
6676 // Never regressed — `&&` is a `binary_expression` whose
6677 // operator the token arm owns. Holds the others level.
6678 ("whenAmp", 2, 3),
6679 // The agreement #1421 exists to establish: a
6680 // subject-less arm scores what the equivalent `if`
6681 // predicate scores.
6682 ("ifAnd", 1, 2),
6683 ],
6684 );
6685 }
6686
6687 #[cfg(feature = "kotlin")]
6688 #[test]
6689 fn kotlin_catch_block_counts() {
6690 check_metrics::<KotlinParser>(
6691 "fun m() {
6692 try {
6693 println(\"ok\")
6694 } catch (e: Exception) {
6695 println(\"err\")
6696 }
6697 }",
6698 "foo.kt",
6699 |metric| {
6700 // `try` (+1) and `catch` (+1) each contribute one condition,
6701 // matching Java / C# / C++ / Groovy (Fitzpatrick counts both
6702 // keywords). Before #696 Kotlin counted only the catch block.
6703 assert_eq!(metric.abc.conditions_sum(), 2);
6704 insta::assert_json_snapshot!(metric.abc);
6705 },
6706 );
6707 }
6708
6709 #[cfg(feature = "kotlin")]
6710 #[test]
6711 fn kotlin_elvis_and_safe_cast() {
6712 // `?:` (elvis) and `as?` (safe cast) are condition-like.
6713 check_metrics::<KotlinParser>(
6714 "fun m(s: String?): Int {
6715 val n = (s as? Int) ?: 0
6716 return n
6717 }",
6718 "foo.kt",
6719 |metric| {
6720 // as? (+1) + ?: (+1) = 2 conditions.
6721 assert_eq!(metric.abc.conditions_sum(), 2);
6722 insta::assert_json_snapshot!(metric.abc);
6723 },
6724 );
6725 }
6726
6727 #[cfg(feature = "kotlin")]
6728 #[test]
6729 fn kotlin_generic_brackets_not_conditions() {
6730 // `<` / `>` used as type-parameter brackets must not be counted.
6731 check_metrics::<KotlinParser>(
6732 "class Box<T>(val v: T)
6733 fun <T> wrap(x: T): Box<T> = Box(x)",
6734 "foo.kt",
6735 |metric| {
6736 // No comparisons — only generic brackets.
6737 assert_eq!(metric.abc.conditions_sum(), 0);
6738 insta::assert_json_snapshot!(metric.abc);
6739 },
6740 );
6741 }
6742
6743 // #1297: a qualified super call disambiguates its supertype with
6744 // the same two bare tokens a comparison uses — `super<A>.g()` is a
6745 // `super_expression`, which the previous denylist
6746 // (`type_arguments` / `type_parameters`) did not name, so it scored
6747 // two conditions. One `super<A>` against one genuine `a < b`
6748 // separates every mis-aim: 3 pre-fix, 1 once the arm allows
6749 // `BinaryExpression`, 0 if it allows the wrong parent or the
6750 // fixture stops parsing.
6751 #[cfg(feature = "kotlin")]
6752 #[test]
6753 fn kotlin_super_type_argument_is_not_a_condition() {
6754 check_metrics::<KotlinParser>(
6755 "class B : A() {
6756 override fun g(a: Int, b: Int): Int {
6757 if (a < b) { return super<A>.g(a, b) }
6758 return 0
6759 }
6760 }",
6761 "foo.kt",
6762 |metric| {
6763 assert_eq!(metric.abc.conditions_sum(), 1);
6764 // Non-vacuity guard: 1 is also what a body whose `if`
6765 // survived but whose super call did not would score, so
6766 // pin the call itself. Measured: dropping
6767 // `super<A>.g(a, b)` takes `branches_sum()` from 2 to 1 —
6768 // the remaining branch is the `A()` primary-constructor
6769 // delegation in the class header, which #1384 taught this
6770 // metric to count.
6771 assert_eq!(metric.abc.branches_sum(), 2);
6772 },
6773 );
6774 }
6775
6776 #[cfg(feature = "kotlin")]
6777 #[test]
6778 fn kotlin_class_with_methods_and_branches() {
6779 check_metrics::<KotlinParser>(
6780 "class C {
6781 var counter: Int = 0
6782 fun bump() {
6783 counter += 1
6784 println(counter)
6785 }
6786 }",
6787 "foo.kt",
6788 |metric| {
6789 // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
6790 // branches: println(counter) = 1
6791 assert_eq!(metric.abc.assignments_sum(), 2);
6792 assert_eq!(metric.abc.branches_sum(), 1);
6793 assert_eq!(metric.abc.conditions_sum(), 0);
6794 insta::assert_json_snapshot!(metric.abc);
6795 },
6796 );
6797 }
6798
6799 #[cfg(feature = "kotlin")]
6800 #[test]
6801 fn kotlin_object_singleton_abc() {
6802 check_metrics::<KotlinParser>(
6803 "object Util {
6804 fun work(x: Int): Int {
6805 var y = x
6806 y += 1
6807 if (y > 0) {
6808 return y
6809 }
6810 return -1
6811 }
6812 }",
6813 "foo.kt",
6814 |metric| {
6815 // assignments: var y = x (+1), y += 1 (+1) = 2
6816 // branches: 0 (return is not a call)
6817 // conditions: y > 0 (+1) = 1
6818 assert_eq!(metric.abc.assignments_sum(), 2);
6819 assert_eq!(metric.abc.branches_sum(), 0);
6820 assert_eq!(metric.abc.conditions_sum(), 1);
6821 insta::assert_json_snapshot!(metric.abc);
6822 },
6823 );
6824 }
6825
6826 #[cfg(feature = "kotlin")]
6827 #[test]
6828 fn kotlin_interface_abc() {
6829 // Pure-abstract interface with no bodies — all-zero.
6830 check_metrics::<KotlinParser>(
6831 "interface I {
6832 fun work(): Int
6833 fun describe(): String
6834 }",
6835 "foo.kt",
6836 |metric| {
6837 assert_eq!(metric.abc.assignments_sum(), 0);
6838 assert_eq!(metric.abc.branches_sum(), 0);
6839 assert_eq!(metric.abc.conditions_sum(), 0);
6840 insta::assert_json_snapshot!(metric.abc);
6841 },
6842 );
6843 }
6844
6845 #[cfg(feature = "kotlin")]
6846 #[test]
6847 fn kotlin_nested_class_abc() {
6848 check_metrics::<KotlinParser>(
6849 "class Outer {
6850 var o: Int = 0
6851 class Nested {
6852 var n: Int = 0
6853 fun bump() { n += 1 }
6854 }
6855 }",
6856 "foo.kt",
6857 |metric| {
6858 // Outer: var o = 0 (+1)
6859 // Nested: var n = 0 (+1), n += 1 (+1) = 2
6860 // total assignments = 3
6861 assert_eq!(metric.abc.assignments_sum(), 3);
6862 insta::assert_json_snapshot!(metric.abc);
6863 },
6864 );
6865 }
6866
6867 #[cfg(feature = "kotlin")]
6868 #[test]
6869 fn kotlin_data_class_abc() {
6870 // `data class` with primary-constructor `val`s — no assignments
6871 // (vals don't count) and no body conditions.
6872 check_metrics::<KotlinParser>(
6873 "data class Point(val x: Int, val y: Int)",
6874 "foo.kt",
6875 |metric| {
6876 assert_eq!(metric.abc.assignments_sum(), 0);
6877 assert_eq!(metric.abc.branches_sum(), 0);
6878 assert_eq!(metric.abc.conditions_sum(), 0);
6879 insta::assert_json_snapshot!(metric.abc);
6880 },
6881 );
6882 }
6883
6884 #[cfg(feature = "kotlin")]
6885 #[test]
6886 fn kotlin_primary_constructor_default_value_not_assignment() {
6887 // Regression: default values on primary-constructor `val`
6888 // parameters are initialisers, not assignments. Without
6889 // `ClassParameter` pushing a declaration sentinel, the `=` token
6890 // here would be counted unconditionally as a standalone
6891 // assignment.
6892 check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
6893 // `val a = 5` → suppressed (Const sentinel).
6894 assert_eq!(metric.abc.assignments_sum(), 0);
6895 insta::assert_json_snapshot!(metric.abc);
6896 });
6897 }
6898
6899 #[cfg(feature = "kotlin")]
6900 #[test]
6901 fn kotlin_unary_conditions_in_chain() {
6902 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
6903 // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
6904 // contribute one; the `&&` / `||` operators contribute nothing.
6905 // expected: 3 unary conditions, no comparisons, no `if`-keyword
6906 // condition in Kotlin (matches the Java byte-equivalent of 3).
6907 check_metrics::<KotlinParser>(
6908 "fun f(a: Boolean, b: Boolean, c: Boolean) {
6909 if (a && b || c) { println(\"x\") }
6910 }",
6911 "foo.kt",
6912 |metric| {
6913 assert_eq!(metric.abc.conditions_sum(), 3);
6914 },
6915 );
6916 }
6917
6918 #[cfg(feature = "kotlin")]
6919 #[test]
6920 fn kotlin_comparison_operands_add_nothing() {
6921 // Isolation check: comparison operands of a `&&` chain are nested
6922 // `binary_expression` nodes, not bare boolean leaves, so the
6923 // walker adds nothing — only the two `>` comparisons count.
6924 // expected: 2 (the two `>` tokens), walker contributes 0.
6925 check_metrics::<KotlinParser>(
6926 "fun g(x: Int, y: Int) {
6927 if (x > 0 && y > 0) { println(\"x\") }
6928 }",
6929 "foo.kt",
6930 |metric| {
6931 assert_eq!(metric.abc.conditions_sum(), 2);
6932 },
6933 );
6934 }
6935
6936 #[cfg(feature = "kotlin")]
6937 #[test]
6938 fn kotlin_negated_operand_is_unary_condition() {
6939 // A `!`-negated operand is still a unary condition: `a && !b`
6940 // unwraps the `unary_expression` to reach the inner identifier.
6941 // expected: 2 (`a` and the `!b` operand).
6942 check_metrics::<KotlinParser>(
6943 "fun f(a: Boolean, b: Boolean) {
6944 if (a && !b) { println(\"x\") }
6945 }",
6946 "foo.kt",
6947 |metric| {
6948 assert_eq!(metric.abc.conditions_sum(), 2);
6949 },
6950 );
6951 }
6952
6953 // `kotlin_inspect_container` has two callers, and #1459's fixture
6954 // members all reach it through the *slot*
6955 // (`kotlin_count_condition`). This is the other one: the `&&` / `||`
6956 // walker, which routes any named non-terminal operand through the
6957 // same peel. The two wrappers #1459 taught it therefore have a
6958 // second, structurally independent path into the count, and a
6959 // fixture covering only the slot leaves it untested
6960 // (`.claude/rules/grammar-dispatch.md` §11).
6961 //
6962 // Each chain is one identifier plus one wrapped operand, so 2 is
6963 // "the walker peeled the wrapper" and 1 is "it gave up on it" —
6964 // which is what both lines scored before #1459. The bare `a && b`
6965 // control is `kotlin_unary_conditions_in_chain`'s shape at 2, so a
6966 // regression cannot be read as the chain itself changing.
6967 #[cfg(feature = "kotlin")]
6968 #[test]
6969 fn kotlin_chain_operands_peel_null_assertions_and_casts() {
6970 // Anchored per row, because the assertion alone cannot tell the
6971 // wrapper from its operand: `a && b` scores the same 2 / 3 as
6972 // every row here, so trimming the `!!` or the cast out of a
6973 // fixture would leave this green with its subject gone
6974 // (`.claude/rules/testing.md`, "Perturb the fixture as well as
6975 // the production line").
6976 for (chain, spelling, anchors) in [
6977 (
6978 "a && b!!",
6979 "null assertion",
6980 &[(Kotlin::BANGBANG as u16, 1usize, "the `!!`")][..],
6981 ),
6982 (
6983 "a && b as Boolean",
6984 "cast",
6985 &[(Kotlin::AsExpression as u16, 1, "the `as` cast")][..],
6986 ),
6987 (
6988 "a && (b!!)",
6989 "parenthesised null assertion",
6990 &[
6991 (Kotlin::BANGBANG as u16, 1, "the `!!`"),
6992 (Kotlin::ParenthesizedExpression as u16, 1, "the parens"),
6993 ][..],
6994 ),
6995 ] {
6996 let src = format!("fun f(a: Boolean, b: Any) {{ if ({chain}) {{ println(\"x\") }} }}");
6997 assert_fixture_spells::<KotlinParser>(&src, "foo.kt", anchors);
6998 check_func_space::<KotlinParser, _>(&src, "foo.kt", |space| {
6999 let f = child_space(&space, "f");
7000 assert_eq!(
7001 f.metrics.abc.conditions(),
7002 2,
7003 "`{chain}`: the identifier plus the {spelling} operand"
7004 );
7005 assert_eq!(f.metrics.cyclomatic.cyclomatic(), 3, "`{chain}`: decisions");
7006 });
7007 }
7008 }
7009
7010 #[cfg(feature = "kotlin")]
7011 #[test]
7012 fn kotlin_bare_if_predicate_is_one_condition() {
7013 // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one
7014 // Fitzpatrick unary condition. Before the Phase-2B arm it counted
7015 // 0, so `if (flag) 1 else -1` scored 1 (only the `else`) instead of
7016 // 2, dropping below Kotlin's own cyclomatic decision count.
7017 // expected: predicate (1) + else (1) = 2.
7018 check_metrics::<KotlinParser>(
7019 "fun m(flag: Boolean): Int { return if (flag) 1 else -1 }",
7020 "foo.kt",
7021 |metric| {
7022 assert_eq!(metric.abc.conditions_sum(), 2);
7023 },
7024 );
7025 }
7026
7027 #[cfg(feature = "kotlin")]
7028 #[test]
7029 fn kotlin_bare_while_predicate_is_one_condition() {
7030 // Issue #773: the bare predicate of a `while` loop counts one
7031 // condition via the `condition` field. expected: 1.
7032 check_metrics::<KotlinParser>(
7033 "fun m(running: Boolean) { while (running) { println(\"x\") } }",
7034 "foo.kt",
7035 |metric| {
7036 assert_eq!(metric.abc.conditions_sum(), 1);
7037 },
7038 );
7039 }
7040
7041 #[cfg(feature = "kotlin")]
7042 #[test]
7043 fn kotlin_bare_do_while_predicate_is_one_condition() {
7044 // Issue #773: the bare predicate of a `do`/`while` loop counts one
7045 // condition. expected: 1.
7046 check_metrics::<KotlinParser>(
7047 "fun m(ok: Boolean) { do { println(\"x\") } while (ok) }",
7048 "foo.kt",
7049 |metric| {
7050 assert_eq!(metric.abc.conditions_sum(), 1);
7051 },
7052 );
7053 }
7054
7055 #[cfg(feature = "kotlin")]
7056 #[test]
7057 fn kotlin_comparison_predicate_not_double_counted() {
7058 // Double-count guard (#773): a comparison predicate (`if (a == b)`)
7059 // is a nested `binary_expression` already counted by the `==` token
7060 // arm, so the Phase-2B condition-slot arm must add nothing here.
7061 // expected: `==` (1) + else (1) = 2 — unchanged by the new arm.
7062 check_metrics::<KotlinParser>(
7063 "fun m(a: Int, b: Int): Int { return if (a == b) 1 else -1 }",
7064 "foo.kt",
7065 |metric| {
7066 assert_eq!(metric.abc.conditions_sum(), 2);
7067 },
7068 );
7069 }
7070
7071 #[cfg(feature = "kotlin")]
7072 #[test]
7073 fn kotlin_short_circuit_predicate_not_double_counted() {
7074 // Double-count guard (#773): an `&&`/`||` predicate is counted by
7075 // the Rule 9 chain walker (each operand once); the Phase-2B arm
7076 // must add nothing for it. expected: `x` (1) + `y` (1) + else (1)
7077 // = 3 — unchanged by the new arm.
7078 check_metrics::<KotlinParser>(
7079 "fun m(x: Boolean, y: Boolean): Int { return if (x && y) 1 else -1 }",
7080 "foo.kt",
7081 |metric| {
7082 assert_eq!(metric.abc.conditions_sum(), 3);
7083 },
7084 );
7085 }
7086
7087 #[cfg(feature = "kotlin")]
7088 #[test]
7089 fn kotlin_parenthesised_bare_predicate_is_one_condition() {
7090 // A parenthesised bare predicate (`if ((flag))`) is unwrapped by
7091 // `kotlin_inspect_container` and still counts one condition (#773).
7092 // expected: 1.
7093 check_metrics::<KotlinParser>(
7094 "fun m(flag: Boolean) { if ((flag)) { println(\"x\") } }",
7095 "foo.kt",
7096 |metric| {
7097 assert_eq!(metric.abc.conditions_sum(), 1);
7098 },
7099 );
7100 }
7101
7102 // --- TypeScript / TSX ABC tests --------------------------------------
7103 //
7104 // Assignment, branch, condition counting per Fitzpatrick:
7105 // - Augmented assignment / `++` / `--` always count.
7106 // - Plain `=` counts unless inside `const` declaration.
7107 // - `call_expression` / `new_expression` count as branches.
7108 // - Comparison / equality operators, ternary `?`, `??`, control-flow
7109 // arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
7110 // and `<`/`>` (outside `type_arguments` / `type_parameters`) count
7111 // as conditions.
7112
7113 #[cfg(feature = "typescript")]
7114 #[test]
7115 fn typescript_assignments_basic() {
7116 check_metrics::<TypescriptParser>(
7117 "class C {
7118 m(): void {
7119 let x = 0; // +1 — only a `const` initializer is suppressed
7120 x = 1; // +1
7121 x += 2; // +1
7122 x++; // +1
7123 }
7124 }",
7125 "foo.ts",
7126 |metric| {
7127 assert_eq!(metric.abc.assignments_sum(), 4);
7128 insta::assert_json_snapshot!(metric.abc);
7129 },
7130 );
7131 }
7132
7133 #[cfg(feature = "typescript")]
7134 #[test]
7135 fn typescript_const_excluded_from_assignments() {
7136 check_metrics::<TypescriptParser>(
7137 "class C {
7138 m(): void {
7139 const a = 1; // suppressed (`const` initializer)
7140 const b = 2; // suppressed
7141 let c = 3; // +1 — `let` initializers count
7142 }
7143 }",
7144 "foo.ts",
7145 |metric| {
7146 assert_eq!(metric.abc.assignments_sum(), 1);
7147 insta::assert_json_snapshot!(metric.abc);
7148 },
7149 );
7150 }
7151
7152 // Regression cluster for #1277. The pre-fix implementation decided
7153 // "is this `=` a `const` initializer?" from a sentinel stack cleared
7154 // only on a `SEMI` token, so the answer for one statement depended on
7155 // the *previous* statement's terminator. Automatic semicolon
7156 // insertion makes that terminator optional in all four JS-family
7157 // languages. The replacement is structural — see
7158 // `impl_js_family_const_binding!` in `src/metrics/abc/js_family.rs`.
7159
7160 #[cfg(feature = "typescript")]
7161 #[test]
7162 fn typescript_asi_const_does_not_suppress_later_assignments() {
7163 check_metrics::<TypescriptParser>(
7164 "function f() {
7165 const a = 1
7166 x = 2
7167 y = 3
7168 }",
7169 "foo.ts",
7170 |metric| {
7171 // `const a = 1` suppressed; `x = 2` and `y = 3` count.
7172 // Pre-#1277 this reported 0: the unterminated `const`
7173 // never popped its sentinel.
7174 assert_eq!(metric.abc.assignments_sum(), 2);
7175 },
7176 );
7177 }
7178
7179 #[cfg(feature = "typescript")]
7180 #[test]
7181 fn typescript_semicolon_const_does_not_suppress_later_assignments() {
7182 // The semicolon-terminated spelling of the fixture above, which
7183 // must score the same as it. Pre-#1277 they scored 2 and 0 — the
7184 // pair is what pins the terminator out of the answer.
7185 check_metrics::<TypescriptParser>(
7186 "function f() {
7187 const a = 1;
7188 x = 2;
7189 y = 3;
7190 }",
7191 "foo.ts",
7192 |metric| {
7193 assert_eq!(metric.abc.assignments_sum(), 2);
7194 },
7195 );
7196 }
7197
7198 #[cfg(feature = "typescript")]
7199 #[test]
7200 fn typescript_as_const_does_not_suppress_later_assignments() {
7201 // The sentinel stack was also reachable from the other side: the
7202 // `const` token of a TypeScript `x as const` assertion promoted a
7203 // live `let` slot to `Const` and suppressed every `=` until the
7204 // next `;`. Structurally that `const` is a child of an
7205 // `as_expression`, not a declaration keyword.
7206 check_metrics::<TypescriptParser>(
7207 "function f(x: number) {
7208 let y = x as const
7209 w = 3
7210 }",
7211 "foo.ts",
7212 |metric| {
7213 // `let` initializer (+1) and `w = 3` (+1); pre-#1277: 1.
7214 assert_eq!(metric.abc.assignments_sum(), 2);
7215 },
7216 );
7217 }
7218
7219 #[cfg(feature = "typescript")]
7220 #[test]
7221 fn typescript_const_declarator_shapes_stay_suppressed() {
7222 // Shapes the sentinel stack handled implicitly, which the
7223 // structural predicate must reproduce: *every* declarator under a
7224 // `const`-bearing `lexical_declaration` is suppressed, including
7225 // the second element of a multi-declarator list, a destructuring
7226 // pattern, and the defaults inside one — `c = 5`, the nested
7227 // `e = 6` and its `= {}`, `g = 7`, and the rest element's `h = 8`,
7228 // each one more pattern layer the predicate's climb has to cross
7229 // — while `for (const x of xs)` carries no `=` at all. Only
7230 // `let i = 3` and `var j = 4` count — the deliberate deviation
7231 // documented on `js_abc_compute!`. Gating on
7232 // `variable_declaration` instead of `lexical_declaration`,
7233 // dropping the `const` check, or stopping the climb at the
7234 // declarator's own `=`, each flips one of these rows. The
7235 // `for`-of row is the exception: it carries no `=`, so no change
7236 // to the predicate can move it. It pins the grammar shape instead
7237 // — a future grammar emitting a `variable_declarator` there would
7238 // start suppressing something that never counted.
7239 check_metrics::<TypescriptParser>(
7240 "function f(o: any, xs: number[]) {
7241 const a = 1, b = 2
7242 const {c = 5, d: {e = 6} = {}} = o
7243 const [g = 7, ...[h = 8]] = xs
7244 let i = 3
7245 var j = 4
7246 for (const x of xs) { k(x) }
7247 }",
7248 "foo.ts",
7249 |metric| {
7250 assert_eq!(metric.abc.assignments_sum(), 2);
7251 },
7252 );
7253 }
7254
7255 #[cfg(feature = "typescript")]
7256 #[test]
7257 fn typescript_const_initializer_value_assignments_still_count() {
7258 // TypeScript half of
7259 // `javascript_const_initializer_value_assignments_still_count`.
7260 check_metrics::<TypescriptParser>(
7261 "function m(o: any, a: any, b: any) { const x = (o.p = 1); const y = a || (b = 2); }",
7262 "foo.ts",
7263 |metric| {
7264 assert_eq!(metric.abc.assignments_sum(), 2);
7265 },
7266 );
7267 }
7268
7269 #[cfg(feature = "typescript")]
7270 #[test]
7271 fn typescript_branches_function_calls() {
7272 check_metrics::<TypescriptParser>(
7273 "class C {
7274 m(): void {
7275 foo(); // +1
7276 bar(1, 2); // +1
7277 new Date(); // +1
7278 }
7279 }",
7280 "foo.ts",
7281 |metric| {
7282 assert_eq!(metric.abc.branches_sum(), 3);
7283 insta::assert_json_snapshot!(metric.abc);
7284 },
7285 );
7286 }
7287
7288 #[cfg(feature = "typescript")]
7289 #[test]
7290 fn typescript_conditions_comparison_operators() {
7291 check_metrics::<TypescriptParser>(
7292 "class C {
7293 m(x: number, y: number): boolean {
7294 return x == y // +1
7295 || x === y // +1
7296 || x != y // +1
7297 || x !== y // +1
7298 || x < y // +1
7299 || x <= y // +1
7300 || x > y // +1
7301 || x >= y; // +1
7302 }
7303 }",
7304 "foo.ts",
7305 |metric| {
7306 assert_eq!(metric.abc.conditions_sum(), 8);
7307 insta::assert_json_snapshot!(metric.abc);
7308 },
7309 );
7310 }
7311
7312 #[cfg(feature = "typescript")]
7313 #[test]
7314 fn typescript_conditions_control_flow_arms() {
7315 check_metrics::<TypescriptParser>(
7316 "class C {
7317 m(x: number): number {
7318 try { // +1 (try)
7319 if (x > 0) { // +1 (>)
7320 return 1;
7321 } else { // +1 (else)
7322 return -1;
7323 }
7324 } catch (e) { // +1 (catch)
7325 return 0;
7326 }
7327 }
7328 }",
7329 "foo.ts",
7330 |metric| {
7331 assert_eq!(metric.abc.conditions_sum(), 4);
7332 insta::assert_json_snapshot!(metric.abc);
7333 },
7334 );
7335 }
7336
7337 #[cfg(feature = "typescript")]
7338 #[test]
7339 fn typescript_conditions_switch_case() {
7340 check_metrics::<TypescriptParser>(
7341 "class C {
7342 m(x: number): number {
7343 switch (x) {
7344 case 1: // +1
7345 return 1;
7346 case 2: // +1
7347 return 2;
7348 default: // +0 (fallthrough, #469)
7349 return 0;
7350 }
7351 }
7352 }",
7353 "foo.ts",
7354 |metric| {
7355 assert_eq!(metric.abc.conditions_sum(), 2);
7356 insta::assert_json_snapshot!(metric.abc);
7357 },
7358 );
7359 }
7360
7361 #[cfg(feature = "typescript")]
7362 #[test]
7363 fn typescript_ternary_and_nullish() {
7364 check_metrics::<TypescriptParser>(
7365 "class C {
7366 m(x: number | null): number {
7367 return x !== null // +1 (!==)
7368 ? x // +1 (ternary ?)
7369 : 0;
7370 }
7371 n(x: number | null): number {
7372 return x ?? 0; // +1 (??)
7373 }
7374 }",
7375 "foo.ts",
7376 |metric| {
7377 assert_eq!(metric.abc.conditions_sum(), 3);
7378 insta::assert_json_snapshot!(metric.abc);
7379 },
7380 );
7381 }
7382
7383 #[cfg(feature = "typescript")]
7384 #[test]
7385 fn typescript_instanceof_counts_as_condition() {
7386 check_metrics::<TypescriptParser>(
7387 "class C {
7388 m(o: unknown): boolean {
7389 return o instanceof C; // +1
7390 }
7391 }",
7392 "foo.ts",
7393 |metric| {
7394 assert_eq!(metric.abc.conditions_sum(), 1);
7395 insta::assert_json_snapshot!(metric.abc);
7396 },
7397 );
7398 }
7399
7400 #[cfg(feature = "typescript")]
7401 #[test]
7402 fn typescript_generic_lt_gt_not_a_condition() {
7403 // `<T>` in `class C<T>` and `Array<number>` should not contribute
7404 // to conditions even though the tokens are `<` and `>`.
7405 check_metrics::<TypescriptParser>(
7406 "class C<T> {
7407 xs: Array<number> = [];
7408 m(): void {
7409 const arr: Array<string> = []; // suppressed const
7410 void arr;
7411 }
7412 }",
7413 "foo.ts",
7414 |metric| {
7415 assert_eq!(metric.abc.conditions_sum(), 0);
7416 insta::assert_json_snapshot!(metric.abc);
7417 },
7418 );
7419 }
7420
7421 // #1297, TypeScript half: the `<` / `>` arm flipped from denying
7422 // `type_arguments` / `type_parameters` to allowing
7423 // `binary_expression`, and this pins the direction that flip could
7424 // have broken. `Array<string>` keeps a generic in scope so the
7425 // fixture proves both sides at once, and the assertion is the
7426 // grammar-dispatch §8 pin: on the function's own space the ABC
7427 // condition count equals the cyclomatic decision count
7428 // (`cyclomatic()` minus the per-space base of 1). Both are 2, one
7429 // per `if`.
7430 //
7431 // As with the Lua fixture, `Array<string>` leaves no ABC trace once
7432 // excluded, so nothing here fails if a later edit drops the
7433 // generic; that the generic reaches the arm is established by the
7434 // revert test, which fails this test when the gate is removed.
7435 //
7436 // TypeScript gets no JSX fixture because the `.ts` dialect has no
7437 // JSX to exercise: it lexes `return <div>…</div>` as a type
7438 // assertion and a comparison chain, sometimes with `ERROR` nodes and
7439 // sometimes without, depending on what follows the tag. Either way
7440 // the tokens land under `type_arguments` and `binary_expression`
7441 // rather than the `jsx_*` productions, so the construct that
7442 // motivated #1297 is only reachable through
7443 // `tsx_jsx_elements_are_not_conditions` below.
7444 //
7445 // That mis-parse still over-counts — a `.ts` file containing JSX
7446 // scores conditions this gate cannot exclude, because the grammar
7447 // genuinely reports a `binary_expression`. It is a wrong-dialect
7448 // input rather than a defect in this arm, and no allowlist can
7449 // distinguish it; see the same caveat on Kotlin's arm.
7450 #[cfg(feature = "typescript")]
7451 #[test]
7452 fn typescript_comparison_operators_still_count_alongside_generics() {
7453 check_func_space::<TypescriptParser, _>(
7454 "function m(xs: Array<string>, a: number, b: number): number {
7455 if (a < b) { return 1; }
7456 if (a > b) { return 2; }
7457 void xs;
7458 return 0;
7459 }",
7460 "foo.ts",
7461 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
7462 );
7463 }
7464
7465 // #1275, TypeScript half. Eleven grammar productions emit a bare
7466 // `?` and only `ternary_expression` is a decision; the other ten are
7467 // type syntax. This fixture exercises five of them — `optional_
7468 // parameter`, `property_signature`, `method_signature`,
7469 // `abstract_method_signature` and `public_field_definition`, plus an
7470 // `optional_type` in a tuple — against one real ternary over one
7471 // real `>`.
7472 //
7473 // Six type-syntax `?` means the numbers separate cleanly: 8 pre-fix,
7474 // 2 once the allowlist is aimed at `TernaryExpression`, 1 if it is
7475 // aimed at anything else (the ternary stops counting too), 0 if the
7476 // fixture stops parsing. Any partial gate — one that named some of
7477 // the type-syntax parents in a denylist instead — lands between 3
7478 // and 7 and is equally visible.
7479 #[cfg(feature = "typescript")]
7480 #[test]
7481 fn typescript_optional_type_syntax_is_not_a_condition() {
7482 check_metrics::<TypescriptParser>(
7483 "interface I { a?: string; m?(x: number): void; }
7484 abstract class K { f?: number; abstract g?(): void; }
7485 type Tup = [number, string?];
7486 function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
7487 "foo.ts",
7488 |metric| {
7489 assert_eq!(metric.abc.conditions_sum(), 2);
7490 },
7491 );
7492 }
7493
7494 // The explicit half of #1275's TypeScript decision: a conditional
7495 // type (`T extends U ? X : Y`) is resolved by the type checker and
7496 // erased before runtime, so its `?` is not a condition. That falls
7497 // out of the `TernaryExpression` allowlist rather than being named,
7498 // which is exactly why it needs its own test — the choice reads as
7499 // an omission otherwise, and nothing else here would notice if a
7500 // later edit added `ConditionalType` to the allowlist "for
7501 // symmetry".
7502 //
7503 // The real ternary below keeps the expectation off zero: 3 pre-fix,
7504 // 2 with the conditional type excluded, 1 if the ternary is
7505 // swallowed too.
7506 #[cfg(feature = "typescript")]
7507 #[test]
7508 fn typescript_conditional_type_is_not_a_condition() {
7509 check_metrics::<TypescriptParser>(
7510 "type Cond<T> = T extends string ? number : boolean;
7511 function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
7512 "foo.ts",
7513 |metric| {
7514 assert_eq!(metric.abc.conditions_sum(), 2);
7515 },
7516 );
7517 }
7518
7519 // grammar-dispatch §2. `Typescript::QMARK2` is the enum entry for
7520 // the external scanner's `_ternary_qmark` token, and
7521 // tree-sitter-typescript's public symbol map folds it back onto
7522 // `anon_sym_QMARK`, so `kind_id()` never reports it — every ternary
7523 // `?` in the fixture below arrives as plain `QMARK`. Listing
7524 // `QMARK2` in the gated arm would therefore be dead code, and
7525 // leaving it out is safe only for as long as that mapping holds.
7526 // This pins both halves so a grammar bump that starts exposing the
7527 // alias fails here rather than silently zeroing every TypeScript
7528 // ternary.
7529 //
7530 // The fixture's only `?` is the ternary's, deliberately. An optional
7531 // parameter (`b?: number`) would emit a `QMARK` of its own and
7532 // satisfy the positive assertion on its own, leaving it true no
7533 // matter what id the ternary's `?` came back as — decoration rather
7534 // than the non-vacuity guard it is here for.
7535 #[cfg(feature = "typescript")]
7536 #[test]
7537 fn typescript_ternary_qmark_alias_stays_unreachable() {
7538 let parser = TypescriptParser::new(
7539 "function f(a: boolean): number { return a ? 1 : 2; }\n"
7540 .as_bytes()
7541 .to_vec(),
7542 std::path::Path::new("foo.ts"),
7543 None,
7544 );
7545 assert!(ast_has_kind_id(&parser, Typescript::QMARK as u16));
7546 assert!(!ast_has_kind_id(&parser, Typescript::QMARK2 as u16));
7547 }
7548
7549 #[cfg(feature = "typescript")]
7550 #[test]
7551 fn typescript_abstract_class_abc() {
7552 // Abstract methods have no body — they contribute nothing.
7553 check_metrics::<TypescriptParser>(
7554 "abstract class C {
7555 abstract a(): void;
7556 m(x: number): number {
7557 if (x > 0) return 1; // +1 condition
7558 return 0;
7559 }
7560 }",
7561 "foo.ts",
7562 |metric| {
7563 assert_eq!(metric.abc.conditions_sum(), 1);
7564 assert_eq!(metric.abc.branches_sum(), 0);
7565 insta::assert_json_snapshot!(metric.abc);
7566 },
7567 );
7568 }
7569
7570 #[cfg(feature = "typescript")]
7571 #[test]
7572 fn typescript_interface_abc_zero() {
7573 check_metrics::<TypescriptParser>(
7574 "interface I {
7575 a(): void;
7576 b(): number;
7577 p: string;
7578 }",
7579 "foo.ts",
7580 |metric| {
7581 assert_eq!(metric.abc.assignments_sum(), 0);
7582 assert_eq!(metric.abc.branches_sum(), 0);
7583 assert_eq!(metric.abc.conditions_sum(), 0);
7584 insta::assert_json_snapshot!(metric.abc);
7585 },
7586 );
7587 }
7588
7589 #[cfg(feature = "typescript")]
7590 #[test]
7591 fn typescript_arrow_field_contributes_abc() {
7592 // Arrow function class members are function spaces; their
7593 // assignments/branches/conditions are counted.
7594 check_metrics::<TypescriptParser>(
7595 "class C {
7596 arrow = (x: number) => {
7597 if (x > 0) { // +1 condition
7598 return foo(); // +1 branch
7599 }
7600 return 0;
7601 };
7602 }",
7603 "foo.ts",
7604 |metric| {
7605 assert_eq!(metric.abc.conditions_sum(), 1);
7606 assert_eq!(metric.abc.branches_sum(), 1);
7607 insta::assert_json_snapshot!(metric.abc);
7608 },
7609 );
7610 }
7611
7612 #[cfg(feature = "typescript")]
7613 #[test]
7614 fn typescript_parameter_property_init_not_assignment() {
7615 // Parameter properties don't introduce a `=` token themselves;
7616 // only the explicit `let z = 0` body assignment is counted.
7617 // The class field initializer `f: number = 0` likewise has a `=`
7618 // that DOES count (matches `typescript_assignments_basic`).
7619 check_metrics::<TypescriptParser>(
7620 "class C {
7621 f: number = 0;
7622 constructor(public x: number, private y: string) {
7623 let z = 0;
7624 }
7625 }",
7626 "foo.ts",
7627 |metric| {
7628 // f's initializer + `let z = 0` = 2 assignments; the
7629 // parameter properties contribute zero.
7630 assert_eq!(metric.abc.assignments_sum(), 2);
7631 insta::assert_json_snapshot!(metric.abc);
7632 },
7633 );
7634 }
7635
7636 // TSX parity
7637
7638 #[cfg(feature = "typescript")]
7639 #[test]
7640 fn tsx_assignments_basic() {
7641 check_metrics::<TsxParser>(
7642 "class C {
7643 m(): void {
7644 let x = 0;
7645 x = 1;
7646 x += 2;
7647 x++;
7648 }
7649 }",
7650 "foo.tsx",
7651 |metric| {
7652 assert_eq!(metric.abc.assignments_sum(), 4);
7653 insta::assert_json_snapshot!(metric.abc);
7654 },
7655 );
7656 }
7657
7658 #[cfg(feature = "typescript")]
7659 #[test]
7660 fn tsx_const_excluded_from_assignments() {
7661 check_metrics::<TsxParser>(
7662 "class C {
7663 m(): void {
7664 const a = 1;
7665 let b = 2;
7666 }
7667 }",
7668 "foo.tsx",
7669 |metric| {
7670 assert_eq!(metric.abc.assignments_sum(), 1);
7671 insta::assert_json_snapshot!(metric.abc);
7672 },
7673 );
7674 }
7675
7676 #[cfg(feature = "typescript")]
7677 #[test]
7678 fn tsx_branches_function_calls() {
7679 check_metrics::<TsxParser>(
7680 "class C {
7681 m(): void {
7682 foo();
7683 new Date();
7684 }
7685 }",
7686 "foo.tsx",
7687 |metric| {
7688 assert_eq!(metric.abc.branches_sum(), 2);
7689 insta::assert_json_snapshot!(metric.abc);
7690 },
7691 );
7692 }
7693
7694 #[cfg(feature = "typescript")]
7695 #[test]
7696 fn tsx_conditions_comparison_operators() {
7697 check_metrics::<TsxParser>(
7698 "class C {
7699 m(x: number, y: number): boolean {
7700 return x == y || x < y || x >= y;
7701 }
7702 }",
7703 "foo.tsx",
7704 |metric| {
7705 assert_eq!(metric.abc.conditions_sum(), 3);
7706 insta::assert_json_snapshot!(metric.abc);
7707 },
7708 );
7709 }
7710
7711 #[cfg(feature = "typescript")]
7712 #[test]
7713 fn tsx_conditions_control_flow_arms() {
7714 check_metrics::<TsxParser>(
7715 "class C {
7716 m(x: number): number {
7717 try {
7718 if (x > 0) return 1;
7719 else return -1;
7720 } catch (e) {
7721 return 0;
7722 }
7723 }
7724 }",
7725 "foo.tsx",
7726 |metric| {
7727 assert_eq!(metric.abc.conditions_sum(), 4);
7728 insta::assert_json_snapshot!(metric.abc);
7729 },
7730 );
7731 }
7732
7733 #[cfg(feature = "typescript")]
7734 #[test]
7735 fn tsx_conditions_switch_case() {
7736 check_metrics::<TsxParser>(
7737 "class C {
7738 m(x: number): number {
7739 switch (x) {
7740 case 1: return 1; // +1
7741 case 2: return 2; // +1
7742 default: return 0; // +0 (fallthrough, #469)
7743 }
7744 }
7745 }",
7746 "foo.tsx",
7747 |metric| {
7748 assert_eq!(metric.abc.conditions_sum(), 2);
7749 insta::assert_json_snapshot!(metric.abc);
7750 },
7751 );
7752 }
7753
7754 #[cfg(feature = "typescript")]
7755 #[test]
7756 fn tsx_ternary_and_nullish() {
7757 check_metrics::<TsxParser>(
7758 "class C {
7759 m(x: number | null): number {
7760 return x !== null ? x : 0;
7761 }
7762 n(x: number | null): number { return x ?? 0; }
7763 }",
7764 "foo.tsx",
7765 |metric| {
7766 assert_eq!(metric.abc.conditions_sum(), 3);
7767 insta::assert_json_snapshot!(metric.abc);
7768 },
7769 );
7770 }
7771
7772 #[cfg(feature = "typescript")]
7773 #[test]
7774 fn tsx_instanceof_counts_as_condition() {
7775 check_metrics::<TsxParser>(
7776 "class C { m(o: unknown): boolean { return o instanceof C; } }",
7777 "foo.tsx",
7778 |metric| {
7779 assert_eq!(metric.abc.conditions_sum(), 1);
7780 insta::assert_json_snapshot!(metric.abc);
7781 },
7782 );
7783 }
7784
7785 #[cfg(feature = "typescript")]
7786 #[test]
7787 fn tsx_generic_lt_gt_not_a_condition() {
7788 check_metrics::<TsxParser>(
7789 "class C<T> { xs: Array<number> = []; }",
7790 "foo.tsx",
7791 |metric| {
7792 assert_eq!(metric.abc.conditions_sum(), 0);
7793 insta::assert_json_snapshot!(metric.abc);
7794 },
7795 );
7796 }
7797
7798 // #1297: a JSX tag delimiter is markup punctuation, not a
7799 // comparison. The `<` / `>` arm denied only `type_arguments` and
7800 // `type_parameters`, so every tag bracket scored a condition —
7801 // `jsx_opening_element` contributes both tokens, `jsx_closing_element`
7802 // a `>` (its `</` is one token) and `jsx_self_closing_element` a `<`
7803 // (its `/>` is one token).
7804 //
7805 // The fixture pairs seven such brackets — two elements' opening and
7806 // closing tags, plus one self-closing tag — against one genuine
7807 // `a < b`, so every way of mis-aiming the gate lands on its own
7808 // number: 8 pre-fix, 1 once it allows `BinaryExpression`, 0 if it
7809 // allows the wrong parent or the fixture stops parsing. Asserting 0
7810 // on a JSX-only body would not have separated those last two.
7811 #[cfg(feature = "typescript")]
7812 #[test]
7813 fn tsx_jsx_elements_are_not_conditions() {
7814 check_metrics::<TsxParser>(
7815 "function f(a: number, b: number) {
7816 if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
7817 return <br />;
7818 }",
7819 "foo.tsx",
7820 |metric| {
7821 assert_eq!(metric.abc.conditions_sum(), 1);
7822 // Fixture-presence anchor: the `className="x"` attribute
7823 // `=` is this fixture's only assignment, so the test
7824 // fails if a later edit drops the JSX. Without it the
7825 // surviving `a < b` alone also scores 1, and the
7826 // conditions assertion would pass while proving nothing.
7827 assert_eq!(metric.abc.assignments_sum(), 1);
7828 },
7829 );
7830 }
7831
7832 // #1275 in the second expansion of `ts_abc_compute!`. TSX shares
7833 // TypeScript's `?` productions and its own `TernaryExpression` /
7834 // `QMARK` ids, so the gate is a distinct instantiation and needs its
7835 // own fixture — a passing TypeScript test says nothing about the
7836 // macro's other expansion. Four type-syntax `?` plus one `>` and one
7837 // ternary: 6 pre-fix, 2 after, 1 if the allowlist is misaimed.
7838 #[cfg(feature = "typescript")]
7839 #[test]
7840 fn tsx_optional_type_syntax_is_not_a_condition() {
7841 check_metrics::<TsxParser>(
7842 "interface I { a?: string; m?(x: number): void; }
7843 class K { f?: number; }
7844 function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
7845 "foo.tsx",
7846 |metric| {
7847 assert_eq!(metric.abc.conditions_sum(), 2);
7848 },
7849 );
7850 }
7851
7852 // The TSX half of `typescript_conditional_type_is_not_a_condition`.
7853 // `ts_abc_compute!` expands twice and the two expansions are
7854 // independent code (grammar-dispatch §11): adding `ConditionalType`
7855 // to the allowlist "for symmetry" fails only the TypeScript test
7856 // without this one, which reads as the TSX expansion being fine
7857 // rather than untested. `conditional_type` is in the tsx grammar's
7858 // `?` set exactly as it is in typescript's.
7859 #[cfg(feature = "typescript")]
7860 #[test]
7861 fn tsx_conditional_type_is_not_a_condition() {
7862 check_metrics::<TsxParser>(
7863 "type Cond<T> = T extends string ? number : boolean;
7864 function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
7865 "foo.tsx",
7866 |metric| {
7867 assert_eq!(metric.abc.conditions_sum(), 2);
7868 },
7869 );
7870 }
7871
7872 // The TSX half of `typescript_ternary_qmark_alias_stays_unreachable`
7873 // — the tsx grammar declares the same `_ternary_qmark` external and
7874 // maps it back onto `anon_sym_QMARK` at its own id. Same
7875 // single-`?` fixture rule; see that test for why.
7876 #[cfg(feature = "typescript")]
7877 #[test]
7878 fn tsx_ternary_qmark_alias_stays_unreachable() {
7879 let parser = TsxParser::new(
7880 "function f(a: boolean): number { return a ? 1 : 2; }\n"
7881 .as_bytes()
7882 .to_vec(),
7883 std::path::Path::new("foo.tsx"),
7884 None,
7885 );
7886 assert!(ast_has_kind_id(&parser, Tsx::QMARK as u16));
7887 assert!(!ast_has_kind_id(&parser, Tsx::QMARK2 as u16));
7888 }
7889
7890 #[cfg(feature = "typescript")]
7891 #[test]
7892 fn tsx_abstract_class_abc() {
7893 check_metrics::<TsxParser>(
7894 "abstract class C {
7895 abstract a(): void;
7896 m(x: number): number {
7897 if (x > 0) return 1;
7898 return 0;
7899 }
7900 }",
7901 "foo.tsx",
7902 |metric| {
7903 assert_eq!(metric.abc.conditions_sum(), 1);
7904 assert_eq!(metric.abc.branches_sum(), 0);
7905 insta::assert_json_snapshot!(metric.abc);
7906 },
7907 );
7908 }
7909
7910 #[cfg(feature = "typescript")]
7911 #[test]
7912 fn tsx_interface_abc_zero() {
7913 check_metrics::<TsxParser>(
7914 "interface I { a(): void; p: string; }",
7915 "foo.tsx",
7916 |metric| {
7917 assert_eq!(metric.abc.assignments_sum(), 0);
7918 assert_eq!(metric.abc.branches_sum(), 0);
7919 assert_eq!(metric.abc.conditions_sum(), 0);
7920 insta::assert_json_snapshot!(metric.abc);
7921 },
7922 );
7923 }
7924
7925 #[cfg(feature = "typescript")]
7926 #[test]
7927 fn tsx_arrow_field_contributes_abc() {
7928 check_metrics::<TsxParser>(
7929 "class C {
7930 arrow = (x: number) => {
7931 if (x > 0) return foo();
7932 return 0;
7933 };
7934 }",
7935 "foo.tsx",
7936 |metric| {
7937 assert_eq!(metric.abc.conditions_sum(), 1);
7938 assert_eq!(metric.abc.branches_sum(), 1);
7939 insta::assert_json_snapshot!(metric.abc);
7940 },
7941 );
7942 }
7943
7944 #[cfg(feature = "typescript")]
7945 #[test]
7946 fn tsx_asi_const_does_not_suppress_later_assignments() {
7947 // TSX half of the #1277 cluster; see
7948 // `typescript_asi_const_does_not_suppress_later_assignments`.
7949 // TSX has its own `Const` / `VariableDeclarator` /
7950 // `LexicalDeclaration` kind ids, so the predicate is generated
7951 // separately and needs its own fixture.
7952 check_metrics::<TsxParser>(
7953 "function f() {
7954 const a = 1
7955 x = 2
7956 y = 3
7957 }",
7958 "foo.tsx",
7959 |metric| {
7960 assert_eq!(metric.abc.assignments_sum(), 2);
7961 },
7962 );
7963 }
7964
7965 #[cfg(feature = "typescript")]
7966 #[test]
7967 fn tsx_const_declarator_shapes_stay_suppressed() {
7968 // TSX half of `typescript_const_declarator_shapes_stay_suppressed`.
7969 check_metrics::<TsxParser>(
7970 "function f(o: any, xs: number[]) {
7971 const a = 1, b = 2
7972 const {c = 5, d: {e = 6} = {}} = o
7973 const [g = 7, ...[h = 8]] = xs
7974 let i = 3
7975 var j = 4
7976 for (const x of xs) { k(x) }
7977 }",
7978 "foo.tsx",
7979 |metric| {
7980 assert_eq!(metric.abc.assignments_sum(), 2);
7981 },
7982 );
7983 }
7984
7985 #[cfg(feature = "typescript")]
7986 #[test]
7987 fn tsx_parameter_property_init_not_assignment() {
7988 // Parameter properties contribute no `=`; the body's `let z = 0`
7989 // and the field initializer do.
7990 check_metrics::<TsxParser>(
7991 "class C {
7992 f: number = 0;
7993 constructor(public x: number) { let z = 0; }
7994 }",
7995 "foo.tsx",
7996 |metric| {
7997 assert_eq!(metric.abc.assignments_sum(), 2);
7998 insta::assert_json_snapshot!(metric.abc);
7999 },
8000 );
8001 }
8002
8003 // --- Ruby ABC tests ---------------------------------------------------
8004 //
8005 // Each Ruby `assignment` / `operator_assignment` is one assignment
8006 // regardless of whether the LHS is a local, instance, or class
8007 // variable. Every `call` / `super` / `yield` is one branch. Every
8008 // comparison-operator token inside a `binary` node plus each
8009 // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
8010 // one condition.
8011
8012 #[cfg(feature = "ruby")]
8013 #[test]
8014 fn ruby_zero_abc() {
8015 check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
8016 assert_eq!(metric.abc.assignments_sum(), 0);
8017 assert_eq!(metric.abc.branches_sum(), 0);
8018 assert_eq!(metric.abc.conditions_sum(), 0);
8019 insta::assert_json_snapshot!(metric.abc);
8020 });
8021 }
8022
8023 #[cfg(feature = "ruby")]
8024 #[test]
8025 fn ruby_simple_assignment() {
8026 check_metrics::<RubyParser>("def f\n a = 1\n b = 2\nend\n", "foo.rb", |metric| {
8027 assert_eq!(metric.abc.assignments_sum(), 2);
8028 assert_eq!(metric.abc.branches_sum(), 0);
8029 assert_eq!(metric.abc.conditions_sum(), 0);
8030 insta::assert_json_snapshot!(metric.abc);
8031 });
8032 }
8033
8034 #[cfg(feature = "ruby")]
8035 #[test]
8036 fn ruby_augmented_assignment() {
8037 // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
8038 // one assignment. Plain `=` to set the initial value adds one
8039 // more.
8040 check_metrics::<RubyParser>(
8041 "def f(x)\n a = 0\n a += x\n a -= 1\n a *= 2\nend\n",
8042 "foo.rb",
8043 |metric| {
8044 assert_eq!(metric.abc.assignments_sum(), 4);
8045 insta::assert_json_snapshot!(metric.abc);
8046 },
8047 );
8048 }
8049
8050 #[cfg(feature = "ruby")]
8051 #[test]
8052 fn ruby_logical_augmented_assignment() {
8053 // `||=` and `&&=` are also `operator_assignment` nodes.
8054 check_metrics::<RubyParser>("def f\n @x ||= 0\n @x &&= 1\nend\n", "foo.rb", |metric| {
8055 assert_eq!(metric.abc.assignments_sum(), 2);
8056 insta::assert_json_snapshot!(metric.abc);
8057 });
8058 }
8059
8060 #[cfg(feature = "ruby")]
8061 #[test]
8062 fn ruby_method_call_branch() {
8063 // Each method invocation is one branch.
8064 check_metrics::<RubyParser>(
8065 "def f(obj)\n foo()\n obj.bar(1)\nend\n",
8066 "foo.rb",
8067 |metric| {
8068 assert_eq!(metric.abc.branches_sum(), 2);
8069 insta::assert_json_snapshot!(metric.abc);
8070 },
8071 );
8072 }
8073
8074 #[cfg(feature = "ruby")]
8075 #[test]
8076 fn ruby_super_and_yield_branches() {
8077 // `super` and `yield` both count as branches (control-pass).
8078 check_metrics::<RubyParser>("def f\n super\n yield\nend\n", "foo.rb", |metric| {
8079 assert_eq!(metric.abc.branches_sum(), 2);
8080 assert_eq!(metric.abc.assignments_sum(), 0);
8081 insta::assert_json_snapshot!(metric.abc);
8082 });
8083 }
8084
8085 #[cfg(feature = "ruby")]
8086 #[test]
8087 fn ruby_attr_macro_is_branch() {
8088 // `attr_accessor` is a `Call3` node and registers as a branch
8089 // like any method invocation.
8090 check_metrics::<RubyParser>("class A\n attr_accessor :x\nend\n", "foo.rb", |metric| {
8091 assert_eq!(metric.abc.branches_sum(), 1);
8092 insta::assert_json_snapshot!(metric.abc);
8093 });
8094 }
8095
8096 #[cfg(feature = "ruby")]
8097 #[test]
8098 fn ruby_comparison_conditions() {
8099 // Each comparison operator is one condition.
8100 check_metrics::<RubyParser>(
8101 "def f(a, b)\n a == b\n a != b\n a < b\n a > b\n a <= b\n a >= b\nend\n",
8102 "foo.rb",
8103 |metric| {
8104 assert_eq!(metric.abc.conditions_sum(), 6);
8105 insta::assert_json_snapshot!(metric.abc);
8106 },
8107 );
8108 }
8109
8110 #[cfg(feature = "ruby")]
8111 #[test]
8112 fn ruby_superclass_clause_is_not_a_condition() {
8113 // Regression for #1280: a superclass clause spells its `<` with the
8114 // same `LT` token as a comparison, but parents it under
8115 // `superclass` rather than `binary`, so the parent gate excludes
8116 // it. Before the gate every subclass declaration scored a phantom
8117 // condition.
8118 // expected: 0 conditions — the file contains no conditional at all;
8119 // the single assignment is `x = 1`.
8120 check_metrics::<RubyParser>(
8121 "class Foo < Bar\n def plain\n x = 1\n end\nend\n",
8122 "foo.rb",
8123 |metric| {
8124 assert_eq!(metric.abc.conditions_sum(), 0);
8125 assert_eq!(metric.abc.assignments_sum(), 1);
8126 },
8127 );
8128 }
8129
8130 #[cfg(feature = "ruby")]
8131 #[test]
8132 fn ruby_operator_method_name_is_not_a_condition() {
8133 // The `<` naming an operator method parents under `operator`, which
8134 // the `binary` gate likewise excludes (#1280). The body carries a
8135 // real comparison so the expected value is not the all-zero default:
8136 // 1 discriminates "only the name token is excluded" from both "the
8137 // gate excludes everything" (0) and "nothing is gated" (2).
8138 // expected: 1 condition — the `@v < other` comparison, not the `def <`.
8139 check_metrics::<RubyParser>("def <(other)\n @v < other\nend\n", "foo.rb", |metric| {
8140 assert_eq!(metric.abc.conditions_sum(), 1);
8141 });
8142 }
8143
8144 #[cfg(feature = "ruby")]
8145 #[test]
8146 fn ruby_every_comparison_operator_method_name_is_not_a_condition() {
8147 // The sibling half of #1280. `<` is not special: every comparison
8148 // and equality token Ruby lets you `def` parents under `operator`
8149 // in that position, so gating only `LT` / `GT` left `def ==`,
8150 // `def <=`, `def >=`, `def <=>`, `def !=` and `def =~` each
8151 // scoring a phantom condition — measured at 1 apiece for a body
8152 // containing no conditional at all.
8153 // expected: 0 conditions per definition; only the name token is on
8154 // the line, so a single non-zero total localises the regression.
8155 check_metrics::<RubyParser>(
8156 "def ==(o)\n 1\nend\n\
8157 def !=(o)\n 1\nend\n\
8158 def <=(o)\n 1\nend\n\
8159 def >=(o)\n 1\nend\n\
8160 def <=>(o)\n 1\nend\n\
8161 def =~(o)\n 1\nend\n\
8162 def <(o)\n 1\nend\n\
8163 def >(o)\n 1\nend\n",
8164 "foo.rb",
8165 |metric| {
8166 assert_eq!(metric.abc.conditions_sum(), 0);
8167 },
8168 );
8169 // The positive control: the same tokens inside a `binary` are real
8170 // comparisons and still count, so the gate is not blanket
8171 // suppression.
8172 check_metrics::<RubyParser>(
8173 "def cmp(a, b)\n a == b || a <= b || a <=> b\nend\n",
8174 "foo.rb",
8175 |metric| {
8176 assert_eq!(metric.abc.conditions_sum(), 3);
8177 },
8178 );
8179 }
8180
8181 #[cfg(feature = "ruby")]
8182 #[test]
8183 fn ruby_case_match_in_arms_are_conditions() {
8184 // Regression for #977: each non-wildcard `case … in` arm is one
8185 // ABC condition, matching Python's `case_clause` handling. Using
8186 // literal patterns (no comparison operators) isolates the
8187 // `in_clause` contribution from any operand tokens.
8188 // expected: 2 conditions — one per `in 1` / `in 2` arm.
8189 check_metrics::<RubyParser>(
8190 "def f(x)\n case x\n in 1 then :one\n in 2 then :two\n end\nend\n",
8191 "foo.rb",
8192 |metric| {
8193 assert_eq!(metric.abc.conditions_sum(), 2);
8194 },
8195 );
8196 }
8197
8198 #[cfg(feature = "ruby")]
8199 #[test]
8200 fn ruby_case_match_guarded_wildcard_is_a_condition() {
8201 // Regression for #977: a guarded wildcard arm `in _ if x` is not a
8202 // bare default and counts as one ABC condition, while the trailing
8203 // bare `in _` adds none.
8204 // expected: 2 conditions — the guarded `in _ if x` arm, plus its
8205 // guard slot. The guard predicate is a bare identifier, so before
8206 // #1454 it contributed nothing and the total was 1; the slot is
8207 // what makes `if x`, `if x.even?` and `if x > 0` agree.
8208 check_metrics::<RubyParser>(
8209 "def f(x)\n case x\n in _ if x then :y\n in _ then :default\n end\nend\n",
8210 "foo.rb",
8211 |metric| {
8212 assert_eq!(metric.abc.conditions_sum(), 2);
8213 },
8214 );
8215 }
8216
8217 #[cfg(feature = "ruby")]
8218 #[test]
8219 fn ruby_case_match_bare_wildcard_is_not_a_condition() {
8220 // Regression for #977: a `case … in` whose only arm is the bare
8221 // wildcard `in _` (no guard) is the default arm and contributes no
8222 // ABC condition, keeping ABC and cyclomatic in lockstep on the
8223 // same construct.
8224 // expected: 0 conditions.
8225 check_metrics::<RubyParser>(
8226 "def f(x)\n case x\n in _ then :default\n end\nend\n",
8227 "foo.rb",
8228 |metric| {
8229 assert_eq!(metric.abc.conditions_sum(), 0);
8230 },
8231 );
8232 }
8233
8234 #[cfg(feature = "ruby")]
8235 #[test]
8236 fn ruby_bare_predicate_control_flow_counts_one_condition() {
8237 // Regression for #696: idiomatic Ruby bare predicates
8238 // (`if flag` / `while flag` / `unless flag` / `until flag`) each
8239 // count one unary condition, matching Rust / C# / PHP / Python. The
8240 // condition field is read for both block and modifier forms.
8241 //
8242 // expected: 8 conditions — four block forms (`if`/`unless`/`while`/
8243 // `until`) plus the same four as modifiers, one each.
8244 check_metrics::<RubyParser>(
8245 "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",
8246 "foo.rb",
8247 |metric| {
8248 assert_eq!(metric.abc.conditions_sum(), 8);
8249 },
8250 );
8251 }
8252
8253 #[cfg(feature = "ruby")]
8254 #[test]
8255 fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() {
8256 // `if a == b` counts only the `==` comparison (the condition field
8257 // is a `binary` node, adding nothing). `if a && b` counts the two
8258 // chain operands via the `&&` walker, again with the condition-field
8259 // arm adding nothing — so neither shape is double-counted (#696).
8260 //
8261 // expected: 3 — `==` (1) + the `a`,`b` operands of `&&` (2).
8262 check_metrics::<RubyParser>(
8263 "def f(a, b)\n if a == b\n x\n end\n if a && b\n y\n end\nend\n",
8264 "foo.rb",
8265 |metric| {
8266 assert_eq!(metric.abc.conditions_sum(), 3);
8267 },
8268 );
8269 }
8270
8271 #[cfg(feature = "ruby")]
8272 #[test]
8273 fn ruby_spaceship_and_case_equality() {
8274 // `<=>` and `===` are comparison operators (conditions).
8275 check_metrics::<RubyParser>(
8276 "def f(a, b)\n a <=> b\n a === b\nend\n",
8277 "foo.rb",
8278 |metric| {
8279 assert_eq!(metric.abc.conditions_sum(), 2);
8280 insta::assert_json_snapshot!(metric.abc);
8281 },
8282 );
8283 }
8284
8285 #[cfg(feature = "ruby")]
8286 #[test]
8287 fn ruby_ternary_condition() {
8288 // The `?` ternary marker is one condition; the inner `==` is
8289 // another.
8290 check_metrics::<RubyParser>("def f(x)\n x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
8291 assert_eq!(metric.abc.conditions_sum(), 2);
8292 insta::assert_json_snapshot!(metric.abc);
8293 });
8294 }
8295
8296 // Issue #1161. Ruby's ternary carried only the `?` token arm, so
8297 // `a ? !b : !c` scored 1 against the 4 that Java, C#, Groovy, the C
8298 // family, the JS family, PHP and Perl all report for the same
8299 // expression (#1102) — and `ruby_inspect_container`'s `Conditional`
8300 // boolean-context seed was unreachable for the same reason.
8301 //
8302 // Every expectation below is the value its C++ sibling
8303 // (`cpp_ternary_operand_slots_count_as_unary_conditions`) already
8304 // asserts for the same expression, so the two read as one table.
8305 #[cfg(feature = "ruby")]
8306 #[test]
8307 fn ruby_ternary_operand_slots_count_as_unary_conditions() {
8308 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
8309 check_metrics::<RubyParser>("def f\n x = a ? !b : !c\nend\n", "foo.rb", |metric| {
8310 assert_eq!(metric.abc.conditions_sum(), 4);
8311 });
8312 // No-double-count pin, and the assertion that catches the trap
8313 // this grammar sets: `-b` and `!b` are the SAME node kind
8314 // (`unary:284`), separated only by child(0). Routing the branch
8315 // slots through `ruby_inspect_container` — which tests for the
8316 // `!` token, not for the kind — is what keeps this at 2. An
8317 // implementation keying on `Unary` reads 3 here.
8318 // `?` (1) + `>` (1) = 2, unchanged by the fix: the parenthesised
8319 // condition unwraps to a `binary`, which is not a boolean
8320 // terminal, and neither branch is negated.
8321 check_metrics::<RubyParser>("def f\n x = (a > 0) ? b : -b\nend\n", "foo.rb", |metric| {
8322 assert_eq!(metric.abc.conditions_sum(), 2);
8323 });
8324 // Nested — Ruby needs the inner ternary parenthesised. Outer `?`
8325 // (1) + outer condition `a` (1) + inner `?` (1) + inner
8326 // condition `b` (1) = 4. The outer consequence unwraps to the
8327 // inner `conditional`, which is neither a boolean terminal nor a
8328 // further paren / `!` layer, so it adds nothing on its own; the
8329 // inner ternary is reached by the walk, not by descent.
8330 check_metrics::<RubyParser>(
8331 "def f\n x = a ? (b ? c : d) : e\nend\n",
8332 "foo.rb",
8333 |metric| {
8334 assert_eq!(metric.abc.conditions_sum(), 4);
8335 },
8336 );
8337 // A parenthesised condition, pinning the `is_parens` unwrap on
8338 // the condition slot: `(a)` is `parenthesized_statements`, not a
8339 // boolean terminal, so it reaches the walker's `else` fallback
8340 // and only `ruby_inspect_container` can resolve it.
8341 // `?` (1) + `(a)` (1) + `!b` (1) + `!c` (1) = 4; drop the
8342 // fallback and this reads 3 while every other case here holds.
8343 check_metrics::<RubyParser>("def f\n x = (a) ? !b : !c\nend\n", "foo.rb", |metric| {
8344 assert_eq!(metric.abc.conditions_sum(), 4);
8345 });
8346 // A negated condition takes the same fallback through the `!`
8347 // unwrap rather than the paren one. `?` (1) + `!a` (1) = 2.
8348 check_metrics::<RubyParser>("def f\n x = !a ? b : c\nend\n", "foo.rb", |metric| {
8349 assert_eq!(metric.abc.conditions_sum(), 2);
8350 });
8351 }
8352
8353 // The boolean-context seed must discriminate between the condition
8354 // slot and the two branch slots — not merely exist. None of the
8355 // fixtures above can tell the difference: their branches are either
8356 // `!`-unaries (which set the flag inside the unwrap loop regardless
8357 // of the seed) or kinds the loop breaks on before any terminal test.
8358 // A seed that returned `true` for every slot of a `Conditional`
8359 // leaves all five at their asserted values and fails nothing.
8360 //
8361 // A parenthesised *branch* is the input that separates them: the
8362 // unwrap reaches a bare terminal, so only the seed decides whether
8363 // it counts. `?` (1) + condition `a` (1) = 2 in both directions.
8364 #[cfg(feature = "ruby")]
8365 #[test]
8366 fn ruby_ternary_branch_operands_are_not_double_counted() {
8367 check_metrics::<RubyParser>("def f\n x = a ? (b) : c\nend\n", "foo.rb", |metric| {
8368 assert_eq!(metric.abc.conditions_sum(), 2);
8369 });
8370 check_metrics::<RubyParser>("def f\n x = a ? b : (c)\nend\n", "foo.rb", |metric| {
8371 assert_eq!(metric.abc.conditions_sum(), 2);
8372 });
8373 // The same pair with a comment before the operand. Comments are
8374 // tree-sitter `extras`, so they become the branch's previous
8375 // sibling — which is why the seed asks the grammar which child
8376 // is the `condition` field rather than testing that sibling for
8377 // `?` / `:` as the C family does. Under the token form both of
8378 // these read 3.
8379 check_metrics::<RubyParser>(
8380 "def f\n x = a ?\n # note\n (b) : c\nend\n",
8381 "foo.rb",
8382 |metric| {
8383 assert_eq!(metric.abc.conditions_sum(), 2);
8384 },
8385 );
8386 check_metrics::<RubyParser>(
8387 "def f\n x = a ? b :\n # note\n (c)\nend\n",
8388 "foo.rb",
8389 |metric| {
8390 assert_eq!(metric.abc.conditions_sum(), 2);
8391 },
8392 );
8393 }
8394
8395 #[cfg(feature = "ruby")]
8396 #[test]
8397 fn ruby_case_when_arms() {
8398 // Each `when` named clause and the `else` clause count as one
8399 // condition each; the `case` head and the implicit `then`
8400 // wrappers do not.
8401 check_metrics::<RubyParser>(
8402 "def f(x)\n case x\n when 1 then 'one'\n when 2 then 'two'\n else 'other'\n end\nend\n",
8403 "foo.rb",
8404 |metric| {
8405 // 2 `when` + 1 `else` = 3 conditions.
8406 assert_eq!(metric.abc.conditions_sum(), 3);
8407 insta::assert_json_snapshot!(metric.abc);
8408 },
8409 );
8410 }
8411
8412 #[cfg(feature = "ruby")]
8413 #[test]
8414 fn ruby_elsif_and_else() {
8415 // `elsif` and `else` named clauses are conditions; their inner
8416 // `then` wrappers are not.
8417 check_metrics::<RubyParser>(
8418 "def f(x)\n if x > 0\n 1\n elsif x < 0\n -1\n else\n 0\n end\nend\n",
8419 "foo.rb",
8420 |metric| {
8421 // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
8422 assert_eq!(metric.abc.conditions_sum(), 4);
8423 insta::assert_json_snapshot!(metric.abc);
8424 },
8425 );
8426 }
8427
8428 #[cfg(feature = "ruby")]
8429 #[test]
8430 fn ruby_rescue_clause_condition() {
8431 // The `rescue` named clause is one condition; the `rescue`
8432 // keyword token (`Rescue2`) is not counted on its own.
8433 // `do_it` without parens is an `identifier`, not a `call`, so
8434 // it contributes no branch. `handle(e)` is a `call` (1 branch).
8435 check_metrics::<RubyParser>(
8436 "def f\n begin\n do_it\n rescue StandardError => e\n handle(e)\n end\nend\n",
8437 "foo.rb",
8438 |metric| {
8439 assert_eq!(metric.abc.conditions_sum(), 1);
8440 assert_eq!(metric.abc.branches_sum(), 1);
8441 insta::assert_json_snapshot!(metric.abc);
8442 },
8443 );
8444 }
8445
8446 #[cfg(feature = "ruby")]
8447 #[test]
8448 fn ruby_class_complex_function() {
8449 // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
8450 check_metrics::<RubyParser>(
8451 "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",
8452 "foo.rb",
8453 |metric| {
8454 assert_eq!(metric.abc.assignments_sum(), 1);
8455 assert_eq!(metric.abc.branches_sum(), 1);
8456 // `>`(1) + `==`(1) = 2 conditions. `if` is not a
8457 // token; `&&` is `AMPAMP` and is not counted (see
8458 // the module-level `Stats` doc-comment for the
8459 // cross-language policy; #395, walker tracked in
8460 // #403).
8461 assert_eq!(metric.abc.conditions_sum(), 2);
8462 insta::assert_json_snapshot!(metric.abc);
8463 },
8464 );
8465 }
8466
8467 #[cfg(feature = "ruby")]
8468 #[test]
8469 fn ruby_unary_conditions_in_chain() {
8470 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
8471 // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
8472 // contribute one. Ruby's `if` keyword is not a condition token.
8473 // expected: 3 unary conditions (matches the Java byte-equivalent).
8474 check_metrics::<RubyParser>(
8475 "def f(a, b, c)\n if a && b || c\n puts \"x\"\n end\nend\n",
8476 "foo.rb",
8477 |metric| {
8478 assert_eq!(metric.abc.conditions_sum(), 3);
8479 },
8480 );
8481 }
8482
8483 #[cfg(feature = "ruby")]
8484 #[test]
8485 fn ruby_keyword_and_or_chain_counts_operands() {
8486 // The keyword forms `and` / `or` get the same Rule 9 treatment as
8487 // `&&` / `||`. expected: 3 unary conditions (a, b, c).
8488 check_metrics::<RubyParser>(
8489 "def f(a, b, c)\n if a and b or c\n puts \"x\"\n end\nend\n",
8490 "foo.rb",
8491 |metric| {
8492 assert_eq!(metric.abc.conditions_sum(), 3);
8493 },
8494 );
8495 }
8496
8497 #[cfg(feature = "ruby")]
8498 #[test]
8499 fn ruby_negated_operand_is_unary_condition() {
8500 // A `!`-negated operand unwraps the `unary` node to the inner
8501 // identifier. expected: 2 (`a` and the `!b` operand).
8502 check_metrics::<RubyParser>(
8503 "def f(a, b)\n if a && !b\n puts \"x\"\n end\nend\n",
8504 "foo.rb",
8505 |metric| {
8506 assert_eq!(metric.abc.conditions_sum(), 2);
8507 },
8508 );
8509 }
8510
8511 #[cfg(feature = "ruby")]
8512 #[test]
8513 fn ruby_comparison_operands_add_nothing() {
8514 // Isolation for Rule 9 (issue #557): when the `&&` operands are
8515 // themselves comparisons, the unary-condition walker must add
8516 // nothing on top of the two `>` comparisons already counted as
8517 // conditions — distinguishing the gap (bare boolean operands)
8518 // from ordinary relational conditions. Mirrors the Kotlin and
8519 // Elixir isolation tests. expected: 2 (the two `>` comparisons).
8520 check_metrics::<RubyParser>(
8521 "def f(x, y)\n if x > 0 && y > 0\n puts \"x\"\n end\nend\n",
8522 "foo.rb",
8523 |metric| {
8524 assert_eq!(metric.abc.conditions_sum(), 2);
8525 },
8526 );
8527 }
8528
8529 // ---------------------------------------------------------------
8530 // Default-impl placeholder smoke tests (audited in #188).
8531 //
8532 // These tests assert that the *current* default-impl languages
8533 // return ABC = 0/0/0 for source that DOES contain branches,
8534 // conditions, and assignments. When the real impl lands for any
8535 // of these languages, the corresponding assertion below will fire
8536 // — the implementer must update the expected values, which is the
8537 // gate. Tag the follow-up issue in each test.
8538 // ---------------------------------------------------------------
8539
8540 // --- Python ABC ---------------------------------------------------
8541
8542 #[cfg(feature = "python")]
8543 #[test]
8544 fn python_empty_module_zero() {
8545 check_metrics::<PythonParser>("", "empty.py", |metric| {
8546 assert_eq!(metric.abc.assignments_sum(), 0);
8547 assert_eq!(metric.abc.branches_sum(), 0);
8548 assert_eq!(metric.abc.conditions_sum(), 0);
8549 insta::assert_json_snapshot!(metric.abc);
8550 });
8551 }
8552
8553 #[cfg(feature = "python")]
8554 #[test]
8555 fn python_plain_assignments_count() {
8556 // Three plain `=` assignments → A=3. No branches, no conditions.
8557 check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
8558 assert_eq!(metric.abc.assignments_sum(), 3);
8559 assert_eq!(metric.abc.branches_sum(), 0);
8560 assert_eq!(metric.abc.conditions_sum(), 0);
8561 insta::assert_json_snapshot!(metric.abc);
8562 });
8563 }
8564
8565 #[cfg(feature = "python")]
8566 #[test]
8567 fn python_typed_assignment_counts_bare_annotation_does_not() {
8568 // `x: int = 1` carries an `=`, so it counts.
8569 // `y: int` is a bare annotation (no `=`) — declares a type but
8570 // binds nothing; it must NOT inflate the assignment count.
8571 check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
8572 assert_eq!(metric.abc.assignments_sum(), 1);
8573 insta::assert_json_snapshot!(metric.abc);
8574 });
8575 }
8576
8577 #[cfg(feature = "python")]
8578 #[test]
8579 fn python_augmented_assignments_count() {
8580 // Each augmented op counts once.
8581 check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
8582 // 1 plain `=` + 3 augmented = 4 assignments.
8583 assert_eq!(metric.abc.assignments_sum(), 4);
8584 insta::assert_json_snapshot!(metric.abc);
8585 });
8586 }
8587
8588 #[cfg(feature = "python")]
8589 #[test]
8590 fn python_walrus_counts_as_assignment() {
8591 // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
8592 // → one assignment under Fitzpatrick's rule.
8593 check_metrics::<PythonParser>("if (n := 10) > 5:\n pass\n", "foo.py", |metric| {
8594 // 1 assignment (walrus) + 1 condition (`> 5` is a
8595 // ComparisonOperator).
8596 assert_eq!(metric.abc.assignments_sum(), 1);
8597 assert_eq!(metric.abc.conditions_sum(), 1);
8598 insta::assert_json_snapshot!(metric.abc);
8599 });
8600 }
8601
8602 #[cfg(feature = "python")]
8603 #[test]
8604 fn python_calls_are_branches() {
8605 // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
8606 // → three branches.
8607 check_metrics::<PythonParser>(
8608 "def foo():\n pass\ndef bar():\n pass\nclass Baz:\n pass\nfoo()\nbar()\nBaz()\n",
8609 "foo.py",
8610 |metric| {
8611 assert_eq!(metric.abc.branches_sum(), 3);
8612 assert_eq!(metric.abc.assignments_sum(), 0);
8613 insta::assert_json_snapshot!(metric.abc);
8614 },
8615 );
8616 }
8617
8618 #[cfg(feature = "python")]
8619 #[test]
8620 fn python_comparisons_count_conditions() {
8621 // `x > 0`, `x == y`, `x is None` are each a single
8622 // `ComparisonOperator` node — three conditions.
8623 check_metrics::<PythonParser>(
8624 "def f(x, y):\n a = x > 0\n b = x == y\n c = x is None\n",
8625 "foo.py",
8626 |metric| {
8627 assert_eq!(metric.abc.conditions_sum(), 3);
8628 // 3 plain assignments; the comparisons are operands.
8629 assert_eq!(metric.abc.assignments_sum(), 3);
8630 insta::assert_json_snapshot!(metric.abc);
8631 },
8632 );
8633 }
8634
8635 #[cfg(feature = "python")]
8636 #[test]
8637 fn python_chained_comparison_counts_once() {
8638 // tree-sitter-python collapses `0 < x < 10` into a single
8639 // `ComparisonOperator` — one condition, not two.
8640 check_metrics::<PythonParser>("def f(x):\n return 0 < x < 10\n", "foo.py", |metric| {
8641 assert_eq!(metric.abc.conditions_sum(), 1);
8642 insta::assert_json_snapshot!(metric.abc);
8643 });
8644 }
8645
8646 #[cfg(feature = "python")]
8647 #[test]
8648 fn python_number_truthy_condition_counts() {
8649 // Regression for #772: Python treats every non-zero number as
8650 // truthy, so `if 5:` and `x and 5` should each count their
8651 // numeric literal as a Fitzpatrick unary condition. Pre-fix
8652 // `python_bool_terminal_kinds!()` listed `True` / `False` but
8653 // omitted `Integer` / `Float`, so the walker dropped every
8654 // numeric-truthy operand (mirrors the Lua `Number` fix).
8655 check_metrics::<PythonParser>(
8656 "def f(a):\n if 5:\n pass\n return a and 2\n",
8657 "foo.py",
8658 |metric| {
8659 // `if 5:` → walker counts the Integer literal (+1).
8660 // `a and 2` → `and` walker counts both operands:
8661 // identifier `a` (+1), Integer `2` (+1).
8662 // Total: 3.
8663 assert_eq!(metric.abc.conditions_sum(), 3);
8664 insta::assert_json_snapshot!(metric.abc);
8665 },
8666 );
8667 }
8668
8669 #[cfg(feature = "python")]
8670 #[test]
8671 fn python_boolean_operators_not_counted_directly() {
8672 // Python's `and` / `or` are not counted as conditions on
8673 // their own (Fitzpatrick Rule 5; #395). Each operand is
8674 // instead counted as a unary conditional by the walker
8675 // (Rule 9; #403). `if a and b or c:` parses left-to-right
8676 // with `or` lower precedence: `(a and b) or c`. Walker
8677 // tallies: inner `and` counts `a`, `b` (+2); outer `or`
8678 // counts only the new outer operand `c` (+1; the inner
8679 // `(a and b)` BooleanOperator is not a terminal). Total
8680 // C = 3.
8681 check_metrics::<PythonParser>(
8682 "def f(a, b, c):\n if a and b or c:\n pass\n",
8683 "foo.py",
8684 |metric| {
8685 assert_eq!(metric.abc.conditions_sum(), 3);
8686 insta::assert_json_snapshot!(metric.abc);
8687 },
8688 );
8689 }
8690
8691 /// Python's unary `not` operator parses as `NotOperator` and now
8692 /// counts as one condition, matching Java's `!x` rule. Closes
8693 /// the parity gap noted in #214: without this, `if not flag:`
8694 /// reported 0 conditions while the Java equivalent reports 1.
8695 #[cfg(feature = "python")]
8696 #[test]
8697 fn python_unary_not_counts_as_condition() {
8698 check_metrics::<PythonParser>(
8699 "def f(flag):\n if not flag:\n return 1\n return 0\n",
8700 "foo.py",
8701 |metric| {
8702 // One `NotOperator` -> 1 condition. The `if` itself
8703 // is structural and doesn't add an Abc condition.
8704 assert_eq!(metric.abc.conditions_sum(), 1);
8705 insta::assert_json_snapshot!(metric.abc);
8706 },
8707 );
8708 }
8709
8710 /// `return not flag` — the unary `not` is the entire return
8711 /// expression. Without `NotOperator` counted, this reports zero
8712 /// conditions; with it, one. Java's `return !flag;` is one.
8713 #[cfg(feature = "python")]
8714 #[test]
8715 fn python_return_unary_not_counts() {
8716 check_metrics::<PythonParser>("def f(flag):\n return not flag\n", "foo.py", |metric| {
8717 assert_eq!(metric.abc.conditions_sum(), 1);
8718 insta::assert_json_snapshot!(metric.abc);
8719 });
8720 }
8721
8722 /// `foo(not ready, value)` — the unary `not` inside an argument
8723 /// list still contributes. Mirrors Java's
8724 /// `java_count_unary_conditions` walk over argument lists.
8725 #[cfg(feature = "python")]
8726 #[test]
8727 fn python_unary_not_in_argument_list_counts() {
8728 check_metrics::<PythonParser>(
8729 "def f(ready, value):\n log(not ready, value)\n",
8730 "foo.py",
8731 |metric| {
8732 // 1 Call (log) -> 1 branch.
8733 // 1 NotOperator (not ready) -> 1 condition.
8734 assert_eq!(metric.abc.branches_sum(), 1);
8735 assert_eq!(metric.abc.conditions_sum(), 1);
8736 insta::assert_json_snapshot!(metric.abc);
8737 },
8738 );
8739 }
8740
8741 /// Nested `not` + comparison counts each unique node once.
8742 /// `not (x > 0)` parses as `NotOperator(ParenthesizedExpression(
8743 /// ComparisonOperator))`; both the unary and the comparison
8744 /// contribute one condition (mirrors Java's `!(x > 0)` = 2
8745 /// conditions).
8746 #[cfg(feature = "python")]
8747 #[test]
8748 fn python_unary_not_with_comparison_counts_each_once() {
8749 check_metrics::<PythonParser>(
8750 "def f(x):\n if not (x > 0):\n return 1\n return 0\n",
8751 "foo.py",
8752 |metric| {
8753 // NotOperator (1) + ComparisonOperator (1) = 2.
8754 assert_eq!(metric.abc.conditions_sum(), 2);
8755 insta::assert_json_snapshot!(metric.abc);
8756 },
8757 );
8758 }
8759
8760 /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
8761 /// y)`. The `and` itself is NOT counted (Fitzpatrick Rule 5
8762 /// lists only comparison operators); the `NotOperator` is
8763 /// counted at the top level (Rule 7); and the `y` operand is
8764 /// counted by the Rule 9 walker (issue #403). Total: 2.
8765 /// `NotOperator` is intentionally not walked-into a second
8766 /// time — the walker skips it to avoid double-counting.
8767 #[cfg(feature = "python")]
8768 #[test]
8769 fn python_unary_not_with_boolean_combinator_counts_each() {
8770 check_metrics::<PythonParser>(
8771 "def f(x, y):\n if not x and y:\n return 1\n return 0\n",
8772 "foo.py",
8773 |metric| {
8774 // NotOperator (1) + walker on `and` finds `y` (1) = 2.
8775 assert_eq!(metric.abc.conditions_sum(), 2);
8776 insta::assert_json_snapshot!(metric.abc);
8777 },
8778 );
8779 }
8780
8781 #[cfg(feature = "python")]
8782 #[test]
8783 fn python_control_flow_arms_count_conditions() {
8784 // `elif`, `else`, `except`, `finally`, `case` each contribute
8785 // one condition. The comparisons in the `if`/`elif`/`while`
8786 // headers contribute their own ComparisonOperator counts.
8787 check_metrics::<PythonParser>(
8788 "def f(x):\n if x > 0:\n a = 1\n elif x > -1:\n a = 2\n else:\n a = 3\n",
8789 "foo.py",
8790 |metric| {
8791 // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
8792 // ElifClause + 1 ElseClause = 4 conditions.
8793 assert_eq!(metric.abc.conditions_sum(), 4);
8794 insta::assert_json_snapshot!(metric.abc);
8795 },
8796 );
8797 }
8798
8799 #[cfg(feature = "python")]
8800 #[test]
8801 fn python_ternary_counts_as_condition() {
8802 // `a if c else b` is `ConditionalExpression` → 1 condition.
8803 // `c > 0` adds 1 more (ComparisonOperator).
8804 check_metrics::<PythonParser>(
8805 "def f(c):\n return 1 if c > 0 else 0\n",
8806 "foo.py",
8807 |metric| {
8808 assert_eq!(metric.abc.conditions_sum(), 2);
8809 insta::assert_json_snapshot!(metric.abc);
8810 },
8811 );
8812 }
8813
8814 // Issue #1161. Python counted the `conditional_expression` node but
8815 // never its condition slot, so `a if c() else b` reported 1 where
8816 // the equivalent `c() ? a : b` reports 2 everywhere else — and
8817 // `python_inspect_container`'s `ConditionalExpression` boolean-
8818 // context seed was unreachable, no call site having passed that
8819 // parent.
8820 #[cfg(feature = "python")]
8821 #[test]
8822 fn python_ternary_condition_slot_counts_as_a_unary_condition() {
8823 // ternary (1) + condition `c()` (1) = 2. `c()` is a `Call`, a
8824 // boolean terminal; it also adds one *branch*, not a condition.
8825 check_metrics::<PythonParser>(
8826 "def f(a, b, c):\n return a if c() else b\n",
8827 "foo.py",
8828 |metric| {
8829 assert_eq!(metric.abc.conditions_sum(), 2);
8830 },
8831 );
8832 // A parenthesised condition, pinning the seed line this fix made
8833 // reachable: `(c)` is a `parenthesized_expression`, so only
8834 // `python_inspect_container` can resolve it, and it counts the
8835 // unwrapped terminal only when the parent seeds boolean context.
8836 // ternary (1) + `(c)` (1) = 2.
8837 check_metrics::<PythonParser>(
8838 "def f(a, b, c):\n return a if (c) else b\n",
8839 "foo.py",
8840 |metric| {
8841 assert_eq!(metric.abc.conditions_sum(), 2);
8842 },
8843 );
8844 // A negated condition is *not* counted by the new slot: it is a
8845 // `NotOperator`, which already has its own top-level dispatcher
8846 // arm. ternary (1) + `not c` (1) = 2, not 3.
8847 check_metrics::<PythonParser>(
8848 "def f(a, b, c):\n return a if not c else b\n",
8849 "foo.py",
8850 |metric| {
8851 assert_eq!(metric.abc.conditions_sum(), 2);
8852 },
8853 );
8854 // The cross-language reference case. `(not b) if a else (not c)`
8855 // is the exact semantic equivalent of `a ? !b : !c`, which every
8856 // other language reports as 4: ternary (1) + condition `a` (1) +
8857 // two `NotOperator`s (2). The negated operands come from
8858 // Python's own arm, the condition from the slot added here.
8859 //
8860 // #1161's resolution plan predicted this would stay 3, having
8861 // measured the condition slot's contribution against the pre-fix
8862 // total. 3 would have left Python disagreeing with every other
8863 // language on the reference expression — the gap the issue was
8864 // filed about.
8865 check_metrics::<PythonParser>(
8866 "def f(a, b, c):\n return (not b) if a else (not c)\n",
8867 "foo.py",
8868 |metric| {
8869 assert_eq!(metric.abc.conditions_sum(), 4);
8870 },
8871 );
8872 }
8873
8874 // The double-count pin, and the reason Python gets a condition-slot
8875 // helper rather than a copy of `cpp_walk_ternary`: Python's branch
8876 // operands are counted by the top-level `NotOperator` /
8877 // `ComparisonOperator` arms, a different mechanism from every other
8878 // language's walker. Routing the branch slots through
8879 // `python_inspect_container` as the C family does would count a
8880 // parenthesised operand that the identical unparenthesised
8881 // expression scores at zero.
8882 //
8883 // Both fixtures below are 2 today and 4 under such a copy, so a
8884 // later "make Python consistent with the others" change cannot land
8885 // silently.
8886 #[cfg(feature = "python")]
8887 #[test]
8888 fn python_ternary_branch_operands_are_not_double_counted() {
8889 // ternary (1) + condition `a` (1) = 2. The two parenthesised
8890 // operands add nothing — an unnegated branch is type-free.
8891 check_metrics::<PythonParser>(
8892 "def f(a, b, c):\n return (b) if a else (c)\n",
8893 "foo.py",
8894 |metric| {
8895 assert_eq!(metric.abc.conditions_sum(), 2);
8896 },
8897 );
8898 // The unparenthesised form must agree: nothing about `(b)`
8899 // versus `b` is a condition.
8900 check_metrics::<PythonParser>(
8901 "def f(a, b, c):\n return b if a else c\n",
8902 "foo.py",
8903 |metric| {
8904 assert_eq!(metric.abc.conditions_sum(), 2);
8905 },
8906 );
8907 }
8908
8909 // Comments are tree-sitter `extras`, so they arrive as direct
8910 // children of `conditional_expression` and shift every positional
8911 // index after them. `python_count_ternary_condition` therefore
8912 // anchors on the `if` keyword and skips comments after it; both
8913 // halves are needed and each fixture below fails without one.
8914 #[cfg(feature = "python")]
8915 #[test]
8916 fn python_ternary_condition_survives_an_interposed_comment() {
8917 // Comment before the keyword: `child(2)` is the `if` token here,
8918 // so a positional lookup reads 1. ternary (1) + `f()` (1) = 2.
8919 check_metrics::<PythonParser>(
8920 "def f(b, c):\n return (b\n # why\n if f() else c)\n",
8921 "foo.py",
8922 |metric| {
8923 assert_eq!(metric.abc.conditions_sum(), 2);
8924 },
8925 );
8926 // Comment after the keyword: the child immediately following
8927 // `if` is the comment, so taking the first rather than the first
8928 // non-comment reads 1. ternary (1) + `f()` (1) = 2.
8929 check_metrics::<PythonParser>(
8930 "def f(b, c):\n return (b if\n # why\n f() else c)\n",
8931 "foo.py",
8932 |metric| {
8933 assert_eq!(metric.abc.conditions_sum(), 2);
8934 },
8935 );
8936 }
8937
8938 #[cfg(feature = "python")]
8939 #[test]
8940 fn python_try_except_finally_count_conditions() {
8941 // ExceptClause + FinallyClause → 2 conditions.
8942 check_metrics::<PythonParser>(
8943 "def f():\n try:\n pass\n except ValueError:\n pass\n finally:\n pass\n",
8944 "foo.py",
8945 |metric| {
8946 assert_eq!(metric.abc.conditions_sum(), 2);
8947 insta::assert_json_snapshot!(metric.abc);
8948 },
8949 );
8950 }
8951
8952 #[cfg(feature = "python")]
8953 #[test]
8954 fn python_match_case_counts_conditions() {
8955 // Each non-wildcard `CaseClause` → 1 condition. The bare
8956 // `case _:` arm is the language-neutral `default:` equivalent
8957 // and is excluded (matches Rust's bare-`_` MatchArm filter and
8958 // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
8959 // `case _:` (excluded) → C = 1.
8960 check_metrics::<PythonParser>(
8961 "def f(x):\n match x:\n case 1:\n pass\n case _:\n pass\n",
8962 "foo.py",
8963 |metric| {
8964 assert_eq!(metric.abc.conditions_sum(), 1);
8965 insta::assert_json_snapshot!(metric.abc);
8966 },
8967 );
8968 }
8969
8970 #[cfg(feature = "python")]
8971 #[test]
8972 fn python_match_case_guarded_wildcard_counts() {
8973 // `case _ if g:` is NOT a bare wildcard — the guard
8974 // contributes real branching, so the arm counts as a
8975 // condition. Mirrors Rust's `_ if g => ...` behavior.
8976 // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
8977 // wildcard, counts) + `case _:` (bare wildcard, excluded) →
8978 // C from CaseClause = 2; the guard's `x > 0` adds one
8979 // ComparisonOperator → total C = 3.
8980 check_metrics::<PythonParser>(
8981 "def f(x):\n match x:\n case 1:\n pass\n case _ if x > 0:\n pass\n case _:\n pass\n",
8982 "foo.py",
8983 |metric| {
8984 assert_eq!(metric.abc.conditions_sum(), 3);
8985 insta::assert_json_snapshot!(metric.abc);
8986 },
8987 );
8988 }
8989
8990 #[cfg(feature = "python")]
8991 #[test]
8992 fn python_complex_function_abc() {
8993 // Mixed-shape regression: assignments, calls, conditions all in
8994 // a single function.
8995 check_metrics::<PythonParser>(
8996 "def f(items, threshold):\n\
8997 \x20 result = []\n\
8998 \x20 for item in items:\n\
8999 \x20 if item > threshold:\n\
9000 \x20 result.append(item)\n\
9001 \x20 return result\n",
9002 "foo.py",
9003 |metric| {
9004 // assignments: `result = []` → 1
9005 // branches: `result.append(item)` is one call → 1
9006 // conditions: `item > threshold` is one
9007 // ComparisonOperator → 1
9008 assert_eq!(metric.abc.assignments_sum(), 1);
9009 assert_eq!(metric.abc.branches_sum(), 1);
9010 assert_eq!(metric.abc.conditions_sum(), 1);
9011 insta::assert_json_snapshot!(metric.abc);
9012 },
9013 );
9014 }
9015
9016 #[cfg(feature = "python")]
9017 #[test]
9018 fn python_if_multiple_conditions() {
9019 // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403).
9020 // - `if a or b or c or d:` → 4 (each operand counted once)
9021 // - `if a and b and c:` → 3
9022 // - `if not a and not b:` → 2 (two `NotOperator`s counted
9023 // by the top-level dispatcher arm; the walker SKIPS
9024 // `NotOperator` children to avoid double-counting)
9025 // Total: 4 + 3 + 2 = 9.
9026 check_metrics::<PythonParser>(
9027 "def f(a, b, c, d):\n\
9028 \x20 if a or b or c or d: # +4c\n\
9029 \x20 pass\n\
9030 \x20 if a and b and c: # +3c\n\
9031 \x20 pass\n\
9032 \x20 if not a and not b: # +2c (NotOperator x2)\n\
9033 \x20 pass\n",
9034 "foo.py",
9035 |metric| {
9036 assert_eq!(metric.abc.conditions_sum(), 9);
9037 insta::assert_json_snapshot!(metric.abc);
9038 },
9039 );
9040 }
9041
9042 #[cfg(feature = "python")]
9043 #[test]
9044 fn python_while_conditions() {
9045 // Python has no `do { ... } while(cond);` construct, so this
9046 // mirrors only the `while` half of the Java suite. The
9047 // walker fires on each `and` / `or` token inside the loop
9048 // header.
9049 check_metrics::<PythonParser>(
9050 "def f(a, b):\n\
9051 \x20 while a or b: # +2c\n\
9052 \x20 break\n\
9053 \x20 while a and not b: # +2c (a + NotOperator)\n\
9054 \x20 break\n",
9055 "foo.py",
9056 |metric| {
9057 assert_eq!(metric.abc.conditions_sum(), 4);
9058 insta::assert_json_snapshot!(metric.abc);
9059 },
9060 );
9061 }
9062
9063 #[cfg(feature = "python")]
9064 #[test]
9065 fn python_short_circuit_with_boolean_literal_operand() {
9066 // `a and True` reports 2 conditions: one identifier, one
9067 // True literal. Confirms `True` / `False` are in the walker
9068 // terminal set.
9069 check_metrics::<PythonParser>("def f(a):\n return a and True\n", "foo.py", |metric| {
9070 assert_eq!(metric.abc.conditions_sum(), 2);
9071 insta::assert_json_snapshot!(metric.abc);
9072 });
9073 }
9074
9075 #[cfg(feature = "python")]
9076 #[test]
9077 fn python_await_expression_condition_counts() {
9078 // Regression for findings.md round-2 #2 (Python):
9079 // `if await ready(): pass` parses with `await` as the
9080 // condition node. Adding `Python::Await` to the
9081 // terminal-bool set mirrors the C# reference (lesson 19).
9082 check_metrics::<PythonParser>(
9083 "async def ready(): return True\n\
9084 async def f():\n if await ready(): pass\n",
9085 "foo.py",
9086 |metric| {
9087 // ready() is a call (1 branch); await is the
9088 // condition (1).
9089 assert_eq!(metric.abc.branches_sum(), 1);
9090 assert_eq!(metric.abc.conditions_sum(), 1);
9091 insta::assert_json_snapshot!(metric.abc);
9092 },
9093 );
9094 }
9095
9096 #[cfg(feature = "python")]
9097 #[test]
9098 fn python_if_call_terminal_condition_counts_once() {
9099 // Pins the Phase-2B behaviour for Python's `Call` terminal-bool
9100 // kind: `if foo():` is a Fitzpatrick Rule 6 unary conditional
9101 // (a bare boolean-evaluating call as the if-condition). The
9102 // walker's terminal-at-top check fires once per call-condition;
9103 // the call itself separately contributes 1 branch. Surfaced
9104 // (and verified intentional) by the code-review pass on
9105 // Phase 2B.
9106 check_metrics::<PythonParser>("def f():\n if foo(): pass\n", "foo.py", |metric| {
9107 assert_eq!(metric.abc.branches_sum(), 1);
9108 assert_eq!(metric.abc.conditions_sum(), 1);
9109 insta::assert_json_snapshot!(metric.abc);
9110 });
9111 }
9112
9113 #[cfg(feature = "python")]
9114 #[test]
9115 fn python_if_boolean_literal_condition() {
9116 // Phase 2B (issue #403): bare-boolean conditions count once.
9117 // Python has no paren wrap around if-conditions, so the
9118 // condition node is checked directly. The existing
9119 // NotOperator / ComparisonOperator arms continue to fire
9120 // for those shapes; only the bare-terminal cases (Identifier,
9121 // True, False, etc.) are added by the new arm.
9122 check_metrics::<PythonParser>(
9123 "def f(a):\n\
9124 \x20 if True: pass # +1c\n\
9125 \x20 if False: pass # +1c\n\
9126 \x20 while True: break # +1c\n\
9127 \x20 if a: pass # +1c (Rule 6 — bare identifier as condition)\n",
9128 "foo.py",
9129 |metric| {
9130 assert_eq!(metric.abc.conditions_sum(), 4);
9131 insta::assert_json_snapshot!(metric.abc);
9132 },
9133 );
9134 }
9135
9136 #[cfg(feature = "python")]
9137 #[test]
9138 fn python_methods_arguments_with_conditions() {
9139 // `m(not a, not b)` reports 2 conditions — both `NotOperator`
9140 // nodes are counted by Python's pre-existing top-level
9141 // NotOperator dispatcher arm. The argument-list walker does
9142 // not need a separate Python arm.
9143 check_metrics::<PythonParser>(
9144 "def f(a, b):\n\
9145 \x20 m(a, b) # +1b\n\
9146 \x20 m(not a, not b) # +1b +2c\n",
9147 "foo.py",
9148 |metric| {
9149 assert_eq!(metric.abc.branches_sum(), 2);
9150 assert_eq!(metric.abc.conditions_sum(), 2);
9151 insta::assert_json_snapshot!(metric.abc);
9152 },
9153 );
9154 }
9155
9156 #[cfg(feature = "python")]
9157 #[test]
9158 fn python_return_with_conditions() {
9159 // Phase 2B (issue #403). Python uses the pre-existing top-
9160 // level NotOperator / ComparisonOperator arms for return
9161 // expressions; no dedicated ReturnStatement walker arm is
9162 // needed.
9163 check_metrics::<PythonParser>(
9164 "def m1(z): return not (z >= 0)\n\
9165 def m2(x): return (((not x)))\n\
9166 def m3(x, y): return x and y\n",
9167 "foo.py",
9168 |metric| {
9169 // m1: NotOperator (1) + ComparisonOperator (1) = 2.
9170 // m2: NotOperator (1).
9171 // m3: walker on `and` counts both operands = 2.
9172 // Sum: 5.
9173 assert_eq!(metric.abc.conditions_sum(), 5);
9174 insta::assert_json_snapshot!(metric.abc);
9175 },
9176 );
9177 }
9178
9179 #[cfg(feature = "rust")]
9180 #[test]
9181 fn rust_empty_unit_zero() {
9182 // No code at all → A=B=C=0. Establishes the trait is wired up
9183 // and the per-language compute is reachable.
9184 check_metrics::<RustParser>("", "empty.rs", |metric| {
9185 assert_eq!(metric.abc.assignments_sum(), 0);
9186 assert_eq!(metric.abc.branches_sum(), 0);
9187 assert_eq!(metric.abc.conditions_sum(), 0);
9188 insta::assert_json_snapshot!(metric.abc);
9189 });
9190 }
9191
9192 #[cfg(feature = "rust")]
9193 #[test]
9194 fn rust_assignments_let_init_plain_and_compound() {
9195 // `let mut x = 0` is a `let_declaration` carrying an `=`
9196 // initializer → counts as 1 (matches Fitzpatrick's literal
9197 // "every `=` is an assignment" rule and the JS impl's
9198 // treatment of `let x = 5`). `x = 5` and `x = 7` are plain
9199 // `=` assignments → 2. `x += 2` is a compound assignment → 1.
9200 // Total A = 4.
9201 check_metrics::<RustParser>(
9202 "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
9203 "foo.rs",
9204 |metric| {
9205 assert_eq!(metric.abc.assignments_sum(), 4);
9206 assert_eq!(metric.abc.branches_sum(), 0);
9207 assert_eq!(metric.abc.conditions_sum(), 0);
9208 insta::assert_json_snapshot!(metric.abc);
9209 },
9210 );
9211 }
9212
9213 #[cfg(feature = "rust")]
9214 #[test]
9215 fn rust_let_without_initializer_does_not_count() {
9216 // `let a;` is a `let_declaration` with NO `=` and no `value`
9217 // field — the binding is uninitialised. The arm only fires
9218 // when `value` is present, so this contributes zero to A.
9219 // `let _b;` is the same shape (the `_` pattern is still a
9220 // pattern, not a wildcard suppression of the binding).
9221 // Regression test for issue #393: only `=` counts, not the
9222 // bare declaration.
9223 check_metrics::<RustParser>(
9224 "fn f() { let a: i32; let _b: i32; a = 5; }",
9225 "foo.rs",
9226 |metric| {
9227 // Only `a = 5` (assignment_expression) → A = 1.
9228 assert_eq!(metric.abc.assignments_sum(), 1);
9229 insta::assert_json_snapshot!(metric.abc);
9230 },
9231 );
9232 }
9233
9234 #[cfg(feature = "rust")]
9235 #[test]
9236 fn rust_let_initializers_immutable_and_mutable_count() {
9237 // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`,
9238 // `let mut d = 0;` are all `let_declaration` nodes carrying
9239 // an `=` initializer — each counts as 1 (Option B in the
9240 // issue body: literal Fitzpatrick, both `let` and `let mut`
9241 // count). `d = 5;` is one plain assignment_expression, `d
9242 // += 1;` is one compound. Total A = 4 + 1 + 1 = 6.
9243 check_metrics::<RustParser>(
9244 "fn f() { let a=1; let b=2; let c=a+b; let mut d=0; d=5; d+=1; }",
9245 "foo.rs",
9246 |metric| {
9247 assert_eq!(metric.abc.assignments_sum(), 6);
9248 insta::assert_json_snapshot!(metric.abc);
9249 },
9250 );
9251 }
9252
9253 #[cfg(feature = "rust")]
9254 #[test]
9255 fn rust_calls_are_branches() {
9256 // Free function call + method call (parses as call_expression
9257 // with a field_expression callee) + associated-fn call. All
9258 // three are `call_expression` → B = 3. Macro invocations like
9259 // `println!` parse as `macro_invocation`, NOT `call_expression`,
9260 // so they are not branches.
9261 check_metrics::<RustParser>(
9262 "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
9263 "foo.rs",
9264 |metric| {
9265 assert_eq!(metric.abc.branches_sum(), 3);
9266 assert_eq!(metric.abc.assignments_sum(), 0);
9267 assert_eq!(metric.abc.conditions_sum(), 0);
9268 insta::assert_json_snapshot!(metric.abc);
9269 },
9270 );
9271 }
9272
9273 #[cfg(feature = "rust")]
9274 #[test]
9275 fn rust_try_operator_is_branch() {
9276 // `?` parses as `try_expression` and counts as one branch
9277 // (short-circuit return on Err / None). The `Err(())` call
9278 // contributes one branch in addition (call_expression).
9279 check_metrics::<RustParser>(
9280 "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
9281 "foo.rs",
9282 |metric| {
9283 // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
9284 assert_eq!(metric.abc.branches_sum(), 3);
9285 insta::assert_json_snapshot!(metric.abc);
9286 },
9287 );
9288 }
9289
9290 #[cfg(feature = "rust")]
9291 #[test]
9292 fn rust_comparisons_count_conditions() {
9293 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
9294 // comparisons → C = 6.
9295 check_metrics::<RustParser>(
9296 "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
9297 "foo.rs",
9298 |metric| {
9299 assert_eq!(metric.abc.conditions_sum(), 6);
9300 insta::assert_json_snapshot!(metric.abc);
9301 },
9302 );
9303 }
9304
9305 #[cfg(feature = "rust")]
9306 #[test]
9307 fn rust_generic_brackets_not_conditions() {
9308 // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
9309 // comparison operators. The parent-check in the LT/GT arms
9310 // must filter them out. Expected C = 0.
9311 check_metrics::<RustParser>(
9312 "fn f() -> Vec<i32> { Vec::<i32>::new() }",
9313 "foo.rs",
9314 |metric| {
9315 assert_eq!(metric.abc.conditions_sum(), 0);
9316 insta::assert_json_snapshot!(metric.abc);
9317 },
9318 );
9319 }
9320
9321 #[cfg(feature = "rust")]
9322 #[test]
9323 fn rust_if_let_counts_as_condition() {
9324 // `if let Some(v) = opt { ... }` introduces a `let_condition`
9325 // → 1 condition. The `if` keyword itself does not add another
9326 // count — Fitzpatrick counts conditions, not branch keywords.
9327 check_metrics::<RustParser>(
9328 "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
9329 "foo.rs",
9330 |metric| {
9331 assert_eq!(metric.abc.conditions_sum(), 1);
9332 insta::assert_json_snapshot!(metric.abc);
9333 },
9334 );
9335 }
9336
9337 #[cfg(feature = "rust")]
9338 #[test]
9339 fn rust_while_let_counts_as_condition() {
9340 // `while let Some(y) = it.next() { ... }` is also a
9341 // `let_condition` (the `while` form). One condition; the
9342 // `it.next()` call adds one branch.
9343 check_metrics::<RustParser>(
9344 "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
9345 "foo.rs",
9346 |metric| {
9347 assert_eq!(metric.abc.conditions_sum(), 1);
9348 assert_eq!(metric.abc.branches_sum(), 1);
9349 insta::assert_json_snapshot!(metric.abc);
9350 },
9351 );
9352 }
9353
9354 #[cfg(feature = "rust")]
9355 #[test]
9356 fn rust_match_arms_count_conditions_wildcard_excluded() {
9357 // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
9358 // bare wildcard is the `default:` equivalent and is skipped.
9359 // The guarded arm has a `n if n > 0` pattern (more than one
9360 // child in the match_pattern) and still counts. Two non-wildcard
9361 // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
9362 // adds one more → C = 3.
9363 check_metrics::<RustParser>(
9364 "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
9365 "foo.rs",
9366 |metric| {
9367 assert_eq!(metric.abc.conditions_sum(), 3);
9368 insta::assert_json_snapshot!(metric.abc);
9369 },
9370 );
9371 }
9372
9373 #[cfg(feature = "rust")]
9374 #[test]
9375 fn rust_else_counts_as_condition() {
9376 // `if a > b { ... } else { ... }` → `a > b` is one condition,
9377 // `else` is one condition → C = 2.
9378 check_metrics::<RustParser>(
9379 "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
9380 "foo.rs",
9381 |metric| {
9382 assert_eq!(metric.abc.conditions_sum(), 2);
9383 insta::assert_json_snapshot!(metric.abc);
9384 },
9385 );
9386 }
9387
9388 #[cfg(feature = "rust")]
9389 #[test]
9390 fn rust_let_chain2_hidden_rule_drift_marker() {
9391 // Drift marker (findings.md round-2 #3): `Rust::LetChain2`
9392 // maps to the hidden grammar rule `_let_chain`. At the
9393 // pinned tree-sitter-rust version it is never emitted as a
9394 // concrete node — the visible `LetChain` (= 352) carries
9395 // every let-chain. We list `LetChain2` defensively in
9396 // `rust_inspect_container` and `rust_count_unary_conditions`
9397 // (lesson 34); if a future grammar bump promotes
9398 // `_let_chain` to a visible rule, this assertion fails
9399 // loudly so the maintainer knows to verify the walker still
9400 // counts correctly for the new shape.
9401 let src = "fn f(a: bool, b: Option<i32>) {\n\
9402 \x20 if a && let Some(_) = b { }\n\
9403 }\n";
9404 let parser = RustParser::new(
9405 src.as_bytes().to_vec(),
9406 &std::path::PathBuf::from("foo.rs"),
9407 None,
9408 );
9409 assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16));
9410 }
9411
9412 #[cfg(feature = "rust")]
9413 #[test]
9414 fn rust_scoped_identifier_condition_counts() {
9415 // Regression for findings.md round-2 #1 (Rust):
9416 // `if crate::FLAG {}` parses with `scoped_identifier` as the
9417 // condition node. Pre-fix, `rust_bool_terminal_kinds!()`
9418 // listed only `Identifier` so the walker reached the
9419 // `scoped_identifier` child, found it non-terminal /
9420 // non-paren / non-unary, and broke without counting.
9421 // Mirrors the C# fix in #372 (lesson 19) for
9422 // `MemberAccessExpression`.
9423 check_metrics::<RustParser>("fn f() { if crate::FLAG { } }\n", "foo.rs", |metric| {
9424 assert_eq!(metric.abc.conditions_sum(), 1);
9425 insta::assert_json_snapshot!(metric.abc);
9426 });
9427 }
9428
9429 #[cfg(feature = "rust")]
9430 #[test]
9431 fn rust_await_expression_condition_counts() {
9432 // Regression for findings.md round-2 #2 (Rust):
9433 // `if ready().await {}` parses with `await_expression` as
9434 // the condition node. Adding `Rust::AwaitExpression` to the
9435 // terminal-bool set closes the parity gap with the C#
9436 // reference (`csharp_bool_terminal_kinds!()`).
9437 check_metrics::<RustParser>(
9438 "async fn ready() -> bool { true }\n\
9439 async fn f() { if ready().await { } }\n",
9440 "foo.rs",
9441 |metric| {
9442 // ready() is a call (1 branch); `ready().await` is
9443 // the unary boolean condition (1).
9444 assert_eq!(metric.abc.branches_sum(), 1);
9445 assert_eq!(metric.abc.conditions_sum(), 1);
9446 insta::assert_json_snapshot!(metric.abc);
9447 },
9448 );
9449 }
9450
9451 #[cfg(feature = "rust")]
9452 #[test]
9453 fn rust_complex_function_abc() {
9454 // Mixed-shape regression: assignments, calls, conditions, `?`,
9455 // `if let`, `match` in one body. Verified by hand:
9456 // - assignments: `let mut x = 0` (let init), `x = 5`, `x += 2`,
9457 // `let _ = ...` (let init), `let r: ... = Err(())` (let init),
9458 // `let _v = r?` (let init) → A = 6 (post-#393: every `=`
9459 // initializer in a `let_declaration` is one assignment, in
9460 // line with the literal Fitzpatrick reading).
9461 // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
9462 // (3 calls + 1 try).
9463 // - conditions: `if let Some(v) = opt` → 1, `match x` arms
9464 // `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
9465 // Total C = 4.
9466 check_metrics::<RustParser>(
9467 "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
9468 \x20 let mut x = 0;\n\
9469 \x20 x = 5;\n\
9470 \x20 x += 2;\n\
9471 \x20 if let Some(_v) = opt { }\n\
9472 \x20 let _ = xs.iter().next();\n\
9473 \x20 let r: Result<i32, ()> = Err(());\n\
9474 \x20 let _v = r?;\n\
9475 \x20 Ok(match x {\n\
9476 \x20 0 => 1,\n\
9477 \x20 n if n > 0 => n,\n\
9478 \x20 _ => -1,\n\
9479 \x20 })\n\
9480 }\n",
9481 "foo.rs",
9482 |metric| {
9483 assert_eq!(metric.abc.assignments_sum(), 6);
9484 // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
9485 // plus 1 try (`r?`) → 5 branches.
9486 assert_eq!(metric.abc.branches_sum(), 5);
9487 // 1 let_condition + 2 non-wildcard match_arms + 1
9488 // comparison (`n > 0`) → 4.
9489 assert_eq!(metric.abc.conditions_sum(), 4);
9490 insta::assert_json_snapshot!(metric.abc);
9491 },
9492 );
9493 }
9494
9495 #[cfg(feature = "rust")]
9496 #[test]
9497 fn rust_let_chain_bare_identifier_operand_counts() {
9498 // Regression: pre-fix, `if a && let Some(_z) = y { }` reported
9499 // 1 condition (only the LetCondition). The bare-identifier
9500 // `a` operand was lost because Rust 2024 wraps let-chain
9501 // `&&` operands in a `LetChain` node (not `BinaryExpression`)
9502 // and `rust_count_unary_conditions` only counted terminals
9503 // under a `BinaryExpression` parent. Allowing `LetChain` /
9504 // `LetChain2` as known-bool list parents fixes the loss.
9505 // Expected: LetCondition (1) + walker on `a` (1) = 2.
9506 check_metrics::<RustParser>(
9507 "fn f(a: bool, y: Option<i32>) {\n\
9508 \x20 if a && let Some(_z) = y { }\n\
9509 }\n",
9510 "foo.rs",
9511 |metric| {
9512 assert_eq!(metric.abc.conditions_sum(), 2);
9513 insta::assert_json_snapshot!(metric.abc);
9514 },
9515 );
9516 }
9517
9518 #[cfg(feature = "rust")]
9519 #[test]
9520 fn rust_if_multiple_conditions() {
9521 // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of
9522 // a `&&` / `||` chain is one condition. Mirrors
9523 // `java_if_multiple_conditions`. Rust's `if` head has no
9524 // parentheses, but the walker fires on each `&&` / `||` token
9525 // and walks the parent `binary_expression` regardless.
9526 check_metrics::<RustParser>(
9527 "fn f(a: bool, b: bool, c: bool, d: bool) -> i32 {\n\
9528 \x20 if a || b || c || d { return 1; } // +4c\n\
9529 \x20 if a && b && c { return 2; } // +3c\n\
9530 \x20 if !a && !b { return 3; } // +2c\n\
9531 \x20 0\n\
9532 }\n",
9533 "foo.rs",
9534 |metric| {
9535 // 4 + 3 + 2 = 9
9536 assert_eq!(metric.abc.conditions_sum(), 9);
9537 insta::assert_json_snapshot!(metric.abc);
9538 },
9539 );
9540 }
9541
9542 #[cfg(feature = "rust")]
9543 #[test]
9544 fn rust_while_conditions() {
9545 // Rust has no `do { ... } while(cond);` construct, so this
9546 // mirrors only the `while` half of `java_while_and_do_while_conditions`.
9547 // Each operand of the `&&` / `||` chain in the loop condition
9548 // counts as one Fitzpatrick condition (Rule 7).
9549 check_metrics::<RustParser>(
9550 "fn f(a: bool, b: bool) {\n\
9551 \x20 while a || b { break; } // +2c\n\
9552 \x20 while a && !b { break; } // +2c\n\
9553 }\n",
9554 "foo.rs",
9555 |metric| {
9556 assert_eq!(metric.abc.conditions_sum(), 4);
9557 insta::assert_json_snapshot!(metric.abc);
9558 },
9559 );
9560 }
9561
9562 #[cfg(feature = "rust")]
9563 #[test]
9564 fn rust_if_boolean_literal_condition() {
9565 // Phase 2B (issue #403): a condition whose entire body is a
9566 // boolean literal counts as one Fitzpatrick condition.
9567 // `if true {}` → 1, `if !false {}` → 1 (unary unwrap), and
9568 // `while true { break }` → 1.
9569 check_metrics::<RustParser>(
9570 "fn f() {\n\
9571 \x20 if true { } // +1c\n\
9572 \x20 if !false { } // +1c\n\
9573 \x20 while true { break; } // +1c\n\
9574 }\n",
9575 "foo.rs",
9576 |metric| {
9577 assert_eq!(metric.abc.conditions_sum(), 3);
9578 insta::assert_json_snapshot!(metric.abc);
9579 },
9580 );
9581 }
9582
9583 #[cfg(feature = "rust")]
9584 #[test]
9585 fn rust_methods_arguments_with_conditions() {
9586 // Phase 2B (issue #403): unary-conditional arguments to a
9587 // call each count once. `m(!a, !b)` → 2 conditions + 1
9588 // branch (the call itself). Bare identifier arguments do
9589 // NOT count (they reach the count_unary_conditions list with
9590 // list_kind = Arguments, not BinaryExpression).
9591 check_metrics::<RustParser>(
9592 "fn f(a: bool, b: bool) {\n\
9593 \x20 m(a, b); // +1b\n\
9594 \x20 m(!a, !b); // +1b +2c\n\
9595 \x20 m(!a, b, !a); // +1b +2c\n\
9596 }\n",
9597 "foo.rs",
9598 |metric| {
9599 assert_eq!(metric.abc.branches_sum(), 3);
9600 assert_eq!(metric.abc.conditions_sum(), 4);
9601 insta::assert_json_snapshot!(metric.abc);
9602 },
9603 );
9604 }
9605
9606 #[cfg(feature = "rust")]
9607 #[test]
9608 fn rust_return_with_conditions() {
9609 // Phase 2B (issue #403). Mirrors `java_return_with_conditions`
9610 // — `return !a` / `return x && y` count their unary
9611 // conditional operands. Per Fitzpatrick Rule 7, a `!`-wrapped
9612 // relational expression contributes ONE condition (the
9613 // relational op itself) — the `!` does not add a second
9614 // count when its operand is already a comparison.
9615 check_metrics::<RustParser>(
9616 "fn m1(z: i32) -> bool { return !(z >= 0); }\n\
9617 fn m2(x: bool) -> bool { return (((!x))); }\n\
9618 fn m3(x: bool, y: bool) -> bool { return x && y; }\n\
9619 fn m4(y: bool, z: i32) -> bool { return y || (z < 0); }\n",
9620 "foo.rs",
9621 |metric| {
9622 // m1: !(z >= 0) → the `>=` contributes 1; the unary
9623 // `!` wraps a paren'd BinaryExpression, which
9624 // inspect_container does not unwrap further →
9625 // no walker count. Total: 1.
9626 // m2: (((!x))) → ReturnExpression arm walks (((!x))).
9627 // inspect_container unwraps three parens and one
9628 // unary, reaches Identifier `x`, has_boolean_content
9629 // was seeded true by the unary-not flip. +1.
9630 // m3: x && y → `&&` walker counts both terminals → 2.
9631 // m4: y || (z < 0) → `||` walker counts `y` (terminal,
9632 // +1); the `<` contributes 1 via its own arm; the
9633 // paren'd BinaryExpression `(z < 0)` is not
9634 // terminal under the walker → no extra count.
9635 // Total: 2.
9636 // Sum: 1 + 1 + 2 + 2 = 6.
9637 assert_eq!(metric.abc.conditions_sum(), 6);
9638 insta::assert_json_snapshot!(metric.abc);
9639 },
9640 );
9641 }
9642
9643 #[cfg(feature = "rust")]
9644 #[test]
9645 fn rust_short_circuit_with_boolean_literal_operand() {
9646 // `if a && true` reports 2 conditions: one for the identifier
9647 // operand, one for the boolean-literal operand. Confirms the
9648 // walker terminal set includes `BooleanLiteral`.
9649 check_metrics::<RustParser>(
9650 "fn f(a: bool) -> bool { a && true }\n",
9651 "foo.rs",
9652 |metric| {
9653 assert_eq!(metric.abc.conditions_sum(), 2);
9654 insta::assert_json_snapshot!(metric.abc);
9655 },
9656 );
9657 }
9658
9659 // ----- Go -----
9660
9661 #[cfg(feature = "go")]
9662 #[test]
9663 fn go_empty_unit_zero() {
9664 // Package declaration only — no Fitzpatrick events. Confirms the
9665 // GoCode Abc trait is wired up and emits zero counts.
9666 check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
9667 assert_eq!(metric.abc.assignments_sum(), 0);
9668 assert_eq!(metric.abc.branches_sum(), 0);
9669 assert_eq!(metric.abc.conditions_sum(), 0);
9670 insta::assert_json_snapshot!(metric.abc);
9671 });
9672 }
9673
9674 #[cfg(feature = "go")]
9675 #[test]
9676 fn go_assignments_count_plain_compound_short_var_and_incdec() {
9677 // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
9678 // `x += 2` (compound), `x++` (inc), and the initialized
9679 // declaration `var y = 1` — which is counted, matching the Rust
9680 // and Java rules for `let y = 1` / `int y = 1` (both measured at
9681 // one assignment each). The comment here previously claimed the
9682 // opposite and pinned Go at 6 (#1278).
9683 check_metrics::<GoParser>(
9684 "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
9685 "foo.go",
9686 |metric| {
9687 // `_ = y` is itself an assignment_statement → +1.
9688 // var y=1 + _=y + x:= + x=5 + x+=2 + x=7 + x++ → 7
9689 assert_eq!(metric.abc.assignments_sum(), 7);
9690 assert_eq!(metric.abc.branches_sum(), 0);
9691 assert_eq!(metric.abc.conditions_sum(), 0);
9692 insta::assert_json_snapshot!(metric.abc);
9693 },
9694 );
9695 }
9696
9697 #[cfg(feature = "go")]
9698 #[test]
9699 fn go_var_declarations_count_only_when_initialized() {
9700 // Regression for #1278: a `var` declaration with an initializer is
9701 // a `var_spec` carrying a `value` field, not an
9702 // `assignment_statement` or `short_var_declaration`, so it scored
9703 // zero — `var x = 5` and `x := 5` are the same binding spelled two
9704 // ways. Both typed and untyped initializers count; an
9705 // uninitialized `var z int` and a `const` do not.
9706 // expected: 3 assignments — `var x = 5`, `var y int = 6`, `z := 7`;
9707 // `var w int` and `const c = 1` contribute nothing.
9708 check_metrics::<GoParser>(
9709 "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",
9710 "foo.go",
9711 |metric| {
9712 assert_eq!(metric.abc.assignments_sum(), 3);
9713 assert_eq!(metric.abc.branches_sum(), 0);
9714 },
9715 );
9716 }
9717
9718 #[cfg(feature = "go")]
9719 #[test]
9720 fn go_grouped_var_block_counts_each_initialized_spec() {
9721 // A grouped `var ( … )` block is one `var_declaration` holding one
9722 // `var_spec` per line, so matching the spec counts each initialized
9723 // line on its own. A multi-name spec is still one binding
9724 // statement, matching `p, q := 1, 2` (#1278).
9725 // expected: 3 assignments — `a = 1`, `p, q = 1, 2`, and `r := 0`;
9726 // the uninitialized `b int` contributes nothing.
9727 check_metrics::<GoParser>(
9728 "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",
9729 "foo.go",
9730 |metric| {
9731 // The trailing `_ = …` is itself an assignment_statement.
9732 assert_eq!(metric.abc.assignments_sum(), 4);
9733 },
9734 );
9735 }
9736
9737 #[cfg(feature = "go")]
9738 #[test]
9739 fn go_calls_are_branches() {
9740 // Three calls: free function `g()`, method call `r.Inc()`, and
9741 // builtin call `len(s)`. All parse as `call_expression` → B = 3.
9742 // Composite literal `Foo{}` is NOT a call.
9743 check_metrics::<GoParser>(
9744 "package main\n\
9745 type R struct{}\n\
9746 func (r R) Inc() {}\n\
9747 func g() {}\n\
9748 func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
9749 "foo.go",
9750 |metric| {
9751 assert_eq!(metric.abc.branches_sum(), 3);
9752 insta::assert_json_snapshot!(metric.abc);
9753 },
9754 );
9755 }
9756
9757 #[cfg(feature = "go")]
9758 #[test]
9759 fn go_comparisons_count_conditions() {
9760 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
9761 // comparisons → C = 6.
9762 check_metrics::<GoParser>(
9763 "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
9764 "foo.go",
9765 |metric| {
9766 assert_eq!(metric.abc.conditions_sum(), 6);
9767 insta::assert_json_snapshot!(metric.abc);
9768 },
9769 );
9770 }
9771
9772 #[cfg(feature = "go")]
9773 #[test]
9774 fn go_generic_brackets_not_conditions() {
9775 // Generic instantiation `Min[int](a, b)` puts `int` inside
9776 // `TypeArguments`, not `BinaryExpression`. The parent guard on
9777 // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
9778 check_metrics::<GoParser>(
9779 "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
9780 "foo.go",
9781 |metric| {
9782 assert_eq!(metric.abc.conditions_sum(), 0);
9783 assert_eq!(metric.abc.branches_sum(), 1);
9784 insta::assert_json_snapshot!(metric.abc);
9785 },
9786 );
9787 }
9788
9789 #[cfg(feature = "go")]
9790 #[test]
9791 fn go_switch_arms_count_conditions_default_excluded() {
9792 // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
9793 // bare `default` is the C/Java `default:` equivalent and is
9794 // excluded — 3 conditions from ExpressionCase. The switch
9795 // expression `x` is bare (no comparison), so no extra
9796 // condition from `==`-style operators.
9797 check_metrics::<GoParser>(
9798 "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",
9799 "foo.go",
9800 |metric| {
9801 assert_eq!(metric.abc.conditions_sum(), 3);
9802 insta::assert_json_snapshot!(metric.abc);
9803 },
9804 );
9805 }
9806
9807 #[cfg(feature = "go")]
9808 #[test]
9809 fn go_type_switch_arms_count_conditions() {
9810 // Type switch: `case int:`, `case string:`, `default:`. Two
9811 // non-default type-case arms → C = 2.
9812 check_metrics::<GoParser>(
9813 "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
9814 "foo.go",
9815 |metric| {
9816 assert_eq!(metric.abc.conditions_sum(), 2);
9817 insta::assert_json_snapshot!(metric.abc);
9818 },
9819 );
9820 }
9821
9822 #[cfg(feature = "go")]
9823 #[test]
9824 fn go_select_arms_count_conditions() {
9825 // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
9826 // Two non-default communication cases → C = 2.
9827 check_metrics::<GoParser>(
9828 "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
9829 "foo.go",
9830 |metric| {
9831 assert_eq!(metric.abc.conditions_sum(), 2);
9832 insta::assert_json_snapshot!(metric.abc);
9833 },
9834 );
9835 }
9836
9837 #[cfg(feature = "go")]
9838 #[test]
9839 fn go_else_counts_as_condition() {
9840 // `if a > b { ... } else { ... }` → `a > b` is one condition,
9841 // `else` is one condition → C = 2.
9842 check_metrics::<GoParser>(
9843 "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
9844 "foo.go",
9845 |metric| {
9846 assert_eq!(metric.abc.conditions_sum(), 2);
9847 insta::assert_json_snapshot!(metric.abc);
9848 },
9849 );
9850 }
9851
9852 #[cfg(feature = "go")]
9853 #[test]
9854 fn go_complex_function_abc() {
9855 // Mixed shape, verified by hand:
9856 // - Assignments: `var x = 10` (an initialized declaration, #1278),
9857 // `_ = x`, `n := 0`, `n = n + 1`, `n += 2`, `n++`,
9858 // `_ = len(s)` → A = 7. Every `_ = ...` IS counted as an
9859 // assignment_statement.
9860 // - Branches: `len(s)` → B = 1.
9861 // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
9862 // and `case 1:` (default excluded) → 2 → total C = 4.
9863 check_metrics::<GoParser>(
9864 "package main\nfunc f(s string) int {\n\
9865 \x20 var x = 10\n\
9866 \x20 _ = x\n\
9867 \x20 n := 0\n\
9868 \x20 if n < 10 { n = n + 1 } else { n += 2 }\n\
9869 \x20 n++\n\
9870 \x20 _ = len(s)\n\
9871 \x20 switch n {\n\
9872 \x20 case 0: return 0\n\
9873 \x20 case 1: return 1\n\
9874 \x20 default: return n\n\
9875 \x20 }\n\
9876 }\n",
9877 "foo.go",
9878 |metric| {
9879 assert_eq!(metric.abc.assignments_sum(), 7);
9880 assert_eq!(metric.abc.branches_sum(), 1);
9881 assert_eq!(metric.abc.conditions_sum(), 4);
9882 insta::assert_json_snapshot!(metric.abc);
9883 },
9884 );
9885 }
9886
9887 #[cfg(feature = "go")]
9888 #[test]
9889 fn go_if_multiple_conditions() {
9890 // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors
9891 // `rust_if_multiple_conditions`.
9892 check_metrics::<GoParser>(
9893 "package p\n\
9894 func F(a, b, c, d bool) int {\n\
9895 \x20 if a || b || c || d { return 1 } // +4c\n\
9896 \x20 if a && b && c { return 2 } // +3c\n\
9897 \x20 if !a && !b { return 3 } // +2c\n\
9898 \x20 return 0\n\
9899 }\n",
9900 "foo.go",
9901 |metric| {
9902 assert_eq!(metric.abc.conditions_sum(), 9);
9903 insta::assert_json_snapshot!(metric.abc);
9904 },
9905 );
9906 }
9907
9908 #[cfg(feature = "go")]
9909 #[test]
9910 fn go_for_with_conditions() {
9911 // Go has no `while` or `do { … } while(…);` — the `for` loop
9912 // header is the sole condition slot. Each operand of the
9913 // `&&` / `||` chain in the for-condition counts as one
9914 // Fitzpatrick condition.
9915 check_metrics::<GoParser>(
9916 "package p\n\
9917 func F(a, b bool) {\n\
9918 \x20 for a || b { break } // +2c\n\
9919 \x20 for a && !b { break } // +2c\n\
9920 }\n",
9921 "foo.go",
9922 |metric| {
9923 assert_eq!(metric.abc.conditions_sum(), 4);
9924 insta::assert_json_snapshot!(metric.abc);
9925 },
9926 );
9927 }
9928
9929 #[cfg(feature = "go")]
9930 #[test]
9931 fn go_for_bare_condition_counts() {
9932 // Regression for findings.md #1: `for true {}` / `for !ready {}`
9933 // are Go's only loop-condition slot. Pre-fix, the Phase-2B
9934 // dispatcher had no `G::ForStatement` arm, so bare-boolean
9935 // and `!`-wrapped `for` conditions silently reported zero.
9936 // `go_count_condition`'s terminal-bool / paren / unary filter
9937 // makes the walker safe across all three for-statement shapes:
9938 // bare condition, `for_clause` (init; cond; post) — whose own
9939 // `condition` field #1276 taught the walker to read — and
9940 // `range_clause`, which has no such field and contributes
9941 // nothing.
9942 check_metrics::<GoParser>(
9943 "package p\n\
9944 func F(ready bool) {\n\
9945 \x20 for true { break } // +1c\n\
9946 \x20 for !ready { break } // +1c\n\
9947 \x20 for i := 0; i < 3; i++ { _ = i } // +1c (the `<`)\n\
9948 }\n",
9949 "foo.go",
9950 |metric| {
9951 // `for true`: walker counts True (+1).
9952 // `for !ready`: walker on unary unwraps to `ready`
9953 // (+1).
9954 // `for_clause`'s condition is `i < 3`, a
9955 // `binary_expression` that `go_count_condition`
9956 // filters out; the `<` itself contributes 1 via the
9957 // pre-existing LT/GT arm.
9958 // Total: 3.
9959 assert_eq!(metric.abc.conditions_sum(), 3);
9960 insta::assert_json_snapshot!(metric.abc);
9961 },
9962 );
9963 }
9964
9965 // Issue #1276, Go's share. `for_statement`'s child(1) is the
9966 // condition only in the `for cond {}` spelling; the three-clause
9967 // form puts it one level down, in the `for_clause`'s `condition`
9968 // field. Letting the `for_clause` fall through — which the arm's
9969 // own comment used to call harmless — scored a bare three-clause
9970 // condition zero while `for a {}` scored one.
9971 #[cfg(feature = "go")]
9972 #[test]
9973 fn go_three_clause_for_condition_counts() {
9974 // Bare identifier in the three-clause header: no comparison
9975 // token, so only the `for_clause` lookup can count it.
9976 check_metrics::<GoParser>(
9977 "package p\n\
9978 func F(a bool) {\n\
9979 \x20 for i := 0; a; i++ { _ = i }\n\
9980 }\n",
9981 "foo.go",
9982 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
9983 );
9984 // Negation, through `go_inspect_container`'s `!` unwrap.
9985 check_metrics::<GoParser>(
9986 "package p\n\
9987 func F(a bool) {\n\
9988 \x20 for i := 0; !a; i++ { _ = i }\n\
9989 }\n",
9990 "foo.go",
9991 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
9992 );
9993 // Empty condition: the `for_clause` exposes no `condition`
9994 // field, so zero — the same answer as Go's bare `for {}` and
9995 // as every other language since #1276.
9996 check_metrics::<GoParser>(
9997 "package p\n\
9998 func F() {\n\
9999 \x20 for i := 0; ; i++ { break }\n\
10000 }\n",
10001 "foo.go",
10002 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
10003 );
10004 // A `range_clause` carries no condition either, and the walker
10005 // must not mistake the clause itself for one.
10006 check_metrics::<GoParser>(
10007 "package p\n\
10008 func F(xs []int) {\n\
10009 \x20 for _, v := range xs { _ = v }\n\
10010 }\n",
10011 "foo.go",
10012 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
10013 );
10014 }
10015
10016 // Go's `for_statement` is the one grammar here that exposes no
10017 // `condition` field, so its header slot is located structurally and
10018 // has to skip what the field-addressed siblings get for free. Two
10019 // things it must skip, each of which `node.child(1)` got wrong:
10020 // a leading comment (tree-sitter counts comments among a node's
10021 // children — the #1181 failure), and the body of a bare `for {}`,
10022 // which IS child(1) and would otherwise be offered to
10023 // `go_count_condition` as though it were a condition.
10024 #[cfg(feature = "go")]
10025 #[test]
10026 fn go_for_header_slot_skips_comments_and_the_body() {
10027 // Each pair is (source, expected conditions). The commented
10028 // spelling must agree with its bare twin.
10029 let cases = [
10030 ("for a { break }", 1),
10031 ("for /* n */ a { break }", 1),
10032 ("for i := 0; a; i++ { _ = i }", 1),
10033 ("for /* n */ i := 0; a; i++ { _ = i }", 1),
10034 ("for i := 0; /* n */ a; i++ { _ = i }", 1),
10035 // Bare infinite loop: the body is child(1) and must not be
10036 // read as the condition.
10037 ("for { break }", 0),
10038 ("for /* n */ { break }", 0),
10039 ("for _, v := range xs { _ = v }", 0),
10040 ("for /* n */ _, v := range xs { _ = v }", 0),
10041 ];
10042 let mut ran = 0;
10043 for (body, expected) in cases {
10044 let src = format!("package p\nfunc F(a bool, xs []int) {{\n\t{body}\n}}\n");
10045 assert_eq!(abc_conditions(LANG::Go, &src), expected, "`{body}`");
10046 ran += 1;
10047 }
10048 // Non-vacuity, both halves: the loop must actually have run
10049 // every row, and the rows must carry both answers — a walker
10050 // stuck at 0 or at 1 would otherwise pass half the table
10051 // silently.
10052 assert_eq!(ran, cases.len());
10053 assert!(cases.iter().any(|&(_, n)| n == 1));
10054 assert!(cases.iter().any(|&(_, n)| n == 0));
10055 }
10056
10057 #[cfg(feature = "go")]
10058 #[test]
10059 fn go_if_init_statement_condition_counts() {
10060 // Regression for the code-review finding: Go's
10061 // `if x := f(); x { ... }` init-statement form puts the
10062 // short-var declaration at child(1) and the condition at
10063 // child(2). Pre-fix, the dispatcher used child(1) and
10064 // counted zero conditions for this idiomatic Go shape.
10065 // The fix uses `child_by_field_name("condition")` which
10066 // returns the condition regardless of init presence.
10067 check_metrics::<GoParser>(
10068 "package p\nfunc F() { if x := g(); x { } }\n",
10069 "foo.go",
10070 |metric| {
10071 // `x` bare-identifier condition contributes 1
10072 // (Rule 6 — bare boolean identifier in if-condition).
10073 // `g()` call contributes 1 branch but no condition.
10074 assert_eq!(metric.abc.branches_sum(), 1);
10075 assert_eq!(metric.abc.conditions_sum(), 1);
10076 insta::assert_json_snapshot!(metric.abc);
10077 },
10078 );
10079 }
10080
10081 #[cfg(feature = "go")]
10082 #[test]
10083 fn go_if_boolean_literal_condition() {
10084 check_metrics::<GoParser>(
10085 "package p\n\
10086 func F() {\n\
10087 \x20 if true {} // +1c\n\
10088 \x20 if !false {} // +1c\n\
10089 }\n",
10090 "foo.go",
10091 |metric| {
10092 assert_eq!(metric.abc.conditions_sum(), 2);
10093 insta::assert_json_snapshot!(metric.abc);
10094 },
10095 );
10096 }
10097
10098 #[cfg(feature = "go")]
10099 #[test]
10100 fn go_methods_arguments_with_conditions() {
10101 check_metrics::<GoParser>(
10102 "package p\n\
10103 func F(a, b bool) {\n\
10104 \x20 m(a, b) // +1b\n\
10105 \x20 m(!a, !b) // +1b +2c\n\
10106 }\n",
10107 "foo.go",
10108 |metric| {
10109 assert_eq!(metric.abc.branches_sum(), 2);
10110 assert_eq!(metric.abc.conditions_sum(), 2);
10111 insta::assert_json_snapshot!(metric.abc);
10112 },
10113 );
10114 }
10115
10116 #[cfg(feature = "go")]
10117 #[test]
10118 fn go_return_with_conditions() {
10119 check_metrics::<GoParser>(
10120 "package p\n\
10121 func M1(z int) bool { return !(z >= 0) }\n\
10122 func M2(x bool) bool { return !x }\n\
10123 func M3(x, y bool) bool { return x && y }\n",
10124 "foo.go",
10125 |metric| {
10126 // M1: `>=` (1). `!(z >= 0)` walker on the unary
10127 // doesn't reach a terminal — stops at the
10128 // BinaryExpression z>=0 inside the parens. +1.
10129 // M2: walker on `!x` → 1.
10130 // M3: `&&` walker counts both → 2.
10131 // Sum: 1 + 1 + 2 = 4.
10132 assert_eq!(metric.abc.conditions_sum(), 4);
10133 insta::assert_json_snapshot!(metric.abc);
10134 },
10135 );
10136 }
10137
10138 #[cfg(feature = "go")]
10139 #[test]
10140 fn go_short_circuit_with_boolean_literal_operand() {
10141 // `a && true` reports 2 conditions: one identifier, one
10142 // boolean literal. Confirms the terminal set includes
10143 // `True` / `False`.
10144 check_metrics::<GoParser>(
10145 "package p\nfunc F(a bool) bool { return a && true }\n",
10146 "foo.go",
10147 |metric| {
10148 assert_eq!(metric.abc.conditions_sum(), 2);
10149 insta::assert_json_snapshot!(metric.abc);
10150 },
10151 );
10152 }
10153
10154 // ----- Elixir -----
10155
10156 // No top-level Calls and no operators → all three vectors are
10157 // zero. Uses a bare expression rather than a `defmodule` wrapper
10158 // (which would itself be a Call → 1 branch). Confirms the
10159 // ElixirCode Abc trait is wired up and the metric emits.
10160 #[cfg(feature = "elixir")]
10161 #[test]
10162 fn elixir_empty_unit_zero() {
10163 check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
10164 assert_eq!(metric.abc.assignments_sum(), 0);
10165 assert_eq!(metric.abc.branches_sum(), 0);
10166 assert_eq!(metric.abc.conditions_sum(), 0);
10167 insta::assert_json_snapshot!(metric.abc);
10168 });
10169 }
10170
10171 // An empty `defmodule Foo do ... end` is itself ONE `Call` →
10172 // Documents that module-/function-defining macros (`defmodule`,
10173 // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
10174 // directives (`alias`, `import`, `require`, `use`) are NOT
10175 // runtime dispatch and therefore do NOT inflate `branches`,
10176 // matching Cognitive's treatment.
10177 #[cfg(feature = "elixir")]
10178 #[test]
10179 fn elixir_defmodule_is_zero_branches() {
10180 check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
10181 assert_eq!(metric.abc.branches_sum(), 0);
10182 assert_eq!(metric.abc.assignments_sum(), 0);
10183 assert_eq!(metric.abc.conditions_sum(), 0);
10184 insta::assert_json_snapshot!(metric.abc);
10185 });
10186 }
10187
10188 // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
10189 // `defmodule` and `def` are declarative-Call wrappers and are
10190 // filtered out of branches; the assertion focuses on assignments
10191 // so we only pin that vector.
10192 #[cfg(feature = "elixir")]
10193 #[test]
10194 fn elixir_pattern_match_is_assignment() {
10195 check_metrics::<ElixirParser>(
10196 "defmodule Foo do\n def f do\n x = 1\n y = x + 1\n y\n end\nend\n",
10197 "foo.ex",
10198 |metric| {
10199 assert_eq!(metric.abc.assignments_sum(), 2);
10200 insta::assert_json_snapshot!(metric.abc);
10201 },
10202 );
10203 }
10204
10205 // `|>` pipeline operator: each `|>` token contributes one branch.
10206 // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
10207 // step also dispatches a Call (`String.upcase(...)`,
10208 // `String.trim(...)`) — these are wrapped inside the outer
10209 // pipeline Call tree, contributing additional Call branches.
10210 // The headline assertion confirms (a) `|>` is detected and (b)
10211 // pipeline steps are not silently dropped.
10212 #[cfg(feature = "elixir")]
10213 #[test]
10214 fn elixir_pipeline_each_step_is_branch() {
10215 check_metrics::<ElixirParser>(
10216 "defmodule Foo do\n def normalize(s) do\n s |> String.trim() |> String.upcase()\n end\nend\n",
10217 "foo.ex",
10218 |metric| {
10219 // Pipeline yields 2 `|>` branches plus Calls for
10220 // String.trim, String.upcase, and the outer pipeline
10221 // (which surfaces as a Call wrapping the binary
10222 // operator). `def` and `defmodule` are declarative
10223 // and excluded. Empirical total: B = 5.
10224 assert_eq!(metric.abc.branches_sum(), 5);
10225 assert_eq!(metric.abc.assignments_sum(), 0);
10226 insta::assert_json_snapshot!(metric.abc);
10227 },
10228 );
10229 }
10230
10231 // Comparison operators all count as conditions. Six comparisons
10232 // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
10233 #[cfg(feature = "elixir")]
10234 #[test]
10235 fn elixir_comparisons_are_conditions() {
10236 check_metrics::<ElixirParser>(
10237 "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",
10238 "foo.ex",
10239 |metric| {
10240 assert_eq!(metric.abc.conditions_sum(), 6);
10241 insta::assert_json_snapshot!(metric.abc);
10242 },
10243 );
10244 }
10245
10246 // Strict-equality operators `===` / `!==` count as conditions too.
10247 #[cfg(feature = "elixir")]
10248 #[test]
10249 fn elixir_strict_equality_is_condition() {
10250 check_metrics::<ElixirParser>(
10251 "defmodule Foo do\n def f(a, b) do\n a === b or a !== b\n end\nend\n",
10252 "foo.ex",
10253 |metric| {
10254 assert_eq!(metric.abc.conditions_sum(), 2);
10255 insta::assert_json_snapshot!(metric.abc);
10256 },
10257 );
10258 }
10259
10260 // #1461 moved every relational construct a grammar spells as its own
10261 // production onto an unconditional arm — scored by use, Fitzpatrick
10262 // Rule 5 — in C#, Java, Groovy, Kotlin and Ruby. Elixir's membership
10263 // and type tests were the same class and were not in that sweep, so
10264 // `a in b` scored zero where `a == b` scored one. The gap stayed
10265 // invisible while the `when` arm paid a flat one for every guard,
10266 // and surfaced the moment the guard became a slot: an
10267 // operator-spelled guard is owned by its operator's arm, and `in`
10268 // had none.
10269 //
10270 // `bare` / `negbare` are the by-use claim; `slot` is the §5 check
10271 // that a boolean slot does not now score the same operator twice;
10272 // `cmp` is the control the two membership rows are levelled against.
10273 // `in` rides the `<` / `>` `binary_operator` gate rather than
10274 // standing alone — it has the same three grammar positions — and
10275 // `elixir_operator_identifier_is_not_a_condition` pins that gate.
10276 #[cfg(feature = "elixir")]
10277 #[test]
10278 fn elixir_membership_is_a_condition_by_use() {
10279 check_func_space::<ElixirParser, _>(
10280 "defmodule Foo do\n\
10281 def bare(a, b), do: a in b\n\
10282 def negbare(a, b), do: a not in b\n\
10283 def slot(a, b) do\n\
10284 if a in b do\n\
10285 IO.puts(\"x\")\n\
10286 end\n\
10287 end\n\
10288 def cmp(a, b), do: a == b\n\
10289 end\n",
10290 "foo.ex",
10291 |space| {
10292 assert_members_score(
10293 &space.spaces[0],
10294 &[
10295 // Each was 0 before the arm.
10296 ("bare", 1, 1),
10297 ("negbare", 1, 1),
10298 // `if` (1) + `in` (1), exactly as `if a == b`
10299 // scores.
10300 ("slot", 2, 2),
10301 ("cmp", 1, 1),
10302 ],
10303 );
10304 },
10305 );
10306 }
10307
10308 // Guard `when` clause counts as a condition — exactly one, and here
10309 // it is the `>` that supplies it. The guard is a condition *slot*
10310 // (#1454), so an operator-spelled guard scores through the operator
10311 // arm that owns it and the slot itself adds nothing; see
10312 // `elixir_guard_is_a_decision_however_spelled` for the four-spelling
10313 // agreement this is one row of.
10314 //
10315 // Was 2, on a flat `+1` for the `when` token laid on top of the `>`:
10316 // the §5 double count a whole-branch review of the #1454 batch
10317 // found.
10318 #[cfg(feature = "elixir")]
10319 #[test]
10320 fn elixir_guard_when_is_condition() {
10321 check_metrics::<ElixirParser>(
10322 "defmodule Foo do\n def f(x) when x > 0 do\n :pos\n end\nend\n",
10323 "foo.ex",
10324 |metric| {
10325 // the guard slot (+0, the guard is an operator
10326 // application) + `>` (+1) = 1
10327 assert_eq!(metric.abc.conditions_sum(), 1);
10328 insta::assert_json_snapshot!(metric.abc);
10329 },
10330 );
10331 }
10332
10333 // Keyword-shaped Calls (`case`, `cond`, `if`, `with`) each count
10334 // as one condition AND one branch. `case` here adds 1 condition
10335 // (the keyword Call) + 1 branch (the Call itself).
10336 #[cfg(feature = "elixir")]
10337 #[test]
10338 fn elixir_case_is_condition_and_branch() {
10339 check_metrics::<ElixirParser>(
10340 "defmodule Foo do\n def f(x) do\n case x do\n 1 -> :one\n _ -> :other\n end\n end\nend\n",
10341 "foo.ex",
10342 |metric| {
10343 // conditions: case → 1
10344 assert_eq!(metric.abc.conditions_sum(), 1);
10345 insta::assert_json_snapshot!(metric.abc);
10346 },
10347 );
10348 }
10349
10350 // `cond` is structurally identical to `case` for Abc.
10351 #[cfg(feature = "elixir")]
10352 #[test]
10353 fn elixir_cond_is_condition() {
10354 check_metrics::<ElixirParser>(
10355 "defmodule Foo do\n def f(x) do\n cond do\n x > 0 -> :pos\n true -> :other\n end\n end\nend\n",
10356 "foo.ex",
10357 |metric| {
10358 // conditions: cond (+1) + > (+1) = 2
10359 assert_eq!(metric.abc.conditions_sum(), 2);
10360 insta::assert_json_snapshot!(metric.abc);
10361 },
10362 );
10363 }
10364
10365 // `for` is a comprehension/loop, NOT in the issue's condition
10366 // list. It is still a Call so it contributes one branch, but no
10367 // condition.
10368 #[cfg(feature = "elixir")]
10369 #[test]
10370 fn elixir_for_is_branch_not_condition() {
10371 check_metrics::<ElixirParser>(
10372 "defmodule Foo do\n def f(xs) do\n for x <- xs, do: x * 2\n end\nend\n",
10373 "foo.ex",
10374 |metric| {
10375 assert_eq!(metric.abc.conditions_sum(), 0);
10376 insta::assert_json_snapshot!(metric.abc);
10377 },
10378 );
10379 }
10380
10381 // Mixed shape, verified by hand: defmodule Call + def Call + if Call
10382 // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
10383 // - Assignments: `x = 1` → A = 1.
10384 // - Branches: `defmodule` and `def` are declarative and excluded;
10385 // `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
10386 // - Conditions: `if` keyword → 1, `x > 0` → 1 → C = 2.
10387 #[cfg(feature = "elixir")]
10388 #[test]
10389 fn elixir_mixed_abc() {
10390 check_metrics::<ElixirParser>(
10391 "defmodule Foo do\n def f do\n x = 1\n if x > 0 do\n side_effect()\n end\n end\nend\n",
10392 "foo.ex",
10393 |metric| {
10394 assert_eq!(metric.abc.assignments_sum(), 1);
10395 assert_eq!(metric.abc.branches_sum(), 2);
10396 assert_eq!(metric.abc.conditions_sum(), 2);
10397 insta::assert_json_snapshot!(metric.abc);
10398 },
10399 );
10400 }
10401
10402 #[cfg(feature = "elixir")]
10403 #[test]
10404 fn elixir_unary_conditions_in_chain() {
10405 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
10406 // `&&` / `||` chain is one condition. For `if a && b || c`: the
10407 // `if` keyword Call contributes 1 condition, and the walker adds
10408 // a, b, c → 3. expected: 4 conditions, consistent with the
10409 // function's cyclomatic complexity of 4 (base 1 + if + && + ||).
10410 check_metrics::<ElixirParser>(
10411 "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",
10412 "foo.ex",
10413 |metric| {
10414 assert_eq!(metric.abc.conditions_sum(), 4);
10415 },
10416 );
10417 }
10418
10419 // Elixir spells negation two ways and `elixir_inspect_container`
10420 // recognised only `!`, so `a && not b` scored 3 where `a && !b`
10421 // scored 4 — the `not` operand reached no terminal and vanished.
10422 // Both forms now read the same. `not` is the stricter of the two (it
10423 // raises on a non-boolean operand where `!` accepts any truthy
10424 // value), so it is at least as good a proof that what it wraps is
10425 // boolean, which is all the walker's flag claims.
10426 //
10427 // Both members in one fixture because the claim is that they agree:
10428 // asserting either alone says nothing about the pair (§11), and a
10429 // shared `check_metrics` total could not tell 3 + 4 from 4 + 3.
10430 //
10431 // `assert_members_score` rather than the parity-asserting sibling:
10432 // a `&&` operand is an ABC condition with no cyclomatic decision
10433 // behind it, so both members legitimately sit one above their
10434 // decision count (`base 1 + if + &&` = 3).
10435 #[cfg(feature = "elixir")]
10436 #[test]
10437 fn elixir_keyword_not_negates_like_bang() {
10438 check_func_space::<ElixirParser, _>(
10439 "defmodule Foo do\n def kw(a, b) do\n if a && not b do\n IO.puts(\"x\")\n end\n end\n def bang(a, b) do\n if a && !b do\n IO.puts(\"x\")\n end\n end\nend\n",
10440 "foo.ex",
10441 |space| {
10442 assert_members_score(&space.spaces[0], &[("kw", 3, 3), ("bang", 3, 3)]);
10443 },
10444 );
10445 }
10446
10447 #[cfg(feature = "elixir")]
10448 #[test]
10449 fn elixir_comparison_operands_add_nothing() {
10450 // Isolation check: comparison operands of a `&&` chain are nested
10451 // `binary_operator` nodes, not bare boolean leaves, so the walker
10452 // adds nothing. expected: 3 = `if` (1) + `>` (1) + `>` (1); the
10453 // `&&` walker contributes 0.
10454 check_metrics::<ElixirParser>(
10455 "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",
10456 "foo.ex",
10457 |metric| {
10458 assert_eq!(metric.abc.conditions_sum(), 3);
10459 },
10460 );
10461 }
10462
10463 #[cfg(feature = "elixir")]
10464 #[test]
10465 fn elixir_keyword_and_or_chain_counts_operands() {
10466 // The keyword forms `and` / `or` get the same Rule 9 treatment as
10467 // `&&` / `||`. expected: 4 = `if` (1) + operands a, b, c (3).
10468 check_metrics::<ElixirParser>(
10469 "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",
10470 "foo.ex",
10471 |metric| {
10472 assert_eq!(metric.abc.conditions_sum(), 4);
10473 },
10474 );
10475 }
10476
10477 // Sigil delimiter choice must not move ABC: `~s<hi>` and `~s(hi)`
10478 // are the same value spelled differently, but the `<` / `>`
10479 // delimiter tokens carry the comparison kind ids, so the unguarded
10480 // condition arm scored `~s<hi>` as 2 conditions and `~s(hi)` as 0.
10481 // The parent-is-`Sigil` guard (mirroring the Halstead getter's,
10482 // #1256) suppresses the delimiter case. expected, for each
10483 // spelling: A = 1 (the `x =` pattern match), B = 0 (a bare sigil
10484 // is not a `Call` node — verified by AST dump: `binary_operator`
10485 // wrapping `identifier`, `=`, `sigil`), C = 0 (no comparison, no
10486 // guard, no keyword Call).
10487 #[cfg(feature = "elixir")]
10488 #[test]
10489 fn elixir_sigil_delimiter_choice_is_abc_invariant() {
10490 for src in ["x = ~s<hi>\n", "x = ~s(hi)\n"] {
10491 check_metrics::<ElixirParser>(src, "foo.ex", |metric| {
10492 assert_eq!(metric.abc.assignments_sum(), 1);
10493 assert_eq!(metric.abc.branches_sum(), 0);
10494 assert_eq!(metric.abc.conditions_sum(), 0);
10495 });
10496 }
10497 }
10498
10499 // Control for the guard above: `<` *outside* a sigil is a genuine
10500 // comparison and must keep counting even with a `<`-delimited
10501 // sigil in the same unit. expected: A = 2 (`x =`, `y =`), C = 1
10502 // (only `a < b`; the sigil's `<` / `>` delimiters are guarded).
10503 #[cfg(feature = "elixir")]
10504 #[test]
10505 fn elixir_lt_comparison_still_counts_beside_sigil() {
10506 check_metrics::<ElixirParser>("x = ~s<hi>\ny = a < b\n", "foo.ex", |metric| {
10507 assert_eq!(metric.abc.assignments_sum(), 2);
10508 assert_eq!(metric.abc.conditions_sum(), 1);
10509 });
10510 }
10511
10512 // #1297's sweep converted six grammars from a denylist of
10513 // non-comparison `<` / `>` parents to an allowlist and recorded
10514 // Elixir as already covered by its sigil guard. It was not: an
10515 // operator *named* rather than applied — the capture `&</2` and the
10516 // qualified call `Kernel.<(a, b)` — puts the bare token under an
10517 // `operator_identifier`, which no denylist entry mentioned, so each
10518 // scored a condition against zero decisions. Same shape as the C#
10519 // `operator <` declaration the sweep did fix.
10520 //
10521 // Each function is its own space and its own row, so a fix that
10522 // reached one spelling and not the other fails here. `max/2` is the
10523 // control: a qualified call whose name is not an operator was
10524 // always 0, so a guard that merely stopped counting `<` everywhere
10525 // would pass this row and fail the two below it.
10526 #[cfg(feature = "elixir")]
10527 #[test]
10528 fn elixir_operator_identifier_is_not_a_condition() {
10529 check_func_space::<ElixirParser, _>(
10530 "defmodule M do\n\
10531 def asc(l), do: Enum.sort(l, &</2)\n\
10532 def lt(a, b), do: Kernel.<(a, b)\n\
10533 def big(a, b), do: Kernel.max(a, b)\n\
10534 def cmp(a, b), do: a < b\n\
10535 end\n",
10536 "foo.ex",
10537 |space| {
10538 let module = &space.spaces[0];
10539 let scored: Vec<(String, u64)> = module
10540 .spaces
10541 .iter()
10542 .map(|f| {
10543 (
10544 f.name.clone().unwrap_or_default(),
10545 f.metrics.abc.conditions(),
10546 )
10547 })
10548 .collect();
10549 assert_eq!(
10550 scored,
10551 vec![
10552 ("asc".to_owned(), 0),
10553 ("lt".to_owned(), 0),
10554 ("big".to_owned(), 0),
10555 ("cmp".to_owned(), 1),
10556 ],
10557 );
10558 },
10559 );
10560 }
10561
10562 // ----- C++ -----
10563
10564 #[cfg(feature = "cpp")]
10565 #[test]
10566 fn cpp_empty_unit_zero() {
10567 // No code → A=B=C=0. Wires up the trait and exercises the
10568 // per-language compute reachability.
10569 check_metrics::<CppParser>("", "empty.cpp", |metric| {
10570 assert_eq!(metric.abc.assignments_sum(), 0);
10571 assert_eq!(metric.abc.branches_sum(), 0);
10572 assert_eq!(metric.abc.conditions_sum(), 0);
10573 insta::assert_json_snapshot!(metric.abc);
10574 });
10575 }
10576
10577 #[cfg(feature = "cpp")]
10578 #[test]
10579 fn cpp_plain_and_compound_assignments_count() {
10580 // `int x = 0` is an `init_declarator` carrying an `=` token
10581 // and counts as 1 (post-#393: the literal Fitzpatrick rule
10582 // counts every `=` operator, matching the JS impl's
10583 // `let x = 5` treatment). `x = 5`, `x += 2`, `x = 7` all
10584 // parse as `assignment_expression` → 3. Total A = 4.
10585 check_metrics::<CppParser>(
10586 "void f() { int x = 0; x = 5; x += 2; x = 7; }",
10587 "foo.cpp",
10588 |metric| {
10589 assert_eq!(metric.abc.assignments_sum(), 4);
10590 assert_eq!(metric.abc.branches_sum(), 0);
10591 assert_eq!(metric.abc.conditions_sum(), 0);
10592 insta::assert_json_snapshot!(metric.abc);
10593 },
10594 );
10595 }
10596
10597 #[cfg(feature = "cpp")]
10598 #[test]
10599 fn cpp_increment_and_decrement_count_as_assignment() {
10600 // `x++` / `--x` / prefix and postfix forms each parse as
10601 // `update_expression` and count as 1 assignment per
10602 // Fitzpatrick — 4. `int x = 0` (init_declarator with `=`)
10603 // adds 1 (post-#393). Total A = 5.
10604 check_metrics::<CppParser>(
10605 "void f() { int x = 0; x++; --x; ++x; x--; }",
10606 "foo.cpp",
10607 |metric| {
10608 assert_eq!(metric.abc.assignments_sum(), 5);
10609 insta::assert_json_snapshot!(metric.abc);
10610 },
10611 );
10612 }
10613
10614 #[cfg(feature = "cpp")]
10615 #[test]
10616 fn cpp_init_declarators_count_as_assignments() {
10617 // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`,
10618 // `int d=0;` are all `init_declarator` nodes with `=` → 4
10619 // assignments. `d=5;` is one plain `assignment_expression`,
10620 // `d+=1;` is one compound. Total A = 4 + 1 + 1 = 6.
10621 check_metrics::<CppParser>(
10622 "void f() { int a=1; int b=2; int c=a+b; int d=0; d=5; d+=1; }",
10623 "foo.cpp",
10624 |metric| {
10625 assert_eq!(metric.abc.assignments_sum(), 6);
10626 insta::assert_json_snapshot!(metric.abc);
10627 },
10628 );
10629 }
10630
10631 #[cfg(feature = "cpp")]
10632 #[test]
10633 fn cpp_declaration_without_initializer_does_not_count() {
10634 // `int a;` parses as a plain declarator inside `declaration`,
10635 // NOT an `init_declarator` (the latter only appears when an
10636 // initializer is present). Regression test for issue #393:
10637 // un-initialised declarations contribute zero to A.
10638 check_metrics::<CppParser>("void f() { int a; a = 5; }", "foo.cpp", |metric| {
10639 // Only `a = 5` (assignment_expression) → A = 1.
10640 assert_eq!(metric.abc.assignments_sum(), 1);
10641 insta::assert_json_snapshot!(metric.abc);
10642 });
10643 }
10644
10645 #[cfg(feature = "cpp")]
10646 #[test]
10647 fn cpp_init_declarator_brace_paren_init_does_not_count() {
10648 // `init_declarator` has two grammar forms: `declarator = value`
10649 // (the `=` form) and `declarator argument_list_or_initializer_list`
10650 // (the `int x(5);` / `int x{5};` direct-init forms). Only the
10651 // first form contains an `=` token, so only it should count.
10652 // Regression test pinning that distinction so that
10653 // refactorings of the init_declarator arm don't accidentally
10654 // start counting direct-init too.
10655 check_metrics::<CppParser>(
10656 "void f() { int x(5); int y{7}; x = 1; }",
10657 "foo.cpp",
10658 |metric| {
10659 // Only `x = 1` (assignment_expression) → A = 1.
10660 assert_eq!(metric.abc.assignments_sum(), 1);
10661 insta::assert_json_snapshot!(metric.abc);
10662 },
10663 );
10664 }
10665
10666 #[cfg(feature = "cpp")]
10667 #[test]
10668 fn cpp_calls_are_branches() {
10669 // Free call + member-fn call (parses as `call_expression` with
10670 // a `field_expression` callee) + `new` allocation. All three
10671 // are branches → B = 3. `auto* p = new int(5)` is also an
10672 // `init_declarator` with `=` so it contributes one assignment
10673 // (post-#393); the snapshot pins that magnitude.
10674 check_metrics::<CppParser>(
10675 "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
10676 "foo.cpp",
10677 |metric| {
10678 assert_eq!(metric.abc.branches_sum(), 3);
10679 assert_eq!(metric.abc.assignments_sum(), 1);
10680 insta::assert_json_snapshot!(metric.abc);
10681 },
10682 );
10683 }
10684
10685 #[cfg(feature = "cpp")]
10686 #[test]
10687 fn cpp_comparisons_count_conditions() {
10688 // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
10689 // `<=>` each contribute one condition. The `||` short-
10690 // circuits add 0 (Fitzpatrick Rule 5, issue #395). Six
10691 // comparisons in the `||` chain plus `<=>` (1) plus the
10692 // outer `== 0` (1) → C = 8.
10693 check_metrics::<CppParser>(
10694 "#include <compare>\n\
10695 bool f(int a, int b) {\n\
10696 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
10697 }\n",
10698 "foo.cpp",
10699 |metric| {
10700 // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 comparisons
10701 // from the chained `||` expression. `(a <=> b) == 0`
10702 // adds the spaceship `<=>` (1) + the outer `== 0`
10703 // (1) → 8 total. The six `||` short-circuits add 0
10704 // (Fitzpatrick Rule 5; issue #395).
10705 assert_eq!(metric.abc.conditions_sum(), 8);
10706 insta::assert_json_snapshot!(metric.abc);
10707 },
10708 );
10709 }
10710
10711 #[cfg(feature = "cpp")]
10712 #[test]
10713 fn cpp_short_circuit_ops_not_counted_directly() {
10714 // `&&` and `||` do NOT count on their own (see the
10715 // module-level `Stats` doc-comment; #395). Phase-2 walker
10716 // counts each operand of a logical chain once (#403), but
10717 // when every operand is itself a relational expression
10718 // (`a == b`, `a > 0`, `b < 0`) the walker doesn't add
10719 // anything on top of the existing comparison-token tally
10720 // — relational sub-expressions are not in
10721 // `cpp_bool_terminal_kinds!()` and `cpp_inspect_container`
10722 // does not recurse into them.
10723 check_metrics::<CppParser>(
10724 "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
10725 "foo.cpp",
10726 |metric| {
10727 // == 1, > 1, < 1; the walker on && and || finds
10728 // BinaryExpression operands (not terminal-bool) and
10729 // adds nothing. Total: 3.
10730 assert_eq!(metric.abc.conditions_sum(), 3);
10731 insta::assert_json_snapshot!(metric.abc);
10732 },
10733 );
10734 }
10735
10736 #[cfg(feature = "cpp")]
10737 #[test]
10738 fn cpp_generic_brackets_not_conditions() {
10739 // `<` / `>` in `std::vector<int>` are `template_argument_list`
10740 // delimiters, NOT comparison operators. The `binary_expression`
10741 // parent check must filter them out → C = 0.
10742 check_metrics::<CppParser>(
10743 "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
10744 "foo.cpp",
10745 |metric| {
10746 assert_eq!(metric.abc.conditions_sum(), 0);
10747 insta::assert_json_snapshot!(metric.abc);
10748 },
10749 );
10750 }
10751
10752 #[cfg(feature = "cpp")]
10753 #[test]
10754 fn cpp_else_and_ternary_count_conditions() {
10755 // `if (cond) ... else ...` + ternary `cond ? a : b`. The
10756 // `if`-keyword is NOT a condition (its condition is the
10757 // comparison inside, which counts separately). `else` adds 1,
10758 // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
10759 check_metrics::<CppParser>(
10760 "int f(int a, int b) {\n\
10761 if (a > b) { return a; } else { return b; }\n\
10762 return (b < 0) ? -b : b;\n\
10763 }\n",
10764 "foo.cpp",
10765 |metric| {
10766 assert_eq!(metric.abc.conditions_sum(), 4);
10767 insta::assert_json_snapshot!(metric.abc);
10768 },
10769 );
10770 }
10771
10772 // Issue #1102. A ternary's condition and both branch operands are
10773 // Fitzpatrick Rule 9 unary conditions, exactly as `java_walk_ternary`
10774 // has always counted them. Before the fix the C family scored
10775 // `a ? !b : !c` as 1 — the `?` token alone — against Java's 4.
10776 #[cfg(feature = "cpp")]
10777 #[test]
10778 fn cpp_ternary_operand_slots_count_as_unary_conditions() {
10779 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
10780 check_metrics::<CppParser>("void f() { x = a ? !b : !c; }", "foo.cpp", |metric| {
10781 assert_eq!(metric.abc.conditions_sum(), 4);
10782 });
10783 // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
10784 // fix. The parenthesised condition unwraps to a
10785 // `binary_expression`, which is not a boolean terminal, and
10786 // neither branch is negated — the `!` is the type-free proxy for
10787 // "this operand is boolean", so an unnegated branch contributes
10788 // nothing.
10789 check_metrics::<CppParser>("void f() { x = (a > 0) ? b : -b; }", "foo.cpp", |metric| {
10790 assert_eq!(metric.abc.conditions_sum(), 2);
10791 });
10792 // Nested: outer `?` (1) + outer condition `a` (1) + inner `?`
10793 // (1) + inner condition `b` (1) = 4. The outer consequence is
10794 // the inner ternary — neither a boolean terminal nor a
10795 // paren / `!` wrapper — so it adds nothing on its own and the
10796 // inner one is reached by the walk, not by descent.
10797 check_metrics::<CppParser>("void f() { x = a ? b ? c : d : e; }", "foo.cpp", |metric| {
10798 assert_eq!(metric.abc.conditions_sum(), 4);
10799 });
10800 // A negated *condition* is the only input that reaches the
10801 // walker's `else` fallback: `!a` is neither a boolean terminal
10802 // (so the terminal arm skips it) nor an operand slot (so
10803 // `cpp_inspect_container` is never called on it from anywhere
10804 // else). Every other condition fixture in this file wraps a
10805 // comparison, which the fallback resolves to 0 — delete the
10806 // fallback and only this case moves. `?` (1) + `!a` (1) = 2.
10807 check_metrics::<CppParser>("void f() { x = !a ? b : c; }", "foo.cpp", |metric| {
10808 assert_eq!(metric.abc.conditions_sum(), 2);
10809 });
10810 }
10811
10812 // The GNU short-ternary `a ?: b` elides the consequence, so the
10813 // C-family grammar marks that field optional and the alternative
10814 // lands at child(3) rather than child(4). Addressing the operand
10815 // slots by grammar field name — never by index — is what keeps `!b`
10816 // counted here; a fixed `child(4)` reads `None` and scores 2.
10817 #[cfg(feature = "cpp")]
10818 #[test]
10819 fn cpp_elided_ternary_consequence_still_walks_the_alternative() {
10820 // `?` (1) + condition `a` (1) + `!b` (1) = 3.
10821 check_metrics::<CppParser>("void f() { x = a ?: !b; }", "foo.cpp", |metric| {
10822 assert_eq!(metric.abc.conditions_sum(), 3);
10823 });
10824 }
10825
10826 // `cpp_walk_ternary` is shared by the C, ObjC, and Mozcpp ABC impls
10827 // exactly as `cpp_inspect_container` is, so each needs its own
10828 // dispatcher arm. Mozcpp owns no file extension and so gets no
10829 // integration-snapshot coverage at all — this parity assertion is
10830 // its only guard.
10831 //
10832 // The expected value is *derived from the C++ run*, not hardcoded,
10833 // so the four languages cannot silently drift apart if the C++
10834 // expectation ever legitimately moves.
10835 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10836 #[test]
10837 fn c_family_ternary_operand_slots_agree_with_cpp() {
10838 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10839 const SRC: &str = "void f() { x = a ? !b : !c; }\n";
10840 let conditions = abc_conditions;
10841
10842 let cpp = conditions(LANG::Cpp, SRC);
10843 // Non-degenerate: a zeroed reference would make every
10844 // comparison below vacuous.
10845 assert_eq!(cpp, 4, "C++ reference value for `a ? !b : !c`");
10846
10847 assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
10848 assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
10849 assert_eq!(
10850 conditions(
10851 LANG::Objc,
10852 "@implementation Foo\n\
10853 - (void)bar {\n\
10854 x = a ? !b : !c;\n\
10855 }\n\
10856 @end\n",
10857 ),
10858 cpp,
10859 "ObjC must match C++"
10860 );
10861 }
10862
10863 // Issue #1276, C-family half. `cpp_walk_for_statement` is the
10864 // `for` header's counterpart to the `if` / `while` arms: the slot
10865 // is a bare expression rather than a `condition_clause`, so it
10866 // needs the top-level terminal check `cpp_walk_ternary` already
10867 // had. Every fixture is a shape only that walker can classify —
10868 // a comparison-shaped condition proves nothing, the `<` token arm
10869 // counts it either way (grammar-dispatch §11).
10870 #[cfg(feature = "cpp")]
10871 #[test]
10872 fn cpp_for_condition_slot_counts_unary_conditions() {
10873 // Bare identifier: the whole condition, no operator token.
10874 check_metrics::<CppParser>("void f(int a) { for (; a; ) {} }", "foo.cpp", |metric| {
10875 assert_eq!(metric.abc.conditions_sum(), 1);
10876 });
10877 // Negation: reaches the terminal through
10878 // `cpp_inspect_container`'s `!` unwrap.
10879 check_metrics::<CppParser>("void f(int a) { for (; !a; ) {} }", "foo.cpp", |metric| {
10880 assert_eq!(metric.abc.conditions_sum(), 1);
10881 });
10882 // Parentheses: counts only because the `for_statement` parent
10883 // seeds `has_boolean_content` — the seed #1276 found dead.
10884 check_metrics::<CppParser>("void f(int a) { for (; (a); ) {} }", "foo.cpp", |metric| {
10885 assert_eq!(metric.abc.conditions_sum(), 1);
10886 });
10887 // No-double-count pin: the `<` token arm already counted this
10888 // shape before the fix and the walker must not add a second.
10889 // The two assignments confirm the header parsed as the
10890 // three-clause form rather than degenerating.
10891 check_metrics::<CppParser>(
10892 "void f(int n) { for (int i = 0; i < n; i++) {} }",
10893 "foo.cpp",
10894 |metric| {
10895 assert_eq!(metric.abc.conditions_sum(), 1);
10896 assert_eq!(metric.abc.assignments_sum(), 2);
10897 },
10898 );
10899 // Empty condition: no `condition` field, no decision, zero.
10900 check_metrics::<CppParser>("void f() { for (;;) { break; } }", "foo.cpp", |metric| {
10901 assert_eq!(metric.abc.conditions_sum(), 0);
10902 });
10903 }
10904
10905 // `cpp_walk_for_statement` is shared by the C, ObjC and Mozcpp ABC
10906 // impls the way `cpp_walk_ternary` is, so each needs its own
10907 // dispatcher arm — and Mozcpp, which owns no file extension, has no
10908 // integration-snapshot coverage at all, making this its only guard.
10909 // The expected value is derived from the C++ run rather than
10910 // hardcoded, so the four cannot silently drift apart.
10911 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10912 #[test]
10913 fn c_family_for_condition_slot_agrees_with_cpp() {
10914 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
10915 const SRC: &str = "void f(int a) { for (; !a; ) {} }\n";
10916 let conditions = abc_conditions;
10917
10918 let cpp = conditions(LANG::Cpp, SRC);
10919 // Non-degenerate: a zeroed reference makes every comparison
10920 // below vacuous.
10921 assert_eq!(cpp, 1, "C++ reference value for `for (; !a; )`");
10922
10923 assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
10924 assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
10925 assert_eq!(
10926 conditions(
10927 LANG::Objc,
10928 "@implementation Foo\n\
10929 - (void)bar {\n\
10930 for (; !a; ) {}\n\
10931 }\n\
10932 @end\n",
10933 ),
10934 cpp,
10935 "ObjC must match C++"
10936 );
10937 }
10938
10939 #[cfg(feature = "cpp")]
10940 #[test]
10941 fn cpp_switch_cases_count_default_excluded() {
10942 // `case 1`, `case 2` → 2 conditions. `default` is intentionally
10943 // excluded (the unconditional fallthrough, mirroring cyclomatic's
10944 // `Case`-only count). Since #469 every C-family language —
10945 // Java, C#, Groovy, JS, TS — agrees on this; C++ already did.
10946 // C = 2.
10947 check_metrics::<CppParser>(
10948 "void f(int x) {\n\
10949 switch (x) {\n\
10950 case 1: break;\n\
10951 case 2: break;\n\
10952 default: break;\n\
10953 }\n\
10954 }\n",
10955 "foo.cpp",
10956 |metric| {
10957 assert_eq!(metric.abc.conditions_sum(), 2);
10958 insta::assert_json_snapshot!(metric.abc);
10959 },
10960 );
10961 }
10962
10963 #[cfg(feature = "cpp")]
10964 #[test]
10965 fn cpp_try_catch_count_conditions() {
10966 // `try` and `catch` each add one condition (Fitzpatrick's rule;
10967 // Java's impl above counts them too).
10968 check_metrics::<CppParser>(
10969 "void f() { try { } catch (int) { } catch (...) { } }",
10970 "foo.cpp",
10971 |metric| {
10972 // 1 `try` + 2 `catch` arms = 3.
10973 assert_eq!(metric.abc.conditions_sum(), 3);
10974 insta::assert_json_snapshot!(metric.abc);
10975 },
10976 );
10977 }
10978
10979 #[cfg(feature = "cpp")]
10980 #[test]
10981 fn cpp_complex_function_abc() {
10982 // Mixed-shape regression: assignments, calls, conditions,
10983 // ternary, switch, new. Verified by hand:
10984 // - assignments: `int x = 0` (init_declarator with `=`),
10985 // `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`, `x = b`,
10986 // `auto* p = new int(5)` (init_declarator with `=`) → A = 7
10987 // (post-#393: every `=` in an init_declarator counts).
10988 // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
10989 // - conditions: `a == b` (1) + `a > 0` (1) inside the if;
10990 // `&&` itself is NOT a condition (Fitzpatrick Rule 5,
10991 // issue #395). `a > b` (1) + `?` (1) in the ternary.
10992 // `else` (1, from the `else if` keyword) + `a < b` (1)
10993 // in the else-if. `!x` contributes 1 via the unary-
10994 // conditional walker (Fitzpatrick Rule 9, issue #403):
10995 // the `||` walker treats `!x` as a unary boolean operand
10996 // and counts the wrapped Identifier once. `case 1`,
10997 // `case 2` → 2. `default` excluded. Total C = 9.
10998 check_metrics::<CppParser>(
10999 "int f(int a, int b) {\n\
11000 int x = 0;\n\
11001 x = 5;\n\
11002 x += 2;\n\
11003 x++;\n\
11004 if (a == b && a > 0) {\n\
11005 x = (a > b) ? a : b;\n\
11006 } else if (a < b || !x) {\n\
11007 x = b;\n\
11008 }\n\
11009 switch (x) {\n\
11010 case 1: break;\n\
11011 case 2: break;\n\
11012 default: break;\n\
11013 }\n\
11014 auto* p = new int(5);\n\
11015 return f(a, b);\n\
11016 }\n",
11017 "foo.cpp",
11018 |metric| {
11019 assert_eq!(metric.abc.assignments_sum(), 7);
11020 assert_eq!(metric.abc.branches_sum(), 2);
11021 assert_eq!(metric.abc.conditions_sum(), 9);
11022 insta::assert_json_snapshot!(metric.abc);
11023 },
11024 );
11025 }
11026
11027 #[cfg(feature = "cpp")]
11028 #[test]
11029 fn cpp_if_multiple_conditions() {
11030 // Fitzpatrick Rule 9 walker (issue #403): each operand of a
11031 // `&&` / `||` chain is one condition.
11032 check_metrics::<CppParser>(
11033 "void f(bool a, bool b, bool c, bool d) {\n\
11034 \x20 if (a || b || c || d) {} // +4c\n\
11035 \x20 if (a && b && c) {} // +3c\n\
11036 \x20 if (!a && !b) {} // +2c\n\
11037 }\n",
11038 "foo.cpp",
11039 |metric| {
11040 assert_eq!(metric.abc.conditions_sum(), 9);
11041 insta::assert_json_snapshot!(metric.abc);
11042 },
11043 );
11044 }
11045
11046 #[cfg(feature = "cpp")]
11047 #[test]
11048 fn cpp_while_and_do_while_conditions() {
11049 // Exercise both the WhileStatement and DoStatement arms via
11050 // the walker on the `&&` / `||` tokens inside their parens.
11051 check_metrics::<CppParser>(
11052 "void f(bool a, bool b) {\n\
11053 \x20 while (a || b) {} // +2c\n\
11054 \x20 do {} while (a && !b); // +2c\n\
11055 }\n",
11056 "foo.cpp",
11057 |metric| {
11058 assert_eq!(metric.abc.conditions_sum(), 4);
11059 insta::assert_json_snapshot!(metric.abc);
11060 },
11061 );
11062 }
11063
11064 #[cfg(feature = "cpp")]
11065 #[test]
11066 fn cpp_if_constexpr_condition_counts() {
11067 // Regression for the code-review finding: C++ `if constexpr
11068 // (cond)` puts the `constexpr` keyword at child(1) and the
11069 // condition_clause at child(2). Pre-fix, the dispatcher used
11070 // child(1) and counted zero conditions for the `constexpr`
11071 // form. The fix uses `child_by_field_name("condition")`
11072 // which returns the condition_clause regardless of the
11073 // optional `constexpr` keyword.
11074 check_metrics::<CppParser>(
11075 "template <int N> void f() {\n\
11076 \x20 if constexpr (true) { } // +1c\n\
11077 \x20 if (false) { } // +1c\n\
11078 }\n",
11079 "foo.cpp",
11080 |metric| {
11081 assert_eq!(metric.abc.conditions_sum(), 2);
11082 insta::assert_json_snapshot!(metric.abc);
11083 },
11084 );
11085 }
11086
11087 #[cfg(feature = "cpp")]
11088 #[test]
11089 fn cpp_cast_expression_in_logical_chain_counts() {
11090 // Regression for findings.md round-2 #1 (C++):
11091 // `if ((bool)ptr && ready) {}` had the `||` walker missing
11092 // the `(bool)ptr` operand because `CastExpression` was not
11093 // in `cpp_bool_terminal_kinds!()`. Mirrors C#'s
11094 // `csharp_bool_terminal_kinds!()` which lists
11095 // `CastExpression` (lesson 19, #372).
11096 check_metrics::<CppParser>(
11097 "void f(void* ptr, bool ready) { if ((bool)ptr && ready) { } }\n",
11098 "foo.cpp",
11099 |metric| {
11100 // `&&` walker counts both operands: `(bool)ptr` (1)
11101 // and `ready` (1). Total: 2.
11102 assert_eq!(metric.abc.conditions_sum(), 2);
11103 insta::assert_json_snapshot!(metric.abc);
11104 },
11105 );
11106 }
11107
11108 #[cfg(feature = "cpp")]
11109 #[test]
11110 fn cpp_qualified_identifier_condition_counts() {
11111 // Regression for findings.md #3 (C++): tree-sitter-cpp emits
11112 // `qualified_identifier` under four kind_ids (573..576) per
11113 // the production-rule path; runtime kind for `ns::flag` is
11114 // 574 (`QualifiedIdentifier2`). Pre-fix the
11115 // `cpp_bool_terminal_kinds!()` macro listed neither the
11116 // primary nor any alias, so `if (n::flag) {}` reported zero
11117 // conditions. The macro now includes all four variants
11118 // (lesson #2).
11119 check_metrics::<CppParser>(
11120 "namespace n { extern bool flag; }\n\
11121 void f() { if (n::flag) { } }\n",
11122 "foo.cpp",
11123 |metric| {
11124 assert_eq!(metric.abc.conditions_sum(), 1);
11125 insta::assert_json_snapshot!(metric.abc);
11126 },
11127 );
11128 }
11129
11130 #[cfg(feature = "cpp")]
11131 #[test]
11132 fn cpp_if_boolean_literal_condition() {
11133 check_metrics::<CppParser>(
11134 "void f() {\n\
11135 \x20 if (true) {} // +1c\n\
11136 \x20 if (!false) {} // +1c\n\
11137 \x20 while (true) {} // +1c\n\
11138 \x20 do {} while (false); // +1c\n\
11139 }\n",
11140 "foo.cpp",
11141 |metric| {
11142 assert_eq!(metric.abc.conditions_sum(), 4);
11143 insta::assert_json_snapshot!(metric.abc);
11144 },
11145 );
11146 }
11147
11148 #[cfg(feature = "cpp")]
11149 #[test]
11150 fn cpp_methods_arguments_with_conditions() {
11151 check_metrics::<CppParser>(
11152 "void f(bool a, bool b) {\n\
11153 \x20 m(a, b); // +1b\n\
11154 \x20 m(!a, !b); // +1b +2c\n\
11155 }\n",
11156 "foo.cpp",
11157 |metric| {
11158 assert_eq!(metric.abc.branches_sum(), 2);
11159 assert_eq!(metric.abc.conditions_sum(), 2);
11160 insta::assert_json_snapshot!(metric.abc);
11161 },
11162 );
11163 }
11164
11165 #[cfg(feature = "cpp")]
11166 #[test]
11167 fn cpp_return_with_conditions() {
11168 check_metrics::<CppParser>(
11169 "bool m1(int z) { return !(z >= 0); }\n\
11170 bool m2(bool x) { return (((!x))); }\n\
11171 bool m3(bool x, bool y) { return x && y; }\n",
11172 "foo.cpp",
11173 |metric| {
11174 // m1: !(z >= 0) → `>=` (1). `!` wraps a paren'd
11175 // BinaryExpression — inspect_container reaches
11176 // the inner BinaryExpression and stops, no
11177 // walker count. +1.
11178 // m2: (((!x))) → ReturnStatement → inspect_container
11179 // unwraps three parens + one unary → reaches `x`
11180 // in has_boolean_content=true (seeded by the
11181 // unary `!`). +1.
11182 // m3: x && y → `&&` walker counts both → +2.
11183 // Sum: 1 + 1 + 2 = 4.
11184 assert_eq!(metric.abc.conditions_sum(), 4);
11185 insta::assert_json_snapshot!(metric.abc);
11186 },
11187 );
11188 }
11189
11190 #[cfg(feature = "cpp")]
11191 #[test]
11192 fn cpp_short_circuit_with_boolean_literal_operand() {
11193 // `a && true` reports 2 conditions: one for the identifier
11194 // operand, one for the `True` literal operand.
11195 check_metrics::<CppParser>(
11196 "bool f(bool a) { return a && true; }\n",
11197 "foo.cpp",
11198 |metric| {
11199 assert_eq!(metric.abc.conditions_sum(), 2);
11200 insta::assert_json_snapshot!(metric.abc);
11201 },
11202 );
11203 }
11204
11205 #[cfg(feature = "javascript")]
11206 #[test]
11207 fn javascript_empty_unit_zero() {
11208 // No code → A=B=C=0. Wires up the trait and exercises the
11209 // per-language compute reachability.
11210 check_metrics::<JavascriptParser>("", "empty.js", |metric| {
11211 assert_eq!(metric.abc.assignments_sum(), 0);
11212 assert_eq!(metric.abc.branches_sum(), 0);
11213 assert_eq!(metric.abc.conditions_sum(), 0);
11214 insta::assert_json_snapshot!(metric.abc);
11215 });
11216 }
11217
11218 #[cfg(feature = "javascript")]
11219 #[test]
11220 fn javascript_plain_and_compound_assignments_count() {
11221 // `let` / `var` declarations behave like TypeScript: only a
11222 // `const` initializer is suppressed. So `let x = 0` does count as
11223 // A=+1; only `const PI = 3.14` would be elided. Plain `x = 5`,
11224 // `x += 2`, `x = 7` all count → A = 4 total here.
11225 check_metrics::<JavascriptParser>(
11226 "function f() { let x = 0; x = 5; x += 2; x = 7; }",
11227 "foo.js",
11228 |metric| {
11229 assert_eq!(metric.abc.assignments_sum(), 4);
11230 assert_eq!(metric.abc.branches_sum(), 0);
11231 assert_eq!(metric.abc.conditions_sum(), 0);
11232 insta::assert_json_snapshot!(metric.abc);
11233 },
11234 );
11235 }
11236
11237 #[cfg(feature = "javascript")]
11238 #[test]
11239 fn javascript_const_initializer_not_assignment() {
11240 // `const PI = 3.14` must NOT count as an assignment — its `=`
11241 // initialises a `const` binding. `let x = 1` and `var y = 2`
11242 // still count (matches the TS impl: only `const` suppresses).
11243 check_metrics::<JavascriptParser>(
11244 "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
11245 "foo.js",
11246 |metric| {
11247 // `const PI` suppressed; `let x = 1`, `var y = 2`,
11248 // `x = 9` all count → A = 3.
11249 assert_eq!(metric.abc.assignments_sum(), 3);
11250 insta::assert_json_snapshot!(metric.abc);
11251 },
11252 );
11253 }
11254
11255 #[cfg(feature = "javascript")]
11256 #[test]
11257 fn javascript_asi_const_does_not_suppress_later_assignments() {
11258 // The issue #1277 reproducer verbatim. JavaScript half of the
11259 // cluster documented at
11260 // `typescript_asi_const_does_not_suppress_later_assignments`.
11261 check_metrics::<JavascriptParser>(
11262 "function f() {
11263 const a = 1
11264 x = 2
11265 return x
11266 }",
11267 "foo.js",
11268 |metric| {
11269 // Pre-#1277 this reported 0; the semicolon-terminated
11270 // spelling reported 1.
11271 assert_eq!(metric.abc.assignments_sum(), 1);
11272 },
11273 );
11274 }
11275
11276 #[cfg(feature = "javascript")]
11277 #[test]
11278 fn javascript_nested_arrow_const_does_not_leak() {
11279 // The ASI leak beside a nested space: the arrow body opens its
11280 // own space, so `y = 1` was always counted there, but
11281 // `const f = () => …` has no `;`, so `h`'s sentinel stayed live
11282 // and suppressed `z = 2`. The stack was per-space state that
11283 // `Stats::merge` never carried, so this is the terminator defect
11284 // and not a second route.
11285 check_metrics::<JavascriptParser>(
11286 "function h() {
11287 const f = () => { y = 1 }
11288 z = 2
11289 }",
11290 "foo.js",
11291 |metric| {
11292 // `y = 1` and `z = 2`; the `const` initializer is
11293 // suppressed. Pre-#1277: 1.
11294 assert_eq!(metric.abc.assignments_sum(), 2);
11295 },
11296 );
11297 }
11298
11299 #[cfg(feature = "javascript")]
11300 #[test]
11301 fn javascript_non_declarator_equals_still_count() {
11302 // An `=` that does not belong to a `const` declarator is always
11303 // an assignment: a class `field_definition` initializer, a
11304 // default-parameter `assignment_pattern`, and a destructured
11305 // parameter's defaults, whose climb crosses the same pattern
11306 // layers as a `const` pattern's but ends at `formal_parameters`.
11307 // A predicate that accepted any pattern ancestor as a declarator
11308 // would zero the last three.
11309 check_metrics::<JavascriptParser>(
11310 "class K { f = 1; g(p = 2, {q = 3} = {}) { let d = 4 } }",
11311 "foo.js",
11312 |metric| {
11313 // `f = 1`, `p = 2`, `q = 3`, `= {}`, `let d = 4`.
11314 assert_eq!(metric.abc.assignments_sum(), 5);
11315 },
11316 );
11317 }
11318
11319 #[cfg(feature = "javascript")]
11320 #[test]
11321 fn javascript_const_initializer_value_assignments_still_count() {
11322 // An `=` inside a `const` initializer's *value* is an
11323 // `assignment_expression`: its climb reaches no pattern layer and
11324 // no declarator, so it counts. The pre-#1277 sentinel
11325 // blanket-suppressed every `=` between `const` and `;`, which is
11326 // the shape that moved the pdf.js corpus snapshots
11327 // (`const bbox = (this.data.rect = …)`).
11328 check_metrics::<JavascriptParser>(
11329 "function m(o, a, b) { const x = (o.p = 1); const y = a || (b = 2); }",
11330 "foo.js",
11331 |metric| {
11332 // `o.p = 1` and `b = 2`; the two `const` initializers are
11333 // suppressed. Pre-#1277: 0.
11334 assert_eq!(metric.abc.assignments_sum(), 2);
11335 },
11336 );
11337 }
11338
11339 #[cfg(feature = "javascript")]
11340 #[test]
11341 fn javascript_const_declarator_shapes_stay_suppressed() {
11342 // JavaScript half of
11343 // `typescript_const_declarator_shapes_stay_suppressed`.
11344 check_metrics::<JavascriptParser>(
11345 "function f(o, xs) {
11346 const a = 1, b = 2
11347 const {c = 5, d: {e = 6} = {}} = o
11348 const [g = 7, ...[h = 8]] = xs
11349 let i = 3
11350 var j = 4
11351 for (const x of xs) { k(x) }
11352 }",
11353 "foo.js",
11354 |metric| {
11355 assert_eq!(metric.abc.assignments_sum(), 2);
11356 },
11357 );
11358 }
11359
11360 #[cfg(feature = "javascript")]
11361 #[test]
11362 fn javascript_increment_and_decrement_count_as_assignment() {
11363 // `x++` (post) and `--x` (pre) both update an lvalue and so
11364 // count as assignments. Combined with the `let x = 0`
11365 // initializer (which counts under the JS/TS rule — only `const`
11366 // suppresses), A = 3.
11367 check_metrics::<JavascriptParser>(
11368 "function f() { let x = 0; x++; --x; }",
11369 "foo.js",
11370 |metric| {
11371 assert_eq!(metric.abc.assignments_sum(), 3);
11372 insta::assert_json_snapshot!(metric.abc);
11373 },
11374 );
11375 }
11376
11377 #[cfg(feature = "javascript")]
11378 #[test]
11379 fn javascript_calls_are_branches() {
11380 // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
11381 // `new_expression` → B = 1. Total B = 2.
11382 check_metrics::<JavascriptParser>(
11383 "function f() { g(1); new Foo(2); }",
11384 "foo.js",
11385 |metric| {
11386 assert_eq!(metric.abc.branches_sum(), 2);
11387 assert_eq!(metric.abc.conditions_sum(), 0);
11388 insta::assert_json_snapshot!(metric.abc);
11389 },
11390 );
11391 }
11392
11393 #[cfg(feature = "javascript")]
11394 #[test]
11395 fn javascript_comparisons_count_conditions() {
11396 // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
11397 // once. The `&&` / `||` short-circuit operators are NOT
11398 // counted as conditions in this impl (matches the TS
11399 // precedent — short-circuit ops are folded into the
11400 // surrounding `if` / control-flow arm, not separately).
11401 // Total C = 8.
11402 check_metrics::<JavascriptParser>(
11403 "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
11404 "foo.js",
11405 |metric| {
11406 assert_eq!(metric.abc.conditions_sum(), 8);
11407 insta::assert_json_snapshot!(metric.abc);
11408 },
11409 );
11410 }
11411
11412 // #1297 in the `js_abc_compute!` expansion, where `LT` / `GT` had
11413 // no gate at all: plain JavaScript has no generics, but
11414 // tree-sitter-javascript parses JSX unconditionally, so the same
11415 // seven tag brackets that
11416 // `tsx_jsx_elements_are_not_conditions` covers scored seven
11417 // conditions in a `.js` file too. Same fixture minus the type
11418 // annotations, same discriminating numbers: 8 pre-fix, 1 after.
11419 #[cfg(feature = "javascript")]
11420 #[test]
11421 fn javascript_jsx_elements_are_not_conditions() {
11422 check_metrics::<JavascriptParser>(
11423 "function f(a, b) {
11424 if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
11425 return <br />;
11426 }",
11427 "foo.js",
11428 |metric| {
11429 assert_eq!(metric.abc.conditions_sum(), 1);
11430 // Fixture-presence anchor: the `className="x"` attribute
11431 // `=` is this fixture's only assignment, so the test
11432 // fails if a later edit drops the JSX. Without it the
11433 // surviving `a < b` alone also scores 1, and the
11434 // conditions assertion would pass while proving nothing.
11435 assert_eq!(metric.abc.assignments_sum(), 1);
11436 },
11437 );
11438 }
11439
11440 #[cfg(feature = "javascript")]
11441 #[test]
11442 fn javascript_number_truthy_condition_counts() {
11443 // Regression for #772: JS treats every non-zero number as
11444 // truthy, so `while (5)` and `x && 5` should each count their
11445 // numeric literal as a Fitzpatrick unary condition. Pre-fix
11446 // `javascript_bool_terminal_kinds!()` listed `True` / `False`
11447 // but omitted `Number`, so the walker dropped every numeric-
11448 // truthy operand (mirrors the Lua `Number` fix).
11449 check_metrics::<JavascriptParser>(
11450 "function f(x) { while (5) {} return x && 5; }",
11451 "foo.js",
11452 |metric| {
11453 // `while (5)` → Number literal (+1). `x && 5` → both
11454 // operands count: identifier `x` (+1), Number `5` (+1).
11455 // Total: 3.
11456 assert_eq!(metric.abc.conditions_sum(), 3);
11457 insta::assert_json_snapshot!(metric.abc);
11458 },
11459 );
11460 }
11461
11462 #[cfg(feature = "typescript")]
11463 #[test]
11464 fn typescript_number_truthy_condition_counts() {
11465 // Regression for #772: TS shares the JS truthy semantics. The
11466 // numeric *literal* `5` (kind `Number`) counts; the type-keyword
11467 // `number` (kind `Number2`, the `predefined_type`) must not —
11468 // see `typescript_bool_terminal_kinds!`.
11469 check_metrics::<TypescriptParser>(
11470 "function f(x: number) { while (5) {} return x && 5; }",
11471 "foo.ts",
11472 |metric| {
11473 // `while (5)` → +1; `x && 5` → `x` (+1) + `5` (+1).
11474 // Total: 3. The `: number` annotation contributes 0.
11475 assert_eq!(metric.abc.conditions_sum(), 3);
11476 insta::assert_json_snapshot!(metric.abc);
11477 },
11478 );
11479 }
11480
11481 #[cfg(feature = "javascript")]
11482 #[test]
11483 fn javascript_nullish_coalescing_counts_condition() {
11484 // `a ?? b` is one nullish-coalescing operator → C = 1.
11485 check_metrics::<JavascriptParser>(
11486 "function f(a, b) { return a ?? b; }",
11487 "foo.js",
11488 |metric| {
11489 assert_eq!(metric.abc.conditions_sum(), 1);
11490 insta::assert_json_snapshot!(metric.abc);
11491 },
11492 );
11493 }
11494
11495 #[cfg(feature = "javascript")]
11496 #[test]
11497 fn javascript_else_ternary_case_default_try_catch() {
11498 // `else`, `?` (ternary), `case`, `try`, `catch` all count.
11499 // `default` is the unconditional fallthrough → +0 (#469).
11500 // With the comparisons:
11501 // - `a > 0` → 1
11502 // - `else` opens an else_clause → 1
11503 // - `?` ternary → 1
11504 // - the ternary's bare-identifier condition `a` → 1 (#1102)
11505 // - `case 1` → 1
11506 // - `default` → 0 (fallthrough, #469)
11507 // - `try` + `catch` → 2
11508 // Total C = 7.
11509 check_metrics::<JavascriptParser>(
11510 "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
11511 "foo.js",
11512 |metric| {
11513 assert_eq!(metric.abc.conditions_sum(), 7);
11514 insta::assert_json_snapshot!(metric.abc);
11515 },
11516 );
11517 }
11518
11519 // Issue #1102, JS-family half. See
11520 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
11521 // rule; the two families were behind Java by the same three units.
11522 #[cfg(feature = "javascript")]
11523 #[test]
11524 fn javascript_ternary_operand_slots_count_as_unary_conditions() {
11525 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
11526 check_metrics::<JavascriptParser>(
11527 "function f() { x = a ? !b : !c; }",
11528 "foo.js",
11529 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
11530 );
11531 // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
11532 // fix — the parenthesised condition unwraps to a
11533 // `binary_expression` (not a boolean terminal) and neither
11534 // branch is negated.
11535 check_metrics::<JavascriptParser>(
11536 "function f() { x = (a > 0) ? b : -b; }",
11537 "foo.js",
11538 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
11539 );
11540 // Nested: two `?` tokens plus the two bare-identifier
11541 // conditions = 4.
11542 check_metrics::<JavascriptParser>(
11543 "function f() { x = a ? b ? c : d : e; }",
11544 "foo.js",
11545 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
11546 );
11547 // A negated condition is the only input reaching the walker's
11548 // `else` fallback — see the C++ sibling for why. `?` (1) +
11549 // `!a` (1) = 2.
11550 check_metrics::<JavascriptParser>("function f() { x = !a ? b : c; }", "foo.js", |metric| {
11551 assert_eq!(metric.abc.conditions_sum(), 2);
11552 });
11553 }
11554
11555 // TypeScript expands the same `ts_abc_compute!` arm from a separate
11556 // macro body than JavaScript's `js_abc_compute!`, so wiring one and
11557 // not the other is a live failure mode; TSX and Mozjs are clones of
11558 // these two.
11559 #[cfg(feature = "typescript")]
11560 #[test]
11561 fn typescript_ternary_operand_slots_count_as_unary_conditions() {
11562 check_metrics::<TypescriptParser>(
11563 "function f() { x = a ? !b : !c; }",
11564 "foo.ts",
11565 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
11566 );
11567 check_metrics::<TypescriptParser>(
11568 "function f() { x = (a > 0) ? b : -b; }",
11569 "foo.ts",
11570 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
11571 );
11572 }
11573
11574 // Issue #1276, JS-family half. See
11575 // `cpp_for_condition_slot_counts_unary_conditions` for the rule.
11576 // The JS grammar marks the `condition` field on both the expression
11577 // and the `;` closing it, so `child_by_field_name` is the only
11578 // addressing that lands on the expression for every header shape.
11579 #[cfg(feature = "javascript")]
11580 #[test]
11581 fn javascript_for_condition_slot_counts_unary_conditions() {
11582 // Bare identifier: no operator token anywhere in the header.
11583 check_metrics::<JavascriptParser>("function f(a) { for (; a; ) {} }", "foo.js", |metric| {
11584 assert_eq!(metric.abc.conditions_sum(), 1);
11585 });
11586 // Negation, through the `!` unwrap.
11587 check_metrics::<JavascriptParser>(
11588 "function f(a) { for (; !a; ) {} }",
11589 "foo.js",
11590 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11591 );
11592 // Parentheses: counts only via the `ForStatement`
11593 // boolean-context seed #1276 found dead.
11594 check_metrics::<JavascriptParser>(
11595 "function f(a) { for (; (a); ) {} }",
11596 "foo.js",
11597 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11598 );
11599 // No-double-count pin: the `<` arm already counted this shape.
11600 // `let i = 0` and `i++` are the two assignments, which also
11601 // confirms the header parsed as the three-clause form.
11602 check_metrics::<JavascriptParser>(
11603 "function f(n) { for (let i = 0; i < n; i++) {} }",
11604 "foo.js",
11605 |metric| {
11606 assert_eq!(metric.abc.conditions_sum(), 1);
11607 assert_eq!(metric.abc.assignments_sum(), 2);
11608 },
11609 );
11610 // Empty condition: the slot holds an `empty_statement`, which
11611 // is neither a terminal nor a wrapper, so it counts nothing.
11612 check_metrics::<JavascriptParser>(
11613 "function f() { for (;;) { break; } }",
11614 "foo.js",
11615 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
11616 );
11617 }
11618
11619 // TypeScript expands the `ForStatement` arm from `ts_abc_compute!`,
11620 // a separate macro body from JavaScript's `js_abc_compute!`, so
11621 // wiring one and not the other is a live failure mode; TSX and
11622 // Mozjs are the clones of those two. The expected values are
11623 // derived from the JavaScript run rather than hardcoded.
11624 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
11625 #[test]
11626 fn js_family_for_condition_slot_agrees_with_javascript() {
11627 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
11628 const BARE: &str = "function f(a) { for (; a; ) {} }\n";
11629 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
11630 const EMPTY: &str = "function f() { for (;;) { break; } }\n";
11631 let conditions = abc_conditions;
11632
11633 let bare = conditions(LANG::Javascript, BARE);
11634 // Non-degenerate: a zeroed reference makes the comparisons
11635 // below vacuous.
11636 assert_eq!(bare, 1, "JavaScript reference value for `for (; a; )`");
11637 assert_eq!(
11638 conditions(LANG::Javascript, EMPTY),
11639 0,
11640 "JavaScript `for (;;)`"
11641 );
11642
11643 for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
11644 assert_eq!(conditions(lang, BARE), bare, "{lang:?} bare for-condition");
11645 assert_eq!(conditions(lang, EMPTY), 0, "{lang:?} empty for-condition");
11646 }
11647 }
11648
11649 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
11650 #[test]
11651 fn js_family_ternary_operand_slots_agree_with_javascript() {
11652 // `a ? !b : !c` is the one shape that tells the ternary walker
11653 // from the `for`-header walker: both read the `condition` field
11654 // and share the `(&Node, &mut f64)` signature, so a transposed
11655 // pair in a `ts_abc_compute!` / `js_abc_compute!` invocation
11656 // compiles, passes every condition-slot test, and drops only the
11657 // two branch operands. TypeScript alone pinned those before; the
11658 // other three expansions now do too.
11659 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
11660 const SRC: &str = "function f(a, b, c) { x = a ? !b : !c; }\n";
11661 let javascript = abc_conditions(LANG::Javascript, SRC);
11662 // expected: 4 — the `?`, the `a` condition slot and both negated
11663 // branch operands; non-degenerate by construction.
11664 assert_eq!(
11665 javascript, 4,
11666 "JavaScript reference value for `a ? !b : !c`"
11667 );
11668 for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
11669 assert_eq!(
11670 abc_conditions(lang, SRC),
11671 javascript,
11672 "{lang:?} ternary operand slots"
11673 );
11674 }
11675 }
11676
11677 #[cfg(feature = "javascript")]
11678 #[test]
11679 fn javascript_instanceof_counts_condition() {
11680 // `x instanceof Foo` is a binary expression whose operator is
11681 // the `instanceof` keyword token → C = 1.
11682 check_metrics::<JavascriptParser>(
11683 "function f(x) { return x instanceof Foo; }",
11684 "foo.js",
11685 |metric| {
11686 assert_eq!(metric.abc.conditions_sum(), 1);
11687 insta::assert_json_snapshot!(metric.abc);
11688 },
11689 );
11690 }
11691
11692 #[cfg(feature = "javascript")]
11693 #[test]
11694 fn javascript_complex_function_abc() {
11695 // Mixed-shape regression. Verified by hand:
11696 // - assignments: `let x = 0` (a `let` initializer counts)
11697 // + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
11698 // `let p = ...` (likewise) → A = 7.
11699 // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
11700 // - conditions: `a == b`, `a > 0` → 2 inside the if header
11701 // (`&&` is not counted directly). `else` (1) + `a > b`,
11702 // `?` → 2 in the ternary. `a < b` → 1 in the else-if.
11703 // `!x` → 1 from the Fitzpatrick Rule 9 walker on `||`
11704 // (issue #403): the wrapped Identifier counts once.
11705 // `case 1` → 1 in the switch; `default` → 0 (fallthrough,
11706 // #469). Total C = 8.
11707 check_metrics::<JavascriptParser>(
11708 "function f(a, b) {\n\
11709 let x = 0;\n\
11710 x = 5;\n\
11711 x += 2;\n\
11712 x++;\n\
11713 if (a == b && a > 0) {\n\
11714 x = (a > b) ? a : b;\n\
11715 } else if (a < b || !x) {\n\
11716 x = b;\n\
11717 }\n\
11718 switch (x) {\n\
11719 case 1: break;\n\
11720 default: break;\n\
11721 }\n\
11722 let p = new Bar();\n\
11723 return f(a, b);\n\
11724 }\n",
11725 "foo.js",
11726 |metric| {
11727 assert_eq!(metric.abc.assignments_sum(), 7);
11728 assert_eq!(metric.abc.branches_sum(), 2);
11729 assert_eq!(metric.abc.conditions_sum(), 8);
11730 insta::assert_json_snapshot!(metric.abc);
11731 },
11732 );
11733 }
11734
11735 #[cfg(feature = "mozjs")]
11736 #[test]
11737 fn mozjs_asi_const_does_not_suppress_later_assignments() {
11738 // Mozjs half of the #1277 cluster; the fork carries its own
11739 // kind-id numbering, so it needs its own fixture. See
11740 // `typescript_asi_const_does_not_suppress_later_assignments`.
11741 check_metrics::<MozjsParser>(
11742 "function f() {
11743 const a = 1
11744 x = 2
11745 y = 3
11746 }",
11747 "foo.jsm",
11748 |metric| {
11749 assert_eq!(metric.abc.assignments_sum(), 2);
11750 },
11751 );
11752 }
11753
11754 #[cfg(feature = "mozjs")]
11755 #[test]
11756 fn mozjs_const_declarator_shapes_stay_suppressed() {
11757 // Mozjs half of
11758 // `javascript_const_declarator_shapes_stay_suppressed`.
11759 check_metrics::<MozjsParser>(
11760 "function f(o, xs) {
11761 const a = 1, b = 2
11762 const {c = 5, d: {e = 6} = {}} = o
11763 const [g = 7, ...[h = 8]] = xs
11764 let i = 3
11765 var j = 4
11766 for (const x of xs) { k(x) }
11767 }",
11768 "foo.jsm",
11769 |metric| {
11770 assert_eq!(metric.abc.assignments_sum(), 2);
11771 },
11772 );
11773 }
11774
11775 #[cfg(feature = "mozjs")]
11776 #[test]
11777 fn mozjs_complex_function_abc() {
11778 // Mozjs shares JavaScript's expression / statement vocabulary;
11779 // the `js_abc_compute!` macro expands identical token-level
11780 // rules for both. This test pins parity against the JS impl.
11781 check_metrics::<MozjsParser>(
11782 "function f(a, b) {\n\
11783 let x = 0;\n\
11784 x = 5;\n\
11785 x += 2;\n\
11786 x++;\n\
11787 if (a == b && a > 0) {\n\
11788 x = (a > b) ? a : b;\n\
11789 } else if (a < b || !x) {\n\
11790 x = b;\n\
11791 }\n\
11792 switch (x) {\n\
11793 case 1: break;\n\
11794 default: break;\n\
11795 }\n\
11796 let p = new Bar();\n\
11797 return f(a, b);\n\
11798 }\n",
11799 "foo.js",
11800 |metric| {
11801 assert_eq!(metric.abc.assignments_sum(), 7);
11802 assert_eq!(metric.abc.branches_sum(), 2);
11803 assert_eq!(metric.abc.conditions_sum(), 8);
11804 insta::assert_json_snapshot!(metric.abc);
11805 },
11806 );
11807 }
11808
11809 // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots -----
11810
11811 #[cfg(feature = "javascript")]
11812 #[test]
11813 fn javascript_await_expression_condition_counts() {
11814 // Regression for findings.md round-2 #2 (JS):
11815 // `if (await ready()) {}` parses with `await_expression` as
11816 // the condition node inside the `parenthesized_expression`.
11817 // `javascript_inspect_container` unwraps the paren but the
11818 // await child was not in the terminal-bool set, so the
11819 // walker broke without counting. Mirrors C# (lesson 19).
11820 check_metrics::<JavascriptParser>(
11821 "async function ready() { return true; }\n\
11822 async function f() { if (await ready()) { } }\n",
11823 "foo.js",
11824 |metric| {
11825 assert_eq!(metric.abc.branches_sum(), 1);
11826 assert_eq!(metric.abc.conditions_sum(), 1);
11827 insta::assert_json_snapshot!(metric.abc);
11828 },
11829 );
11830 }
11831
11832 #[cfg(feature = "javascript")]
11833 #[test]
11834 fn javascript_member_expression_condition_counts() {
11835 // Regression for findings.md #3 (JS-family): tree-sitter-
11836 // javascript emits `member_expression` under three kind_ids
11837 // (191 primary, 208, 228 — `MemberExpression2/3`) depending
11838 // on the production rule path. The verifier in this audit
11839 // confirmed runtime kind for `o.x` is 208. Pre-fix the
11840 // shared `js_family_bool_terminal_kinds!()` macro listed
11841 // only the primary, so every `if (o.x) {}` / `o.x && o.y`
11842 // condition silently reported zero. The per-language macro
11843 // now includes all three aliases (lesson #2).
11844 check_metrics::<JavascriptParser>(
11845 "function f(o) {\n\
11846 \x20 if (o.x) {} // +1c\n\
11847 \x20 return o.x && o.y; // +2c (walker on &&)\n\
11848 }\n",
11849 "foo.js",
11850 |metric| {
11851 assert_eq!(metric.abc.conditions_sum(), 3);
11852 insta::assert_json_snapshot!(metric.abc);
11853 },
11854 );
11855 }
11856
11857 #[cfg(feature = "javascript")]
11858 #[test]
11859 fn javascript_if_boolean_literal_condition() {
11860 check_metrics::<JavascriptParser>(
11861 "function f() {\n\
11862 \x20 if (true) {} // +1c\n\
11863 \x20 if (!false) {} // +1c\n\
11864 \x20 while (true) {} // +1c\n\
11865 \x20 do {} while (false); // +1c\n\
11866 }\n",
11867 "foo.js",
11868 |metric| {
11869 assert_eq!(metric.abc.conditions_sum(), 4);
11870 insta::assert_json_snapshot!(metric.abc);
11871 },
11872 );
11873 }
11874
11875 #[cfg(feature = "javascript")]
11876 #[test]
11877 fn javascript_methods_arguments_with_conditions() {
11878 check_metrics::<JavascriptParser>(
11879 "function f(a, b) {\n\
11880 \x20 m(a, b); // +1b\n\
11881 \x20 m(!a, !b); // +1b +2c\n\
11882 }\n",
11883 "foo.js",
11884 |metric| {
11885 assert_eq!(metric.abc.branches_sum(), 2);
11886 assert_eq!(metric.abc.conditions_sum(), 2);
11887 insta::assert_json_snapshot!(metric.abc);
11888 },
11889 );
11890 }
11891
11892 #[cfg(feature = "javascript")]
11893 #[test]
11894 fn javascript_return_with_conditions() {
11895 check_metrics::<JavascriptParser>(
11896 "function m1(z) { return !(z >= 0); }\n\
11897 function m2(x) { return (((!x))); }\n\
11898 function m3(x, y) { return x && y; }\n",
11899 "foo.js",
11900 |metric| {
11901 // m1: 1 (`>=`). m2: 1 (walker unwraps to `x`).
11902 // m3: 2 (`&&` walker counts both terminals).
11903 assert_eq!(metric.abc.conditions_sum(), 4);
11904 insta::assert_json_snapshot!(metric.abc);
11905 },
11906 );
11907 }
11908
11909 #[cfg(feature = "typescript")]
11910 #[test]
11911 fn typescript_if_boolean_literal_condition() {
11912 check_metrics::<TypescriptParser>(
11913 "function f() {\n\
11914 \x20 if (true) {}\n\
11915 \x20 if (!false) {}\n\
11916 \x20 while (true) {}\n\
11917 \x20 do {} while (false);\n\
11918 }\n",
11919 "foo.ts",
11920 |metric| {
11921 assert_eq!(metric.abc.conditions_sum(), 4);
11922 insta::assert_json_snapshot!(metric.abc);
11923 },
11924 );
11925 }
11926
11927 #[cfg(feature = "typescript")]
11928 #[test]
11929 fn typescript_methods_arguments_with_conditions() {
11930 check_metrics::<TypescriptParser>(
11931 "function f(a: boolean, b: boolean) {\n\
11932 \x20 m(a, b);\n\
11933 \x20 m(!a, !b);\n\
11934 }\n",
11935 "foo.ts",
11936 |metric| {
11937 assert_eq!(metric.abc.branches_sum(), 2);
11938 assert_eq!(metric.abc.conditions_sum(), 2);
11939 insta::assert_json_snapshot!(metric.abc);
11940 },
11941 );
11942 }
11943
11944 #[cfg(feature = "typescript")]
11945 #[test]
11946 fn typescript_return_with_conditions() {
11947 check_metrics::<TypescriptParser>(
11948 "function m1(z: number): boolean { return !(z >= 0); }\n\
11949 function m2(x: boolean): boolean { return (((!x))); }\n\
11950 function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
11951 "foo.ts",
11952 |metric| {
11953 assert_eq!(metric.abc.conditions_sum(), 4);
11954 insta::assert_json_snapshot!(metric.abc);
11955 },
11956 );
11957 }
11958
11959 #[cfg(feature = "typescript")]
11960 #[test]
11961 fn tsx_if_boolean_literal_condition() {
11962 check_metrics::<TsxParser>(
11963 "function f() {\n\
11964 \x20 if (true) {}\n\
11965 \x20 if (!false) {}\n\
11966 \x20 while (true) {}\n\
11967 \x20 do {} while (false);\n\
11968 }\n",
11969 "foo.tsx",
11970 |metric| {
11971 assert_eq!(metric.abc.conditions_sum(), 4);
11972 insta::assert_json_snapshot!(metric.abc);
11973 },
11974 );
11975 }
11976
11977 #[cfg(feature = "typescript")]
11978 #[test]
11979 fn tsx_methods_arguments_with_conditions() {
11980 check_metrics::<TsxParser>(
11981 "function f(a: boolean, b: boolean) {\n\
11982 \x20 m(a, b);\n\
11983 \x20 m(!a, !b);\n\
11984 }\n",
11985 "foo.tsx",
11986 |metric| {
11987 assert_eq!(metric.abc.branches_sum(), 2);
11988 assert_eq!(metric.abc.conditions_sum(), 2);
11989 insta::assert_json_snapshot!(metric.abc);
11990 },
11991 );
11992 }
11993
11994 #[cfg(feature = "typescript")]
11995 #[test]
11996 fn tsx_return_with_conditions() {
11997 check_metrics::<TsxParser>(
11998 "function m1(z: number): boolean { return !(z >= 0); }\n\
11999 function m2(x: boolean): boolean { return (((!x))); }\n\
12000 function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
12001 "foo.tsx",
12002 |metric| {
12003 assert_eq!(metric.abc.conditions_sum(), 4);
12004 insta::assert_json_snapshot!(metric.abc);
12005 },
12006 );
12007 }
12008
12009 #[cfg(feature = "mozjs")]
12010 #[test]
12011 fn mozjs_jsx_elements_are_not_conditions() {
12012 // #1297 in the second expansion of `js_abc_compute!`. The
12013 // vendored mozjs fork carries the same three JSX productions as
12014 // upstream tree-sitter-javascript with its own `kind_id`s, so
12015 // the gate is a distinct instantiation and needs its own
12016 // fixture — a passing JavaScript test says nothing about the
12017 // macro's other expansion. Same shape and same discriminating
12018 // numbers as `javascript_jsx_elements_are_not_conditions`: 8
12019 // pre-fix, 1 after.
12020 check_metrics::<MozjsParser>(
12021 "function f(a, b) {
12022 if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
12023 return <br />;
12024 }",
12025 "foo.js",
12026 |metric| {
12027 assert_eq!(metric.abc.conditions_sum(), 1);
12028 // Fixture-presence anchor: the `className="x"` attribute
12029 // `=` is this fixture's only assignment, so the test
12030 // fails if a later edit drops the JSX. Without it the
12031 // surviving `a < b` alone also scores 1, and the
12032 // conditions assertion would pass while proving nothing.
12033 assert_eq!(metric.abc.assignments_sum(), 1);
12034 },
12035 );
12036 }
12037
12038 #[cfg(feature = "mozjs")]
12039 #[test]
12040 fn mozjs_if_boolean_literal_condition() {
12041 check_metrics::<MozjsParser>(
12042 "function f() {\n\
12043 \x20 if (true) {}\n\
12044 \x20 if (!false) {}\n\
12045 \x20 while (true) {}\n\
12046 \x20 do {} while (false);\n\
12047 }\n",
12048 "foo.js",
12049 |metric| {
12050 assert_eq!(metric.abc.conditions_sum(), 4);
12051 insta::assert_json_snapshot!(metric.abc);
12052 },
12053 );
12054 }
12055
12056 #[cfg(feature = "mozjs")]
12057 #[test]
12058 fn mozjs_methods_arguments_with_conditions() {
12059 check_metrics::<MozjsParser>(
12060 "function f(a, b) {\n\
12061 \x20 m(a, b);\n\
12062 \x20 m(!a, !b);\n\
12063 }\n",
12064 "foo.js",
12065 |metric| {
12066 assert_eq!(metric.abc.branches_sum(), 2);
12067 assert_eq!(metric.abc.conditions_sum(), 2);
12068 insta::assert_json_snapshot!(metric.abc);
12069 },
12070 );
12071 }
12072
12073 #[cfg(feature = "mozjs")]
12074 #[test]
12075 fn mozjs_return_with_conditions() {
12076 check_metrics::<MozjsParser>(
12077 "function m1(z) { return !(z >= 0); }\n\
12078 function m2(x) { return (((!x))); }\n\
12079 function m3(x, y) { return x && y; }\n",
12080 "foo.js",
12081 |metric| {
12082 assert_eq!(metric.abc.conditions_sum(), 4);
12083 insta::assert_json_snapshot!(metric.abc);
12084 },
12085 );
12086 }
12087
12088 // ----- JS / TS / Tsx / Mozjs unary-conditional walker -----
12089
12090 #[cfg(feature = "javascript")]
12091 #[test]
12092 fn javascript_if_multiple_conditions() {
12093 check_metrics::<JavascriptParser>(
12094 "function f(a, b, c, d) {\n\
12095 \x20 if (a || b || c || d) {} // +4c\n\
12096 \x20 if (a && b && c) {} // +3c\n\
12097 \x20 if (!a && !b) {} // +2c\n\
12098 }\n",
12099 "foo.js",
12100 |metric| {
12101 assert_eq!(metric.abc.conditions_sum(), 9);
12102 insta::assert_json_snapshot!(metric.abc);
12103 },
12104 );
12105 }
12106
12107 #[cfg(feature = "javascript")]
12108 #[test]
12109 fn javascript_while_and_do_while_conditions() {
12110 check_metrics::<JavascriptParser>(
12111 "function f(a, b) {\n\
12112 \x20 while (a || b) {} // +2c\n\
12113 \x20 do {} while (a && !b); // +2c\n\
12114 }\n",
12115 "foo.js",
12116 |metric| {
12117 assert_eq!(metric.abc.conditions_sum(), 4);
12118 insta::assert_json_snapshot!(metric.abc);
12119 },
12120 );
12121 }
12122
12123 #[cfg(feature = "javascript")]
12124 #[test]
12125 fn javascript_short_circuit_with_boolean_literal_operand() {
12126 check_metrics::<JavascriptParser>(
12127 "function f(a) { return a && true; }\n",
12128 "foo.js",
12129 |metric| {
12130 assert_eq!(metric.abc.conditions_sum(), 2);
12131 insta::assert_json_snapshot!(metric.abc);
12132 },
12133 );
12134 }
12135
12136 #[cfg(feature = "typescript")]
12137 #[test]
12138 fn typescript_if_multiple_conditions() {
12139 check_metrics::<TypescriptParser>(
12140 "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
12141 \x20 if (a || b || c || d) {} // +4c\n\
12142 \x20 if (a && b && c) {} // +3c\n\
12143 \x20 if (!a && !b) {} // +2c\n\
12144 }\n",
12145 "foo.ts",
12146 |metric| {
12147 assert_eq!(metric.abc.conditions_sum(), 9);
12148 insta::assert_json_snapshot!(metric.abc);
12149 },
12150 );
12151 }
12152
12153 #[cfg(feature = "typescript")]
12154 #[test]
12155 fn typescript_while_and_do_while_conditions() {
12156 check_metrics::<TypescriptParser>(
12157 "function f(a: boolean, b: boolean) {\n\
12158 \x20 while (a || b) {} // +2c\n\
12159 \x20 do {} while (a && !b); // +2c\n\
12160 }\n",
12161 "foo.ts",
12162 |metric| {
12163 assert_eq!(metric.abc.conditions_sum(), 4);
12164 insta::assert_json_snapshot!(metric.abc);
12165 },
12166 );
12167 }
12168
12169 #[cfg(feature = "typescript")]
12170 #[test]
12171 fn typescript_short_circuit_with_boolean_literal_operand() {
12172 check_metrics::<TypescriptParser>(
12173 "function f(a: boolean): boolean { return a && true; }\n",
12174 "foo.ts",
12175 |metric| {
12176 assert_eq!(metric.abc.conditions_sum(), 2);
12177 insta::assert_json_snapshot!(metric.abc);
12178 },
12179 );
12180 }
12181
12182 #[cfg(feature = "typescript")]
12183 #[test]
12184 fn tsx_if_multiple_conditions() {
12185 check_metrics::<TsxParser>(
12186 "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
12187 \x20 if (a || b || c || d) {} // +4c\n\
12188 \x20 if (a && b && c) {} // +3c\n\
12189 \x20 if (!a && !b) {} // +2c\n\
12190 }\n",
12191 "foo.tsx",
12192 |metric| {
12193 assert_eq!(metric.abc.conditions_sum(), 9);
12194 insta::assert_json_snapshot!(metric.abc);
12195 },
12196 );
12197 }
12198
12199 #[cfg(feature = "typescript")]
12200 #[test]
12201 fn tsx_while_and_do_while_conditions() {
12202 check_metrics::<TsxParser>(
12203 "function f(a: boolean, b: boolean) {\n\
12204 \x20 while (a || b) {} // +2c\n\
12205 \x20 do {} while (a && !b); // +2c\n\
12206 }\n",
12207 "foo.tsx",
12208 |metric| {
12209 assert_eq!(metric.abc.conditions_sum(), 4);
12210 insta::assert_json_snapshot!(metric.abc);
12211 },
12212 );
12213 }
12214
12215 #[cfg(feature = "typescript")]
12216 #[test]
12217 fn tsx_short_circuit_with_boolean_literal_operand() {
12218 check_metrics::<TsxParser>(
12219 "function f(a: boolean): boolean { return a && true; }\n",
12220 "foo.tsx",
12221 |metric| {
12222 assert_eq!(metric.abc.conditions_sum(), 2);
12223 insta::assert_json_snapshot!(metric.abc);
12224 },
12225 );
12226 }
12227
12228 #[cfg(feature = "mozjs")]
12229 #[test]
12230 fn mozjs_if_multiple_conditions() {
12231 check_metrics::<MozjsParser>(
12232 "function f(a, b, c, d) {\n\
12233 \x20 if (a || b || c || d) {} // +4c\n\
12234 \x20 if (a && b && c) {} // +3c\n\
12235 \x20 if (!a && !b) {} // +2c\n\
12236 }\n",
12237 "foo.js",
12238 |metric| {
12239 assert_eq!(metric.abc.conditions_sum(), 9);
12240 insta::assert_json_snapshot!(metric.abc);
12241 },
12242 );
12243 }
12244
12245 #[cfg(feature = "mozjs")]
12246 #[test]
12247 fn mozjs_while_and_do_while_conditions() {
12248 check_metrics::<MozjsParser>(
12249 "function f(a, b) {\n\
12250 \x20 while (a || b) {} // +2c\n\
12251 \x20 do {} while (a && !b); // +2c\n\
12252 }\n",
12253 "foo.js",
12254 |metric| {
12255 assert_eq!(metric.abc.conditions_sum(), 4);
12256 insta::assert_json_snapshot!(metric.abc);
12257 },
12258 );
12259 }
12260
12261 #[cfg(feature = "mozjs")]
12262 #[test]
12263 fn mozjs_short_circuit_with_boolean_literal_operand() {
12264 check_metrics::<MozjsParser>(
12265 "function f(a) { return a && true; }\n",
12266 "foo.js",
12267 |metric| {
12268 assert_eq!(metric.abc.conditions_sum(), 2);
12269 insta::assert_json_snapshot!(metric.abc);
12270 },
12271 );
12272 }
12273
12274 // ---------- Perl ABC tests ----------
12275
12276 #[cfg(feature = "perl")]
12277 #[test]
12278 fn perl_empty_unit_zero() {
12279 // Empty source produces zero ABC magnitude — pins the trait
12280 // wiring without exercising any compute branch.
12281 check_metrics::<PerlParser>("", "empty.pl", |metric| {
12282 assert_eq!(metric.abc.assignments_sum(), 0);
12283 assert_eq!(metric.abc.branches_sum(), 0);
12284 assert_eq!(metric.abc.conditions_sum(), 0);
12285 insta::assert_json_snapshot!(metric.abc);
12286 });
12287 }
12288
12289 #[cfg(feature = "perl")]
12290 #[test]
12291 fn perl_plain_and_compound_assignments_count() {
12292 // `my $x = 0` parses as a `binary_expression` with an `=`
12293 // token, so the initialiser counts (Perl has no equivalent of
12294 // the JS `const` initialiser-suppression rule). Each
12295 // assignment operator token contributes one assignment:
12296 // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
12297 // from the `my $x = 0` initialiser and the later `$x = 5`
12298 // reassignment.
12299 check_metrics::<PerlParser>(
12300 "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
12301 "foo.pl",
12302 |metric| {
12303 assert_eq!(metric.abc.assignments_sum(), 5);
12304 assert_eq!(metric.abc.branches_sum(), 0);
12305 assert_eq!(metric.abc.conditions_sum(), 0);
12306 insta::assert_json_snapshot!(metric.abc);
12307 },
12308 );
12309 }
12310
12311 #[cfg(feature = "perl")]
12312 #[test]
12313 fn perl_calls_are_branches() {
12314 // `foo()` parses as `call_expression_with_args_with_brackets`
12315 // wrapping an inner `call_expression_with_bareword(foo)`;
12316 // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
12317 // appears as a standalone bareword. The bareword-inside-
12318 // wrapper case must NOT double-count — only the outer wrapper
12319 // contributes a branch. So B = 3 (foo, bar, shift), not 5.
12320 check_metrics::<PerlParser>(
12321 "sub f { foo(); bar 1, 2; my $a = shift; }",
12322 "foo.pl",
12323 |metric| {
12324 // shift's `my $a = shift` initialiser contributes one
12325 // assignment via the `=` token.
12326 assert_eq!(metric.abc.assignments_sum(), 1);
12327 assert_eq!(metric.abc.branches_sum(), 3);
12328 assert_eq!(metric.abc.conditions_sum(), 0);
12329 insta::assert_json_snapshot!(metric.abc);
12330 },
12331 );
12332 }
12333
12334 #[cfg(feature = "perl")]
12335 #[test]
12336 fn perl_method_invocation_counts_as_branch() {
12337 // `$obj->method(...)` parses as `method_invocation`. Any
12338 // arrow-dispatch counts as one branch regardless of how the
12339 // arguments are passed.
12340 check_metrics::<PerlParser>(
12341 "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
12342 "foo.pl",
12343 |metric| {
12344 // `my $obj = shift` → A=1, B=1 (shift bareword).
12345 // `$obj->run($x)` and `$obj->ping` → 2 more branches.
12346 assert_eq!(metric.abc.assignments_sum(), 1);
12347 assert_eq!(metric.abc.branches_sum(), 3);
12348 assert_eq!(metric.abc.conditions_sum(), 0);
12349 insta::assert_json_snapshot!(metric.abc);
12350 },
12351 );
12352 }
12353
12354 // #1297's own sweep cleared Perl, and it was wrong. `<STDIN>` does
12355 // lex as a single `standard_input` token and scores 0, which is
12356 // what the issue measured — but the filehandle and lexical-handle
12357 // readlines do not: `<FH>` is `standard_input_to_identifier` and
12358 // `<$fh>` is `standard_input_to_variable`, each a plain three-token
12359 // sequence whose brackets are the same bare `<` / `>` a comparison
12360 // uses, and the arm was ungated. Each scored two phantom
12361 // conditions.
12362 //
12363 // All three readline spellings against two genuine comparisons, so
12364 // every mis-aim lands on its own number: 6 pre-fix, 2 once the arm
12365 // allows `BinaryExpression`, 0 if it allows the wrong parent or the
12366 // fixture stops parsing. The assertion is also the grammar-dispatch
12367 // §8 pin — `cyclomatic()` is 3 on this space, so decisions is 2 and
12368 // the two counts agree exactly.
12369 #[cfg(feature = "perl")]
12370 #[test]
12371 fn perl_readline_angle_brackets_are_not_conditions() {
12372 check_func_space::<PerlParser, _>(
12373 "sub g {\n\
12374 my ($a, $b, $fh) = @_;\n\
12375 my $l = <FH>;\n\
12376 my $m = <$fh>;\n\
12377 my $n = <STDIN>;\n\
12378 if ($a < $b) { return 1; }\n\
12379 if ($a > $b) { return 2; }\n\
12380 return 0;\n\
12381 }",
12382 "foo.pl",
12383 |space| {
12384 assert_deepest_conditions_match_cyclomatic(&space, 2);
12385 // Fixture-presence anchor: the three readlines are
12386 // three of this sub's four assignments (the fourth is
12387 // the `my (...) = @_` unpack), so dropping them from
12388 // the fixture fails here rather than leaving a test
12389 // that still reports 2 conditions and proves nothing.
12390 assert_eq!(space.metrics.abc.assignments_sum(), 4);
12391 },
12392 );
12393 }
12394
12395 #[cfg(feature = "perl")]
12396 #[test]
12397 fn perl_numeric_and_string_comparisons_count_conditions() {
12398 // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
12399 // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
12400 // fire once per token. The sample below uses one of each →
12401 // C = 14. No assignments, no branches.
12402 check_metrics::<PerlParser>(
12403 "sub f {\n\
12404 my $r;\n\
12405 $r = $a == $b;\n\
12406 $r = $a != $b;\n\
12407 $r = $a < $b;\n\
12408 $r = $a > $b;\n\
12409 $r = $a <= $b;\n\
12410 $r = $a >= $b;\n\
12411 $r = $a <=> $b;\n\
12412 $r = $a eq $b;\n\
12413 $r = $a ne $b;\n\
12414 $r = $a lt $b;\n\
12415 $r = $a gt $b;\n\
12416 $r = $a le $b;\n\
12417 $r = $a ge $b;\n\
12418 $r = $a cmp $b;\n\
12419 }",
12420 "foo.pl",
12421 |metric| {
12422 // 15 `=` tokens: one declaration `my $r` (no `=`),
12423 // then 14 `$r = …` plus there's no `=` in `my $r;`.
12424 // Actually: `my $r;` has no `=`; the 14 `$r = …` are
12425 // 14 `=` tokens. So A=14, C=14.
12426 assert_eq!(metric.abc.assignments_sum(), 14);
12427 assert_eq!(metric.abc.branches_sum(), 0);
12428 assert_eq!(metric.abc.conditions_sum(), 14);
12429 insta::assert_json_snapshot!(metric.abc);
12430 },
12431 );
12432 }
12433
12434 #[cfg(feature = "perl")]
12435 #[test]
12436 fn perl_short_circuit_not_counted_directly_ternary_counts() {
12437 // `&&`, `||`, `//`, low-precedence `and`, `or`, `xor` are
12438 // NOT counted as conditions on their own (Fitzpatrick Rule
12439 // 5; #395) — instead each operand is counted as a unary
12440 // conditional by the walker (Rule 9; #403). At the pinned
12441 // tree-sitter-perl grammar version, only the four
12442 // punctuation forms plus one keyword form parse under a
12443 // `binary_expression` parent that triggers the walker; the
12444 // other two keyword forms parse under a distinct grammar
12445 // node and contribute zero. Net: 4 walker-firing lines × 2
12446 // scalar-variable operands + 1 ternary node + 1 for the
12447 // ternary's bare `$a` condition operand (#1102) = 10. The
12448 // exact mix of "which two keyword forms are silent" is
12449 // grammar-version-dependent; a future grammar bump that
12450 // normalises the keyword forms' parent kind will shift this
12451 // count to 14. See follow-up note above the test name.
12452 check_metrics::<PerlParser>(
12453 "sub f {\n\
12454 my $r;\n\
12455 $r = $a && $b;\n\
12456 $r = $a || $b;\n\
12457 $r = $a // $b;\n\
12458 $r = $a and $b;\n\
12459 $r = $a or $b;\n\
12460 $r = $a xor $b;\n\
12461 $r = $a ? 1 : 2;\n\
12462 }",
12463 "foo.pl",
12464 |metric| {
12465 // 7 `=` tokens (one per reassignment line).
12466 assert_eq!(metric.abc.assignments_sum(), 7);
12467 assert_eq!(metric.abc.branches_sum(), 0);
12468 // 4 walker-triggered lines × 2 operands + 1 ternary
12469 // node + 1 for its bare `$a` condition operand = 10.
12470 // The two remaining low-precedence keyword forms (one
12471 // of `and`/`or`/`xor`) fall under a
12472 // non-binary_expression parent in this grammar
12473 // version and contribute zero via the walker.
12474 assert_eq!(metric.abc.conditions_sum(), 10);
12475 insta::assert_json_snapshot!(metric.abc);
12476 },
12477 );
12478 }
12479
12480 // Issue #1102, Perl half. See
12481 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
12482 // rule. Like PHP, Perl's ABC dispatcher has no `?`-token arm — the
12483 // grammar does emit the token, but the `ternary_expression` node is
12484 // what carries the tally's +1. tree-sitter-perl names the branch
12485 // fields `true` / `false` rather than the C-family `consequence` /
12486 // `alternative`, so a copied C-family gate would match nothing.
12487 #[cfg(feature = "perl")]
12488 #[test]
12489 fn perl_ternary_operand_slots_count_as_unary_conditions() {
12490 // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
12491 check_metrics::<PerlParser>("sub f { my $x = $a ? !$b : !$c; }", "foo.pl", |metric| {
12492 assert_eq!(metric.abc.conditions_sum(), 4);
12493 });
12494 // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
12495 // the fix.
12496 check_metrics::<PerlParser>(
12497 "sub f { my $x = ($a > 0) ? $b : -$b; }",
12498 "foo.pl",
12499 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
12500 );
12501 // A negated *condition* takes the walker's `else` fallback —
12502 // `!$a` is neither a boolean terminal nor a paren wrapper, so
12503 // only `perl_inspect_container` can classify it. Delete the
12504 // fallback and this reads 1. ternary (1) + `!$a` (1) = 2.
12505 check_metrics::<PerlParser>("sub f { my $x = !$a ? $b : $c; }", "foo.pl", |metric| {
12506 assert_eq!(metric.abc.conditions_sum(), 2);
12507 });
12508 // Nested: two ternary nodes plus the two bare-variable
12509 // conditions = 4.
12510 check_metrics::<PerlParser>(
12511 "sub f { my $x = $a ? ($b ? $c : $d) : $e; }",
12512 "foo.pl",
12513 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
12514 );
12515 }
12516
12517 #[cfg(feature = "perl")]
12518 #[test]
12519 fn perl_elsif_and_else_count_conditions() {
12520 // `if (… == …) { … } elsif (… < …) { … } else { … }` →
12521 // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
12522 // `else_clause` each + 1 → C = 4. Branches: 0 (only
12523 // assignments). Assignments: just the `=` initialisers /
12524 // reassignments — there are 4 here (`$x` init plus three
12525 // `$x = …` reassigns).
12526 check_metrics::<PerlParser>(
12527 "sub f {\n\
12528 my $x = 0;\n\
12529 if ($a == $b) {\n\
12530 $x = 1;\n\
12531 } elsif ($a < $b) {\n\
12532 $x = 2;\n\
12533 } else {\n\
12534 $x = 3;\n\
12535 }\n\
12536 }",
12537 "foo.pl",
12538 |metric| {
12539 assert_eq!(metric.abc.assignments_sum(), 4);
12540 assert_eq!(metric.abc.branches_sum(), 0);
12541 assert_eq!(metric.abc.conditions_sum(), 4);
12542 insta::assert_json_snapshot!(metric.abc);
12543 },
12544 );
12545 }
12546
12547 #[cfg(feature = "perl")]
12548 #[test]
12549 fn perl_regex_match_operators_count_conditions() {
12550 // `=~` and `!~` are pattern-match operators; we count both
12551 // as conditions because they evaluate the regex match in a
12552 // boolean context.
12553 check_metrics::<PerlParser>(
12554 "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
12555 "foo.pl",
12556 |metric| {
12557 // 3 `=` tokens, 0 branches except `shift` bareword.
12558 assert_eq!(metric.abc.assignments_sum(), 3);
12559 assert_eq!(metric.abc.branches_sum(), 1);
12560 assert_eq!(metric.abc.conditions_sum(), 2);
12561 insta::assert_json_snapshot!(metric.abc);
12562 },
12563 );
12564 }
12565
12566 #[cfg(feature = "perl")]
12567 #[test]
12568 fn perl_complex_function_abc() {
12569 // Mixed program exercising every category. Computed
12570 // expected:
12571 // Assignments: `my $i = 0` (1), `$i++` is a unary
12572 // increment — Perl's grammar emits `PLUSPLUS` not an `=`
12573 // operator, so it does NOT count under the operator-
12574 // token rule. The for-loop's `$i++` is similarly
12575 // uncounted.
12576 // Total A: 1 from `my $i = 0`, 1 from `$total += $i`
12577 // (the `+=` token) → A = 2.
12578 // Branches: `do_work($i)` → 1; `print "done\n"` is a
12579 // call_expression_with_spaced_args → 1; `return $total`
12580 // uses the `return` keyword not a call → 0. B = 2.
12581 // Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
12582 // 1; `else_clause` → 1. C = 3.
12583 check_metrics::<PerlParser>(
12584 "sub run {\n\
12585 my $total = 0;\n\
12586 for (my $i = 0; $i < 10; $i++) {\n\
12587 if ($i % 2 == 0) {\n\
12588 do_work($i);\n\
12589 } else {\n\
12590 $total += $i;\n\
12591 }\n\
12592 }\n\
12593 print \"done\\n\";\n\
12594 return $total;\n\
12595 }",
12596 "foo.pl",
12597 |metric| {
12598 // `my $total = 0` is one `=`; `my $i = 0` is another
12599 // `=`; `$total += $i` is one `+=`. Total = 3.
12600 assert_eq!(metric.abc.assignments_sum(), 3);
12601 assert_eq!(metric.abc.branches_sum(), 2);
12602 assert_eq!(metric.abc.conditions_sum(), 3);
12603 insta::assert_json_snapshot!(metric.abc);
12604 },
12605 );
12606 }
12607
12608 #[cfg(feature = "perl")]
12609 #[test]
12610 fn perl_if_multiple_conditions() {
12611 // Fitzpatrick Rule 9 walker (issue #403): each operand of a
12612 // `&&` / `||` / `//` / `and` / `or` / `xor` chain is one
12613 // condition. ScalarVariable operands ($a, $b, …) qualify as
12614 // terminal-bool kinds for the walker.
12615 check_metrics::<PerlParser>(
12616 "sub f {\n\
12617 my ($a, $b, $c, $d) = @_;\n\
12618 if ($a || $b || $c || $d) { return 1; } # +4c\n\
12619 if ($a && $b && $c) { return 2; } # +3c\n\
12620 if (!$a && !$b) { return 3; } # +2c\n\
12621 return 0;\n\
12622 }",
12623 "foo.pl",
12624 |metric| {
12625 assert_eq!(metric.abc.conditions_sum(), 9);
12626 insta::assert_json_snapshot!(metric.abc);
12627 },
12628 );
12629 }
12630
12631 #[cfg(feature = "perl")]
12632 #[test]
12633 fn perl_while_and_until_conditions() {
12634 // Perl has no `do { ... } while(cond);` shape in this grammar
12635 // — `while` and `until` are the loop forms with a condition
12636 // slot. The walker fires on each `&&` / `||` token inside
12637 // those headers.
12638 check_metrics::<PerlParser>(
12639 "sub f {\n\
12640 my ($a, $b) = @_;\n\
12641 while ($a || $b) { last; } # +2c\n\
12642 until ($a && !$b) { last; } # +2c\n\
12643 }",
12644 "foo.pl",
12645 |metric| {
12646 assert_eq!(metric.abc.conditions_sum(), 4);
12647 insta::assert_json_snapshot!(metric.abc);
12648 },
12649 );
12650 }
12651
12652 #[cfg(feature = "perl")]
12653 #[test]
12654 fn perl_for_header_condition_slot_counts_unary_conditions() {
12655 // The Perl half of #1276. The C-style `for` header's condition
12656 // slot is a Rule 9 unary condition like the `if` slot: a bare
12657 // `$ok`, a negated `!$ok` and a parenthesised `($ok)` each count
12658 // one, an empty header and a `foreach` count zero, and a
12659 // comparison-shaped `$i < $n` stays at the one the `<` arm
12660 // already counts. Every C-style row is the three-clause spelling
12661 // because tree-sitter-perl does not parse an empty initializer.
12662 let cases = [
12663 ("for (my $i = 0; $ok; $i++) { }", 1),
12664 ("for (my $i = 0; !$ok; $i++) { }", 1),
12665 ("for (my $i = 0; ($ok); $i++) { }", 1),
12666 ("for (my $i = 0; $i < $n; $i++) { }", 1),
12667 ("for (my $i = 0; $ok && $j; $i++) { }", 2),
12668 ("for (;;) { last; }", 0),
12669 ("for my $x (@l) { }", 0),
12670 ];
12671 let mut ran = 0;
12672 for (body, expected) in cases {
12673 let src = format!("sub f {{ {body} }}\n");
12674 assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
12675 ran += 1;
12676 }
12677 assert_eq!(ran, cases.len());
12678 assert!(cases.iter().any(|&(_, n)| n == 0));
12679 assert!(cases.iter().any(|&(_, n)| n == 2));
12680
12681 // The slot agrees with the `if` slot for every shape, which is
12682 // the property the fix restores; the reference is non-degenerate
12683 // by the table above.
12684 for condition in ["$ok", "!$ok", "($ok)", "$ok && $j"] {
12685 let in_for = format!("sub f {{ for (my $i = 0; {condition}; $i++) {{ }} }}\n");
12686 let in_if = format!("sub f {{ if ({condition}) {{ }} }}\n");
12687 assert_eq!(
12688 abc_conditions(LANG::Perl, &in_for),
12689 abc_conditions(LANG::Perl, &in_if),
12690 "`{condition}` must score alike in a `for` header and an `if`",
12691 );
12692 }
12693 }
12694
12695 #[cfg(feature = "perl")]
12696 #[test]
12697 fn perl_short_circuit_counts_scalar_variable_operands() {
12698 // `$a && $b` reports 2 conditions — one walker count per
12699 // `ScalarVariable` operand. Renamed from the cross-language
12700 // `_with_boolean_literal_operand` convention because Perl has
12701 // no readily-grammar-exposed boolean literal in an `&&`
12702 // operand slot at the pinned grammar version (the `Boolean`
12703 // kind only fires on the `boolean` pragma's named constants,
12704 // not bareword `1` / `0`). Two scalar variables are the
12705 // grammar-stable terminal-set witness for Perl.
12706 check_metrics::<PerlParser>(
12707 "sub f { my ($a) = @_; return $a && $b; }\n",
12708 "foo.pl",
12709 |metric| {
12710 assert_eq!(metric.abc.conditions_sum(), 2);
12711 insta::assert_json_snapshot!(metric.abc);
12712 },
12713 );
12714 }
12715
12716 #[cfg(feature = "perl")]
12717 #[test]
12718 fn perl_array_in_binary_operand_descends_to_scalar_context_value() {
12719 // Regression test for the code-review findings on the
12720 // Phase-2B Perl walker:
12721 // - Pre-fix-A: `perl_inspect_container` descended `Array`
12722 // via `node.child(1)` — the FIRST element — wrongly
12723 // attributing `$x` for `($x, $y)` (semantically `$y`
12724 // is the scalar-context value).
12725 // - Fix-A (the `array_is_paren` guard, 5db8078): dropped
12726 // Array-as-paren entirely in `BinaryExpression` operand
12727 // contexts to avoid the wrong attribution — but
12728 // regressed `$a || ($x)` (single paren-grouped operand)
12729 // to C=1 instead of 2.
12730 // - Fix-B (this change): keeps Array-as-paren unconditional
12731 // but descends via the LAST named child. `$a || ($x)`
12732 // reaches `$x` (count both operands → 2);
12733 // `$a || ($x, $y)` reaches `$y` (count `$a` + `$y` →
12734 // still 2, matching Fitzpatrick Rule 7 "one per
12735 // operand"); `if ($a)` still reaches `$a` (single-
12736 // element grouping → 1).
12737 check_metrics::<PerlParser>(
12738 "sub f { my ($a, $x, $y) = @_;\n\
12739 \x20 my $r = $a || ($x, $y); # +2c: $a + last-named $y\n\
12740 \x20 my $s = $a || ($x); # +2c: $a + only-named $x\n\
12741 \x20 $r + $s;\n\
12742 }\n",
12743 "foo.pl",
12744 |metric| {
12745 // 2 + 2 = 4 unary conditions from the two `||`s.
12746 assert_eq!(metric.abc.conditions_sum(), 4);
12747 insta::assert_json_snapshot!(metric.abc);
12748 },
12749 );
12750 }
12751
12752 #[cfg(feature = "perl")]
12753 #[test]
12754 fn perl_if_scalar_variable_condition() {
12755 // Renamed from the cross-language
12756 // `_if_boolean_literal_condition` convention because
12757 // Perl has no readily-grammar-exposed boolean literal in
12758 // an `if (cond)` slot at the pinned grammar version:
12759 // tree-sitter-perl's `Boolean` kind only fires for the
12760 // `boolean` pragma's named constants (not bareword `1` /
12761 // `0`, which surface as `Integer` / not in the
12762 // terminal-bool set). A scalar-variable condition is the
12763 // grammar-stable witness — `if ($a)` reaches
12764 // `scalar_variable` via the `Array` paren unwrap.
12765 check_metrics::<PerlParser>(
12766 "sub f { my ($a) = @_; if ($a) { return 1; } }\n",
12767 "foo.pl",
12768 |metric| {
12769 assert_eq!(metric.abc.conditions_sum(), 1);
12770 insta::assert_json_snapshot!(metric.abc);
12771 },
12772 );
12773 }
12774
12775 #[cfg(feature = "perl")]
12776 #[test]
12777 fn perl_methods_arguments_with_conditions() {
12778 // `call(!$a, !$b)` — argument list walker counts each
12779 // unary-conditional argument once. Cannot use `m(...)` as
12780 // the function name — tree-sitter-perl parses `m(...)` as
12781 // the regex-match operator, not a function call.
12782 check_metrics::<PerlParser>(
12783 "sub f { my ($a, $b) = @_; call($a, $b); call(!$a, !$b); }\n",
12784 "foo.pl",
12785 |metric| {
12786 // Two calls × 1 branch each = 2 branches.
12787 // `call(!$a, !$b)` contributes 2 walker conditions
12788 // (one per `!`-wrapped scalar-variable argument);
12789 // `call($a, $b)` contributes 0 (bare-args don't
12790 // count via the Arguments walker — list_kind !=
12791 // BinaryExpression).
12792 assert_eq!(metric.abc.branches_sum(), 2);
12793 assert_eq!(metric.abc.conditions_sum(), 2);
12794 insta::assert_json_snapshot!(metric.abc);
12795 },
12796 );
12797 }
12798
12799 #[cfg(feature = "perl")]
12800 #[test]
12801 fn perl_return_with_conditions() {
12802 // `return !$a` reports 1 condition via the walker (unary
12803 // unwrap to scalar-variable terminal). `return $a` reports
12804 // 0 (no paren / unary wrap, has_boolean_content stays
12805 // false from ReturnExpression parent).
12806 check_metrics::<PerlParser>(
12807 "sub m1 { my ($z) = @_; return !($z); }\n\
12808 sub m2 { my ($x) = @_; return (((!$x))); }\n\
12809 sub m3 { my ($x, $y) = @_; return $x && $y; }\n",
12810 "foo.pl",
12811 |metric| {
12812 // m1: !($z) → walker on `!` unwraps paren to $z (1).
12813 // m2: (((!$x))) → walker unwraps three parens + one
12814 // unary to $x (1).
12815 // m3: $x && $y → walker on `&&` counts both (2).
12816 // Sum: 4.
12817 assert_eq!(metric.abc.conditions_sum(), 4);
12818 insta::assert_json_snapshot!(metric.abc);
12819 },
12820 );
12821 }
12822
12823 // ---------- Lua ABC tests ----------
12824
12825 #[cfg(feature = "lua")]
12826 #[test]
12827 fn lua_empty_unit_zero() {
12828 check_metrics::<LuaParser>("", "empty.lua", |metric| {
12829 assert_eq!(metric.abc.assignments_sum(), 0);
12830 assert_eq!(metric.abc.branches_sum(), 0);
12831 assert_eq!(metric.abc.conditions_sum(), 0);
12832 insta::assert_json_snapshot!(metric.abc);
12833 });
12834 }
12835
12836 #[cfg(feature = "lua")]
12837 #[test]
12838 fn lua_assignments_count_locals_and_plain() {
12839 // `local x = 0` wraps an `assignment_statement` under a
12840 // `variable_declaration`; the inner wrapper still counts.
12841 // Multi-target assignment `a, b = 1, 2` is a single
12842 // `assignment_statement` and contributes 1, NOT 2 — the
12843 // wrapper is the unit of counting (matches the Python rule:
12844 // one `Assignment` node, one assignment).
12845 check_metrics::<LuaParser>(
12846 "function f()\n\
12847 local x = 0\n\
12848 x = 1\n\
12849 local a, b = 1, 2\n\
12850 a, b = b, a\n\
12851 end",
12852 "foo.lua",
12853 |metric| {
12854 assert_eq!(metric.abc.assignments_sum(), 4);
12855 assert_eq!(metric.abc.branches_sum(), 0);
12856 assert_eq!(metric.abc.conditions_sum(), 0);
12857 insta::assert_json_snapshot!(metric.abc);
12858 },
12859 );
12860 }
12861
12862 #[cfg(feature = "lua")]
12863 #[test]
12864 fn lua_calls_are_branches() {
12865 // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
12866 // call form is a `function_call` node. The nested
12867 // `f(g(1))` counts as 2 branches (one per dispatch).
12868 check_metrics::<LuaParser>(
12869 "function r(x)\n\
12870 print(x)\n\
12871 obj.m(x)\n\
12872 obj:m(x)\n\
12873 return f(g(1))\n\
12874 end",
12875 "foo.lua",
12876 |metric| {
12877 assert_eq!(metric.abc.assignments_sum(), 0);
12878 assert_eq!(metric.abc.branches_sum(), 5);
12879 assert_eq!(metric.abc.conditions_sum(), 0);
12880 insta::assert_json_snapshot!(metric.abc);
12881 },
12882 );
12883 }
12884
12885 #[cfg(feature = "lua")]
12886 #[test]
12887 fn lua_comparisons_count_logical_ops_do_not() {
12888 // Each comparison token contributes one condition; `and` /
12889 // `or` are NOT counted on their own (Fitzpatrick Rule 5;
12890 // #395) — instead each operand is counted as a unary
12891 // conditional by the walker (Rule 9; #403). The two
12892 // `a and b` / `a or b` lines add 2 walker conditions each.
12893 check_metrics::<LuaParser>(
12894 "function f(a, b)\n\
12895 local r\n\
12896 r = a == b\n\
12897 r = a ~= b\n\
12898 r = a < b\n\
12899 r = a > b\n\
12900 r = a <= b\n\
12901 r = a >= b\n\
12902 r = a and b\n\
12903 r = a or b\n\
12904 end",
12905 "foo.lua",
12906 |metric| {
12907 // 8 `r = …` reassignments, plus `local r` (no `=`).
12908 assert_eq!(metric.abc.assignments_sum(), 8);
12909 assert_eq!(metric.abc.branches_sum(), 0);
12910 // 6 comparisons (+6) + 2 logical lines × 2 walker
12911 // operands (+4) = 10.
12912 assert_eq!(metric.abc.conditions_sum(), 10);
12913 insta::assert_json_snapshot!(metric.abc);
12914 },
12915 );
12916 }
12917
12918 // #1297: Lua 5.4 brackets a variable attribute with the same two
12919 // bare tokens a comparison uses, and the `<` / `>` arm had no gate,
12920 // so `local x <const> = 1` scored two conditions. Two attributes
12921 // against one genuine `a < b` separate every mis-aim: 5 pre-fix, 1
12922 // once the arm allows `BinaryExpression`, 0 if it allows the wrong
12923 // parent or the fixture stops parsing.
12924 //
12925 // No fixture-presence anchor is available, unlike the JSX and Perl
12926 // fixtures: once excluded, a Lua attribute contributes to no ABC
12927 // axis at all (`local x <const> = 1` and `local x = 1` score
12928 // identically on assignments, branches and conditions), and it
12929 // opens no space. Deleting the two attributes would leave this test
12930 // passing on the `a < b` alone. The coverage that the attributes
12931 // are reaching the arm is the revert test: removing the gate makes
12932 // this the only failing test in the suite.
12933 #[cfg(feature = "lua")]
12934 #[test]
12935 fn lua_variable_attributes_are_not_conditions() {
12936 check_metrics::<LuaParser>(
12937 "local function f(a, b)\n\
12938 local x <const> = 1\n\
12939 local y <close> = nil\n\
12940 if a < b then return x end\n\
12941 return y\n\
12942 end",
12943 "foo.lua",
12944 |metric| {
12945 assert_eq!(metric.abc.conditions_sum(), 1);
12946 },
12947 );
12948 }
12949
12950 #[cfg(feature = "lua")]
12951 #[test]
12952 fn lua_elseif_and_else_count_conditions() {
12953 // Each elseif / else arm of the if contributes one
12954 // condition, mirroring the Python rule.
12955 check_metrics::<LuaParser>(
12956 "function f(x)\n\
12957 if x > 0 then\n\
12958 return 1\n\
12959 elseif x < 0 then\n\
12960 return -1\n\
12961 else\n\
12962 return 0\n\
12963 end\n\
12964 end",
12965 "foo.lua",
12966 |metric| {
12967 // Comparisons: `>`, `<` → 2; elseif_statement → 1;
12968 // else_statement → 1. C = 4. No branches (no calls).
12969 assert_eq!(metric.abc.assignments_sum(), 0);
12970 assert_eq!(metric.abc.branches_sum(), 0);
12971 assert_eq!(metric.abc.conditions_sum(), 4);
12972 insta::assert_json_snapshot!(metric.abc);
12973 },
12974 );
12975 }
12976
12977 #[cfg(feature = "lua")]
12978 #[test]
12979 fn lua_complex_function_abc() {
12980 // Combines every category to pin the metric.
12981 check_metrics::<LuaParser>(
12982 "function run(n)\n\
12983 local total = 0\n\
12984 for i = 1, n do\n\
12985 if i % 2 == 0 then\n\
12986 do_work(i)\n\
12987 else\n\
12988 total = total + i\n\
12989 end\n\
12990 end\n\
12991 print(\"done\")\n\
12992 return total\n\
12993 end",
12994 "foo.lua",
12995 |metric| {
12996 // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
12997 // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
12998 // Conditions: `==` (1), `else_statement` (1) → 2.
12999 assert_eq!(metric.abc.assignments_sum(), 2);
13000 assert_eq!(metric.abc.branches_sum(), 2);
13001 assert_eq!(metric.abc.conditions_sum(), 2);
13002 insta::assert_json_snapshot!(metric.abc);
13003 },
13004 );
13005 }
13006
13007 #[cfg(feature = "lua")]
13008 #[test]
13009 fn lua_if_multiple_conditions() {
13010 // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or`
13011 // are keyword tokens inside a `binary_expression`.
13012 check_metrics::<LuaParser>(
13013 "function f(a, b, c, d)\n\
13014 if a or b or c or d then return 1 end -- +4c\n\
13015 if a and b and c then return 2 end -- +3c\n\
13016 if not a and not b then return 3 end -- +2c\n\
13017 return 0\n\
13018 end",
13019 "foo.lua",
13020 |metric| {
13021 assert_eq!(metric.abc.conditions_sum(), 9);
13022 insta::assert_json_snapshot!(metric.abc);
13023 },
13024 );
13025 }
13026
13027 #[cfg(feature = "lua")]
13028 #[test]
13029 fn lua_while_conditions() {
13030 // Lua has no `do { ... } while(cond);` — `while cond do …
13031 // end` and `repeat … until cond` are the loop forms.
13032 check_metrics::<LuaParser>(
13033 "function f(a, b)\n\
13034 while a or b do break end -- +2c\n\
13035 repeat break until a and not b -- +2c\n\
13036 end",
13037 "foo.lua",
13038 |metric| {
13039 assert_eq!(metric.abc.conditions_sum(), 4);
13040 insta::assert_json_snapshot!(metric.abc);
13041 },
13042 );
13043 }
13044
13045 #[cfg(feature = "lua")]
13046 #[test]
13047 fn lua_short_circuit_with_boolean_literal_operand() {
13048 // `a and true` reports 2 conditions: one Identifier, one
13049 // True keyword literal.
13050 check_metrics::<LuaParser>("function f(a) return a and true end", "foo.lua", |metric| {
13051 assert_eq!(metric.abc.conditions_sum(), 2);
13052 insta::assert_json_snapshot!(metric.abc);
13053 });
13054 }
13055
13056 #[cfg(feature = "lua")]
13057 #[test]
13058 fn lua_number_truthy_condition_counts() {
13059 // Regression for findings.md #2: Lua treats every non-nil,
13060 // non-false value as truthy, so `if 1 then ... end` and
13061 // `return a and 2` should each count their numeric literal
13062 // as a Fitzpatrick Rule 6 / 7 unary condition. Pre-fix,
13063 // `lua_bool_terminal_kinds!()` listed `True` / `False` /
13064 // `Nil` but omitted `Number`, so the walker dropped every
13065 // numeric-truthy operand. The walker comment at the top of
13066 // `lua_inspect_container` already promised numbers were
13067 // terminal-bool kinds; this commit closes the gap.
13068 check_metrics::<LuaParser>(
13069 "function f(a)\n\
13070 \x20 if 1 then return 1 end\n\
13071 \x20 return a and 2\n\
13072 end",
13073 "foo.lua",
13074 |metric| {
13075 // `if 1 then` → walker counts the Number literal (+1).
13076 // `a and 2` → `and` walker counts both operands:
13077 // identifier `a` (+1), Number `2` (+1).
13078 // Total: 3.
13079 assert_eq!(metric.abc.conditions_sum(), 3);
13080 insta::assert_json_snapshot!(metric.abc);
13081 },
13082 );
13083 }
13084
13085 #[cfg(feature = "lua")]
13086 #[test]
13087 fn lua_if_boolean_literal_condition() {
13088 check_metrics::<LuaParser>(
13089 "function f()\n\
13090 if true then end -- +1c\n\
13091 if not false then end -- +1c\n\
13092 while true do break end -- +1c\n\
13093 repeat break until false -- +1c\n\
13094 end",
13095 "foo.lua",
13096 |metric| {
13097 assert_eq!(metric.abc.conditions_sum(), 4);
13098 insta::assert_json_snapshot!(metric.abc);
13099 },
13100 );
13101 }
13102
13103 #[cfg(feature = "lua")]
13104 #[test]
13105 fn lua_methods_arguments_with_conditions() {
13106 // `m(not a, not b)` — argument list walker counts each
13107 // unary-conditional argument once. Bare-identifier args
13108 // (`m(a, b)`) do not count (list_kind != BinaryExpression).
13109 check_metrics::<LuaParser>(
13110 "function f(a, b) m(a, b); m(not a, not b) end",
13111 "foo.lua",
13112 |metric| {
13113 assert_eq!(metric.abc.branches_sum(), 2);
13114 assert_eq!(metric.abc.conditions_sum(), 2);
13115 insta::assert_json_snapshot!(metric.abc);
13116 },
13117 );
13118 }
13119
13120 #[cfg(feature = "lua")]
13121 #[test]
13122 fn lua_return_with_conditions() {
13123 // `return not (z >= 0)` → walker on `not` unwraps the paren
13124 // chain and reaches the inner BinaryExpression; the inner
13125 // `>=` comparison is the actual Fitzpatrick condition.
13126 check_metrics::<LuaParser>(
13127 "function m1(z) return not (z >= 0) end\n\
13128 function m2(x) return (((not x))) end\n\
13129 function m3(x, y) return x and y end",
13130 "foo.lua",
13131 |metric| {
13132 // m1: `>=` (1). `not` wraps a paren'd
13133 // BinaryExpression — Lua's lua_inspect_container
13134 // reaches the inner BinaryExpression and stops,
13135 // no walker count. +1.
13136 // m2: ReturnStatement → iterate expression_list →
13137 // inspect_container on the outermost paren →
13138 // unwraps to `x` in has_boolean_content-true
13139 // (seeded by the `not`). +1.
13140 // m3: x and y → `and` walker counts both → +2.
13141 // Sum: 4.
13142 assert_eq!(metric.abc.conditions_sum(), 4);
13143 insta::assert_json_snapshot!(metric.abc);
13144 },
13145 );
13146 }
13147
13148 // ---------- Tcl ABC tests ----------
13149
13150 #[cfg(feature = "tcl")]
13151 #[test]
13152 fn tcl_empty_unit_zero() {
13153 check_metrics::<TclParser>("", "empty.tcl", |metric| {
13154 assert_eq!(metric.abc.assignments_sum(), 0);
13155 assert_eq!(metric.abc.branches_sum(), 0);
13156 assert_eq!(metric.abc.conditions_sum(), 0);
13157 insta::assert_json_snapshot!(metric.abc);
13158 });
13159 }
13160
13161 #[cfg(feature = "tcl")]
13162 #[test]
13163 fn tcl_set_command_counts_assignment() {
13164 // `set` has its own grammar production; each invocation is
13165 // one assignment.
13166 check_metrics::<TclParser>(
13167 "proc f {} {\n\
13168 set x 1\n\
13169 set y 2\n\
13170 set x [expr {$x + $y}]\n\
13171 }",
13172 "foo.tcl",
13173 |metric| {
13174 // 3 `set` invocations → A=3. The inner `expr` is a
13175 // sub-command (`command_substitution` + `expr_cmd`),
13176 // not a `command` node, so it doesn't add a branch.
13177 assert_eq!(metric.abc.assignments_sum(), 3);
13178 assert_eq!(metric.abc.branches_sum(), 0);
13179 assert_eq!(metric.abc.conditions_sum(), 0);
13180 insta::assert_json_snapshot!(metric.abc);
13181 },
13182 );
13183 }
13184
13185 #[cfg(feature = "tcl")]
13186 #[test]
13187 fn tcl_incr_append_lappend_count_assignment() {
13188 // Variable-mutation commands (`incr`, `append`, `lappend`)
13189 // are recognised by name and count as assignments, not
13190 // branches.
13191 check_metrics::<TclParser>(
13192 "proc f {} {\n\
13193 set x 0\n\
13194 incr x\n\
13195 append s \"hi\"\n\
13196 lappend lst 1\n\
13197 }",
13198 "foo.tcl",
13199 |metric| {
13200 // `set` (1) + `incr` (1) + `append` (1) + `lappend`
13201 // (1) → A=4. No branches, no conditions.
13202 assert_eq!(metric.abc.assignments_sum(), 4);
13203 assert_eq!(metric.abc.branches_sum(), 0);
13204 assert_eq!(metric.abc.conditions_sum(), 0);
13205 insta::assert_json_snapshot!(metric.abc);
13206 },
13207 );
13208 }
13209
13210 #[cfg(feature = "tcl")]
13211 #[test]
13212 fn tcl_qualified_mutator_commands_count_assignment() {
13213 // `::incr` is `incr` through the global namespace. Anchored on
13214 // `branches_sum() == 0` as well as the assignment total: an
13215 // unresolved name falls to the branch arm, so the two columns
13216 // move in opposite directions and no single total can mask the
13217 // regression (#1381 review).
13218 check_metrics::<TclParser>(
13219 "proc f {} {\n\
13220 ::incr x\n\
13221 ::append s \"hi\"\n\
13222 ::lappend lst 1\n\
13223 }",
13224 "foo.tcl",
13225 |metric| {
13226 assert_eq!(metric.abc.assignments_sum(), 3);
13227 assert_eq!(metric.abc.branches_sum(), 0);
13228 assert_eq!(metric.abc.conditions_sum(), 0);
13229 },
13230 );
13231 }
13232
13233 #[cfg(feature = "tcl")]
13234 #[test]
13235 fn tcl_namespaced_mutator_command_stays_a_branch() {
13236 // Control: only the *leading* qualifier names the core command,
13237 // so `ns::incr` is a user proc and keeps the branch column — the
13238 // direction `BRACED_WORD_VALUE_CASES` pins for `ns::eval`.
13239 check_metrics::<TclParser>(
13240 "proc f {} {\n\
13241 ns::incr x\n\
13242 }",
13243 "foo.tcl",
13244 |metric| {
13245 assert_eq!(metric.abc.assignments_sum(), 0);
13246 assert_eq!(metric.abc.branches_sum(), 1);
13247 },
13248 );
13249 }
13250
13251 #[cfg(feature = "tcl")]
13252 #[test]
13253 fn tcl_computed_command_name_is_not_an_assignment() {
13254 // A command whose leading word is computed (`$cmd args`) names no
13255 // builtin the parser can resolve, so it stays a branch. Pins the
13256 // field-addressed, `simple_word`-gated read the classifier shares
13257 // with the Cognitive / Cyclomatic detectors (grammar-dispatch §3):
13258 // the earlier `child(0)` byte-slice addressed the slot by position
13259 // and compared whatever literal text sat there, which agrees with
13260 // the gated read on today's grammar only because no mutator name is
13261 // spelled with a leading `$`. A grammar that moved the name out of
13262 // `child(0)`, or a mutator list that grew a computed-looking entry,
13263 // would diverge — this fixture is where that surfaces.
13264 check_metrics::<TclParser>(
13265 "proc f {cmd x} {\n\
13266 $cmd $x\n\
13267 incr x\n\
13268 }",
13269 "foo.tcl",
13270 |metric| {
13271 // `incr x` is the only assignment; `$cmd $x` is a branch.
13272 assert_eq!(metric.abc.assignments_sum(), 1);
13273 assert_eq!(metric.abc.branches_sum(), 1);
13274 assert_eq!(metric.abc.conditions_sum(), 0);
13275 },
13276 );
13277 }
13278
13279 #[cfg(feature = "tcl")]
13280 #[test]
13281 fn tcl_generic_commands_are_branches() {
13282 // Anything that isn't `set` or a known mutator command
13283 // counts as a branch — including builtins like `puts` and
13284 // `return`.
13285 check_metrics::<TclParser>(
13286 "proc f {} {\n\
13287 puts \"hello\"\n\
13288 do_work 1 2\n\
13289 return 0\n\
13290 }",
13291 "foo.tcl",
13292 |metric| {
13293 // 3 commands, all branches.
13294 assert_eq!(metric.abc.assignments_sum(), 0);
13295 assert_eq!(metric.abc.branches_sum(), 3);
13296 assert_eq!(metric.abc.conditions_sum(), 0);
13297 insta::assert_json_snapshot!(metric.abc);
13298 },
13299 );
13300 }
13301
13302 #[cfg(feature = "tcl")]
13303 #[test]
13304 fn tcl_comparisons_count_logical_ops_do_not() {
13305 // `expr` predicates expose comparison / logical tokens at
13306 // the leaf level. Each comparison token contributes one
13307 // condition; `&&` and `||` are NOT counted on their own
13308 // (Fitzpatrick Rule 5; #395) — instead each operand is
13309 // counted as a unary conditional by the walker (Rule 9;
13310 // #403). The two logical lines add 2 walker conditions
13311 // each (variable-substitution operands).
13312 check_metrics::<TclParser>(
13313 "proc f {a b} {\n\
13314 set r [expr {$a == $b}]\n\
13315 set r [expr {$a != $b}]\n\
13316 set r [expr {$a < $b}]\n\
13317 set r [expr {$a > $b}]\n\
13318 set r [expr {$a <= $b}]\n\
13319 set r [expr {$a >= $b}]\n\
13320 set r [expr {$a eq $b}]\n\
13321 set r [expr {$a ne $b}]\n\
13322 set r [expr {$a && $b}]\n\
13323 set r [expr {$a || $b}]\n\
13324 }",
13325 "foo.tcl",
13326 |metric| {
13327 // 10 `set` assignments.
13328 assert_eq!(metric.abc.assignments_sum(), 10);
13329 assert_eq!(metric.abc.branches_sum(), 0);
13330 // 8 comparisons (+8) + 2 logical lines × 2 walker
13331 // operands (+4) = 12.
13332 assert_eq!(metric.abc.conditions_sum(), 12);
13333 insta::assert_json_snapshot!(metric.abc);
13334 },
13335 );
13336 }
13337
13338 #[cfg(feature = "tcl")]
13339 #[test]
13340 fn tcl_ternary_counts_condition() {
13341 // The `ternary_expr` node is one condition and its condition
13342 // slot `$a` — a bare truthy test — is another, matching C++'s
13343 // `int r = a ? b : c;` (also 2). The two branch operands are
13344 // unnegated and so contribute nothing (#1180).
13345 check_metrics::<TclParser>(
13346 "proc f {a b c} {\n\
13347 set r [expr {$a ? $b : $c}]\n\
13348 }",
13349 "foo.tcl",
13350 |metric| {
13351 assert_eq!(metric.abc.assignments_sum(), 1);
13352 assert_eq!(metric.abc.branches_sum(), 0);
13353 assert_eq!(metric.abc.conditions_sum(), 2);
13354 insta::assert_json_snapshot!(metric.abc);
13355 },
13356 );
13357 }
13358
13359 /// The Tcl half of the ternary slot-location guard (#1180).
13360 ///
13361 /// `ternary_expr` exposes no grammar fields, so the slots are found
13362 /// relative to the `?` and `:` tokens. A *parenthesised* condition is
13363 /// the input that discriminates that from a fixed-index reading:
13364 /// `_expr` inlines `( … )` as anonymous children of `ternary_expr`,
13365 /// so `($a) ? !$b : !$c` shifts every operand right by one and
13366 /// `child(0)` / `child(2)` / `child(4)` land on `(`, `)` and `?`.
13367 /// Without this case the whole fixed-index revert passes.
13368 #[cfg(feature = "tcl")]
13369 #[test]
13370 fn tcl_parenthesised_ternary_condition_matches_the_bare_form() {
13371 let conditions = |source: &str| {
13372 crate::test_support::metrics_verbatim(
13373 crate::LANG::Tcl,
13374 source.as_bytes(),
13375 crate::MetricsOptions::default(),
13376 )
13377 .abc
13378 .conditions_sum()
13379 };
13380 let bare = conditions("proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}");
13381 assert_eq!(bare, 4, "the bare form is the documented reference value");
13382 assert_eq!(
13383 conditions("proc f {a b c} {\n set r [expr {($a) ? !$b : !$c}]\n}"),
13384 bare,
13385 "parenthesising the condition must not change the count"
13386 );
13387 }
13388
13389 /// A parenthesised operand under `!` counts the same as a bare one.
13390 ///
13391 /// `_expr` inlines `( … )` as anonymous children, so `!($a)` puts
13392 /// `(` where a positional read expects the operand. The walker's
13393 /// negation branch kept a fixed `child(1)` through the first draft of
13394 /// #1180 and scored these 0 while the unparenthesised forms scored 1
13395 /// — an inconsistency the fix itself introduced, since before it
13396 /// neither form counted. Found in review, not by the tests: the
13397 /// parenthesised fixtures added with #1180 covered the ternary
13398 /// *condition* slot only.
13399 #[cfg(feature = "tcl")]
13400 #[test]
13401 fn tcl_parenthesised_negated_operands_match_the_bare_form() {
13402 let conditions = |source: &str| {
13403 crate::test_support::metrics_verbatim(
13404 crate::LANG::Tcl,
13405 source.as_bytes(),
13406 crate::MetricsOptions::default(),
13407 )
13408 .abc
13409 .conditions_sum()
13410 };
13411 for (bare, parenthesised) in [
13412 (
13413 "proc f {a} {\n if {!$a} { puts x }\n}",
13414 "proc f {a} {\n if {!($a)} { puts x }\n}",
13415 ),
13416 (
13417 "proc f {a b} {\n if {$a && !$b} { puts x }\n}",
13418 "proc f {a b} {\n if {$a && !($b)} { puts x }\n}",
13419 ),
13420 (
13421 "proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}",
13422 "proc f {a b c} {\n set r [expr {$a ? !($b) : !$c}]\n}",
13423 ),
13424 ] {
13425 let want = conditions(bare);
13426 assert!(want > 0, "the bare form must count something: {bare}");
13427 assert_eq!(
13428 conditions(parenthesised),
13429 want,
13430 "parenthesising the negated operand changed the count\n bare: {bare}\n paren: {parenthesised}"
13431 );
13432 }
13433 }
13434
13435 #[cfg(feature = "irules")]
13436 #[test]
13437 fn irules_abc_parenthesised_negated_operands_match_the_bare_form() {
13438 let conditions = |source: &str| {
13439 crate::test_support::metrics_verbatim(
13440 crate::LANG::Irules,
13441 source.as_bytes(),
13442 crate::MetricsOptions::default(),
13443 )
13444 .abc
13445 .conditions_sum()
13446 };
13447 let want = conditions("when X {\n if { !$a } { log local0. hi }\n}\n");
13448 assert_eq!(want, 1);
13449 assert_eq!(
13450 conditions("when X {\n if { !($a) } { log local0. hi }\n}\n"),
13451 want
13452 );
13453 }
13454
13455 #[cfg(feature = "tcl")]
13456 #[test]
13457 fn tcl_bare_truthy_and_negated_predicates_count_one_condition() {
13458 // The headline #1180 fix, on the Tcl side: both were 0 before.
13459 let conditions = |source: &str| {
13460 crate::test_support::metrics_verbatim(
13461 crate::LANG::Tcl,
13462 source.as_bytes(),
13463 crate::MetricsOptions::default(),
13464 )
13465 .abc
13466 .conditions_sum()
13467 };
13468 assert_eq!(conditions("proc f {a} {\n if {$a} { puts x }\n}"), 1);
13469 assert_eq!(conditions("proc f {a} {\n if {!$a} { puts x }\n}"), 1);
13470 assert_eq!(conditions("proc f {a} {\n while {$a} { puts x }\n}"), 1);
13471 assert_eq!(conditions("proc f {a} {\n while {!$a} { puts x }\n}"), 1);
13472 }
13473
13474 #[cfg(feature = "tcl")]
13475 #[test]
13476 fn tcl_bare_truthy_elseif_predicate_counts_one_condition() {
13477 // The `Tcl::Elseif` arm routes its predicate through
13478 // `tcl_condition_expr` exactly as `If` / `While` do (#1180), but
13479 // a comparison predicate is counted by its leaf operator, so it
13480 // cannot tell the routing from its absence. Only a bare truthy
13481 // predicate can: `$b` scores through the routed walker or not at
13482 // all.
13483 let conditions = |source: &str| {
13484 crate::test_support::metrics_verbatim(
13485 crate::LANG::Tcl,
13486 source.as_bytes(),
13487 crate::MetricsOptions::default(),
13488 )
13489 .abc
13490 .conditions_sum()
13491 };
13492 // `$a` truthy (1) + `elseif` clause (1) + `$b` truthy (1).
13493 assert_eq!(
13494 conditions("proc f {a b} {\n if {$a} { puts x } elseif {$b} { puts y }\n}"),
13495 3
13496 );
13497 }
13498
13499 #[cfg(feature = "tcl")]
13500 #[test]
13501 fn tcl_elseif_and_else_count_conditions() {
13502 // `if` / `elseif` / `else` clause productions each
13503 // contribute one condition. The leaf comparison inside the
13504 // predicate is counted independently.
13505 check_metrics::<TclParser>(
13506 "proc f {x} {\n\
13507 if {$x > 0} {\n\
13508 return 1\n\
13509 } elseif {$x < 0} {\n\
13510 return -1\n\
13511 } else {\n\
13512 return 0\n\
13513 }\n\
13514 }",
13515 "foo.tcl",
13516 |metric| {
13517 // Branches: three `return` commands → 3.
13518 // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
13519 // (1) → 4.
13520 assert_eq!(metric.abc.assignments_sum(), 0);
13521 assert_eq!(metric.abc.branches_sum(), 3);
13522 assert_eq!(metric.abc.conditions_sum(), 4);
13523 insta::assert_json_snapshot!(metric.abc);
13524 },
13525 );
13526 }
13527
13528 #[cfg(feature = "tcl")]
13529 #[test]
13530 fn tcl_if_multiple_conditions() {
13531 // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot
13532 // exposes `&&` / `||` operands as variable substitutions
13533 // (`$a`, `$b`, …) inside a `binop_expr`.
13534 check_metrics::<TclParser>(
13535 "proc f {a b c d} {\n\
13536 if {[expr {$a || $b || $c || $d}]} { return 1 } \n\
13537 if {[expr {$a && $b && $c}]} { return 2 } \n\
13538 return 0\n\
13539 }",
13540 "foo.tcl",
13541 |metric| {
13542 // The two chains feed the walker: 4 + 3 = 7. Each `if`
13543 // predicate is additionally a bare truthy test of a
13544 // command substitution — `{[expr {…}]}` is structurally
13545 // `if {[somecmd]}`, which counts 1 exactly as `if {$a}`
13546 // does — so 7 + 2 = 9 (#1180). Written without the
13547 // redundant `[expr …]` wrapper, `if {$a || $b}` scores
13548 // 2, matching C++'s `if (a || b)`.
13549 assert_eq!(metric.abc.conditions_sum(), 9);
13550 insta::assert_json_snapshot!(metric.abc);
13551 },
13552 );
13553 }
13554
13555 #[cfg(feature = "tcl")]
13556 #[test]
13557 fn tcl_while_conditions() {
13558 // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is
13559 // the standard loop. The walker fires on `&&` / `||` tokens
13560 // inside the `expr` predicate.
13561 check_metrics::<TclParser>(
13562 "proc f {a b} {\n\
13563 while {[expr {$a || $b}]} { break } \n\
13564 while {[expr {$a && $b}]} { break } \n\
13565 }",
13566 "foo.tcl",
13567 |metric| {
13568 // 2 + 2 from the chains, plus one bare truthy test per
13569 // `while` predicate — see `tcl_if_multiple_conditions`
13570 // (#1180).
13571 assert_eq!(metric.abc.conditions_sum(), 6);
13572 insta::assert_json_snapshot!(metric.abc);
13573 },
13574 );
13575 }
13576
13577 #[cfg(feature = "tcl")]
13578 #[test]
13579 fn tcl_short_circuit_with_boolean_literal_operand() {
13580 // `$a && 1` reports 2 conditions: a VariableSubstitution
13581 // operand plus a Number-literal operand. Confirms `Number`
13582 // is in the walker terminal set. `true` / `false` Tcl
13583 // keywords are not literal tokens in tree-sitter-tcl —
13584 // they're emitted as the operator-context word, which is
13585 // captured separately by the `Tcl::Boolean` kind for
13586 // dedicated `expr {true}` predicates but not as a `&&`
13587 // operand at this iteration; using a numeric literal keeps
13588 // the assertion grammar-stable.
13589 check_metrics::<TclParser>(
13590 "proc f {a} { return [expr {$a && 1}] }\n",
13591 "foo.tcl",
13592 |metric| {
13593 assert_eq!(metric.abc.conditions_sum(), 2);
13594 insta::assert_json_snapshot!(metric.abc);
13595 },
13596 );
13597 }
13598
13599 #[cfg(feature = "tcl")]
13600 #[test]
13601 fn tcl_complex_function_abc() {
13602 // Mixed program covering every category. Tcl's grammar
13603 // re-parses braced content that looks command-shaped as a
13604 // nested `command` node, which inflates the branch count
13605 // relative to a naive read of the source — see breakdown.
13606 check_metrics::<TclParser>(
13607 "proc run {n} {\n\
13608 set total 0\n\
13609 for {set i 0} {$i < $n} {incr i} {\n\
13610 if {$i % 2 == 0} {\n\
13611 do_work $i\n\
13612 } else {\n\
13613 incr total $i\n\
13614 }\n\
13615 }\n\
13616 puts \"done\"\n\
13617 return $total\n\
13618 }",
13619 "foo.tcl",
13620 |metric| {
13621 // Assignments: `set total 0` (1), `set i 0` (1),
13622 // `incr i` (1), `incr total $i` (1) → A = 4.
13623 // Branches: the outer `for …` is one `command` node;
13624 // the `{$i < $n}` predicate ALSO re-parses as a
13625 // `command` node (tree-sitter-tcl treats braced
13626 // predicates as nested commands at the pinned
13627 // grammar version); plus `do_work $i`, `puts
13628 // "done"`, and `return $total`. The for-loop body's
13629 // `incr` and `incr total $i` are assignment commands
13630 // and don't add branches. Total B = 5.
13631 // Conditions: `==` (1) and `else` (1) → C = 2. The
13632 // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
13633 // that predicate re-parses as a `command`, the `<`
13634 // is emitted as `simple_word`. Only `<` inside a
13635 // real `expr` production becomes `Tcl::LT`.
13636 assert_eq!(metric.abc.assignments_sum(), 4);
13637 assert_eq!(metric.abc.branches_sum(), 5);
13638 assert_eq!(metric.abc.conditions_sum(), 2);
13639 insta::assert_json_snapshot!(metric.abc);
13640 },
13641 );
13642 }
13643
13644 /// The dedicated `set name value` production counts as one assignment.
13645 #[cfg(feature = "irules")]
13646 #[test]
13647 fn irules_abc_set_assignment() {
13648 check_metrics::<IrulesParser>("when X {\n set x 1\n}\n", "foo.irule", |metric| {
13649 assert_eq!(metric.abc.assignments_sum(), 1);
13650 assert_eq!(metric.abc.branches_sum(), 0);
13651 assert_eq!(metric.abc.conditions_sum(), 0);
13652 });
13653 }
13654
13655 /// Mutator commands (`incr` / `append` / `lappend`) count as
13656 /// assignments, not branches — iRules has no assignment operators, so
13657 /// mutation is always a command invocation.
13658 #[cfg(feature = "irules")]
13659 #[test]
13660 fn irules_abc_mutator_commands() {
13661 check_metrics::<IrulesParser>(
13662 "when X {\n incr x\n append s \"y\"\n lappend l 1\n}\n",
13663 "foo.irule",
13664 |metric| {
13665 assert_eq!(metric.abc.assignments_sum(), 3);
13666 assert_eq!(metric.abc.branches_sum(), 0);
13667 },
13668 );
13669 }
13670
13671 /// `::incr` is `incr` through the global namespace. iRules resolves
13672 /// the leading word in `irules_command_is_assignment` rather than
13673 /// through `tcl_command_name`, so this pins the strip on that second
13674 /// path (#1381 review).
13675 #[cfg(feature = "irules")]
13676 #[test]
13677 fn irules_abc_qualified_mutator_commands() {
13678 check_metrics::<IrulesParser>(
13679 "when X {\n ::incr x\n ::append s \"y\"\n ::lappend l 1\n}\n",
13680 "foo.irule",
13681 |metric| {
13682 assert_eq!(metric.abc.assignments_sum(), 3);
13683 assert_eq!(metric.abc.branches_sum(), 0);
13684 },
13685 );
13686 }
13687
13688 /// Control: `ns::incr` is a proc in `ns`, so it stays a branch.
13689 #[cfg(feature = "irules")]
13690 #[test]
13691 fn irules_abc_namespaced_mutator_command_stays_a_branch() {
13692 check_metrics::<IrulesParser>("when X {\n ns::incr x\n}\n", "foo.irule", |metric| {
13693 assert_eq!(metric.abc.assignments_sum(), 0);
13694 assert_eq!(metric.abc.branches_sum(), 1);
13695 });
13696 }
13697
13698 /// Generic (non-mutator) commands count as branches.
13699 #[cfg(feature = "irules")]
13700 #[test]
13701 fn irules_abc_branch_commands() {
13702 check_metrics::<IrulesParser>(
13703 "when X {\n log local0. hi\n pool p1\n}\n",
13704 "foo.irule",
13705 |metric| {
13706 assert_eq!(metric.abc.assignments_sum(), 0);
13707 assert_eq!(metric.abc.branches_sum(), 2);
13708 assert_eq!(metric.abc.conditions_sum(), 0);
13709 },
13710 );
13711 }
13712
13713 /// A numeric comparison (`==`) is one condition; the `log` inside the
13714 /// `if` body is one branch.
13715 #[cfg(feature = "irules")]
13716 #[test]
13717 fn irules_abc_comparison_condition() {
13718 check_metrics::<IrulesParser>(
13719 "when X {\n if { $a == 1 } { log local0. hi }\n}\n",
13720 "foo.irule",
13721 |metric| {
13722 assert_eq!(metric.abc.branches_sum(), 1);
13723 assert_eq!(metric.abc.conditions_sum(), 1);
13724 },
13725 );
13726 }
13727
13728 /// A word-form string comparator (`contains`) is a condition just like
13729 /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If
13730 /// `contains` were dropped from the condition set this would report 0.
13731 #[cfg(feature = "irules")]
13732 #[test]
13733 fn irules_abc_string_op_condition() {
13734 check_metrics::<IrulesParser>(
13735 "when X {\n if { $a contains \"x\" } { log local0. hi }\n}\n",
13736 "foo.irule",
13737 |metric| {
13738 assert_eq!(metric.abc.branches_sum(), 1);
13739 assert_eq!(metric.abc.conditions_sum(), 1);
13740 },
13741 );
13742 }
13743
13744 /// Each `elseif` / `else` clause is one condition; the three `set`s are
13745 /// assignments. The leading `if` is not itself a condition.
13746 #[cfg(feature = "irules")]
13747 #[test]
13748 fn irules_abc_elseif_else_conditions() {
13749 check_metrics::<IrulesParser>(
13750 "when X {\n if { $a } { set r 1 } elseif { $b } { set r 2 } else { set r 3 }\n}\n",
13751 "foo.irule",
13752 |metric| {
13753 assert_eq!(metric.abc.assignments_sum(), 3);
13754 // The `elseif` and `else` clauses are one condition each,
13755 // as before; #1180 adds the two bare truthy predicates
13756 // (`{ $a }`, `{ $b }`). C++'s
13757 // `if(a){} else if(b){} else {}` also scores 4.
13758 assert_eq!(metric.abc.conditions_sum(), 4);
13759 },
13760 );
13761 }
13762
13763 /// A ternary contributes its own condition plus the `>` comparison in
13764 /// its test: conditions 2; the `set` is one assignment.
13765 #[cfg(feature = "irules")]
13766 #[test]
13767 fn irules_abc_ternary_condition() {
13768 check_metrics::<IrulesParser>(
13769 "when X {\n set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
13770 "foo.irule",
13771 |metric| {
13772 assert_eq!(metric.abc.assignments_sum(), 1);
13773 assert_eq!(metric.abc.conditions_sum(), 2);
13774 },
13775 );
13776 }
13777
13778 /// Fitzpatrick Rule 9: the short-circuit `&&` is not itself a condition,
13779 /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards
13780 /// the `irules_count_unary_conditions` / `irules_inspect_container`
13781 /// walker — conditions 2.
13782 #[cfg(feature = "irules")]
13783 #[test]
13784 fn irules_abc_negated_operands_in_chain() {
13785 check_metrics::<IrulesParser>(
13786 "when X {\n if { !$a && !$b } { log local0. hi }\n}\n",
13787 "foo.irule",
13788 |metric| {
13789 assert_eq!(metric.abc.branches_sum(), 1);
13790 assert_eq!(metric.abc.conditions_sum(), 2);
13791 },
13792 );
13793 }
13794
13795 /// A bare-truthy `if {$a}` predicate is one condition (#1180).
13796 ///
13797 /// It carries no comparison, ternary or short-circuit operator, so
13798 /// before the Phase 2B slot routing landed nothing invoked the
13799 /// unary-conditional walker and the whole predicate scored 0 — this
13800 /// test previously pinned that absence, and the metrics book's ABC
13801 /// deviation table said so. Now the `if` node routes its `expr`
13802 /// predicate and the count matches C++'s `if (a)`, which is also 1.
13803 /// The `log` command remains the single branch.
13804 #[cfg(feature = "irules")]
13805 #[test]
13806 fn irules_abc_bare_truthy_counts_one_condition() {
13807 check_metrics::<IrulesParser>(
13808 "when X {\n if { $a } { log local0. hi }\n}\n",
13809 "foo.irule",
13810 |metric| {
13811 assert_eq!(metric.abc.branches_sum(), 1);
13812 assert_eq!(metric.abc.conditions_sum(), 1);
13813 },
13814 );
13815 }
13816
13817 /// The negated form of the same predicate, which was *also* 0 before
13818 /// #1180: `!$a` reached the walker but no parent seeded boolean
13819 /// context, so the terminal operand was never counted. Distinct from
13820 /// `irules_abc_negated_operands_in_chain`, whose `&&` supplied the
13821 /// seed the bare form lacked.
13822 #[cfg(feature = "irules")]
13823 #[test]
13824 fn irules_abc_negated_bare_truthy_counts_one_condition() {
13825 check_metrics::<IrulesParser>(
13826 "when X {\n if { !$a } { log local0. hi }\n}\n",
13827 "foo.irule",
13828 |metric| {
13829 assert_eq!(metric.abc.branches_sum(), 1);
13830 assert_eq!(metric.abc.conditions_sum(), 1);
13831 },
13832 );
13833 }
13834
13835 /// The ternary's three operand slots, located relative to the `?`
13836 /// and `:` tokens because the grammar exposes no fields (#1180).
13837 ///
13838 /// `$a ? !$b : !$c` is four: the `ternary_expr` node, the bare
13839 /// truthy condition, and one per negated branch — the same value
13840 /// Java, C#, Groovy, the C family, the JS family, PHP, Perl, Ruby
13841 /// and Python report for the identical expression.
13842 #[cfg(feature = "irules")]
13843 #[test]
13844 fn irules_abc_ternary_routes_its_operand_slots() {
13845 check_metrics::<IrulesParser>(
13846 "when X {\n set y [expr { $a ? !$b : !$c }]\n}\n",
13847 "foo.irule",
13848 |metric| {
13849 assert_eq!(metric.abc.conditions_sum(), 4);
13850 },
13851 );
13852 }
13853
13854 /// The #1161 control: a ternary whose condition is a *comparison*
13855 /// must not move. The `>` already supplied its condition and the
13856 /// branches are unnegated, so routing the slots adds nothing.
13857 #[cfg(feature = "irules")]
13858 #[test]
13859 fn irules_abc_comparison_ternary_is_unchanged_by_slot_routing() {
13860 check_metrics::<IrulesParser>(
13861 "when X {\n set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
13862 "foo.irule",
13863 |metric| {
13864 assert_eq!(metric.abc.conditions_sum(), 2);
13865 },
13866 );
13867 }
13868
13869 /// A parenthesised ternary condition scores the same as a bare one.
13870 ///
13871 /// The grammar inlines `( … )` as anonymous children of
13872 /// `ternary_expr` rather than wrapping them in a node, so a
13873 /// fixed-index reading of the slots would shift right by one and
13874 /// mis-assign every operand. This is the input that discriminates
13875 /// the token-relative location the fix uses.
13876 #[cfg(feature = "irules")]
13877 #[test]
13878 fn irules_abc_parenthesised_ternary_condition_matches_the_bare_form() {
13879 // `check_metrics` takes a bare `fn`, so it cannot carry the
13880 // first measurement into the second comparison; `metrics_verbatim`
13881 // returns a value instead.
13882 let conditions = |source: &str| {
13883 crate::test_support::metrics_verbatim(
13884 crate::LANG::Irules,
13885 source.as_bytes(),
13886 crate::MetricsOptions::default(),
13887 )
13888 .abc
13889 .conditions_sum()
13890 };
13891 let bare = conditions("when X {\n set y [expr { $a ? !$b : !$c }]\n}\n");
13892 assert_eq!(bare, 4, "the bare form is the documented reference value");
13893 assert_eq!(
13894 conditions("when X {\n set y [expr { ($a) ? !$b : !$c }]\n}\n"),
13895 bare,
13896 "parenthesising the condition must not change the count"
13897 );
13898 }
13899
13900 // #1454's five-language sweep of #1422's C# rule: a pattern-match
13901 // guard is a condition *slot*, so every spelling of one contributes
13902 // exactly one condition, and the guard is a cyclomatic decision the
13903 // arm it guards does not already pay for.
13904 //
13905 // Each fixture is the §11 triple — an operator guard (`> 5`, whose
13906 // comparison token already counted before the fix), a call guard,
13907 // and a bare-identifier guard (the two that counted nothing) —
13908 // plus a parenthesised guard, which is the only member that
13909 // exercises the boolean-context seed, plus an unguarded control
13910 // that must stay where it was. A single-spelling fixture cannot
13911 // show the slot works: before the fix the operator member already
13912 // read the right number.
13913 //
13914 // The controls are load-bearing twice over. `none` pins that the
13915 // slot fires on the guard and not on the arm — a `+1` on every arm
13916 // would satisfy every guarded row and break this one — and the
13917 // helper (`is_even` / `isEven`) pins that the call-guard row is
13918 // scoring its guard rather than its call, since a call is an ABC
13919 // *branch* everywhere and a condition nowhere.
13920 //
13921 // `assert_members_score` rather than `assert_every_member_scores`:
13922 // the guarded members and their control sit at different values,
13923 // which is the comparison these tests exist to make.
13924
13925 #[cfg(feature = "rust")]
13926 #[test]
13927 fn rust_match_guard_scores_one_condition_however_spelled() {
13928 let src = "fn is_even(n: i32) -> bool {
13929 n == 0
13930 }
13931 fn tok(x: i32) -> i32 {
13932 match x { n if n > 5 => 1, _ => 0 }
13933 }
13934 fn call(x: i32) -> i32 {
13935 match x { n if is_even(n) => 1, _ => 0 }
13936 }
13937 fn bare(x: i32, b: bool) -> i32 {
13938 match x { _ if b => 1, _ => 0 }
13939 }
13940 fn paren(x: i32, b: bool) -> i32 {
13941 match x { n if (b) => n, _ => 0 }
13942 }
13943 fn none(x: i32) -> i32 {
13944 match x { 1 => 1, _ => 0 }
13945 }";
13946 assert_fixture_spells::<RustParser>(
13947 src,
13948 "foo.rs",
13949 &[
13950 // The guard's own `if` keyword, one per guarded member.
13951 // It is also how Rust cyclomatic already saw the guard,
13952 // which is why only the ABC half moved here.
13953 (Rust::If as u16, 4, "match guards"),
13954 (Rust::GT as u16, 1, "`tok`'s `>`"),
13955 (Rust::CallExpression as u16, 1, "the `is_even` call"),
13956 (
13957 Rust::ParenthesizedExpression as u16,
13958 1,
13959 "`paren`'s parenthesised guard",
13960 ),
13961 // Two arms per member: the guarded one and its bare `_`.
13962 (Rust::MatchPattern as u16, 10, "match arms"),
13963 ],
13964 );
13965 check_func_space::<RustParser, _>(src, "foo.rs", |space| {
13966 assert_members_score(
13967 &space,
13968 &[
13969 ("is_even", 1, 1),
13970 ("tok", 2, 3),
13971 // Was 1 — the guard spelling nothing counted.
13972 ("call", 2, 3),
13973 ("bare", 2, 3),
13974 ("paren", 2, 3),
13975 ("none", 1, 2),
13976 ],
13977 );
13978 });
13979 }
13980
13981 #[cfg(feature = "java")]
13982 #[test]
13983 fn java_pattern_switch_guard_scores_one_condition_however_spelled() {
13984 let src = "class T {
13985 static boolean isEven(int i) { return i == 0; }
13986 int tok(Object o) { return switch (o) { case Integer i when i > 5 -> 1; default -> 0; }; }
13987 int call(Object o) { return switch (o) { case Integer i when isEven(i) -> 1; default -> 0; }; }
13988 int bare(Object o, boolean b) { return switch (o) { case Integer i when b -> 1; default -> 0; }; }
13989 int paren(Object o, boolean b) { return switch (o) { case Integer i when (b) -> 1; default -> 0; }; }
13990 int none(Object o) { return switch (o) { case Integer i -> 1; default -> 0; }; }
13991 }";
13992 assert_fixture_spells::<JavaParser>(
13993 src,
13994 "foo.java",
13995 &[
13996 (Java::Guard as u16, 4, "`when` guards"),
13997 (Java::GT as u16, 1, "`tok`'s `>`"),
13998 (Java::MethodInvocation as u16, 1, "the `isEven` call"),
13999 // Five `switch (o)` subjects plus `paren`'s `when (b)`.
14000 // Java wraps a switch subject in the same kind, so this
14001 // count moves if either is edited out.
14002 (
14003 Java::ParenthesizedExpression as u16,
14004 6,
14005 "the switch subjects and `paren`'s guard",
14006 ),
14007 ],
14008 );
14009 check_func_space::<JavaParser, _>(src, "foo.java", |space| {
14010 assert_members_score(
14011 &space.spaces[0],
14012 &[
14013 ("isEven", 1, 1),
14014 // Was 2 conditions against a *flat* cyclomatic of 2:
14015 // Java had neither half of the rule, so the guard
14016 // was invisible to cyclomatic and visible to ABC
14017 // only through whatever operator it spelled.
14018 ("tok", 2, 3),
14019 ("call", 2, 3),
14020 ("bare", 2, 3),
14021 ("paren", 2, 3),
14022 ("none", 1, 2),
14023 ],
14024 );
14025 });
14026 }
14027
14028 #[cfg(feature = "python")]
14029 #[test]
14030 fn python_case_guard_scores_one_condition_however_spelled() {
14031 let src = "def is_even(n):
14032 return n == 0
14033def tok(x):
14034 match x:
14035 case n if n > 5:
14036 return 1
14037 case _:
14038 return 0
14039def call(x):
14040 match x:
14041 case n if is_even(x):
14042 return 1
14043 case _:
14044 return 0
14045def bare(x, b):
14046 match x:
14047 case _ if b:
14048 return 1
14049 case _:
14050 return 0
14051def paren(x, b):
14052 match x:
14053 case n if (b):
14054 return n
14055 case _:
14056 return 0
14057def none(x):
14058 match x:
14059 case 1:
14060 return 1
14061 case _:
14062 return 0
14063";
14064 assert_fixture_spells::<PythonParser>(
14065 src,
14066 "foo.py",
14067 &[
14068 // The `case` guard clause. A comprehension filter is the
14069 // same kind in a different role, and the `guard` field
14070 // is what keeps it out of the slot — there is none in
14071 // this fixture, and `python_comprehension_if_clause_is_not_a_case_guard`
14072 // is where that separation is pinned.
14073 (Python::IfClause as u16, 4, "`case` guards"),
14074 (
14075 Python::ComparisonOperator as u16,
14076 2,
14077 "`tok`'s `>` and `is_even`'s `==`",
14078 ),
14079 (Python::Call as u16, 1, "the `is_even` call"),
14080 (
14081 Python::ParenthesizedExpression as u16,
14082 1,
14083 "`paren`'s parenthesised guard",
14084 ),
14085 ],
14086 );
14087 check_func_space::<PythonParser, _>(src, "foo.py", |space| {
14088 assert_members_score(
14089 &space,
14090 &[
14091 ("is_even", 1, 1),
14092 ("tok", 2, 3),
14093 ("call", 2, 3),
14094 ("bare", 2, 3),
14095 ("paren", 2, 3),
14096 ("none", 1, 2),
14097 ],
14098 );
14099 });
14100 }
14101
14102 // A comprehension's `if` filter is an `if_clause` too, and it is not
14103 // a `case` guard: the slot reads `case_clause`'s `guard` field, so
14104 // the two cannot be confused by construction. Python cyclomatic does
14105 // count the filter (through the same `If` keyword token it counts a
14106 // guard by), and ABC does not — a pre-existing divergence this
14107 // change deliberately leaves where it found it. The test is here so
14108 // that a later `IfClause` arm added without the field read fails
14109 // loudly rather than moving comprehensions silently.
14110 #[cfg(feature = "python")]
14111 #[test]
14112 fn python_comprehension_if_clause_is_not_a_case_guard() {
14113 let src = "def m(xs):
14114 return [x for x in xs if x]
14115";
14116 assert_fixture_spells::<PythonParser>(
14117 src,
14118 "foo.py",
14119 &[(Python::IfClause as u16, 1, "the comprehension filter")],
14120 );
14121 check_func_space::<PythonParser, _>(src, "foo.py", |space| {
14122 assert_members_score(&space, &[("m", 0, 3)]);
14123 });
14124 }
14125
14126 #[cfg(feature = "ruby")]
14127 #[test]
14128 fn ruby_in_clause_guard_scores_one_condition_however_spelled() {
14129 let src = "def is_even(x)
14130 x == 0
14131end
14132def tok(x)
14133 case x
14134 in [n] if n > 5 then 1
14135 in _ then 0
14136 end
14137end
14138def call(x)
14139 case x
14140 in [n] if n.even? then 1
14141 in _ then 0
14142 end
14143end
14144def bare(x, b)
14145 case x
14146 in [n] if b then 1
14147 in _ then 0
14148 end
14149end
14150def unguard(x, b)
14151 case x
14152 in [n] unless b then 1
14153 in _ then 0
14154 end
14155end
14156def paren(x, b)
14157 case x
14158 in [n] if (b) then 1
14159 in _ then 0
14160 end
14161end
14162def none(x)
14163 case x
14164 in [n] then 1
14165 in _ then 0
14166 end
14167end
14168";
14169 assert_fixture_spells::<RubyParser>(
14170 src,
14171 "foo.rb",
14172 &[
14173 (Ruby::IfGuard as u16, 4, "`if` guards"),
14174 (Ruby::UnlessGuard as u16, 1, "the `unless` guard"),
14175 (Ruby::GT as u16, 1, "`tok`'s `>`"),
14176 (
14177 Ruby::ParenthesizedStatements as u16,
14178 1,
14179 "`paren`'s parenthesised guard",
14180 ),
14181 ],
14182 );
14183 // `Ruby::Guard` (210) is the hidden `_guard` supertype: the two
14184 // dispatchers list it beside the concrete kinds so a grammar
14185 // that starts emitting it keeps working, and this pins that it
14186 // does not emit it today — otherwise the defensive arm is
14187 // indistinguishable from a dead one
14188 // (`.claude/rules/grammar-dispatch.md` §2).
14189 let parser = RubyParser::new(
14190 src.as_bytes().to_vec(),
14191 &std::path::PathBuf::from("foo.rb"),
14192 None,
14193 );
14194 assert!(
14195 !ast_has_kind_id(&parser, Ruby::Guard as u16),
14196 "`_guard` stopped being hidden — the defensive arms now fire"
14197 );
14198 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
14199 assert_members_score(
14200 &space,
14201 &[
14202 ("is_even", 1, 1),
14203 // Was 2 / 2: Ruby had neither half either, and the
14204 // arm's own condition supplied the 2 that made the
14205 // operator spelling look correct.
14206 ("tok", 2, 3),
14207 ("call", 2, 3),
14208 ("bare", 2, 3),
14209 ("unguard", 2, 3),
14210 ("paren", 2, 3),
14211 ("none", 1, 2),
14212 ],
14213 );
14214 });
14215 }
14216
14217 // Elixir is the inverse of the four above: ABC counted the `when`
14218 // token from the start and cyclomatic had no arm at all, so a guard
14219 // read as a condition with no decision behind it. #1454 adds the
14220 // decision, which is why `tok` / `call` / `bare` move on the
14221 // cyclomatic axis here and on the ABC axis everywhere else.
14222 //
14223 // Elixir was also the one language of the six that #1454 left
14224 // *without* the slot. Its `when` arm added a flat one on top of
14225 // whatever the guard's sub-structure already scored, so `tok` came
14226 // back 3 against `call` and `bare`'s 2 — the spelling-dependence the
14227 // batch exists to remove, and a `.claude/rules/grammar-dispatch.md`
14228 // §5 double count with the `>` token arm. A whole-branch review of
14229 // the batch found it; all three rows now read 2, level with the
14230 // sibling fixtures above and with their own decision counts.
14231 #[cfg(feature = "elixir")]
14232 #[test]
14233 fn elixir_guard_is_a_decision_however_spelled() {
14234 let src = "defmodule T do
14235 def is_even(x) do
14236 x == 0
14237 end
14238 def tok(x) do
14239 case x do
14240 n when n > 5 -> 1
14241 _ -> 0
14242 end
14243 end
14244 def call(x) do
14245 case x do
14246 n when is_integer(n) -> 1
14247 _ -> 0
14248 end
14249 end
14250 def bare(x, b) do
14251 case x do
14252 _n when b -> 1
14253 _ -> 0
14254 end
14255 end
14256 def none(x) do
14257 case x do
14258 1 -> 1
14259 _ -> 0
14260 end
14261 end
14262end
14263";
14264 assert_fixture_spells::<ElixirParser>(
14265 src,
14266 "foo.ex",
14267 &[
14268 (Elixir::When as u16, 3, "`when` guards"),
14269 (Elixir::GT as u16, 1, "`tok`'s `>`"),
14270 ],
14271 );
14272 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
14273 assert_members_score(
14274 &space.spaces[0],
14275 &[
14276 ("is_even", 1, 1),
14277 // All three guarded members were cyclomatic 2 —
14278 // level with `none` — before the decision arm, and
14279 // `tok` was abc 3 before the slot.
14280 ("tok", 2, 3),
14281 ("call", 2, 3),
14282 ("bare", 2, 3),
14283 ("none", 1, 2),
14284 ],
14285 );
14286 });
14287 }
14288
14289 // The slot's whole claim, on every spelling the Elixir grammar gives
14290 // a guard — and on `assert_every_member_scores`, which asserts §8
14291 // parity where the fixture above cannot (its unguarded control and
14292 // its bare-comparison member legitimately sit off their decision
14293 // counts, which is why that one takes a per-member table).
14294 //
14295 // Nine members, nine routes to the same 2:
14296 //
14297 // * `tok` / `eq` — the guard is an operator application, so the
14298 // slot adds nothing and the `>` / `==` token arm supplies the
14299 // one. These were 3 before the slot.
14300 // * `call` / `bare` — `Call` and `Identifier` are
14301 // `elixir_bool_terminal_kinds!()` members, so the slot supplies
14302 // the one directly.
14303 // * `paren` / `negated` — `block` and `unary_operator` are
14304 // wrappers, peeled by `elixir_inspect_container`. `negated`
14305 // covers the keyword `not`, which that walker did not recognise
14306 // as a negation until this change and which would otherwise have
14307 // scored zero here.
14308 // * `membership` / `nonmembership` — `in` / `not in` are
14309 // relational operators, given the by-use arm #1461 gave the
14310 // other five languages. Without it these two would score zero,
14311 // since the slot correctly declines an operator application.
14312 //
14313 // A repeated guard (`when a when b`) is deliberately absent: it is
14314 // an or-chain and scores one per alternative, so it belongs with
14315 // `elixir_repeated_guard_matches_the_or_chain` below rather than in
14316 // a table whose every row reads 2.
14317 //
14318 // §11: each bullet is an independent path, and no other member can
14319 // stand in for it — deleting the `in` arm leaves `membership` and
14320 // `nonmembership` alone failing, deleting the `not` recognition
14321 // leaves `negated` alone failing.
14322 #[cfg(feature = "elixir")]
14323 #[test]
14324 fn elixir_guard_scores_one_however_spelled() {
14325 let src = "defmodule T do
14326 def tok(x) do
14327 case x do
14328 n when n > 5 -> 1
14329 _ -> 0
14330 end
14331 end
14332 def eq(x) do
14333 case x do
14334 n when n == 5 -> 1
14335 _ -> 0
14336 end
14337 end
14338 def call(x) do
14339 case x do
14340 n when is_integer(n) -> 1
14341 _ -> 0
14342 end
14343 end
14344 def bare(x, b) do
14345 case x do
14346 _n when b -> 1
14347 _ -> 0
14348 end
14349 end
14350 def paren(x, b) do
14351 case x do
14352 _n when (b) -> 1
14353 _ -> 0
14354 end
14355 end
14356 def negated(x, b) do
14357 case x do
14358 _n when not b -> 1
14359 _ -> 0
14360 end
14361 end
14362 def membership(x) do
14363 case x do
14364 n when n in [1, 2] -> 1
14365 _ -> 0
14366 end
14367 end
14368 def nonmembership(x) do
14369 case x do
14370 n when n not in [1, 2] -> 1
14371 _ -> 0
14372 end
14373 end
14374end
14375";
14376 assert_fixture_spells::<ElixirParser>(
14377 src,
14378 "foo.ex",
14379 &[
14380 (Elixir::When as u16, 8, "one `when` guard per member"),
14381 (Elixir::GT as u16, 1, "`tok`'s `>`"),
14382 (Elixir::EQEQ as u16, 1, "`eq`'s `==`"),
14383 (Elixir::Block as u16, 1, "`paren`'s parenthesised guard"),
14384 (Elixir::Not as u16, 1, "`negated`'s `not`"),
14385 (Elixir::In as u16, 1, "`membership`'s `in`"),
14386 (Elixir::Notin as u16, 1, "`nonmembership`'s `not in`"),
14387 ],
14388 );
14389 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
14390 assert_every_member_scores(
14391 &space.spaces[0],
14392 8,
14393 2,
14394 "one for the `case` arm and one for the guard, however the guard is spelled",
14395 );
14396 });
14397 }
14398
14399 // Elixir's repeated guard is an or-chain, not a longer spelling of
14400 // one guard: each `when` expression is tried in turn and evaluation
14401 // moves to the next when the previous one is false *or raises*. So
14402 // `when a when b` carries the alternatives `when a or b` does, and
14403 // the two spellings have to score alike.
14404 //
14405 // They did not. The nesting is right-associative
14406 // (`head when (a when (b when c))`), only the outermost `when` sat
14407 // on the anchor `elixir_when_is_guard` tests, and the `Abc` slot
14408 // peeled to the last alternative and scored that one — so a
14409 // repeated guard read *identically to a single guard* on both axes
14410 // while the `or` form read one higher per alternative.
14411 //
14412 // Asserted as an equality against the `or` spelling rather than
14413 // against separate literals, so neither row can drift alone; the
14414 // literals are here too, because a pair of equal wrong numbers
14415 // would satisfy the equality on its own.
14416 //
14417 // Both anchors, because the climb has to reach each: a definition
14418 // `Call`'s `arguments` (`repeated` / `ored`) and a `stab_clause`'s
14419 // `left` (`clause` / `clause_or`). `single` is the control the
14420 // repeated rows must sit *above*, which is the comparison that
14421 // failed before.
14422 #[cfg(feature = "elixir")]
14423 #[test]
14424 fn elixir_repeated_guard_matches_the_or_chain() {
14425 let src = "defmodule T do
14426 def repeated(n) when is_integer(n) when is_float(n) when is_atom(n) do
14427 n
14428 end
14429 def ored(n) when is_integer(n) or is_float(n) or is_atom(n) do
14430 n
14431 end
14432 def single(n) when is_integer(n) do
14433 n
14434 end
14435 def clause(x) do
14436 case x do
14437 n when is_integer(n) when is_float(n) -> 1
14438 _ -> 0
14439 end
14440 end
14441 def clause_or(x) do
14442 case x do
14443 n when is_integer(n) or is_float(n) -> 1
14444 _ -> 0
14445 end
14446 end
14447end
14448";
14449 assert_fixture_spells::<ElixirParser>(
14450 src,
14451 "foo.ex",
14452 &[
14453 (
14454 Elixir::When as u16,
14455 8,
14456 "three repeated, one `or`-spelled, one single, two \
14457 repeated and one `or`-spelled in a clause",
14458 ),
14459 (Elixir::Or as u16, 3, "the two `or` spellings' operators"),
14460 ],
14461 );
14462 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
14463 // Positional: a `def` whose head carries a guard parses its
14464 // name out of a `binary_operator` rather than a plain
14465 // `Call`, so `repeated`, `ored` and `single` all come back
14466 // `<anonymous>` (the same naming gap
14467 // `elixir_typespec_when_is_not_a_guard` reads around).
14468 let members: Vec<(u64, u64)> = space.spaces[0]
14469 .spaces
14470 .iter()
14471 .map(|m| {
14472 (
14473 m.metrics.abc.conditions(),
14474 m.metrics.cyclomatic.cyclomatic(),
14475 )
14476 })
14477 .collect();
14478 assert_eq!(
14479 members,
14480 vec![(3, 4), (3, 4), (1, 2), (3, 4), (3, 4)],
14481 "a repeated guard scores one alternative per `when`; the \
14482 single guard is the control one alternative lower"
14483 );
14484 assert_eq!(
14485 members[0], members[1],
14486 "`when a when b when c` is the or-chain `when a or b or c`"
14487 );
14488 assert_eq!(
14489 members[3], members[4],
14490 "and the same holds on a `stab_clause` anchor"
14491 );
14492 });
14493 }
14494
14495 // The gate that makes the Elixir arm safe. Elixir has no dedicated
14496 // guard production, and a typespec's binding clause spells the same
14497 // `when` token — so an ungated arm would have made type syntax a
14498 // decision. It was already an ABC condition against no decision
14499 // anywhere, which this removes.
14500 //
14501 // Both members carry the same `@spec`; only `guarded` carries a real
14502 // head guard, so the difference between the two rows is the guard
14503 // and nothing else.
14504 #[cfg(feature = "elixir")]
14505 #[test]
14506 fn elixir_typespec_when_is_not_a_guard() {
14507 let src = "defmodule T do
14508 @spec plain(a) :: a when a: integer
14509 def plain(x) do
14510 x
14511 end
14512 @spec guarded(a) :: a when a: integer
14513 def guarded(x) when is_integer(x) do
14514 x
14515 end
14516end
14517";
14518 assert_fixture_spells::<ElixirParser>(
14519 src,
14520 "foo.ex",
14521 &[(
14522 Elixir::When as u16,
14523 3,
14524 "two typespec `when`s and one head guard",
14525 )],
14526 );
14527 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
14528 let module = &space.spaces[0];
14529 // Positional rather than by name: a `def` whose head carries
14530 // a guard parses its name out of a `binary_operator` instead
14531 // of a plain `Call` target, and the space comes back
14532 // `<anonymous>`. That naming gap predates this change and is
14533 // why `assert_members_score` cannot serve here.
14534 let members: Vec<(u64, u64)> = module
14535 .spaces
14536 .iter()
14537 .map(|m| {
14538 (
14539 m.metrics.abc.conditions(),
14540 m.metrics.cyclomatic.cyclomatic(),
14541 )
14542 })
14543 .collect();
14544 assert_eq!(
14545 members,
14546 vec![(0, 1), (1, 2)],
14547 "`plain` scores nothing; `guarded` scores its head guard \
14548 in both metrics"
14549 );
14550 // The typespecs sit in the module body, outside either
14551 // member, so their (non-)contribution has to be read off
14552 // the container. Was 2 before the gate: one per `@spec`.
14553 assert_eq!(
14554 module.metrics.abc.conditions(),
14555 0,
14556 "a typespec `when` is type syntax, not a guard"
14557 );
14558 });
14559 }
14560
14561 // The other half of that gate's allowlist. `elixir_when_is_guard`
14562 // accepts a definition `Call`'s `arguments` anchor when the call's
14563 // keyword is a method macro *or* one of `defguard` / `defguardp`,
14564 // and the second disjunct is its own path: `elixir_is_method_macro`
14565 // spells only `def` / `defp` / `defmacro` / `defmacrop`, so a
14566 // `defguard` head reaches the anchor test solely through the
14567 // `matches!`. Nothing in the suite spelled `defguard` before this,
14568 // which left the disjunct scored by no fixture at all.
14569 //
14570 // `defguard` is where a guard is most obviously a decision — the
14571 // macro exists to name one — so an ungated head would have scored
14572 // the construct zero on both axes while the `def f(x) when g`
14573 // spelling it expands into scores one.
14574 //
14575 // Read off the *module*, not off a member: `defguard` is not a
14576 // method macro, so it opens no function space and its guard lands
14577 // in the container's body. `plain` is the control that pins the
14578 // module's rows to the two guards and nothing else, and the guard
14579 // bodies are calls (`is_integer/1`, `is_atom/1`) rather than
14580 // comparisons so neither can supply a condition of its own.
14581 #[cfg(feature = "elixir")]
14582 #[test]
14583 fn elixir_defguard_head_is_a_guard() {
14584 let src = "defmodule T do
14585 defguard is_int(x) when is_integer(x)
14586 defguardp is_at(x) when is_atom(x)
14587 def plain(x) do
14588 x
14589 end
14590end
14591";
14592 assert_fixture_spells::<ElixirParser>(
14593 src,
14594 "foo.ex",
14595 &[(
14596 Elixir::When as u16,
14597 2,
14598 "one per `defguard` / `defguardp` head",
14599 )],
14600 );
14601 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
14602 let module = &space.spaces[0];
14603 assert_eq!(
14604 (
14605 module.metrics.abc.conditions(),
14606 module.metrics.cyclomatic.cyclomatic()
14607 ),
14608 (2, 3),
14609 "each of `defguard` and `defguardp` carries one guard, on \
14610 both axes (cyclomatic counts from its base of 1)"
14611 );
14612 let members: Vec<(u64, u64)> = module
14613 .spaces
14614 .iter()
14615 .map(|m| {
14616 (
14617 m.metrics.abc.conditions(),
14618 m.metrics.cyclomatic.cyclomatic(),
14619 )
14620 })
14621 .collect();
14622 assert_eq!(
14623 members,
14624 vec![(0, 1)],
14625 "`defguard` opens no function space, so `plain` is the \
14626 module's only member and scores nothing"
14627 );
14628 });
14629 }
14630
14631 // #1461's structural half: a relational operator scores by *use*,
14632 // a value-bearing operand scores in a boolean *slot*.
14633 //
14634 // Until this, the relational constructs a grammar spells as their
14635 // own production — C#'s two `is` tests, Java's and Groovy's
14636 // `instanceof`, Groovy's `in`, Kotlin's `is` / `in`, Ruby's
14637 // one-line `in` — reached `stats.conditions` only through
14638 // `<lang>_bool_terminal_kinds!()`, which every walker consults
14639 // inside a boolean slot and nowhere else. The comparison token
14640 // beside each of them carried no such gate, so one language scored
14641 // two spellings of the same relational test differently by
14642 // position:
14643 //
14644 // val b = a == c // 1 condition
14645 // val b = a is String // 0 conditions
14646 //
14647 // Fitzpatrick Rule 5 scores a relational operator wherever it is
14648 // written, so the asymmetry was in the mechanism. Each construct
14649 // now has an unconditional arm in its language's `compute` and has
14650 // left the terminal set; listing it in both would score it twice
14651 // (`.claude/rules/grammar-dispatch.md` §5).
14652 //
14653 // Every fixture below carries the same four shapes, because they
14654 // are structurally independent paths and covering one says nothing
14655 // about the others (§11):
14656 //
14657 // - `out*` — the construct with no boolean slot above it. This is
14658 // the case the change exists for, and the one no prior fixture
14659 // covered: measured against the pre-change binary, every `out*`
14660 // member here scored **0**.
14661 // - `outCtl` — the same member spelling `==`. It read 1 before and
14662 // after, so it is the anchor that makes `out*`'s 1 a claim about
14663 // the construct rather than about the member shape
14664 // (`.claude/rules/testing.md`, "Perturb the fixture as well as
14665 // the production line").
14666 // - `in*` / `inAnd` — the predicate slot and the `&&`-chain walker,
14667 // the two paths that already owned the count. They must be
14668 // *unchanged*; a §5 double count surfaces here as a 2 against the
14669 // control's 1, which is how #1459 caught the Kotlin `as?`
14670 // collision.
14671 // - `forIn` (Kotlin / Groovy / Ruby) — the `for … in …` header,
14672 // which spells the same keyword from a different production and
14673 // must stay at 0. It is the reason those three count the node and
14674 // not the token.
14675
14676 #[cfg(feature = "csharp")]
14677 #[test]
14678 fn csharp_is_tests_score_outside_a_boolean_slot() {
14679 let src = "class A {
14680 bool outIs(object x) => x is int;
14681 bool outPat(object x) => x is null;
14682 bool outCtl(int x) => x == 1;
14683 int inIs(object x) { if (x is int) { return 1; } return 0; }
14684 int inAnd(object x, bool b) { if (x is int && b) { return 1; } return 0; }
14685 }";
14686 assert_csharp_fixture_spells(
14687 src,
14688 &[
14689 (Csharp::IsExpression as u16, 3, "the bare `is` type tests"),
14690 (
14691 Csharp::IsPatternExpression as u16,
14692 1,
14693 "`outPat`'s null pattern",
14694 ),
14695 (Csharp::EQEQ as u16, 1, "`outCtl`'s comparison"),
14696 ],
14697 );
14698 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
14699 assert_members_score(
14700 &space.spaces[0],
14701 &[
14702 // Both were 0, against `outCtl`'s 1 on the same
14703 // expression-bodied shape.
14704 ("outIs", 1, 1),
14705 ("outPat", 1, 1),
14706 ("outCtl", 1, 1),
14707 // Unchanged: the slot no longer counts the test, the
14708 // new arm does, and the total stays put.
14709 ("inIs", 1, 2),
14710 ("inAnd", 2, 3),
14711 ],
14712 );
14713 });
14714 }
14715
14716 #[cfg(feature = "java")]
14717 #[test]
14718 fn java_instanceof_scores_outside_a_boolean_slot() {
14719 let src = "class A {
14720 boolean outOf(Object x) { return x instanceof String; }
14721 boolean outCtl(int x) { return x == 1; }
14722 int inOf(Object x) { if (x instanceof String) { return 1; } return 0; }
14723 int inAnd(Object x, boolean b) { if (x instanceof String && b) { return 1; } return 0; }
14724 }";
14725 assert_fixture_spells::<JavaParser>(
14726 src,
14727 "foo.java",
14728 &[
14729 (
14730 Java::InstanceofExpression as u16,
14731 3,
14732 "the `instanceof` tests",
14733 ),
14734 (Java::EQEQ as u16, 1, "`outCtl`'s comparison"),
14735 ],
14736 );
14737 check_func_space::<JavaParser, _>(src, "foo.java", |space| {
14738 assert_members_score(
14739 &space.spaces[0],
14740 &[
14741 // Was 0, against `outCtl`'s 1 on the same
14742 // `return`-a-predicate shape.
14743 ("outOf", 1, 1),
14744 ("outCtl", 1, 1),
14745 ("inOf", 1, 2),
14746 ("inAnd", 2, 3),
14747 ],
14748 );
14749 });
14750 }
14751
14752 // Groovy carries item 4 as well as the structural pass. `<=>`
14753 // yields -1 / 0 / 1 rather than a boolean, which is why it never
14754 // belonged in the terminal set — that set holds operands — but it
14755 // is a relational operator, and `LTEQGT` is already a condition
14756 // token in Ruby, PHP, C++ and Mozcpp. Groovy was the outlier at 0.
14757 #[cfg(feature = "groovy")]
14758 #[test]
14759 fn groovy_relational_productions_score_outside_a_boolean_slot() {
14760 let src = "class A {
14761 def outIs(x) { def b = x instanceof String; return b }
14762 def outIn(x, l) { def b = x in l; return b }
14763 def outShip(a, c) { def r = a <=> c; return r }
14764 def outCtl(x) { def b = x == 1; return b }
14765 def inIs(x) { if (x instanceof String) { return 1 }; return 0 }
14766 def inIn(x, l) { if (x in l) { return 1 }; return 0 }
14767 def forIn(l) { for (q in l) { }; return 0 }
14768 }";
14769 assert_fixture_spells::<GroovyParser>(
14770 src,
14771 "foo.groovy",
14772 &[
14773 (
14774 Groovy::InstanceofExpression as u16,
14775 2,
14776 "the `instanceof` tests",
14777 ),
14778 (Groovy::MembershipExpression as u16, 2, "the `in` tests"),
14779 (Groovy::SpaceshipExpression as u16, 1, "`outShip`'s `<=>`"),
14780 (
14781 Groovy::ForInStatement as u16,
14782 1,
14783 "`forIn`'s loop header, which spells the same `in`",
14784 ),
14785 (Groovy::EQEQ as u16, 1, "`outCtl`'s comparison"),
14786 ],
14787 );
14788 check_func_space::<GroovyParser, _>(src, "foo.groovy", |space| {
14789 assert_members_score(
14790 &space.spaces[0],
14791 &[
14792 // All three were 0, against `outCtl`'s 1 on the
14793 // identical declare-and-return shape.
14794 ("outIs", 1, 1),
14795 ("outIn", 1, 1),
14796 ("outShip", 1, 1),
14797 ("outCtl", 1, 1),
14798 ("inIs", 1, 2),
14799 ("inIn", 1, 2),
14800 // The loop header is not a membership test and
14801 // stays at 0 — counting the `in` *token* would have
14802 // scored it.
14803 ("forIn", 0, 2),
14804 ],
14805 );
14806 });
14807 }
14808
14809 #[cfg(feature = "kotlin")]
14810 #[test]
14811 fn kotlin_is_and_in_score_outside_a_boolean_slot() {
14812 let src = "class K {
14813 fun outIs(a: Any): Boolean { val b = a is String; return b }
14814 fun outIn(a: Int): Boolean { val b = a in 1..2; return b }
14815 fun outCtl(a: Int): Boolean { val b = a == 1; return b }
14816 fun inIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
14817 fun forIn(l: List<Int>): Int { for (q in l) { }; return 0 }
14818 }";
14819 assert_kotlin_class_members(
14820 src,
14821 &[
14822 (Kotlin::IsExpression as u16, 2, "the `is` tests"),
14823 (Kotlin::InExpression as u16, 1, "`outIn`'s `in` test"),
14824 (
14825 Kotlin::ForStatement as u16,
14826 1,
14827 "`forIn`'s loop header, which spells the same `in`",
14828 ),
14829 (Kotlin::EQEQ as u16, 1, "`outCtl`'s comparison"),
14830 ],
14831 &[
14832 // Both were 0, against `outCtl`'s 1 on the identical
14833 // `val`-and-return shape.
14834 ("outIs", 1, 1),
14835 ("outIn", 1, 1),
14836 ("outCtl", 1, 1),
14837 ("inIs", 1, 2),
14838 ("forIn", 0, 2),
14839 ],
14840 );
14841 }
14842
14843 #[cfg(feature = "ruby")]
14844 #[test]
14845 fn ruby_test_pattern_scores_outside_a_boolean_slot() {
14846 let src = "def out_in(a)
14847 b = a in Integer
14848 b
14849end
14850def out_ctl(a)
14851 b = a == 1
14852 b
14853end
14854def in_in(a)
14855 if a in Integer
14856 return 1
14857 end
14858 0
14859end
14860def for_in(l)
14861 for q in l do end
14862 0
14863end
14864";
14865 assert_fixture_spells::<RubyParser>(
14866 src,
14867 "foo.rb",
14868 &[
14869 (Ruby::TestPattern as u16, 2, "the one-line `in` tests"),
14870 (
14871 Ruby::For as u16,
14872 1,
14873 "`for_in`'s loop header, which spells the same `in`",
14874 ),
14875 (Ruby::EQEQ as u16, 1, "`out_ctl`'s comparison"),
14876 ],
14877 );
14878 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
14879 assert_members_score(
14880 &space,
14881 &[
14882 // Was 0, against `out_ctl`'s 1 on the identical
14883 // assign-and-return shape.
14884 ("out_in", 1, 1),
14885 ("out_ctl", 1, 1),
14886 ("in_in", 1, 2),
14887 ("for_in", 0, 2),
14888 ],
14889 );
14890 });
14891 }
14892
14893 // #1461 item 1. The walrus is an *operand*, not an operator, so it
14894 // joins `python_bool_terminal_kinds!()` and stays slot-scoped —
14895 // `b = (n := g())` outside a predicate is a binding, not a
14896 // decision. Inside one the slot tests `g()`'s truth, and
14897 // `if (n := g()):` scored 0 where the `if g():` it is a
14898 // refactoring of scored 1.
14899 //
14900 // `assignments` is asserted beside `conditions` because the walrus
14901 // is the one construct in the survey that pays on two axes, and
14902 // that is deliberate rather than a §5 double count: ABC's axes are
14903 // independent measurements of the same source, and `if (n := g()):`
14904 // genuinely both binds a name and decides a branch. Without this
14905 // row a later reader has no way to tell the intent from an
14906 // oversight.
14907 #[cfg(feature = "python")]
14908 #[test]
14909 fn python_walrus_is_a_unary_condition() {
14910 let src = "def in_slot(g):
14911 if (n := g()):
14912 return 1
14913 return 0
14914
14915def chain(g, b):
14916 return b and (n := g())
14917
14918def ctrl(g):
14919 if g():
14920 return 1
14921 return 0
14922";
14923 assert_fixture_spells::<PythonParser>(
14924 src,
14925 "foo.py",
14926 &[(Python::NamedExpression as u16, 2, "the walrus bindings")],
14927 );
14928 check_func_space::<PythonParser, _>(src, "foo.py", |space| {
14929 assert_members_score(
14930 &space,
14931 &[
14932 // Was 0, against `ctrl`'s 1 for the same predicate
14933 // without the binding.
14934 ("in_slot", 1, 2),
14935 // The `and`-chain walker is the second, independent
14936 // path (§11): was 1, the bare `b` operand alone.
14937 ("chain", 2, 2),
14938 ("ctrl", 1, 2),
14939 ],
14940 );
14941 for (name, assignments) in [("in_slot", 1u64), ("chain", 1), ("ctrl", 0)] {
14942 assert_eq!(
14943 child_space(&space, name).metrics.abc.assignments(),
14944 assignments,
14945 "{name}: the walrus also pays on the A axis, on purpose"
14946 );
14947 }
14948 });
14949 }
14950
14951 // #1461 item 2. A macro in a boolean slot expands to a boolean
14952 // expression, and `if matches!(x, Some(_))` scored 0 against a
14953 // cyclomatic decision of 1.
14954 //
14955 // `cfg_slot` is in the fixture because the arm's *breadth* is the
14956 // decision, not a side effect: these sets discriminate on slot and
14957 // never on return type, so `cfg!` counts exactly as `matches!`
14958 // does. It is also the construct the corpus actually moves —
14959 // serde's one changed snapshot is `if cfg!(no_underscore_consts)`,
14960 // not a `matches!` — so asserting only `matches!` would leave the
14961 // measured case uncovered.
14962 #[cfg(feature = "rust")]
14963 #[test]
14964 fn rust_macro_invocation_is_a_unary_condition() {
14965 let src = "fn in_slot(x: Option<u8>) -> u8 { if matches!(x, Some(_)) { 1 } else { 0 } }
14966fn cfg_slot() -> u8 { if cfg!(unix) { 1 } else { 0 } }
14967fn chain(x: Option<u8>, b: bool) -> bool { b && matches!(x, Some(_)) }
14968fn ctrl(x: Option<u8>) -> u8 { if x.is_some() { 1 } else { 0 } }
14969";
14970 assert_fixture_spells::<RustParser>(
14971 src,
14972 "foo.rs",
14973 &[(
14974 Rust::MacroInvocation as u16,
14975 3,
14976 "the `matches!` / `cfg!` predicates",
14977 )],
14978 );
14979 check_func_space::<RustParser, _>(src, "foo.rs", |space| {
14980 assert_members_score(
14981 &space,
14982 &[
14983 // Each was 1 — the `else` alone — against `ctrl`'s
14984 // 2 for the same shape with a method call in the
14985 // predicate.
14986 ("in_slot", 2, 2),
14987 ("cfg_slot", 2, 2),
14988 // The `&&`-chain walker, the second independent
14989 // path (§11): was 1, the bare `b` operand alone.
14990 ("chain", 2, 2),
14991 ("ctrl", 2, 2),
14992 ],
14993 );
14994 });
14995 }
14996}
14997
14998/// A comment inside a ternary must not change its ABC conditions
14999/// (#1181).
15000///
15001/// Two opposite defects, one cause: tree-sitter counts a comment among a
15002/// node's children, so it is the operand's previous sibling *and* it
15003/// shifts every positional index.
15004///
15005/// * Languages whose seed asked "is my previous sibling `?` or `:`"
15006/// (C family, PHP, Perl, JS family) read the comment as "not a
15007/// ternary token", flipped the boolean-context seed on for a *branch*
15008/// slot, and **over**-counted: `a ? /*n*/ (b) : c` scored 3 where
15009/// `a ? (b) : c` scores 2.
15010/// * Languages whose branch walk read `child(2)` / `child(4)` (Java,
15011/// C#, Groovy) landed on the comment instead of the operand, never
15012/// inspected it, and **under**-counted: `a ? /*n*/ !b : c` scored 2
15013/// where `a ? !b : c` scores 3.
15014///
15015/// Both slots are now addressed by grammar field. The parenthesised
15016/// operand is the only input that discriminates the first defect and
15017/// the negated operand the only one that discriminates the second —
15018/// existing ternary fixtures use neither.
15019// Gated on the union of the features [`ternary_comment_invariance::cases`]
15020// has a row for, so a build enabling none of them — `--no-default-features
15021// --features go` — drops both tests rather than tripping their
15022// `checked > 0` guards (`.claude/rules/testing.md`, #1286). Both tests
15023// read the same table, so one gate on the module is exact for each.
15024#[cfg(test)]
15025#[cfg(any(
15026 feature = "c",
15027 feature = "cpp",
15028 feature = "csharp",
15029 feature = "groovy",
15030 feature = "java",
15031 feature = "javascript",
15032 feature = "mozcpp",
15033 feature = "mozjs",
15034 feature = "objc",
15035 feature = "perl",
15036 feature = "php",
15037 feature = "typescript"
15038))]
15039mod ternary_comment_invariance {
15040 use crate::test_support::metrics_verbatim;
15041 use crate::{LANG, MetricsOptions};
15042
15043 fn conditions(lang: LANG, source: &str) -> u64 {
15044 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15045 .abc
15046 .conditions_sum()
15047 }
15048
15049 /// `(base, with_comment)` for a parenthesised and a negated branch
15050 /// operand, per language.
15051 fn cases(lang: LANG) -> Option<[(String, String); 2]> {
15052 // `{}` marks the consequence slot; `/*n*/` the inserted comment.
15053 let (template, paren, negated, comment) = match lang {
15054 LANG::Cpp | LANG::C | LANG::Objc | LANG::Mozcpp => {
15055 ("int f(){ int x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
15056 }
15057 LANG::Java => (
15058 "class K{ void f(){ int x = a ? {} : c; } }",
15059 "(b)",
15060 "!b",
15061 "/*n*/ ",
15062 ),
15063 LANG::Csharp => (
15064 "class K{ void f(){ var x = a ? {} : c; } }",
15065 "(b)",
15066 "!b",
15067 "/*n*/ ",
15068 ),
15069 LANG::Groovy => ("def f(){ def x = a ? {} : c }", "(b)", "!b", "/*n*/ "),
15070 LANG::Javascript | LANG::Typescript | LANG::Tsx | LANG::Mozjs => {
15071 ("function f(){ var x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
15072 }
15073 LANG::Php => (
15074 "<?php function f(){ $x = $a ? {} : $c; }",
15075 "($b)",
15076 "!$b",
15077 "/*n*/ ",
15078 ),
15079 // Perl has no block comment: `#` runs to end of line, so the
15080 // comment must carry its own newline.
15081 LANG::Perl => ("sub f { my $x = $a ? {} : $c; }", "($b)", "!$b", "# n\n "),
15082 _ => return None,
15083 };
15084 let build = |operand: &str, with_comment: bool| {
15085 let slot = if with_comment {
15086 format!("{comment}{operand}")
15087 } else {
15088 operand.to_owned()
15089 };
15090 template.replace("{}", &slot)
15091 };
15092 Some([
15093 (build(paren, false), build(paren, true)),
15094 (build(negated, false), build(negated, true)),
15095 ])
15096 }
15097
15098 #[test]
15099 fn a_comment_before_a_branch_operand_changes_nothing() {
15100 let mut checked = 0;
15101 for lang in LANG::into_enum_iter() {
15102 if !lang.is_enabled() {
15103 continue;
15104 }
15105 let Some(pairs) = cases(lang) else { continue };
15106 checked += 1;
15107 for (base, commented) in pairs {
15108 assert_eq!(
15109 conditions(lang, &commented),
15110 conditions(lang, &base),
15111 "{lang:?}: a comment changed the ABC conditions of a ternary\n \
15112 without: {base}\n with: {commented}"
15113 );
15114 }
15115 }
15116 assert!(
15117 checked > 0,
15118 "no ternary language enabled; this test asserted nothing"
15119 );
15120 }
15121
15122 /// The absolute values the invariance test compares against, so a
15123 /// regression that moved *both* sides equally still fails.
15124 ///
15125 /// expected: `a ? (b) : c` counts the `?` marker plus the condition
15126 /// `a` in boolean context = 2. Negating the consequence adds one
15127 /// more, since `!b` establishes boolean content for that slot = 3.
15128 #[test]
15129 fn the_baseline_values_are_two_and_three() {
15130 let mut checked = 0;
15131 for lang in LANG::into_enum_iter() {
15132 if !lang.is_enabled() {
15133 continue;
15134 }
15135 let Some([(paren, _), (negated, _)]) = cases(lang) else {
15136 continue;
15137 };
15138 assert_eq!(
15139 conditions(lang, &paren),
15140 2,
15141 "{lang:?}: parenthesised branch"
15142 );
15143 assert_eq!(conditions(lang, &negated), 3, "{lang:?}: negated branch");
15144 checked += 1;
15145 }
15146 assert!(
15147 checked > 0,
15148 "no ternary language enabled; this test asserted nothing"
15149 );
15150 }
15151}
15152
15153/// A keyword negation must score like its symbolic twin (#1182).
15154///
15155/// `not b` and `!b` are the same negation — they differ in precedence,
15156/// not in meaning, and ABC counts the negation rather than the parse.
15157/// Ruby and Perl tested only the `!` token, so `if not b` scored 0
15158/// against `if !b`'s 1, and a `not` ternary scored 2 against the `!`
15159/// form's 4.
15160///
15161/// Lua and Elixir were checked in the same sweep and were already
15162/// correct: Lua's only negation keyword *is* `not` and it was the token
15163/// being tested, and Elixir reaches the same count by another path.
15164/// They are exercised here so a future edit cannot regress them
15165/// silently. Python counts `not` through its own dispatcher arm and has
15166/// no `!` spelling to compare against.
15167// Gated on the union of every row below — the three `pairs` languages
15168// plus Lua, whose test is separate — so a build enabling none of them
15169// drops the module instead of failing its guards
15170// (`.claude/rules/testing.md`, #1286 / #1411). The three tests do *not*
15171// share a row set, so each carries its own narrower gate: a Lua-only
15172// build must not be asked to run the Ruby/Perl baselines.
15173#[cfg(test)]
15174#[cfg(any(
15175 feature = "elixir",
15176 feature = "lua",
15177 feature = "perl",
15178 feature = "ruby"
15179))]
15180mod keyword_negation_parity {
15181 use crate::test_support::metrics_verbatim;
15182 use crate::{LANG, MetricsOptions};
15183
15184 fn conditions(lang: LANG, source: &str) -> u64 {
15185 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15186 .abc
15187 .conditions_sum()
15188 }
15189
15190 /// `(bang_form, keyword_form)` pairs that must score identically.
15191 #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
15192 fn pairs(lang: LANG) -> Option<Vec<(String, String)>> {
15193 let build =
15194 |t: &str| -> (String, String) { (t.replace("{NOT}", "!"), t.replace("{NOT}", "not ")) };
15195 let templates: &[&str] = match lang {
15196 LANG::Ruby => &[
15197 "def f(b)\n if {NOT}b\n 1\n end\nend\n",
15198 "def f(a, b, c)\n x = a ? ({NOT}b) : ({NOT}c)\nend\n",
15199 "def f(a, b, c)\n x = a ? b : ({NOT}c)\nend\n",
15200 ],
15201 LANG::Perl => &[
15202 "sub f { if ({NOT}$b) { 1; } }",
15203 "sub f { my $x = $a ? ({NOT}$b) : ({NOT}$c); }",
15204 "sub f { my $x = $a ? $b : ({NOT}$c); }",
15205 ],
15206 // Already correct before #1182; pinned so they stay that way.
15207 LANG::Elixir => &["def f(b) do\n if {NOT}b do\n 1\n end\nend\n"],
15208 _ => return None,
15209 };
15210 Some(templates.iter().map(|t| build(t)).collect())
15211 }
15212
15213 #[test]
15214 #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
15215 fn the_not_keyword_scores_like_bang() {
15216 let mut checked = 0;
15217 for lang in LANG::into_enum_iter() {
15218 if !lang.is_enabled() {
15219 continue;
15220 }
15221 let Some(pairs) = pairs(lang) else { continue };
15222 checked += 1;
15223 for (bang, keyword) in pairs {
15224 assert_eq!(
15225 conditions(lang, &keyword),
15226 conditions(lang, &bang),
15227 "{lang:?}: `not` and `!` scored differently\n bang: {bang}\n keyword: {keyword}"
15228 );
15229 }
15230 }
15231 assert!(
15232 checked > 0,
15233 "no language enabled; this test asserted nothing"
15234 );
15235 }
15236
15237 /// The absolute values, so a regression that moved both spellings
15238 /// equally still fails.
15239 ///
15240 /// expected: `if !b` is one condition — the negated bare operand.
15241 /// `a ? (!b) : (!c)` is four: the `?` marker, the condition `a` in
15242 /// boolean context, and one per negated branch operand.
15243 #[test]
15244 #[cfg(any(feature = "perl", feature = "ruby"))]
15245 fn the_baseline_values_are_one_and_four() {
15246 let mut checked = 0;
15247 for (lang, guard, ternary) in [
15248 (
15249 LANG::Ruby,
15250 "def f(b)\n if not b\n 1\n end\nend\n",
15251 "def f(a, b, c)\n x = a ? (not b) : (not c)\nend\n",
15252 ),
15253 (
15254 LANG::Perl,
15255 "sub f { if (not $b) { 1; } }",
15256 "sub f { my $x = $a ? (not $b) : (not $c); }",
15257 ),
15258 ] {
15259 if !lang.is_enabled() {
15260 continue;
15261 }
15262 assert_eq!(conditions(lang, guard), 1, "{lang:?}: `if not b`");
15263 assert_eq!(conditions(lang, ternary), 4, "{lang:?}: `not` ternary");
15264 checked += 1;
15265 }
15266 assert!(
15267 checked > 0,
15268 "no language enabled; this test asserted nothing"
15269 );
15270 }
15271
15272 /// Lua's only negation keyword is `not`, so it has no `!` twin to
15273 /// compare against — its guard is that the keyword counts at all.
15274 #[test]
15275 #[cfg(feature = "lua")]
15276 fn lua_counts_its_only_negation_keyword() {
15277 // Was a silent `if !is_enabled() { return; }`, which made the
15278 // whole test a no-op rather than absent whenever the runtime
15279 // check and the feature disagreed. The `cfg` above is the
15280 // absence half; this is the loud residual half (#1286).
15281 assert!(
15282 LANG::Lua.is_enabled(),
15283 "compiled under `feature = \"lua\"` but `LANG::Lua` reports disabled; \
15284 this test asserted nothing"
15285 );
15286 assert_eq!(
15287 conditions(
15288 LANG::Lua,
15289 "function f(b)\n if not b then return 1 end\nend\n"
15290 ),
15291 1
15292 );
15293 }
15294}
15295
15296/// A numeric literal used as a bare `&&` / `||` operand is a Fitzpatrick
15297/// Rule 9 unary condition in every truthy-valued language (#1379).
15298///
15299/// Ruby's terminal-bool set named `integer` and none of `float` /
15300/// `rational` / `complex`, Elixir's named `integer` and not `float`, and
15301/// Perl's named no numeric kind at all — so `a && 1.0` scored one
15302/// condition where `a && 1` scored two, and in Perl even `$a && 1`
15303/// scored one. Python had both kinds since #772 and is the control the
15304/// other three were brought level with.
15305///
15306/// #1410 closed the six sets #1379 deferred: PHP, Groovy, and the
15307/// name-keyed C / C++ / Mozcpp / Objective-C set, where the omission
15308/// was visible *within* one language — `if (true)` scored one condition
15309/// and `if (1)` none, because `"true"` was in the set and
15310/// `number_literal` was not.
15311///
15312/// The headline claim of each case is a *comparison*: a numeric operand
15313/// must score exactly what an identifier operand scores in the same slot.
15314/// That is what discriminates the defect — with the kind missing from the
15315/// set the numeric form drops while the identifier form does not — and it
15316/// gives a failure message naming both spellings.
15317/// `every_operand_scores_its_recorded_values` then pins the absolute
15318/// numbers, so a regression moving *both* sides equally still fails, and
15319/// carries the cross-metric anchor `.claude/rules/grammar-dispatch.md` §8
15320/// asks for.
15321///
15322/// Two slots per language, because the sets feed two independent walker
15323/// paths (grammar-dispatch §11) and Perl's defect showed in both: the
15324/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
15325/// only the first leaves `perl_count_condition` and `ruby_count_condition`
15326/// untested.
15327///
15328/// The recorded cyclomatic figure is the same 3 for both slots — one file
15329/// space, one function space, one decision — while conditions differ (2
15330/// for the chain, 1 for the predicate, since ABC scores an `if` through
15331/// its predicate rather than the keyword). So §8's `conditions ==
15332/// cyclomatic - 1` identity is chain-specific arithmetic, not a law; what
15333/// generalises is that cyclomatic must not move when only the operand
15334/// spelling does, which is what makes a `conditions` move unambiguously
15335/// ABC's.
15336///
15337/// `for_each_case` guards the two ways this table could decay into
15338/// asserting nothing: an emptied `numerics` slice, and a template that
15339/// loses its `{}` slot (which would make every comparison `x == x`).
15340/// Both were reachable in the first draft and neither failed a test.
15341#[cfg(test)]
15342#[cfg(any(
15343 feature = "ruby",
15344 feature = "elixir",
15345 feature = "perl",
15346 feature = "python",
15347 feature = "lua",
15348 feature = "javascript",
15349 feature = "php",
15350 feature = "groovy",
15351 feature = "c",
15352 feature = "cpp",
15353 feature = "mozcpp",
15354 feature = "objc"
15355))]
15356mod numeric_bool_operands {
15357 use crate::test_support::metrics_verbatim;
15358 use crate::{LANG, MetricsOptions};
15359
15360 /// One fixture shape: a source template with a `{}` operand slot, and
15361 /// the `abc.conditions_sum` / `cyclomatic_sum` every spelling of that
15362 /// operand must produce.
15363 type Slot = (&'static str, u64, u64);
15364
15365 /// A language's two slots, its identifier baseline operand, the
15366 /// truthy literal operands that must score the same, and how many of
15367 /// those there should be. Every row but the C family's lists numeric
15368 /// literals only; that one adds `char_literal`, which is a second
15369 /// integral-literal kind rather than a second numeric spelling.
15370 ///
15371 /// The count is not bookkeeping. `for_each_case` counts *languages*,
15372 /// so trimming a row's operand list back to `&["1"]` — which is
15373 /// exactly the pre-#1379 fixture — left the whole module green when
15374 /// measured. Pinning the length makes that a deliberate two-line
15375 /// edit instead of a silent one.
15376 type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
15377
15378 fn conditions(lang: LANG, source: &str) -> u64 {
15379 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15380 .abc
15381 .conditions_sum()
15382 }
15383
15384 fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
15385 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15386 .cyclomatic
15387 .cyclomatic_sum()
15388 }
15389
15390 /// `([chain_slot, condition_slot], identifier, numerics)` per language.
15391 ///
15392 /// `{}` is the right-hand operand of a two-operand short-circuit chain
15393 /// in the first slot and the whole `if` predicate in the second, so
15394 /// every fixture differs from its own baseline in exactly one token.
15395 ///
15396 /// Each row lists **one spelling per numeric kind the grammar emits**,
15397 /// which is the unit that matters: #1379's misses were sibling rules
15398 /// under a shared supertype, not aliases of one rule.
15399 ///
15400 /// - Ruby: `integer` / `float` / `rational` / `complex`. `1r` parses
15401 /// to `rational(integer)`, `2i` to `complex(integer)` and `1ri` to
15402 /// `complex(rational(integer))` — verified by `bca dump` — and the
15403 /// walker cannot descend into any wrapper, so listing the wrapper
15404 /// is the only way each scores at all. Radix prefixes and `_`
15405 /// separators fold into `integer`.
15406 /// - Perl: all five members of the hidden `_numeric_literals` choice.
15407 /// `0xff` and `1.5e10` are here because the first cut of #1379
15408 /// named only `integer` and `floating_point` and left them scoring
15409 /// 1 against an identifier's 2. `017` is *not* a sixth row — it
15410 /// lexes as `integer`, and the `octal` kind is unreachable.
15411 /// - Elixir: `integer` / `float` / `char`. `?a` is the codepoint 97,
15412 /// a numeric literal wearing a sigil.
15413 /// - PHP: `integer` / `float`. Every radix prefix, `_` separator and
15414 /// exponent folds into those two, and the `float` *type* keyword
15415 /// `Float2` is a third id rendering to the same name that must
15416 /// stay out of the set (#1410).
15417 /// - Groovy: one consolidated `number_literal`, type suffixes
15418 /// (`1L`, `1.5f`, `1G`) included.
15419 /// - C, C++, Mozcpp and Objective-C: one consolidated
15420 /// `number_literal`, plus `char_literal` — not a numeric spelling
15421 /// but a second integral-literal *kind*, since `'c'` has type
15422 /// `int` in C and `char` in C++ and is truthy for the same reason.
15423 /// All four share one name-keyed set and so run the same fixture,
15424 /// which is the only coverage `Mozcpp` can have: it owns no file
15425 /// extension, and `metrics_verbatim` is the one entry point that
15426 /// reaches a `LANG` without going through one.
15427 ///
15428 /// Python, Lua and JavaScript were already correct and ride along as
15429 /// controls — they are what the first three were measured against,
15430 /// and a future edit that breaks them fails here too. Each folds
15431 /// every radix into one `Number` / `integer` kind, checked by
15432 /// measurement.
15433 fn cases(lang: LANG) -> Option<Case> {
15434 Some(match lang {
15435 LANG::Ruby => (
15436 [
15437 ("def f(a)\n a && {}\nend\n", 2, 3),
15438 ("def f\n if {}\n 1\n end\nend\n", 1, 3),
15439 ],
15440 "b",
15441 &["1", "1.0", "1r", "2i", "1ri"],
15442 5,
15443 ),
15444 LANG::Elixir => (
15445 [
15446 ("def f(a) do\n a && {}\nend\n", 2, 3),
15447 ("def f() do\n if {} do\n 1\n end\nend\n", 1, 3),
15448 ],
15449 "b",
15450 &["1", "1.0", "?a"],
15451 3,
15452 ),
15453 LANG::Perl => (
15454 [
15455 ("sub f {\n my $x = $a && {};\n}\n", 2, 3),
15456 ("sub f {\n if ({}) { 1; }\n}\n", 1, 3),
15457 ],
15458 "$b",
15459 &["1", "1.0", "1.5e10", "0xff"],
15460 4,
15461 ),
15462 LANG::Python => (
15463 [
15464 ("def f(a):\n return a and {}\n", 2, 3),
15465 ("def f():\n if {}:\n return 1\n", 1, 3),
15466 ],
15467 "b",
15468 &["1", "1.0", "0xff", "1j"],
15469 4,
15470 ),
15471 LANG::Lua => (
15472 [
15473 ("function f(a)\n return a and {}\nend\n", 2, 3),
15474 ("function f()\n if {} then return 1 end\nend\n", 1, 3),
15475 ],
15476 "b",
15477 &["1", "1.0", "0xff"],
15478 3,
15479 ),
15480 LANG::Javascript => (
15481 [
15482 ("function f(a) {\n return a && {};\n}\n", 2, 3),
15483 ("function f() {\n if ({}) { return 1; }\n}\n", 1, 3),
15484 ],
15485 "b",
15486 &["1", "1.0", "0xff"],
15487 3,
15488 ),
15489 LANG::Php => (
15490 [
15491 ("<?php\nfunction f($a) {\n return $a && {};\n}\n", 2, 3),
15492 ("<?php\nfunction f() {\n if ({}) { return 1; }\n}\n", 1, 3),
15493 ],
15494 "$b",
15495 &["1", "1.0"],
15496 2,
15497 ),
15498 LANG::Groovy => (
15499 [
15500 ("def f(a) {\n return a && {}\n}\n", 2, 3),
15501 ("def f() {\n if ({}) { return 1 }\n}\n", 1, 3),
15502 ],
15503 "b",
15504 &["1"],
15505 1,
15506 ),
15507 LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc => (
15508 [
15509 ("int f(int a) {\n return a && {};\n}\n", 2, 3),
15510 ("int f() {\n if ({}) { return 1; }\n}\n", 1, 3),
15511 ],
15512 "b",
15513 &["1", "'c'"],
15514 2,
15515 ),
15516 _ => return None,
15517 })
15518 }
15519
15520 /// Runs `check` once per enabled language that has a case, having
15521 /// first established that the case can still assert something.
15522 ///
15523 /// Three guards, all of which had to be added after the first draft
15524 /// shipped without them and a measured perturbation of each left the
15525 /// whole module green:
15526 ///
15527 /// - **`checked > 0`** is the non-vacuity half of the rule in
15528 /// `.claude/rules/testing.md`. The `#[cfg(any(feature = …))]` on
15529 /// this module makes the tests *absent* when no truthy-valued
15530 /// language is compiled in; this catches the residual case where
15531 /// the runtime `is_enabled()` check stops agreeing with the feature
15532 /// it compiled under.
15533 /// - **the `numerics` list is non-empty and keeps its recorded
15534 /// length** — `checked` counts *languages*, so trimming a row's
15535 /// numeric list back to `&["1"]` (the pre-#1379 fixture) or
15536 /// emptying it left every test passing when measured, with that
15537 /// language's coverage deleted. Two assertions, because the
15538 /// length check passes an emptied row whose count was zeroed with
15539 /// it.
15540 /// - **every template keeps its `{}`** — without the slot,
15541 /// `str::replace` is a no-op, baseline and candidate are computed
15542 /// from the same string, and the comparison degenerates to
15543 /// `x == x`.
15544 ///
15545 /// Sharing one driver means no test can lose any of the three.
15546 fn for_each_case(check: impl Fn(LANG, Case)) {
15547 let mut checked = 0;
15548 for lang in LANG::into_enum_iter() {
15549 if !lang.is_enabled() {
15550 continue;
15551 }
15552 let Some(case @ (slots, _, numerics, expected_kinds)) = cases(lang) else {
15553 continue;
15554 };
15555 // Emptying a row is the half the length check below cannot
15556 // see: `&[]` against an `expected_kinds` of 0 agrees with
15557 // itself, the inner loops run zero times, and `checked` —
15558 // which counts languages, not spellings — still reads
15559 // satisfied.
15560 assert!(
15561 !numerics.is_empty(),
15562 "{lang:?}: the numeric-operand list is empty; this language asserted nothing"
15563 );
15564 assert_eq!(
15565 numerics.len(),
15566 expected_kinds,
15567 "{lang:?}: the numeric-operand list no longer covers one spelling \
15568 per grammar numeric kind"
15569 );
15570 for (template, _, _) in slots {
15571 assert!(
15572 template.contains("{}"),
15573 "{lang:?}: template lost its `{{}}` operand slot: {template}"
15574 );
15575 }
15576 check(lang, case);
15577 checked += 1;
15578 }
15579 assert!(
15580 checked > 0,
15581 "no truthy-valued language enabled; this test asserted nothing"
15582 );
15583 }
15584
15585 #[test]
15586 fn a_numeric_operand_scores_like_an_identifier_operand() {
15587 for_each_case(|lang, (slots, identifier, numerics, _)| {
15588 for (template, _, _) in slots {
15589 let baseline = conditions(lang, &template.replace("{}", identifier));
15590 for numeric in numerics {
15591 let source = template.replace("{}", numeric);
15592 let scored = conditions(lang, &source);
15593 assert_eq!(
15594 scored, baseline,
15595 "{lang:?}: `{numeric}` scored {scored} unary conditions against \
15596 `{identifier}`'s {baseline}\n source: {source}"
15597 );
15598 }
15599 }
15600 });
15601 }
15602
15603 /// The absolute anchor under the comparison above: every operand
15604 /// spelling must produce the slot's recorded `conditions`, and must
15605 /// leave `cyclomatic` alone. The second half is what rules out a
15606 /// regression that moved both metrics together.
15607 #[test]
15608 fn every_operand_scores_its_recorded_values() {
15609 for_each_case(|lang, (slots, identifier, numerics, _)| {
15610 for (template, expected_conditions, expected_cyclomatic) in slots {
15611 for operand in std::iter::once(identifier).chain(numerics.iter().copied()) {
15612 let source = template.replace("{}", operand);
15613 assert_eq!(
15614 conditions(lang, &source),
15615 expected_conditions,
15616 "{lang:?}: `{operand}` conditions\n source: {source}"
15617 );
15618 assert_eq!(
15619 cyclomatic_sum(lang, &source),
15620 expected_cyclomatic,
15621 "{lang:?}: `{operand}` cyclomatic_sum\n source: {source}"
15622 );
15623 }
15624 }
15625 });
15626 }
15627}
15628
15629/// A relational construct with its own grammar production must score
15630/// like an identifier in the same boolean slot (#1449, #1461).
15631///
15632/// Every language here spells at least one boolean test as a dedicated
15633/// node rather than as a `binary_expression`, so no comparison-token
15634/// arm ever sees it. A kind missing from `<lang>_bool_terminal_kinds!()`
15635/// scores **zero**, silently, and zero is indistinguishable from a
15636/// construct that legitimately scores zero — which is why the headline
15637/// claim of each case is a *comparison* against an identifier control
15638/// in the identical slot rather than an absolute number.
15639///
15640/// The constructs, and the route each fix took:
15641///
15642/// - **Groovy** `a in l` / `a !in l` (`membership_expression`) joins
15643/// the terminal set. The other four — `a === b` / `a !== b`
15644/// (`identity_expression`) and `s =~ /p/` / `s ==~ /p/`
15645/// (`regex_find_expression`, `regex_match_expression`) — are counted
15646/// as operator tokens in `groovy_count_token_condition` instead, so
15647/// they also score outside a boolean slot as `==` already did. Both
15648/// macro and arm carry the reasoning; the rule that matters here is
15649/// that a construct takes exactly one of the two routes, never both
15650/// (`.claude/rules/grammar-dispatch.md` §5).
15651/// - **Perl** `/^#/` (`pattern_matcher`) and `m{^#}`
15652/// (`pattern_matcher_m`), the two spellings of a match against the
15653/// implicit `$_`. They are sibling rules, not aliases, so both are
15654/// listed — a fixture carrying only the first would have left the
15655/// `m{}` half at zero and read as covered.
15656/// - **Ruby** `a in Integer` (`test_pattern`), the one-line pattern
15657/// test. Decided against `match_pattern` (`expr => pat`), which
15658/// raises rather than yielding a boolean.
15659/// - **Objective-C** `@available(iOS 13.0, *)` and its
15660/// `__builtin_available` synonym (`available_expression`), the runtime
15661/// OS-version check. Not relational, but boolean by definition and
15662/// invisible to every comparison-token arm, so it measured short in
15663/// exactly the same way (#1457). Its entry lands in the *name*-keyed
15664/// `cpp_bool_terminal_kinds!()` that C, C++ and Mozcpp share, which is
15665/// why `the_c_family_grammars_do_not_emit_available_expression` below
15666/// pins the inertness the other three rely on.
15667///
15668/// Rust's `matches!(…)` measures short in the same way, and is
15669/// deliberately **not** here: its fix is `macro_invocation`, which also
15670/// catches `cfg!` / `dbg!` / `todo!`, and the one corpus snapshot it
15671/// moves is a `cfg!` rather than a `matches!`. That breadth is what
15672/// #1461 item 2 exists to decide, so it is left to that issue rather
15673/// than settled by inclusion here.
15674///
15675/// Two slots per language, because the sets feed two independent walker
15676/// paths (§11) and every construct above measured short in **both**: the
15677/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
15678/// only one leaves the other untested.
15679///
15680/// Each row also carries the construct's negated spelling. That is not
15681/// redundancy: `<lang>_inspect_container` is a *third* consumer of every
15682/// terminal set, reached only after a `(…)` or `!…` wrapper is peeled,
15683/// and it is the consumer that decides whether the peeled operand sits
15684/// in boolean context at all. Without a negated row, Groovy's and
15685/// Rust's `inspect_container` arms were the site no fixture here
15686/// exercised.
15687///
15688/// Even with it, no language exercises all three consumers through
15689/// these two slots, and which one is missed differs by language —
15690/// Perl's `perl_count_condition` (reached only from a ternary or a
15691/// C-style `for` header) and Ruby's `ruby_count_unary_conditions`
15692/// (unreachable here because `in` binds looser than `&&`, so the chain
15693/// slot must parenthesise and routes through `inspect_container`
15694/// instead). Both are covered elsewhere in this file; the gap is
15695/// recorded rather than papered over, because a reader comparing the
15696/// slot count to the consumer count will otherwise assume it is three.
15697///
15698/// The `cyclomatic` half of `every_construct_scores_its_recorded_values`
15699/// is what rules out a regression that moved both metrics together:
15700/// cyclomatic must not move when only the operand spelling does, which
15701/// is what makes a `conditions` move unambiguously ABC's. The recorded
15702/// figures differ per slot (Rust's chain slot carries the extra `&&`
15703/// decision), so each slot pins its own pair rather than sharing one.
15704// Gated on the union of the three languages with rows, so a build
15705// enabling none of them drops the module instead of failing its
15706// guards (`.claude/rules/testing.md`, #1286 / #1411).
15707#[cfg(test)]
15708#[cfg(any(
15709 feature = "groovy",
15710 feature = "perl",
15711 feature = "ruby",
15712 feature = "objc"
15713))]
15714mod own_production_bool_constructs {
15715 use crate::test_support::metrics_verbatim;
15716 use crate::{LANG, MetricsOptions};
15717
15718 /// One fixture shape: a source template with a `{}` slot for the
15719 /// whole boolean expression, and the `abc.conditions_sum` /
15720 /// `cyclomatic_sum` every spelling in that slot must produce.
15721 type Slot = (&'static str, u64, u64);
15722
15723 /// A language's two slots, its identifier control, the constructs
15724 /// that must score the same, and how many of those there should be.
15725 ///
15726 /// The count is not bookkeeping. `for_each_case` counts *languages*,
15727 /// so trimming Groovy's list back to a single construct — or
15728 /// dropping Perl's `m{}` half, which is a separate grammar rule from
15729 /// its `//` half — leaves the module green with that coverage
15730 /// deleted. Pinning the length makes such a trim a deliberate
15731 /// two-line edit instead of a silent one.
15732 type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
15733
15734 fn conditions(lang: LANG, source: &str) -> u64 {
15735 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15736 .abc
15737 .conditions_sum()
15738 }
15739
15740 fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
15741 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
15742 .cyclomatic
15743 .cyclomatic_sum()
15744 }
15745
15746 /// `([chain_slot, predicate_slot], identifier, constructs)` per
15747 /// language.
15748 ///
15749 /// `{}` is the left-hand operand of a two-operand short-circuit
15750 /// chain in the first slot and the whole `if` predicate in the
15751 /// second, so every fixture differs from its own control in exactly
15752 /// the construct under test.
15753 ///
15754 /// Ruby's chain slot parenthesises the slot because `in` binds
15755 /// looser than `&&` there; the control is parenthesised identically,
15756 /// so the parens cancel out of the comparison and
15757 /// `ruby_inspect_container` unwraps them for both sides alike.
15758 fn cases(lang: LANG) -> Option<Case> {
15759 Some(match lang {
15760 LANG::Groovy => (
15761 [
15762 ("def f(a, b, l, s) {\n return {} && b\n}\n", 2, 3),
15763 ("def f(a, b, l, s) {\n if ({}) { return 1 }\n}\n", 1, 3),
15764 ],
15765 "b",
15766 &[
15767 "a in l",
15768 "a !in l",
15769 "a === b",
15770 "a !== b",
15771 "s =~ /p/",
15772 "s ==~ /p/",
15773 "!(a in l)",
15774 // The indexing / navigation kinds added by #1466.
15775 // These two are the cyclomatic-neutral ones, so
15776 // they belong in this module's same-as-the-control
15777 // shape. The three *null-safe* spellings (`a?.b`,
15778 // `a??.b`, `l?[0]`) each short-circuit on a null
15779 // receiver and so add a cyclomatic decision, which
15780 // a row whose contract is "the control's
15781 // cyclomatic" cannot express; they get their own
15782 // test,
15783 // `groovy_safe_navigation_closes_the_two_below_gap`,
15784 // which anchors them on that axis explicitly.
15785 "l[0]",
15786 "a.@b",
15787 ],
15788 9,
15789 ),
15790 LANG::Perl => (
15791 [
15792 ("sub f {\n my $x = {} && $b;\n}\n", 2, 3),
15793 ("sub f {\n if ({}) { 1; }\n}\n", 1, 3),
15794 ],
15795 "$a",
15796 &["/^#/", "m{^#}", "!/^#/"],
15797 3,
15798 ),
15799 LANG::Ruby => (
15800 [
15801 ("def f(a)\n ({}) && b\nend\n", 2, 3),
15802 ("def f(a)\n if {}\n 1\n end\nend\n", 1, 3),
15803 ],
15804 "a",
15805 &["a in Integer", "!(a in Integer)"],
15806 2,
15807 ),
15808 // Objective-C's `@available` is not relational like the three
15809 // above, but it reaches the terminal set by the same route: a
15810 // dedicated `available_expression` production whose value is a
15811 // boolean, seen by no comparison-token arm (#1457). Both
15812 // spellings are listed because they are alternatives of one
15813 // rule's leading token rather than two rules — so unlike
15814 // Perl's `m{}` above they cannot drift apart, and the second
15815 // row is here to keep that claim measured rather than assumed.
15816 LANG::Objc => (
15817 [
15818 ("int f(int a) {\n return {} && b;\n}\n", 2, 3),
15819 ("int f() {\n if ({}) { return 1; }\n}\n", 1, 3),
15820 ],
15821 "a",
15822 &[
15823 "@available(iOS 13.0, *)",
15824 "__builtin_available(macOS 10.15, *)",
15825 "!@available(iOS 13.0, *)",
15826 ],
15827 3,
15828 ),
15829 _ => return None,
15830 })
15831 }
15832
15833 /// Runs `check` once per enabled language that has a case, having
15834 /// first established that the case can still assert something.
15835 ///
15836 /// The four guards mirror `numeric_bool_operands::for_each_case`,
15837 /// where each was added only after a measured perturbation of it
15838 /// left that module green: `checked > 0` for the runtime half of
15839 /// the feature gate, a non-empty construct list and its recorded
15840 /// count so a trimmed row cannot vanish silently, and the `{}` slot
15841 /// so `str::replace` does not degenerate into comparing a string
15842 /// with itself.
15843 fn for_each_case(check: impl Fn(LANG, Case)) {
15844 let mut checked = 0;
15845 for lang in LANG::into_enum_iter() {
15846 if !lang.is_enabled() {
15847 continue;
15848 }
15849 let Some(case @ (slots, _, constructs, expected_constructs)) = cases(lang) else {
15850 continue;
15851 };
15852 // Both halves are needed, and the second does not imply the
15853 // first. The length check alone only asserts that the list
15854 // and its recorded count *agree*, so emptying a row and
15855 // setting its count to 0 — the shape a careless "the test
15856 // failed, fix the number" edit takes — deleted that
15857 // language's coverage with both tests still green when
15858 // measured.
15859 assert!(
15860 !constructs.is_empty(),
15861 "{lang:?}: the construct list is empty; this language asserted nothing"
15862 );
15863 assert_eq!(
15864 constructs.len(),
15865 expected_constructs,
15866 "{lang:?}: the construct list no longer covers every own-production \
15867 boolean spelling this language was fixed for"
15868 );
15869 for (template, _, _) in slots {
15870 assert!(
15871 template.contains("{}"),
15872 "{lang:?}: template lost its `{{}}` slot: {template}"
15873 );
15874 }
15875 check(lang, case);
15876 checked += 1;
15877 }
15878 assert!(
15879 checked > 0,
15880 "no language with an own-production boolean construct enabled; \
15881 this test asserted nothing"
15882 );
15883 }
15884
15885 #[test]
15886 fn an_own_production_construct_scores_like_an_identifier() {
15887 for_each_case(|lang, (slots, identifier, constructs, _)| {
15888 for (template, _, _) in slots {
15889 let baseline = conditions(lang, &template.replace("{}", identifier));
15890 for construct in constructs {
15891 let source = template.replace("{}", construct);
15892 let scored = conditions(lang, &source);
15893 assert_eq!(
15894 scored, baseline,
15895 "{lang:?}: `{construct}` scored {scored} conditions against \
15896 `{identifier}`'s {baseline}\n source: {source}"
15897 );
15898 }
15899 }
15900 });
15901 }
15902
15903 /// Groovy's three null-safe spellings, on the cyclomatic axis.
15904 ///
15905 /// They cannot ride the rows above, whose contract is "scores the
15906 /// control's `conditions` *and* the control's `cyclomatic`":
15907 /// `groovy_bool_terminal_kinds!()` does not move cyclomatic, but
15908 /// `?.` (`QMARKDOT`), `??.` (`QMARKQMARKDOT`) and `?[`
15909 /// (`QMARKLBRACK`) are already cyclomatic decisions in their own
15910 /// right (`src/metrics/cyclomatic/groovy.rs`), so each spelling
15911 /// scores the control's conditions against the control's cyclomatic
15912 /// **plus one**. That is the whole reason this family was the worst
15913 /// case in #1466: before the fix ABC sat *two* below its own
15914 /// decision count on `if (a?.b)`, against one below for every other
15915 /// spelling.
15916 ///
15917 /// `l?[0]` joined the family in #1471. It sat in the neutral rows
15918 /// above until then, passing only because Groovy cyclomatic had no
15919 /// `?[` arm — a fixture that would have rejected the fix for the
15920 /// very gap it recorded.
15921 ///
15922 /// Asserting the offset rather than the bare conditions is what
15923 /// makes this a §5 double-count guard as well. If a later change
15924 /// added `QMARKDOT` to `groovy_count_token_condition`'s arm while
15925 /// the wrapper stayed in the terminal set, `conditions` would go to
15926 /// `control + 1` and this test — not a snapshot — would say so.
15927 #[test]
15928 #[cfg(feature = "groovy")]
15929 fn groovy_safe_navigation_closes_the_two_below_gap() {
15930 for (template, control_conditions, control_cyclomatic) in [
15931 ("def f(a, b, l) {\n return {} && b\n}\n", 2, 3),
15932 ("def f(a, b, l) {\n if ({}) { return 1 }\n}\n", 1, 3),
15933 ] {
15934 let control = template.replace("{}", "b");
15935 assert_eq!(conditions(LANG::Groovy, &control), control_conditions);
15936 assert_eq!(cyclomatic_sum(LANG::Groovy, &control), control_cyclomatic);
15937
15938 for spelling in ["a?.b", "a??.b", "l?[0]"] {
15939 let source = template.replace("{}", spelling);
15940 assert_eq!(
15941 conditions(LANG::Groovy, &source),
15942 control_conditions,
15943 "`{spelling}` conditions\n source: {source}"
15944 );
15945 assert_eq!(
15946 cyclomatic_sum(LANG::Groovy, &source),
15947 control_cyclomatic + 1,
15948 "`{spelling}` cyclomatic_sum: the navigation operator's own \
15949 decision has moved\n source: {source}"
15950 );
15951 }
15952 }
15953 }
15954
15955 /// The `!` operand is read by field, so an `extra` cannot hide it.
15956 ///
15957 /// `groovy_wrapper_operand` took `child(1)` until #1466, which is
15958 /// the `!` operand only when nothing sits between the two. A comment
15959 /// is an `extra` and occupies that slot, so `if (! /*c*/ a)` scored
15960 /// zero conditions where `if (!a)` scored one — the positional-read
15961 /// class grammar-dispatch §3 is about, and the observable half of
15962 /// the peel rewrite.
15963 ///
15964 /// `parenthesized_expression` names nothing in node-types.json, so
15965 /// it keeps the positional read and keeps the bug; that half is
15966 /// pinned here as a *measured* gap rather than left to be discovered
15967 /// as a surprise. Kotlin records the identical pair
15968 /// (`kotlin_wrapper_operand`).
15969 #[test]
15970 #[cfg(feature = "groovy")]
15971 fn groovy_negation_operand_survives_an_interposed_comment() {
15972 let template = "def f(a) {\n if ({}) { return 1 }\n}\n";
15973
15974 assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "!a")), 1);
15975 assert_eq!(
15976 conditions(LANG::Groovy, &template.replace("{}", "! /*c*/ a")),
15977 1,
15978 "the `!` operand is being read positionally again; a comment displaces it"
15979 );
15980
15981 assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "(a)")), 1);
15982 assert_eq!(
15983 conditions(LANG::Groovy, &template.replace("{}", "( /*c*/ a)")),
15984 0,
15985 "`parenthesized_expression` now survives an interposed comment — if the \
15986 grammar gained a field for its inner expression, read it and delete this"
15987 );
15988 }
15989
15990 /// The arithmetic unary operators stay out of the boolean slot.
15991 ///
15992 /// `groovy_count_condition` routed every `unary_expression` to the
15993 /// peel while the peel handled only the `!` spelling, so the arm
15994 /// claimed `~a` / `-a` / `+a` and dropped them
15995 /// (grammar-dispatch §7). #1466 made the arm ask the peel, which
15996 /// removes the divergence without moving a number — so this test
15997 /// cannot be verified by reverting the production change, and is
15998 /// here to pin the *answer* those three spellings give against a
15999 /// future peel that starts accepting them by accident.
16000 #[test]
16001 #[cfg(feature = "groovy")]
16002 fn groovy_arithmetic_unary_is_not_a_condition() {
16003 let template = "def f(a) {\n if ({}) { return 1 }\n}\n";
16004
16005 assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "a")), 1);
16006 for spelling in ["~a", "-a", "+a"] {
16007 assert_eq!(
16008 conditions(LANG::Groovy, &template.replace("{}", spelling)),
16009 0,
16010 "`{spelling}` is arithmetic, not a boolean operand"
16011 );
16012 }
16013 }
16014
16015 /// The two Perl spellings must remain two distinct grammar kinds.
16016 ///
16017 /// `perl_bool_terminal_kinds!()` lists `PatternMatcher` **and**
16018 /// `PatternMatcherM` on the stated grounds that `/^#/` and `m{^#}`
16019 /// are sibling rules rather than aliases of one. Nothing above pins
16020 /// that: if a grammar bump collapsed `m{}` onto `pattern_matcher`,
16021 /// both fixtures would keep scoring through the surviving entry,
16022 /// the `PatternMatcherM` arm would become dead code, and every test
16023 /// in this module would stay green — the silent-drift shape lesson
16024 /// 34 and grammar-dispatch §2 exist to catch.
16025 ///
16026 /// Asserting the ids differ is not enough on its own, since two
16027 /// distinct enum variants prove nothing about what the parser
16028 /// emits. So each spelling is parsed and required to produce its
16029 /// own kind and not the other's.
16030 #[test]
16031 #[cfg(feature = "perl")]
16032 fn the_two_perl_match_spellings_are_distinct_kinds() {
16033 use crate::{ParserTrait, Perl, PerlParser};
16034 use big_code_analysis_ast::test_support::ast_has_kind_id;
16035
16036 assert_ne!(
16037 Perl::PatternMatcher as u16,
16038 Perl::PatternMatcherM as u16,
16039 "the two spellings share one kind id; one terminal-set entry is dead"
16040 );
16041
16042 for (src, present, absent) in [
16043 (
16044 "sub f { if (/^#/) { 1; } }",
16045 ("pattern_matcher", Perl::PatternMatcher as u16),
16046 ("pattern_matcher_m", Perl::PatternMatcherM as u16),
16047 ),
16048 (
16049 "sub f { if (m{^#}) { 1; } }",
16050 ("pattern_matcher_m", Perl::PatternMatcherM as u16),
16051 ("pattern_matcher", Perl::PatternMatcher as u16),
16052 ),
16053 ] {
16054 let parser = PerlParser::new(
16055 src.as_bytes().to_vec(),
16056 &std::path::PathBuf::from("f.pl"),
16057 None,
16058 );
16059 let (present_name, present_id) = present;
16060 let (absent_name, absent_id) = absent;
16061 assert!(
16062 ast_has_kind_id(&parser, present_id),
16063 "`{src}` no longer emits `{present_name}`"
16064 );
16065 assert!(
16066 !ast_has_kind_id(&parser, absent_id),
16067 "`{src}` now also emits `{absent_name}`; the two spellings have \
16068 collapsed and one `perl_bool_terminal_kinds!()` entry is dead"
16069 );
16070 }
16071 }
16072
16073 /// `"available_expression"` must stay an Objective-C-only kind.
16074 ///
16075 /// Alone among the terminal sets, `cpp_bool_terminal_kinds!()` keys
16076 /// on node-kind *names* so that C, C++, Mozcpp and Objective-C can
16077 /// share one list despite assigning different ids to the same kinds
16078 /// (#720 / #732). The price is that every entry is live in all four:
16079 /// the Objective-C rows above say nothing about what the addition
16080 /// did to the other three, and a grammar bump that gave any of them
16081 /// an `available_expression` node would start counting it with no
16082 /// test anywhere noticing.
16083 ///
16084 /// The Objective-C half is not decoration. Without it a fixture that
16085 /// stopped parsing — or a typo'd kind name — would leave the
16086 /// negative assertions passing for the wrong reason. Both halves
16087 /// were verified by perturbing the probed kind name: to a typo,
16088 /// which fails the positive, and to `if_statement`, which fails the
16089 /// negatives.
16090 #[test]
16091 #[cfg(all(
16092 feature = "objc",
16093 any(feature = "c", feature = "cpp", feature = "mozcpp")
16094 ))]
16095 fn the_c_family_grammars_do_not_emit_available_expression() {
16096 use crate::ParserTrait;
16097
16098 const SOURCE: &str = "int f(int a) {\n if (@available(iOS 13.0, *)) { return 1; }\n}\n";
16099
16100 fn emits<P: ParserTrait>(path: &str) -> bool {
16101 let parser = P::new(
16102 SOURCE.as_bytes().to_vec(),
16103 &std::path::PathBuf::from(path),
16104 None,
16105 );
16106 parser
16107 .root()
16108 .preorder()
16109 .any(|node| node.kind() == "available_expression")
16110 }
16111
16112 assert!(
16113 emits::<crate::ObjcParser>("f.m"),
16114 "the fixture no longer parses to an `available_expression`; every \
16115 Objective-C row above is now measuring some other node"
16116 );
16117
16118 // The siblings are `#[cfg]`-gated array *elements* rather than
16119 // conditional pushes, so the `any(…)` half of this test's gate
16120 // makes the list non-empty by construction: a build reaching
16121 // this line always has at least one row. That is the same
16122 // non-vacuity guarantee the `checked > 0` counters elsewhere in
16123 // this module buy at runtime, moved to compile time because here
16124 // the row set is fixed rather than iterated over `LANG`.
16125 let c_family = [
16126 #[cfg(feature = "c")]
16127 ("C", emits::<crate::CParser>("f.c")),
16128 #[cfg(feature = "cpp")]
16129 ("C++", emits::<crate::CppParser>("f.cpp")),
16130 #[cfg(feature = "mozcpp")]
16131 ("Mozcpp", emits::<crate::MozcppParser>("f.cpp")),
16132 ];
16133
16134 for (language, emitted) in c_family {
16135 assert!(
16136 !emitted,
16137 "{language} now emits `available_expression`, so the shared \
16138 `cpp_bool_terminal_kinds!()` entry is no longer inert there \
16139 and its ABC conditions have moved"
16140 );
16141 }
16142 }
16143
16144 /// The absolute anchor under the comparison above: every spelling
16145 /// must produce the slot's recorded `conditions`, and must leave
16146 /// `cyclomatic` alone.
16147 #[test]
16148 fn every_construct_scores_its_recorded_values() {
16149 for_each_case(|lang, (slots, identifier, constructs, _)| {
16150 for (template, expected_conditions, expected_cyclomatic) in slots {
16151 for spelling in std::iter::once(identifier).chain(constructs.iter().copied()) {
16152 let source = template.replace("{}", spelling);
16153 assert_eq!(
16154 conditions(lang, &source),
16155 expected_conditions,
16156 "{lang:?}: `{spelling}` conditions\n source: {source}"
16157 );
16158 assert_eq!(
16159 cyclomatic_sum(lang, &source),
16160 expected_cyclomatic,
16161 "{lang:?}: `{spelling}` cyclomatic_sum\n source: {source}"
16162 );
16163 }
16164 }
16165 });
16166 }
16167}
16168
16169/// A Perl statement modifier must score like the block form it is
16170/// shorthand for (#1464).
16171///
16172/// `return 1 if $x;` and `if ($x) { return 1; }` are the same decision
16173/// written two ways, and the modifier is the idiomatic Perl spelling —
16174/// `next unless $ok;`, `warn "..." if $debug;`. Perl's *cyclomatic*
16175/// dispatcher already counted all six modifier kinds, so this was a
16176/// straight ABC undercount against Perl's own decision count rather
16177/// than a policy disagreement between the two metrics.
16178///
16179/// Each pair is asserted equal **and** equal to a literal, so a
16180/// regression that zeroed both spellings still fails. The fixtures are
16181/// spell-anchored because the modifier is the only thing in them that
16182/// scores a condition: deleting `if $x` from the modifier fixture would
16183/// otherwise turn it into a silent copy of nothing at all
16184/// (`.claude/rules/testing.md`, "Perturb the fixture as well as the
16185/// production line").
16186#[cfg(test)]
16187#[cfg(feature = "perl")]
16188mod perl_statement_modifier_parity {
16189 use crate::test_support::{assert_perl_fixture_spells, metrics_verbatim};
16190 use crate::{LANG, MetricsOptions, Perl};
16191
16192 fn conditions(source: &str) -> u64 {
16193 metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default())
16194 .abc
16195 .conditions_sum()
16196 }
16197
16198 /// `(keyword, kind id, kind name)` — the four modifier keywords
16199 /// whose grammar slot is a `condition`. `for` / `foreach` is the
16200 /// fifth spelling and is covered by its own test below; `when` is
16201 /// the sixth and has no fixture at all, being unreachable from
16202 /// valid Perl (see `perl_walk_statement_modifier`).
16203 const CONDITION_MODIFIERS: [(&str, u16, &str); 4] = [
16204 ("if", Perl::IfSimpleStatement as u16, "if_simple_statement"),
16205 (
16206 "unless",
16207 Perl::UnlessSimpleStatement as u16,
16208 "unless_simple_statement",
16209 ),
16210 (
16211 "while",
16212 Perl::WhileSimpleStatement as u16,
16213 "while_simple_statement",
16214 ),
16215 (
16216 "until",
16217 Perl::UntilSimpleStatement as u16,
16218 "until_simple_statement",
16219 ),
16220 ];
16221
16222 #[test]
16223 fn a_modifier_scores_like_its_block_form() {
16224 assert!(
16225 LANG::Perl.is_enabled(),
16226 "compiled under `feature = \"perl\"` but `LANG::Perl` reports disabled; \
16227 this test asserted nothing"
16228 );
16229 for (keyword, kind, name) in CONDITION_MODIFIERS {
16230 let modifier = format!("sub f {{ my $x = shift; return 1 {keyword} $x; }}");
16231 let block = format!("sub f {{ my $x = shift; {keyword} ($x) {{ return 1; }} }}");
16232 assert_perl_fixture_spells(&modifier, &[(kind, 1, name)]);
16233 assert_eq!(
16234 conditions(&modifier),
16235 conditions(&block),
16236 "`{keyword}` modifier and block form disagree\n modifier: {modifier}\n block: {block}"
16237 );
16238 assert_eq!(
16239 conditions(&modifier),
16240 1,
16241 "`{keyword}` modifier: the bare scalar predicate is one condition"
16242 );
16243 }
16244 }
16245
16246 /// The modifier's `condition` field goes through the shared
16247 /// condition classifier, not a flat `+1`, so a compound predicate
16248 /// keeps its sub-structure — three shapes the block form already
16249 /// scores above and below one.
16250 #[test]
16251 fn a_modifier_condition_keeps_its_substructure() {
16252 for (predicate, expected) in [("!$x", 1), ("($x)", 1), ("$x && $x", 2), ("$x > 2", 1)] {
16253 let modifier = format!("sub f {{ my $x = shift; return 1 if {predicate}; }}");
16254 let block = format!("sub f {{ my $x = shift; if ({predicate}) {{ return 1; }} }}");
16255 assert_perl_fixture_spells(
16256 &modifier,
16257 &[(Perl::IfSimpleStatement as u16, 1, "if_simple_statement")],
16258 );
16259 assert_eq!(
16260 conditions(&modifier),
16261 expected,
16262 "`if {predicate}` modifier\n source: {modifier}"
16263 );
16264 assert_eq!(
16265 conditions(&block),
16266 expected,
16267 "`if ({predicate})` block form\n source: {block}"
16268 );
16269 }
16270 }
16271
16272 /// The `for` / `foreach` modifier iterates a list and is not a
16273 /// boolean test — the grammar names its slot `list`, not
16274 /// `condition`. Pinned against the `if` modifier so the zero reads
16275 /// as a decision rather than as an arm nothing reaches.
16276 ///
16277 /// Measured: simply *adding* `ForSimpleStatement` to the dispatch
16278 /// arm does not fail this test, because the walker reads the slot by
16279 /// field name and `for_simple_statement` exposes no `condition` —
16280 /// the exclusion is structural, not a listed omission. What it does
16281 /// catch is a rework that reaches the slot some other way (a child
16282 /// index, the `list` field) or a grammar that renames the field.
16283 #[test]
16284 fn the_for_modifier_is_not_a_condition() {
16285 for keyword in ["for", "foreach"] {
16286 let source = format!("sub f {{ my $x = shift; print 5 {keyword} @$x; }}");
16287 assert_perl_fixture_spells(
16288 &source,
16289 &[(Perl::ForSimpleStatement as u16, 1, "for_simple_statement")],
16290 );
16291 let abc =
16292 metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default()).abc;
16293 assert_eq!(abc.conditions_sum(), 0, "`{keyword}` modifier: {source}");
16294 // The expected value *is* the default, so the zero above
16295 // proves nothing on its own — a fixture that stopped
16296 // parsing would satisfy it too. `my $x = shift` is the
16297 // file's one assignment, and `shift` and `print 5` its two
16298 // branches; all three must survive.
16299 assert_eq!(
16300 abc.assignments_sum(),
16301 1,
16302 "`{keyword}`: fixture stopped scoring"
16303 );
16304 assert_eq!(
16305 abc.branches_sum(),
16306 2,
16307 "`{keyword}`: fixture stopped scoring"
16308 );
16309 }
16310 assert_eq!(
16311 conditions("sub f { my $x = shift; return 1 if $x; }"),
16312 1,
16313 "control: the `if` modifier still counts, so the zero above is a \
16314 policy decision and not a dead arm"
16315 );
16316 }
16317}
16318
16319/// A non-numeric literal in a boolean operand slot must score like an
16320/// identifier in the same slot (#1462).
16321///
16322/// The sibling module above pins the *numeric* half, closed by #1410.
16323/// The non-numeric literals were never swept and were missing from
16324/// every set the numerics were added to — same mechanism, same silent
16325/// zero, same file. Measured before the fix, every row below scored
16326/// exactly one condition short of its identifier control, in **both**
16327/// slots: `x || "default"` — the language's commonest truthy-default
16328/// idiom — reported 1 against `x || y`'s 2.
16329///
16330/// Scope is the eleven truthy-valued sets. #1462 shipped eight of them
16331/// and called that complete; a whole-branch review of the batch found
16332/// Perl, Ruby and Elixir in neither the swept list nor the
16333/// compile-error exemption, each already carrying `False` / `Nil`, and
16334/// each reproducing the gap exactly. They were swept on the same terms
16335/// and are rows here. Tcl and iRules are truthy too and needed nothing:
16336/// their `quoted_word` / `braced_word_simple` / `number` kinds already
16337/// cover every literal an `expr {…}` operand can hold, so they have no
16338/// row rather than a vacuous one.
16339///
16340/// C#, Java, Kotlin, Rust and Go name no literal kind at all and stay
16341/// that way: a bare literal in a boolean slot is a compile error there,
16342/// so there is nothing to count. The C family is integer-truthy and
16343/// does carry the same gap for `string_literal`, but it is the one
16344/// group in that class with integration-corpus exposure, so it is
16345/// deferred rather than decided (see `cpp_bool_terminal_kinds!`).
16346///
16347/// Three things each row pins that a conditions comparison alone
16348/// cannot:
16349///
16350/// - **The `kind_id` each spelling parses to.** This is the whole bug
16351/// class (`.claude/rules/grammar-dispatch.md` §1): the grammars here
16352/// emit `string` under two ids in JavaScript, Mozjs and Tsx and under
16353/// three in Tsx counting the type keyword, and listing the wrong one
16354/// compiles, runs, and returns the unfixed number. Asserting the id
16355/// turns that measurement into a standing claim rather than a note in
16356/// an issue. It is also the fixture anchor `.claude/rules/testing.md`
16357/// asks for — editing `"s"` to `1` in a row now fails by name instead
16358/// of quietly turning that row into a copy of the numeric module.
16359/// - **`cyclomatic` does not move.** Only the operand spelling changes
16360/// between a row and its control, so a `conditions` difference is
16361/// unambiguously ABC's.
16362/// - **Both walker paths.** The sets feed two structurally independent
16363/// consumers (§11) — the operands of a `&&` / `and` chain, and the
16364/// predicate of an `if` — and every row measured short in both. A
16365/// fixture of only one leaves the other path invisible.
16366///
16367/// The JS-family rows go through `js_literals!` rather than four hand-
16368/// written arrays, because the four languages differ in exactly one
16369/// place — which `string` alias an operand slot carries — and making
16370/// that the macro's only argument states the asymmetry instead of
16371/// leaving a reader to diff four near-identical lists for it.
16372#[cfg(test)]
16373#[cfg(any(
16374 feature = "javascript",
16375 feature = "mozjs",
16376 feature = "typescript",
16377 feature = "python",
16378 feature = "lua",
16379 feature = "php",
16380 feature = "groovy",
16381 feature = "perl",
16382 feature = "ruby",
16383 feature = "elixir"
16384))]
16385mod literal_bool_operands {
16386 use crate::test_support::{assert_fixture_spells, metrics_verbatim};
16387 use crate::{LANG, MetricsOptions};
16388
16389 /// One fixture shape: a source template with a `{}` operand slot,
16390 /// and the `abc.conditions_sum` / `cyclomatic_sum` every spelling of
16391 /// that operand must produce.
16392 type Slot = (&'static str, u64, u64);
16393
16394 /// One literal operand: its spelling, and the `kind_id` the grammar
16395 /// must emit for it.
16396 type Literal = (&'static str, u16);
16397
16398 /// A language's two slots, its identifier baseline operand, the
16399 /// literal operands that must score the same, and how many of those
16400 /// there should be.
16401 ///
16402 /// The count is not bookkeeping. `for_each_case` counts *languages*,
16403 /// so trimming a row's literal list — the pre-#1462 state is the
16404 /// empty list — would leave the whole module green.
16405 type Case = ([Slot; 2], &'static str, &'static [Literal], usize);
16406
16407 /// The seven non-numeric literal kinds of a JS-family grammar, with
16408 /// the `string` alias an operand slot carries passed in: `String2`
16409 /// for JavaScript, Mozjs and Tsx, `String` for TypeScript. That one
16410 /// argument is the entire difference between the four rows, and it
16411 /// is the same split `Checker::is_string` makes (§7).
16412 macro_rules! js_literals {
16413 ($Lang:ident, $string:ident) => {
16414 &[
16415 ("\"s\"", crate::$Lang::$string as u16),
16416 ("`t`", crate::$Lang::TemplateString as u16),
16417 ("/re/", crate::$Lang::Regex as u16),
16418 ("null", crate::$Lang::Null as u16),
16419 ("undefined", crate::$Lang::Undefined as u16),
16420 ("{}", crate::$Lang::Object as u16),
16421 ("[]", crate::$Lang::Array as u16),
16422 ]
16423 };
16424 }
16425
16426 const JS_SLOTS: [Slot; 2] = [
16427 ("function f(a) {\n return a && {};\n}\n", 2, 3),
16428 ("function f() {\n if ({}) { return 1; }\n}\n", 1, 3),
16429 ];
16430
16431 fn conditions(lang: LANG, source: &str) -> u64 {
16432 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
16433 .abc
16434 .conditions_sum()
16435 }
16436
16437 fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
16438 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
16439 .cyclomatic
16440 .cyclomatic_sum()
16441 }
16442
16443 /// Asserts the fixture still spells the construct under test, by
16444 /// parsing it with the language's own parser. Split out of `cases`
16445 /// because only the parser types are feature-gated; the `kind_id`
16446 /// enums are compiled unconditionally.
16447 fn assert_spells(lang: LANG, source: &str, kinds: &[(u16, usize, &str)]) {
16448 match lang {
16449 #[cfg(feature = "javascript")]
16450 LANG::Javascript => {
16451 assert_fixture_spells::<crate::JavascriptParser>(source, "f.js", kinds);
16452 }
16453 #[cfg(feature = "mozjs")]
16454 LANG::Mozjs => assert_fixture_spells::<crate::MozjsParser>(source, "f.jsm", kinds),
16455 #[cfg(feature = "typescript")]
16456 LANG::Typescript => {
16457 assert_fixture_spells::<crate::TypescriptParser>(source, "f.ts", kinds);
16458 }
16459 #[cfg(feature = "typescript")]
16460 LANG::Tsx => assert_fixture_spells::<crate::TsxParser>(source, "f.tsx", kinds),
16461 #[cfg(feature = "python")]
16462 LANG::Python => assert_fixture_spells::<crate::PythonParser>(source, "f.py", kinds),
16463 #[cfg(feature = "lua")]
16464 LANG::Lua => assert_fixture_spells::<crate::LuaParser>(source, "f.lua", kinds),
16465 #[cfg(feature = "php")]
16466 LANG::Php => assert_fixture_spells::<crate::PhpParser>(source, "f.php", kinds),
16467 #[cfg(feature = "groovy")]
16468 LANG::Groovy => {
16469 assert_fixture_spells::<crate::GroovyParser>(source, "f.groovy", kinds);
16470 }
16471 #[cfg(feature = "perl")]
16472 LANG::Perl => assert_fixture_spells::<crate::PerlParser>(source, "f.pl", kinds),
16473 #[cfg(feature = "ruby")]
16474 LANG::Ruby => assert_fixture_spells::<crate::RubyParser>(source, "f.rb", kinds),
16475 #[cfg(feature = "elixir")]
16476 LANG::Elixir => assert_fixture_spells::<crate::ElixirParser>(source, "f.ex", kinds),
16477 other => panic!("{other:?} has a case row but no parser arm"),
16478 }
16479 }
16480
16481 /// `([chain_slot, condition_slot], identifier, literals)` per
16482 /// language, one spelling per literal *kind* the grammar emits.
16483 ///
16484 /// - JS family: `string`, `template_string`, `regex`, `null`,
16485 /// `undefined`, `object`, `array`. TypeScript's `string` / `object`
16486 /// type keywords (`a: string`) are different ids rendering to the
16487 /// same names and are deliberately not in the sets, so a row that
16488 /// accidentally named one would fail the id anchor here.
16489 /// - Python: `string` covers `'s'`, `"""s"""`, `f'x'` and `b'x'`,
16490 /// while the implicit-join `'a' 'b'` is the separate
16491 /// `concatenated_string` rule — the sibling-rule-under-a-supertype
16492 /// shape #1379's Perl numerals were, which an alias sweep cannot
16493 /// see. Then `none`, the four collection displays, and `ellipsis`.
16494 /// - Lua: one `string` kind for `"s"`, `'s'` and `[[s]]`, plus
16495 /// `table_constructor`. Lua's is the strongest case of the eight:
16496 /// everything but `false` and `nil` is truthy, which is why
16497 /// `cond and "a" or "b"` is the language's ternary.
16498 /// - PHP: `string` (single-quoted) and `encapsed_string`
16499 /// (interpolating) are separate rules, as are `heredoc` and
16500 /// `nowdoc`; then `array_creation_expression`, `null`, the
16501 /// backtick `shell_command_expression`, and `cast_expression` —
16502 /// the second finding of #1462, PHP being the only set in the
16503 /// Java / C# / Groovy / PHP group that named no cast kind.
16504 /// - Groovy: `string_literal` (which also covers the slashy `/re/`),
16505 /// `null_literal`, `list_literal`, `map_literal`.
16506 /// - Perl: the four string productions the grammar keeps separate
16507 /// (`'s'`, `q()`, `"s"`, `qq()`), the two command substitutions
16508 /// (`qx()`, backticks), the `qw()` / `[…]` / `{…}` collection
16509 /// literals, `qr//`, and `special_literal` (`__FILE__`).
16510 /// `heredoc_initializer` is the twelfth kind and cannot live in
16511 /// this table — a Perl heredoc body follows the *statement*, not
16512 /// the operand — so it has its own test below.
16513 /// - Ruby: `string` (covering `"s"`, `'s'`, `%q()`, `%Q()`),
16514 /// `chained_string` (the adjacent-literal join `"a" "b"`, a
16515 /// sibling rule an alias sweep cannot see), `subshell`, the four
16516 /// collection literals `array` / `hash` / `%w[]` / `%i[]`,
16517 /// `regex`, the one-character `?a`, and both symbol productions.
16518 /// `heredoc_beginning` is the twelfth and shares the Perl heredoc
16519 /// test for the same reason.
16520 /// - Elixir: `string` (one kind for `"s"` and the `"""` heredoc),
16521 /// `charlist`, `sigil` (one kind for `~r//`, `~s()`, `~w()`),
16522 /// `quoted_atom` — a separate production from `atom`, so
16523 /// `:"q a"` scored zero while `:atom` scored one — and the four
16524 /// collection literals `list` / `tuple` / `map` / `bitstring`.
16525 ///
16526 /// Elixir is the one row whose two slots are not two independent
16527 /// consumers: the language has no bare-truthy `if` predicate slot
16528 /// to route, since an Elixir `if` is a keyword-shaped `Call` scoring
16529 /// one whatever its argument. Its second slot negates the operand
16530 /// instead, which reaches the terminal set through
16531 /// `elixir_inspect_container` rather than through the chain
16532 /// walker's own check — a different path to the same set, which is
16533 /// what the second slot exists to exercise.
16534 ///
16535 /// Shapes that measured short and are deliberately absent, none of
16536 /// them a literal: JavaScript's `this` and Groovy's
16537 /// `object_creation_expression` (recorded in #1462); Perl's
16538 /// `s///` and `tr///` (operations on `$_` evaluating to a count),
16539 /// `anonymous_function` and `array_dereference`; Ruby's `lambda`;
16540 /// and Elixir's `anonymous_function` and `&f/1` capture.
16541 fn cases(lang: LANG) -> Option<Case> {
16542 Some(match lang {
16543 LANG::Javascript => (JS_SLOTS, "b", js_literals!(Javascript, String2), 7),
16544 LANG::Mozjs => (JS_SLOTS, "b", js_literals!(Mozjs, String2), 7),
16545 LANG::Typescript => (JS_SLOTS, "b", js_literals!(Typescript, String), 7),
16546 LANG::Tsx => (JS_SLOTS, "b", js_literals!(Tsx, String2), 7),
16547 LANG::Python => (
16548 [
16549 ("def f(a):\n return a and {}\n", 2, 3),
16550 ("def f():\n if {}:\n return 1\n", 1, 3),
16551 ],
16552 "b",
16553 &[
16554 ("'s'", crate::Python::String as u16),
16555 ("'a' 'b'", crate::Python::ConcatenatedString as u16),
16556 ("None", crate::Python::None as u16),
16557 ("[]", crate::Python::List as u16),
16558 ("{1}", crate::Python::Set as u16),
16559 ("()", crate::Python::Tuple as u16),
16560 ("{}", crate::Python::Dictionary as u16),
16561 ("...", crate::Python::Ellipsis as u16),
16562 ],
16563 8,
16564 ),
16565 LANG::Lua => (
16566 [
16567 ("function f(a)\n return a and {}\nend\n", 2, 3),
16568 ("function f()\n if {} then return 1 end\nend\n", 1, 3),
16569 ],
16570 "b",
16571 &[
16572 ("\"s\"", crate::Lua::String as u16),
16573 ("{}", crate::Lua::TableConstructor as u16),
16574 ],
16575 2,
16576 ),
16577 LANG::Php => (
16578 [
16579 ("<?php\nfunction f($a) {\n return $a && {};\n}\n", 2, 3),
16580 ("<?php\nfunction f() {\n if ({}) { return 1; }\n}\n", 1, 3),
16581 ],
16582 "$b",
16583 &[
16584 ("'s'", crate::Php::String as u16),
16585 ("\"x$b\"", crate::Php::EncapsedString as u16),
16586 ("<<<EOT\ns\nEOT", crate::Php::Heredoc as u16),
16587 ("<<<'EOT'\ns\nEOT", crate::Php::Nowdoc as u16),
16588 ("[]", crate::Php::ArrayCreationExpression as u16),
16589 ("null", crate::Php::Null as u16),
16590 ("`ls`", crate::Php::ShellCommandExpression as u16),
16591 ("(bool)$b", crate::Php::CastExpression as u16),
16592 ],
16593 8,
16594 ),
16595 LANG::Groovy => (
16596 [
16597 ("def f(a) {\n return a && {}\n}\n", 2, 3),
16598 ("def f() {\n if ({}) { return 1 }\n}\n", 1, 3),
16599 ],
16600 "b",
16601 &[
16602 ("\"s\"", crate::Groovy::StringLiteral as u16),
16603 ("null", crate::Groovy::NullLiteral as u16),
16604 ("[]", crate::Groovy::ListLiteral as u16),
16605 ("[:]", crate::Groovy::MapLiteral as u16),
16606 ],
16607 4,
16608 ),
16609 LANG::Perl => (
16610 [
16611 (
16612 "sub f {\n my ($a, $b) = @_;\n if ($a && {}) { print 1; }\n}\n",
16613 2,
16614 4,
16615 ),
16616 (
16617 "sub f {\n my ($a, $b) = @_;\n if ({}) { print 1; }\n}\n",
16618 1,
16619 3,
16620 ),
16621 ],
16622 "$b",
16623 &[
16624 ("'s'", crate::Perl::StringSingleQuoted as u16),
16625 ("q(s)", crate::Perl::StringQQuoted as u16),
16626 ("\"s\"", crate::Perl::StringDoubleQuoted as u16),
16627 ("qq(s)", crate::Perl::StringQqQuoted as u16),
16628 ("qx(ls)", crate::Perl::CommandQxQuoted as u16),
16629 ("`ls`", crate::Perl::BacktickQuoted as u16),
16630 ("qw(a b)", crate::Perl::WordListQw as u16),
16631 ("[1, 2]", crate::Perl::ArrayRef as u16),
16632 ("{ a => 1 }", crate::Perl::HashRef as u16),
16633 ("qr/re/", crate::Perl::RegexPatternQr as u16),
16634 ("__FILE__", crate::Perl::SpecialLiteral as u16),
16635 ],
16636 11,
16637 ),
16638 LANG::Ruby => (
16639 [
16640 ("def f(a, b)\n if a && {} then 1 else 0 end\nend\n", 3, 4),
16641 ("def f(a, b)\n if {} then 1 else 0 end\nend\n", 2, 3),
16642 ],
16643 "b",
16644 &[
16645 ("\"s\"", crate::Ruby::String as u16),
16646 ("\"a\" \"b\"", crate::Ruby::ChainedString as u16),
16647 ("`ls`", crate::Ruby::Subshell as u16),
16648 ("[1, 2]", crate::Ruby::Array as u16),
16649 ("{ a: 1 }", crate::Ruby::Hash as u16),
16650 ("%w[a b]", crate::Ruby::StringArray as u16),
16651 ("%i[a b]", crate::Ruby::SymbolArray as u16),
16652 ("/re/", crate::Ruby::Regex as u16),
16653 ("?a", crate::Ruby::Character as u16),
16654 (":sym", crate::Ruby::SimpleSymbol as u16),
16655 (":\"sym\"", crate::Ruby::DelimitedSymbol as u16),
16656 ],
16657 11,
16658 ),
16659 LANG::Elixir => (
16660 [
16661 (
16662 "defmodule M do\n def f(a, b) do\n if a && {} do\n a\n end\n end\nend\n",
16663 3,
16664 5,
16665 ),
16666 (
16667 "defmodule M do\n def f(a, b) do\n if a && !{} do\n a\n end\n end\nend\n",
16668 3,
16669 5,
16670 ),
16671 ],
16672 "b",
16673 &[
16674 ("\"s\"", crate::Elixir::String as u16),
16675 ("'c'", crate::Elixir::Charlist as u16),
16676 ("~r/re/", crate::Elixir::Sigil as u16),
16677 (":\"q a\"", crate::Elixir::QuotedAtom as u16),
16678 ("[1, 2]", crate::Elixir::List as u16),
16679 ("{1, 2}", crate::Elixir::Tuple as u16),
16680 ("%{a: 1}", crate::Elixir::Map as u16),
16681 ("<<1>>", crate::Elixir::Bitstring as u16),
16682 ],
16683 8,
16684 ),
16685 _ => return None,
16686 })
16687 }
16688
16689 /// Runs `check` once per enabled language that has a case, having
16690 /// first established that the case can still assert something. The
16691 /// four guards are the sibling module's, for the same four ways
16692 /// this table could decay into asserting nothing: no language
16693 /// enabled, an emptied literal list, a list that no longer covers
16694 /// every kind, and a template that lost its `{}` slot (which makes
16695 /// every comparison `x == x`).
16696 fn for_each_case(check: impl Fn(LANG, Case)) {
16697 let mut checked = 0;
16698 for lang in LANG::into_enum_iter() {
16699 if !lang.is_enabled() {
16700 continue;
16701 }
16702 let Some(case @ (slots, _, literals, expected_kinds)) = cases(lang) else {
16703 continue;
16704 };
16705 // The length check does not imply this one: it asserts only
16706 // that the list and its recorded count *agree*, which an
16707 // emptied row with `expected_kinds` set to 0 satisfies —
16708 // the shape a "the test failed, fix the number" edit takes.
16709 // `checked` counts languages, so that row still increments
16710 // it and the non-vacuity guard below reads satisfied.
16711 assert!(
16712 !literals.is_empty(),
16713 "{lang:?}: the literal-operand list is empty; this language asserted nothing"
16714 );
16715 assert_eq!(
16716 literals.len(),
16717 expected_kinds,
16718 "{lang:?}: the literal-operand list no longer covers one spelling \
16719 per grammar literal kind"
16720 );
16721 for (template, _, _) in slots {
16722 assert!(
16723 template.contains("{}"),
16724 "{lang:?}: template lost its `{{}}` operand slot: {template}"
16725 );
16726 }
16727 check(lang, case);
16728 checked += 1;
16729 }
16730 assert!(
16731 checked > 0,
16732 "no truthy-valued language enabled; this test asserted nothing"
16733 );
16734 }
16735
16736 #[test]
16737 fn a_literal_operand_scores_like_an_identifier_operand() {
16738 for_each_case(|lang, (slots, identifier, literals, _)| {
16739 for (template, _, _) in slots {
16740 let baseline = conditions(lang, &template.replace("{}", identifier));
16741 for (literal, _) in literals {
16742 let source = template.replace("{}", literal);
16743 let scored = conditions(lang, &source);
16744 assert_eq!(
16745 scored, baseline,
16746 "{lang:?}: `{literal}` scored {scored} unary conditions against \
16747 `{identifier}`'s {baseline}\n source: {source}"
16748 );
16749 }
16750 }
16751 });
16752 }
16753
16754 /// The absolute anchor under the comparison above: every operand
16755 /// spelling must produce the slot's recorded `conditions`, and must
16756 /// leave `cyclomatic` alone.
16757 #[test]
16758 fn every_literal_operand_scores_its_recorded_values() {
16759 for_each_case(|lang, (slots, identifier, literals, _)| {
16760 for (template, expected_conditions, expected_cyclomatic) in slots {
16761 let operands = std::iter::once(identifier)
16762 .chain(literals.iter().map(|(spelling, _)| *spelling));
16763 for operand in operands {
16764 let source = template.replace("{}", operand);
16765 assert_eq!(
16766 conditions(lang, &source),
16767 expected_conditions,
16768 "{lang:?}: `{operand}` conditions\n source: {source}"
16769 );
16770 assert_eq!(
16771 cyclomatic_sum(lang, &source),
16772 expected_cyclomatic,
16773 "{lang:?}: `{operand}` cyclomatic_sum\n source: {source}"
16774 );
16775 }
16776 }
16777 });
16778 }
16779
16780 /// The twelfth Perl and Ruby literal kind, which the table above
16781 /// cannot hold: in both languages a heredoc body follows the
16782 /// *statement*, so the operand slot carries only the introducer and
16783 /// the rest of the literal lands on later lines. Substituting one
16784 /// into a single-line template produces an unterminated heredoc and
16785 /// an `ERROR` parse, not a measurement.
16786 ///
16787 /// That shape is also why exactly one kind is listed per language
16788 /// (§5): `heredoc_initializer` / `heredoc_beginning` is the node in
16789 /// the operand slot, while `heredoc_body_statement` /
16790 /// `heredoc_body` is a sibling the walker never reaches. Listing
16791 /// both would score one literal twice.
16792 #[cfg(any(feature = "perl", feature = "ruby"))]
16793 #[test]
16794 fn a_heredoc_operand_scores_like_an_identifier_operand() {
16795 #[cfg(feature = "perl")]
16796 {
16797 let heredoc = "sub f {\n my ($a, $b) = @_;\n if ($a && <<\"EOT\") { print 1; }\nhello\nEOT\n}\n";
16798 assert_spells(
16799 LANG::Perl,
16800 heredoc,
16801 &[(crate::Perl::HeredocInitializer as u16, 1, "<<\"EOT\"")],
16802 );
16803 assert_eq!(
16804 conditions(LANG::Perl, heredoc),
16805 conditions(
16806 LANG::Perl,
16807 "sub f {\n my ($a, $b) = @_;\n if ($a && $b) { print 1; }\n}\n"
16808 ),
16809 "Perl heredoc operand"
16810 );
16811 }
16812 #[cfg(feature = "ruby")]
16813 {
16814 let heredoc =
16815 "def f(a, b)\n if a && <<~TXT then 1 else 0 end\n hello\n TXT\nend\n";
16816 assert_spells(
16817 LANG::Ruby,
16818 heredoc,
16819 &[(crate::Ruby::HeredocBeginning as u16, 1, "<<~TXT")],
16820 );
16821 assert_eq!(
16822 conditions(LANG::Ruby, heredoc),
16823 conditions(
16824 LANG::Ruby,
16825 "def f(a, b)\n if a && b then 1 else 0 end\nend\n"
16826 ),
16827 "Ruby heredoc operand"
16828 );
16829 }
16830 }
16831
16832 /// Every spelling must parse to the `kind_id` its row names — the
16833 /// §1 alias claim, which is the one thing a conditions comparison
16834 /// cannot check: a row naming the wrong alias of a multi-id kind
16835 /// would simply keep reporting the unfixed number.
16836 #[test]
16837 fn every_literal_spelling_parses_to_the_kind_its_row_names() {
16838 for_each_case(|lang, (slots, _, literals, _)| {
16839 for (template, _, _) in slots {
16840 for (literal, kind) in literals {
16841 let source = template.replace("{}", literal);
16842 assert_spells(lang, &source, &[(*kind, 1, literal)]);
16843 }
16844 }
16845 });
16846 }
16847}
16848
16849/// PHP's `String3` is the hidden `_string` supertype and Groovy's
16850/// `SlashyString` the hidden `_slashy_string` one — both listed in, or
16851/// deliberately omitted from, their terminal-bool sets on the strength
16852/// of being unreachable (`.claude/rules/grammar-dispatch.md` §2). A
16853/// grammar bump that promotes either changes ABC's answer silently, so
16854/// the unreachability is pinned rather than assumed.
16855// Gated on the union of the two features its tests name, so a build
16856// enabling neither drops the module rather than leaving its imports
16857// unused (`.claude/rules/testing.md`, #1286).
16858#[cfg(all(test, any(feature = "php", feature = "groovy")))]
16859mod hidden_literal_supertypes {
16860 use crate::test_support::ast_has_kind_id;
16861 use crate::*;
16862
16863 #[cfg(feature = "php")]
16864 #[test]
16865 fn php_hidden_string_supertype_is_unreachable() {
16866 let src = "<?php\nfunction f($a) {\n return $a && 's' && \"x$a\" && <<<EOT\ns\nEOT;\n}\n";
16867 let parser = PhpParser::new(src.as_bytes().to_vec(), std::path::Path::new("f.php"), None);
16868 assert!(
16869 ast_has_kind_id(&parser, Php::String as u16),
16870 "control: the fixture must carry the reachable `string` kind"
16871 );
16872 assert!(
16873 !ast_has_kind_id(&parser, Php::String3 as u16),
16874 "`_string` is no longer hidden; the defensive arm in \
16875 `php_bool_terminal_kinds!()` is now live and needs a fixture"
16876 );
16877 }
16878
16879 #[cfg(feature = "groovy")]
16880 #[test]
16881 fn groovy_hidden_slashy_string_is_unreachable() {
16882 let src = "def f(a) {\n return a && /re/ && \"s\"\n}\n";
16883 let parser = GroovyParser::new(
16884 src.as_bytes().to_vec(),
16885 std::path::Path::new("f.groovy"),
16886 None,
16887 );
16888 assert!(
16889 ast_has_kind_id(&parser, Groovy::StringLiteral as u16),
16890 "control: a slashy string must still parse to `string_literal`"
16891 );
16892 assert!(
16893 !ast_has_kind_id(&parser, Groovy::SlashyString as u16),
16894 "`_slashy_string` is no longer hidden; `groovy_bool_terminal_kinds!()` \
16895 omits it on the strength of it being unreachable"
16896 );
16897 }
16898}