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;
29use crate::metrics::ConstraintReach;
30
31use crate::*;
32
33mod bash;
34mod c;
35mod cpp;
36mod csharp;
37mod elixir;
38mod go;
39mod groovy;
40mod irules;
41mod java;
42mod js_family;
43mod kotlin;
44mod lua;
45mod mozcpp;
46mod objc;
47mod perl;
48mod php;
49mod python;
50mod ruby;
51mod rust;
52#[cfg(test)]
53mod slot_pays_tests;
54mod tcl;
55
56/// The `ABC` metric.
57///
58/// The `ABC` metric measures the size of a source code by counting
59/// the number of Assignments (`A`), Branches (`B`) and Conditions (`C`).
60/// The metric defines an ABC score as a vector of three elements (`<A,B,C>`).
61/// The ABC score can be represented by its individual components (`A`, `B` and `C`)
62/// or by the magnitude of the vector (`|<A,B,C>| = sqrt(A^2 + B^2 + C^2)`).
63///
64/// Official paper and definition:
65///
66/// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
67///
68/// <https://www.softwarerenovation.com/Articles.aspx>
69///
70/// # Cross-language `&&` / `||` policy
71///
72/// Per Fitzpatrick's conditional-operator rule (Rule 5 in Figure 2
73/// for C and Figure 4 for Java; Rule 7 in Figure 3 for C++), only
74/// comparison operators (`==`, `!=`, `<=`, `>=`, `<`, `>`) and a
75/// paper-defined keyword set (`else`, `case`, `default`, `?`, plus
76/// `try` / `catch` for C++ and Java) contribute to the condition
77/// count. Per-language `impl Abc` blocks narrow this set where
78/// appropriate — e.g., C++/Rust/Go/Python exclude `default` since
79/// it falls through unconditionally (matching the Rust `_ =>` and
80/// Java `default:` precedent). The short-
81/// circuit logical operators `&&` and `||` (and per-language
82/// equivalents — Python's `and` / `or`, Lua's `and` / `or`, Tcl's
83/// `&&` / `||`, Perl's `&&` / `||` / `//` / `and` / `or`)
84/// are deliberately **not** counted on their own. The paper's
85/// worked Listing 2 annotates `(am >= 0 && am <= 0xF) ? '/' : 'C'`
86/// as `accc` — three conditions for `>=`, `<=`, `?`, zero for
87/// `&&`.
88///
89/// Fitzpatrick's Rule 7 (Figure 3, C++) / Rule 9 (Figure 4, Java) —
90/// "Add one to the condition count for each unary conditional
91/// expression" — instead counts each non-comparison operand of a
92/// `&&` / `||` chain once. The paper's worked example for this
93/// rule is `if (x || y) printf("test failure\n");`, annotated:
94/// "there are two unary conditions since both `x` and `y` are
95/// tested as conditional expressions" (so `||` contributes zero,
96/// `x` contributes one, `y` contributes one, and `printf(...)`
97/// contributes one branch). The walker machinery for this —
98/// modelled on the former `java_count_unary_conditions` /
99/// `java_inspect_container` — is present today for Java, Groovy,
100/// C#, Rust, Go, JavaScript, TypeScript, TSX, Mozjs, PHP, C++,
101/// Python, Perl, Lua, Tcl, iRules, Kotlin, Ruby, and Elixir. So
102/// `if (a && b)` reports 2 conditions across this set, matching
103/// the paper. Bash is the lone exception: its `&&` / `||` are
104/// command-list separators rather than boolean-expression operands
105/// with named leaf operands, so Fitzpatrick's Rule 9 does not map
106/// onto its grammar and the walker is deliberately not wired.
107///
108/// This policy is paper-faithful and deviates from RuboCop's
109/// `Metrics/AbcSize` (which counts `and` / `or` as conditions
110/// directly) while matching `StepicOrg/abcmeter` and
111/// `eoinnoble/python-abc`. The book's *ABC counting rules*
112/// section reproduces the rule tables, a per-language deviation
113/// table, and worked examples — see the chapter at
114/// <https://dekobon.github.io/big-code-analysis/metrics.html#abc>.
115///
116/// # Cross-language empty-`for`-condition policy
117///
118/// `for (;;)` — and every other spelling that omits the test slot
119/// (`for (init; ; update)`, Go's bare `for {}`) — counts **zero**
120/// conditions. Nothing in Fitzpatrick's condition rules attributes a
121/// count to a `for` keyword: they count conditional operators and
122/// unary conditions that are *present*, and an omitted test is not a
123/// decision. Most languages get this for free: `*_walk_for_statement`
124/// asks `for_statement` for its `condition` field and an empty header
125/// has none. Two do not, and neither needs a special case either —
126/// the JS family fills the slot with an `empty_statement`, which is
127/// not a boolean terminal and not a paren / `!` wrapper, so it falls
128/// through; and Go, whose `for_statement` exposes no `condition` field
129/// at all, locates the header slot structurally and finds only the
130/// body. Before #1276 Java and Groovy alone disagreed, counting the
131/// `;` or `)` that landed in a positional child slot as a
132/// vacuously-true condition.
133///
134/// See issue #395 for the Phase-1 cross-language policy
135/// alignment, #403 for the Phase-2 unary-conditional walker
136/// fan-out, #404 for the Phase-3 book documentation, #557
137/// for the Kotlin / Ruby / Elixir walker wiring, and #1276 for the
138/// `for`-header condition slot.
139#[derive(Debug, Clone, PartialEq)]
140#[non_exhaustive]
141pub struct Stats {
142 pub(super) assignments: f64,
143 assignments_sum: f64,
144 assignments_min: f64,
145 assignments_max: f64,
146 pub(super) branches: f64,
147 branches_sum: f64,
148 branches_min: f64,
149 branches_max: f64,
150 pub(super) conditions: f64,
151 conditions_sum: f64,
152 conditions_min: f64,
153 conditions_max: f64,
154 space_count: usize,
155 constraint: ConstraintReach,
156}
157
158impl Default for Stats {
159 fn default() -> Self {
160 Self {
161 assignments: 0.,
162 assignments_sum: 0.,
163 assignments_min: f64::MAX,
164 assignments_max: 0.,
165 branches: 0.,
166 branches_sum: 0.,
167 branches_min: f64::MAX,
168 branches_max: 0.,
169 conditions: 0.,
170 conditions_sum: 0.,
171 conditions_min: f64::MAX,
172 conditions_max: 0.,
173 space_count: 1,
174 constraint: ConstraintReach::default(),
175 }
176 }
177}
178
179impl fmt::Display for Stats {
180 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
181 write!(
182 f,
183 "assignments: {}, branches: {}, conditions: {}, magnitude: {}, \
184 assignments_average: {}, branches_average: {}, conditions_average: {}, \
185 assignments_min: {}, assignments_max: {}, \
186 branches_min: {}, branches_max: {}, \
187 conditions_min: {}, conditions_max: {}",
188 self.assignments_sum(),
189 self.branches_sum(),
190 self.conditions_sum(),
191 self.magnitude_sum(),
192 self.assignments_average(),
193 self.branches_average(),
194 self.conditions_average(),
195 self.assignments_min(),
196 self.assignments_max(),
197 self.branches_min(),
198 self.branches_max(),
199 self.conditions_min(),
200 self.conditions_max()
201 )
202 }
203}
204
205impl Stats {
206 /// Merges a second `Abc` metric into the first one.
207 pub fn merge(&mut self, other: &Stats) {
208 // Calculates minimum and maximum values
209 self.assignments_min = self.assignments_min.min(other.assignments_min);
210 self.assignments_max = self.assignments_max.max(other.assignments_max);
211 self.branches_min = self.branches_min.min(other.branches_min);
212 self.branches_max = self.branches_max.max(other.branches_max);
213 self.conditions_min = self.conditions_min.min(other.conditions_min);
214 self.conditions_max = self.conditions_max.max(other.conditions_max);
215
216 self.assignments_sum += other.assignments_sum;
217 self.branches_sum += other.branches_sum;
218 self.conditions_sum += other.conditions_sum;
219
220 self.space_count += other.space_count;
221 }
222
223 /// Returns the `Abc` assignments metric value.
224 #[must_use]
225 pub fn assignments(&self) -> u64 {
226 self.assignments as u64
227 }
228
229 /// Returns the `Abc` assignments sum metric value.
230 #[must_use]
231 pub fn assignments_sum(&self) -> u64 {
232 self.assignments_sum as u64
233 }
234
235 /// Returns the `Abc` assignments average value.
236 ///
237 /// This value is computed dividing the `Abc`
238 /// assignments value for the number of spaces.
239 #[must_use]
240 pub fn assignments_average(&self) -> f64 {
241 crate::metrics::average(self.assignments_sum() as f64, self.space_count)
242 }
243
244 /// Returns the `Abc` assignments minimum value.
245 ///
246 /// Collapses the `f64::MAX` sentinel that `Stats::default()` plants
247 /// into `assignments_min` to `0`, so a never-observed space
248 /// serializes to a meaningful number rather than `1.7976931e308`.
249 #[allow(clippy::float_cmp)]
250 #[must_use]
251 pub fn assignments_min(&self) -> u64 {
252 if self.assignments_min == f64::MAX {
253 0
254 } else {
255 self.assignments_min as u64
256 }
257 }
258
259 /// Returns the `Abc` assignments maximum value.
260 #[must_use]
261 pub fn assignments_max(&self) -> u64 {
262 self.assignments_max as u64
263 }
264
265 /// Returns the `Abc` branches metric value.
266 #[must_use]
267 pub fn branches(&self) -> u64 {
268 self.branches as u64
269 }
270
271 /// Returns the `Abc` branches sum metric value.
272 #[must_use]
273 pub fn branches_sum(&self) -> u64 {
274 self.branches_sum as u64
275 }
276
277 /// Returns the `Abc` branches average value.
278 ///
279 /// This value is computed dividing the `Abc`
280 /// branches value for the number of spaces.
281 #[must_use]
282 pub fn branches_average(&self) -> f64 {
283 crate::metrics::average(self.branches_sum() as f64, self.space_count)
284 }
285
286 /// Returns the `Abc` branches minimum value.
287 ///
288 /// Same `f64::MAX` sentinel collapse as `assignments_min`.
289 #[allow(clippy::float_cmp)]
290 #[must_use]
291 pub fn branches_min(&self) -> u64 {
292 if self.branches_min == f64::MAX {
293 0
294 } else {
295 self.branches_min as u64
296 }
297 }
298
299 /// Returns the `Abc` branches maximum value.
300 #[must_use]
301 pub fn branches_max(&self) -> u64 {
302 self.branches_max as u64
303 }
304
305 /// Returns the `Abc` conditions metric value.
306 #[must_use]
307 pub fn conditions(&self) -> u64 {
308 self.conditions as u64
309 }
310
311 /// Returns the `Abc` conditions sum metric value.
312 #[must_use]
313 pub fn conditions_sum(&self) -> u64 {
314 self.conditions_sum as u64
315 }
316
317 /// Returns the `Abc` conditions average value.
318 ///
319 /// This value is computed dividing the `Abc`
320 /// conditions value for the number of spaces.
321 #[must_use]
322 pub fn conditions_average(&self) -> f64 {
323 crate::metrics::average(self.conditions_sum() as f64, self.space_count)
324 }
325
326 /// Returns the `Abc` conditions minimum value.
327 ///
328 /// Same `f64::MAX` sentinel collapse as `assignments_min`.
329 #[allow(clippy::float_cmp)]
330 #[must_use]
331 pub fn conditions_min(&self) -> u64 {
332 if self.conditions_min == f64::MAX {
333 0
334 } else {
335 self.conditions_min as u64
336 }
337 }
338
339 /// Returns the `Abc` conditions maximum value.
340 #[must_use]
341 pub fn conditions_max(&self) -> u64 {
342 self.conditions_max as u64
343 }
344
345 /// Returns the `Abc` magnitude metric value.
346 #[must_use]
347 pub fn magnitude(&self) -> f64 {
348 (self.assignments.powi(2) + self.branches.powi(2) + self.conditions.powi(2)).sqrt()
349 }
350
351 /// Returns the `Abc` magnitude sum metric value.
352 #[must_use]
353 pub fn magnitude_sum(&self) -> f64 {
354 (self.assignments_sum.powi(2) + self.branches_sum.powi(2) + self.conditions_sum.powi(2))
355 .sqrt()
356 }
357
358 #[inline]
359 pub(crate) fn compute_sum(&mut self) {
360 self.assignments_sum += self.assignments;
361 self.branches_sum += self.branches;
362 self.conditions_sum += self.conditions;
363 }
364
365 #[inline]
366 pub(crate) fn compute_minmax(&mut self) {
367 self.assignments_min = self.assignments_min.min(self.assignments);
368 self.assignments_max = self.assignments_max.max(self.assignments);
369 self.branches_min = self.branches_min.min(self.branches);
370 self.branches_max = self.branches_max.max(self.branches);
371 self.conditions_min = self.conditions_min.min(self.conditions);
372 self.conditions_max = self.conditions_max.max(self.conditions);
373 self.compute_sum();
374 }
375}
376
377#[doc(hidden)]
378/// Per-language computation of the ABC metric.
379pub(crate) trait Abc
380where
381 Self: Checker,
382{
383 /// Walk `node` and update `stats` with this metric for the language
384 /// implementing the trait.
385 ///
386 /// `code` is the source bytes underlying the parsed tree. Most
387 /// languages ignore it: assignments, branches, and conditions all
388 /// surface as distinct grammar productions and a `kind_id()` match
389 /// is enough. Elixir is the exception — `case` / `cond` / `if` /
390 /// `with` / guard `when` arms surface as `Call` nodes whose keyword
391 /// target lives only in the source text. Matching the `Cyclomatic`
392 /// / `Halstead` / `Exit` / `Cognitive` pattern keeps the signature
393 /// uniform.
394 ///
395 /// `ancestors` is the chain the walker descended through. Nearly
396 /// every language classifies some token by what encloses it: `<`
397 /// and `>` are comparisons only under a binary expression (a type
398 /// -argument list otherwise), and `&&` / `||` reach their operands
399 /// through the enclosing chain node. Reaching that parent with
400 /// [`Node::parent`] costs `O(depth)` per node (#1096). The
401 /// condition-slot walkers take *their* parent as an argument
402 /// instead, because the caller descended from it.
403 fn compute<'a>(
404 node: &Node<'a>,
405 code: &'a [u8],
406 ancestors: Ancestors<'a, '_>,
407 stats: &mut Stats,
408 );
409}
410
411// Shared Phase-2B helper (issue #403): walk every named child of an
412// expression-list-style wrapper (Go's `expression_list`, Lua's
413// `expression_list`) and route each through a language-specific
414// classifier. Used for `return value1, value2, ...` arms where the
415// values live one level below the return statement under a list
416// wrapper. The classifier receives only named children so that
417// `,` / `;` / `(` / `)` tokens never reach it, plus `list` itself as
418// the child's parent — the container classifiers seed their
419// boolean-context flag from the parent kind, and reaching it with
420// `Node::parent` would cost `O(depth)` per node (#1096).
421pub(super) fn for_each_named_child(
422 list: &Node,
423 conditions: &mut f64,
424 f: fn(&Node, &Node, &mut f64),
425) {
426 let mut cursor = list.cursor();
427 if cursor.goto_first_child() {
428 loop {
429 let child = cursor.node();
430 if child.is_named() {
431 f(&child, list, conditions);
432 }
433 if !cursor.goto_next_sibling() {
434 break;
435 }
436 }
437 }
438}
439
440// The operand a single-operand wrapper holds — `(…)`, a negation, a
441// `return` — read as its first named child that is not a tree-sitter
442// `extra`. A positional `child(1)` read assumes nothing sits between the
443// opening token and the operand, and a comment can sit anywhere: `(/*c*/
444// b)` handed the comment to the peel, which declined it, and the
445// condition scored zero (#1455). `is_named` alone is not enough either,
446// because extras are named.
447pub(super) fn wrapped_operand<'a>(node: &Node<'a>) -> Option<Node<'a>> {
448 node.children().find(is_operand)
449}
450
451// The last operand of `node`, by the same rule as `wrapped_operand`: for
452// a wrapper that evaluates to its final element (a Perl list in scalar
453// context, a Ruby statement sequence) and for a slot that follows an
454// unnamed one (a C# declarator's initialiser after its name).
455pub(super) fn last_operand<'a>(node: &Node<'a>) -> Option<Node<'a>> {
456 node.children().filter(is_operand).last()
457}
458
459// Whether `child` can occupy an operand slot: named, and not an `extra`.
460// The `extra` flag is read off the tree-sitter node because the `Node`
461// wrapper exposes no accessor for it.
462pub(super) fn is_operand(child: &Node) -> bool {
463 child.is_named() && !child.as_tree_sitter().is_extra()
464}
465
466// One step of a language's value peel: the operand a wrapper evaluates
467// to — `(…)`, a negation, an assignment's value, a type cast — and
468// whether that wrapper itself proves the operand boolean (only a
469// negation does). `None` for anything that is not a wrapper.
470pub(super) type PeelStep<'a> = fn(&Node<'a>) -> Option<(Node<'a>, bool)>;
471
472// Every wrapper `step` descends, peeled: the operand they evaluate to,
473// and whether any layer proved it boolean.
474pub(super) fn peel<'a>(node: &Node<'a>, step: PeelStep<'a>) -> (Node<'a>, bool) {
475 let mut node = *node;
476 let mut proves_boolean = false;
477 while let Some((operand, proves)) = step(&node) {
478 proves_boolean |= proves;
479 node = operand;
480 }
481 (node, proves_boolean)
482}
483
484// Scores one boolean slot — an `if` / `while` predicate, a ternary
485// condition, a guard, an operand of an `&&` / `||` chain — as
486// Fitzpatrick's "unary conditional expression" (Rule 6 / 7 / 9): one
487// condition, unless another arm already charged the same decision.
488// `scores_itself` says whether the peeled predicate is a comparison,
489// chain, ternary or other construct some arm of the language's
490// `compute` already counts, and must agree with those arms exactly: a
491// kind listed here and counted nowhere leaves its slot at zero, and one
492// counted there but missing here pays twice.
493//
494// The slots once paid only for an occupant that peeled down to a fixed
495// list of terminal kinds, so every kind missing from the list — `-x`,
496// `x + 1`, `this`, `*p`, a lambda — scored 0 against a cyclomatic
497// decision of 1 (#1520, #1526). Asking whether the decision is already
498// paid for covers every kind the grammar can put in the slot, including
499// ones a future grammar adds. Each language supplies only its peel step
500// and `scores_itself`, so the rule has this one home (lesson 59).
501pub(super) fn count_boolean_slot<'a>(
502 slot: &Node<'a>,
503 step: PeelStep<'a>,
504 scores_itself: fn(&Node<'a>) -> bool,
505 conditions: &mut f64,
506) {
507 if !scores_itself(&peel(slot, step).0) {
508 *conditions += 1.;
509 }
510}
511
512// The same rule for an operand outside a boolean slot — a call
513// argument, a `return` value, a ternary branch. Those are type-free, so
514// the operand is a slot only when the peel proves it boolean through a
515// negation layer: `f(!x)` scores one, `f(x)` and `f(-x)` none. A
516// negation is a slot whatever it negates (`!this`, `!-x`), exactly as
517// Ruby's negated ternary branch is (#1529).
518pub(super) fn count_negated_operand<'a>(
519 operand: &Node<'a>,
520 step: PeelStep<'a>,
521 scores_itself: fn(&Node<'a>) -> bool,
522 conditions: &mut f64,
523) {
524 let (node, proves_boolean) = peel(operand, step);
525 if proves_boolean && !scores_itself(&node) {
526 *conditions += 1.;
527 }
528}
529
530// A language's scorer for one operand: its `count_boolean_slot` or
531// `count_negated_operand` wrapper.
532pub(super) type OperandScorer = fn(&Node, &mut f64);
533
534// Fitzpatrick Rule 9 for a chain whose grammar names its two operands
535// `left` and `right`: each is scored by `score`. Read by field rather
536// than as every operand child, so a comment beside the operator — a
537// named child too — does not pay. The caller owns the gate deciding
538// that `chain` is a chain at all.
539pub(super) fn count_field_operands(chain: &Node, score: OperandScorer, conditions: &mut f64) {
540 for field in ["left", "right"] {
541 if let Some(operand) = chain.child_by_field_name(field) {
542 score(&operand, conditions);
543 }
544 }
545}
546
547// Each operand child of `list` — an argument list, a `return` value
548// list, a chain whose grammar names no operand field — scored by
549// `score`. `is_operand` keeps punctuation and comments out.
550pub(super) fn count_each_operand(list: &Node, score: OperandScorer, conditions: &mut f64) {
551 for operand in list.children().filter(is_operand) {
552 score(&operand, conditions);
553 }
554}
555
556// Default no-op `Abc` impls. Audited in #188; the matrix below
557// records the rationale for every entry so the no-op default is a
558// deliberate choice, not scaffolding leftover.
559//
560// Real defaults (the language has no construct ABC measures, so the
561// metric is genuinely 0):
562// - PreprocCode, CcommentCode: no executable code (comments /
563// preprocessor lines only).
564implement_metric_trait!(Abc, PreprocCode, CcommentCode);
565
566#[cfg(test)]
567#[allow(
568 clippy::float_cmp,
569 clippy::cast_precision_loss,
570 clippy::cast_possible_truncation,
571 clippy::cast_sign_loss,
572 clippy::similar_names,
573 clippy::doc_markdown,
574 clippy::needless_raw_string_hashes,
575 clippy::too_many_lines
576)]
577mod tests {
578 #[cfg(feature = "csharp")]
579 use crate::test_support::assert_csharp_fixture_spells;
580 use crate::test_support::{
581 assert_fixture_spells, ast_has_kind_id, check_func_space_only_shim,
582 check_metrics_only_shim, child_space, metrics_verbatim,
583 };
584 // Hand-written, not derived. A trait is used through its methods, so
585 // nothing in a test body names `ParserTrait` and
586 // `check-test-lang-gates.py` derives no gate for an import at all,
587 // so an ungated one stays compiled into builds that never call
588 // `<Lang>Parser::new`. These seven are the languages that do; a new
589 // `XParser::new` test in this module needs its feature added here,
590 // and the leg that lacks it says so as an unused import.
591 #[cfg(any(
592 feature = "csharp",
593 feature = "groovy",
594 feature = "kotlin",
595 feature = "php",
596 feature = "ruby",
597 feature = "rust",
598 feature = "typescript",
599 ))]
600 use crate::traits::ParserTrait;
601
602 use super::*;
603
604 check_metrics_only_shim!(check_metrics, Abc);
605 // Every `check_func_space` caller in this module is an ABC-versus-
606 // cyclomatic parity test (`abc.conditions() == cyclomatic() - 1` on
607 // the same space), so the func-space shim carries Cyclomatic too.
608 check_func_space_only_shim!(check_func_space, Abc, Cyclomatic);
609
610 /// `abc.conditions_sum()` of `src` under a walk restricted to ABC
611 /// and the metrics it resolves, for the cross-language parity tests
612 /// that compare one construct across sibling grammars.
613 /// `check_metrics` expands to a plain `fn` and so cannot close over
614 /// a reference value; this can. Restricted rather than
615 /// `MetricsOptions::default()` per `metrics_verbatim`'s own doc
616 /// (#1127).
617 #[cfg(any(
618 feature = "c",
619 feature = "cpp",
620 feature = "csharp",
621 feature = "go",
622 feature = "java",
623 feature = "javascript",
624 feature = "mozcpp",
625 feature = "mozjs",
626 feature = "objc",
627 feature = "perl",
628 feature = "typescript",
629 ))]
630 fn abc_conditions(lang: LANG, src: &str) -> u64 {
631 metrics_verbatim(
632 lang,
633 src.as_bytes(),
634 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
635 )
636 .abc
637 .conditions_sum()
638 }
639
640 // Recurse to the innermost function space and assert the invariant
641 // named above there. `conditions()` is that one space's own count,
642 // so it pairs with `cyclomatic()` and never `cyclomatic_sum()`,
643 // which folds in a base of 1 per nested space.
644 #[cfg(any(
645 feature = "cpp",
646 feature = "csharp",
647 feature = "groovy",
648 feature = "java",
649 feature = "perl",
650 feature = "php",
651 feature = "typescript",
652 ))]
653 fn assert_deepest_conditions_match_cyclomatic(space: &crate::FuncSpace, expected: u64) {
654 let mut deepest = space;
655 while let Some(child) = deepest.spaces.last() {
656 deepest = child;
657 }
658 let decisions = deepest.metrics.cyclomatic.cyclomatic() - 1;
659 assert_eq!(decisions, expected);
660 assert_eq!(deepest.metrics.abc.conditions(), decisions);
661 }
662
663 // The sibling form of the helper above, for a fixture whose point is
664 // that several *different* spellings of one construct agree. The
665 // deepest-space walk cannot serve those: it follows `spaces.last()`
666 // and so inspects exactly one member, which is the shape #1383's
667 // over-count hid behind — a merged root total cannot tell a
668 // relational method scoring 2 from the constant control scoring 2.
669 #[cfg(any(feature = "csharp", feature = "elixir"))]
670 fn assert_every_member_scores(
671 container: &crate::FuncSpace,
672 members: usize,
673 expected: u64,
674 why: &str,
675 ) {
676 // The count is a parameter rather than a `!is_empty()` check so
677 // a caller cannot omit it: without it a fixture that lost every
678 // member but one still satisfies "every member scores N".
679 assert_eq!(
680 container.spaces.len(),
681 members,
682 "member count changed — the fixture moved, not the metric"
683 );
684 for member in &container.spaces {
685 let name = member.name.as_deref().unwrap_or("?");
686 assert_eq!(member.metrics.abc.conditions(), expected, "{name}: {why}");
687 assert_eq!(
688 member.metrics.abc.conditions(),
689 member.metrics.cyclomatic.cyclomatic() - 1,
690 "{name}: §8 parity with the cyclomatic decision count"
691 );
692 }
693 }
694
695 // The second sibling, for a fixture whose point is that a construct
696 // moves a member *off* the value its unguarded control scores.
697 // `assert_every_member_scores` cannot serve those: it takes one
698 // expected value for the whole container, and every #1422 guard
699 // fixture deliberately pairs guarded members with an unguarded
700 // control one lower — which is the comparison the test exists to
701 // make. Both tiers per member, because a guard is a decision in each
702 // and a fix that moved only one of them would satisfy neither claim
703 // on its own, and because a member may legitimately sit *above* its
704 // decision count — an `else` arm, or a comparison nested inside
705 // another comparison, is an ABC condition with no cyclomatic
706 // decision behind it (#1421).
707 #[cfg(any(
708 feature = "csharp",
709 feature = "elixir",
710 feature = "go",
711 feature = "groovy",
712 feature = "java",
713 feature = "kotlin",
714 feature = "perl",
715 feature = "python",
716 feature = "ruby",
717 feature = "rust",
718 ))]
719 #[track_caller]
720 fn assert_members_score(container: &crate::FuncSpace, expected: &[(&str, u64, u64)]) {
721 assert_eq!(
722 container.spaces.len(),
723 expected.len(),
724 "member count changed — the fixture moved, not the metric"
725 );
726 for (name, conditions, cyclomatic) in expected {
727 let member = child_space(container, name);
728 assert_eq!(
729 member.metrics.abc.conditions(),
730 *conditions,
731 "{name}: abc.conditions"
732 );
733 assert_eq!(
734 member.metrics.cyclomatic.cyclomatic(),
735 *cyclomatic,
736 "{name}: cyclomatic"
737 );
738 }
739 }
740
741 /// Regression for #227: a `Stats::default()` that never sees an
742 /// observation must not leak the `f64::MAX` sentinel for
743 /// `assignments_min`, `branches_min`, or `conditions_min`. All
744 /// three getters collapse the sentinel to `0.0` so JSON never
745 /// emits `1.7976931e308`.
746 #[test]
747 fn abc_empty_file_min_is_zero() {
748 let stats = Stats::default();
749 assert_eq!(stats.assignments_min(), 0);
750 assert_eq!(stats.branches_min(), 0);
751 assert_eq!(stats.conditions_min(), 0);
752 }
753
754 // The `EQ` arm of `java_count_token_assignment`: a plain `=` counts
755 // unless `java_eq_initializes_final_binding` finds it initialising a
756 // `final` binding, whether the declaration is a local or a field.
757 #[cfg(feature = "java")]
758 #[test]
759 fn java_eq_arm_counts_outside_final_declarations() {
760 check_metrics::<JavaParser>(
761 "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
762 "foo.java",
763 |metric| {
764 // `int x = 0;` is not `final`, so it counts like each
765 // `x = N;` that follows.
766 assert_eq!(metric.abc.assignments_sum(), 4);
767 },
768 );
769 }
770
771 #[cfg(feature = "java")]
772 #[test]
773 fn java_eq_arm_skips_final_initializers() {
774 check_metrics::<JavaParser>(
775 "class A {
776 final int X = 1;
777 @Deprecated private final int Y = 2;
778 void m() { final int Z = 3; }
779 }",
780 "foo.java",
781 |metric| {
782 // All three `=` tokens are `final` initializers — the
783 // second behind an annotation and an access modifier in
784 // the same `modifiers` node — so assignments are 0
785 // across all spaces.
786 assert_eq!(metric.abc.assignments_sum(), 0);
787 },
788 );
789 }
790
791 #[cfg(feature = "java")]
792 #[test]
793 fn java_final_initializer_does_not_suppress_the_assignments_inside_it() {
794 // The sentinel stack the structural predicate replaced stayed
795 // live from `final` to the next `;`, so every `=` nested in the
796 // initializer — a lambda body's, an array initializer's — was
797 // suppressed with the declarator's own. Java lambdas open no
798 // space, so nothing else separated them. Each row names the
799 // pre-fix value.
800 let cases = [
801 ("void m() { final Runnable r = () -> { x = 1; }; }", 1, 0),
802 (
803 "void m() { final Runnable r = () -> { x = 1; y = 2; }; }",
804 2,
805 1,
806 ),
807 ("void m() { final Runnable r = () -> x = 1; }", 1, 0),
808 ("void m() { final int[] a = { x = 1 }; }", 1, 0),
809 ("private final Runnable f = () -> { x = 1; };", 1, 0),
810 // The `;` that closed the leak is the reference: the same
811 // body after a `final` local counts as it always did.
812 ("void m() { final int q = 1; x = 1; }", 1, 1),
813 ];
814 let mut ran = 0;
815 for (body, expected, before) in cases {
816 let src = format!("class K {{ int x, y; {body} }}\n");
817 let assignments = metrics_verbatim(
818 LANG::Java,
819 src.as_bytes(),
820 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
821 )
822 .abc
823 .assignments_sum();
824 assert_eq!(assignments, expected, "`{body}` (pre-fix {before})");
825 ran += 1;
826 }
827 assert_eq!(ran, cases.len());
828 assert!(cases.iter().any(|&(_, now, before)| now != before));
829 }
830
831 // Constant declarations are not counted as assignments
832 #[cfg(feature = "java")]
833 #[test]
834 fn java_constant_declarations() {
835 check_metrics::<JavaParser>(
836 "class A {
837 private final int X1 = 0, Y1 = 0;
838 public final float PI = 3.14f;
839 final static String HELLO = \"Hello,\";
840 protected String world = \" world!\"; // +1a
841 public float e = 2.718f; // +1a
842 private int x2 = 1, y2 = 2; // +2a
843
844 void m() {
845 final int Z1 = 0, Z2 = 0, Z3 = 0;
846 final float T = 0.0f;
847 int z1 = 1, z2 = 2, z3 = 3; // +3a
848 float t = 60.0f; // +1a
849 }
850 }",
851 "foo.java",
852 |metric| {
853 // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
854 // space count: 3 (1 unit, 1 class and 1 method)
855 insta::assert_json_snapshot!(
856 metric.abc,
857 @r#"
858 {
859 "assignments": 8,
860 "branches": 0,
861 "conditions": 0,
862 "magnitude": 8.0,
863 "value": 0.0,
864 "assignments_average": 2.6666666666666665,
865 "branches_average": 0.0,
866 "conditions_average": 0.0,
867 "assignments_min": 0,
868 "assignments_max": 4,
869 "branches_min": 0,
870 "branches_max": 0,
871 "conditions_min": 0,
872 "conditions_max": 0
873 }
874 "#
875 );
876 },
877 );
878 }
879
880 // "In computer science, conditionals (that is, conditional statements, conditional expressions
881 // and conditional constructs,) are programming language commands for handling decisions."
882 // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
883 // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
884 // Variables, method invocations and true or false values used inside
885 // variable declarations and assignment expressions are not counted as conditions
886 #[cfg(feature = "java")]
887 #[test]
888 fn java_declarations_with_conditions() {
889 check_metrics::<JavaParser>(
890 "
891 boolean a = (1 > 2); // +1a +1c
892 boolean b = 3 > 4; // +1a +1c
893 boolean c = (1 > 2) && 3 > 4; // +1a +2c
894 boolean d = b && (x > 5) || c; // +1a +3c
895 boolean e = !d; // +1a +1c
896 boolean f = ((!false)); // +1a +1c
897 boolean g = !(!(true)); // +1a +1c
898 boolean h = true; // +1a
899 boolean i = (false); // +1a
900 boolean j = (((((true))))); // +1a
901 boolean k = (((((m()))))); // +1a +1b
902 boolean l = (((((!m()))))); // +1a +1b +1c
903 boolean m = (!(!((m())))); // +1a +1b +1c
904 List<String> n = null; // +1a (< and > used for generic types are not counted as conditions)
905 ",
906 "foo.java",
907 |metric| {
908 // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
909 // space count: 1 (1 unit)
910 insta::assert_json_snapshot!(
911 metric.abc,
912 @r#"
913 {
914 "assignments": 14,
915 "branches": 3,
916 "conditions": 12,
917 "magnitude": 18.681541692269406,
918 "value": 18.681541692269406,
919 "assignments_average": 14.0,
920 "branches_average": 3.0,
921 "conditions_average": 12.0,
922 "assignments_min": 14,
923 "assignments_max": 14,
924 "branches_min": 3,
925 "branches_max": 3,
926 "conditions_min": 12,
927 "conditions_max": 12
928 }
929 "#
930 );
931 },
932 );
933 }
934
935 // Conditions can be found in assignment expressions
936 #[cfg(feature = "java")]
937 #[test]
938 fn java_assignments_with_conditions() {
939 check_metrics::<JavaParser>(
940 "
941 a = 2 < 1; // +1a +1c
942 b = (4 >= 3) && 2 <= 1; // +1a +2c
943 c = a || (x != 10) && b; // +1a +3c
944 d = !false; // +1a +1c
945 e = (!false); // +1a +1c
946 f = !(false); // +1a +1c
947 g = (!(((true)))); // +1a +1c
948 h = ((true)); // +1a
949 i = !m(); // +1a +1b +1c
950 j = !((m())); // +1a +1b +1c
951 k = (!(m())); // +1a +1b +1c
952 l = ((!(m()))); // +1a +1b +1c
953 m = !B.<Integer>m(2); // +1a +1b +1c
954 n = !((B.<Integer>m(4))); // +1a +1b +1c
955 ",
956 "foo.java",
957 |metric| {
958 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
959 // space count: 1 (1 unit)
960 insta::assert_json_snapshot!(
961 metric.abc,
962 @r#"
963 {
964 "assignments": 14,
965 "branches": 6,
966 "conditions": 16,
967 "magnitude": 22.090722034374522,
968 "value": 22.090722034374522,
969 "assignments_average": 14.0,
970 "branches_average": 6.0,
971 "conditions_average": 16.0,
972 "assignments_min": 14,
973 "assignments_max": 14,
974 "branches_min": 6,
975 "branches_max": 6,
976 "conditions_min": 16,
977 "conditions_max": 16
978 }
979 "#
980 );
981 },
982 );
983 }
984
985 // Conditions can be found in method arguments
986 #[cfg(feature = "java")]
987 #[test]
988 fn java_methods_arguments_with_conditions() {
989 check_metrics::<JavaParser>(
990 "
991 m1(a); // +1b
992 m2(a, b); // +1b
993 m3(true, (false), (((true)))); // +1b
994 m3(m1(false), m1(true), m1(false)); // +4b
995 m1(!a); // +1b +1c
996 m2((((a))), (!b)); // +1b +1c
997 m3(!(a), b, !!!c); // +1b +2c
998 m3(a, !b, m2(!a, !m2(!b, !m1(!c)))); // +4b +6c
999 ",
1000 "foo.java",
1001 |metric| {
1002 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
1003 // space count: 1 (1 unit)
1004 insta::assert_json_snapshot!(
1005 metric.abc,
1006 @r#"
1007 {
1008 "assignments": 0,
1009 "branches": 14,
1010 "conditions": 10,
1011 "magnitude": 17.204650534085253,
1012 "value": 17.204650534085253,
1013 "assignments_average": 0.0,
1014 "branches_average": 14.0,
1015 "conditions_average": 10.0,
1016 "assignments_min": 0,
1017 "assignments_max": 0,
1018 "branches_min": 14,
1019 "branches_max": 14,
1020 "conditions_min": 10,
1021 "conditions_max": 10
1022 }
1023 "#
1024 );
1025 },
1026 );
1027 }
1028
1029 // "A unary conditional expression is an implicit condition that uses no relational operators."
1030 // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
1031 // https://www.softwarerenovation.com/Articles.aspx (page 5)
1032 #[cfg(feature = "java")]
1033 #[test]
1034 fn java_if_single_conditions() {
1035 check_metrics::<JavaParser>(
1036 "
1037 if ( a < 0 ) {} // +1c
1038 if ( ((a != 0)) ) {} // +1c
1039 if ( !(a > 0) ) {} // +1c
1040 if ( !(((a == 0))) ) {} // +1c
1041 if ( b.m1() ) {} // +1b +1c
1042 if ( !b.m1() ) {} // +1b +1c
1043 if ( !!b.m2() ) {} // +1b +1c
1044 if ( (!(b.m1())) ) {} // +1b +1c
1045 if ( (!(!b.m1())) ) {} // +1b +1c
1046 if ( ((b.m2())) ) {} // +1b +1c
1047 if ( ((b.m().m1())) ) {} // +2b +1c
1048 if ( c ) {} // +1c
1049 if ( !c ) {} // +1c
1050 if ( !!!!!!!!!!c ) {} // +1c
1051 if ( (((c))) ) {} // +1c
1052 if ( (((!c))) ) {} // +1c
1053 if ( ((!(c))) ) {} // +1c
1054 if ( true ) {} // +1c
1055 if ( !true ) {} // +1c
1056 if ( ((false)) ) {} // +1c
1057 if ( !(!(false)) ) {} // +1c
1058 if ( !!!false ) {} // +1c
1059 ",
1060 "foo.java",
1061 |metric| {
1062 // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
1063 // space count: 1 (1 unit)
1064 insta::assert_json_snapshot!(
1065 metric.abc,
1066 @r#"
1067 {
1068 "assignments": 0,
1069 "branches": 8,
1070 "conditions": 22,
1071 "magnitude": 23.40939982143925,
1072 "value": 23.40939982143925,
1073 "assignments_average": 0.0,
1074 "branches_average": 8.0,
1075 "conditions_average": 22.0,
1076 "assignments_min": 0,
1077 "assignments_max": 0,
1078 "branches_min": 8,
1079 "branches_max": 8,
1080 "conditions_min": 22,
1081 "conditions_max": 22
1082 }
1083 "#
1084 );
1085 },
1086 );
1087 }
1088
1089 #[cfg(feature = "java")]
1090 #[test]
1091 fn java_if_multiple_conditions() {
1092 check_metrics::<JavaParser>(
1093 "
1094 if ( a || b || c || d ) {} // +4c
1095 if ( a || b && c && d ) {} // +4c
1096 if ( x < y && a == b ) {} // +2c
1097 if ( ((z < (x + y))) ) {} // +1c
1098 if ( a || ((((b))) && c) ) {} // +3c
1099 if ( a && ((((a == b))) && c) ) {} // +3c
1100 if ( a || ((((a == b))) || ((c))) ) {} // +3c
1101 if ( x < y && B.m() ) {} // +1b +2c
1102 if ( x < y && !(((B.m()))) ) {} // +1b +2c
1103 if ( !(x < y) && !B.m() ) {} // +1b +2c
1104 if ( !!!(!!!(a)) && B.m() || // +1b +2c
1105 !B.m() && (((x > 4))) ) {} // +1b +2c
1106 ",
1107 "foo.java",
1108 |metric| {
1109 // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
1110 // space count: 1 (1 unit)
1111 insta::assert_json_snapshot!(
1112 metric.abc,
1113 @r#"
1114 {
1115 "assignments": 0,
1116 "branches": 5,
1117 "conditions": 30,
1118 "magnitude": 30.4138126514911,
1119 "value": 30.4138126514911,
1120 "assignments_average": 0.0,
1121 "branches_average": 5.0,
1122 "conditions_average": 30.0,
1123 "assignments_min": 0,
1124 "assignments_max": 0,
1125 "branches_min": 5,
1126 "branches_max": 5,
1127 "conditions_min": 30,
1128 "conditions_max": 30
1129 }
1130 "#
1131 );
1132 },
1133 );
1134 }
1135
1136 #[cfg(feature = "java")]
1137 #[test]
1138 fn java_while_and_do_while_conditions() {
1139 check_metrics::<JavaParser>(
1140 "
1141 while ( (!(!(!(a)))) ) {} // +1c
1142 while ( b || 1 > 2 ) {} // +2c
1143 while ( x.m() && (((c))) ) {} // +1b +2c
1144 do {} while ( !!!(((!!!a))) ); // +1c
1145 do {} while ( a || (b && c) ); // +3c
1146 do {} while ( !x.m() && 1 > 2 || !true ); // +1b +3c
1147 ",
1148 "foo.java",
1149 |metric| {
1150 // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
1151 // space count: 1 (1 unit)
1152 insta::assert_json_snapshot!(
1153 metric.abc,
1154 @r#"
1155 {
1156 "assignments": 0,
1157 "branches": 2,
1158 "conditions": 12,
1159 "magnitude": 12.165525060596439,
1160 "value": 12.165525060596439,
1161 "assignments_average": 0.0,
1162 "branches_average": 2.0,
1163 "conditions_average": 12.0,
1164 "assignments_min": 0,
1165 "assignments_max": 0,
1166 "branches_min": 2,
1167 "branches_max": 2,
1168 "conditions_min": 12,
1169 "conditions_max": 12
1170 }
1171 "#
1172 );
1173 },
1174 );
1175 }
1176
1177 // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
1178 // "These are cases where a single variable/field/value is treated as a boolean value.
1179 // Examples include `if (x)` and `return !ready`."
1180 // According to this definition, unary conditional expressions are counted also in function return values.
1181 // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
1182 // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
1183 #[cfg(feature = "java")]
1184 #[test]
1185 fn java_return_with_conditions() {
1186 check_metrics::<JavaParser>(
1187 "class A {
1188 boolean m1() {
1189 return !(z >= 0); // +1c
1190 }
1191 boolean m2() {
1192 return (((!x))); // +1c
1193 }
1194 boolean m3() {
1195 return x && y; // +2c
1196 }
1197 boolean m4() {
1198 return y || (z < 0); // +2c
1199 }
1200 boolean m5() {
1201 return x || y ? // +3c (two unary conditions and one ?)
1202 true : false;
1203 }
1204 }",
1205 "foo.java",
1206 |metric| {
1207 // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
1208 // space count: 7 (1 unit, 1 class and 5 methods)
1209 insta::assert_json_snapshot!(
1210 metric.abc,
1211 @r#"
1212 {
1213 "assignments": 0,
1214 "branches": 0,
1215 "conditions": 9,
1216 "magnitude": 9.0,
1217 "value": 0.0,
1218 "assignments_average": 0.0,
1219 "branches_average": 0.0,
1220 "conditions_average": 1.2857142857142858,
1221 "assignments_min": 0,
1222 "assignments_max": 0,
1223 "branches_min": 0,
1224 "branches_max": 0,
1225 "conditions_min": 0,
1226 "conditions_max": 3
1227 }
1228 "#
1229 );
1230 },
1231 );
1232 }
1233
1234 // Variables, method invocations, and true or false values
1235 // inside return statements are not counted as conditions
1236 #[cfg(feature = "java")]
1237 #[test]
1238 fn java_return_without_conditions() {
1239 check_metrics::<JavaParser>(
1240 "class A {
1241 boolean m1() {
1242 return x;
1243 }
1244 boolean m2() {
1245 return (x);
1246 }
1247 boolean m3() {
1248 return y.m(); // +1b
1249 }
1250 boolean m4() {
1251 return false;
1252 }
1253 void m5() {
1254 return;
1255 }
1256 }",
1257 "foo.java",
1258 |metric| {
1259 // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
1260 // space count: 7 (1 unit, 1 class and 5 methods)
1261 insta::assert_json_snapshot!(
1262 metric.abc,
1263 @r#"
1264 {
1265 "assignments": 0,
1266 "branches": 1,
1267 "conditions": 0,
1268 "magnitude": 1.0,
1269 "value": 0.0,
1270 "assignments_average": 0.0,
1271 "branches_average": 0.14285714285714285,
1272 "conditions_average": 0.0,
1273 "assignments_min": 0,
1274 "assignments_max": 0,
1275 "branches_min": 0,
1276 "branches_max": 1,
1277 "conditions_min": 0,
1278 "conditions_max": 0
1279 }
1280 "#
1281 );
1282 },
1283 );
1284 }
1285
1286 // Variables, method invocations, and true or false values
1287 // in lambda expression return values are not counted as conditions
1288 #[cfg(feature = "java")]
1289 #[test]
1290 fn java_lambda_expressions_return_with_conditions() {
1291 check_metrics::<JavaParser>(
1292 "
1293 Predicate<Boolean> p1 = a -> a; // +1a
1294 Predicate<Boolean> p2 = b -> true; // +1a
1295 Predicate<Boolean> p3 = c -> m(); // +1a
1296 Predicate<Integer> p4 = d -> d > 10; // +1a +1c
1297 Predicate<Boolean> p5 = (e) -> !e; // +1a +1c
1298 Predicate<Boolean> p6 = (f) -> !((!f)); // +1a +1c
1299 Predicate<Boolean> p7 = (g) -> !g && true; // +1a +2c
1300 BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
1301 BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
1302 return j || k; // +1a +2c
1303 };
1304 ",
1305 "foo.java",
1306 |metric| {
1307 // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
1308 // space count: 1 (1 unit)
1309 insta::assert_json_snapshot!(
1310 metric.abc,
1311 @r#"
1312 {
1313 "assignments": 9,
1314 "branches": 1,
1315 "conditions": 9,
1316 "magnitude": 12.767145334803704,
1317 "value": 12.767145334803704,
1318 "assignments_average": 9.0,
1319 "branches_average": 1.0,
1320 "conditions_average": 9.0,
1321 "assignments_min": 9,
1322 "assignments_max": 9,
1323 "branches_min": 1,
1324 "branches_max": 1,
1325 "conditions_min": 9,
1326 "conditions_max": 9
1327 }
1328 "#
1329 );
1330 },
1331 );
1332 }
1333
1334 #[cfg(feature = "java")]
1335 #[test]
1336 fn java_for_with_variable_declaration() {
1337 check_metrics::<JavaParser>(
1338 "
1339 for ( int i1 = 0; !(!(!(!a))); i1++ ) {} // +2a +1c
1340 for ( int i2 = 0; !B.m(); i2++ ) {} // +2a +1b +1c
1341 for ( int i3 = 0; a || false; i3++ ) {} // +2a +2c
1342 for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {} // +2a +1b +3c
1343 for ( int i5 = 0; true; i5++ ) {} // +2a +1c
1344 ",
1345 "foo.java",
1346 |metric| {
1347 // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1348 // space count: 1 (1 unit)
1349 insta::assert_json_snapshot!(
1350 metric.abc,
1351 @r#"
1352 {
1353 "assignments": 10,
1354 "branches": 2,
1355 "conditions": 8,
1356 "magnitude": 12.96148139681572,
1357 "value": 12.96148139681572,
1358 "assignments_average": 10.0,
1359 "branches_average": 2.0,
1360 "conditions_average": 8.0,
1361 "assignments_min": 10,
1362 "assignments_max": 10,
1363 "branches_min": 2,
1364 "branches_max": 2,
1365 "conditions_min": 8,
1366 "conditions_max": 8
1367 }
1368 "#
1369 );
1370 },
1371 );
1372 }
1373
1374 #[cfg(feature = "java")]
1375 #[test]
1376 fn java_for_without_variable_declaration() {
1377 check_metrics::<JavaParser>(
1378 "class A{
1379 void m1() {
1380 for (i = 0; x < y; i++) {} // +2a +1c
1381 for (i = 0; ((x < y)); i++) {} // +2a +1c
1382 for (i = 0; !(!(x < y)); i++) {} // +2a +1c
1383 for (i = 0; true; i++) {} // +2a +1c
1384 }
1385 void m2() {
1386 for ( ; true; ) {} // +1c
1387 }
1388 void m3() {
1389 for ( ; ; ) {} // +0c — no condition to count (#1276)
1390 }
1391 }",
1392 "foo.java",
1393 |metric| {
1394 // magnitude: sqrt(64 + 0 + 25) = sqrt(89)
1395 // space count: 5 (1 unit, 1 class and 3 methods)
1396 insta::assert_json_snapshot!(
1397 metric.abc,
1398 @r#"
1399 {
1400 "assignments": 8,
1401 "branches": 0,
1402 "conditions": 5,
1403 "magnitude": 9.433981132056603,
1404 "value": 0.0,
1405 "assignments_average": 1.6,
1406 "branches_average": 0.0,
1407 "conditions_average": 1.0,
1408 "assignments_min": 0,
1409 "assignments_max": 8,
1410 "branches_min": 0,
1411 "branches_max": 0,
1412 "conditions_min": 0,
1413 "conditions_max": 4
1414 }
1415 "#
1416 );
1417 },
1418 );
1419 }
1420
1421 // Issue #1276 changed Java's answer here. `java_walk_for_statement`
1422 // used to read child(3), fall through to child(4) when that was the
1423 // `;` an expression initializer leaves behind, and count a `;` or
1424 // `)` landing there as a vacuously-true condition — so `for (;;)`
1425 // scored one. Nothing in Fitzpatrick's C dimension attributes a
1426 // condition to a `for` keyword; it counts conditional operators and
1427 // unary conditions that are present. Java and Groovy were the only
1428 // two impls disagreeing, and the field-addressed walker now reports
1429 // zero the way the C family, the JS family, PHP, C# and Go all do.
1430 #[cfg(feature = "java")]
1431 #[test]
1432 fn java_empty_for_condition_counts_nothing() {
1433 check_metrics::<JavaParser>(
1434 "class A { void m() { for (;;) { break; } } }",
1435 "foo.java",
1436 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1437 );
1438 // The other empty spelling, and the one the old cascade
1439 // actually mis-scored: with an **expression** initializer the
1440 // children are `for ( init ; ; update )`, so child(3) was the
1441 // separating `;` and child(4) the empty condition's `;`, which
1442 // the vacuous-true arm counted. The `for (int i = 0; ; i++)`
1443 // spelling scored 0 both before and after — Java's
1444 // `local_variable_declaration` swallows its own `;`, putting the
1445 // update expression at child(4), where it matched no arm — so it
1446 // would be a vacuous regression test and is deliberately not
1447 // used here.
1448 check_metrics::<JavaParser>(
1449 "class A { void m() { int i; for (i = 0; ; i++) { break; } } }",
1450 "foo.java",
1451 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1452 );
1453 }
1454
1455 // The second defect the positional cascade carried: tree-sitter
1456 // counts comments among a node's children, so a comment anywhere in
1457 // the header shifted every index and the condition went unread —
1458 // the same failure #1181 removed from `java_walk_ternary`. Reading
1459 // the `condition` field cannot shift.
1460 #[cfg(feature = "java")]
1461 #[test]
1462 fn java_for_condition_survives_a_header_comment() {
1463 check_metrics::<JavaParser>(
1464 "class A { void m(boolean a) { for (; /* n */ a; ) { break; } } }",
1465 "foo.java",
1466 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1467 );
1468 // Unchanged control: the same loop without the comment. Both
1469 // spellings must agree, which is the property the positional
1470 // form broke.
1471 check_metrics::<JavaParser>(
1472 "class A { void m(boolean a) { for (; a; ) { break; } } }",
1473 "foo.java",
1474 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1475 );
1476 }
1477
1478 // Variables, method invocations, and true or false values
1479 // in ternary expression return values are not counted as conditions
1480 #[cfg(feature = "java")]
1481 #[test]
1482 fn java_ternary_conditions() {
1483 check_metrics::<JavaParser>(
1484 "
1485 a = true; // +1a
1486 b = a ? true : false; // +1a +2c
1487 c = ((((a)))) ? !false : !b; // +1a +4c
1488 d = !this.m() ? !!a : (false); // +1a +1b +3c
1489 e = !(a) && b ? ((c)) : !d; // +1a +4c
1490 if ( this.m() ? a : !this.m() ) {} // +2b +3c
1491 if ( x > 0 ? !(false) : this.m() ) {} // +1b +3c
1492 if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {} // +5c
1493 ",
1494 "foo.java",
1495 |metric| {
1496 // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1497 // space count: 1 (1 unit)
1498 insta::assert_json_snapshot!(
1499 metric.abc,
1500 @r#"
1501 {
1502 "assignments": 5,
1503 "branches": 4,
1504 "conditions": 24,
1505 "magnitude": 24.839484696748443,
1506 "value": 24.839484696748443,
1507 "assignments_average": 5.0,
1508 "branches_average": 4.0,
1509 "conditions_average": 24.0,
1510 "assignments_min": 5,
1511 "assignments_max": 5,
1512 "branches_min": 4,
1513 "branches_max": 4,
1514 "conditions_min": 24,
1515 "conditions_max": 24
1516 }
1517 "#
1518 );
1519 },
1520 );
1521 }
1522
1523 #[cfg(feature = "bash")]
1524 #[test]
1525 fn bash_assignments_only() {
1526 check_metrics::<BashParser>(
1527 "f() {
1528 a=1
1529 b=2
1530 c+=3
1531 }",
1532 "foo.sh",
1533 |metric| {
1534 insta::assert_json_snapshot!(
1535 metric.abc,
1536 @r#"
1537 {
1538 "assignments": 3,
1539 "branches": 0,
1540 "conditions": 0,
1541 "magnitude": 3.0,
1542 "value": 0.0,
1543 "assignments_average": 1.5,
1544 "branches_average": 0.0,
1545 "conditions_average": 0.0,
1546 "assignments_min": 0,
1547 "assignments_max": 3,
1548 "branches_min": 0,
1549 "branches_max": 0,
1550 "conditions_min": 0,
1551 "conditions_max": 0
1552 }
1553 "#
1554 );
1555 },
1556 );
1557 }
1558
1559 #[cfg(feature = "bash")]
1560 #[test]
1561 fn bash_commands_only() {
1562 check_metrics::<BashParser>(
1563 "f() {
1564 echo a
1565 ls
1566 }",
1567 "foo.sh",
1568 |metric| {
1569 insta::assert_json_snapshot!(
1570 metric.abc,
1571 @r#"
1572 {
1573 "assignments": 0,
1574 "branches": 2,
1575 "conditions": 0,
1576 "magnitude": 2.0,
1577 "value": 0.0,
1578 "assignments_average": 0.0,
1579 "branches_average": 1.0,
1580 "conditions_average": 0.0,
1581 "assignments_min": 0,
1582 "assignments_max": 0,
1583 "branches_min": 0,
1584 "branches_max": 2,
1585 "conditions_min": 0,
1586 "conditions_max": 0
1587 }
1588 "#
1589 );
1590 },
1591 );
1592 }
1593
1594 #[cfg(feature = "bash")]
1595 #[test]
1596 fn bash_control_flow_counts_conditions() {
1597 // Regression for #696: Bash control-flow branches are ABC
1598 // conditions (a Bash predicate is a command, so the branch keyword
1599 // is the only condition signal). Each mirrors a cyclomatic decision.
1600 //
1601 // expected: 4 conditions — `if` (1) + `elif` (1) + `while` (1) +
1602 // the non-wildcard case arm `a)` (1). The bare-`*)` wildcard arm is
1603 // the Bash analogue of `default:` and is excluded, exactly as the
1604 // cyclomatic standard count excludes it. No comparison / test
1605 // operators appear, so every condition here is control-flow.
1606 check_metrics::<BashParser>(
1607 "f() {
1608 if cmd; then
1609 echo a
1610 elif other; then
1611 echo b
1612 fi
1613 while running; do
1614 echo c
1615 done
1616 case \"$x\" in
1617 a) echo d ;;
1618 *) echo e ;;
1619 esac
1620 }",
1621 "foo.sh",
1622 |metric| {
1623 assert_eq!(metric.abc.conditions_sum(), 4);
1624 },
1625 );
1626 }
1627
1628 #[cfg(feature = "bash")]
1629 #[test]
1630 fn bash_conditions_mix() {
1631 // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
1632 // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
1633 // inside `[ ]`. Each `if` body's `echo` contributes a branch.
1634 //
1635 // expected: 8 conditions — each of the four `if`s contributes one
1636 // for the control-flow branch (#696) plus one for its comparison /
1637 // test operator (`==`, `!=`, `<`, `-z`). 4 branches (one `echo`
1638 // each). magnitude = sqrt(4² + 8²) = sqrt(80).
1639 check_metrics::<BashParser>(
1640 "f() {
1641 if [[ \"$a\" == \"$b\" ]]; then
1642 echo eq
1643 fi
1644 if [[ \"$x\" != \"$y\" ]]; then
1645 echo ne
1646 fi
1647 if (( $a < $b )); then
1648 echo lt
1649 fi
1650 if [ -z \"$x\" ]; then
1651 echo empty
1652 fi
1653 }",
1654 "foo.sh",
1655 |metric| {
1656 assert_eq!(metric.abc.conditions_sum(), 8);
1657 assert_eq!(metric.abc.branches_sum(), 4);
1658 insta::assert_json_snapshot!(
1659 metric.abc,
1660 @r#"
1661 {
1662 "assignments": 0,
1663 "branches": 4,
1664 "conditions": 8,
1665 "magnitude": 8.94427190999916,
1666 "value": 0.0,
1667 "assignments_average": 0.0,
1668 "branches_average": 2.0,
1669 "conditions_average": 4.0,
1670 "assignments_min": 0,
1671 "assignments_max": 0,
1672 "branches_min": 0,
1673 "branches_max": 4,
1674 "conditions_min": 0,
1675 "conditions_max": 8
1676 }
1677 "#
1678 );
1679 },
1680 );
1681 }
1682
1683 #[cfg(feature = "bash")]
1684 #[test]
1685 fn bash_redirection_is_not_a_condition() {
1686 // `>` and `<` spell an I/O redirection as well as a comparison, and
1687 // the grammar parents the redirection under `file_redirect` rather
1688 // than `binary_expression`. Ungated, every redirect in a script
1689 // scored a condition: this fixture measured 2 before the parent
1690 // gate — the Bash instance of #1280's positive-parent polarity.
1691 // expected: 0 conditions — no test, no `if`, no comparison.
1692 check_metrics::<BashParser>(
1693 "f() {\n echo hi > out.txt\n read x < in.txt\n}\n",
1694 "foo.sh",
1695 |metric| {
1696 assert_eq!(metric.abc.conditions_sum(), 0);
1697 // The two `echo` / `read` commands still count as branches,
1698 // so the zero above is the gate firing rather than the walk
1699 // skipping the function body.
1700 assert_eq!(metric.abc.branches_sum(), 2);
1701 },
1702 );
1703 }
1704
1705 #[cfg(feature = "bash")]
1706 #[test]
1707 fn bash_comparison_inside_an_arithmetic_or_test_context_is_a_condition() {
1708 // The positive control for the gate above: the same tokens under a
1709 // `binary_expression` are real comparisons in both the `[[ … ]]`
1710 // test form and the `(( … ))` arithmetic form, and still count.
1711 // expected: 4 — one `if` control-flow condition and one `>` per
1712 // function.
1713 check_metrics::<BashParser>(
1714 "f() {\n if [[ $a > $b ]]; then :; fi\n}\ng() {\n if (( a > b )); then :; fi\n}\n",
1715 "foo.sh",
1716 |metric| {
1717 assert_eq!(metric.abc.conditions_sum(), 4);
1718 },
1719 );
1720 }
1721
1722 #[cfg(feature = "bash")]
1723 #[test]
1724 fn bash_arithmetic_ternary_is_a_condition() {
1725 // The ABC half of #1268. Cyclomatic and cognitive both count Bash's
1726 // only ternary form; ABC did not, so the identical construct scored
1727 // 1 here against the C family's 2.
1728 // expected: 2 — the `>` comparison and the ternary itself, matching
1729 // `int m = a > b ? a : b;` in C.
1730 check_metrics::<BashParser>(
1731 "f() {\n local m=$(( a > b ? a : b ))\n}\n",
1732 "foo.sh",
1733 |metric| {
1734 assert_eq!(metric.abc.conditions_sum(), 2);
1735 },
1736 );
1737 }
1738
1739 #[cfg(feature = "bash")]
1740 #[test]
1741 fn bash_magnitude() {
1742 // Combined assignments + branches + conditions. The single `if`
1743 // contributes two conditions (the control-flow branch, #696, plus
1744 // the `==` operator), so magnitude = sqrt(2² + 1² + 2²) = sqrt(9).
1745 check_metrics::<BashParser>(
1746 "f() {
1747 a=1
1748 b=2
1749 if [[ \"$a\" == \"$b\" ]]; then
1750 echo eq
1751 fi
1752 }",
1753 "foo.sh",
1754 |metric| {
1755 assert_eq!(metric.abc.conditions_sum(), 2);
1756 insta::assert_json_snapshot!(
1757 metric.abc,
1758 @r#"
1759 {
1760 "assignments": 2,
1761 "branches": 1,
1762 "conditions": 2,
1763 "magnitude": 3.0,
1764 "value": 0.0,
1765 "assignments_average": 1.0,
1766 "branches_average": 0.5,
1767 "conditions_average": 1.0,
1768 "assignments_min": 0,
1769 "assignments_max": 2,
1770 "branches_min": 0,
1771 "branches_max": 1,
1772 "conditions_min": 0,
1773 "conditions_max": 2
1774 }
1775 "#
1776 );
1777 },
1778 );
1779 }
1780
1781 #[cfg(feature = "java")]
1782 #[test]
1783 fn java_malformed_parenthesized_no_panic() {
1784 check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
1785 // tree-sitter emits ERROR nodes for this malformed source, so no
1786 // IfStatement, branch, or condition is recognised — all counts are 0.
1787 // Primary goal: the unwrap-free path does not panic.
1788 assert_eq!(metric.abc.assignments(), 0);
1789 assert_eq!(metric.abc.branches(), 0);
1790 assert_eq!(metric.abc.conditions(), 0);
1791 assert_eq!(metric.abc.magnitude(), 0.0);
1792 });
1793 }
1794
1795 #[cfg(feature = "java")]
1796 #[test]
1797 fn java_bool_returning_terminal_kinds_count() {
1798 // Companion to `csharp_bool_returning_terminal_kinds_count`
1799 // (issue #372 / lesson #19). Java's grammar wraps every
1800 // if/while/do condition in `parenthesized_expression`, so
1801 // the gap lived in the former `java_inspect_container`'s
1802 // terminal-arm recognizer: `FieldAccess` (`cfg.flag`), `CastExpression`
1803 // (`(boolean)v`), `ArrayAccess` (`flags[0]`), and
1804 // `InstanceofExpression` (`x instanceof Foo`) were never
1805 // counted. Java has no `await` or `is_pattern` analogues,
1806 // so the C# fix's five-kind set collapses to four here.
1807 //
1808 // `instanceof` has since moved out of the terminal set to an
1809 // unconditional arm (#1461), so it no longer depends on the
1810 // slot to be counted. The total is unchanged, which is the
1811 // point of that change rather than an accident of it.
1812 //
1813 // expected: 4 conditions (one per `if`), 0 assignments,
1814 // 0 branches (no invocations).
1815 check_metrics::<JavaParser>(
1816 "class Cfg { boolean flag; }
1817 class A {
1818 void m(Object v, boolean[] flags, Cfg cfg) {
1819 if (cfg.flag) { }
1820 if ((boolean) v) { }
1821 if (v instanceof Cfg) { }
1822 if (flags[0]) { }
1823 }
1824 }",
1825 "foo.java",
1826 |metric| {
1827 assert_eq!(metric.abc.conditions_sum(), 4);
1828 assert_eq!(metric.abc.assignments_sum(), 0);
1829 assert_eq!(metric.abc.branches_sum(), 0);
1830 },
1831 );
1832 }
1833
1834 // Issue #1274: a generic *declaration* is type syntax, not a
1835 // decision. `java_count_token_condition` denied only
1836 // `type_arguments`, so `class Gen<T>` and `<U> U ident(U x)` — both
1837 // `type_parameters` — each scored two conditions. The fix counts
1838 // `<` / `>` only under `binary_expression`, the polarity C / C++ /
1839 // Rust / Go already use; a `grammar.json` sweep proves the token is
1840 // emitted from exactly three productions, so the two shapes agree on
1841 // every well-formed input.
1842 //
1843 // One fixture per production the arm must ignore: the class-level
1844 // `type_parameters` (`Gen<T …>`), its `extends` bound's nested
1845 // `type_arguments` (`Comparable<T>`), the method-level
1846 // `type_parameters` (`<U>`), and a nested generic
1847 // (`Map<String, List<T>>`) whose two closing `>` lex as separate
1848 // tokens under separate `type_arguments`. Pre-fix this file scored
1849 // 4 conditions — two per `type_parameters` bracket pair; expected 0,
1850 // the file contains no conditional construct at all.
1851 #[cfg(feature = "java")]
1852 #[test]
1853 fn java_generic_declarations_are_not_conditions() {
1854 check_metrics::<JavaParser>(
1855 "class Gen<T extends Comparable<T>> {
1856 <U> U ident(U x) { return x; }
1857 List<T> pick(Map<String, List<T>> m) { return m.get(\"k\"); }
1858 }",
1859 "foo.java",
1860 |metric| {
1861 assert_eq!(metric.abc.conditions_sum(), 0);
1862 // Non-vacuity guard: 0 is also what an unparsed file
1863 // scores, so pin a value only a walked body can produce
1864 // — the `m.get("k")` invocation.
1865 assert_eq!(metric.abc.branches_sum(), 1);
1866 },
1867 );
1868 }
1869
1870 // Second half of #1274, same premise and same match: a wildcard type
1871 // argument's `?` is type syntax, not a ternary. tree-sitter-java emits
1872 // a bare `?` from exactly two productions — `ternary_expression` and
1873 // `wildcard` — so the `QMARK` arm carries the same allowlist gate the
1874 // `<` / `>` arm does.
1875 //
1876 // The body carries a real ternary over a real comparison so the
1877 // expected total is 2, not 0. Asserting 0 on a wildcard-only fixture
1878 // would have been vacuous — an unparsable file scores 0 too, so the
1879 // test could not tell "the wildcard was ignored" from "the fixture
1880 // stopped being recognised". Each failure mode now lands on its own
1881 // number: 4 if the wildcard `?` counts again (the pre-fix value), 3
1882 // if the gate is aimed at `Wildcard` instead, 1 if it swallows the
1883 // genuine ternary, 0 if parsing breaks. Two wildcards against one
1884 // ternary is deliberate — with one of each, aiming the gate at the
1885 // wrong one of the two productions still totals 2 and the test
1886 // cannot see the difference.
1887 #[cfg(feature = "java")]
1888 #[test]
1889 fn java_generic_wildcard_is_not_a_condition() {
1890 check_metrics::<JavaParser>(
1891 "class A {
1892 int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
1893 int a, int b) { return a < b ? 1 : 2; }
1894 }",
1895 "foo.java",
1896 |metric| {
1897 assert_eq!(metric.abc.conditions_sum(), 2);
1898 },
1899 );
1900 }
1901
1902 // The other half of #1274: narrowing the `<` / `>` arm must not
1903 // swallow real comparisons. `List<String>` in the signature keeps a
1904 // generic in scope so the fixture proves both directions at once,
1905 // and the assertion is the grammar-dispatch §8 pin — on the
1906 // method's own space the ABC condition count must equal the
1907 // cyclomatic decision count (`cyclomatic()` minus the per-space
1908 // base of 1). Both are 2, one per `if`.
1909 #[cfg(feature = "java")]
1910 #[test]
1911 fn java_comparison_operators_still_count_alongside_generics() {
1912 check_func_space::<JavaParser, _>(
1913 "class A {
1914 int m(List<String> xs, int a, int b) {
1915 if (a < b) { return 1; }
1916 if (a > b) { return 2; }
1917 return 0;
1918 }
1919 }",
1920 "foo.java",
1921 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
1922 );
1923 }
1924
1925 #[cfg(feature = "java")]
1926 #[test]
1927 fn java_constructor_delegation_is_a_branch() {
1928 // Regression for #1279: `super(…)` / `this(…)` parse as
1929 // `explicit_constructor_invocation`, not `method_invocation`, so
1930 // each scored zero branches while Groovy scored one for identical
1931 // source. Both constructors delegate, and neither body contains any
1932 // other call.
1933 // expected: 2 branches — one delegation each.
1934 check_metrics::<JavaParser>(
1935 "class Sub extends Base {
1936 Sub() { super(1); }
1937 Sub(int a) { this(); }
1938 }",
1939 "foo.java",
1940 |metric| {
1941 assert_eq!(metric.abc.branches_sum(), 2);
1942 },
1943 );
1944 }
1945
1946 #[cfg(feature = "java")]
1947 #[test]
1948 fn java_constructor_delegation_does_not_double_count_arguments() {
1949 // The delegation node does not wrap a `method_invocation` for the
1950 // call itself, so `super(f())` is exactly two branches — the
1951 // delegation and the argument call — not three (#1279).
1952 // expected: 2 branches.
1953 check_metrics::<JavaParser>(
1954 "class Sub extends Base {
1955 Sub() { super(f()); }
1956 }",
1957 "foo.java",
1958 |metric| {
1959 assert_eq!(metric.abc.branches_sum(), 2);
1960 },
1961 );
1962 }
1963
1964 #[cfg(feature = "csharp")]
1965 #[test]
1966 fn csharp_constructor_initializer_is_a_branch() {
1967 // C# spells the same delegation as a `constructor_initializer`
1968 // (`: base(…)` / `: this(…)`), which likewise scored zero (#1279).
1969 // expected: 2 branches — one initializer each, no other calls.
1970 check_metrics::<CsharpParser>(
1971 "class Sub : Base {
1972 Sub() : base(1) { }
1973 Sub(int a) : this() { }
1974 }",
1975 "foo.cs",
1976 |metric| {
1977 assert_eq!(metric.abc.branches_sum(), 2);
1978 },
1979 );
1980 }
1981
1982 #[cfg(feature = "csharp")]
1983 #[test]
1984 fn csharp_primary_constructor_base_call_is_a_branch() {
1985 // The C# 12 primary-constructor superclass call invokes the base
1986 // constructor exactly as the `: base(…)` above it does, and scored
1987 // zero until #1406 — the C# sibling of Kotlin's #1384.
1988 //
1989 // tree-sitter-c-sharp 0.23.5 spells the `record` and `class`
1990 // families differently, so per `.claude/rules/grammar-dispatch.md`
1991 // §11 they are two independent paths and each needs its own
1992 // fixture: the record form nests the arguments under a
1993 // `primary_constructor_base_type`, the class form hangs them
1994 // straight off the `base_list`. A record-only fixture says nothing
1995 // about the class arm, and vice versa.
1996 //
1997 // `S` and `R3` are the negatives: a base type with no argument
1998 // list is not a call, and neither spelling emits an
1999 // `argument_list` for it.
2000 let src = "class Sub(int x) : Base(x) { }
2001 record R1(int x) : Base(x);
2002 record class R2(int x) : Base(x);
2003 class G<T>(int x) : GBase<T>(x) where T : class { }
2004 class Multi(int x) : Base(x), IBase { }
2005 struct S(int x) : IBase { }
2006 record R3(int x) : Base;";
2007 // Anchors every spelling against a fixture edit: with presence
2008 // checks only, deleting `(x)` from the class form leaves the
2009 // record form satisfying the arm and this test green. The last
2010 // two counts anchor the declarations that would otherwise decay
2011 // into duplicates of `Sub` — `Multi` owns the fixture's only
2012 // comma (`Base(x), IBase`) and `G` its only type argument list
2013 // (`GBase<T>`), so trimming either back fails here by name.
2014 assert_csharp_fixture_spells(
2015 src,
2016 &[
2017 (
2018 Csharp::PrimaryConstructorBaseType as u16,
2019 2,
2020 "`record` base calls",
2021 ),
2022 (Csharp::ArgumentList as u16, 5, "base-call argument lists"),
2023 (Csharp::BaseList2 as u16, 7, "base lists"),
2024 // The §2 drift marker for the arm's other half. Both ids
2025 // render to `"base_list"`; at this pin every declaration
2026 // family above emits 252 and none emits 246, so
2027 // `BaseList` is defensive and this zero says so. A
2028 // grammar bump that starts emitting it fails here rather
2029 // than silently changing which arm does the work.
2030 (Csharp::BaseList as u16, 0, "unaliased `base_list` nodes"),
2031 (Csharp::COMMA as u16, 1, "`Multi`'s trailing interface"),
2032 (Csharp::TypeArgumentList as u16, 1, "`G`'s generic base"),
2033 ],
2034 );
2035 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2036 for name in ["Sub", "R1", "R2", "G", "Multi"] {
2037 assert_eq!(
2038 child_space(&space, name).metrics.abc.branches(),
2039 1,
2040 "{name}: the primary-constructor base call is one branch"
2041 );
2042 }
2043 for name in ["S", "R3"] {
2044 assert_eq!(
2045 child_space(&space, name).metrics.abc.branches(),
2046 0,
2047 "{name}: a base type with no argument list is not a call"
2048 );
2049 }
2050 });
2051 }
2052
2053 #[cfg(feature = "csharp")]
2054 #[test]
2055 fn csharp_base_list_gate_excludes_other_argument_lists() {
2056 // The `base_list` parent gate on the `ArgumentList` arm is
2057 // load-bearing, not decoration: ungated it bills a branch for the
2058 // argument list of every call and allocation in the file, on top
2059 // of the `InvocationExpression` / `ObjectCreationExpression` the
2060 // arm already counts — doubling both (#1406).
2061 //
2062 // The fixture pairs one real base call with the four other
2063 // argument-carrying productions C# spells nearby: an attribute
2064 // (`attribute_argument_list`), an invocation, an object creation,
2065 // and an indexer (`bracketed_argument_list`) alongside a generic
2066 // (`type_argument_list`). Those last three are distinct kind ids
2067 // rather than `argument_list`, so they pin the §1 neighbour set
2068 // as much as the gate.
2069 let src = "[System.Obsolete(\"why\")]
2070 class A(int x) : Base(x) {
2071 void M(int y) { Helper(y); }
2072 object N() { return new Foo(1); }
2073 int P(System.Collections.Generic.List<int> l, int i) { return l[i]; }
2074 }";
2075 assert_csharp_fixture_spells(
2076 src,
2077 &[
2078 (
2079 Csharp::AttributeArgumentList as u16,
2080 1,
2081 "attribute argument lists",
2082 ),
2083 (
2084 Csharp::BracketedArgumentList as u16,
2085 1,
2086 "bracketed argument lists",
2087 ),
2088 (Csharp::TypeArgumentList as u16, 1, "type argument lists"),
2089 (Csharp::ArgumentList as u16, 3, "argument lists"),
2090 ],
2091 );
2092 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2093 let class = child_space(&space, "A");
2094 assert_eq!(
2095 class.metrics.abc.branches(),
2096 1,
2097 "`: Base(x)` alone — the attribute's arguments are not a call"
2098 );
2099 // The controls. 2 apiece would mean the gate let the
2100 // argument list through beside the call that owns it.
2101 assert_eq!(
2102 child_space(class, "M").metrics.abc.branches(),
2103 1,
2104 "`Helper(y)` is one branch, not one per argument list"
2105 );
2106 assert_eq!(
2107 child_space(class, "N").metrics.abc.branches(),
2108 1,
2109 "`new Foo(1)` is one branch, not one per argument list"
2110 );
2111 // `P` carries the indexer and the generic deliberately, but
2112 // gets no assertion: `bracketed_argument_list` and
2113 // `type_argument_list` are distinct kind ids, so `P` reads 0
2114 // with the gate, without it, and without this change
2115 // altogether. The census above is what actually pins them —
2116 // an assertion here would read as coverage it cannot provide.
2117 });
2118 }
2119
2120 #[cfg(feature = "csharp")]
2121 #[test]
2122 fn csharp_base_call_and_constructor_initializer_do_not_double_count() {
2123 // A class can spell *both* delegations at once: the primary
2124 // constructor's `: Base(x)` and a secondary constructor's
2125 // `: this(a)`. They are separate nodes in separate spaces, so the
2126 // file scores 2 — one each. 3 would mean the #1406 arm also fired
2127 // on something `ConstructorInitializer` already owned; 1 would
2128 // mean one of the two stopped counting.
2129 //
2130 // Java has the equivalent guard
2131 // (`java_constructor_delegation_does_not_double_count_arguments`);
2132 // C# now has two delegation spellings and this is the pairing
2133 // test for them.
2134 let src = "class Both(int x) : Base(x) {
2135 public Both(int a, int b) : this(a) { }
2136 }";
2137 assert_csharp_fixture_spells(
2138 src,
2139 &[
2140 (Csharp::ConstructorInitializer as u16, 1, "`: this(a)`"),
2141 (Csharp::BaseList2 as u16, 1, "the primary-constructor base"),
2142 ],
2143 );
2144 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2145 let class = child_space(&space, "Both");
2146 assert_eq!(
2147 class.metrics.abc.branches(),
2148 1,
2149 "the class space owns `: Base(x)` and nothing else"
2150 );
2151 assert_eq!(
2152 child_space(class, "Both").metrics.abc.branches(),
2153 1,
2154 "the secondary constructor owns `: this(a)` and nothing else"
2155 );
2156 });
2157 }
2158
2159 #[cfg(feature = "kotlin")]
2160 #[test]
2161 fn kotlin_constructor_delegation_is_a_branch() {
2162 // Kotlin's secondary-constructor delegation is a
2163 // `constructor_delegation_call`, a distinct production from
2164 // `CallExpression`, so it scored zero for the same reason (#1279).
2165 // expected: 1 branch — the `: super(x)` delegation.
2166 check_metrics::<KotlinParser>(
2167 "class Sub : Base {
2168 constructor(x: Int) : super(x) { }
2169 }",
2170 "foo.kt",
2171 |metric| {
2172 assert_eq!(metric.abc.branches_sum(), 1);
2173 },
2174 );
2175 }
2176
2177 #[cfg(feature = "kotlin")]
2178 #[test]
2179 fn kotlin_primary_constructor_superclass_call_is_a_branch() {
2180 // Kotlin's *primary*-constructor superclass call is a
2181 // `constructor_invocation` under a `delegation_specifier` — a third
2182 // production, distinct from both `CallExpression` and the
2183 // `ConstructorDelegationCall` #1279 added, so it scored zero while
2184 // the secondary form beside it scored one (#1384). An object
2185 // expression's superclass call uses the same production.
2186 //
2187 // `class Plain : Marker` is the negative case: a delegation
2188 // specifier with no argument list is a plain `user_type`, so it
2189 // must stay at zero. Broadening the arm to bare
2190 // `DelegationSpecifier` fails here at `branches_sum` (5 against
2191 // 3), though not on `Plain`'s account alone: `class Classic :
2192 // Base` below is argument-less too, so deleting `Plain` still
2193 // leaves the broadening failing, at 4. What `Plain` buys is the
2194 // case stated outright rather than riding on the contrast fixture,
2195 // and since the per-space assertions below name it, trimming it
2196 // from the fixture fails in `child_space` instead of passing
2197 // silently as it did while this test read only the sum.
2198 //
2199 // `class Classic` carries the secondary spelling of the same call
2200 // alongside, which is what makes the per-space assertions below a
2201 // contrast rather than a census: a class header's superclass call
2202 // sits in `delegation_specifiers`, outside every member, so no
2203 // function space encloses it and the branch is the **class**
2204 // space's, while `constructor(x: Int) : super(x)` opens a function
2205 // space and takes the branch with it. `branches_sum` cannot see the
2206 // difference — the file totals 3 either way — so a codebase moving
2207 // to primary constructors shifts per-function `branches_max` /
2208 // `branches_average` and per-space `abc` thresholds without any
2209 // behaviour change. That is deliberate (there is no function to
2210 // attribute the call to), and C# has the identical split, pinned by
2211 // `csharp_base_call_and_constructor_initializer_do_not_double_count`
2212 // (#1456).
2213 //
2214 // expected: 3 branches — `Base(1, 2)`, `Base(3)` and `super(x)`.
2215 // Nothing else in the fixture is a call, so deleting any one
2216 // construction from the fixture moves both the total and the space
2217 // it belongs to.
2218 check_func_space::<KotlinParser, _>(
2219 "class Sub : Base(1, 2) { }
2220 class Plain : Marker { }
2221 fun make(): Any = object : Base(3) { }
2222 class Classic : Base {
2223 constructor(x: Int) : super(x) { }
2224 }",
2225 "foo.kt",
2226 |space| {
2227 assert_eq!(space.metrics.abc.branches_sum(), 3);
2228 assert_eq!(
2229 child_space(&space, "Sub").metrics.abc.branches(),
2230 1,
2231 "the primary-constructor superclass call is the class space's"
2232 );
2233 assert_eq!(
2234 child_space(&space, "Plain").metrics.abc.branches(),
2235 0,
2236 "a delegation specifier with no argument list is not a call"
2237 );
2238 // The object expression is its own Class space (#463), so
2239 // `make` holds nothing of its own — the same attribution
2240 // one level down.
2241 let make = child_space(&space, "make");
2242 assert_eq!(
2243 make.metrics.abc.branches(),
2244 0,
2245 "`make` owns no branch; `object : Base(3)` is its own space"
2246 );
2247 assert_eq!(
2248 child_space(make, "<anonymous>").metrics.abc.branches(),
2249 1,
2250 "the object expression's superclass call is that space's"
2251 );
2252 // The contrast. `Classic` spells the same call through a
2253 // secondary constructor, which *does* open a function
2254 // space, so the class owns nothing.
2255 let classic = child_space(&space, "Classic");
2256 assert_eq!(
2257 classic.metrics.abc.branches(),
2258 0,
2259 "a secondary constructor's `: super(x)` is not the class's"
2260 );
2261 assert_eq!(
2262 child_space(classic, "<anonymous>").metrics.abc.branches(),
2263 1,
2264 "it belongs to the constructor's own function space"
2265 );
2266 },
2267 );
2268 }
2269
2270 #[cfg(feature = "kotlin")]
2271 #[test]
2272 fn kotlin_annotation_arguments_are_not_a_branch() {
2273 // The parent gate on that arm is load-bearing, not decoration:
2274 // tree-sitter-kotlin-ng spells an annotation's argument list with
2275 // the *same* `constructor_invocation` production — `@Mark("a")`
2276 // parses as `annotation > constructor_invocation` and
2277 // `@file:Suppress(…)` as `file_annotation > constructor_invocation`.
2278 // Ungated, every argument-carrying annotation in a Kotlin file
2279 // would bill a branch, and annotations are everywhere (#1384).
2280 //
2281 // The fixture pairs three annotation spellings — file-level,
2282 // class-level and use-site-targeted — with one real superclass
2283 // call, so the total discriminates in both directions: 1 with the
2284 // gate, 4 without it, 0 without the arm at all.
2285 //
2286 // A correctly-excluded annotation contributes to no ABC axis, so
2287 // `branches_sum()` alone cannot notice the annotations being
2288 // trimmed out of the fixture — measured: dropping any of the three
2289 // leaves this test green, and with all three gone, removing the
2290 // production gate fails nothing. The node census below is the
2291 // anchor: it pins that the fixture really does hand the arm four
2292 // `constructor_invocation` nodes for it to score 1 out of, and
2293 // that the `file_annotation` parent — the spelling a denylist of
2294 // `Annotation` alone would have missed — is among them.
2295 //
2296 // expected: 1 branch — `Base(1)` only.
2297 let src = r#"@file:Suppress("unused")
2298 @Mark("a")
2299 class Ann : Base(1) {
2300 @get:Mark("b")
2301 val v: Int = 0
2302 }"#;
2303 let parser = KotlinParser::new(
2304 src.as_bytes().to_vec(),
2305 &std::path::PathBuf::from("foo.kt"),
2306 None,
2307 );
2308 assert_eq!(
2309 parser
2310 .root()
2311 .preorder()
2312 .filter(|n| n.kind_id() == Kotlin::ConstructorInvocation as u16)
2313 .count(),
2314 4,
2315 "fixture must keep three annotations plus the superclass call"
2316 );
2317 assert!(ast_has_kind_id(&parser, Kotlin::FileAnnotation as u16));
2318
2319 check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
2320 assert_eq!(metric.abc.branches_sum(), 1);
2321 });
2322 }
2323
2324 #[cfg(feature = "groovy")]
2325 #[test]
2326 fn groovy_constructor_delegation_is_a_branch() {
2327 // Groovy already counted this shape before #1279; the assertion
2328 // pins the JVM-family parity the Java and Kotlin fixes restore.
2329 // expected: 1 branch — the `super(1)` delegation.
2330 check_metrics::<GroovyParser>(
2331 "class Sub extends Base {
2332 Sub() { super(1) }
2333 }",
2334 "foo.groovy",
2335 |metric| {
2336 assert_eq!(metric.abc.branches_sum(), 1);
2337 },
2338 );
2339 }
2340
2341 // The three tests below are one fix seen from three grammars (#1407):
2342 // an enum constant carrying constructor arguments is an object
2343 // construction and scored zero in every JVM-family language, which
2344 // #1279 and #1384 left behind because both looked at superclass
2345 // delegation only.
2346 //
2347 // Each fixture pairs an argument-carrying constant with a bare one in
2348 // the *same* enum, because that is the only shape that shows the gate
2349 // working: a fixture of only `A(1)` scores the same whether the arm is
2350 // gated on the argument list or matches every constant
2351 // (`.claude/rules/grammar-dispatch.md` §6). Each also carries a
2352 // constant whose argument is itself a call, which must score 2 rather
2353 // than 1 (§5).
2354 //
2355 // `branches_sum()` alone cannot notice the bare constant being trimmed
2356 // out — it contributes to no axis — so every fixture is anchored on a
2357 // node census that fails by name when a spelling is edited away
2358 // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
2359 // production line"). The two are coupled in one helper for the same
2360 // reason `assert_kotlin_class_members` couples its pair: a branch
2361 // total added without the census reads as coverage and is not.
2362
2363 #[cfg(any(feature = "groovy", feature = "java", feature = "kotlin"))]
2364 fn assert_enum_branches<P: MetricSuite>(
2365 src: &str,
2366 path: &str,
2367 kinds: &[(u16, usize, &str)],
2368 branches: u64,
2369 ) {
2370 assert_fixture_spells::<P>(src, path, kinds);
2371 check_func_space::<P, _>(src, path, |space| {
2372 assert_eq!(space.metrics.abc.branches_sum(), branches);
2373 });
2374 }
2375
2376 #[cfg(feature = "java")]
2377 #[test]
2378 fn java_enum_constant_with_arguments_is_a_branch() {
2379 // `@SuppressWarnings("x") B` is the discriminating case for the
2380 // gate's *kind*, not just its presence: the annotation carries an
2381 // argument list of its own, so an arm gated on "has some argument
2382 // list anywhere below" would score it. It cannot satisfy this one
2383 // twice over — `annotation_argument_list` is a distinct kind id
2384 // from `argument_list`, and it hangs off the constant's `modifiers`
2385 // child rather than the constant itself. Both verified with `bca
2386 // dump`.
2387 //
2388 // expected: 4 branches — `A(1)`, `K(2)`, `D(…)`, and the `f()`
2389 // inside `D`'s argument list. `B` scores 0.
2390 let src = "class Outer {
2391 static int f() { return 1; }
2392 enum E {
2393 A(1),
2394 @SuppressWarnings(\"x\") B,
2395 K(2),
2396 D(f());
2397 E(int v) {}
2398 }
2399 }";
2400 assert_enum_branches::<JavaParser>(
2401 src,
2402 "foo.java",
2403 &[
2404 (Java::EnumConstant as u16, 4, "enum constants"),
2405 (Java::ArgumentList as u16, 4, "argument lists"),
2406 (
2407 Java::AnnotationArgumentList as u16,
2408 1,
2409 "`@SuppressWarnings`'s argument list",
2410 ),
2411 (Java::MethodInvocation as u16, 1, "`D`'s nested `f()` call"),
2412 ],
2413 4,
2414 );
2415 }
2416
2417 #[cfg(feature = "kotlin")]
2418 #[test]
2419 fn kotlin_enum_entry_with_arguments_is_a_branch() {
2420 // Kotlin needs a defaulted primary-constructor parameter to spell
2421 // both cases in one enum: `enum class E(val v: Int)` obliges every
2422 // entry to pass an argument, so `= 0` is what lets the bare `B`
2423 // sit beside `A(1)`. `enum class P` covers the other shape the gate
2424 // has to leave alone — an enum with no constructor at all, whose
2425 // entries can never carry an argument list.
2426 //
2427 // expected: 3 branches — `A(1)`, `C(…)`, and the `g()` inside `C`'s
2428 // argument list. `B`, `X` and `Y` score 0.
2429 let src = "fun g(): Int = 1
2430 enum class E(val v: Int = 0) { A(1), B, C(g()) }
2431 enum class P { X, Y }";
2432 assert_enum_branches::<KotlinParser>(
2433 src,
2434 "foo.kt",
2435 &[
2436 (Kotlin::EnumEntry as u16, 5, "enum entries"),
2437 (Kotlin::ValueArguments as u16, 3, "argument lists"),
2438 (Kotlin::CallExpression as u16, 1, "`C`'s nested `g()` call"),
2439 ],
2440 3,
2441 );
2442 }
2443
2444 #[cfg(feature = "groovy")]
2445 #[test]
2446 fn groovy_enum_constant_with_arguments_is_a_branch() {
2447 // Groovy's enum has no annotated-constant case to cover: the
2448 // dekobon grammar cannot parse `@Deprecated A(1)` inside an enum
2449 // body and recovers into `ERROR` nodes, emitting no `enum_constant`
2450 // at all. The remaining two cases are the ones that matter.
2451 //
2452 // expected: 3 branches — `A(1)`, `D(…)`, and the `Outer.f()` inside
2453 // `D`'s argument list. `B` scores 0.
2454 let src = "class Outer { static int f() { 1 } }
2455 enum E { A(1), B, D(Outer.f()) }";
2456 assert_enum_branches::<GroovyParser>(
2457 src,
2458 "foo.groovy",
2459 &[
2460 (Groovy::EnumConstant as u16, 3, "enum constants"),
2461 (Groovy::ArgumentList as u16, 3, "argument lists"),
2462 (
2463 Groovy::MethodInvocation as u16,
2464 1,
2465 "`D`'s nested `Outer.f()` call",
2466 ),
2467 ],
2468 3,
2469 );
2470 }
2471
2472 #[cfg(feature = "groovy")]
2473 #[test]
2474 fn groovy_no_abc() {
2475 // Comment-only file has no executable code → all-zero ABC.
2476 check_metrics::<GroovyParser>(
2477 "// just a comment, no executable code",
2478 "foo.groovy",
2479 |metric| {
2480 assert_eq!(metric.abc.assignments_sum(), 0);
2481 assert_eq!(metric.abc.branches_sum(), 0);
2482 assert_eq!(metric.abc.conditions_sum(), 0);
2483 },
2484 );
2485 }
2486
2487 #[cfg(feature = "groovy")]
2488 #[test]
2489 fn groovy_single_assignment() {
2490 // `int x = 1` is a local-variable declaration whose `=` counts
2491 // as one assignment (matches Java's semantics).
2492 check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
2493 assert_eq!(metric.abc.assignments_sum(), 1);
2494 assert_eq!(metric.abc.branches_sum(), 0);
2495 assert_eq!(metric.abc.conditions_sum(), 0);
2496 });
2497 }
2498
2499 #[cfg(feature = "groovy")]
2500 #[test]
2501 fn groovy_assignments() {
2502 check_metrics::<GroovyParser>(
2503 "void f() {
2504 int a = 1
2505 int b = 2
2506 a = 3
2507 b = 4
2508 a += 1
2509 b -= 1
2510 }",
2511 "foo.groovy",
2512 |metric| {
2513 // Six `=` tokens total. The two `Final`-less local
2514 // var-decls (`int a = 1`, `int b = 2`) and the two
2515 // bare assignments (`a = 3`, `b = 4`) each contribute
2516 // one assignment via the `EQ` arm; the `+=` / `-=`
2517 // each contribute one via the compound-assign arm.
2518 assert_eq!(metric.abc.assignments_sum(), 6);
2519 },
2520 );
2521 }
2522
2523 #[cfg(feature = "groovy")]
2524 #[test]
2525 fn groovy_branches() {
2526 check_metrics::<GroovyParser>(
2527 "void f() {
2528 doStuff()
2529 helper.invoke()
2530 new Worker()
2531 }",
2532 "foo.groovy",
2533 |metric| {
2534 // 2 method invocations + 1 object creation = 3 branches
2535 assert_eq!(metric.abc.branches_sum(), 3);
2536 },
2537 );
2538 }
2539
2540 #[cfg(feature = "groovy")]
2541 #[test]
2542 fn groovy_conditions_in_if() {
2543 check_metrics::<GroovyParser>(
2544 "void f(int a) {
2545 if (a == 0) { println(a) }
2546 if (a >= 1) { println(a) }
2547 if (a != 2) { println(a) }
2548 }",
2549 "foo.groovy",
2550 |metric| {
2551 // Three relational ops = 3 conditions
2552 assert_eq!(metric.abc.conditions_sum(), 3);
2553 },
2554 );
2555 }
2556
2557 #[cfg(feature = "groovy")]
2558 #[test]
2559 fn groovy_branches_with_juxt_call() {
2560 // Groovy's parens-less call form `println foo` must be counted
2561 // as a branch (`JuxtFunctionCall`).
2562 check_metrics::<GroovyParser>(
2563 "void f() {
2564 println 'hi'
2565 println 'bye'
2566 }",
2567 "foo.groovy",
2568 |metric| {
2569 // 2 juxt calls = 2 branches.
2570 assert_eq!(metric.abc.branches_sum(), 2);
2571 },
2572 );
2573 }
2574
2575 #[cfg(feature = "groovy")]
2576 #[test]
2577 fn groovy_try_catch_conditions() {
2578 // Each `try` and `catch` keyword token contributes +1 to
2579 // conditions (mirrors Java).
2580 check_metrics::<GroovyParser>(
2581 "void f() {
2582 try {
2583 risky()
2584 } catch (Exception e) {
2585 handle(e)
2586 }
2587 }",
2588 "foo.groovy",
2589 |metric| {
2590 // try + catch = 2 conditions
2591 assert_eq!(metric.abc.conditions_sum(), 2);
2592 },
2593 );
2594 }
2595
2596 #[cfg(feature = "groovy")]
2597 #[test]
2598 fn groovy_ternary_conditions() {
2599 check_metrics::<GroovyParser>(
2600 "void f(int x) {
2601 def y = x > 0 ? 1 : 2
2602 }",
2603 "foo.groovy",
2604 |metric| {
2605 // QMARK alone is +1 condition, plus the `>` condition = 2.
2606 assert_eq!(metric.abc.conditions_sum(), 2);
2607 },
2608 );
2609 }
2610
2611 #[cfg(feature = "groovy")]
2612 #[test]
2613 fn groovy_constant_excluded_from_assignments() {
2614 // `final` declarations are not counted as assignments
2615 // (mirrors Java's `Final` handling).
2616 check_metrics::<GroovyParser>(
2617 "class A {
2618 final int CONST = 42
2619 int field = 0
2620 }",
2621 "foo.groovy",
2622 |metric| {
2623 // The `=` on `final int CONST = 42` is a constant
2624 // initialiser (skipped). Only `field = 0` counts.
2625 assert_eq!(metric.abc.assignments_sum(), 1);
2626 },
2627 );
2628 }
2629
2630 #[cfg(feature = "groovy")]
2631 #[test]
2632 fn groovy_malformed_parenthesized_no_panic() {
2633 // Regression: malformed Groovy input must not panic the ABC
2634 // walker; the `spaces.rs` Unit fallback (lesson 9) covers
2635 // structural recovery. amaanq's grammar treats `def x = (((`
2636 // as a `local_variable_declaration` whose initialiser is the
2637 // first opening paren — the `=` still fires the assignment
2638 // arm.
2639 check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
2640 assert_eq!(metric.abc.assignments_sum(), 1);
2641 });
2642 }
2643
2644 #[cfg(feature = "groovy")]
2645 #[test]
2646 fn groovy_bool_returning_terminal_kinds_count() {
2647 // Companion to `csharp_bool_returning_terminal_kinds_count`
2648 // (issue #372 / lesson #19). The dekobon Groovy grammar
2649 // shares Java's wrapping conventions for `FieldAccess` and
2650 // `InstanceofExpression`, but it splits casts into two
2651 // distinct kinds — `cast_expression` for the Groovy-idiomatic
2652 // `v as Boolean` and `parenthesized_type_cast` for the
2653 // Java-style `(boolean) v`. The grammar has no `await` or
2654 // `array_access` analogues, so the C# fix's five-kind set
2655 // collapses to four here (with the cast slot doubled).
2656 //
2657 // `instanceof` has since moved out of the terminal set to an
2658 // unconditional arm (#1461), so it no longer depends on the
2659 // slot to be counted. The total is unchanged, which is the
2660 // point of that change rather than an accident of it.
2661 //
2662 // expected: 4 conditions (one per `if`), 0 assignments,
2663 // 0 branches (no invocations).
2664 check_metrics::<GroovyParser>(
2665 "class Cfg { boolean flag }
2666 class A {
2667 void m(Object v, Cfg cfg) {
2668 if (cfg.flag) { }
2669 if ((boolean) v) { }
2670 if (v as Boolean) { }
2671 if (v instanceof Cfg) { }
2672 }
2673 }",
2674 "foo.groovy",
2675 |metric| {
2676 assert_eq!(metric.abc.conditions_sum(), 4);
2677 assert_eq!(metric.abc.assignments_sum(), 0);
2678 assert_eq!(metric.abc.branches_sum(), 0);
2679 },
2680 );
2681 }
2682
2683 // Groovy half of #1274 — see `java_generic_declarations_are_not_conditions`.
2684 // The class-level `type_parameters` and its bound's nested
2685 // `type_arguments` mirror the Java fixture; pre-fix this file scored
2686 // 2 conditions, expected 0.
2687 #[cfg(feature = "groovy")]
2688 #[test]
2689 fn groovy_generic_declarations_are_not_conditions() {
2690 check_metrics::<GroovyParser>(
2691 "class Gen<T extends Comparable<T>> {
2692 List<T> pick(Map<String, List<T>> m) { m.get(\"k\") }
2693 }",
2694 "foo.groovy",
2695 |metric| {
2696 assert_eq!(metric.abc.conditions_sum(), 0);
2697 // Non-vacuity guard — see the Java counterpart.
2698 assert_eq!(metric.abc.branches_sum(), 1);
2699 },
2700 );
2701 }
2702
2703 // Groovy counterpart of `java_generic_wildcard_is_not_a_condition`,
2704 // including its choice of a non-zero expected total and its
2705 // two-wildcards-one-ternary shape; the dekobon grammar emits the
2706 // same `wildcard` node. Pre-fix: 4.
2707 #[cfg(feature = "groovy")]
2708 #[test]
2709 fn groovy_generic_wildcard_is_not_a_condition() {
2710 check_metrics::<GroovyParser>(
2711 "class A {
2712 int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
2713 int a, int b) { a < b ? 1 : 2 }
2714 }",
2715 "foo.groovy",
2716 |metric| {
2717 assert_eq!(metric.abc.conditions_sum(), 2);
2718 },
2719 );
2720 }
2721
2722 // A generic *method* gets its own fixture because the dekobon
2723 // grammar gives it its own production: `def <U> U ident(U x)` emits
2724 // `method_type_parameters`, not the `type_parameters` the class form
2725 // above uses. It is the fourth and last production from which that
2726 // grammar emits a bare `<` / `>` (the others being
2727 // `binary_expression`, `type_arguments` and `type_parameters`), and
2728 // the one a denylist extended only with `TypeParameters` — the fix
2729 // this issue's own plan proposed — would still miss. Revert-verified
2730 // in both directions: under that denylist this test still fails
2731 // while `groovy_generic_declarations_are_not_conditions` passes.
2732 //
2733 // The body carries a ternary over a comparison for the same
2734 // non-vacuity reason as the wildcard tests: expected 2, pre-fix 4
2735 // (the `<U>` bracket pair), 0 if the fixture stops parsing.
2736 #[cfg(feature = "groovy")]
2737 #[test]
2738 fn groovy_method_type_parameters_are_not_conditions() {
2739 check_metrics::<GroovyParser>(
2740 "class A {
2741 def <U> U ident(U x, int a, int b) { a < b ? x : null }
2742 }",
2743 "foo.groovy",
2744 |metric| {
2745 assert_eq!(metric.abc.conditions_sum(), 2);
2746 },
2747 );
2748 }
2749
2750 // Groovy counterpart of
2751 // `java_comparison_operators_still_count_alongside_generics`: the
2752 // narrowed arm keeps counting real comparisons, pinned against the
2753 // cyclomatic decision count on the method's own space (§8).
2754 #[cfg(feature = "groovy")]
2755 #[test]
2756 fn groovy_elvis_counts_one_condition_per_token() {
2757 // `a ?: c` is a short-circuit decision Groovy cyclomatic already
2758 // counts and the Kotlin ABC arm counts for its identical token;
2759 // Groovy's arm listed neither `?:` nor its `elvis_expression`, so
2760 // a method whose only branching is elvis chains reported
2761 // cyclomatic > 1 with zero conditions. One per token, so the
2762 // grammar-dispatch §8 invariant holds on the chain:
2763 // `conditions() == cyclomatic() - 1`.
2764 check_func_space::<GroovyParser, _>(
2765 "class K { def f(a, c) { return a ?: c } }",
2766 "foo.groovy",
2767 |space| assert_deepest_conditions_match_cyclomatic(&space, 1),
2768 );
2769 check_func_space::<GroovyParser, _>(
2770 "class K { def g(a, b, c) { return a ?: b ?: c } }",
2771 "foo.groovy",
2772 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2773 );
2774 }
2775
2776 #[cfg(feature = "groovy")]
2777 #[test]
2778 fn groovy_comparison_operators_still_count_alongside_generics() {
2779 check_func_space::<GroovyParser, _>(
2780 "class A {
2781 int m(List<String> xs, int a, int b) {
2782 if (a < b) { return 1 }
2783 if (a > b) { return 2 }
2784 return 0
2785 }
2786 }",
2787 "foo.groovy",
2788 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2789 );
2790 }
2791
2792 #[cfg(feature = "groovy")]
2793 #[test]
2794 fn groovy_if_multiple_conditions() {
2795 // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
2796 // and parenthesised unary forms each contribute one
2797 // condition per primitive comparison; the inspect-container
2798 // pass picks up the unary `!a` / `!b` arguments inside the
2799 // `BinaryExpression` and counts them too.
2800 check_metrics::<GroovyParser>(
2801 "void f(boolean a, boolean b, boolean c) {
2802 if (a || b || c) { println(a) }
2803 if (a && b && c) { println(a) }
2804 if (!a && !b) { println(a) }
2805 }",
2806 "foo.groovy",
2807 |metric| {
2808 // Conditions counted via the AMPAMP/PIPEPIPE arms
2809 // (one count per identifier in the chain — three
2810 // for `||`, three for `&&`, two for the unary chain)
2811 // = 8.
2812 assert_eq!(metric.abc.conditions_sum(), 8);
2813 // Three `println a` juxt calls — each is a branch.
2814 assert_eq!(metric.abc.branches_sum(), 3);
2815 },
2816 );
2817 }
2818
2819 #[cfg(feature = "groovy")]
2820 #[test]
2821 fn groovy_while_and_do_while_conditions() {
2822 // Covers the WhileStatement and DoStatement arms in
2823 // `impl Abc for GroovyCode`. Each `while` / `do-while` has
2824 // its condition scored through `groovy_count_condition`.
2825 check_metrics::<GroovyParser>(
2826 "void f(boolean a, boolean b) {
2827 while (a) {
2828 a = false
2829 }
2830 do {
2831 b = !b
2832 } while (b)
2833 }",
2834 "foo.groovy",
2835 |metric| {
2836 // `while(a)` + `while(b)` each contribute one condition;
2837 // the unary `!b` on the do body's right-hand side adds
2838 // one more via the assignment-arm inspection = 3.
2839 assert_eq!(metric.abc.conditions_sum(), 3);
2840 // Two assignments to existing variables (`a = false`,
2841 // `b = !b`).
2842 assert_eq!(metric.abc.assignments_sum(), 2);
2843 },
2844 );
2845 }
2846
2847 #[cfg(feature = "groovy")]
2848 #[test]
2849 fn groovy_if_while_boolean_literal_condition() {
2850 // Regression for the Groovy half of #371-class bugs: the
2851 // dekobon tree-sitter-groovy grammar wraps a bare
2852 // `true` / `false` literal used as the condition of
2853 // `if` / `while` / `do` / `?:` in a `boolean_literal` node
2854 // (`Groovy::BooleanLiteral`, kind_id 270), not the leaf
2855 // `True` / `False` keyword tokens. `groovy_count_condition`
2856 // must therefore match `BooleanLiteral` (the wrapper).
2857 // Without that, every literal-condition statement silently
2858 // scored 0 conditions. Mirror of
2859 // `csharp_if_while_boolean_literal_condition`.
2860 check_metrics::<GroovyParser>(
2861 "void m() {
2862 if (true) { println 'a' }
2863 if (false) { println 'b' }
2864 while (true) { break }
2865 int t = true ? 1 : 0
2866 }",
2867 "foo.groovy",
2868 |metric| {
2869 // Four literal-condition statements contribute 4
2870 // `BooleanLiteral` conditions (if / if / while /
2871 // ternary), plus the ternary's `?` token adds one
2872 // more via `groovy_count_token_condition` → 5
2873 // total. The `println` calls contribute 2 branches
2874 // (the `while` body's `break` is not a branch).
2875 // The `int t = …` initializer contributes 1
2876 // assignment.
2877 assert_eq!(metric.abc.conditions_sum(), 5);
2878 assert_eq!(metric.abc.branches_sum(), 2);
2879 assert_eq!(metric.abc.assignments_sum(), 1);
2880 },
2881 );
2882 }
2883
2884 #[cfg(feature = "groovy")]
2885 #[test]
2886 fn groovy_return_unary_boolean_literal() {
2887 // Companion to `groovy_if_while_boolean_literal_condition`:
2888 // a `!true` / `!false` operand inside a `return` statement
2889 // routes through `groovy_count_negated`. The `!` proves
2890 // boolean context, and a `BooleanLiteral` does not score
2891 // itself (`groovy_condition_scores_itself`), so the
2892 // negated operand pays one condition.
2893 check_metrics::<GroovyParser>(
2894 "boolean f() {
2895 return !true
2896 }
2897 boolean g() {
2898 return !false
2899 }",
2900 "foo.groovy",
2901 |metric| {
2902 // Each `return !X` walks into `groovy_count_negated`
2903 // with a UnaryExpression wrapping a `BANG` +
2904 // BooleanLiteral. The peel through `!` proves boolean
2905 // context (a `return` is not a slot), then the
2906 // BooleanLiteral operand contributes one condition.
2907 // Two `return !X` → 2 conditions, no branches, no
2908 // assignments.
2909 assert_eq!(metric.abc.conditions_sum(), 2);
2910 assert_eq!(metric.abc.branches_sum(), 0);
2911 assert_eq!(metric.abc.assignments_sum(), 0);
2912 },
2913 );
2914 }
2915
2916 #[cfg(feature = "groovy")]
2917 #[test]
2918 fn groovy_short_circuit_with_boolean_literal_operand() {
2919 // Companion to `groovy_if_while_boolean_literal_condition`:
2920 // a bare `true` / `false` operand of `&&` / `||` is a
2921 // chain-operand slot scored through `groovy_count_condition`.
2922 // A `BooleanLiteral` does not score itself, so the slot pays
2923 // for it; otherwise the operand would silently score zero.
2924 check_metrics::<GroovyParser>(
2925 "void m(boolean x) {
2926 if (x && true) { println 'a' }
2927 if (false || x) { println 'b' }
2928 }",
2929 "foo.groovy",
2930 |metric| {
2931 // `&&` and `||` themselves are NOT in
2932 // `groovy_count_token_condition`'s match list —
2933 // they route through
2934 // `groovy_walk_for_conditions::AMPAMP|PIPEPIPE`,
2935 // which scores each operand of the parent
2936 // BinaryExpression through `groovy_count_condition`.
2937 // An operand pays one condition unless it scores
2938 // itself. For `x && true`: Identifier x
2939 // (+1) + BooleanLiteral true (+1) = 2. For
2940 // `false || x`: BooleanLiteral false (+1) +
2941 // Identifier x (+1) = 2. Total 4.
2942 assert_eq!(metric.abc.conditions_sum(), 4);
2943 assert_eq!(metric.abc.branches_sum(), 2);
2944 assert_eq!(metric.abc.assignments_sum(), 0);
2945 },
2946 );
2947 }
2948
2949 #[cfg(feature = "groovy")]
2950 #[test]
2951 fn groovy_methods_arguments_with_conditions() {
2952 // Mirror of `java_methods_arguments_with_conditions`: a
2953 // unary `!x` inside an argument list must count both the
2954 // method invocation as a branch AND the unary as a
2955 // condition. The `ArgumentList | ArgumentList2` arm in
2956 // `impl Abc for GroovyCode` is what exercises this.
2957 check_metrics::<GroovyParser>(
2958 "void f(boolean a, boolean b, boolean c) {
2959 m1(a)
2960 m1(!a)
2961 m2(!a, !b)
2962 }",
2963 "foo.groovy",
2964 |metric| {
2965 // 3 method invocations (m1, m1, m2) — each fires the
2966 // branches arm.
2967 assert_eq!(metric.abc.branches_sum(), 3);
2968 // Three `!` unaries — `m1(!a)` and the two args of
2969 // `m2(!a, !b)` — each contribute one condition via
2970 // the ArgumentList inspection.
2971 assert_eq!(metric.abc.conditions_sum(), 3);
2972 },
2973 );
2974 }
2975
2976 #[cfg(feature = "groovy")]
2977 #[test]
2978 fn groovy_return_with_conditions() {
2979 // Mirror of `java_return_with_conditions`: a parenthesised
2980 // or unary expression inside `return` flows through the
2981 // `ReturnStatement` arm to `groovy_count_negated`.
2982 check_metrics::<GroovyParser>(
2983 "boolean f(boolean a) {
2984 return (a)
2985 }
2986 boolean g(boolean a) {
2987 return !a
2988 }",
2989 "foo.groovy",
2990 |metric| {
2991 // Only one of the two return forms surfaces a
2992 // condition: `return !a` hits the UnaryExpression
2993 // path and adds one; `return (a)` reaches
2994 // `groovy_count_negated` but no `!` proves the inner
2995 // identifier `a` boolean, so no condition is added.
2996 assert_eq!(metric.abc.conditions_sum(), 1);
2997 },
2998 );
2999 }
3000
3001 #[cfg(feature = "groovy")]
3002 #[test]
3003 fn groovy_for_with_variable_declaration() {
3004 // Classical `for (int i = 0; cond; i++)` form. The init
3005 // slot's `int i = 0` is suppressed from assignments by the
3006 // `LocalVariableDeclaration` push/pop dance; the `i++` in
3007 // the update slot contributes one assignment via the
3008 // `PLUSPLUS` arm. The condition `i < 10` flows through the
3009 // `ForStatement` arm.
3010 check_metrics::<GroovyParser>(
3011 "void f() {
3012 for (int i = 0; i < 10; i++) {
3013 println(i)
3014 }
3015 }",
3016 "foo.groovy",
3017 |metric| {
3018 // `int i = 0` fires the EQ arm + `i++` fires the
3019 // PLUSPLUS arm = 2 assignments.
3020 assert_eq!(metric.abc.assignments_sum(), 2);
3021 // `i < 10` is one condition (the LT arm).
3022 assert_eq!(metric.abc.conditions_sum(), 1);
3023 },
3024 );
3025 }
3026
3027 /// The existing `for` test uses `i < 10`, which the `LT` token arm
3028 /// counts on its own — `groovy_walk_for_statement` never
3029 /// contributes there, so it stayed uncovered. A bare-identifier
3030 /// condition has no comparison token, so the count can only come
3031 /// from the walker.
3032 #[cfg(feature = "groovy")]
3033 #[test]
3034 fn groovy_for_with_bare_identifier_condition() {
3035 check_metrics::<GroovyParser>(
3036 "void f(boolean go) {
3037 for (int i = 0; go; i++) {
3038 println(i)
3039 }
3040 }",
3041 "foo.groovy",
3042 |metric| {
3043 // `go` is the whole condition and counts once.
3044 assert_eq!(metric.abc.conditions_sum(), 1);
3045 // `int i = 0` (EQ) + `i++` (PLUSPLUS) = 2, as in
3046 // `groovy_for_with_variable_declaration`.
3047 assert_eq!(metric.abc.assignments_sum(), 2);
3048 },
3049 );
3050 }
3051
3052 /// The same slot with the initialiser hoisted out of the header.
3053 /// Under the pre-#1276 positional cascade this was a distinct code
3054 /// path — the condition moved from child(4) to child(3) — and the
3055 /// pair is kept as a shape guard now that the walker reads the
3056 /// `condition` field and cannot see the difference.
3057 #[cfg(feature = "groovy")]
3058 #[test]
3059 fn groovy_for_with_empty_initializer_counts_the_condition() {
3060 check_metrics::<GroovyParser>(
3061 "void f(boolean go) {
3062 int i = 0
3063 for (; go; i++) {
3064 println(i)
3065 }
3066 }",
3067 "foo.groovy",
3068 |metric| {
3069 assert_eq!(metric.abc.conditions_sum(), 1);
3070 // `int i = 0` (EQ) + `i++` (PLUSPLUS), as above — the
3071 // initialiser just moved out of the loop header.
3072 assert_eq!(metric.abc.assignments_sum(), 2);
3073 },
3074 );
3075 }
3076
3077 /// Issue #1276 changed Groovy's answer here, exactly as it changed
3078 /// Java's: the positional cascade counted a `;` / `)` landing at
3079 /// child(4) as a vacuously-true condition, so `for (;;)` scored
3080 /// one. An omitted test is not a decision, and every other impl
3081 /// scores it zero. See `java_empty_for_condition_counts_nothing`.
3082 #[cfg(feature = "groovy")]
3083 #[test]
3084 fn groovy_empty_for_condition_counts_nothing() {
3085 check_metrics::<GroovyParser>("void f() { for (;;) { break } }", "foo.groovy", |metric| {
3086 assert_eq!(metric.abc.conditions_sum(), 0);
3087 });
3088 // The second spelling that moved. Unlike Java's, Groovy's
3089 // `local_variable_declaration` does not swallow its `;`, so the
3090 // old cascade found `;` at child(3) and `;` at child(4) here
3091 // too, and counted one.
3092 check_metrics::<GroovyParser>(
3093 "void f() { for (int i = 0; ; i++) { break } }",
3094 "foo.groovy",
3095 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
3096 );
3097 }
3098
3099 /// The cascade's other defect, shared with Java: a comment in the
3100 /// header shifted every child index, so the condition went unread.
3101 /// Reading the `condition` field cannot shift.
3102 #[cfg(feature = "groovy")]
3103 #[test]
3104 fn groovy_for_condition_survives_a_header_comment() {
3105 check_metrics::<GroovyParser>(
3106 "void f(boolean go) { for (; /* n */ go; ) { break } }",
3107 "foo.groovy",
3108 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
3109 );
3110 // Unchanged control: the same loop without the comment.
3111 check_metrics::<GroovyParser>(
3112 "void f(boolean go) { for (; go; ) { break } }",
3113 "foo.groovy",
3114 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
3115 );
3116 }
3117
3118 /// C#'s `csharp_walk_for_statement` reads the loop condition off
3119 /// the named `condition` field and scores a parenthesised or
3120 /// `!`-prefixed one through `csharp_count_condition`. Every other
3121 /// C# `for` test uses a comparison (`i < n`), which the `LT` token
3122 /// arm counts without entering the walker.
3123 #[cfg(feature = "csharp")]
3124 #[test]
3125 fn csharp_for_with_negated_condition() {
3126 check_metrics::<CsharpParser>(
3127 "class A {
3128 void M(bool done) {
3129 for (int i = 0; !done; i++) { System.Console.WriteLine(i); }
3130 }
3131 }",
3132 "foo.cs",
3133 |metric| {
3134 // `!done` unwraps to the `done` terminal: one condition,
3135 // and no comparison token to double-count it.
3136 assert_eq!(metric.abc.conditions_sum(), 1);
3137 // `int i = 0` + `i++`.
3138 assert_eq!(metric.abc.assignments_sum(), 2);
3139 assert_eq!(metric.abc.branches_sum(), 1);
3140 },
3141 );
3142 }
3143
3144 #[cfg(feature = "groovy")]
3145 #[test]
3146 fn groovy_eq_arm_counts_outside_final_declarations() {
3147 // Bare reassignment of an already-declared variable: the `=`
3148 // belongs to no `final` declaration, so it counts. Mirrors
3149 // `java_eq_arm_counts_outside_final_declarations`.
3150 check_metrics::<GroovyParser>(
3151 "void f(int x) {
3152 x = 42
3153 }",
3154 "foo.groovy",
3155 |metric| {
3156 assert_eq!(metric.abc.assignments_sum(), 1);
3157 assert_eq!(metric.abc.branches_sum(), 0);
3158 assert_eq!(metric.abc.conditions_sum(), 0);
3159 },
3160 );
3161 }
3162
3163 #[cfg(feature = "groovy")]
3164 #[test]
3165 fn groovy_final_field_initializer_does_not_suppress_the_closure_body() {
3166 // The Groovy spelling of
3167 // `java_final_initializer_does_not_suppress_the_assignments_inside_it`:
3168 // a `final` field's closure body opens no space, and the sentinel
3169 // stack suppressed its `x = 1` with the declarator's own `=`.
3170 check_metrics::<GroovyParser>(
3171 "class K {
3172 int x
3173 final Closure c = { x = 1 }
3174 Closure d = { x = 2 }
3175 final int q = 3
3176 }",
3177 "foo.groovy",
3178 |metric| {
3179 // `x = 1`, `d = {…}`, `x = 2`; the two `final`
3180 // initializers are suppressed. Pre-fix: 2.
3181 assert_eq!(metric.abc.assignments_sum(), 3);
3182 },
3183 );
3184 }
3185
3186 #[cfg(feature = "groovy")]
3187 #[test]
3188 fn groovy_final_local_is_an_error_at_the_pinned_grammar() {
3189 // `groovy_eq_initializes_final_binding` lists
3190 // `local_variable_declaration` for symmetry with Java, but at
3191 // dekobon-tree-sitter-groovy 0.2.2 a `final` local never
3192 // reaches it: the parser emits an `ERROR` node for the
3193 // declaration, with or without a terminator, so both of its `=`
3194 // tokens count. This pins the grammar limitation so a bump that
3195 // starts parsing the local shows up here rather than as a silent
3196 // change in what the predicate suppresses.
3197 let source = b"class K { int x; def m() { final int a = 0; x = 1 } }";
3198 let parser = GroovyParser::new(
3199 source.to_vec(),
3200 &std::path::PathBuf::from("foo.groovy"),
3201 None,
3202 );
3203 assert!(
3204 ast_has_kind_id(&parser, u16::MAX),
3205 "a `final` local should still parse to an ERROR node; if the grammar \
3206 now accepts it, re-derive the local half of the predicate",
3207 );
3208 check_metrics::<GroovyParser>(
3209 "class K { int x; def m() { final int a = 0; x = 1 } }",
3210 "foo.groovy",
3211 |metric| {
3212 assert_eq!(metric.abc.assignments_sum(), 2);
3213 },
3214 );
3215 }
3216
3217 #[cfg(feature = "csharp")]
3218 #[test]
3219 fn csharp_const_initializer_shapes() {
3220 // `csharp_eq_initializes_const_binding` reads the `const`
3221 // through the `modifier` node one hop above the
3222 // `variable_declaration`, for a local and a field alike, behind
3223 // other modifiers; `readonly` is not `const` and its initializer
3224 // counts. A lambda's body opens its own space in C#, so unlike
3225 // Java there was never a nested `=` to leak — the row pins that
3226 // the structural rule keeps the count the sentinel gave.
3227 check_metrics::<CsharpParser>(
3228 "class K {
3229 int x;
3230 private const int Q = 1;
3231 public static readonly int R = 2;
3232 void M() { const int q = 3; System.Action a = () => { x = 4; }; }
3233 }",
3234 "foo.cs",
3235 |metric| {
3236 // `R = 2`, `a = () => …`, `x = 4`.
3237 assert_eq!(metric.abc.assignments_sum(), 3);
3238 },
3239 );
3240 }
3241
3242 #[cfg(feature = "csharp")]
3243 #[test]
3244 fn csharp_constant_declarations() {
3245 check_metrics::<CsharpParser>(
3246 "class A {
3247 private const int X1 = 0, Y1 = 0;
3248 public const float PI = 3.14f;
3249 const string HELLO = \"Hello,\";
3250 protected string world = \" world!\";
3251 public float e = 2.718f;
3252 private int x2 = 1, y2 = 2;
3253 void M() {
3254 const int Z1 = 0, Z2 = 0, Z3 = 0;
3255 const float T = 0.0f;
3256 int z1 = 1, z2 = 2, z3 = 3;
3257 }
3258 }",
3259 "foo.cs",
3260 |metric| insta::assert_json_snapshot!(metric.abc),
3261 );
3262 }
3263
3264 #[cfg(feature = "csharp")]
3265 #[test]
3266 fn csharp_declarations_with_conditions() {
3267 check_metrics::<CsharpParser>(
3268 "class A {
3269 bool a = (1 == 2);
3270 bool b = (1 < 2);
3271 bool c = !true;
3272 bool d = !false;
3273 }",
3274 "foo.cs",
3275 |metric| insta::assert_json_snapshot!(metric.abc),
3276 );
3277 }
3278
3279 #[cfg(feature = "csharp")]
3280 #[test]
3281 fn csharp_assignments_with_conditions() {
3282 check_metrics::<CsharpParser>(
3283 "class A {
3284 void M() {
3285 int a = 0;
3286 a += 1;
3287 a -= 2;
3288 a *= 3;
3289 a /= 4;
3290 a %= 5;
3291 a++;
3292 a--;
3293 }
3294 }",
3295 "foo.cs",
3296 |metric| insta::assert_json_snapshot!(metric.abc),
3297 );
3298 }
3299
3300 #[cfg(feature = "csharp")]
3301 #[test]
3302 fn csharp_methods_arguments_with_conditions() {
3303 check_metrics::<CsharpParser>(
3304 "class A {
3305 void M(int x, int y) {
3306 F(x == y, x < y, !x.Equals(y));
3307 }
3308 void F(bool a, bool b, bool c) {}
3309 }",
3310 "foo.cs",
3311 |metric| {
3312 // expected: `==` +1, `<` +1, and the negated argument
3313 // `!x.Equals(y)` +1 — it scored 0 until #1537, when every
3314 // C# argument reached the peel still in its `argument`
3315 // wrapper.
3316 assert_eq!(metric.abc.conditions_sum(), 3);
3317 insta::assert_json_snapshot!(metric.abc);
3318 },
3319 );
3320 }
3321
3322 // #1537: every C# argument is wrapped in an `argument` node that the
3323 // negation peel declined, so a negated argument scored 0 where every
3324 // other C-family language scores 1. Each row is a C# call next to its
3325 // Java twin, which takes the expected value from outside the fix.
3326 #[cfg(all(feature = "csharp", feature = "java"))]
3327 #[test]
3328 fn csharp_negated_call_argument_matches_java() {
3329 let rows = [
3330 ("G(!a);", "g(!a);", 1),
3331 ("G((!a));", "g((!a));", 1),
3332 ("G(!a, !b);", "g(!a, !b);", 2),
3333 ("G(a, !b);", "g(a, !b);", 1),
3334 ("new B(!a);", "new B(!a);", 1),
3335 ("G(-a);", "g(-a);", 0),
3336 // A negated comparison pays once, for its `>`; a chain pays
3337 // per operand. Neither argument may pay again as a negation.
3338 ("G(!(a > b));", "g(!(a > b));", 1),
3339 ("G(!a && b);", "g(!a && b);", 2),
3340 ];
3341 for (csharp, java, expected) in rows {
3342 let cs = abc_conditions(
3343 LANG::Csharp,
3344 &format!("class A {{ void M() {{ {csharp} }} }}"),
3345 );
3346 let jv = abc_conditions(LANG::Java, &format!("class A {{ void m() {{ {java} }} }}"));
3347 assert_eq!(jv, expected, "Java twin `{java}`");
3348 assert_eq!(cs, expected, "C# `{csharp}` against Java `{java}`");
3349 }
3350 }
3351
3352 // The C#-only spellings around an argument. The value is the
3353 // `argument` wrapper's last operand, so a named-argument label, a
3354 // passing-mode keyword or a comment before it must not hide it.
3355 #[cfg(feature = "csharp")]
3356 #[test]
3357 fn csharp_negated_call_argument_spellings() {
3358 let rows = [
3359 // A named argument: the value follows `name` and `:`.
3360 ("class A { void M() { G(name: !a); } }", 1),
3361 // A comment between the label and the value sits inside the
3362 // `argument` wrapper, so a positional read of the value lands
3363 // on it. A comment before or after a whole argument is a
3364 // sibling of the wrapper instead and never reaches this read.
3365 ("class A { void M() { G(name: /*c*/ !a); } }", 1),
3366 // `ref` / `out` / `in` precede a variable, never a negation. A
3367 // control only: 0 is also what the pre-#1537 walk scored, so
3368 // this row cannot fail for any defect the fix addresses.
3369 ("class A { void M() { G(ref x, out var y, in z); } }", 0),
3370 // The lambda arm pays for its `!a` body; the argument holding
3371 // the lambda is not a negation and must not pay again.
3372 ("class A { void M() { G(x => !a); } }", 1),
3373 // `: base(!a)` — a `constructor_initializer`'s list reaches the
3374 // walk like any call's.
3375 ("class A : B { A(bool a) : base(!a) {} }", 1),
3376 // The two primary-constructor base calls: the record nests its
3377 // list in a `primary_constructor_base_type`, the class's list
3378 // is the branch node itself and is scored on the branch path.
3379 ("record R(bool a) : Base(!a);", 1),
3380 ("class S(bool a) : Base(!a) {}", 1),
3381 // An indexer's `bracketed_argument_list` is not a call, as
3382 // Java's array index is not.
3383 ("class A { void M() { var q = o[!a]; } }", 0),
3384 ];
3385 for (src, expected) in rows {
3386 assert_eq!(abc_conditions(LANG::Csharp, src), expected, "{src}");
3387 }
3388 }
3389
3390 #[cfg(feature = "csharp")]
3391 #[test]
3392 fn csharp_if_single_conditions() {
3393 check_metrics::<CsharpParser>(
3394 "class A {
3395 void M(int x) {
3396 if (x > 0) { System.Console.WriteLine(\"a\"); }
3397 if (x < 0) { System.Console.WriteLine(\"b\"); }
3398 if (x == 0) { System.Console.WriteLine(\"c\"); }
3399 }
3400 }",
3401 "foo.cs",
3402 |metric| insta::assert_json_snapshot!(metric.abc),
3403 );
3404 }
3405
3406 #[cfg(feature = "csharp")]
3407 #[test]
3408 fn csharp_if_multiple_conditions() {
3409 check_metrics::<CsharpParser>(
3410 "class A {
3411 void M(int x, int y) {
3412 if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
3413 if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
3414 }
3415 }",
3416 "foo.cs",
3417 |metric| insta::assert_json_snapshot!(metric.abc),
3418 );
3419 }
3420
3421 #[cfg(feature = "csharp")]
3422 #[test]
3423 fn csharp_while_and_do_while_conditions() {
3424 check_metrics::<CsharpParser>(
3425 "class A {
3426 void M(int x) {
3427 while (x > 0) { x--; }
3428 do { x++; } while (x < 10);
3429 }
3430 }",
3431 "foo.cs",
3432 |metric| insta::assert_json_snapshot!(metric.abc),
3433 );
3434 }
3435
3436 #[cfg(feature = "csharp")]
3437 #[test]
3438 fn csharp_return_with_conditions() {
3439 check_metrics::<CsharpParser>(
3440 "class A {
3441 bool M(int x) {
3442 return (x > 0);
3443 }
3444 bool N(int x) {
3445 return !(x < 0);
3446 }
3447 }",
3448 "foo.cs",
3449 |metric| insta::assert_json_snapshot!(metric.abc),
3450 );
3451 }
3452
3453 // C# `switch` *expression* arms scored zero ABC conditions before
3454 // #456 — they carry no `case` / `default` token, so the token-driven
3455 // `csharp_count_token_condition` never saw them, even though C#
3456 // cyclomatic counts each non-discard arm. Revert-verified: adding the
3457 // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The
3458 // bare `_ =>` discard arm is excluded (the `default:` analogue),
3459 // mirroring the cyclomatic gate (lesson 11).
3460 #[cfg(feature = "csharp")]
3461 #[test]
3462 fn csharp_switch_expression_arm_counts_condition() {
3463 check_metrics::<CsharpParser>(
3464 "class A {
3465 int M(int x) {
3466 return x switch { 1 => 10, 2 => 20, _ => 0 };
3467 }
3468 }",
3469 "foo.cs",
3470 |metric| {
3471 // arm `1 =>` (+1) + arm `2 =>` (+1) + `_ =>` discard (+0).
3472 assert_eq!(metric.abc.conditions_sum(), 2);
3473 },
3474 );
3475 }
3476
3477 // Cross-language parity (lesson 11): a C# `switch` expression and the
3478 // equivalent Java arrow-`switch` must report the same ABC condition
3479 // count on equivalent code. Both have two concrete case arms and no
3480 // fallback arm, so both must count exactly 2. `check_metrics` takes a
3481 // non-capturing `fn` pointer, so the shared expected value (2) is
3482 // asserted in each callback rather than compared across closures; the
3483 // matching constant is what enforces parity. This guards against the
3484 // C# fix drifting away from the Java arrow-case treatment.
3485 #[cfg(all(feature = "csharp", feature = "java"))]
3486 #[test]
3487 fn csharp_java_switch_arm_abc_parity() {
3488 // C# switch expression: two arms, no fallback → 2 conditions.
3489 check_metrics::<CsharpParser>(
3490 "class A {
3491 int M(int x) {
3492 return x switch { 1 => 10, 2 => 20 };
3493 }
3494 }",
3495 "foo.cs",
3496 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3497 );
3498
3499 // Equivalent Java arrow-`switch`: two case arms, no default → 2.
3500 check_metrics::<JavaParser>(
3501 "class A {
3502 int m(int x) {
3503 return switch (x) { case 1 -> 10; case 2 -> 20; };
3504 }
3505 }",
3506 "foo.java",
3507 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3508 );
3509 }
3510
3511 // Issue #469: the `default` arm of a C-family `switch` is the
3512 // unconditional fallthrough and must NOT count as an ABC condition,
3513 // mirroring cyclomatic — which counts only the `Case` arms, never
3514 // the `Default` token.
3515 //
3516 // expected: each fixture is a single function whose switch has two
3517 // concrete `case` arms plus one `default`. ABC must count exactly
3518 // the two case arms (conditions = 2), matching cyclomatic's two
3519 // case-arm decisions. Pre-fix, every language below scored 3 (the
3520 // `Default` token leaked into the condition tally) — revert-verified
3521 // against the pre-#469 condition arms. We anchor on the integer
3522 // `conditions_sum()` headline (the value the public JSON serializes;
3523 // float magnitude is bit-brittle and excluded by the snapshot
3524 // policy). The cyclomatic side is pinned separately in
3525 // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where
3526 // the per-space `cyclomatic()` decision count is isolated.
3527 #[cfg(feature = "java")]
3528 #[test]
3529 fn java_switch_default_not_a_condition() {
3530 // Classic statement `default:`.
3531 check_metrics::<JavaParser>(
3532 "class A {
3533 int m(int x) {
3534 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3535 }
3536 }",
3537 "foo.java",
3538 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3539 );
3540 // Arrow `default ->` — shares the same `Default` token.
3541 check_metrics::<JavaParser>(
3542 "class A {
3543 int m(int x) {
3544 return switch (x) { case 1 -> 1; case 2 -> 2; default -> 0; };
3545 }
3546 }",
3547 "foo.java",
3548 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3549 );
3550 }
3551
3552 // Over-exclusion guard (issue #469): a statement `switch` with two
3553 // `case` arms and NO `default` must still count both cases. This
3554 // pins that the fix excludes only the `Default` token, never a
3555 // `Case` arm — the count is identical before and after #469 (two
3556 // cases → 2), so it would catch a fix that over-eagerly dropped a
3557 // real case (e.g. treating the trailing case as a fallthrough).
3558 // expected: case 1 (+1) + case 2 (+1) = 2.
3559 #[cfg(feature = "java")]
3560 #[test]
3561 fn java_switch_without_default_counts_all_cases() {
3562 check_metrics::<JavaParser>(
3563 "class A {
3564 int m(int x) {
3565 switch (x) { case 1: return 1; case 2: return 2; }
3566 return -1;
3567 }
3568 }",
3569 "foo.java",
3570 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3571 );
3572 }
3573
3574 #[cfg(feature = "csharp")]
3575 #[test]
3576 fn csharp_switch_default_not_a_condition() {
3577 check_metrics::<CsharpParser>(
3578 "class A {
3579 int M(int x) {
3580 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3581 }
3582 }",
3583 "foo.cs",
3584 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3585 );
3586 }
3587
3588 /// Regression #1450 / #1451: `goto case 2;` spells the same `case`
3589 /// keyword token (id 114) as a real arm, from a second production
3590 /// (`goto_statement`), and scored an ABC condition it does not earn —
3591 /// an unconditional jump to an arm is not a decision.
3592 ///
3593 /// `jmp` differs from `ctl` by exactly one statement, so the
3594 /// `goto case` is the only thing that can separate their scores
3595 /// (`.claude/rules/grammar-dispatch.md` §11 — an arm and a
3596 /// `goto case` are independent paths into the same counter, and a
3597 /// fixture carrying only arms cannot show the gate works). `dfl` is
3598 /// the control that was already correct, by accident rather than
3599 /// design: `goto default;` spells `Default`, which neither metric
3600 /// counts because it is the switch's unconditional fallthrough
3601 /// (#456, #469).
3602 ///
3603 /// [`assert_every_member_scores`] covers cyclomatic as well as ABC,
3604 /// through its per-member §8 parity assertion, and would catch the
3605 /// gate being removed from *either* side alone: dropping ABC's puts
3606 /// `jmp` at conditions 3 against the asserted 2, and dropping
3607 /// cyclomatic's leaves conditions 2 against a decision count of 3.
3608 /// Removing both restores the parity at `3 == 4 - 1` and is caught by
3609 /// the conditions value, which is why 2 is named rather than derived.
3610 ///
3611 /// The `Case` anchor is the second axis. Every member scores 2 from
3612 /// its two arms alone, so deleting `goto case 2;` leaves every
3613 /// assertion below satisfied and the construct under test gone; the
3614 /// anchor's count of 7 — six arms plus the one jump — fails by name
3615 /// instead.
3616 #[cfg(feature = "csharp")]
3617 #[test]
3618 fn csharp_goto_case_is_not_a_condition() {
3619 let src = "class A {
3620 int ctl(int x) { switch (x) { case 1: return 1; case 2: return 2; default: return 0; } }
3621 int jmp(int x) { switch (x) { case 1: goto case 2; case 2: return 2; default: return 0; } }
3622 int dfl(int x) { switch (x) { case 1: goto default; case 2: return 2; default: return 0; } }
3623 }";
3624 assert_csharp_fixture_spells(
3625 src,
3626 &[
3627 (Csharp::Case as u16, 7, "`case` tokens (6 arms + 1 jump)"),
3628 (Csharp::SwitchSection as u16, 9, "switch arms"),
3629 (Csharp::GotoStatement as u16, 2, "`goto` statements"),
3630 (
3631 Csharp::Default as u16,
3632 4,
3633 "`default` tokens (3 arms + 1 jump)",
3634 ),
3635 ],
3636 );
3637 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
3638 assert_every_member_scores(
3639 &space.spaces[0],
3640 3,
3641 2,
3642 "one condition per `case` arm, none for a `goto` into one",
3643 );
3644 });
3645 }
3646
3647 #[cfg(feature = "cpp")]
3648 #[test]
3649 fn cpp_switch_default_not_a_condition() {
3650 // C++ (and plain C, which shares this grammar) already excluded
3651 // `default`; this pins the cross-language parity invariant.
3652 check_metrics::<CppParser>(
3653 "void f(int x) {
3654 switch (x) { case 1: return; case 2: return; default: return; }
3655 }",
3656 "foo.cpp",
3657 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3658 );
3659 }
3660
3661 #[cfg(feature = "objc")]
3662 #[test]
3663 fn objc_abc() {
3664 // ObjC ABC reuses the C/C++ walker with two additions: a message
3665 // send `[obj msg]` is a call (B), and `@try` / `@catch` count as
3666 // conditions (C) like C++ try/catch.
3667 // A: `int total = 0`, `int i = 0`, `i++`, `total = total + …`,
3668 // `total = -1` = 5.
3669 // B: `[self valueAt:i]`, `[self risky]` = 2 message sends.
3670 // C: `i < n`, `@try`, `@catch`, `total >= 0` = 4.
3671 check_metrics::<ObjcParser>(
3672 "@implementation Foo\n\
3673 - (int)bar:(int)n {\n\
3674 int total = 0;\n\
3675 for (int i = 0; i < n; i++) {\n\
3676 total = total + [self valueAt:i];\n\
3677 }\n\
3678 @try {\n\
3679 [self risky];\n\
3680 } @catch (NSException *e) {\n\
3681 total = -1;\n\
3682 }\n\
3683 if (total >= 0) {\n\
3684 return total;\n\
3685 }\n\
3686 return 0;\n\
3687 }\n\
3688 @end\n",
3689 "foo.m",
3690 |metric| {
3691 assert_eq!(metric.abc.assignments_sum(), 5);
3692 assert_eq!(metric.abc.branches_sum(), 2);
3693 assert_eq!(metric.abc.conditions_sum(), 4);
3694 },
3695 );
3696 }
3697
3698 #[cfg(feature = "objc")]
3699 #[test]
3700 fn objc_abc_conditions() {
3701 // Exercises the condition-slot arms shared with C/C++: a `while`
3702 // head, a `&&` chain, a `do … while` trailing condition, and a
3703 // `return <comparison>`. ObjC routes these through the same
3704 // grammar-agnostic `cpp_inspect_*` helpers.
3705 // A: `p++`, `p--` = 2. B: no calls = 0.
3706 // C: `p != 0`, `*p > 0`, `*p < 9`, `*p == 0` = 4.
3707 check_metrics::<ObjcParser>(
3708 "@implementation Foo\n\
3709 - (int)g:(int *)p {\n\
3710 while (p != 0 && *p > 0) {\n\
3711 p++;\n\
3712 }\n\
3713 do {\n\
3714 p--;\n\
3715 } while (*p < 9);\n\
3716 return *p == 0;\n\
3717 }\n\
3718 @end\n",
3719 "foo.m",
3720 |metric| {
3721 assert_eq!(metric.abc.assignments_sum(), 2);
3722 assert_eq!(metric.abc.branches_sum(), 0);
3723 assert_eq!(metric.abc.conditions_sum(), 4);
3724 },
3725 );
3726 }
3727
3728 #[cfg(feature = "objc")]
3729 #[test]
3730 fn objc_abc_message_send_unary_condition() {
3731 // A negated boolean passed as a message-send argument is a unary
3732 // condition (Fitzpatrick Rule 9), the same as in a C-call argument.
3733 // Message args are direct children of `message_expression` (no
3734 // `argument_list`), so they are inspected in the `MessageExpression`
3735 // arm. Here: `[self use:!a]` (1 call + 1 unary condition) +
3736 // `cFunc(!a)` (1 call + 1 unary condition) → B=2, C=2.
3737 check_metrics::<ObjcParser>(
3738 "@implementation Foo\n\
3739 - (void)bar:(int)a {\n\
3740 [self use:!a];\n\
3741 cFunc(!a);\n\
3742 }\n\
3743 @end\n",
3744 "foo.m",
3745 |metric| {
3746 assert_eq!(metric.abc.branches_sum(), 2);
3747 assert_eq!(metric.abc.conditions_sum(), 2);
3748 },
3749 );
3750 }
3751
3752 #[cfg(feature = "objc")]
3753 #[test]
3754 fn objc_message_send_is_a_bool_terminal_in_condition_slots() {
3755 // `[obj ok]` is Objective-C's call, so in a condition slot it is
3756 // the unary condition `ok()` is (Fitzpatrick Rule 9). Until
3757 // `message_expression` joined the former `cpp_bool_terminal_kinds!` every
3758 // row here scored zero conditions where its C-call twin scored
3759 // one — and #1276's `for` slot inherited the gap. Each row is a
3760 // message send as the *whole* slot (grammar-dispatch §11: a
3761 // comparison would be counted by its own operator arm anyway),
3762 // and branches are asserted beside conditions so the arm cannot
3763 // be read as counting the call twice. The last three are the
3764 // over-count guards: a value position counts nothing, as `ok()`'s
3765 // does not.
3766 let cases = [
3767 ("if ([self ok]) {}", 1, 1),
3768 ("while (![self ok]) {}", 1, 1),
3769 ("do {} while ([self ok]);", 1, 1),
3770 ("for (; [self ok]; ) {}", 1, 1),
3771 ("x = [self ok] ? 1 : 2;", 2, 1),
3772 ("if ([self ok] && [o ok]) {}", 2, 2),
3773 ("return [self ok];", 0, 1),
3774 ("[self use:[self ok]];", 0, 2),
3775 ("[self use:![self ok]];", 1, 2),
3776 ];
3777 let mut ran = 0;
3778 for (body, conditions, branches) in cases {
3779 let src = format!("@implementation Foo\n- (int)bar {{\n {body}\n}}\n@end\n");
3780 let abc = metrics_verbatim(
3781 LANG::Objc,
3782 src.as_bytes(),
3783 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
3784 )
3785 .abc;
3786 assert_eq!(abc.conditions_sum(), conditions, "`{body}` conditions");
3787 assert_eq!(abc.branches_sum(), branches, "`{body}` branches");
3788 ran += 1;
3789 }
3790 assert_eq!(ran, cases.len());
3791 // Both answers are present, so a walker stuck at 0 or at 1 cannot
3792 // pass half the table silently.
3793 assert!(cases.iter().any(|&(_, c, _)| c == 0));
3794 assert!(cases.iter().any(|&(_, c, _)| c == 2));
3795 }
3796
3797 #[cfg(feature = "objc")]
3798 #[test]
3799 fn objc_message_send_condition_agrees_with_c_call() {
3800 // The intra-ObjC parity C++ cannot express: a message send and a
3801 // C call in the same slot score alike. Non-degenerate by the
3802 // `assert_eq!(…, 1)` on the reference.
3803 for (send, call) in [
3804 ("if ([a ok]) {}", "if (ok()) {}"),
3805 ("for (; [a ok]; ) {}", "for (; ok(); ) {}"),
3806 ("x = [a ok] ? 1 : 2;", "x = ok() ? 1 : 2;"),
3807 ] {
3808 let wrap =
3809 |body: &str| format!("@implementation Foo\n- (int)bar {{\n {body}\n}}\n@end\n");
3810 let reference = abc_conditions(LANG::Objc, &wrap(call));
3811 assert!(reference >= 1, "`{call}` must count at least the slot");
3812 assert_eq!(
3813 abc_conditions(LANG::Objc, &wrap(send)),
3814 reference,
3815 "`{send}`"
3816 );
3817 }
3818 }
3819
3820 #[cfg(feature = "groovy")]
3821 #[test]
3822 fn groovy_switch_default_not_a_condition() {
3823 check_metrics::<GroovyParser>(
3824 "class A {
3825 int m(int x) {
3826 switch (x) { case 1: return 1; case 2: return 2; default: return 0 }
3827 }
3828 }",
3829 "foo.groovy",
3830 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3831 );
3832 }
3833
3834 #[cfg(feature = "javascript")]
3835 #[test]
3836 fn js_switch_default_not_a_condition() {
3837 check_metrics::<JavascriptParser>(
3838 "function f(x) {
3839 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3840 }",
3841 "foo.js",
3842 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3843 );
3844 }
3845
3846 #[cfg(feature = "typescript")]
3847 #[test]
3848 fn ts_switch_default_not_a_condition() {
3849 check_metrics::<TypescriptParser>(
3850 "function f(x: number): number {
3851 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3852 }",
3853 "foo.ts",
3854 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3855 );
3856 }
3857
3858 // Cross-language parity (lesson 11): the equivalent statement-`switch`
3859 // with a `default` arm reports the same ABC condition count across
3860 // Java / C# / C++. All three have two concrete case arms plus a
3861 // fallthrough `default`, so all three must count exactly 2 conditions
3862 // (the `default` excluded). `check_metrics` takes a non-capturing
3863 // `fn` pointer, so the shared expected value is asserted in each
3864 // callback; the matching constant is what enforces parity.
3865 #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3866 #[test]
3867 fn java_csharp_cpp_switch_default_abc_parity() {
3868 check_metrics::<JavaParser>(
3869 "class A {
3870 int m(int x) {
3871 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3872 }
3873 }",
3874 "foo.java",
3875 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3876 );
3877 check_metrics::<CsharpParser>(
3878 "class A {
3879 int M(int x) {
3880 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3881 }
3882 }",
3883 "foo.cs",
3884 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3885 );
3886 check_metrics::<CppParser>(
3887 "void f(int x) {
3888 switch (x) { case 1: return; case 2: return; default: return; }
3889 }",
3890 "foo.cpp",
3891 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3892 );
3893 }
3894
3895 // Pins the ABC-vs-cyclomatic agreement the fix is about (lesson 11):
3896 // on the method's own function space, the cyclomatic decision count
3897 // (`cyclomatic()` minus the per-space base of 1) must equal the ABC
3898 // `conditions()` for the same switch. Both must be 2 — the two case
3899 // arms — with the `default` excluded from each. Revert-verified: pre-
3900 // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2.
3901 #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3902 #[test]
3903 fn java_csharp_cpp_switch_default_cyclomatic_parity() {
3904 check_func_space::<JavaParser, _>(
3905 "class A {
3906 int m(int x) {
3907 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3908 }
3909 }",
3910 "foo.java",
3911 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3912 );
3913 check_func_space::<CsharpParser, _>(
3914 "class A {
3915 int M(int x) {
3916 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3917 }
3918 }",
3919 "foo.cs",
3920 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3921 );
3922 check_func_space::<CppParser, _>(
3923 "void f(int x) {
3924 switch (x) { case 1: return; case 2: return; default: return; }
3925 }",
3926 "foo.cpp",
3927 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3928 );
3929 }
3930
3931 // Issue #473: PHP `switch` `default:` (`DefaultStatement`) is the
3932 // unconditional fallthrough, not a condition. ABC `conditions()` must
3933 // equal the cyclomatic decision count (`cyclomatic() - 1`) on the
3934 // function's own space — both 2 for the two `case` arms, with the
3935 // `default` excluded. Revert-verified: re-adding `DefaultStatement` to
3936 // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic
3937 // stays at 2, failing the invariant.
3938 #[cfg(feature = "php")]
3939 #[test]
3940 fn php_switch_default_not_a_condition() {
3941 check_func_space::<PhpParser, _>(
3942 "<?php
3943 function f($x) {
3944 switch ($x) {
3945 case 1: return 1;
3946 case 2: return 2;
3947 default: return 0;
3948 }
3949 }",
3950 "foo.php",
3951 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3952 );
3953 }
3954
3955 // Issue #473: PHP `match` `default =>` (`MatchDefaultExpression`) is the
3956 // unconditional fallthrough, mirroring the switch `default:` case above.
3957 // ABC `conditions()` must equal the cyclomatic decision count
3958 // (`cyclomatic() - 1`) — both 2 for the two non-default match arms.
3959 // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC
3960 // condition arm makes `conditions()` 3 here while cyclomatic stays at 2.
3961 #[cfg(feature = "php")]
3962 #[test]
3963 fn php_match_default_not_a_condition() {
3964 check_func_space::<PhpParser, _>(
3965 "<?php
3966 function g($x) {
3967 return match ($x) {
3968 1 => \"a\",
3969 2 => \"b\",
3970 default => \"z\",
3971 };
3972 }",
3973 "foo.php",
3974 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3975 );
3976 }
3977
3978 #[cfg(feature = "csharp")]
3979 #[test]
3980 fn csharp_if_bare_identifier_condition() {
3981 check_metrics::<CsharpParser>(
3982 "class A {
3983 void M(bool x) {
3984 if (x) { System.Console.WriteLine(\"a\"); }
3985 }
3986 }",
3987 "foo.cs",
3988 |metric| {
3989 // `if (x)` contributes 1 condition (bare identifier).
3990 // `System.Console.WriteLine(...)` is the only call → 1 branch.
3991 // `*_sum()` is what the public JSON serializes as the
3992 // headline value (see `crate::wire::Abc`).
3993 assert_eq!(metric.abc.conditions_sum(), 1);
3994 assert_eq!(metric.abc.branches_sum(), 1);
3995 assert_eq!(metric.abc.assignments_sum(), 0);
3996 },
3997 );
3998 }
3999
4000 #[cfg(feature = "csharp")]
4001 #[test]
4002 fn csharp_while_bare_identifier_condition() {
4003 check_metrics::<CsharpParser>(
4004 "class A {
4005 void M(bool x) {
4006 while (x) { x = false; }
4007 }
4008 }",
4009 "foo.cs",
4010 |metric| {
4011 // `while (x)` contributes 1 condition; `x = false` is 1 assignment.
4012 assert_eq!(metric.abc.conditions_sum(), 1);
4013 assert_eq!(metric.abc.assignments_sum(), 1);
4014 assert_eq!(metric.abc.branches_sum(), 0);
4015 },
4016 );
4017 }
4018
4019 #[cfg(feature = "csharp")]
4020 #[test]
4021 fn csharp_do_while_bare_identifier_condition() {
4022 check_metrics::<CsharpParser>(
4023 "class A {
4024 void M(bool x) {
4025 do { x = true; } while (x);
4026 }
4027 }",
4028 "foo.cs",
4029 |metric| {
4030 // `do { ... } while (x)` contributes 1 condition;
4031 // `x = true` is 1 assignment.
4032 assert_eq!(metric.abc.conditions_sum(), 1);
4033 assert_eq!(metric.abc.assignments_sum(), 1);
4034 assert_eq!(metric.abc.branches_sum(), 0);
4035 },
4036 );
4037 }
4038
4039 #[cfg(feature = "csharp")]
4040 #[test]
4041 fn csharp_if_unary_not_condition() {
4042 // Two cases share one test:
4043 //
4044 // if (!x) { … } — the `if` predicate is a boolean slot, so
4045 // `csharp_count_condition` pays for it once the peel
4046 // (`csharp_wrapper_operand`) has taken the `!` off; the
4047 // slot would pay without the `!` too.
4048 //
4049 // return !x; — a `return` value is not a slot, so the `!`
4050 // is the *only* thing that makes the operand count, through
4051 // `csharp_count_negated`. Asserting the `return !x;`
4052 // case isolates the negation path from the slot path.
4053 check_metrics::<CsharpParser>(
4054 "class A {
4055 void M(bool x) {
4056 if (!x) { System.Console.WriteLine(\"a\"); }
4057 }
4058 bool N(bool x) {
4059 return !x;
4060 }
4061 }",
4062 "foo.cs",
4063 |metric| {
4064 // `if (!x)` contributes 1 condition (the slot pays for the
4065 // peeled `x`). `return !x;` contributes 1 condition (not a
4066 // slot — the unary `!` is what makes `x` count).
4067 // → 2 conditions total. 1 branch from WriteLine().
4068 assert_eq!(metric.abc.conditions_sum(), 2);
4069 assert_eq!(metric.abc.branches_sum(), 1);
4070 assert_eq!(metric.abc.assignments_sum(), 0);
4071 },
4072 );
4073 }
4074
4075 #[cfg(feature = "csharp")]
4076 #[test]
4077 fn csharp_if_double_parenthesized_condition() {
4078 // Audit-tests follow-up: with only the
4079 // `csharp_prefix_unary_expr_kinds!()` arm covered by
4080 // `csharp_if_unary_not_condition`, the
4081 // `csharp_paren_expr_kinds!()` delegation arm in
4082 // `csharp_count_condition` was a pure dead-code candidate —
4083 // disabling it caused zero existing tests to fail (verified
4084 // 2026-05-26).
4085 //
4086 // `if ((x))` puts a `ParenthesizedExpression` at child(2) of
4087 // the IfStatement (child(1) is the literal `(`, child(2) is
4088 // the inner parenthesised expression, child(3) is the literal
4089 // `)`). `csharp_count_condition` must peel that parenthesis
4090 // (`csharp_wrapper_operand`) down to the inner Identifier,
4091 // which does not score itself, so the slot pays once.
4092 check_metrics::<CsharpParser>(
4093 "class A {
4094 void M(bool x) {
4095 if ((x)) { System.Console.WriteLine(\"a\"); }
4096 }
4097 }",
4098 "foo.cs",
4099 |metric| {
4100 assert_eq!(metric.abc.conditions_sum(), 1);
4101 assert_eq!(metric.abc.branches_sum(), 1);
4102 assert_eq!(metric.abc.assignments_sum(), 0);
4103 },
4104 );
4105 }
4106
4107 #[cfg(feature = "csharp")]
4108 #[test]
4109 fn csharp_bool_returning_terminal_kinds_count() {
4110 // Regression for issue #372 (lesson #19): before the fix,
4111 // `csharp_count_condition` / the former `csharp_inspect_container`
4112 // only recognised invocation / identifier / boolean literal as
4113 // terminal-bool operands, so the five idiomatic boolean
4114 // expressions in the `if (...)` slots below silently scored
4115 // zero conditions:
4116 //
4117 // - `cfg.flag` — MemberAccessExpression
4118 // - `await c.Check()` — AwaitExpression
4119 // - `(bool)v` — CastExpression
4120 // - `v is not null` — IsPatternExpression
4121 // - `flags[0]` — ElementAccessExpression
4122 //
4123 // The `is` test has since moved out of the former terminal set to an
4124 // unconditional arm (#1461), so it no longer depends on the
4125 // slot to be counted. The total is unchanged, which is the
4126 // point of that change rather than an accident of it.
4127 //
4128 // expected: 5 conditions (one per `if`), 0 assignments,
4129 // 1 branch (the single `c.Check()` invocation; the other
4130 // `if`-condition expressions are not invocations).
4131 check_metrics::<CsharpParser>(
4132 "using System.Threading.Tasks;
4133 class A {
4134 async Task M(object v, bool[] flags, Cfg cfg, C c) {
4135 if (cfg.flag) { }
4136 if (await c.Check()) { }
4137 if ((bool)v) { }
4138 if (v is not null) { }
4139 if (flags[0]) { }
4140 }
4141 }
4142 class Cfg { public bool flag; }
4143 class C { public Task<bool> Check() => null; }",
4144 "foo.cs",
4145 |metric| {
4146 assert_eq!(metric.abc.conditions_sum(), 5);
4147 assert_eq!(metric.abc.assignments_sum(), 0);
4148 assert_eq!(metric.abc.branches_sum(), 1);
4149 },
4150 );
4151 }
4152
4153 #[cfg(feature = "csharp")]
4154 #[test]
4155 fn csharp_if_method_call_condition() {
4156 check_metrics::<CsharpParser>(
4157 "class A {
4158 void M(string s) {
4159 if (s.StartsWith(\"x\")) { System.Console.WriteLine(\"a\"); }
4160 }
4161 }",
4162 "foo.cs",
4163 |metric| {
4164 // `if (s.StartsWith("x"))` contributes 1 condition
4165 // (InvocationExpression) plus 1 branch for the call itself,
4166 // plus 1 branch for WriteLine.
4167 assert_eq!(metric.abc.conditions_sum(), 1);
4168 assert_eq!(metric.abc.branches_sum(), 2);
4169 assert_eq!(metric.abc.assignments_sum(), 0);
4170 },
4171 );
4172 }
4173
4174 #[cfg(feature = "csharp")]
4175 #[test]
4176 fn csharp_if_while_boolean_literal_condition() {
4177 // Regression for #371: the tree-sitter-c-sharp grammar wraps a
4178 // bare `true` / `false` literal used as the condition of
4179 // `if` / `while` / `do` / `?:` in a `boolean_literal` node,
4180 // not the leaf `true` / `false` tokens. `csharp_count_condition`
4181 // must therefore match `BooleanLiteral` (the wrapper),
4182 // mirroring the existing `csharp_walk_for_statement` arm.
4183 // Without that, every literal-condition statement scored 0
4184 // conditions. The `&&` / `||` chain-operand slot is covered
4185 // separately by `csharp_short_circuit_with_boolean_literal_operand`
4186 // and the negated-operand path (`csharp_count_negated`) by
4187 // `csharp_declarations_with_conditions` (`!true` / `!false`).
4188 check_metrics::<CsharpParser>(
4189 "class A {
4190 void M() {
4191 if (true) { System.Console.WriteLine(\"a\"); }
4192 if (false) { System.Console.WriteLine(\"b\"); }
4193 while (true) { break; }
4194 do { break; } while (false);
4195 int t = true ? 1 : 0;
4196 }
4197 }",
4198 "foo.cs",
4199 |metric| {
4200 // Five literal-condition statements contribute 5
4201 // `BooleanLiteral` conditions (one per if/if/while/
4202 // do-while/ternary), plus the ternary's `?` token
4203 // adds one more via `csharp_count_token_condition`
4204 // → 6 total. The two `System.Console.WriteLine`
4205 // calls contribute 2 branches; the `int t = …`
4206 // initializer contributes 1 assignment.
4207 assert_eq!(metric.abc.conditions_sum(), 6);
4208 assert_eq!(metric.abc.branches_sum(), 2);
4209 assert_eq!(metric.abc.assignments_sum(), 1);
4210 },
4211 );
4212 }
4213
4214 #[cfg(feature = "csharp")]
4215 #[test]
4216 fn csharp_short_circuit_with_boolean_literal_operand() {
4217 // Regression for #371 (companion to
4218 // `csharp_if_while_boolean_literal_condition`): a bare
4219 // `true` / `false` operand of `&&` / `||` is a chain-operand
4220 // slot scored through `csharp_count_condition`. A
4221 // `BooleanLiteral` does not score itself, so the slot pays for
4222 // it — otherwise the operand silently scores zero.
4223 check_metrics::<CsharpParser>(
4224 "class A {
4225 void M(bool x) {
4226 if (x && true) { System.Console.WriteLine(\"a\"); }
4227 if (false || x) { System.Console.WriteLine(\"b\"); }
4228 }
4229 }",
4230 "foo.cs",
4231 |metric| {
4232 // `&&` and `||` themselves are NOT in
4233 // `csharp_count_token_condition`'s match list — they
4234 // route through `csharp_walk_for_conditions::AMPAMP|
4235 // PIPEPIPE`, which scores each operand of the parent
4236 // BinaryExpression through `csharp_count_condition`.
4237 // An operand pays one condition unless it scores
4238 // itself. For
4239 // `x && true`: 1 (Identifier x) + 1 (BooleanLiteral
4240 // true) = 2. For `false || x`: 1 (BooleanLiteral
4241 // false) + 1 (Identifier x) = 2. Total 4.
4242 assert_eq!(metric.abc.conditions_sum(), 4);
4243 assert_eq!(metric.abc.branches_sum(), 2);
4244 assert_eq!(metric.abc.assignments_sum(), 0);
4245 },
4246 );
4247 }
4248
4249 #[cfg(feature = "csharp")]
4250 #[test]
4251 fn csharp_return_without_conditions() {
4252 check_metrics::<CsharpParser>(
4253 "class A {
4254 int M() { return 42; }
4255 string N() { return \"hi\"; }
4256 }",
4257 "foo.cs",
4258 |metric| insta::assert_json_snapshot!(metric.abc),
4259 );
4260 }
4261
4262 #[cfg(feature = "csharp")]
4263 #[test]
4264 fn csharp_lambda_expressions_return_with_conditions() {
4265 check_metrics::<CsharpParser>(
4266 "class A {
4267 public void M() {
4268 System.Func<int, bool> f = x => (x > 0);
4269 System.Func<int, bool> g = x => !(x < 0);
4270 }
4271 }",
4272 "foo.cs",
4273 |metric| insta::assert_json_snapshot!(metric.abc),
4274 );
4275 }
4276
4277 #[cfg(feature = "csharp")]
4278 #[test]
4279 fn csharp_for_with_variable_declaration() {
4280 check_metrics::<CsharpParser>(
4281 "class A {
4282 void M() {
4283 for (int i = 0; i < 10; i++) {
4284 System.Console.WriteLine(i);
4285 }
4286 }
4287 }",
4288 "foo.cs",
4289 |metric| insta::assert_json_snapshot!(metric.abc),
4290 );
4291 }
4292
4293 #[cfg(feature = "csharp")]
4294 #[test]
4295 fn csharp_for_without_variable_declaration() {
4296 check_metrics::<CsharpParser>(
4297 "class A {
4298 void M() {
4299 int i;
4300 for (i = 0; i < 10; i++) {
4301 System.Console.WriteLine(i);
4302 }
4303 }
4304 }",
4305 "foo.cs",
4306 |metric| insta::assert_json_snapshot!(metric.abc),
4307 );
4308 }
4309
4310 #[cfg(feature = "csharp")]
4311 #[test]
4312 fn csharp_for_identifier_condition() {
4313 check_metrics::<CsharpParser>(
4314 "class A {
4315 void M(bool ready) {
4316 for (; ready ;) { }
4317 }
4318 }",
4319 "foo.cs",
4320 |metric| {
4321 // expected: assignments=0 (no `=` / `++` / `--`),
4322 // branches=0 (no invocation / object creation),
4323 // conditions=1 (bare-identifier for-loop condition).
4324 // Averages divide by 3 spaces (top-level + class + method).
4325 insta::assert_json_snapshot!(
4326 metric.abc,
4327 @r#"
4328 {
4329 "assignments": 0,
4330 "branches": 0,
4331 "conditions": 1,
4332 "magnitude": 1.0,
4333 "value": 0.0,
4334 "assignments_average": 0.0,
4335 "branches_average": 0.0,
4336 "conditions_average": 0.3333333333333333,
4337 "assignments_min": 0,
4338 "assignments_max": 0,
4339 "branches_min": 0,
4340 "branches_max": 0,
4341 "conditions_min": 0,
4342 "conditions_max": 1
4343 }
4344 "#
4345 );
4346 },
4347 );
4348 }
4349
4350 #[cfg(feature = "csharp")]
4351 #[test]
4352 fn csharp_for_invocation_condition() {
4353 check_metrics::<CsharpParser>(
4354 "class A {
4355 bool Ok() { return true; }
4356 void M() {
4357 for (; Ok() ;) { }
4358 }
4359 }",
4360 "foo.cs",
4361 |metric| {
4362 // expected: assignments=0, branches=1 (the `Ok()` call),
4363 // conditions=1 (invocation as for-loop condition).
4364 // Averages divide by 4 spaces (top-level + class + two
4365 // methods).
4366 insta::assert_json_snapshot!(
4367 metric.abc,
4368 @r#"
4369 {
4370 "assignments": 0,
4371 "branches": 1,
4372 "conditions": 1,
4373 "magnitude": 1.4142135623730951,
4374 "value": 0.0,
4375 "assignments_average": 0.0,
4376 "branches_average": 0.25,
4377 "conditions_average": 0.25,
4378 "assignments_min": 0,
4379 "assignments_max": 0,
4380 "branches_min": 0,
4381 "branches_max": 1,
4382 "conditions_min": 0,
4383 "conditions_max": 1
4384 }
4385 "#
4386 );
4387 },
4388 );
4389 }
4390
4391 // Regression coverage for #279: the C# grammar wraps a literal
4392 // `true` / `false` for-loop condition in a `boolean_literal` node.
4393 // The `BooleanLiteral` arm in the `ForStatement` dispatch must
4394 // attribute one condition; without it, `for (; true ;)` would
4395 // contribute 0 (the bug fixed by this commit also affected this
4396 // shape).
4397 #[cfg(feature = "csharp")]
4398 #[test]
4399 fn csharp_for_boolean_literal_condition() {
4400 check_metrics::<CsharpParser>(
4401 "class A {
4402 void M() {
4403 for (; true ;) { }
4404 }
4405 }",
4406 "foo.cs",
4407 |metric| {
4408 // expected: assignments=0, branches=0,
4409 // conditions=1 (the `true` literal as condition).
4410 assert_eq!(metric.abc.conditions_sum(), 1);
4411 assert_eq!(metric.abc.assignments_sum(), 0);
4412 assert_eq!(metric.abc.branches_sum(), 0);
4413 },
4414 );
4415 }
4416
4417 // Regression coverage for #279: an empty for-loop condition such as
4418 // `for (; ;) {}` must contribute 0 to conditions — there is no
4419 // condition node to count.
4420 #[cfg(feature = "csharp")]
4421 #[test]
4422 fn csharp_for_empty_condition() {
4423 check_metrics::<CsharpParser>(
4424 "class A {
4425 void M() {
4426 for (; ;) { }
4427 }
4428 }",
4429 "foo.cs",
4430 |metric| {
4431 // expected: assignments=0, branches=0, conditions=0
4432 // (no condition expression in `for (; ;)`).
4433 insta::assert_json_snapshot!(
4434 metric.abc,
4435 @r#"
4436 {
4437 "assignments": 0,
4438 "branches": 0,
4439 "conditions": 0,
4440 "magnitude": 0.0,
4441 "value": 0.0,
4442 "assignments_average": 0.0,
4443 "branches_average": 0.0,
4444 "conditions_average": 0.0,
4445 "assignments_min": 0,
4446 "assignments_max": 0,
4447 "branches_min": 0,
4448 "branches_max": 0,
4449 "conditions_min": 0,
4450 "conditions_max": 0
4451 }
4452 "#
4453 );
4454 },
4455 );
4456 }
4457
4458 #[cfg(feature = "csharp")]
4459 #[test]
4460 fn csharp_ternary_conditions() {
4461 check_metrics::<CsharpParser>(
4462 "class A {
4463 int Sign(int x) {
4464 return (x > 0) ? 1 : (x < 0 ? -1 : 0);
4465 }
4466 }",
4467 "foo.cs",
4468 |metric| insta::assert_json_snapshot!(metric.abc),
4469 );
4470 }
4471
4472 #[cfg(feature = "csharp")]
4473 #[test]
4474 fn csharp_malformed_parenthesized_no_panic() {
4475 check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
4476 // Don't panic on malformed source.
4477 assert_eq!(metric.abc.assignments(), 0);
4478 assert_eq!(metric.abc.branches(), 0);
4479 });
4480 }
4481
4482 #[cfg(feature = "csharp")]
4483 #[test]
4484 fn csharp_function_pointer_type_no_double_count() {
4485 // EC1 extension — `<` and `>` are also parameter-list delimiters
4486 // for unsafe function-pointer types. Since #1297 the arm is an
4487 // allowlist, so `FunctionPointerType` is excluded by being
4488 // absent from it rather than by being named in a deny set;
4489 // either way these brackets must not accumulate `conditions`.
4490 check_metrics::<CsharpParser>(
4491 "unsafe class A {
4492 public delegate*<int, int, int> Adder;
4493 public delegate*<string, void> Logger;
4494 }",
4495 "foo.cs",
4496 |metric| {
4497 assert_eq!(
4498 metric.abc.conditions(),
4499 0,
4500 "function-pointer-type angle brackets must not count"
4501 );
4502 },
4503 );
4504 }
4505
4506 #[cfg(feature = "csharp")]
4507 #[test]
4508 fn csharp_generic_type_args_no_double_count() {
4509 // EC1 — `<` and `>` inside TypeArgumentList must not count as
4510 // boolean conditions.
4511 check_metrics::<CsharpParser>(
4512 "class A {
4513 void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
4514 System.Console.WriteLine(d);
4515 }
4516 }",
4517 "foo.cs",
4518 |metric| insta::assert_json_snapshot!(metric.abc),
4519 );
4520 }
4521
4522 // #1297: `operator <` names the operator a type *defines*; applying
4523 // one is what a decision looks like. tree-sitter-c-sharp spells the
4524 // declaration's operator with the same bare token as a comparison,
4525 // and the previous denylist named only `type_argument_list`,
4526 // `type_parameter_list` and `function_pointer_type`, so every
4527 // comparison-operator overload scored a condition.
4528 //
4529 // The fixture carries the two overloads plus one `a < b` inside a
4530 // `binary_expression` and one `x is > 0 ? 2 : 3`, which the grammar
4531 // parses as a `relational_pattern` whose operand is the ternary (see
4532 // the assertion). Every mis-aim lands on its own number: 7
4533 // with neither gate, 5 if `RelationalPattern` is readmitted to the
4534 // allowlist (#1383 dropped it), 3 if the gate swallows
4535 // `BinaryExpression` too (only the ternary's `?` and slot and the
4536 // `is` test survive), 0 if the fixture stops parsing. Each is two
4537 // higher than before #1461 and #1526, which added the `is` test and
4538 // the ternary's slot as counts no gate on the `<` / `>` token can
4539 // reach.
4540 #[cfg(feature = "csharp")]
4541 #[test]
4542 fn csharp_operator_declaration_is_not_a_condition() {
4543 check_func_space::<CsharpParser, _>(
4544 "class V {
4545 public static bool operator <(V a, V b) { return true; }
4546 public static bool operator >(V a, V b) { return true; }
4547 int m(int a, int b, int x) {
4548 if (a < b) { return 1; }
4549 return x is > 0 ? 2 : 3;
4550 }
4551 }",
4552 "foo.cs",
4553 |space| {
4554 // Assert the claim per space rather than through the
4555 // file total, which is 2 with the two overloads and 2
4556 // without them — an aggregate assertion would pass on a
4557 // fixture that had lost the very construct under test.
4558 let class = &space.spaces[0];
4559 assert_eq!(class.spaces.len(), 3, "two overloads plus `m`");
4560 for (i, member) in class.spaces.iter().enumerate().take(2) {
4561 assert_eq!(
4562 member.metrics.abc.conditions(),
4563 0,
4564 "operator declaration {i} must score no condition"
4565 );
4566 }
4567 // 4: the `a < b` comparison, the ternary `?`, its
4568 // condition slot, and the `is` test. tree-sitter-c-sharp
4569 // 0.23.5 parses `x is > 0 ? 2 : 3` as `x is > (0 ? 2 :
4570 // 3)` — the ternary is the pattern's operand, so the
4571 // pattern sits in no decision slot and its `>` still
4572 // scores nothing (#1383), while the ternary's condition
4573 // is the literal `0`, which pays as a slot since #1526.
4574 // The fourth count is the enclosing
4575 // `is_pattern_expression` itself, which since #1461
4576 // scores by use rather than only inside a boolean slot.
4577 // C# itself binds the source `(x is > 0) ? 2 : 3`; a
4578 // grammar that agrees reads 3 here, because the `is`
4579 // test in the condition slot already pays for it.
4580 assert_eq!(class.spaces[2].metrics.abc.conditions(), 4);
4581 },
4582 );
4583 }
4584
4585 // #1420, the other four spellings of the test above. C# overloads six
4586 // comparison operators and #1297 gated two: `<=` `>=` `==` `!=` are
4587 // distinct tokens that reach a different arm of
4588 // `csharp_count_token_condition` and each still scored one spurious
4589 // condition on its declaration — measured 1 apiece against `<` / `>`'s
4590 // 0, on a class where `cyclomatic()` is 1 for all six.
4591 //
4592 // Each token gets its own member so a partial fix names the spelling
4593 // it missed: gating `EQEQ | BANGEQ` and forgetting `GTEQ | LTEQ` fails
4594 // here by operator, where the file total cannot tell the two apart —
4595 // it reads 4 before the fix and 0 after, so any two-of-four fix halves
4596 // it and still looks like movement.
4597 //
4598 // `n` is the deliberate control (§11): the same four tokens *applied*,
4599 // inside `binary_expression` parents, which must keep scoring one
4600 // condition each. Without it the gate could be satisfied by refusing
4601 // to count these tokens at all.
4602 #[cfg(feature = "csharp")]
4603 #[test]
4604 fn csharp_comparison_operator_overloads_are_not_conditions() {
4605 let src = "class V {
4606 public static bool operator <=(V a, V b) { return true; }
4607 public static bool operator >=(V a, V b) { return true; }
4608 public static bool operator ==(V a, V b) { return true; }
4609 public static bool operator !=(V a, V b) { return true; }
4610 int n(int a, int b) {
4611 if (a <= b) { return 1; }
4612 if (a >= b) { return 2; }
4613 if (a == b) { return 3; }
4614 if (a != b) { return 4; }
4615 return 0;
4616 }
4617 }";
4618 // Two of each token: one declaring, one applying. Trimming either
4619 // half out collapses this test into the other half's control and
4620 // is what this anchor exists to catch.
4621 assert_csharp_fixture_spells(
4622 src,
4623 &[
4624 (
4625 Csharp::OperatorDeclaration as u16,
4626 4,
4627 "operator declarations",
4628 ),
4629 (Csharp::LTEQ as u16, 2, "`<=`"),
4630 (Csharp::GTEQ as u16, 2, "`>=`"),
4631 (Csharp::EQEQ as u16, 2, "`==`"),
4632 (Csharp::BANGEQ as u16, 2, "`!=`"),
4633 ],
4634 );
4635 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4636 let class = &space.spaces[0];
4637 assert_eq!(class.spaces.len(), 5, "four overloads plus `n`");
4638 for (member, spelling) in class.spaces.iter().zip(["<=", ">=", "==", "!="]) {
4639 assert_eq!(
4640 member.metrics.abc.conditions(),
4641 0,
4642 "`operator {spelling}` declares an operator, it does not apply one"
4643 );
4644 assert_eq!(
4645 member.metrics.abc.conditions(),
4646 member.metrics.cyclomatic.cyclomatic() - 1,
4647 "`operator {spelling}`: §8 parity with the cyclomatic decision count"
4648 );
4649 }
4650 // The control, found by name so a reordered fixture cannot
4651 // silently re-point it at an overload. 0 here would mean the
4652 // gate swallowed the applying form along with the declaring
4653 // one.
4654 let control = child_space(class, "n");
4655 assert_eq!(
4656 control.metrics.abc.conditions(),
4657 4,
4658 "`n` applies all four tokens: one condition each"
4659 );
4660 });
4661 }
4662
4663 // The allowlist #1420 installed is the only thing keeping `#if A == B`
4664 // a condition, and it keeps it for a reason no arm states: the grammar
4665 // aliases `preproc_binary_expression` onto `binary_expression`, so the
4666 // preprocessor's `==` arrives with kind id `BinaryExpression` (369)
4667 // and not with one of its own. Before #1420 the token was ungated and
4668 // no fact about its parent mattered; now a grammar bump that gave the
4669 // alias a distinct id would stop counting every `#if` equality with
4670 // nothing red. This is that pin.
4671 //
4672 // `BinaryExpression2` (469) is the pre-alias symbol the arm also
4673 // lists, defensively, per `.claude/rules/grammar-dispatch.md` §1. §2
4674 // asks that such an entry be kept *and* have its unreachability
4675 // asserted, so that a pin promoting the symbol changes behaviour
4676 // loudly rather than invisibly.
4677 #[cfg(feature = "csharp")]
4678 #[test]
4679 fn csharp_preproc_equality_counts_through_the_binary_expression_alias() {
4680 let src = "class P {
4681 #if A == B
4682 int x;
4683 #endif
4684 #if C != D
4685 int y;
4686 #endif
4687 }";
4688 let parser = CsharpParser::new(
4689 src.as_bytes().to_vec(),
4690 std::path::Path::new("foo.cs"),
4691 None,
4692 );
4693 assert!(
4694 ast_has_kind_id(&parser, Csharp::BinaryExpression as u16),
4695 "the `#if` expressions must parse as `binary_expression` — \
4696 the alias is what the arm's allowlist matches"
4697 );
4698 assert!(
4699 !ast_has_kind_id(&parser, Csharp::BinaryExpression2 as u16),
4700 "`BinaryExpression2` is the pre-alias symbol and must stay \
4701 unreachable; if the grammar starts emitting it, re-derive the \
4702 allowlist rather than trusting this arm"
4703 );
4704 // 2, one per `#if` expression. The two directives carry no
4705 // declarations the walk would score, so nothing else contributes.
4706 check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
4707 assert_eq!(metric.abc.conditions_sum(), 2);
4708 });
4709 }
4710
4711 // #1448, the C++ / Mozcpp instance of #1420. C++ overloads seven
4712 // comparison operators; before the fix only `<` / `>` were gated on a
4713 // `binary_expression` parent, so `operator<=` `>=` `==` `!=` and the
4714 // spaceship `operator<=>` each scored one spurious condition on a
4715 // struct where every member has `cyclomatic()` 1.
4716 //
4717 // Members are looked up by name, so a partial fix fails on the
4718 // spelling it missed rather than on a total that merely moved. `n`
4719 // is the §11 control: the same seven tokens applied, which must
4720 // keep scoring one condition each. The census anchors both halves —
4721 // trimming an overload or an application out of the fixture fails
4722 // by name instead of decaying into its counterpart.
4723 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4724 const CPP_COMPARISON_OVERLOADS: &str = "struct V {
4725 bool operator<(const V& o) const { return true; }
4726 bool operator>(const V& o) const { return true; }
4727 bool operator<=(const V& o) const { return true; }
4728 bool operator>=(const V& o) const { return true; }
4729 bool operator==(const V& o) const { return true; }
4730 bool operator!=(const V& o) const { return true; }
4731 auto operator<=>(const V& o) const { return 0; }
4732 };
4733 struct W {
4734 int n;
4735 bool operator==(const W&) const = default;
4736 };
4737 int n(int a, int b) {
4738 return (a < b) + (a > b) + (a <= b) + (a >= b) + (a == b) + (a != b) + (a <=> b);
4739 }";
4740
4741 // `operator_name` id, then the seven token ids in source order, for
4742 // whichever of the two grammars `P` parses.
4743 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4744 fn assert_cpp_comparison_overloads_are_not_conditions<P: MetricSuite>(
4745 operator_name: u16,
4746 tokens: [u16; 7],
4747 ) {
4748 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4749 const SPELLINGS: [&str; 7] = ["<", ">", "<=", ">=", "==", "!=", "<=>"];
4750 let mut census = vec![(operator_name, 8, "operator names")];
4751 // Two of each token, one declaring and one applying, plus the
4752 // third `==` that `W`'s defaulted comparison declares.
4753 census.extend(
4754 tokens
4755 .iter()
4756 .zip(SPELLINGS)
4757 .map(|(&id, s)| (id, if s == "==" { 3 } else { 2 }, s)),
4758 );
4759 assert_fixture_spells::<P>(CPP_COMPARISON_OVERLOADS, "foo.cpp", &census);
4760 check_func_space::<P, _>(CPP_COMPARISON_OVERLOADS, "foo.cpp", |space| {
4761 let class = child_space(&space, "V");
4762 assert_eq!(class.spaces.len(), 7, "seven overloads");
4763 for spelling in SPELLINGS {
4764 let member = child_space(class, &format!("operator{spelling}"));
4765 assert_eq!(
4766 member.metrics.abc.conditions(),
4767 0,
4768 "`operator{spelling}` declares an operator, it does not apply one"
4769 );
4770 assert_eq!(
4771 member.metrics.abc.conditions(),
4772 member.metrics.cyclomatic.cyclomatic() - 1,
4773 "`operator{spelling}`: §8 parity with the cyclomatic decision count"
4774 );
4775 }
4776 assert_eq!(
4777 child_space(&space, "n").metrics.abc.conditions(),
4778 7,
4779 "`n` applies all seven tokens: one condition each"
4780 );
4781 // A defaulted C++20 comparison declares the operator with no
4782 // body; it is the common modern spelling and must score
4783 // nothing anywhere in the file.
4784 assert_eq!(
4785 space.metrics.abc.conditions_sum(),
4786 7,
4787 "only `n`'s seven applications are conditions"
4788 );
4789 });
4790 }
4791
4792 #[cfg(feature = "cpp")]
4793 #[test]
4794 fn cpp_comparison_operator_overloads_are_not_conditions() {
4795 assert_cpp_comparison_overloads_are_not_conditions::<CppParser>(
4796 Cpp::OperatorName as u16,
4797 [
4798 Cpp::LT,
4799 Cpp::GT,
4800 Cpp::LTEQ,
4801 Cpp::GTEQ,
4802 Cpp::EQEQ,
4803 Cpp::BANGEQ,
4804 Cpp::LTEQGT,
4805 ]
4806 .map(|k| k as u16),
4807 );
4808 }
4809
4810 // Mozcpp owns no file extension, so this is its only coverage of the
4811 // arm: no integration snapshot routes to it.
4812 #[cfg(feature = "mozcpp")]
4813 #[test]
4814 fn mozcpp_comparison_operator_overloads_are_not_conditions() {
4815 use crate::Mozcpp;
4816 assert_cpp_comparison_overloads_are_not_conditions::<crate::MozcppParser>(
4817 Mozcpp::OperatorName as u16,
4818 [
4819 Mozcpp::LT,
4820 Mozcpp::GT,
4821 Mozcpp::LTEQ,
4822 Mozcpp::GTEQ,
4823 Mozcpp::EQEQ,
4824 Mozcpp::BANGEQ,
4825 Mozcpp::LTEQGT,
4826 ]
4827 .map(|k| k as u16),
4828 );
4829 }
4830
4831 // ISO C++ spells `!`, `&&`, `||` and `!=` equally as `not`, `and`,
4832 // `or` and `not_eq` ([lex.digraph]), and the C++ grammars give each
4833 // alternative token a kind of its own. ABC tested only the symbols,
4834 // so `if (not b)` scored 0 against `if (!b)`'s 1, `b and c` scored 0
4835 // against `b && c`'s 2, and `b not_eq c` scored 0 against `b != c`'s
4836 // 1. Each row pairs a symbolic spelling with its alternative and the
4837 // value both must give; the census proves the alternative fixture
4838 // really carries the alternative token rather than parsing it as an
4839 // identifier, which is what tree-sitter-c does with the same text.
4840 //
4841 // Arguments: the `not`, `and`, `or` and `not_eq` ids of `P`'s grammar.
4842 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4843 fn assert_cpp_alternative_tokens_score_like_symbols<P: MetricSuite>(alt: [u16; 4]) {
4844 let [not, and, or, not_eq] = alt;
4845 let rows = [
4846 ("if (!b) {}", "if (not b) {}", not, 1),
4847 ("bool x = !b && c;", "bool x = not b && c;", not, 2),
4848 ("bool x = c ? !b : c;", "bool x = c ? not b : c;", not, 3),
4849 ("bool x = b && c;", "bool x = b and c;", and, 2),
4850 ("if (b || c) {}", "if (b or c) {}", or, 2),
4851 ("if (b != c) {}", "if (b not_eq c) {}", not_eq, 1),
4852 ];
4853 for (symbolic, alternative, token, expected) in rows {
4854 let alt_source = format!("void f(bool b, bool c) {{\n {alternative}\n}}\n");
4855 assert_fixture_spells::<P>(&alt_source, "foo.cpp", &[(token, 1, alternative)]);
4856 for source in [symbolic, alternative] {
4857 let source = format!("void f(bool b, bool c) {{\n {source}\n}}\n");
4858 check_func_space::<P, _>(&source, "foo.cpp", |space| {
4859 assert_eq!(
4860 space.metrics.abc.conditions_sum(),
4861 expected,
4862 "`{source}` must score like its symbolic spelling `{symbolic}`"
4863 );
4864 });
4865 }
4866 }
4867 }
4868
4869 #[cfg(feature = "cpp")]
4870 #[test]
4871 fn cpp_alternative_tokens_score_like_symbols() {
4872 assert_cpp_alternative_tokens_score_like_symbols::<CppParser>(
4873 [Cpp::Not, Cpp::And, Cpp::Or, Cpp::NotEq].map(|k| k as u16),
4874 );
4875 }
4876
4877 // Mozcpp owns no file extension, so this is its only coverage of the
4878 // alternative tokens: no integration snapshot routes to it.
4879 #[cfg(feature = "mozcpp")]
4880 #[test]
4881 fn mozcpp_alternative_tokens_score_like_symbols() {
4882 use crate::Mozcpp;
4883 assert_cpp_alternative_tokens_score_like_symbols::<crate::MozcppParser>(
4884 [Mozcpp::Not, Mozcpp::And, Mozcpp::Or, Mozcpp::NotEq].map(|k| k as u16),
4885 );
4886 }
4887
4888 // The two other productions #1448's allowlist decides, alongside the
4889 // operator overloads above.
4890 //
4891 // `#if A <= B` is a real decision and must keep counting: the grammar
4892 // aliases `preproc_binary_expression` onto `binary_expression`, and
4893 // the allowlist is the only thing that admits it now that `<=` / `!=`
4894 // are gated. The fixture's two directives are the root's only
4895 // conditions.
4896 //
4897 // A fold over a comparison operator counts once, through the fold's
4898 // `operator` field: before the fix `lt` scored 0, `le` 1 and the
4899 // binary fold `eq` 2 (one per spelling of its single operator).
4900 // `idiom` is the control — its `==` sits in a `binary_expression`
4901 // inside the fold and counts there, while the fold's own `&&` is not
4902 // a comparison: it scores what its twin `(a == 0) && (a == 0)` does,
4903 // 2 (#1533). `neg` is the same twice-spelled operator under the `||`
4904 // chain arm: it scores what `true || !a` does, 2, where walking the
4905 // fold once per spelling would pay 4.
4906 //
4907 // The census also pins §2: every `binary_expression` here carries the
4908 // `BinaryExpression2` id and the pre-alias `BinaryExpression` is never
4909 // emitted. `cpp_operator_is_applied` matches the kind *name*, which
4910 // covers both, so a grammar that starts emitting the pre-alias id
4911 // changes nothing — this census is what would notice it.
4912 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4913 fn assert_cpp_comparison_folds_and_preproc_operands<P: MetricSuite>(
4914 binary_expression: u16,
4915 binary_expression2: u16,
4916 fold_expression: u16,
4917 ) {
4918 let src = "#if A <= B
4919 #endif
4920 #if C != D
4921 #endif
4922 template <typename... T> bool lt(T... a) { return (a < ...); }
4923 template <typename... T> bool le(T... a) { return (a <= ...); }
4924 template <typename... T> bool eq(T... a) { return (0 == ... == a); }
4925 template <typename... T> bool idiom(T... a) { return ((a == 0) && ...); }
4926 template <typename... T> bool neg(T... a) { return (true || ... || !a); }";
4927 assert_fixture_spells::<P>(
4928 src,
4929 "foo.cpp",
4930 &[
4931 (fold_expression, 5, "fold expressions"),
4932 (binary_expression2, 3, "binary expressions"),
4933 (binary_expression, 0, "pre-alias binary expressions"),
4934 ],
4935 );
4936 check_func_space::<P, _>(src, "foo.cpp", |space| {
4937 for (name, want) in [("lt", 1), ("le", 1), ("eq", 1), ("idiom", 2), ("neg", 2)] {
4938 assert_eq!(
4939 child_space(&space, name).metrics.abc.conditions(),
4940 want,
4941 "fold in `{name}`"
4942 );
4943 }
4944 assert_eq!(
4945 space.metrics.abc.conditions(),
4946 2,
4947 "one condition per `#if` comparison"
4948 );
4949 });
4950 }
4951
4952 #[cfg(feature = "cpp")]
4953 #[test]
4954 fn cpp_comparison_folds_and_preproc_operands() {
4955 assert_cpp_comparison_folds_and_preproc_operands::<CppParser>(
4956 Cpp::BinaryExpression as u16,
4957 Cpp::BinaryExpression2 as u16,
4958 Cpp::FoldExpression as u16,
4959 );
4960 }
4961
4962 #[cfg(feature = "mozcpp")]
4963 #[test]
4964 fn mozcpp_comparison_folds_and_preproc_operands() {
4965 use crate::Mozcpp;
4966 assert_cpp_comparison_folds_and_preproc_operands::<crate::MozcppParser>(
4967 Mozcpp::BinaryExpression as u16,
4968 Mozcpp::BinaryExpression2 as u16,
4969 Mozcpp::FoldExpression as u16,
4970 );
4971 }
4972
4973 // #1383: a `relational_pattern`'s operator is not a condition of
4974 // its own — the `switch_expression_arm` that owns it already scores
4975 // the decision, exactly as it does for the constant arm `5 => 1`.
4976 // Counting both charged a relational arm twice, and twice C#'s own
4977 // cyclomatic decision count.
4978 //
4979 // Per `.claude/rules/grammar-dispatch.md` §11 the `operator <`
4980 // fixture above cannot prove this alone: its `is > 0` sits beside a
4981 // `binary_expression` comparison that supplies a plausible total on
4982 // its own. Here the pattern operators are the *only* `<` / `>` /
4983 // `>=` / `<=` in the file, so a readmitted `RelationalPattern`
4984 // parent is the only thing that can lift the count.
4985 //
4986 // Both spellings are covered, and asserted per member rather than
4987 // through a total, because they reached two different arms when this
4988 // was written: `n`'s `>` / `<` were gated by a `GT | LT` parent
4989 // allowlist and `g`'s `>=` / `<=` by a separate `RelationalPattern`
4990 // denial, so fixing one and not the other left the *other method* at
4991 // 4. #1420 merged all six comparison tokens onto the one allowlist,
4992 // which removes that particular way to get it half right — but the
4993 // per-member shape still earns its keep, since a total of 6 is what
4994 // both `{2, 2, 2}` and a regressed `{4, 2, 0}` report:
4995 //
4996 // | state | `n` | `g` | `c` |
4997 // |---|---|---|---|
4998 // | `relational_pattern` excluded (shipped) | 2 | 2 | 2 |
4999 // | readmitted to the allowlist | 4 | 4 | 2 |
5000 //
5001 // `c` is the constant-pattern control and reads 2 in both rows: it is
5002 // what the relational methods are supposed to agree with.
5003 #[cfg(feature = "csharp")]
5004 #[test]
5005 fn csharp_relational_pattern_does_not_double_count_its_arm() {
5006 let src = "class A {
5007 int n(int x) => x switch { > 5 => 1, < 0 => 2, _ => 3 };
5008 int g(int x) => x switch { >= 5 => 1, <= 0 => 2, _ => 3 };
5009 int c(int x) => x switch { 5 => 1, 0 => 2, _ => 3 };
5010 }";
5011 assert_csharp_fixture_spells(
5012 src,
5013 &[
5014 (Csharp::RelationalPattern as u16, 4, "relational patterns"),
5015 (Csharp::GT as u16, 1, "`>`"),
5016 (Csharp::LT as u16, 1, "`<`"),
5017 (Csharp::GTEQ as u16, 1, "`>=`"),
5018 (Csharp::LTEQ as u16, 1, "`<=`"),
5019 ],
5020 );
5021 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5022 // Per space, not through the root: the root can only see
5023 // the sum, and a `{4, 2, 0}` regression sums to the same 6
5024 // as the correct `{2, 2, 2}`. `cyclomatic()` parity is also
5025 // only expressible per space — `cyclomatic_sum()` folds in
5026 // a base of 1 for each one.
5027 assert_every_member_scores(&space.spaces[0], 3, 2, "one condition per non-discard arm");
5028 });
5029 }
5030
5031 // The `is` half of #1383, and the one whose over-count came from a
5032 // different owner: `if (x is > 0)` scored the `IfStatement`
5033 // condition slot *and* the pattern's `>`. `f`'s guard is the
5034 // deliberate control — a `binary_expression` comparison in the same
5035 // slot still scores exactly one, so the fix cannot be read as
5036 // "stop counting `if` conditions".
5037 //
5038 // The two combinator methods pin the compound forms the issue asks
5039 // about. `and` / `or` are keyword tokens C# ABC never listed and C#
5040 // cyclomatic never counts, so the arm stays the single decision
5041 // however many relational operands it carries — which is exactly
5042 // what would regress if a later fix re-derived the gate from the
5043 // operand instead of the parent.
5044 #[cfg(feature = "csharp")]
5045 #[test]
5046 fn csharp_is_pattern_and_combinators_score_one_decision() {
5047 let src = "class A {
5048 int m(int x) { if (x is > 0) { return 1; } return 0; }
5049 int f(int x) { if (x > 0) { return 1; } return 0; }
5050 int a(int x) { if (x is > 0 and < 9) { return 1; } return 0; }
5051 int o(int x) { if (x is < 0 or >= 100) { return 1; } return 0; }
5052 int n(int x) { if (x is not > 5) { return 1; } return 0; }
5053 }";
5054 assert_csharp_fixture_spells(
5055 src,
5056 &[
5057 (Csharp::RelationalPattern as u16, 6, "relational patterns"),
5058 (Csharp::IsPatternExpression as u16, 4, "`is` pattern tests"),
5059 (Csharp::AndPattern as u16, 1, "`and`"),
5060 (Csharp::OrPattern as u16, 1, "`or`"),
5061 (Csharp::NegatedPattern as u16, 1, "`not`"),
5062 (Csharp::GTEQ as u16, 1, "`>=` (only `o` carries one)"),
5063 ],
5064 );
5065 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5066 assert_every_member_scores(&space.spaces[0], 5, 1, "the `if` is the only decision");
5067 });
5068 }
5069
5070 // #1383's second, quieter effect, and the one its issue does not
5071 // mention: a relational pattern outside any decision slot went from
5072 // 1 to 0 as well, because the gate is on the operator's parent and
5073 // not on what encloses the pattern. That left `q` — the equivalent
5074 // binary comparison `x > 5` in the same slot — scoring 1 against
5075 // these four's 0, so a relational pattern read one *lower* than the
5076 // comparison it is sugar for. #1383 recorded that as a deliberate
5077 // exception to Fitzpatrick Rule 5 and kept it, because the only
5078 // route it had was to readmit the pattern's `<` / `>` token, which
5079 // would have scored the decision-slot case twice.
5080 //
5081 // #1461 closed it from the other end, and the test now pins the
5082 // agreement rather than the exception. The enclosing
5083 // `is_pattern_expression` scores by use, so `x is > 5` reads 1
5084 // wherever it is written — level with `q`, and level with the plain
5085 // type test `x is int` (`t`, whose `is_expression` moved for the
5086 // same reason). The pattern's own `>` still scores nothing, which
5087 // is what keeps the whole test at 1 rather than 2: one relational
5088 // operation, one condition.
5089 //
5090 // The four probes stay because they are four different enclosings —
5091 // a declarator, a bare `return`, an argument, a type test — and the
5092 // point was never that the pattern is special in one of them.
5093 #[cfg(feature = "csharp")]
5094 #[test]
5095 fn csharp_relational_pattern_scores_one_wherever_it_is_written() {
5096 let src = "class A {
5097 static bool M(bool b) { return b; }
5098 bool p(int x) { bool b = x is > 5; return b; }
5099 bool q(int x) { bool b = x > 5; return b; }
5100 bool r(int x) { return x is > 5; }
5101 bool s(int x) { return M(x is > 5); }
5102 bool t(object x) { bool b = x is int; return b; }
5103 }";
5104 assert_csharp_fixture_spells(
5105 src,
5106 &[
5107 (Csharp::RelationalPattern as u16, 3, "relational patterns"),
5108 (Csharp::IsPatternExpression as u16, 3, "`is` pattern tests"),
5109 (Csharp::IsExpression as u16, 1, "the `is int` control"),
5110 ],
5111 );
5112 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5113 let class = &space.spaces[0];
5114 assert_eq!(class.spaces.len(), 6, "`M` plus five probes");
5115 let by_name = |n: &str| {
5116 class
5117 .spaces
5118 .iter()
5119 .find(|s| s.name.as_deref() == Some(n))
5120 .unwrap_or_else(|| panic!("fixture lost `{n}`"))
5121 .metrics
5122 .abc
5123 .conditions()
5124 };
5125 // Named rather than indexed: the claim is about which
5126 // spelling scores what, so a reordering of the fixture must
5127 // not silently re-point the assertions.
5128 for probe in ["p", "r", "s"] {
5129 assert_eq!(by_name(probe), 1, "`{probe}`: the `is` test, once");
5130 }
5131 assert_eq!(by_name("t"), 1, "the bare type test, the same once");
5132 assert_eq!(by_name("q"), 1, "the comparison it is sugar for");
5133 assert_eq!(by_name("M"), 0, "no relational operator anywhere");
5134 });
5135 }
5136
5137 // The boundary of #1383: the pattern's operator stops counting, a
5138 // *binary* operator in the arm's `when` guard keeps counting (`w`).
5139 // Both sit under the same `switch_expression_arm`, so this is what
5140 // stops a later "patterns don't count" pass from suppressing the
5141 // guard too.
5142 //
5143 // A relational *pattern* in the guard (`g`) is the other side of that
5144 // line: the operator itself still scores nothing, because the gate is
5145 // on the operator's parent — but since #1422 the guard is a condition
5146 // slot, and an `is` test scores itself (`csharp_condition_scores_itself`),
5147 // so the slot does not pay again. That is what closed the gap this test used to
5148 // carry a `FIXME(#1422)` for: `when n is > 5` now reads level with
5149 // `when n > 5` instead of one below it.
5150 //
5151 // §8 holds throughout, which it did not before #1422. The old table
5152 // here recorded the defect: neither metric modelled the guard, so
5153 // ABC's count was whatever operator token happened to sit inside it
5154 // and cyclomatic's was nothing at all. Both tiers measured, before
5155 // and after:
5156 //
5157 // | guard | `conditions()` | `cyclomatic() - 1` |
5158 // |---|---|---|
5159 // | `when x % 2 == 0` (this fixture) | 3 → 3 | 2 → 3 |
5160 // | `when x > 2` | 3 → 3 | 2 → 3 |
5161 // | `when IsEven(x)` | 2 → 3 | 2 → 3 |
5162 //
5163 // The `==` is the guard's own operator and the only `==` in the
5164 // file, so trimming the `when` clause out of `w` drops its count to
5165 // 2 rather than leaving the assertion satisfied by something else;
5166 // `assert_csharp_fixture_spells` pins both guards by kind against
5167 // the same decay.
5168 #[cfg(feature = "csharp")]
5169 #[test]
5170 fn csharp_switch_arm_guard_operator_still_counts() {
5171 let src = "class A {
5172 int w(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
5173 int g(int x) => x switch { int n when n is > 5 => 1, _ => 0 };
5174 }";
5175 assert_csharp_fixture_spells(
5176 src,
5177 &[
5178 (Csharp::WhenClause as u16, 2, "`when` guards"),
5179 (Csharp::EQEQ as u16, 1, "the guard's `==`"),
5180 (Csharp::RelationalPattern as u16, 3, "relational patterns"),
5181 ],
5182 );
5183 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5184 let m = &space.spaces[0].spaces[0];
5185 assert_eq!(m.name.as_deref(), Some("w"));
5186 // 4 if the guard slot started counting a `binary_expression`
5187 // on top of its `==`, 2 if the `==` were suppressed along
5188 // with the pattern operators — both are live regressions.
5189 assert_eq!(
5190 m.metrics.abc.conditions(),
5191 3,
5192 "two arms plus the guard's `==`"
5193 );
5194 assert_eq!(m.metrics.cyclomatic.cyclomatic(), 4);
5195
5196 let g = &space.spaces[0].spaces[1];
5197 assert_eq!(g.name.as_deref(), Some("g"));
5198 assert_eq!(
5199 g.metrics.abc.conditions(),
5200 2,
5201 "one arm plus the guard slot, which scores the `is` test"
5202 );
5203 assert_eq!(g.metrics.cyclomatic.cyclomatic(), 3);
5204 });
5205 }
5206
5207 // #1422's headline: four spellings of one guard, previously worth
5208 // two different ABC numbers because nothing modelled the guard and
5209 // ABC counted whatever operator token happened to sit inside it —
5210 // `when x % 2 == 0` and `when x > 2` scored 1 through the
5211 // comparison-token arm, `when IsEven(x)` scored 0. Modelled as a
5212 // condition slot each contributes exactly 1, and the cyclomatic
5213 // `WhenClause` arm lifts the decision count to match.
5214 //
5215 // `none` is the unguarded control and the anchor: a two-arm switch
5216 // over the same relational patterns carrying no guard at all, so
5217 // every member here is exactly one guard away from it. Without that
5218 // row a fix that scored the *arms* differently would satisfy the
5219 // four guarded rows alike. Its arms run `> 2` before `> 0` because
5220 // the reverse order is a C# compile error (CS8510, the later arm
5221 // subsumed) — the guarded members escape that only because a guard
5222 // can fail, which is what makes their repeated `> 0` legal.
5223 //
5224 // `paren` is not a fourth spelling for its own sake — it is the only
5225 // member whose guard reaches the slot as a
5226 // `parenthesized_expression` for the peel (`csharp_wrapper_operand`)
5227 // to take off, since `when_clause` wraps a parenthesised guard in a
5228 // real `parenthesized_expression` rather than the anonymous parens
5229 // `catch_filter_clause` uses.
5230 //
5231 // `nullc` is #1459's row and the counter-example that falsified the
5232 // claim above when it was first written. The slot leaves an
5233 // operator guard to the arm that already counts the operator, and
5234 // no ABC arm counted `??`, so this one spelling read 2 where the
5235 // other four read 3. Counting the token levels it without touching
5236 // the slot; an unconditional `+1` in the slot instead would have
5237 // taken `cmp` to 4, re-creating the double count #1422 removed.
5238 //
5239 // It is the one guarded member whose cyclomatic is not 4, and that
5240 // is not a discrepancy to fix: C# cyclomatic counts `??` as a
5241 // decision *in addition to* the guard clause, so the arm below
5242 // narrows the ABC-versus-cyclomatic gap from two to one rather than
5243 // closing it. The remaining one is the slot's standing policy of
5244 // leaving a counted operator to its arm, and `cmp` pays it
5245 // too — it just happens to break even there.
5246 #[cfg(feature = "csharp")]
5247 #[test]
5248 fn csharp_switch_arm_guard_scores_one_condition_however_spelled() {
5249 let src = "class A {
5250 static bool IsEven(int x) { return true; }
5251 int tok(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
5252 int cmp(int x) => x switch { > 0 when x > 2 => 1, > 0 => 2, _ => 3 };
5253 int call(int x) => x switch { > 0 when IsEven(x) => 1, > 0 => 2, _ => 3 };
5254 int paren(int x) => x switch { > 0 when (IsEven(x)) => 1, > 0 => 2, _ => 3 };
5255 int nullc(int x, bool? b) => x switch { > 0 when b ?? false => 1, > 0 => 2, _ => 3 };
5256 int none(int x) => x switch { > 2 => 1, > 0 => 2, _ => 3 };
5257 }";
5258 assert_csharp_fixture_spells(
5259 src,
5260 &[
5261 (Csharp::WhenClause as u16, 5, "`when` guards"),
5262 (Csharp::EQEQ as u16, 1, "`tok`'s `==`"),
5263 (Csharp::QMARKQMARK as u16, 1, "`nullc`'s `??`"),
5264 (
5265 Csharp::ParenthesizedExpression as u16,
5266 1,
5267 "`paren`'s parenthesised guard",
5268 ),
5269 // By the call's *argument list*, which carries no
5270 // numeric-suffix alias, rather than by
5271 // `invocation_expression`, which has three
5272 // (`InvocationExpression` / `2` / `3`) and would pin this
5273 // anchor to whichever one the pinned grammar happens to
5274 // emit. `call` and `paren` hold the only two argument
5275 // lists in the fixture — `IsEven(int x)` is a
5276 // `parameter_list`.
5277 (Csharp::ArgumentList as u16, 2, "the `IsEven` calls"),
5278 ],
5279 );
5280 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5281 assert_members_score(
5282 &space.spaces[0],
5283 &[
5284 ("IsEven", 0, 1),
5285 ("tok", 3, 4),
5286 ("cmp", 3, 4),
5287 ("call", 3, 4),
5288 ("paren", 3, 4),
5289 // Was 2 — the guard spelling nothing counted.
5290 // Cyclomatic 5 rather than 4 because `??` is a
5291 // decision there on top of the guard clause.
5292 ("nullc", 3, 5),
5293 ("none", 2, 3),
5294 ],
5295 );
5296 });
5297 }
5298
5299 // The bare type test `x is int` is `is_expression` (391); only once
5300 // a pattern is involved (`x is int y`, `x is null`) does the grammar
5301 // emit `is_pattern_expression` (392). They are distinct kinds, and
5302 // the former `csharp_bool_terminal_kinds!()` listed only the
5303 // second, so the two spellings of one test disagreed: `if (x is int)` scored zero
5304 // conditions against a cyclomatic decision of one while
5305 // `if (x is int y)` scored one.
5306 //
5307 // Found by the whole-branch review of this batch, and it falsified
5308 // #1422's own claim in the arm above that a guard "scores one
5309 // however it is spelled" — `when x is int` read 1 where
5310 // `when IsEven(x)` read 2, which is the spelling-dependence that
5311 // fix exists to remove. It is the C# half of the gap #1421 closed
5312 // for Kotlin by adding `IsExpression | InExpression` there. Both
5313 // languages' type tests have since moved out of the terminal set to
5314 // an unconditional arm (#1461), which leaves every number here
5315 // unchanged — the slot no longer counts the test and the arm does.
5316 //
5317 // The plain `if` members are the control that keeps this honest: a
5318 // guard-only fixture could be satisfied by a guard-specific rule,
5319 // and the defect was never guard-specific. `q` and `iq` are the
5320 // already-correct `is_pattern_expression` twins, so a regression
5321 // that reintroduced the asymmetry fails on the pair rather than on
5322 // an absolute number.
5323 #[cfg(feature = "csharp")]
5324 #[test]
5325 fn csharp_bare_is_type_test_scores_one_condition() {
5326 let src = "class A {
5327 static bool IsEven(int x) { return true; }
5328 int g(object x) => x switch { object o when x is int => 1, _ => 0 };
5329 int gq(object x) => x switch { object o when x is int y => 1, _ => 0 };
5330 int gc(object x) => x switch { object o when IsEven(1) => 1, _ => 0 };
5331 int p(object x) { if (x is int) { return 1; } return 0; }
5332 int q(object x) { if (x is int y) { return 1; } return 0; }
5333 }";
5334 assert_csharp_fixture_spells(
5335 src,
5336 &[
5337 (Csharp::IsExpression as u16, 2, "the bare `is` type tests"),
5338 (
5339 Csharp::IsPatternExpression as u16,
5340 2,
5341 "the designation-pattern twins",
5342 ),
5343 (Csharp::WhenClause as u16, 3, "`when` guards"),
5344 ],
5345 );
5346 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5347 assert_members_score(
5348 &space.spaces[0],
5349 &[
5350 ("IsEven", 0, 1),
5351 // Level with `gq` and `gc`: every guard spelling
5352 // scores one, which is what #1422 claims.
5353 ("g", 2, 3),
5354 ("gq", 2, 3),
5355 ("gc", 2, 3),
5356 // Level with `q`: the two spellings of one type
5357 // test agree outside a guard too.
5358 ("p", 1, 2),
5359 ("q", 1, 2),
5360 ],
5361 );
5362 });
5363 }
5364
5365 // The null-forgiving `b!` is transparent: it scores whatever the
5366 // operand it wraps scores (#1463).
5367 //
5368 // `postfix_unary_expression` was in neither
5369 // the former `csharp_bool_terminal_kinds!()` nor the wrapper peel,
5370 // so every spelling below scored **zero** conditions against a cyclomatic
5371 // decision of one, while the bare `b` control scored one. In a
5372 // codebase with nullable reference types enabled the suffix is
5373 // everywhere, so the gap is not an exotic corner.
5374 //
5375 // Both walker paths, per `.claude/rules/grammar-dispatch.md` §11:
5376 // `p*` go through the `if` predicate slot
5377 // (`csharp_count_condition`) and `c*` through the `&&` chain slot
5378 // (also `csharp_count_condition`, per chain operand), which reach
5379 // the peel independently. A fixture of only one
5380 // reads correct with the other path dead.
5381 //
5382 // The controls are load-bearing in both directions. `p` / `c` pin
5383 // what the wrapper must score, so a regression fails on the pair
5384 // rather than on an absolute number; and the cyclomatic column pins
5385 // that only `conditions` moved — the suffix is not a decision in any
5386 // language, so a fix that moved both together would be a different
5387 // bug.
5388 #[cfg(feature = "csharp")]
5389 #[test]
5390 fn csharp_null_forgiving_operand_scores_like_its_operand() {
5391 let src = "class A {
5392 int p(bool b) { if (b) { return 1; } return 0; }
5393 int pn(bool b) { if (b!) { return 1; } return 0; }
5394 int pp(bool b) { if ((b!)) { return 1; } return 0; }
5395 int pd(bool b) { if (b!!) { return 1; } return 0; }
5396 int pb(bool b) { if (!b!) { return 1; } return 0; }
5397 int c(bool b) { if (b && b) { return 1; } return 0; }
5398 int cn(bool b) { if (b! && b) { return 1; } return 0; }
5399 int cp(bool b) { if ((b!) && b) { return 1; } return 0; }
5400 }";
5401 assert_csharp_fixture_spells(
5402 src,
5403 &[
5404 // Seven, not five: `b!!` is two nested nodes, and each
5405 // chain member carries one. Trimming a `!` out of any
5406 // member fails here by name instead of turning that
5407 // member into a silent copy of its control.
5408 (
5409 Csharp::PostfixUnaryExpression as u16,
5410 7,
5411 "the null-forgiving suffixes",
5412 ),
5413 // The `if (…)` parens are anonymous tokens rather than
5414 // nodes in this grammar, so these two are `pp`'s and
5415 // `cp`'s explicit ones — the wrappers that make the peel
5416 // chain rather than peel once.
5417 (
5418 Csharp::ParenthesizedExpression as u16,
5419 2,
5420 "the explicit parenthesised spellings",
5421 ),
5422 (Csharp::PrefixUnaryExpression as u16, 1, "`pb`'s `!`"),
5423 ],
5424 );
5425 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5426 assert_members_score(
5427 &space.spaces[0],
5428 &[
5429 ("p", 1, 2),
5430 ("pn", 1, 2),
5431 ("pp", 1, 2),
5432 ("pd", 1, 2),
5433 ("pb", 1, 2),
5434 ("c", 2, 3),
5435 ("cn", 2, 3),
5436 ("cp", 2, 3),
5437 ],
5438 );
5439 });
5440 }
5441
5442 // The null-forgiving operand is read by position, but from the end
5443 // the grammar cannot move: a `postfix_unary_expression` *starts* at
5444 // its operand, so `child(0)` is the operand whatever `extra` sits
5445 // between it and the operator. The operator is identified by
5446 // membership rather than by index for the same reason.
5447 //
5448 // The other two wrappers name no field either — the C# grammar gives
5449 // `parenthesized_expression`, `prefix_unary_expression` and
5450 // `postfix_unary_expression` an empty `fields` map in
5451 // node-types.json — so `child(1)` was a comment whenever one was
5452 // written there, and `nc` / `rc` scored zero. They read their
5453 // operand as the first one that is not an extra since #1455, so all
5454 // three pairs agree with their controls.
5455 #[cfg(feature = "csharp")]
5456 #[test]
5457 fn csharp_null_forgiving_operand_survives_an_interposed_comment() {
5458 let src = "class A {
5459 int p(bool b) { if (b!) { return 1; } return 0; }
5460 int pc(bool b) { if (b /*c*/ !) { return 1; } return 0; }
5461 int n(bool b) { if (!b) { return 1; } return 0; }
5462 int nc(bool b) { if (! /*c*/ b) { return 1; } return 0; }
5463 int r(bool b) { if ((b)) { return 1; } return 0; }
5464 int rc(bool b) { if (( /*c*/ b)) { return 1; } return 0; }
5465 }";
5466 assert_csharp_fixture_spells(
5467 src,
5468 &[
5469 (
5470 Csharp::Comment as u16,
5471 3,
5472 "the interposed comments — without them every row is its own control",
5473 ),
5474 (Csharp::PostfixUnaryExpression as u16, 2, "`p` and `pc`"),
5475 (Csharp::PrefixUnaryExpression as u16, 2, "`n` and `nc`"),
5476 (
5477 Csharp::ParenthesizedExpression as u16,
5478 2,
5479 "`r` and `rc` — the `if` parens are anonymous tokens",
5480 ),
5481 ],
5482 );
5483 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5484 assert_members_score(
5485 &space.spaces[0],
5486 &[
5487 ("p", 1, 2),
5488 // The row #1463 fixes.
5489 ("pc", 1, 2),
5490 ("n", 1, 2),
5491 // #1455: `child(1)` of the prefix wrapper is the
5492 // comment.
5493 ("nc", 1, 2),
5494 ("r", 1, 2),
5495 // #1455 again, through the paren wrapper.
5496 ("rc", 1, 2),
5497 ],
5498 );
5499 });
5500 }
5501
5502 // The statement `switch` reaches the same `when_clause` kind through
5503 // a different parent — `switch_section` rather than
5504 // `switch_expression_arm` — which makes it an independent path in
5505 // the sense of `.claude/rules/grammar-dispatch.md` §11: the arms
5506 // themselves are counted by two different owners (a `Case` token
5507 // here, the `SwitchExpressionArm` node there), so a guard rule
5508 // written against one shape could be dead for the other and every
5509 // expression-form fixture would still read correct.
5510 #[cfg(feature = "csharp")]
5511 #[test]
5512 fn csharp_statement_switch_section_guard_counts() {
5513 let src = "class A {
5514 static bool IsEven(int x) { return true; }
5515 int guarded(int x) {
5516 switch (x) {
5517 case > 0 when IsEven(x): return 1;
5518 case > 0: return 2;
5519 default: return 3;
5520 }
5521 }
5522 int plain(int x) {
5523 switch (x) {
5524 case > 2: return 1;
5525 case > 0: return 2;
5526 default: return 3;
5527 }
5528 }
5529 }";
5530 assert_csharp_fixture_spells(
5531 src,
5532 &[
5533 (Csharp::WhenClause as u16, 1, "the `case … when` guard"),
5534 (Csharp::SwitchSection as u16, 6, "switch sections"),
5535 (
5536 Csharp::SwitchExpressionArm as u16,
5537 0,
5538 "expression arms (this fixture is the statement form)",
5539 ),
5540 ],
5541 );
5542 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5543 assert_members_score(
5544 &space.spaces[0],
5545 &[("IsEven", 0, 1), ("guarded", 3, 4), ("plain", 2, 3)],
5546 );
5547 });
5548 }
5549
5550 // The sibling #1422's issue body does not mention, measured to have
5551 // the identical defect: `catch (E e) when (…)` is a second guard
5552 // spelling on its own `catch_filter_clause` kind, and it scored
5553 // conditions 3 / 2 / 2 for the comparison / call spellings and the
5554 // unfiltered control while `cyclomatic()` sat at 2 for all three.
5555 //
5556 // ABC runs one above `cyclomatic() - 1` on every row, guarded and
5557 // unguarded alike, and that offset is not this fix's: C# ABC counts
5558 // the `Try` token as a condition (Fitzpatrick Rule 5, the same
5559 // family as `else`) where cyclomatic counts only the `catch`. The
5560 // guard moves both tiers by exactly one, which is what `none`
5561 // anchors — the offset is a constant here, not something a guard
5562 // introduces.
5563 //
5564 // `paren` carries the *double* parenthesis on purpose. The grammar
5565 // spells `catch_filter_clause`'s own parentheses as anonymous tokens
5566 // the way `if_statement` does, so `when (IsEven(x))` hands the slot a
5567 // bare `invocation_expression` with no parenthesis for the peel
5568 // (`csharp_wrapper_operand`) to take off; only a
5569 // second pair of parentheses produces a `parenthesized_expression`
5570 // there.
5571 #[cfg(feature = "csharp")]
5572 #[test]
5573 fn csharp_catch_filter_guard_scores_one_condition_however_spelled() {
5574 let src = "class A {
5575 static bool IsEven(int x) { return true; }
5576 int tok(int x) { try { return 1; } catch (System.Exception e) when (x % 2 == 0) { return 2; } }
5577 int cmp(int x) { try { return 1; } catch (System.Exception e) when (x > 0) { return 2; } }
5578 int call(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5579 int paren(int x) { try { return 1; } catch (System.Exception e) when ((IsEven(x))) { return 2; } }
5580 int none(int x) { try { return 1; } catch (System.Exception e) { return 2; } }
5581 }";
5582 assert_csharp_fixture_spells(
5583 src,
5584 &[
5585 (
5586 Csharp::CatchFilterClause as u16,
5587 4,
5588 "`catch … when` filters",
5589 ),
5590 (Csharp::CatchClause as u16, 5, "catch clauses"),
5591 (Csharp::WhenClause as u16, 0, "switch guards (none here)"),
5592 (
5593 Csharp::ParenthesizedExpression as u16,
5594 1,
5595 "`paren`'s inner parentheses",
5596 ),
5597 ],
5598 );
5599 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5600 assert_members_score(
5601 &space.spaces[0],
5602 &[
5603 ("IsEven", 0, 1),
5604 ("tok", 3, 3),
5605 ("cmp", 3, 3),
5606 ("call", 3, 3),
5607 ("paren", 3, 3),
5608 ("none", 2, 2),
5609 ],
5610 );
5611 });
5612 }
5613
5614 // A guarded discard is where the two rules meet. `_ when g =>` is
5615 // already exempt from the `default:` exclusion —
5616 // `csharp_switch_expression_arm_is_bare_discard` looks for a
5617 // `when_clause` for exactly this reason — so after #1422 it scores
5618 // twice: once as an arm that is no longer unconditional, once for
5619 // the guard that makes it conditional. `bare` is the control that
5620 // keeps the exclusion itself pinned: drop the guard and the arm goes
5621 // back to costing nothing.
5622 #[cfg(feature = "csharp")]
5623 #[test]
5624 fn csharp_guarded_discard_arm_scores_arm_and_guard() {
5625 let src = "class A {
5626 static bool IsEven(int x) { return true; }
5627 int guarded(int x) => x switch { > 0 => 1, _ when IsEven(x) => 2, _ => 3 };
5628 int bare(int x) => x switch { > 0 => 1, _ => 3 };
5629 }";
5630 assert_csharp_fixture_spells(
5631 src,
5632 &[
5633 (Csharp::WhenClause as u16, 1, "the guarded discard"),
5634 (Csharp::Discard as u16, 3, "discard patterns"),
5635 ],
5636 );
5637 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5638 assert_members_score(
5639 &space.spaces[0],
5640 &[("IsEven", 0, 1), ("guarded", 3, 4), ("bare", 1, 2)],
5641 );
5642 });
5643 }
5644
5645 // A comment inside the guard must not cost it its condition.
5646 // tree-sitter `extra`s are *named* nodes, so `comment` can be the
5647 // first named child of either guard clause — and a slot that read
5648 // only the first named child scored `when /*c*/ IsEven(x)` as 1
5649 // where `when IsEven(x)` scores 2, silently reinstating the very
5650 // spelling-dependence #1422 removes and breaking the §8 parity the
5651 // fix establishes. Found in review of #1422 before it shipped.
5652 //
5653 // This is the C# instance of the class #1181 fixed for the ternary,
5654 // where a comment between a token and its operand shifted every
5655 // positional read. Each commented member is asserted against the
5656 // same literal pair as its uncommented twin in the same table, so a
5657 // regression that moved only the commented spelling fails here; the
5658 // literals themselves are pinned by
5659 // `csharp_switch_arm_guard_scores_one_condition_however_spelled` and
5660 // its catch-filter sibling.
5661 #[cfg(feature = "csharp")]
5662 #[test]
5663 fn csharp_guard_keeps_its_condition_across_a_comment() {
5664 let src = "class A {
5665 static bool IsEven(int x) => true;
5666 int plain(int x) => x switch { > 0 when IsEven(x) => 1, _ => 0 };
5667 int cmt(int x) => x switch { > 0 when /*c*/ IsEven(x) => 1, _ => 0 };
5668 int cplain(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5669 int ccmt(int x) { try { return 1; } catch (System.Exception e) when (/*c*/ IsEven(x)) { return 2; } }
5670 }";
5671 assert_csharp_fixture_spells(
5672 src,
5673 &[
5674 (Csharp::Comment as u16, 2, "the in-guard comments"),
5675 (Csharp::WhenClause as u16, 2, "switch guards"),
5676 (Csharp::CatchFilterClause as u16, 2, "catch filters"),
5677 ],
5678 );
5679 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5680 assert_members_score(
5681 &space.spaces[0],
5682 &[
5683 ("IsEven", 0, 1),
5684 ("plain", 2, 3),
5685 ("cmt", 2, 3),
5686 ("cplain", 3, 3),
5687 ("ccmt", 3, 3),
5688 ],
5689 );
5690 });
5691 }
5692
5693 // Why the guard is a condition *slot* and not a flat +1: a compound
5694 // guard keeps its sub-structure. `when a > 1 && b < 2` scores the
5695 // two comparisons through the token arm and nothing extra from the
5696 // slot, because a `&&` chain scores itself
5697 // (`csharp_condition_scores_itself`) — so it reads one above `single`'s
5698 // lone comparison rather than collapsing to the same number, and
5699 // cyclomatic agrees because it counts the `&&`.
5700 #[cfg(feature = "csharp")]
5701 #[test]
5702 fn csharp_compound_guard_keeps_its_sub_structure() {
5703 let src = "class A {
5704 int compound(int a, int b) => a switch { > 0 when a > 1 && b < 2 => 1, _ => 0 };
5705 int single(int a, int b) => a switch { > 0 when a > 1 => 1, _ => 0 };
5706 }";
5707 assert_csharp_fixture_spells(
5708 src,
5709 &[
5710 (Csharp::WhenClause as u16, 2, "`when` guards"),
5711 (Csharp::AMPAMP as u16, 1, "`compound`'s `&&`"),
5712 (Csharp::LT as u16, 1, "`compound`'s `<`"),
5713 ],
5714 );
5715 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5716 assert_members_score(&space.spaces[0], &[("compound", 3, 4), ("single", 2, 3)]);
5717 });
5718 }
5719
5720 // #1275: tree-sitter-c-sharp spells `int?`, `where T : class?`, the
5721 // ternary and `a?.b` with one and the same bare `?` token, so the
5722 // unguarded `QMARK` arm scored the two type-syntax forms as
5723 // decisions.
5724 //
5725 // The fixture carries two `nullable_type` `?`, one
5726 // `type_parameter_constraint` `?`, and one real ternary over a real
5727 // `>` comparison, so every way of getting the gate wrong lands on
5728 // its own number: 5 pre-fix, 3 if only `NullableType` is denied, 4
5729 // if only `TypeParameterConstraint` is, 1 if the gate swallows the
5730 // genuine ternary, 0 if the fixture stops parsing. Asserting 0 on a
5731 // nullable-only body would have been vacuous — an unparsable file
5732 // scores 0 too.
5733 #[cfg(feature = "csharp")]
5734 #[test]
5735 fn csharp_nullable_type_syntax_is_not_a_condition() {
5736 check_metrics::<CsharpParser>(
5737 "class A {
5738 int M<T>(int? p, int a, int b) where T : class? {
5739 int? q = null;
5740 return a > b ? 1 : 2;
5741 }
5742 }",
5743 "foo.cs",
5744 |metric| {
5745 assert_eq!(metric.abc.conditions_sum(), 2);
5746 },
5747 );
5748 }
5749
5750 // The polarity pin for #1275's C# half. `csharp_count_token_condition`
5751 // denies the two type-syntax parents rather than allowing the two
5752 // decision-bearing ones, so that `a?.b` and `a?[0]` keep counting by
5753 // construction: both spell their operator as the same bare `?` under
5754 // `conditional_access_expression`, and C# cyclomatic counts that node
5755 // (`safe_navigation_chain_parity` pins C# at one decision per
5756 // operator alongside every other safe-navigation language).
5757 //
5758 // This test is what stops a later "make all three languages
5759 // consistent" pass from flipping C# to a `ConditionalExpression`
5760 // allowlist: that would silently drop both `?` counts here while
5761 // every other ABC test still passed. The third condition is the
5762 // `??`, which #1459 added to close the gap this comment used to
5763 // record as out of scope for #1275 — so a flipped `QMARK` polarity
5764 // now reads 1 rather than 0, and the discrimination is unchanged.
5765 #[cfg(feature = "csharp")]
5766 #[test]
5767 fn csharp_conditional_access_still_counts_as_a_condition() {
5768 let src = "class A {
5769 object M(string s, int[] xs) {
5770 return s?.Length ?? xs?[0];
5771 }
5772 }";
5773 // The total is now fed by two token arms rather than one, so it
5774 // carries the per-source claim only alongside these counts: 3 is
5775 // two bare `?` plus one `??`, and without them a `QMARK` arm
5776 // that had stopped counting `a?[0]` while `??` gained a second
5777 // count elsewhere would still read 3.
5778 assert_csharp_fixture_spells(
5779 src,
5780 &[
5781 (Csharp::QMARK as u16, 2, "the `?.` and `?[` operators"),
5782 (Csharp::QMARKQMARK as u16, 1, "the `??`"),
5783 ],
5784 );
5785 check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
5786 assert_eq!(metric.abc.conditions_sum(), 3);
5787 });
5788 }
5789
5790 // The `QMARKQMARK` arm away from any guard, where nothing else can
5791 // supply the count (#1459). `coalesce`'s body holds no comparison,
5792 // no call and no control-flow keyword, so its one condition is the
5793 // `??` and nothing else; it read 0 before the arm while C#
5794 // cyclomatic scored the decision, which is the direction ABC is not
5795 // allowed to sit on.
5796 //
5797 // `plain` is the control that keeps this from being an assertion
5798 // about `return` or about the method shape: same body without the
5799 // operator, 0 conditions and 1 decision.
5800 #[cfg(feature = "csharp")]
5801 #[test]
5802 fn csharp_null_coalescing_is_a_condition() {
5803 let src = "class A {
5804 int coalesce(int? x) { return x ?? 0; }
5805 int plain(int x) { return x; }
5806 }";
5807 assert_csharp_fixture_spells(src, &[(Csharp::QMARKQMARK as u16, 1, "the `??`")]);
5808 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5809 assert_members_score(&space.spaces[0], &[("coalesce", 1, 2), ("plain", 0, 1)]);
5810 });
5811 }
5812
5813 // The other direction of #1275's C# gate, isolated: narrowing
5814 // `QMARK` must not swallow a real ternary. The fixture carries no
5815 // comparison operator at all, so the ternary is the only thing that
5816 // can put the count above the walker's own contribution — in the
5817 // test above, a surviving `>` would have masked an over-suppressed
5818 // `?` at 1 instead of 0 (grammar-dispatch §11). A nullable
5819 // parameter keeps the denied production live in the same parse, so
5820 // one fixture proves both directions: 3 pre-fix, 2 after, 1 if the
5821 // allowlist/denylist is aimed at `ConditionalExpression`.
5822 //
5823 // The 2 is the `?` token plus the ternary's condition slot, which
5824 // `csharp_walk_for_conditions` counts as a Fitzpatrick unary
5825 // condition. That is why this is not written as an
5826 // `assert_deepest_conditions_match_cyclomatic` parity pin the way
5827 // the Java `<` / `>` sibling is: C# cyclomatic scores this method 1
5828 // decision, and the divergence is the pre-existing unary-condition
5829 // rule, not the `?` gate.
5830 #[cfg(feature = "csharp")]
5831 #[test]
5832 fn csharp_ternary_still_counts_alongside_nullable_types() {
5833 check_metrics::<CsharpParser>(
5834 "class A {
5835 int M(int? n, bool c) {
5836 return c ? 1 : 2;
5837 }
5838 }",
5839 "foo.cs",
5840 |metric| {
5841 assert_eq!(metric.abc.conditions_sum(), 2);
5842 },
5843 );
5844 }
5845
5846 #[cfg(feature = "csharp")]
5847 #[test]
5848 fn csharp_aliased_invocation_expression_branches() {
5849 // Regression for issue #94 (lesson #2): the C# grammar emits three
5850 // aliased `kind_id`s for `invocation_expression`. Code that matches
5851 // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
5852 // branches whenever the AST emits an aliased variant. The three
5853 // method calls live in `M`, so the per-method maximum (visible at
5854 // the unit-space aggregate as `branches_max`) must be 3.
5855 check_metrics::<CsharpParser>(
5856 "class A {
5857 void M() {
5858 System.Console.WriteLine(1);
5859 System.Console.WriteLine(2);
5860 System.Console.WriteLine(3);
5861 }
5862 }",
5863 "foo.cs",
5864 |metric| {
5865 assert_eq!(metric.abc.branches_max(), 3);
5866 assert_eq!(metric.abc.conditions_max(), 0);
5867 },
5868 );
5869 }
5870
5871 #[cfg(feature = "php")]
5872 #[test]
5873 fn php_zero_abc() {
5874 check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
5875 assert_eq!(metric.abc.assignments_sum(), 0);
5876 assert_eq!(metric.abc.branches_sum(), 0);
5877 assert_eq!(metric.abc.conditions_sum(), 0);
5878 insta::assert_json_snapshot!(metric.abc);
5879 });
5880 }
5881
5882 #[cfg(feature = "php")]
5883 #[test]
5884 fn php_simple_assignment() {
5885 check_metrics::<PhpParser>(
5886 "<?php
5887function f(): void {
5888 $a = 1;
5889 $b = 2;
5890}",
5891 "foo.php",
5892 |metric| insta::assert_json_snapshot!(metric.abc),
5893 );
5894 }
5895
5896 #[cfg(feature = "php")]
5897 #[test]
5898 fn php_augmented_assignment() {
5899 check_metrics::<PhpParser>(
5900 "<?php
5901function f(int $x): int {
5902 $a = 0;
5903 $a += $x;
5904 $a -= 1;
5905 $a *= 2;
5906 return $a;
5907}",
5908 "foo.php",
5909 |metric| insta::assert_json_snapshot!(metric.abc),
5910 );
5911 }
5912
5913 #[cfg(feature = "php")]
5914 #[test]
5915 fn php_const_excluded() {
5916 // Constant declarations and enum cases are NOT counted as
5917 // assignments — they declare immutable values.
5918 check_metrics::<PhpParser>(
5919 "<?php
5920class A {
5921 const PI = 3.14;
5922 const E = 2.71;
5923}
5924enum Color {
5925 case Red;
5926 case Green;
5927}",
5928 "foo.php",
5929 |metric| insta::assert_json_snapshot!(metric.abc),
5930 );
5931 }
5932
5933 #[cfg(feature = "php")]
5934 #[test]
5935 fn php_function_call() {
5936 check_metrics::<PhpParser>(
5937 "<?php
5938function f(): void {
5939 foo();
5940 bar(1, 2);
5941}",
5942 "foo.php",
5943 |metric| insta::assert_json_snapshot!(metric.abc),
5944 );
5945 }
5946
5947 #[cfg(feature = "php")]
5948 #[test]
5949 fn php_method_call() {
5950 check_metrics::<PhpParser>(
5951 "<?php
5952function f($obj): void {
5953 $obj->m1();
5954 $obj->m2(1);
5955}",
5956 "foo.php",
5957 |metric| insta::assert_json_snapshot!(metric.abc),
5958 );
5959 }
5960
5961 #[cfg(feature = "php")]
5962 #[test]
5963 fn php_static_call() {
5964 check_metrics::<PhpParser>(
5965 "<?php
5966function f(): void {
5967 Foo::bar();
5968 Foo::baz(1);
5969}",
5970 "foo.php",
5971 |metric| insta::assert_json_snapshot!(metric.abc),
5972 );
5973 }
5974
5975 #[cfg(feature = "php")]
5976 #[test]
5977 fn php_nullsafe_call() {
5978 check_metrics::<PhpParser>(
5979 "<?php
5980function f($obj): void {
5981 $obj?->m1();
5982 $obj?->m2(1);
5983}",
5984 "foo.php",
5985 |metric| insta::assert_json_snapshot!(metric.abc),
5986 );
5987 }
5988
5989 #[cfg(feature = "php")]
5990 #[test]
5991 fn php_object_creation() {
5992 check_metrics::<PhpParser>(
5993 "<?php
5994function f(): void {
5995 new Foo();
5996 new Bar(1);
5997}",
5998 "foo.php",
5999 |metric| insta::assert_json_snapshot!(metric.abc),
6000 );
6001 }
6002
6003 #[cfg(feature = "php")]
6004 #[test]
6005 fn php_comparison_eq() {
6006 check_metrics::<PhpParser>(
6007 "<?php
6008function f(int $a, int $b): bool {
6009 return $a == $b || $a != $b;
6010}",
6011 "foo.php",
6012 |metric| insta::assert_json_snapshot!(metric.abc),
6013 );
6014 }
6015
6016 #[cfg(feature = "php")]
6017 #[test]
6018 fn php_comparison_strict() {
6019 check_metrics::<PhpParser>(
6020 "<?php
6021function f(int $a, int $b): bool {
6022 return $a === $b || $a !== $b;
6023}",
6024 "foo.php",
6025 |metric| insta::assert_json_snapshot!(metric.abc),
6026 );
6027 }
6028
6029 #[cfg(feature = "php")]
6030 #[test]
6031 fn php_spaceship() {
6032 check_metrics::<PhpParser>(
6033 "<?php
6034function f(int $a, int $b): int {
6035 return $a <=> $b;
6036}",
6037 "foo.php",
6038 |metric| insta::assert_json_snapshot!(metric.abc),
6039 );
6040 }
6041
6042 #[cfg(feature = "php")]
6043 #[test]
6044 fn php_instanceof() {
6045 check_metrics::<PhpParser>(
6046 "<?php
6047function f($x): bool {
6048 return $x instanceof Foo;
6049}",
6050 "foo.php",
6051 |metric| insta::assert_json_snapshot!(metric.abc),
6052 );
6053 }
6054
6055 #[cfg(feature = "php")]
6056 #[test]
6057 fn php_complex_function() {
6058 // One snippet exercising A, B, C buckets together.
6059 check_metrics::<PhpParser>(
6060 "<?php
6061function f(int $a, int $b): int {
6062 $sum = $a + $b;
6063 $prod = $a * $b;
6064 if ($sum > 0 && $prod === 0) {
6065 return foo($sum);
6066 }
6067 return bar()->double();
6068}",
6069 "foo.php",
6070 |metric| insta::assert_json_snapshot!(metric.abc),
6071 );
6072 }
6073
6074 #[cfg(feature = "php")]
6075 #[test]
6076 fn php_if_boolean_literal_condition() {
6077 check_metrics::<PhpParser>(
6078 "<?php\n\
6079 function f() {\n\
6080 \x20 if (true) {} // +1c\n\
6081 \x20 if (!false) {} // +1c\n\
6082 \x20 while (true) {} // +1c\n\
6083 \x20 do {} while (false); // +1c\n\
6084 }\n",
6085 "foo.php",
6086 |metric| {
6087 assert_eq!(metric.abc.conditions_sum(), 4);
6088 insta::assert_json_snapshot!(metric.abc);
6089 },
6090 );
6091 }
6092
6093 #[cfg(feature = "php")]
6094 #[test]
6095 fn php_methods_arguments_with_conditions() {
6096 check_metrics::<PhpParser>(
6097 "<?php\n\
6098 function f($a, $b) {\n\
6099 \x20 m($a, $b); // +1b\n\
6100 \x20 m(!$a, !$b); // +1b +2c\n\
6101 }\n",
6102 "foo.php",
6103 |metric| {
6104 assert_eq!(metric.abc.branches_sum(), 2);
6105 assert_eq!(metric.abc.conditions_sum(), 2);
6106 insta::assert_json_snapshot!(metric.abc);
6107 },
6108 );
6109 }
6110
6111 #[cfg(feature = "php")]
6112 #[test]
6113 fn php_return_with_conditions() {
6114 check_metrics::<PhpParser>(
6115 "<?php\n\
6116 function m1($z) { return !($z >= 0); }\n\
6117 function m2($x) { return (((!$x))); }\n\
6118 function m3($x, $y) { return $x && $y; }\n",
6119 "foo.php",
6120 |metric| {
6121 // m1: `>=` (1). m2: walker unwraps to $x (1).
6122 // m3: `&&` walker counts both (2). Sum: 4.
6123 assert_eq!(metric.abc.conditions_sum(), 4);
6124 insta::assert_json_snapshot!(metric.abc);
6125 },
6126 );
6127 }
6128
6129 #[cfg(feature = "php")]
6130 #[test]
6131 fn php_name2_hidden_rule_drift_marker() {
6132 // Drift marker (findings.md round-2 #3): `Php::Name2` maps
6133 // to the hidden grammar rule `_name`. At the pinned
6134 // tree-sitter-php version it is never emitted as a concrete
6135 // node — the visible `Name` (= 1) carries every name.
6136 // The former `php_bool_terminal_kinds!()` listed `Name2`
6137 // defensively (lesson 34); if a future grammar bump promotes `_name`
6138 // to a visible rule, this assertion fails loudly.
6139 let src = "<?php\nfunction f($x) { if ($x) { foo($x); } }\n";
6140 let parser = PhpParser::new(
6141 src.as_bytes().to_vec(),
6142 &std::path::PathBuf::from("foo.php"),
6143 None,
6144 );
6145 assert!(!ast_has_kind_id(&parser, Php::Name2 as u16));
6146 }
6147
6148 #[cfg(feature = "php")]
6149 #[test]
6150 fn php_scoped_property_access_condition_counts() {
6151 // Regression for findings.md round-2 #1 (PHP):
6152 // `if (Config::$enabled) {}` parses with
6153 // `scoped_property_access_expression` as the condition
6154 // node (kind_id 333 at the pinned grammar version — the
6155 // `*2` alias). Pre-fix, neither `ScopedPropertyAccessExpression`
6156 // nor its alias was in the former `php_bool_terminal_kinds!()`.
6157 // The walker reached the access node, found it non-terminal,
6158 // and broke. Mirrors C#'s `MemberAccessExpression` rule
6159 // (lesson 19, #372).
6160 check_metrics::<PhpParser>(
6161 "<?php\n\
6162 class Config { public static $enabled = true; }\n\
6163 function f() { if (Config::$enabled) { } }\n",
6164 "foo.php",
6165 |metric| {
6166 assert_eq!(metric.abc.conditions_sum(), 1);
6167 insta::assert_json_snapshot!(metric.abc);
6168 },
6169 );
6170 }
6171
6172 #[cfg(feature = "php")]
6173 #[test]
6174 fn php_named_argument_unary_conditional_counts() {
6175 // Regression for the code-review finding: PHP 8 named-argument
6176 // syntax `m(name: !$a)` parses as `argument(name, ':',
6177 // unary_op_expression)`. Pre-fix, the count walker took
6178 // `argument.child(0)` (the name) and missed the value at the
6179 // last child. Now it picks the last named child as the value.
6180 check_metrics::<PhpParser>(
6181 "<?php\nfunction f($a) { m(name: !$a); }\n",
6182 "foo.php",
6183 |metric| {
6184 // 1 call (branch) + 1 unary-conditional named argument.
6185 assert_eq!(metric.abc.branches_sum(), 1);
6186 assert_eq!(metric.abc.conditions_sum(), 1);
6187 insta::assert_json_snapshot!(metric.abc);
6188 },
6189 );
6190 }
6191
6192 #[cfg(feature = "php")]
6193 #[test]
6194 fn php_low_precedence_keyword_logical_ops_trigger_walker() {
6195 // Regression: pre-fix, `$a or $b` reported 0 conditions
6196 // because the dispatcher only handled `AMPAMP|PIPEPIPE`,
6197 // skipping the PHP-specific `and` / `or` keyword forms even
6198 // though they parse under the same `binary_expression` shape.
6199 // (`xor` is no chain: it is a value since #1536.)
6200 check_metrics::<PhpParser>(
6201 "<?php\n\
6202 function f($a, $b) {\n\
6203 \x20 return $a or $b;\n\
6204 }\n",
6205 "foo.php",
6206 |metric| {
6207 assert_eq!(metric.abc.conditions_sum(), 2);
6208 insta::assert_json_snapshot!(metric.abc);
6209 },
6210 );
6211 }
6212
6213 #[cfg(feature = "php")]
6214 #[test]
6215 fn php_if_multiple_conditions() {
6216 check_metrics::<PhpParser>(
6217 "<?php\n\
6218 function f($a, $b, $c, $d) {\n\
6219 \x20 if ($a || $b || $c || $d) {} // +4c\n\
6220 \x20 if ($a && $b && $c) {} // +3c\n\
6221 \x20 if (!$a && !$b) {} // +2c\n\
6222 }\n",
6223 "foo.php",
6224 |metric| {
6225 assert_eq!(metric.abc.conditions_sum(), 9);
6226 insta::assert_json_snapshot!(metric.abc);
6227 },
6228 );
6229 }
6230
6231 #[cfg(feature = "php")]
6232 #[test]
6233 fn php_while_and_do_while_conditions() {
6234 check_metrics::<PhpParser>(
6235 "<?php\n\
6236 function f($a, $b) {\n\
6237 \x20 while ($a || $b) {} // +2c\n\
6238 \x20 do {} while ($a && !$b); // +2c\n\
6239 }\n",
6240 "foo.php",
6241 |metric| {
6242 assert_eq!(metric.abc.conditions_sum(), 4);
6243 insta::assert_json_snapshot!(metric.abc);
6244 },
6245 );
6246 }
6247
6248 #[cfg(feature = "php")]
6249 #[test]
6250 fn php_short_circuit_with_boolean_literal_operand() {
6251 check_metrics::<PhpParser>(
6252 "<?php\nfunction f($a) { return $a && true; }\n",
6253 "foo.php",
6254 |metric| {
6255 assert_eq!(metric.abc.conditions_sum(), 2);
6256 insta::assert_json_snapshot!(metric.abc);
6257 },
6258 );
6259 }
6260
6261 // Issue #1102, PHP half. See
6262 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
6263 // rule. PHP's ABC dispatcher has no `?`-token arm — the grammar
6264 // does emit the token, but the `conditional_expression` node is
6265 // what carries the tally's +1 — so the arm keeps that increment and
6266 // adds the operand slots.
6267 #[cfg(feature = "php")]
6268 #[test]
6269 fn php_ternary_operand_slots_count_as_unary_conditions() {
6270 // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
6271 check_metrics::<PhpParser>(
6272 "<?php\nfunction f() { $x = $a ? !$b : !$c; }\n",
6273 "foo.php",
6274 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
6275 );
6276 // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
6277 // the fix.
6278 check_metrics::<PhpParser>(
6279 "<?php\nfunction f() { $x = ($a > 0) ? $b : -$b; }\n",
6280 "foo.php",
6281 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
6282 );
6283 // Nested (PHP 8 requires the inner ternary parenthesised): two
6284 // ternary nodes plus the two bare-variable conditions = 4.
6285 check_metrics::<PhpParser>(
6286 "<?php\nfunction f() { $x = $a ? ($b ? $c : $d) : $e; }\n",
6287 "foo.php",
6288 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
6289 );
6290 // A negated condition is the only input reaching the walker's
6291 // `else` fallback — see the C++ sibling for why. ternary (1) +
6292 // `!$a` (1) = 2.
6293 check_metrics::<PhpParser>(
6294 "<?php\nfunction f() { $x = !$a ? $b : $c; }\n",
6295 "foo.php",
6296 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
6297 );
6298 }
6299
6300 // PHP's short ternary `$a ?: $b` elides the consequence, which the
6301 // grammar names `body` (not `consequence`) and marks optional. The
6302 // alternative lands at child(3), so addressing the slot by field
6303 // name rather than a fixed child(4) is what keeps `!$b` counted.
6304 #[cfg(feature = "php")]
6305 #[test]
6306 fn php_elided_ternary_body_still_walks_the_alternative() {
6307 // ternary (1) + condition `$a` (1) + `!$b` (1) = 3.
6308 check_metrics::<PhpParser>(
6309 "<?php\nfunction f() { $x = $a ?: !$b; }\n",
6310 "foo.php",
6311 |metric| assert_eq!(metric.abc.conditions_sum(), 3),
6312 );
6313 }
6314
6315 // Issue #1276, PHP half. The `for` header's condition slot was the
6316 // one condition slot `PhpCode::compute` never dispatched, so a
6317 // bare / negated / parenthesised loop condition scored zero while
6318 // the identical predicate in an `if` header scored one. Each
6319 // fixture below is a shape only the new arm can classify — a
6320 // comparison-shaped condition proves nothing here, because the `<`
6321 // token arm counts it either way (grammar-dispatch §11).
6322 #[cfg(feature = "php")]
6323 #[test]
6324 fn php_for_condition_slot_counts_unary_conditions() {
6325 // Bare variable: the whole condition, no operator token.
6326 check_metrics::<PhpParser>(
6327 "<?php\nfunction f($a) { for (; $a; ) {} }\n",
6328 "foo.php",
6329 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6330 );
6331 // Negation: the slot peels the `!` (`php_wrapper_operand`) and
6332 // pays for the operand beneath it.
6333 check_metrics::<PhpParser>(
6334 "<?php\nfunction f($a) { for (; !$a; ) {} }\n",
6335 "foo.php",
6336 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6337 );
6338 // Parentheses: counted because the slot peels them
6339 // (`php_wrapper_operand`), the path #1276 found dead.
6340 check_metrics::<PhpParser>(
6341 "<?php\nfunction f($a) { for (; ($a); ) {} }\n",
6342 "foo.php",
6343 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6344 );
6345 // No-double-count pin: the `<` token arm already counted this
6346 // shape before the fix, and the walker must not add a second.
6347 // Two assignments (`$i = 0`, `$i++`) confirm the header parsed
6348 // as the three-clause form rather than degenerating.
6349 check_metrics::<PhpParser>(
6350 "<?php\nfunction f($n) { for ($i = 0; $i < $n; $i++) {} }\n",
6351 "foo.php",
6352 |metric| {
6353 assert_eq!(metric.abc.conditions_sum(), 1);
6354 assert_eq!(metric.abc.assignments_sum(), 2);
6355 },
6356 );
6357 // Empty condition: no `condition` field, no decision, zero.
6358 check_metrics::<PhpParser>(
6359 "<?php\nfunction f() { for (;;) { break; } }\n",
6360 "foo.php",
6361 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
6362 );
6363 }
6364
6365 // --- Kotlin ABC tests -------------------------------------------------
6366
6367 #[cfg(feature = "kotlin")]
6368 #[test]
6369 fn kotlin_empty_class() {
6370 check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
6371 assert_eq!(metric.abc.assignments_sum(), 0);
6372 assert_eq!(metric.abc.branches_sum(), 0);
6373 assert_eq!(metric.abc.conditions_sum(), 0);
6374 insta::assert_json_snapshot!(metric.abc);
6375 });
6376 }
6377
6378 #[cfg(feature = "kotlin")]
6379 #[test]
6380 fn kotlin_val_declarations_are_not_assignments() {
6381 // `val` introduces an immutable binding — the `=` initialising it
6382 // is not an assignment in the ABC sense.
6383 check_metrics::<KotlinParser>(
6384 "class C {
6385 val a: Int = 1
6386 val b: Int = 2
6387 val c: Int = 3
6388 }",
6389 "foo.kt",
6390 |metric| {
6391 assert_eq!(metric.abc.assignments_sum(), 0);
6392 assert_eq!(metric.abc.branches_sum(), 0);
6393 insta::assert_json_snapshot!(metric.abc);
6394 },
6395 );
6396 }
6397
6398 #[cfg(feature = "kotlin")]
6399 #[test]
6400 fn kotlin_var_declarations_count_assignment() {
6401 // `var` initialisers count as assignments (mutable binding).
6402 check_metrics::<KotlinParser>(
6403 "class C {
6404 var a: Int = 1
6405 var b: Int = 2
6406 }",
6407 "foo.kt",
6408 |metric| {
6409 assert_eq!(metric.abc.assignments_sum(), 2);
6410 insta::assert_json_snapshot!(metric.abc);
6411 },
6412 );
6413 }
6414
6415 #[cfg(feature = "kotlin")]
6416 #[test]
6417 fn kotlin_val_then_assignments_count() {
6418 // Regression for #455: a `val` initialiser must not suppress the
6419 // standalone `=` assignments that follow it. tree-sitter-kotlin
6420 // emits no `SEMI` token (even for explicit semicolons), so the
6421 // pre-#455 `SEMI`-cleared declaration stack never cleared and the
6422 // immutable-`val` sentinel leaked, reporting A=0 here.
6423 check_metrics::<KotlinParser>(
6424 "fun f() {
6425 val cfg = 0
6426 a = 1
6427 b = 2
6428 }",
6429 "foo.kt",
6430 |metric| {
6431 // val initialiser suppressed; `a = 1` and `b = 2` count.
6432 assert_eq!(metric.abc.assignments_sum(), 2);
6433 insta::assert_json_snapshot!(metric.abc);
6434 },
6435 );
6436 }
6437
6438 #[cfg(feature = "kotlin")]
6439 #[test]
6440 fn kotlin_var_then_assignments_count() {
6441 // Companion to the #455 regression: a `var` declaration leaves a
6442 // mutable-binding sentinel that *permits* the `=` — this path
6443 // accidentally masked the leak (its `Var` sentinel never suppressed
6444 // anything), so it must keep counting both the initialiser and the
6445 // following standalone assignments.
6446 check_metrics::<KotlinParser>(
6447 "fun f() {
6448 var cfg = 0
6449 a = 1
6450 b = 2
6451 }",
6452 "foo.kt",
6453 |metric| {
6454 // var initialiser (+1) plus `a = 1` and `b = 2` (+2).
6455 assert_eq!(metric.abc.assignments_sum(), 3);
6456 insta::assert_json_snapshot!(metric.abc);
6457 },
6458 );
6459 }
6460
6461 #[cfg(feature = "kotlin")]
6462 #[test]
6463 fn kotlin_augmented_assignments_count() {
6464 // Augmented operators (+=, -=, etc.) and ++/-- always count.
6465 check_metrics::<KotlinParser>(
6466 "fun m() {
6467 var x = 0
6468 x += 1
6469 x -= 2
6470 x *= 3
6471 x++
6472 --x
6473 }",
6474 "foo.kt",
6475 |metric| {
6476 // var declaration (var x = 0): +1
6477 // x += 1, x -= 2, x *= 3, x++, --x: +5
6478 assert_eq!(metric.abc.assignments_sum(), 6);
6479 insta::assert_json_snapshot!(metric.abc);
6480 },
6481 );
6482 }
6483
6484 #[cfg(feature = "kotlin")]
6485 #[test]
6486 fn kotlin_branches_call_expression() {
6487 check_metrics::<KotlinParser>(
6488 "fun m() {
6489 println(\"a\")
6490 println(\"b\")
6491 println(\"c\")
6492 }",
6493 "foo.kt",
6494 |metric| {
6495 assert_eq!(metric.abc.branches_sum(), 3);
6496 insta::assert_json_snapshot!(metric.abc);
6497 },
6498 );
6499 }
6500
6501 #[cfg(feature = "kotlin")]
6502 #[test]
6503 fn kotlin_object_construction_branch() {
6504 // Kotlin's object construction is just `Foo()` — a `CallExpression`.
6505 check_metrics::<KotlinParser>(
6506 "class P(val x: Int)
6507 fun m(): P = P(1)",
6508 "foo.kt",
6509 |metric| {
6510 assert_eq!(metric.abc.branches_sum(), 1);
6511 insta::assert_json_snapshot!(metric.abc);
6512 },
6513 );
6514 }
6515
6516 #[cfg(feature = "kotlin")]
6517 #[test]
6518 fn kotlin_comparisons_count_conditions() {
6519 check_metrics::<KotlinParser>(
6520 "fun m(a: Int, b: Int): Boolean {
6521 val r1 = a < b
6522 val r2 = a > b
6523 val r3 = a <= b
6524 val r4 = a >= b
6525 val r5 = a == b
6526 val r6 = a != b
6527 return r1 || r2 || r3 || r4 || r5 || r6
6528 }",
6529 "foo.kt",
6530 |metric| {
6531 // Six comparison operators in the `val` initialisers
6532 // (<, >, <=, >=, ==, !=) → 6, plus the six bare-identifier
6533 // operands of the `r1 || … || r6` return chain, each a
6534 // Fitzpatrick Rule 9 unary condition (issue #557) → 6.
6535 // Total 12. Before the Kotlin walker was wired the chain
6536 // operands were silently dropped and this read 6.
6537 assert_eq!(metric.abc.conditions_sum(), 12);
6538 insta::assert_json_snapshot!(metric.abc);
6539 },
6540 );
6541 }
6542
6543 #[cfg(feature = "kotlin")]
6544 #[test]
6545 fn kotlin_identity_equality_conditions() {
6546 // `===` / `!==` are referential equality in Kotlin; they count too.
6547 check_metrics::<KotlinParser>(
6548 "fun m(a: Any, b: Any): Boolean {
6549 return a === b || a !== b
6550 }",
6551 "foo.kt",
6552 |metric| {
6553 assert_eq!(metric.abc.conditions_sum(), 2);
6554 insta::assert_json_snapshot!(metric.abc);
6555 },
6556 );
6557 }
6558
6559 #[cfg(feature = "kotlin")]
6560 #[test]
6561 fn kotlin_else_branch_counts() {
6562 check_metrics::<KotlinParser>(
6563 "fun m(x: Int): Int {
6564 return if (x > 0) 1 else -1
6565 }",
6566 "foo.kt",
6567 |metric| {
6568 // condition: > (1) + else (1) = 2
6569 assert_eq!(metric.abc.conditions_sum(), 2);
6570 insta::assert_json_snapshot!(metric.abc);
6571 },
6572 );
6573 }
6574
6575 #[cfg(feature = "kotlin")]
6576 #[test]
6577 fn kotlin_when_entries_count() {
6578 check_metrics::<KotlinParser>(
6579 "fun m(x: Int): Int {
6580 return when (x) {
6581 1 -> 10
6582 2 -> 20
6583 else -> 0
6584 }
6585 }",
6586 "foo.kt",
6587 |metric| {
6588 // Non-`else` WhenEntry arms count; the `else ->` fallback
6589 // arm does not (issue #456). Two case arms + zero for the
6590 // `else` arm = 2.
6591 assert_eq!(metric.abc.conditions_sum(), 2);
6592 insta::assert_json_snapshot!(metric.abc);
6593 },
6594 );
6595 }
6596
6597 // Pins the `else ->` exclusion directly: a `when` whose only fallback
6598 // is `else ->` must not count that arm. Revert-verified — gating the
6599 // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this
6600 // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate.
6601 #[cfg(feature = "kotlin")]
6602 #[test]
6603 fn kotlin_when_else_not_a_condition() {
6604 check_metrics::<KotlinParser>(
6605 "fun m(x: Int): Int {
6606 return when (x) { 1 -> 10; 2 -> 20; else -> 0 }
6607 }",
6608 "foo.kt",
6609 |metric| {
6610 // case `1 ->` (+1) + case `2 ->` (+1) + `else ->` (+0) = 2.
6611 assert_eq!(metric.abc.conditions_sum(), 2);
6612 },
6613 );
6614 }
6615
6616 // The Kotlin binding of the two assertions every #1421 fixture needs,
6617 // coupled on purpose. A per-member score table is worth only as much
6618 // as the anchor proving the fixture still spells what it claims to
6619 // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
6620 // production line"), and the `when_subject` count is the anchor no
6621 // score can stand in for: a `when` that grew a subject scores its
6622 // entries the pre-#1421 way with every other row still satisfied.
6623 // Each fixture is one class, so the members are `spaces[0]`'s.
6624 #[cfg(feature = "kotlin")]
6625 #[track_caller]
6626 fn assert_kotlin_class_members(
6627 src: &str,
6628 kinds: &[(u16, usize, &str)],
6629 expected: &[(&str, u64, u64)],
6630 ) {
6631 assert_fixture_spells::<KotlinParser>(src, "foo.kt", kinds);
6632 check_func_space::<KotlinParser, _>(src, "foo.kt", |space| {
6633 assert_members_score(&space.spaces[0], expected);
6634 });
6635 }
6636
6637 // #1421's headline. A subject-less `when` arm's condition is an
6638 // ordinary boolean expression, so its comparison operator is already
6639 // an ABC condition through the token arms; the `WhenEntry` arm added
6640 // a second one on top, and `two` reported 4 against a decision count
6641 // of 2.
6642 //
6643 // `bare` is the control that isolates the defect rather than merely
6644 // observing it: same arm count, same entry shape, no comparison
6645 // operator anywhere — so it scored the right 2 before the fix and
6646 // after it. A fix that changed both members alike would be attacking
6647 // the entry count, which was never wrong.
6648 //
6649 // `ge` and `eq` exist because the issue named `<` and `>` only. All
6650 // six comparison spellings share the defect: `>=` and `==` reach
6651 // `stats.conditions` through the `LTEQ | GTEQ | EQEQ | …` arm rather
6652 // than the parent-gated `LT | GT` arm, and both scored 2 against a
6653 // decision count of 1.
6654 //
6655 // `iff` is the member that legitimately sits *above* its decision
6656 // count, and it is here so the table cannot be read as "§8 parity
6657 // everywhere". ABC counts the `else` (Fitzpatrick Rule 5) and
6658 // cyclomatic does not, so 2 conditions against 1 decision is correct
6659 // and unchanged by this fix.
6660 //
6661 // The `when_subject` anchor is the load-bearing one: the whole test
6662 // turns on these `when`s having no subject, and nothing in the
6663 // measured numbers would notice a fixture that grew one — a
6664 // subject-ful `when (x) { … }` scores its entries the old way and
6665 // `two` would read 4 again with every other row still satisfied.
6666 #[cfg(feature = "kotlin")]
6667 #[test]
6668 fn kotlin_subjectless_when_arm_counts_its_condition_once() {
6669 let src = "class K {
6670 fun f(): Boolean = true
6671 fun two(x: Int): Int = when { x > 5 -> 1; x < 0 -> 2; else -> 0 }
6672 fun bare(x: Boolean, y: Boolean): Int = when { x -> 1; y -> 2; else -> 0 }
6673 fun ge(x: Int): Int = when { x >= 5 -> 1; else -> 0 }
6674 fun eq(x: Int): Int = when { x == 5 -> 1; else -> 0 }
6675 fun andd(a: Int, b: Int): Int = when { a > 1 && b < 2 -> 1; else -> 0 }
6676 fun call(): Int = when { f() -> 1; else -> 0 }
6677 fun iff(x: Int): Int = if (x > 5) 1 else 0
6678 }";
6679 assert_kotlin_class_members(
6680 src,
6681 &[
6682 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6683 (Kotlin::WhenEntry as u16, 14, "`when` entries"),
6684 (
6685 Kotlin::BinaryExpression as u16,
6686 8,
6687 "comparison / chain expressions",
6688 ),
6689 (Kotlin::AMPAMP as u16, 1, "`andd`'s `&&`"),
6690 ],
6691 &[
6692 // (member, abc.conditions, cyclomatic)
6693 ("f", 0, 1),
6694 // Was 4: two entries, two comparisons, counted twice.
6695 ("two", 2, 3),
6696 // The no-comparison control — 2 before and after.
6697 ("bare", 2, 3),
6698 // Was 2 each: the `GTEQ` / `EQEQ` spellings.
6699 ("ge", 1, 2),
6700 ("eq", 1, 2),
6701 // Was 3. A compound condition keeps both of its
6702 // comparisons — suppressing the operators instead
6703 // would have collapsed this one to 1.
6704 ("andd", 2, 3),
6705 // The condition slot scores a call directly, so a
6706 // spelling with no operator in it still reads 1.
6707 ("call", 1, 2),
6708 // Above its decision count on purpose: ABC counts
6709 // the `else`, cyclomatic does not.
6710 ("iff", 2, 2),
6711 ],
6712 );
6713 }
6714
6715 // #1459, the regression #1421 left behind. Turning the `when`
6716 // entry's blanket count into a condition slot moved the payment from
6717 // the entry to the condition — so every shape the slot cannot
6718 // classify went from 1 to 0, silently. Two shapes did:
6719 //
6720 // - `a!!`. `unary_expression` is one kind for *both* Kotlin unary
6721 // spellings, and the slot routed it to the peel, so this reads as
6722 // handled at every call site. The peel then looked for the operand
6723 // at child(1) — right for the prefix `!x`, wrong for a postfix
6724 // `x!!`, whose child(1) is the `!!` token — and stopped.
6725 // - `a as Boolean`. `as_expression` was not a wrapper the peel knew
6726 // at all, so the slot fell off the end of its `else if`.
6727 //
6728 // The controls are `bare` / `cmp` / `call`, one per surviving path
6729 // (terminal, comparison token, invocation): a fix that moved the
6730 // entry count back would take those to 2 and fail here, which is
6731 // what distinguishes repairing the peel from reverting #1421.
6732 //
6733 // `safe` is the §5 row and the one measurement decided rather than
6734 // taste. `as?` is already a condition *token*
6735 // (`src/metrics/abc/kotlin.rs`), so a peel that treated `as` and
6736 // `as?` alike would score it 2 — the double count that arm's own
6737 // fix was avoiding. `kotlin_wrapper_operand` excludes `as?` for
6738 // that reason, and this member is what fails if the exclusion goes:
6739 // `(a as? Boolean)!!` is valid Kotlin whose peel reaches the cast
6740 // through two other wrappers, so it needs no invalid fixture to
6741 // discriminate.
6742 //
6743 // `iff` carries the same `a!!` through the `if` slot. That half is
6744 // not #1421 fallout — `if (a!!)` has read 0 since the slot landed in
6745 // #773 — but it is the same peel and the same edit, and a fix
6746 // narrowed to `when` entries would leave it at 0.
6747 //
6748 // Every member is valid Kotlin: `a!!` on a `Boolean?`, `as` on an
6749 // `Any`, and `(a as? Boolean)!!` back to a `Boolean`. The kind
6750 // anchors are what stop a later edit from trimming a spelling out
6751 // and turning its member into a silent copy of `bare`.
6752 //
6753 // Since #1533 the slot pays for any operand it cannot see a
6754 // comparison or chain inside, so `bang`, `paren`, `cast` and `chain`
6755 // score 1 with or without the peel. `bangCmp` and `castCmp` are what
6756 // still need it: unpeeled, the wrapper pays as a value on top of the
6757 // `>` it hides, and they read 2.
6758 #[cfg(feature = "kotlin")]
6759 #[test]
6760 fn kotlin_condition_slot_peels_null_assertions_and_casts() {
6761 let src = "class K {
6762 fun g(x: Boolean): Boolean = true
6763 fun bare(a: Boolean): Int = when { a -> 1; else -> 0 }
6764 fun cmp(a: Int): Int = when { a > 5 -> 1; else -> 0 }
6765 fun call(a: Boolean): Int = when { g(a) -> 1; else -> 0 }
6766 fun bang(a: Boolean?): Int = when { a!! -> 1; else -> 0 }
6767 fun paren(a: Boolean?): Int = when { (a!!) -> 1; else -> 0 }
6768 fun cast(a: Any): Int = when { a as Boolean -> 1; else -> 0 }
6769 fun chain(a: Any?): Int = when { a!! as Boolean -> 1; else -> 0 }
6770 fun safe(a: Any): Int = when { (a as? Boolean)!! -> 1; else -> 0 }
6771 fun cmt(a: Boolean): Int = when { ! /*c*/ a -> 1; else -> 0 }
6772 fun iff(a: Boolean?): Int { if (a!!) { return 1 }; return 0 }
6773 fun bangCmp(a: Int): Int = when { (a > 5)!! -> 1; else -> 0 }
6774 fun castCmp(a: Int): Int = when { (a > 5) as Boolean -> 1; else -> 0 }
6775 }";
6776 assert_kotlin_class_members(
6777 src,
6778 &[
6779 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6780 (
6781 Kotlin::BANGBANG as u16,
6782 6,
6783 "the `!!` null assertions of `bang` / `paren` / `chain` / `safe` / `iff` / \
6784 `bangCmp`",
6785 ),
6786 (
6787 Kotlin::AsExpression as u16,
6788 4,
6789 "the casts of `cast` / `chain` / `safe` / `castCmp`",
6790 ),
6791 (Kotlin::AsQMARK as u16, 1, "`safe`'s safe cast"),
6792 (
6793 Kotlin::BlockComment as u16,
6794 1,
6795 "`cmt`'s interposed comment, spaces and all",
6796 ),
6797 (
6798 Kotlin::ParenthesizedExpression as u16,
6799 4,
6800 "the parenthesised operands of `paren` / `safe` / `bangCmp` / `castCmp`",
6801 ),
6802 ],
6803 &[
6804 // (member, abc.conditions, cyclomatic)
6805 ("g", 0, 1),
6806 // The three controls, unchanged by this fix.
6807 ("bare", 1, 2),
6808 ("cmp", 1, 2),
6809 ("call", 1, 2),
6810 // Each was 0: the peel stopped on the wrapper.
6811 ("bang", 1, 2),
6812 ("paren", 1, 2),
6813 ("cast", 1, 2),
6814 ("chain", 1, 2),
6815 // 1, not 2 — the `as?` token owns this one, and the
6816 // peel declines to charge for it a second time.
6817 ("safe", 1, 2),
6818 // The peel reads its operand through the `operator` /
6819 // `argument` fields, so an `extra` between the two no
6820 // longer displaces it. The spacing is load-bearing and
6821 // must not be tidied away: `!/*c*/a` unspaced parses
6822 // with the comment somewhere a `child(1)` read still
6823 // skips, so only the spaced spelling can tell the field
6824 // read from the positional one. Measured both ways.
6825 ("cmt", 1, 2),
6826 // The `if` slot, which has read 0 since #773.
6827 ("iff", 1, 2),
6828 // The `>` pays; the peel keeps the wrapper from paying too.
6829 ("bangCmp", 1, 2),
6830 ("castCmp", 1, 2),
6831 ],
6832 );
6833 }
6834
6835 // The other half of #1421: a subject-ful entry must not move. Its
6836 // condition is a *pattern* matched against the subject, not an
6837 // independent boolean expression, so the implicit `subject ==
6838 // pattern` is the decision and the entry itself pays for it —
6839 // `range_test`, `type_test` and a bare constant carry no token the
6840 // comparison arms would count.
6841 //
6842 // `cmp` is the interesting row and the reason `nested` sits beside
6843 // it. `when (x) { y > 5 -> … }` is legal when `x` is `Boolean`, and
6844 // it reads 2 conditions against 1 decision — the entry's implicit
6845 // equality plus the `>` inside the operand. That is not the #1421
6846 // double-count reappearing: `nested` spells the same thing as an
6847 // `if` (`x == (y > 5)`) and ABC has always scored it 2 against the
6848 // same 1 decision. A comparison nested inside a comparison is two
6849 // comparisons; only one of them is a branch.
6850 #[cfg(feature = "kotlin")]
6851 #[test]
6852 fn kotlin_subjectful_when_arms_keep_the_per_entry_count() {
6853 let src = "class K {
6854 fun inn(a: Int): Int = when (a) { in 1..2 -> 1; else -> 0 }
6855 fun notin(a: Int): Int = when (a) { !in 1..2 -> 1; else -> 0 }
6856 fun iss(a: Any): Int = when (a) { is String -> 1; else -> 0 }
6857 fun konst(a: Int): Int = when (a) { 1 -> 10; 2 -> 20; else -> 0 }
6858 fun cmp(x: Boolean, y: Int): Int = when (x) { y > 5 -> 1; else -> 0 }
6859 fun nested(x: Boolean, y: Int): Int { if (x == (y > 5)) { return 1 }; return 0 }
6860 }";
6861 assert_kotlin_class_members(
6862 src,
6863 &[
6864 (Kotlin::WhenSubject as u16, 5, "`when` subjects"),
6865 (Kotlin::WhenEntry as u16, 11, "`when` entries"),
6866 (Kotlin::RangeTest as u16, 2, "`in` / `!in` patterns"),
6867 (Kotlin::TypeTest as u16, 1, "the `is` pattern"),
6868 (
6869 Kotlin::BinaryExpression as u16,
6870 3,
6871 "`cmp`'s and `nested`'s comparisons",
6872 ),
6873 ],
6874 &[
6875 ("inn", 1, 2),
6876 ("notin", 1, 2),
6877 ("iss", 1, 2),
6878 ("konst", 2, 3),
6879 // Above its decision count, like `nested` below it
6880 // and for the same reason.
6881 ("cmp", 2, 2),
6882 ("nested", 2, 2),
6883 ],
6884 );
6885 }
6886
6887 // `kotlin_enclosing_when_has_subject` stops scanning at the `{`, so
6888 // that a subject-less `when` does not walk all N arms once per arm.
6889 // The bound has to clear the header first, and the header can carry
6890 // an `extra`: tree-sitter hangs `when /*c*/ (x)`'s comment off the
6891 // `when_expression` between the keyword and the subject, so a bound
6892 // one child tighter — `child(1)`, or a stop at the first *named*
6893 // child — reads `cmtSubject` as subject-less. It would then score
6894 // its `1` arm through the slot, where a `number_literal` is not a
6895 // Kotlin terminal, and report 0 conditions instead of 1. That is
6896 // what discriminates here; `cmtNone` is the other direction, where
6897 // the brace is all the bound has to stop on.
6898 //
6899 // The `block_comment` census is the fixture anchor: delete either
6900 // comment and the members still read 1, because a plain `when (x)`
6901 // and a plain `when {` both do. The count fails by name instead.
6902 #[cfg(feature = "kotlin")]
6903 #[test]
6904 fn kotlin_when_subject_scan_clears_a_comment_before_the_brace() {
6905 let src = "class K {
6906 fun cmtSubject(x: Int): Int = when /*c*/ (x) { 1 -> 1; else -> 0 }
6907 fun cmtNone(a: Boolean): Int = when /*c*/ { a -> 1; else -> 0 }
6908 }";
6909 assert_kotlin_class_members(
6910 src,
6911 &[
6912 (Kotlin::BlockComment as u16, 2, "the two header comments"),
6913 (Kotlin::WhenSubject as u16, 1, "`cmtSubject`'s subject"),
6914 (Kotlin::WhenEntry as u16, 4, "`when` entries"),
6915 ],
6916 &[("cmtSubject", 1, 2), ("cmtNone", 1, 2)],
6917 );
6918 }
6919
6920 // A subject-less condition reaches the slot through the same
6921 // `kotlin_wrapper_operand` peel an `if` predicate does, which
6922 // is what separates this design from suppressing the entry's
6923 // operator. `notted` and `paren` unwrap to a bare terminal and score
6924 // through the slot; `notCmp` and `parenCmp` unwrap to a
6925 // `binary_expression`, score nothing from the slot, and take their
6926 // one condition from the `>` the token arm already owned. Both pairs
6927 // land on 1, which no blanket `+1` can produce for the second pair —
6928 // they read 2 before the fix.
6929 //
6930 // `paren` once reached the slot only through a `WhenEntry` seed in the
6931 // retired walker's boolean-context set, and dropped to 0 without it.
6932 // On the shared slot (#1533) it pins that a `when` entry peels a
6933 // parenthesis as `if ((a))` does; `notted` cannot stand in for it,
6934 // since a `!` is a wrapper of its own.
6935 #[cfg(feature = "kotlin")]
6936 #[test]
6937 fn kotlin_subjectless_when_condition_wrappers_count_once() {
6938 let src = "class K {
6939 fun notted(a: Boolean): Int = when { !a -> 1; else -> 0 }
6940 fun paren(a: Boolean): Int = when { (a) -> 1; else -> 0 }
6941 fun notCmp(a: Int): Int = when { !(a > 1) -> 1; else -> 0 }
6942 fun parenCmp(a: Int): Int = when { (a > 1) -> 1; else -> 0 }
6943 fun nest(x: Int, y: Int): Int =
6944 when { x > 1 -> when { y > 2 -> 1; else -> 2 }; else -> 0 }
6945 fun blockBody(x: Int): Int { when { x > 5 -> { return 1 } }; return 0 }
6946 }";
6947 assert_kotlin_class_members(
6948 src,
6949 &[
6950 // Paired, not bare: "no subjects" alone is satisfied by a
6951 // fixture that lost its `when`s altogether.
6952 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6953 (Kotlin::WhenEntry as u16, 13, "`when` entries"),
6954 (Kotlin::UnaryExpression as u16, 2, "the two `!` operands"),
6955 (
6956 Kotlin::ParenthesizedExpression as u16,
6957 3,
6958 "the three parenthesised operands",
6959 ),
6960 (Kotlin::BinaryExpression as u16, 5, "comparisons"),
6961 ],
6962 &[
6963 ("notted", 1, 2),
6964 ("paren", 1, 2),
6965 // Both were 2: the entry's blanket count plus the `>`.
6966 ("notCmp", 1, 2),
6967 ("parenCmp", 1, 2),
6968 // Was 4 — one entry and one comparison per `when`,
6969 // each counted twice.
6970 ("nest", 2, 3),
6971 // The `when`-as-statement shape: a `block` body and no
6972 // `else` arm at all. Reading the condition from the
6973 // `condition` field rather than by position is what keeps
6974 // the block out of the slot; it was 2 before the fix.
6975 ("blockBody", 1, 2),
6976 ],
6977 );
6978 }
6979
6980 // The one place this design deliberately does not reach parity, put
6981 // under test so the choice cannot drift unremarked. A `when` entry
6982 // may list alternatives (`when { a, b -> … }`, an implicit `or`), and
6983 // the `condition` field is `multiple` — but cyclomatic scores the
6984 // whole entry as one decision, so `bareAlts` is at parity only
6985 // because the slot reads the *first* alternative and stops. Routing
6986 // every alternative through the slot would push it to 2.
6987 //
6988 // `alts` is the row that cannot be brought to parity from the ABC
6989 // side at all: the comparison-token arms count `>` and `<` wherever
6990 // they appear, so it reads 2 against 1 decision no matter what the
6991 // entry slot does. It was 3 before #1421 and the remaining gap is
6992 // cyclomatic's single count per multi-alternative entry, not a
6993 // double-count here.
6994 //
6995 // `subjAlts` is the subject-ful control, unchanged and at parity: its
6996 // alternatives are constants carrying no token to count.
6997 #[cfg(feature = "kotlin")]
6998 #[test]
6999 fn kotlin_subjectless_when_multi_alternative_entry() {
7000 let src = "class K {
7001 fun alts(x: Int, y: Int): Int = when { x > 5, y < 0 -> 1; else -> 0 }
7002 fun bareAlts(x: Boolean, y: Boolean): Int = when { x, y -> 1; else -> 0 }
7003 fun subjAlts(a: Int): Int = when (a) { 1, 2 -> 10; else -> 0 }
7004 }";
7005 assert_kotlin_class_members(
7006 src,
7007 &[
7008 (Kotlin::WhenSubject as u16, 1, "`subjAlts`'s subject"),
7009 (Kotlin::WhenEntry as u16, 6, "`when` entries"),
7010 (
7011 Kotlin::BinaryExpression as u16,
7012 2,
7013 "`alts`'s two comparisons",
7014 ),
7015 ],
7016 &[
7017 // Above its decision count: cyclomatic scores the entry
7018 // once, the token arms score both comparisons. Was 3.
7019 ("alts", 2, 2),
7020 ("bareAlts", 1, 2),
7021 ("subjAlts", 1, 2),
7022 ],
7023 );
7024 }
7025
7026 // `is_expression` and `in_expression` are the two relational forms
7027 // tree-sitter-kotlin-ng spells as their own production rather than as
7028 // a `binary_expression`, so no comparison-token arm sees them and
7029 // nothing counted them: `if (a is String)` scored 0 against a
7030 // decision count of 1, and the same test in a subject-less `when`
7031 // scored 1 only because the entry's blanket `+1` happened to cover
7032 // it. Removing that blanket without counting these two somewhere
7033 // would have regressed `whenIs` and `whenIn` to 0, and `isAnd` —
7034 // where the `is` test is one operand of a `&&` chain and so reaches
7035 // the walker rather than the slot — from 2 to 1.
7036 //
7037 // The `if` members are here because the fix was never `when`-
7038 // specific: they are the call sites that prove it reaches the `if` /
7039 // `while` predicate slot too.
7040 //
7041 // #1421 counted them through the former
7042 // `kotlin_bool_terminal_kinds!()` and
7043 // #1461 moved them to an unconditional arm, which leaves every
7044 // number below unchanged — the slot no longer counts the test and
7045 // the arm does. `kotlin_is_and_in_score_outside_a_boolean_slot` is
7046 // the half this fixture cannot see, every member of it being inside
7047 // a slot.
7048 #[cfg(feature = "kotlin")]
7049 #[test]
7050 fn kotlin_is_and_in_expressions_are_unary_conditions() {
7051 let src = "class K {
7052 fun ifIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
7053 fun ifIn(a: Int): Int { if (a in 1..2) { return 1 }; return 0 }
7054 fun whenIs(a: Any): Int = when { a is String -> 1; else -> 0 }
7055 fun whenIn(a: Int): Int = when { a in 1..2 -> 1; else -> 0 }
7056 fun isAnd(a: Any, b: Boolean): Int = when { a is String && b -> 1; else -> 0 }
7057 }";
7058 assert_kotlin_class_members(
7059 src,
7060 &[
7061 // Paired, not bare — see the wrappers test above.
7062 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
7063 (Kotlin::WhenEntry as u16, 6, "`when` entries"),
7064 (Kotlin::IsExpression as u16, 3, "`is` tests"),
7065 (Kotlin::InExpression as u16, 2, "`in` tests"),
7066 (Kotlin::AMPAMP as u16, 1, "`isAnd`'s `&&`"),
7067 ],
7068 &[
7069 // Both were 0 — the predicate slot saw a kind it did
7070 // not classify and the token arms saw no operator.
7071 ("ifIs", 1, 2),
7072 ("ifIn", 1, 2),
7073 ("whenIs", 1, 2),
7074 ("whenIn", 1, 2),
7075 ("isAnd", 2, 3),
7076 ],
7077 );
7078 }
7079
7080 // Kotlin spells boolean `and` / `or` / `xor` as infix *functions*, so
7081 // `a and b` parses as `infix_expression` — not `binary_expression`,
7082 // and not any token arm. A slot pays for it for the same reason it
7083 // pays for a `call_expression`: `a and b` is `a.and(b)`.
7084 //
7085 // This was a regression of #1421, not a pre-existing gap, and it is
7086 // the third shape the blanket per-entry count turned out to be
7087 // propping up — after `is` / `in` and bare parens, which that fix
7088 // caught. Found by the whole-branch review. Measured on both sides:
7089 // `when { a and b -> … }` scored 1 before and 0 after, while the
7090 // `if (a and b)` it is supposed to agree with still scored 1.
7091 //
7092 // The `if` members are the anchor rather than a second `when`: the
7093 // whole premise of #1421 is that a subject-less arm scores like an
7094 // `if` predicate, so a fixture where the two disagree is the
7095 // statement of the bug. `andAmp` holds the pair level against the
7096 // `&&` spelling, which never regressed and would otherwise be the
7097 // only form under test.
7098 #[cfg(feature = "kotlin")]
7099 #[test]
7100 fn kotlin_infix_boolean_functions_are_unary_conditions() {
7101 let src = "class K {
7102 fun whenAnd(a: Boolean, b: Boolean): Int = when { a and b -> 1; else -> 0 }
7103 fun whenOr(a: Boolean, b: Boolean): Int = when { a or b -> 1; else -> 0 }
7104 fun whenXor(a: Boolean, b: Boolean): Int = when { a xor b -> 1; else -> 0 }
7105 fun whenAmp(a: Boolean, b: Boolean): Int = when { a && b -> 1; else -> 0 }
7106 fun ifAnd(a: Boolean, b: Boolean): Int { if (a and b) { return 1 }; return 0 }
7107 }";
7108 assert_kotlin_class_members(
7109 src,
7110 &[
7111 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
7112 (Kotlin::InfixExpression as u16, 4, "the infix calls"),
7113 (Kotlin::AMPAMP as u16, 1, "`whenAmp`'s `&&`"),
7114 ],
7115 &[
7116 // 0 before this fix: the predicate slot saw a kind it
7117 // did not classify and no token arm matched.
7118 ("whenAnd", 1, 2),
7119 ("whenOr", 1, 2),
7120 ("whenXor", 1, 2),
7121 // Never regressed — `&&` is a `binary_expression` whose
7122 // operator the token arm owns. Holds the others level.
7123 ("whenAmp", 2, 3),
7124 // The agreement #1421 exists to establish: a
7125 // subject-less arm scores what the equivalent `if`
7126 // predicate scores.
7127 ("ifAnd", 1, 2),
7128 ],
7129 );
7130 }
7131
7132 #[cfg(feature = "kotlin")]
7133 #[test]
7134 fn kotlin_catch_block_counts() {
7135 check_metrics::<KotlinParser>(
7136 "fun m() {
7137 try {
7138 println(\"ok\")
7139 } catch (e: Exception) {
7140 println(\"err\")
7141 }
7142 }",
7143 "foo.kt",
7144 |metric| {
7145 // `try` (+1) and `catch` (+1) each contribute one condition,
7146 // matching Java / C# / C++ / Groovy (Fitzpatrick counts both
7147 // keywords). Before #696 Kotlin counted only the catch block.
7148 assert_eq!(metric.abc.conditions_sum(), 2);
7149 insta::assert_json_snapshot!(metric.abc);
7150 },
7151 );
7152 }
7153
7154 #[cfg(feature = "kotlin")]
7155 #[test]
7156 fn kotlin_elvis_and_safe_cast() {
7157 // `?:` (elvis) and `as?` (safe cast) are condition-like.
7158 check_metrics::<KotlinParser>(
7159 "fun m(s: String?): Int {
7160 val n = (s as? Int) ?: 0
7161 return n
7162 }",
7163 "foo.kt",
7164 |metric| {
7165 // as? (+1) + ?: (+1) = 2 conditions.
7166 assert_eq!(metric.abc.conditions_sum(), 2);
7167 insta::assert_json_snapshot!(metric.abc);
7168 },
7169 );
7170 }
7171
7172 #[cfg(feature = "kotlin")]
7173 #[test]
7174 fn kotlin_generic_brackets_not_conditions() {
7175 // `<` / `>` used as type-parameter brackets must not be counted.
7176 check_metrics::<KotlinParser>(
7177 "class Box<T>(val v: T)
7178 fun <T> wrap(x: T): Box<T> = Box(x)",
7179 "foo.kt",
7180 |metric| {
7181 // No comparisons — only generic brackets.
7182 assert_eq!(metric.abc.conditions_sum(), 0);
7183 insta::assert_json_snapshot!(metric.abc);
7184 },
7185 );
7186 }
7187
7188 // #1297: a qualified super call disambiguates its supertype with
7189 // the same two bare tokens a comparison uses — `super<A>.g()` is a
7190 // `super_expression`, which the previous denylist
7191 // (`type_arguments` / `type_parameters`) did not name, so it scored
7192 // two conditions. One `super<A>` against one genuine `a < b`
7193 // separates every mis-aim: 3 pre-fix, 1 once the arm allows
7194 // `BinaryExpression`, 0 if it allows the wrong parent or the
7195 // fixture stops parsing.
7196 #[cfg(feature = "kotlin")]
7197 #[test]
7198 fn kotlin_super_type_argument_is_not_a_condition() {
7199 check_metrics::<KotlinParser>(
7200 "class B : A() {
7201 override fun g(a: Int, b: Int): Int {
7202 if (a < b) { return super<A>.g(a, b) }
7203 return 0
7204 }
7205 }",
7206 "foo.kt",
7207 |metric| {
7208 assert_eq!(metric.abc.conditions_sum(), 1);
7209 // Non-vacuity guard: 1 is also what a body whose `if`
7210 // survived but whose super call did not would score, so
7211 // pin the call itself. Measured: dropping
7212 // `super<A>.g(a, b)` takes `branches_sum()` from 2 to 1 —
7213 // the remaining branch is the `A()` primary-constructor
7214 // delegation in the class header, which #1384 taught this
7215 // metric to count.
7216 assert_eq!(metric.abc.branches_sum(), 2);
7217 },
7218 );
7219 }
7220
7221 #[cfg(feature = "kotlin")]
7222 #[test]
7223 fn kotlin_class_with_methods_and_branches() {
7224 check_metrics::<KotlinParser>(
7225 "class C {
7226 var counter: Int = 0
7227 fun bump() {
7228 counter += 1
7229 println(counter)
7230 }
7231 }",
7232 "foo.kt",
7233 |metric| {
7234 // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
7235 // branches: println(counter) = 1
7236 assert_eq!(metric.abc.assignments_sum(), 2);
7237 assert_eq!(metric.abc.branches_sum(), 1);
7238 assert_eq!(metric.abc.conditions_sum(), 0);
7239 insta::assert_json_snapshot!(metric.abc);
7240 },
7241 );
7242 }
7243
7244 #[cfg(feature = "kotlin")]
7245 #[test]
7246 fn kotlin_object_singleton_abc() {
7247 check_metrics::<KotlinParser>(
7248 "object Util {
7249 fun work(x: Int): Int {
7250 var y = x
7251 y += 1
7252 if (y > 0) {
7253 return y
7254 }
7255 return -1
7256 }
7257 }",
7258 "foo.kt",
7259 |metric| {
7260 // assignments: var y = x (+1), y += 1 (+1) = 2
7261 // branches: 0 (return is not a call)
7262 // conditions: y > 0 (+1) = 1
7263 assert_eq!(metric.abc.assignments_sum(), 2);
7264 assert_eq!(metric.abc.branches_sum(), 0);
7265 assert_eq!(metric.abc.conditions_sum(), 1);
7266 insta::assert_json_snapshot!(metric.abc);
7267 },
7268 );
7269 }
7270
7271 #[cfg(feature = "kotlin")]
7272 #[test]
7273 fn kotlin_interface_abc() {
7274 // Pure-abstract interface with no bodies — all-zero.
7275 check_metrics::<KotlinParser>(
7276 "interface I {
7277 fun work(): Int
7278 fun describe(): String
7279 }",
7280 "foo.kt",
7281 |metric| {
7282 assert_eq!(metric.abc.assignments_sum(), 0);
7283 assert_eq!(metric.abc.branches_sum(), 0);
7284 assert_eq!(metric.abc.conditions_sum(), 0);
7285 insta::assert_json_snapshot!(metric.abc);
7286 },
7287 );
7288 }
7289
7290 #[cfg(feature = "kotlin")]
7291 #[test]
7292 fn kotlin_nested_class_abc() {
7293 check_metrics::<KotlinParser>(
7294 "class Outer {
7295 var o: Int = 0
7296 class Nested {
7297 var n: Int = 0
7298 fun bump() { n += 1 }
7299 }
7300 }",
7301 "foo.kt",
7302 |metric| {
7303 // Outer: var o = 0 (+1)
7304 // Nested: var n = 0 (+1), n += 1 (+1) = 2
7305 // total assignments = 3
7306 assert_eq!(metric.abc.assignments_sum(), 3);
7307 insta::assert_json_snapshot!(metric.abc);
7308 },
7309 );
7310 }
7311
7312 #[cfg(feature = "kotlin")]
7313 #[test]
7314 fn kotlin_data_class_abc() {
7315 // `data class` with primary-constructor `val`s — no assignments
7316 // (vals don't count) and no body conditions.
7317 check_metrics::<KotlinParser>(
7318 "data class Point(val x: Int, val y: Int)",
7319 "foo.kt",
7320 |metric| {
7321 assert_eq!(metric.abc.assignments_sum(), 0);
7322 assert_eq!(metric.abc.branches_sum(), 0);
7323 assert_eq!(metric.abc.conditions_sum(), 0);
7324 insta::assert_json_snapshot!(metric.abc);
7325 },
7326 );
7327 }
7328
7329 #[cfg(feature = "kotlin")]
7330 #[test]
7331 fn kotlin_primary_constructor_default_value_not_assignment() {
7332 // Regression: default values on primary-constructor `val`
7333 // parameters are initialisers, not assignments. Without
7334 // `ClassParameter` pushing a declaration sentinel, the `=` token
7335 // here would be counted unconditionally as a standalone
7336 // assignment.
7337 check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
7338 // `val a = 5` → suppressed (Const sentinel).
7339 assert_eq!(metric.abc.assignments_sum(), 0);
7340 insta::assert_json_snapshot!(metric.abc);
7341 });
7342 }
7343
7344 #[cfg(feature = "kotlin")]
7345 #[test]
7346 fn kotlin_unary_conditions_in_chain() {
7347 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
7348 // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
7349 // contribute one; the `&&` / `||` operators contribute nothing.
7350 // expected: 3 unary conditions, no comparisons, no `if`-keyword
7351 // condition in Kotlin (matches the Java byte-equivalent of 3).
7352 check_metrics::<KotlinParser>(
7353 "fun f(a: Boolean, b: Boolean, c: Boolean) {
7354 if (a && b || c) { println(\"x\") }
7355 }",
7356 "foo.kt",
7357 |metric| {
7358 assert_eq!(metric.abc.conditions_sum(), 3);
7359 },
7360 );
7361 }
7362
7363 #[cfg(feature = "kotlin")]
7364 #[test]
7365 fn kotlin_comparison_operands_add_nothing() {
7366 // Isolation check: comparison operands of a `&&` chain are nested
7367 // `binary_expression` nodes, not bare boolean leaves, so the
7368 // walker adds nothing — only the two `>` comparisons count.
7369 // expected: 2 (the two `>` tokens), walker contributes 0.
7370 check_metrics::<KotlinParser>(
7371 "fun g(x: Int, y: Int) {
7372 if (x > 0 && y > 0) { println(\"x\") }
7373 }",
7374 "foo.kt",
7375 |metric| {
7376 assert_eq!(metric.abc.conditions_sum(), 2);
7377 },
7378 );
7379 }
7380
7381 #[cfg(feature = "kotlin")]
7382 #[test]
7383 fn kotlin_negated_operand_is_unary_condition() {
7384 // A `!`-negated operand is still a unary condition: `a && !b`
7385 // unwraps the `unary_expression` to reach the inner identifier.
7386 // expected: 2 (`a` and the `!b` operand).
7387 check_metrics::<KotlinParser>(
7388 "fun f(a: Boolean, b: Boolean) {
7389 if (a && !b) { println(\"x\") }
7390 }",
7391 "foo.kt",
7392 |metric| {
7393 assert_eq!(metric.abc.conditions_sum(), 2);
7394 },
7395 );
7396 }
7397
7398 // The wrapper peel has two callers, and #1459's fixture members all
7399 // reach it through the *slot* (`kotlin_count_condition`). This is the
7400 // other one: the `&&` / `||` walker (`count_field_operands`),
7401 // which hands each operand to the same slot. The two wrappers #1459
7402 // taught it therefore have a
7403 // second, structurally independent path into the count, and a
7404 // fixture covering only the slot leaves it untested
7405 // (`.claude/rules/grammar-dispatch.md` §11).
7406 //
7407 // Each chain is one identifier plus one wrapped comparison, so 2 is
7408 // "the walker peeled the wrapper to the comparison, which pays for
7409 // itself" and 3 is "it paid the wrapper as a value as well". Since
7410 // the slot pays for any operand it cannot see a comparison inside
7411 // (#1533), a wrapped bare identifier scores 2 either way, so only a
7412 // wrapped comparison tells the peel from its absence. The bare
7413 // `a && b` control is `kotlin_unary_conditions_in_chain`'s shape at
7414 // 2, so a regression cannot be read as the chain itself changing.
7415 #[cfg(feature = "kotlin")]
7416 #[test]
7417 fn kotlin_chain_operands_peel_null_assertions_and_casts() {
7418 // Anchored per row, because the assertion alone cannot tell the
7419 // wrapper from its operand: `a && x > 1` scores the same 2 / 3 as
7420 // every row here, so trimming the `!!` or the cast out of a
7421 // fixture would leave this green with its subject gone
7422 // (`.claude/rules/testing.md`, "Perturb the fixture as well as
7423 // the production line").
7424 for (chain, spelling, anchors) in [
7425 (
7426 "a && (x > 1)!!",
7427 "null assertion",
7428 &[(Kotlin::BANGBANG as u16, 1usize, "the `!!`")][..],
7429 ),
7430 (
7431 "a && (x > 1) as Boolean",
7432 "cast",
7433 &[(Kotlin::AsExpression as u16, 1, "the `as` cast")][..],
7434 ),
7435 (
7436 "a && ((x > 1)!!)",
7437 "parenthesised null assertion",
7438 &[
7439 (Kotlin::BANGBANG as u16, 1, "the `!!`"),
7440 (Kotlin::ParenthesizedExpression as u16, 2, "the parens"),
7441 ][..],
7442 ),
7443 ] {
7444 let src = format!("fun f(a: Boolean, x: Int) {{ if ({chain}) {{ println(\"x\") }} }}");
7445 assert_fixture_spells::<KotlinParser>(&src, "foo.kt", anchors);
7446 check_func_space::<KotlinParser, _>(&src, "foo.kt", |space| {
7447 let f = child_space(&space, "f");
7448 assert_eq!(
7449 f.metrics.abc.conditions(),
7450 2,
7451 "`{chain}`: the identifier plus the comparison under the {spelling}"
7452 );
7453 assert_eq!(f.metrics.cyclomatic.cyclomatic(), 3, "`{chain}`: decisions");
7454 });
7455 }
7456 }
7457
7458 #[cfg(feature = "kotlin")]
7459 #[test]
7460 fn kotlin_bare_if_predicate_is_one_condition() {
7461 // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one
7462 // Fitzpatrick unary condition. Before the Phase-2B arm it counted
7463 // 0, so `if (flag) 1 else -1` scored 1 (only the `else`) instead of
7464 // 2, dropping below Kotlin's own cyclomatic decision count.
7465 // expected: predicate (1) + else (1) = 2.
7466 check_metrics::<KotlinParser>(
7467 "fun m(flag: Boolean): Int { return if (flag) 1 else -1 }",
7468 "foo.kt",
7469 |metric| {
7470 assert_eq!(metric.abc.conditions_sum(), 2);
7471 },
7472 );
7473 }
7474
7475 #[cfg(feature = "kotlin")]
7476 #[test]
7477 fn kotlin_bare_while_predicate_is_one_condition() {
7478 // Issue #773: the bare predicate of a `while` loop counts one
7479 // condition via the `condition` field. expected: 1.
7480 check_metrics::<KotlinParser>(
7481 "fun m(running: Boolean) { while (running) { println(\"x\") } }",
7482 "foo.kt",
7483 |metric| {
7484 assert_eq!(metric.abc.conditions_sum(), 1);
7485 },
7486 );
7487 }
7488
7489 #[cfg(feature = "kotlin")]
7490 #[test]
7491 fn kotlin_bare_do_while_predicate_is_one_condition() {
7492 // Issue #773: the bare predicate of a `do`/`while` loop counts one
7493 // condition. expected: 1.
7494 check_metrics::<KotlinParser>(
7495 "fun m(ok: Boolean) { do { println(\"x\") } while (ok) }",
7496 "foo.kt",
7497 |metric| {
7498 assert_eq!(metric.abc.conditions_sum(), 1);
7499 },
7500 );
7501 }
7502
7503 #[cfg(feature = "kotlin")]
7504 #[test]
7505 fn kotlin_comparison_predicate_not_double_counted() {
7506 // Double-count guard (#773): a comparison predicate (`if (a == b)`)
7507 // is a nested `binary_expression` already counted by the `==` token
7508 // arm, so the Phase-2B condition-slot arm must add nothing here.
7509 // expected: `==` (1) + else (1) = 2 — unchanged by the new arm.
7510 check_metrics::<KotlinParser>(
7511 "fun m(a: Int, b: Int): Int { return if (a == b) 1 else -1 }",
7512 "foo.kt",
7513 |metric| {
7514 assert_eq!(metric.abc.conditions_sum(), 2);
7515 },
7516 );
7517 }
7518
7519 #[cfg(feature = "kotlin")]
7520 #[test]
7521 fn kotlin_short_circuit_predicate_not_double_counted() {
7522 // Double-count guard (#773): an `&&`/`||` predicate is counted by
7523 // the Rule 9 chain walker (each operand once); the Phase-2B arm
7524 // must add nothing for it. expected: `x` (1) + `y` (1) + else (1)
7525 // = 3 — unchanged by the new arm.
7526 check_metrics::<KotlinParser>(
7527 "fun m(x: Boolean, y: Boolean): Int { return if (x && y) 1 else -1 }",
7528 "foo.kt",
7529 |metric| {
7530 assert_eq!(metric.abc.conditions_sum(), 3);
7531 },
7532 );
7533 }
7534
7535 #[cfg(feature = "kotlin")]
7536 #[test]
7537 fn kotlin_parenthesised_bare_predicate_is_one_condition() {
7538 // A parenthesised bare predicate (`if ((flag))`) is unwrapped by
7539 // `kotlin_wrapper_operand` and still counts one condition (#773).
7540 // expected: 1.
7541 check_metrics::<KotlinParser>(
7542 "fun m(flag: Boolean) { if ((flag)) { println(\"x\") } }",
7543 "foo.kt",
7544 |metric| {
7545 assert_eq!(metric.abc.conditions_sum(), 1);
7546 },
7547 );
7548 }
7549
7550 // --- TypeScript / TSX ABC tests --------------------------------------
7551 //
7552 // Assignment, branch, condition counting per Fitzpatrick:
7553 // - Augmented assignment / `++` / `--` always count.
7554 // - Plain `=` counts unless inside `const` declaration.
7555 // - `call_expression` / `new_expression` count as branches.
7556 // - Comparison / equality operators, ternary `?`, `??`, control-flow
7557 // arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
7558 // and `<`/`>` (outside `type_arguments` / `type_parameters`) count
7559 // as conditions.
7560
7561 #[cfg(feature = "typescript")]
7562 #[test]
7563 fn typescript_assignments_basic() {
7564 check_metrics::<TypescriptParser>(
7565 "class C {
7566 m(): void {
7567 let x = 0; // +1 — only a `const` initializer is suppressed
7568 x = 1; // +1
7569 x += 2; // +1
7570 x++; // +1
7571 }
7572 }",
7573 "foo.ts",
7574 |metric| {
7575 assert_eq!(metric.abc.assignments_sum(), 4);
7576 insta::assert_json_snapshot!(metric.abc);
7577 },
7578 );
7579 }
7580
7581 #[cfg(feature = "typescript")]
7582 #[test]
7583 fn typescript_const_excluded_from_assignments() {
7584 check_metrics::<TypescriptParser>(
7585 "class C {
7586 m(): void {
7587 const a = 1; // suppressed (`const` initializer)
7588 const b = 2; // suppressed
7589 let c = 3; // +1 — `let` initializers count
7590 }
7591 }",
7592 "foo.ts",
7593 |metric| {
7594 assert_eq!(metric.abc.assignments_sum(), 1);
7595 insta::assert_json_snapshot!(metric.abc);
7596 },
7597 );
7598 }
7599
7600 // Regression cluster for #1277. The pre-fix implementation decided
7601 // "is this `=` a `const` initializer?" from a sentinel stack cleared
7602 // only on a `SEMI` token, so the answer for one statement depended on
7603 // the *previous* statement's terminator. Automatic semicolon
7604 // insertion makes that terminator optional in all four JS-family
7605 // languages. The replacement is structural — see
7606 // `impl_js_family_const_binding!` in `src/metrics/abc/js_family.rs`.
7607
7608 #[cfg(feature = "typescript")]
7609 #[test]
7610 fn typescript_asi_const_does_not_suppress_later_assignments() {
7611 check_metrics::<TypescriptParser>(
7612 "function f() {
7613 const a = 1
7614 x = 2
7615 y = 3
7616 }",
7617 "foo.ts",
7618 |metric| {
7619 // `const a = 1` suppressed; `x = 2` and `y = 3` count.
7620 // Pre-#1277 this reported 0: the unterminated `const`
7621 // never popped its sentinel.
7622 assert_eq!(metric.abc.assignments_sum(), 2);
7623 },
7624 );
7625 }
7626
7627 #[cfg(feature = "typescript")]
7628 #[test]
7629 fn typescript_semicolon_const_does_not_suppress_later_assignments() {
7630 // The semicolon-terminated spelling of the fixture above, which
7631 // must score the same as it. Pre-#1277 they scored 2 and 0 — the
7632 // pair is what pins the terminator out of the answer.
7633 check_metrics::<TypescriptParser>(
7634 "function f() {
7635 const a = 1;
7636 x = 2;
7637 y = 3;
7638 }",
7639 "foo.ts",
7640 |metric| {
7641 assert_eq!(metric.abc.assignments_sum(), 2);
7642 },
7643 );
7644 }
7645
7646 #[cfg(feature = "typescript")]
7647 #[test]
7648 fn typescript_as_const_does_not_suppress_later_assignments() {
7649 // The sentinel stack was also reachable from the other side: the
7650 // `const` token of a TypeScript `x as const` assertion promoted a
7651 // live `let` slot to `Const` and suppressed every `=` until the
7652 // next `;`. Structurally that `const` is a child of an
7653 // `as_expression`, not a declaration keyword.
7654 check_metrics::<TypescriptParser>(
7655 "function f(x: number) {
7656 let y = x as const
7657 w = 3
7658 }",
7659 "foo.ts",
7660 |metric| {
7661 // `let` initializer (+1) and `w = 3` (+1); pre-#1277: 1.
7662 assert_eq!(metric.abc.assignments_sum(), 2);
7663 },
7664 );
7665 }
7666
7667 #[cfg(feature = "typescript")]
7668 #[test]
7669 fn typescript_const_declarator_shapes_stay_suppressed() {
7670 // Shapes the sentinel stack handled implicitly, which the
7671 // structural predicate must reproduce: *every* declarator under a
7672 // `const`-bearing `lexical_declaration` is suppressed, including
7673 // the second element of a multi-declarator list, a destructuring
7674 // pattern, and the defaults inside one — `c = 5`, the nested
7675 // `e = 6` and its `= {}`, `g = 7`, and the rest element's `h = 8`,
7676 // each one more pattern layer the predicate's climb has to cross
7677 // — while `for (const x of xs)` carries no `=` at all. Only
7678 // `let i = 3` and `var j = 4` count — the deliberate deviation
7679 // documented on `js_abc_compute!`. Gating on
7680 // `variable_declaration` instead of `lexical_declaration`,
7681 // dropping the `const` check, or stopping the climb at the
7682 // declarator's own `=`, each flips one of these rows. The
7683 // `for`-of row is the exception: it carries no `=`, so no change
7684 // to the predicate can move it. It pins the grammar shape instead
7685 // — a future grammar emitting a `variable_declarator` there would
7686 // start suppressing something that never counted.
7687 check_metrics::<TypescriptParser>(
7688 "function f(o: any, xs: number[]) {
7689 const a = 1, b = 2
7690 const {c = 5, d: {e = 6} = {}} = o
7691 const [g = 7, ...[h = 8]] = xs
7692 let i = 3
7693 var j = 4
7694 for (const x of xs) { k(x) }
7695 }",
7696 "foo.ts",
7697 |metric| {
7698 assert_eq!(metric.abc.assignments_sum(), 2);
7699 },
7700 );
7701 }
7702
7703 #[cfg(feature = "typescript")]
7704 #[test]
7705 fn typescript_const_initializer_value_assignments_still_count() {
7706 // TypeScript half of
7707 // `javascript_const_initializer_value_assignments_still_count`.
7708 check_metrics::<TypescriptParser>(
7709 "function m(o: any, a: any, b: any) { const x = (o.p = 1); const y = a || (b = 2); }",
7710 "foo.ts",
7711 |metric| {
7712 assert_eq!(metric.abc.assignments_sum(), 2);
7713 },
7714 );
7715 }
7716
7717 #[cfg(feature = "typescript")]
7718 #[test]
7719 fn typescript_branches_function_calls() {
7720 check_metrics::<TypescriptParser>(
7721 "class C {
7722 m(): void {
7723 foo(); // +1
7724 bar(1, 2); // +1
7725 new Date(); // +1
7726 }
7727 }",
7728 "foo.ts",
7729 |metric| {
7730 assert_eq!(metric.abc.branches_sum(), 3);
7731 insta::assert_json_snapshot!(metric.abc);
7732 },
7733 );
7734 }
7735
7736 #[cfg(feature = "typescript")]
7737 #[test]
7738 fn typescript_conditions_comparison_operators() {
7739 check_metrics::<TypescriptParser>(
7740 "class C {
7741 m(x: number, y: number): boolean {
7742 return x == y // +1
7743 || x === y // +1
7744 || x != y // +1
7745 || x !== y // +1
7746 || x < y // +1
7747 || x <= y // +1
7748 || x > y // +1
7749 || x >= y; // +1
7750 }
7751 }",
7752 "foo.ts",
7753 |metric| {
7754 assert_eq!(metric.abc.conditions_sum(), 8);
7755 insta::assert_json_snapshot!(metric.abc);
7756 },
7757 );
7758 }
7759
7760 #[cfg(feature = "typescript")]
7761 #[test]
7762 fn typescript_conditions_control_flow_arms() {
7763 check_metrics::<TypescriptParser>(
7764 "class C {
7765 m(x: number): number {
7766 try { // +1 (try)
7767 if (x > 0) { // +1 (>)
7768 return 1;
7769 } else { // +1 (else)
7770 return -1;
7771 }
7772 } catch (e) { // +1 (catch)
7773 return 0;
7774 }
7775 }
7776 }",
7777 "foo.ts",
7778 |metric| {
7779 assert_eq!(metric.abc.conditions_sum(), 4);
7780 insta::assert_json_snapshot!(metric.abc);
7781 },
7782 );
7783 }
7784
7785 #[cfg(feature = "typescript")]
7786 #[test]
7787 fn typescript_conditions_switch_case() {
7788 check_metrics::<TypescriptParser>(
7789 "class C {
7790 m(x: number): number {
7791 switch (x) {
7792 case 1: // +1
7793 return 1;
7794 case 2: // +1
7795 return 2;
7796 default: // +0 (fallthrough, #469)
7797 return 0;
7798 }
7799 }
7800 }",
7801 "foo.ts",
7802 |metric| {
7803 assert_eq!(metric.abc.conditions_sum(), 2);
7804 insta::assert_json_snapshot!(metric.abc);
7805 },
7806 );
7807 }
7808
7809 #[cfg(feature = "typescript")]
7810 #[test]
7811 fn typescript_ternary_and_nullish() {
7812 check_metrics::<TypescriptParser>(
7813 "class C {
7814 m(x: number | null): number {
7815 return x !== null // +1 (!==)
7816 ? x // +1 (ternary ?)
7817 : 0;
7818 }
7819 n(x: number | null): number {
7820 return x ?? 0; // +1 (??)
7821 }
7822 }",
7823 "foo.ts",
7824 |metric| {
7825 assert_eq!(metric.abc.conditions_sum(), 3);
7826 insta::assert_json_snapshot!(metric.abc);
7827 },
7828 );
7829 }
7830
7831 #[cfg(feature = "typescript")]
7832 #[test]
7833 fn typescript_instanceof_counts_as_condition() {
7834 check_metrics::<TypescriptParser>(
7835 "class C {
7836 m(o: unknown): boolean {
7837 return o instanceof C; // +1
7838 }
7839 }",
7840 "foo.ts",
7841 |metric| {
7842 assert_eq!(metric.abc.conditions_sum(), 1);
7843 insta::assert_json_snapshot!(metric.abc);
7844 },
7845 );
7846 }
7847
7848 #[cfg(feature = "typescript")]
7849 #[test]
7850 fn typescript_generic_lt_gt_not_a_condition() {
7851 // `<T>` in `class C<T>` and `Array<number>` should not contribute
7852 // to conditions even though the tokens are `<` and `>`.
7853 check_metrics::<TypescriptParser>(
7854 "class C<T> {
7855 xs: Array<number> = [];
7856 m(): void {
7857 const arr: Array<string> = []; // suppressed const
7858 void arr;
7859 }
7860 }",
7861 "foo.ts",
7862 |metric| {
7863 assert_eq!(metric.abc.conditions_sum(), 0);
7864 insta::assert_json_snapshot!(metric.abc);
7865 },
7866 );
7867 }
7868
7869 // #1297, TypeScript half: the `<` / `>` arm flipped from denying
7870 // `type_arguments` / `type_parameters` to allowing
7871 // `binary_expression`, and this pins the direction that flip could
7872 // have broken. `Array<string>` keeps a generic in scope so the
7873 // fixture proves both sides at once, and the assertion is the
7874 // grammar-dispatch §8 pin: on the function's own space the ABC
7875 // condition count equals the cyclomatic decision count
7876 // (`cyclomatic()` minus the per-space base of 1). Both are 2, one
7877 // per `if`.
7878 //
7879 // As with the Lua fixture, `Array<string>` leaves no ABC trace once
7880 // excluded, so nothing here fails if a later edit drops the
7881 // generic; that the generic reaches the arm is established by the
7882 // revert test, which fails this test when the gate is removed.
7883 //
7884 // TypeScript gets no JSX fixture because the `.ts` dialect has no
7885 // JSX to exercise: it lexes `return <div>…</div>` as a type
7886 // assertion and a comparison chain, sometimes with `ERROR` nodes and
7887 // sometimes without, depending on what follows the tag. Either way
7888 // the tokens land under `type_arguments` and `binary_expression`
7889 // rather than the `jsx_*` productions, so the construct that
7890 // motivated #1297 is only reachable through
7891 // `tsx_jsx_elements_are_not_conditions` below.
7892 //
7893 // That mis-parse still over-counts — a `.ts` file containing JSX
7894 // scores conditions this gate cannot exclude, because the grammar
7895 // genuinely reports a `binary_expression`. It is a wrong-dialect
7896 // input rather than a defect in this arm, and no allowlist can
7897 // distinguish it; see the same caveat on Kotlin's arm.
7898 #[cfg(feature = "typescript")]
7899 #[test]
7900 fn typescript_comparison_operators_still_count_alongside_generics() {
7901 check_func_space::<TypescriptParser, _>(
7902 "function m(xs: Array<string>, a: number, b: number): number {
7903 if (a < b) { return 1; }
7904 if (a > b) { return 2; }
7905 void xs;
7906 return 0;
7907 }",
7908 "foo.ts",
7909 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
7910 );
7911 }
7912
7913 // #1275, TypeScript half. Eleven grammar productions emit a bare
7914 // `?` and only `ternary_expression` is a decision; the other ten are
7915 // type syntax. This fixture exercises five of them — `optional_
7916 // parameter`, `property_signature`, `method_signature`,
7917 // `abstract_method_signature` and `public_field_definition`, plus an
7918 // `optional_type` in a tuple — against one real ternary over one
7919 // real `>`.
7920 //
7921 // Six type-syntax `?` means the numbers separate cleanly: 8 pre-fix,
7922 // 2 once the allowlist is aimed at `TernaryExpression`, 1 if it is
7923 // aimed at anything else (the ternary stops counting too), 0 if the
7924 // fixture stops parsing. Any partial gate — one that named some of
7925 // the type-syntax parents in a denylist instead — lands between 3
7926 // and 7 and is equally visible.
7927 #[cfg(feature = "typescript")]
7928 #[test]
7929 fn typescript_optional_type_syntax_is_not_a_condition() {
7930 check_metrics::<TypescriptParser>(
7931 "interface I { a?: string; m?(x: number): void; }
7932 abstract class K { f?: number; abstract g?(): void; }
7933 type Tup = [number, string?];
7934 function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
7935 "foo.ts",
7936 |metric| {
7937 assert_eq!(metric.abc.conditions_sum(), 2);
7938 },
7939 );
7940 }
7941
7942 // The explicit half of #1275's TypeScript decision: a conditional
7943 // type (`T extends U ? X : Y`) is resolved by the type checker and
7944 // erased before runtime, so its `?` is not a condition. That falls
7945 // out of the `TernaryExpression` allowlist rather than being named,
7946 // which is exactly why it needs its own test — the choice reads as
7947 // an omission otherwise, and nothing else here would notice if a
7948 // later edit added `ConditionalType` to the allowlist "for
7949 // symmetry".
7950 //
7951 // The real ternary below keeps the expectation off zero: 3 pre-fix,
7952 // 2 with the conditional type excluded, 1 if the ternary is
7953 // swallowed too.
7954 #[cfg(feature = "typescript")]
7955 #[test]
7956 fn typescript_conditional_type_is_not_a_condition() {
7957 check_metrics::<TypescriptParser>(
7958 "type Cond<T> = T extends string ? number : boolean;
7959 function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
7960 "foo.ts",
7961 |metric| {
7962 assert_eq!(metric.abc.conditions_sum(), 2);
7963 },
7964 );
7965 }
7966
7967 // grammar-dispatch §2. `Typescript::QMARK2` is the enum entry for
7968 // the external scanner's `_ternary_qmark` token, and
7969 // tree-sitter-typescript's public symbol map folds it back onto
7970 // `anon_sym_QMARK`, so `kind_id()` never reports it — every ternary
7971 // `?` in the fixture below arrives as plain `QMARK`. Listing
7972 // `QMARK2` in the gated arm would therefore be dead code, and
7973 // leaving it out is safe only for as long as that mapping holds.
7974 // This pins both halves so a grammar bump that starts exposing the
7975 // alias fails here rather than silently zeroing every TypeScript
7976 // ternary.
7977 //
7978 // The fixture's only `?` is the ternary's, deliberately. An optional
7979 // parameter (`b?: number`) would emit a `QMARK` of its own and
7980 // satisfy the positive assertion on its own, leaving it true no
7981 // matter what id the ternary's `?` came back as — decoration rather
7982 // than the non-vacuity guard it is here for.
7983 #[cfg(feature = "typescript")]
7984 #[test]
7985 fn typescript_ternary_qmark_alias_stays_unreachable() {
7986 let parser = TypescriptParser::new(
7987 "function f(a: boolean): number { return a ? 1 : 2; }\n"
7988 .as_bytes()
7989 .to_vec(),
7990 std::path::Path::new("foo.ts"),
7991 None,
7992 );
7993 assert!(ast_has_kind_id(&parser, Typescript::QMARK as u16));
7994 assert!(!ast_has_kind_id(&parser, Typescript::QMARK2 as u16));
7995 }
7996
7997 #[cfg(feature = "typescript")]
7998 #[test]
7999 fn typescript_abstract_class_abc() {
8000 // Abstract methods have no body — they contribute nothing.
8001 check_metrics::<TypescriptParser>(
8002 "abstract class C {
8003 abstract a(): void;
8004 m(x: number): number {
8005 if (x > 0) return 1; // +1 condition
8006 return 0;
8007 }
8008 }",
8009 "foo.ts",
8010 |metric| {
8011 assert_eq!(metric.abc.conditions_sum(), 1);
8012 assert_eq!(metric.abc.branches_sum(), 0);
8013 insta::assert_json_snapshot!(metric.abc);
8014 },
8015 );
8016 }
8017
8018 #[cfg(feature = "typescript")]
8019 #[test]
8020 fn typescript_interface_abc_zero() {
8021 check_metrics::<TypescriptParser>(
8022 "interface I {
8023 a(): void;
8024 b(): number;
8025 p: string;
8026 }",
8027 "foo.ts",
8028 |metric| {
8029 assert_eq!(metric.abc.assignments_sum(), 0);
8030 assert_eq!(metric.abc.branches_sum(), 0);
8031 assert_eq!(metric.abc.conditions_sum(), 0);
8032 insta::assert_json_snapshot!(metric.abc);
8033 },
8034 );
8035 }
8036
8037 #[cfg(feature = "typescript")]
8038 #[test]
8039 fn typescript_arrow_field_contributes_abc() {
8040 // Arrow function class members are function spaces; their
8041 // assignments/branches/conditions are counted.
8042 check_metrics::<TypescriptParser>(
8043 "class C {
8044 arrow = (x: number) => {
8045 if (x > 0) { // +1 condition
8046 return foo(); // +1 branch
8047 }
8048 return 0;
8049 };
8050 }",
8051 "foo.ts",
8052 |metric| {
8053 assert_eq!(metric.abc.conditions_sum(), 1);
8054 assert_eq!(metric.abc.branches_sum(), 1);
8055 insta::assert_json_snapshot!(metric.abc);
8056 },
8057 );
8058 }
8059
8060 #[cfg(feature = "typescript")]
8061 #[test]
8062 fn typescript_parameter_property_init_not_assignment() {
8063 // Parameter properties don't introduce a `=` token themselves;
8064 // only the explicit `let z = 0` body assignment is counted.
8065 // The class field initializer `f: number = 0` likewise has a `=`
8066 // that DOES count (matches `typescript_assignments_basic`).
8067 check_metrics::<TypescriptParser>(
8068 "class C {
8069 f: number = 0;
8070 constructor(public x: number, private y: string) {
8071 let z = 0;
8072 }
8073 }",
8074 "foo.ts",
8075 |metric| {
8076 // f's initializer + `let z = 0` = 2 assignments; the
8077 // parameter properties contribute zero.
8078 assert_eq!(metric.abc.assignments_sum(), 2);
8079 insta::assert_json_snapshot!(metric.abc);
8080 },
8081 );
8082 }
8083
8084 // TSX parity
8085
8086 #[cfg(feature = "typescript")]
8087 #[test]
8088 fn tsx_assignments_basic() {
8089 check_metrics::<TsxParser>(
8090 "class C {
8091 m(): void {
8092 let x = 0;
8093 x = 1;
8094 x += 2;
8095 x++;
8096 }
8097 }",
8098 "foo.tsx",
8099 |metric| {
8100 assert_eq!(metric.abc.assignments_sum(), 4);
8101 insta::assert_json_snapshot!(metric.abc);
8102 },
8103 );
8104 }
8105
8106 #[cfg(feature = "typescript")]
8107 #[test]
8108 fn tsx_const_excluded_from_assignments() {
8109 check_metrics::<TsxParser>(
8110 "class C {
8111 m(): void {
8112 const a = 1;
8113 let b = 2;
8114 }
8115 }",
8116 "foo.tsx",
8117 |metric| {
8118 assert_eq!(metric.abc.assignments_sum(), 1);
8119 insta::assert_json_snapshot!(metric.abc);
8120 },
8121 );
8122 }
8123
8124 #[cfg(feature = "typescript")]
8125 #[test]
8126 fn tsx_branches_function_calls() {
8127 check_metrics::<TsxParser>(
8128 "class C {
8129 m(): void {
8130 foo();
8131 new Date();
8132 }
8133 }",
8134 "foo.tsx",
8135 |metric| {
8136 assert_eq!(metric.abc.branches_sum(), 2);
8137 insta::assert_json_snapshot!(metric.abc);
8138 },
8139 );
8140 }
8141
8142 #[cfg(feature = "typescript")]
8143 #[test]
8144 fn tsx_conditions_comparison_operators() {
8145 check_metrics::<TsxParser>(
8146 "class C {
8147 m(x: number, y: number): boolean {
8148 return x == y || x < y || x >= y;
8149 }
8150 }",
8151 "foo.tsx",
8152 |metric| {
8153 assert_eq!(metric.abc.conditions_sum(), 3);
8154 insta::assert_json_snapshot!(metric.abc);
8155 },
8156 );
8157 }
8158
8159 #[cfg(feature = "typescript")]
8160 #[test]
8161 fn tsx_conditions_control_flow_arms() {
8162 check_metrics::<TsxParser>(
8163 "class C {
8164 m(x: number): number {
8165 try {
8166 if (x > 0) return 1;
8167 else return -1;
8168 } catch (e) {
8169 return 0;
8170 }
8171 }
8172 }",
8173 "foo.tsx",
8174 |metric| {
8175 assert_eq!(metric.abc.conditions_sum(), 4);
8176 insta::assert_json_snapshot!(metric.abc);
8177 },
8178 );
8179 }
8180
8181 #[cfg(feature = "typescript")]
8182 #[test]
8183 fn tsx_conditions_switch_case() {
8184 check_metrics::<TsxParser>(
8185 "class C {
8186 m(x: number): number {
8187 switch (x) {
8188 case 1: return 1; // +1
8189 case 2: return 2; // +1
8190 default: return 0; // +0 (fallthrough, #469)
8191 }
8192 }
8193 }",
8194 "foo.tsx",
8195 |metric| {
8196 assert_eq!(metric.abc.conditions_sum(), 2);
8197 insta::assert_json_snapshot!(metric.abc);
8198 },
8199 );
8200 }
8201
8202 #[cfg(feature = "typescript")]
8203 #[test]
8204 fn tsx_ternary_and_nullish() {
8205 check_metrics::<TsxParser>(
8206 "class C {
8207 m(x: number | null): number {
8208 return x !== null ? x : 0;
8209 }
8210 n(x: number | null): number { return x ?? 0; }
8211 }",
8212 "foo.tsx",
8213 |metric| {
8214 assert_eq!(metric.abc.conditions_sum(), 3);
8215 insta::assert_json_snapshot!(metric.abc);
8216 },
8217 );
8218 }
8219
8220 #[cfg(feature = "typescript")]
8221 #[test]
8222 fn tsx_instanceof_counts_as_condition() {
8223 check_metrics::<TsxParser>(
8224 "class C { m(o: unknown): boolean { return o instanceof C; } }",
8225 "foo.tsx",
8226 |metric| {
8227 assert_eq!(metric.abc.conditions_sum(), 1);
8228 insta::assert_json_snapshot!(metric.abc);
8229 },
8230 );
8231 }
8232
8233 #[cfg(feature = "typescript")]
8234 #[test]
8235 fn tsx_generic_lt_gt_not_a_condition() {
8236 check_metrics::<TsxParser>(
8237 "class C<T> { xs: Array<number> = []; }",
8238 "foo.tsx",
8239 |metric| {
8240 assert_eq!(metric.abc.conditions_sum(), 0);
8241 insta::assert_json_snapshot!(metric.abc);
8242 },
8243 );
8244 }
8245
8246 // #1297: a JSX tag delimiter is markup punctuation, not a
8247 // comparison. The `<` / `>` arm denied only `type_arguments` and
8248 // `type_parameters`, so every tag bracket scored a condition —
8249 // `jsx_opening_element` contributes both tokens, `jsx_closing_element`
8250 // a `>` (its `</` is one token) and `jsx_self_closing_element` a `<`
8251 // (its `/>` is one token).
8252 //
8253 // The fixture pairs seven such brackets — two elements' opening and
8254 // closing tags, plus one self-closing tag — against one genuine
8255 // `a < b`, so every way of mis-aiming the gate lands on its own
8256 // number: 8 pre-fix, 1 once it allows `BinaryExpression`, 0 if it
8257 // allows the wrong parent or the fixture stops parsing. Asserting 0
8258 // on a JSX-only body would not have separated those last two.
8259 #[cfg(feature = "typescript")]
8260 #[test]
8261 fn tsx_jsx_elements_are_not_conditions() {
8262 check_metrics::<TsxParser>(
8263 "function f(a: number, b: number) {
8264 if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
8265 return <br />;
8266 }",
8267 "foo.tsx",
8268 |metric| {
8269 assert_eq!(metric.abc.conditions_sum(), 1);
8270 // Fixture-presence anchor: the `className="x"` attribute
8271 // `=` is this fixture's only assignment, so the test
8272 // fails if a later edit drops the JSX. Without it the
8273 // surviving `a < b` alone also scores 1, and the
8274 // conditions assertion would pass while proving nothing.
8275 assert_eq!(metric.abc.assignments_sum(), 1);
8276 },
8277 );
8278 }
8279
8280 // #1275 in the second expansion of `ts_abc_compute!`. TSX shares
8281 // TypeScript's `?` productions and its own `TernaryExpression` /
8282 // `QMARK` ids, so the gate is a distinct instantiation and needs its
8283 // own fixture — a passing TypeScript test says nothing about the
8284 // macro's other expansion. Four type-syntax `?` plus one `>` and one
8285 // ternary: 6 pre-fix, 2 after, 1 if the allowlist is misaimed.
8286 #[cfg(feature = "typescript")]
8287 #[test]
8288 fn tsx_optional_type_syntax_is_not_a_condition() {
8289 check_metrics::<TsxParser>(
8290 "interface I { a?: string; m?(x: number): void; }
8291 class K { f?: number; }
8292 function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
8293 "foo.tsx",
8294 |metric| {
8295 assert_eq!(metric.abc.conditions_sum(), 2);
8296 },
8297 );
8298 }
8299
8300 // The TSX half of `typescript_conditional_type_is_not_a_condition`.
8301 // `ts_abc_compute!` expands twice and the two expansions are
8302 // independent code (grammar-dispatch §11): adding `ConditionalType`
8303 // to the allowlist "for symmetry" fails only the TypeScript test
8304 // without this one, which reads as the TSX expansion being fine
8305 // rather than untested. `conditional_type` is in the tsx grammar's
8306 // `?` set exactly as it is in typescript's.
8307 #[cfg(feature = "typescript")]
8308 #[test]
8309 fn tsx_conditional_type_is_not_a_condition() {
8310 check_metrics::<TsxParser>(
8311 "type Cond<T> = T extends string ? number : boolean;
8312 function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
8313 "foo.tsx",
8314 |metric| {
8315 assert_eq!(metric.abc.conditions_sum(), 2);
8316 },
8317 );
8318 }
8319
8320 // The TSX half of `typescript_ternary_qmark_alias_stays_unreachable`
8321 // — the tsx grammar declares the same `_ternary_qmark` external and
8322 // maps it back onto `anon_sym_QMARK` at its own id. Same
8323 // single-`?` fixture rule; see that test for why.
8324 #[cfg(feature = "typescript")]
8325 #[test]
8326 fn tsx_ternary_qmark_alias_stays_unreachable() {
8327 let parser = TsxParser::new(
8328 "function f(a: boolean): number { return a ? 1 : 2; }\n"
8329 .as_bytes()
8330 .to_vec(),
8331 std::path::Path::new("foo.tsx"),
8332 None,
8333 );
8334 assert!(ast_has_kind_id(&parser, Tsx::QMARK as u16));
8335 assert!(!ast_has_kind_id(&parser, Tsx::QMARK2 as u16));
8336 }
8337
8338 #[cfg(feature = "typescript")]
8339 #[test]
8340 fn tsx_abstract_class_abc() {
8341 check_metrics::<TsxParser>(
8342 "abstract class C {
8343 abstract a(): void;
8344 m(x: number): number {
8345 if (x > 0) return 1;
8346 return 0;
8347 }
8348 }",
8349 "foo.tsx",
8350 |metric| {
8351 assert_eq!(metric.abc.conditions_sum(), 1);
8352 assert_eq!(metric.abc.branches_sum(), 0);
8353 insta::assert_json_snapshot!(metric.abc);
8354 },
8355 );
8356 }
8357
8358 #[cfg(feature = "typescript")]
8359 #[test]
8360 fn tsx_interface_abc_zero() {
8361 check_metrics::<TsxParser>(
8362 "interface I { a(): void; p: string; }",
8363 "foo.tsx",
8364 |metric| {
8365 assert_eq!(metric.abc.assignments_sum(), 0);
8366 assert_eq!(metric.abc.branches_sum(), 0);
8367 assert_eq!(metric.abc.conditions_sum(), 0);
8368 insta::assert_json_snapshot!(metric.abc);
8369 },
8370 );
8371 }
8372
8373 #[cfg(feature = "typescript")]
8374 #[test]
8375 fn tsx_arrow_field_contributes_abc() {
8376 check_metrics::<TsxParser>(
8377 "class C {
8378 arrow = (x: number) => {
8379 if (x > 0) return foo();
8380 return 0;
8381 };
8382 }",
8383 "foo.tsx",
8384 |metric| {
8385 assert_eq!(metric.abc.conditions_sum(), 1);
8386 assert_eq!(metric.abc.branches_sum(), 1);
8387 insta::assert_json_snapshot!(metric.abc);
8388 },
8389 );
8390 }
8391
8392 #[cfg(feature = "typescript")]
8393 #[test]
8394 fn tsx_asi_const_does_not_suppress_later_assignments() {
8395 // TSX half of the #1277 cluster; see
8396 // `typescript_asi_const_does_not_suppress_later_assignments`.
8397 // TSX has its own `Const` / `VariableDeclarator` /
8398 // `LexicalDeclaration` kind ids, so the predicate is generated
8399 // separately and needs its own fixture.
8400 check_metrics::<TsxParser>(
8401 "function f() {
8402 const a = 1
8403 x = 2
8404 y = 3
8405 }",
8406 "foo.tsx",
8407 |metric| {
8408 assert_eq!(metric.abc.assignments_sum(), 2);
8409 },
8410 );
8411 }
8412
8413 #[cfg(feature = "typescript")]
8414 #[test]
8415 fn tsx_const_declarator_shapes_stay_suppressed() {
8416 // TSX half of `typescript_const_declarator_shapes_stay_suppressed`.
8417 check_metrics::<TsxParser>(
8418 "function f(o: any, xs: number[]) {
8419 const a = 1, b = 2
8420 const {c = 5, d: {e = 6} = {}} = o
8421 const [g = 7, ...[h = 8]] = xs
8422 let i = 3
8423 var j = 4
8424 for (const x of xs) { k(x) }
8425 }",
8426 "foo.tsx",
8427 |metric| {
8428 assert_eq!(metric.abc.assignments_sum(), 2);
8429 },
8430 );
8431 }
8432
8433 #[cfg(feature = "typescript")]
8434 #[test]
8435 fn tsx_parameter_property_init_not_assignment() {
8436 // Parameter properties contribute no `=`; the body's `let z = 0`
8437 // and the field initializer do.
8438 check_metrics::<TsxParser>(
8439 "class C {
8440 f: number = 0;
8441 constructor(public x: number) { let z = 0; }
8442 }",
8443 "foo.tsx",
8444 |metric| {
8445 assert_eq!(metric.abc.assignments_sum(), 2);
8446 insta::assert_json_snapshot!(metric.abc);
8447 },
8448 );
8449 }
8450
8451 // --- Ruby ABC tests ---------------------------------------------------
8452 //
8453 // Each Ruby `assignment` / `operator_assignment` is one assignment
8454 // regardless of whether the LHS is a local, instance, or class
8455 // variable. Every `call` / `super` / `yield` is one branch. Every
8456 // comparison-operator token inside a `binary` node plus each
8457 // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
8458 // one condition.
8459
8460 #[cfg(feature = "ruby")]
8461 #[test]
8462 fn ruby_zero_abc() {
8463 check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
8464 assert_eq!(metric.abc.assignments_sum(), 0);
8465 assert_eq!(metric.abc.branches_sum(), 0);
8466 assert_eq!(metric.abc.conditions_sum(), 0);
8467 insta::assert_json_snapshot!(metric.abc);
8468 });
8469 }
8470
8471 #[cfg(feature = "ruby")]
8472 #[test]
8473 fn ruby_simple_assignment() {
8474 check_metrics::<RubyParser>("def f\n a = 1\n b = 2\nend\n", "foo.rb", |metric| {
8475 assert_eq!(metric.abc.assignments_sum(), 2);
8476 assert_eq!(metric.abc.branches_sum(), 0);
8477 assert_eq!(metric.abc.conditions_sum(), 0);
8478 insta::assert_json_snapshot!(metric.abc);
8479 });
8480 }
8481
8482 #[cfg(feature = "ruby")]
8483 #[test]
8484 fn ruby_augmented_assignment() {
8485 // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
8486 // one assignment. Plain `=` to set the initial value adds one
8487 // more.
8488 check_metrics::<RubyParser>(
8489 "def f(x)\n a = 0\n a += x\n a -= 1\n a *= 2\nend\n",
8490 "foo.rb",
8491 |metric| {
8492 assert_eq!(metric.abc.assignments_sum(), 4);
8493 insta::assert_json_snapshot!(metric.abc);
8494 },
8495 );
8496 }
8497
8498 #[cfg(feature = "ruby")]
8499 #[test]
8500 fn ruby_logical_augmented_assignment() {
8501 // `||=` and `&&=` are also `operator_assignment` nodes.
8502 check_metrics::<RubyParser>("def f\n @x ||= 0\n @x &&= 1\nend\n", "foo.rb", |metric| {
8503 assert_eq!(metric.abc.assignments_sum(), 2);
8504 insta::assert_json_snapshot!(metric.abc);
8505 });
8506 }
8507
8508 #[cfg(feature = "ruby")]
8509 #[test]
8510 fn ruby_method_call_branch() {
8511 // Each method invocation is one branch.
8512 check_metrics::<RubyParser>(
8513 "def f(obj)\n foo()\n obj.bar(1)\nend\n",
8514 "foo.rb",
8515 |metric| {
8516 assert_eq!(metric.abc.branches_sum(), 2);
8517 insta::assert_json_snapshot!(metric.abc);
8518 },
8519 );
8520 }
8521
8522 #[cfg(feature = "ruby")]
8523 #[test]
8524 fn ruby_super_and_yield_branches() {
8525 // `super` and `yield` both count as branches (control-pass).
8526 check_metrics::<RubyParser>("def f\n super\n yield\nend\n", "foo.rb", |metric| {
8527 assert_eq!(metric.abc.branches_sum(), 2);
8528 assert_eq!(metric.abc.assignments_sum(), 0);
8529 insta::assert_json_snapshot!(metric.abc);
8530 });
8531 }
8532
8533 #[cfg(feature = "ruby")]
8534 #[test]
8535 fn ruby_attr_macro_is_branch() {
8536 // `attr_accessor` is a `Call3` node and registers as a branch
8537 // like any method invocation.
8538 check_metrics::<RubyParser>("class A\n attr_accessor :x\nend\n", "foo.rb", |metric| {
8539 assert_eq!(metric.abc.branches_sum(), 1);
8540 insta::assert_json_snapshot!(metric.abc);
8541 });
8542 }
8543
8544 #[cfg(feature = "ruby")]
8545 #[test]
8546 fn ruby_comparison_conditions() {
8547 // Each comparison operator is one condition.
8548 check_metrics::<RubyParser>(
8549 "def f(a, b)\n a == b\n a != b\n a < b\n a > b\n a <= b\n a >= b\nend\n",
8550 "foo.rb",
8551 |metric| {
8552 assert_eq!(metric.abc.conditions_sum(), 6);
8553 insta::assert_json_snapshot!(metric.abc);
8554 },
8555 );
8556 }
8557
8558 #[cfg(feature = "ruby")]
8559 #[test]
8560 fn ruby_superclass_clause_is_not_a_condition() {
8561 // Regression for #1280: a superclass clause spells its `<` with the
8562 // same `LT` token as a comparison, but parents it under
8563 // `superclass` rather than `binary`, so the parent gate excludes
8564 // it. Before the gate every subclass declaration scored a phantom
8565 // condition.
8566 // expected: 0 conditions — the file contains no conditional at all;
8567 // the single assignment is `x = 1`.
8568 check_metrics::<RubyParser>(
8569 "class Foo < Bar\n def plain\n x = 1\n end\nend\n",
8570 "foo.rb",
8571 |metric| {
8572 assert_eq!(metric.abc.conditions_sum(), 0);
8573 assert_eq!(metric.abc.assignments_sum(), 1);
8574 },
8575 );
8576 }
8577
8578 #[cfg(feature = "ruby")]
8579 #[test]
8580 fn ruby_operator_method_name_is_not_a_condition() {
8581 // The `<` naming an operator method parents under `operator`, which
8582 // the `binary` gate likewise excludes (#1280). The body carries a
8583 // real comparison so the expected value is not the all-zero default:
8584 // 1 discriminates "only the name token is excluded" from both "the
8585 // gate excludes everything" (0) and "nothing is gated" (2).
8586 // expected: 1 condition — the `@v < other` comparison, not the `def <`.
8587 check_metrics::<RubyParser>("def <(other)\n @v < other\nend\n", "foo.rb", |metric| {
8588 assert_eq!(metric.abc.conditions_sum(), 1);
8589 });
8590 }
8591
8592 #[cfg(feature = "ruby")]
8593 #[test]
8594 fn ruby_every_comparison_operator_method_name_is_not_a_condition() {
8595 // The sibling half of #1280. `<` is not special: every comparison
8596 // and equality token Ruby lets you `def` parents under `operator`
8597 // in that position, so gating only `LT` / `GT` left `def ==`,
8598 // `def <=`, `def >=`, `def <=>`, `def !=` and `def =~` each
8599 // scoring a phantom condition — measured at 1 apiece for a body
8600 // containing no conditional at all.
8601 // expected: 0 conditions per definition; only the name token is on
8602 // the line, so a single non-zero total localises the regression.
8603 check_metrics::<RubyParser>(
8604 "def ==(o)\n 1\nend\n\
8605 def !=(o)\n 1\nend\n\
8606 def <=(o)\n 1\nend\n\
8607 def >=(o)\n 1\nend\n\
8608 def <=>(o)\n 1\nend\n\
8609 def =~(o)\n 1\nend\n\
8610 def <(o)\n 1\nend\n\
8611 def >(o)\n 1\nend\n",
8612 "foo.rb",
8613 |metric| {
8614 assert_eq!(metric.abc.conditions_sum(), 0);
8615 },
8616 );
8617 // The positive control: the same tokens inside a `binary` are real
8618 // comparisons and still count, so the gate is not blanket
8619 // suppression.
8620 check_metrics::<RubyParser>(
8621 "def cmp(a, b)\n a == b || a <= b || a <=> b\nend\n",
8622 "foo.rb",
8623 |metric| {
8624 assert_eq!(metric.abc.conditions_sum(), 3);
8625 },
8626 );
8627 }
8628
8629 #[cfg(feature = "ruby")]
8630 #[test]
8631 fn ruby_case_match_in_arms_are_conditions() {
8632 // Regression for #977: each non-wildcard `case … in` arm is one
8633 // ABC condition, matching Python's `case_clause` handling. Using
8634 // literal patterns (no comparison operators) isolates the
8635 // `in_clause` contribution from any operand tokens.
8636 // expected: 2 conditions — one per `in 1` / `in 2` arm.
8637 check_metrics::<RubyParser>(
8638 "def f(x)\n case x\n in 1 then :one\n in 2 then :two\n end\nend\n",
8639 "foo.rb",
8640 |metric| {
8641 assert_eq!(metric.abc.conditions_sum(), 2);
8642 },
8643 );
8644 }
8645
8646 #[cfg(feature = "ruby")]
8647 #[test]
8648 fn ruby_case_match_guarded_wildcard_is_a_condition() {
8649 // Regression for #977: a guarded wildcard arm `in _ if x` is not a
8650 // bare default and counts as one ABC condition, while the trailing
8651 // bare `in _` adds none.
8652 // expected: 2 conditions — the guarded `in _ if x` arm, plus its
8653 // guard slot. The guard predicate is a bare identifier, so before
8654 // #1454 it contributed nothing and the total was 1; the slot is
8655 // what makes `if x`, `if x.even?` and `if x > 0` agree.
8656 check_metrics::<RubyParser>(
8657 "def f(x)\n case x\n in _ if x then :y\n in _ then :default\n end\nend\n",
8658 "foo.rb",
8659 |metric| {
8660 assert_eq!(metric.abc.conditions_sum(), 2);
8661 },
8662 );
8663 }
8664
8665 #[cfg(feature = "ruby")]
8666 #[test]
8667 fn ruby_case_match_bare_wildcard_is_not_a_condition() {
8668 // Regression for #977: a `case … in` whose only arm is the bare
8669 // wildcard `in _` (no guard) is the default arm and contributes no
8670 // ABC condition, keeping ABC and cyclomatic in lockstep on the
8671 // same construct.
8672 // expected: 0 conditions.
8673 check_metrics::<RubyParser>(
8674 "def f(x)\n case x\n in _ then :default\n end\nend\n",
8675 "foo.rb",
8676 |metric| {
8677 assert_eq!(metric.abc.conditions_sum(), 0);
8678 },
8679 );
8680 }
8681
8682 #[cfg(feature = "ruby")]
8683 #[test]
8684 fn ruby_bare_predicate_control_flow_counts_one_condition() {
8685 // Regression for #696: idiomatic Ruby bare predicates
8686 // (`if flag` / `while flag` / `unless flag` / `until flag`) each
8687 // count one unary condition, matching Rust / C# / PHP / Python. The
8688 // condition field is read for both block and modifier forms.
8689 //
8690 // expected: 8 conditions — four block forms (`if`/`unless`/`while`/
8691 // `until`) plus the same four as modifiers, one each.
8692 check_metrics::<RubyParser>(
8693 "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",
8694 "foo.rb",
8695 |metric| {
8696 assert_eq!(metric.abc.conditions_sum(), 8);
8697 },
8698 );
8699 }
8700
8701 #[cfg(feature = "ruby")]
8702 #[test]
8703 fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() {
8704 // `if a == b` counts only the `==` comparison (the condition field
8705 // is a `binary` node, adding nothing). `if a && b` counts the two
8706 // chain operands via the `&&` walker, again with the condition-field
8707 // arm adding nothing — so neither shape is double-counted (#696).
8708 //
8709 // expected: 3 — `==` (1) + the `a`,`b` operands of `&&` (2).
8710 check_metrics::<RubyParser>(
8711 "def f(a, b)\n if a == b\n x\n end\n if a && b\n y\n end\nend\n",
8712 "foo.rb",
8713 |metric| {
8714 assert_eq!(metric.abc.conditions_sum(), 3);
8715 },
8716 );
8717 }
8718
8719 #[cfg(feature = "ruby")]
8720 #[test]
8721 fn ruby_spaceship_and_case_equality() {
8722 // `<=>` and `===` are comparison operators (conditions).
8723 check_metrics::<RubyParser>(
8724 "def f(a, b)\n a <=> b\n a === b\nend\n",
8725 "foo.rb",
8726 |metric| {
8727 assert_eq!(metric.abc.conditions_sum(), 2);
8728 insta::assert_json_snapshot!(metric.abc);
8729 },
8730 );
8731 }
8732
8733 #[cfg(feature = "ruby")]
8734 #[test]
8735 fn ruby_ternary_condition() {
8736 // The `?` ternary marker is one condition; the inner `==` is
8737 // another.
8738 check_metrics::<RubyParser>("def f(x)\n x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
8739 assert_eq!(metric.abc.conditions_sum(), 2);
8740 insta::assert_json_snapshot!(metric.abc);
8741 });
8742 }
8743
8744 // Issue #1161. Ruby's ternary carried only the `?` token arm, so
8745 // `a ? !b : !c` scored 1 against the 4 that Java, C#, Groovy, the C
8746 // family, the JS family, PHP and Perl all report for the same
8747 // expression (#1102).
8748 //
8749 // Every expectation below is the value its C++ sibling
8750 // (`cpp_ternary_operand_slots_count_as_unary_conditions`) already
8751 // asserts for the same expression, so the two read as one table.
8752 #[cfg(feature = "ruby")]
8753 #[test]
8754 fn ruby_ternary_operand_slots_count_as_unary_conditions() {
8755 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
8756 check_metrics::<RubyParser>("def f\n x = a ? !b : !c\nend\n", "foo.rb", |metric| {
8757 assert_eq!(metric.abc.conditions_sum(), 4);
8758 });
8759 // No-double-count pin, and the assertion that catches the trap
8760 // this grammar sets: `-b` and `!b` are the SAME node kind
8761 // (`unary:284`), separated only by their operator. Scoring a
8762 // branch only when the peel proves it boolean — a `!` / `not`
8763 // layer, not the `unary` kind — is what keeps this at 2. An
8764 // implementation keying on `Unary` reads 3 here.
8765 // `?` (1) + `>` (1) = 2, unchanged by the fix: the parenthesised
8766 // condition unwraps to a comparison its own token already
8767 // counts, and neither branch is negated.
8768 check_metrics::<RubyParser>("def f\n x = (a > 0) ? b : -b\nend\n", "foo.rb", |metric| {
8769 assert_eq!(metric.abc.conditions_sum(), 2);
8770 });
8771 // Nested — Ruby needs the inner ternary parenthesised. Outer `?`
8772 // (1) + outer condition `a` (1) + inner `?` (1) + inner
8773 // condition `b` (1) = 4. The outer consequence unwraps to the
8774 // inner `conditional`, which proves nothing boolean and is no
8775 // further paren / `!` layer, so it adds nothing on its own; the
8776 // inner ternary is reached by the walk, not by descent.
8777 check_metrics::<RubyParser>(
8778 "def f\n x = a ? (b ? c : d) : e\nend\n",
8779 "foo.rb",
8780 |metric| {
8781 assert_eq!(metric.abc.conditions_sum(), 4);
8782 },
8783 );
8784 // A parenthesised condition, pinning the paren unwrap on the
8785 // condition slot: `(a)` is `parenthesized_statements`, which the
8786 // peel must look through.
8787 // `?` (1) + `(a)` (1) + `!b` (1) + `!c` (1) = 4.
8788 check_metrics::<RubyParser>("def f\n x = (a) ? !b : !c\nend\n", "foo.rb", |metric| {
8789 assert_eq!(metric.abc.conditions_sum(), 4);
8790 });
8791 // A negated condition takes the same fallback through the `!`
8792 // unwrap rather than the paren one. `?` (1) + `!a` (1) = 2.
8793 check_metrics::<RubyParser>("def f\n x = !a ? b : c\nend\n", "foo.rb", |metric| {
8794 assert_eq!(metric.abc.conditions_sum(), 2);
8795 });
8796 }
8797
8798 // The boolean-context seed must discriminate between the condition
8799 // slot and the two branch slots — not merely exist. None of the
8800 // fixtures above can tell the difference: their branches are either
8801 // `!`-unaries (which set the flag inside the unwrap loop regardless
8802 // of the seed) or kinds the loop breaks on before any terminal test.
8803 // A seed that returned `true` for every slot of a `Conditional`
8804 // leaves all five at their asserted values and fails nothing.
8805 //
8806 // A parenthesised *branch* is the input that separates them: the
8807 // unwrap reaches a bare terminal, so only the seed decides whether
8808 // it counts. `?` (1) + condition `a` (1) = 2 in both directions.
8809 #[cfg(feature = "ruby")]
8810 #[test]
8811 fn ruby_ternary_branch_operands_are_not_double_counted() {
8812 check_metrics::<RubyParser>("def f\n x = a ? (b) : c\nend\n", "foo.rb", |metric| {
8813 assert_eq!(metric.abc.conditions_sum(), 2);
8814 });
8815 check_metrics::<RubyParser>("def f\n x = a ? b : (c)\nend\n", "foo.rb", |metric| {
8816 assert_eq!(metric.abc.conditions_sum(), 2);
8817 });
8818 // The same pair with a comment before the operand. Comments are
8819 // tree-sitter `extras`, so they become the branch's previous
8820 // sibling — which is why the seed asks the grammar which child
8821 // is the `condition` field rather than testing that sibling for
8822 // `?` / `:` as the C family does. Under the token form both of
8823 // these read 3.
8824 check_metrics::<RubyParser>(
8825 "def f\n x = a ?\n # note\n (b) : c\nend\n",
8826 "foo.rb",
8827 |metric| {
8828 assert_eq!(metric.abc.conditions_sum(), 2);
8829 },
8830 );
8831 check_metrics::<RubyParser>(
8832 "def f\n x = a ? b :\n # note\n (c)\nend\n",
8833 "foo.rb",
8834 |metric| {
8835 assert_eq!(metric.abc.conditions_sum(), 2);
8836 },
8837 );
8838 }
8839
8840 #[cfg(feature = "ruby")]
8841 #[test]
8842 fn ruby_case_when_arms() {
8843 // Each `when` named clause and the `else` clause count as one
8844 // condition each; the `case` head and the implicit `then`
8845 // wrappers do not.
8846 check_metrics::<RubyParser>(
8847 "def f(x)\n case x\n when 1 then 'one'\n when 2 then 'two'\n else 'other'\n end\nend\n",
8848 "foo.rb",
8849 |metric| {
8850 // 2 `when` + 1 `else` = 3 conditions.
8851 assert_eq!(metric.abc.conditions_sum(), 3);
8852 insta::assert_json_snapshot!(metric.abc);
8853 },
8854 );
8855 }
8856
8857 #[cfg(feature = "ruby")]
8858 #[test]
8859 fn ruby_elsif_and_else() {
8860 // `elsif` and `else` named clauses are conditions; their inner
8861 // `then` wrappers are not.
8862 check_metrics::<RubyParser>(
8863 "def f(x)\n if x > 0\n 1\n elsif x < 0\n -1\n else\n 0\n end\nend\n",
8864 "foo.rb",
8865 |metric| {
8866 // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
8867 assert_eq!(metric.abc.conditions_sum(), 4);
8868 insta::assert_json_snapshot!(metric.abc);
8869 },
8870 );
8871 }
8872
8873 #[cfg(feature = "ruby")]
8874 #[test]
8875 fn ruby_rescue_clause_condition() {
8876 // The `rescue` named clause is one condition; the `rescue`
8877 // keyword token (`Rescue2`) is not counted on its own.
8878 // `do_it` without parens is an `identifier`, not a `call`, so
8879 // it contributes no branch. `handle(e)` is a `call` (1 branch).
8880 check_metrics::<RubyParser>(
8881 "def f\n begin\n do_it\n rescue StandardError => e\n handle(e)\n end\nend\n",
8882 "foo.rb",
8883 |metric| {
8884 assert_eq!(metric.abc.conditions_sum(), 1);
8885 assert_eq!(metric.abc.branches_sum(), 1);
8886 insta::assert_json_snapshot!(metric.abc);
8887 },
8888 );
8889 }
8890
8891 #[cfg(feature = "ruby")]
8892 #[test]
8893 fn ruby_class_complex_function() {
8894 // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
8895 check_metrics::<RubyParser>(
8896 "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",
8897 "foo.rb",
8898 |metric| {
8899 assert_eq!(metric.abc.assignments_sum(), 1);
8900 assert_eq!(metric.abc.branches_sum(), 1);
8901 // `>`(1) + `==`(1) = 2 conditions. `if` is not a
8902 // token; `&&` is `AMPAMP` and is not counted (see
8903 // the module-level `Stats` doc-comment for the
8904 // cross-language policy; #395, walker tracked in
8905 // #403).
8906 assert_eq!(metric.abc.conditions_sum(), 2);
8907 insta::assert_json_snapshot!(metric.abc);
8908 },
8909 );
8910 }
8911
8912 #[cfg(feature = "ruby")]
8913 #[test]
8914 fn ruby_unary_conditions_in_chain() {
8915 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
8916 // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
8917 // contribute one. Ruby's `if` keyword is not a condition token.
8918 // expected: 3 unary conditions (matches the Java byte-equivalent).
8919 check_metrics::<RubyParser>(
8920 "def f(a, b, c)\n if a && b || c\n puts \"x\"\n end\nend\n",
8921 "foo.rb",
8922 |metric| {
8923 assert_eq!(metric.abc.conditions_sum(), 3);
8924 },
8925 );
8926 }
8927
8928 #[cfg(feature = "ruby")]
8929 #[test]
8930 fn ruby_keyword_and_or_chain_counts_operands() {
8931 // The keyword forms `and` / `or` get the same Rule 9 treatment as
8932 // `&&` / `||`. expected: 3 unary conditions (a, b, c).
8933 check_metrics::<RubyParser>(
8934 "def f(a, b, c)\n if a and b or c\n puts \"x\"\n end\nend\n",
8935 "foo.rb",
8936 |metric| {
8937 assert_eq!(metric.abc.conditions_sum(), 3);
8938 },
8939 );
8940 }
8941
8942 #[cfg(feature = "ruby")]
8943 #[test]
8944 fn ruby_negated_operand_is_unary_condition() {
8945 // A `!`-negated operand unwraps the `unary` node to the inner
8946 // identifier. expected: 2 (`a` and the `!b` operand).
8947 check_metrics::<RubyParser>(
8948 "def f(a, b)\n if a && !b\n puts \"x\"\n end\nend\n",
8949 "foo.rb",
8950 |metric| {
8951 assert_eq!(metric.abc.conditions_sum(), 2);
8952 },
8953 );
8954 }
8955
8956 #[cfg(feature = "ruby")]
8957 #[test]
8958 fn ruby_comparison_operands_add_nothing() {
8959 // Isolation for Rule 9 (issue #557): when the `&&` operands are
8960 // themselves comparisons, the unary-condition walker must add
8961 // nothing on top of the two `>` comparisons already counted as
8962 // conditions — distinguishing the gap (bare boolean operands)
8963 // from ordinary relational conditions. Mirrors the Kotlin and
8964 // Elixir isolation tests. expected: 2 (the two `>` comparisons).
8965 check_metrics::<RubyParser>(
8966 "def f(x, y)\n if x > 0 && y > 0\n puts \"x\"\n end\nend\n",
8967 "foo.rb",
8968 |metric| {
8969 assert_eq!(metric.abc.conditions_sum(), 2);
8970 },
8971 );
8972 }
8973
8974 // The two anchors every #1453 fixture needs, coupled on purpose as
8975 // `assert_kotlin_class_members` couples them for #1421. `subjects` is
8976 // the load-bearing one: the whole distinction is whether a `case`
8977 // carries a `value` field, no node kind spells that, and a fixture
8978 // that grew a subject would score its clauses the pre-#1453 way with
8979 // every other row still satisfied. Each fixture is top-level `def`s,
8980 // so the members are the unit space's.
8981 #[cfg(feature = "ruby")]
8982 fn assert_ruby_case_members(
8983 src: &str,
8984 subjects: usize,
8985 kinds: &[(u16, usize, &str)],
8986 expected: &[(&str, u64, u64)],
8987 ) {
8988 let parser = RubyParser::new(
8989 src.as_bytes().to_vec(),
8990 std::path::Path::new("foo.rb"),
8991 None,
8992 );
8993 let found = parser
8994 .root()
8995 .preorder()
8996 .filter(|n| n.kind_id() == Ruby::Case && n.child_by_field_name("value").is_some())
8997 .count();
8998 assert_eq!(found, subjects, "`case` subjects — the fixture moved");
8999 assert_fixture_spells::<RubyParser>(src, "foo.rb", kinds);
9000 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9001 assert_members_score(&space, expected);
9002 });
9003 }
9004
9005 // #1453's headline. A subject-less `when` clause's condition is an
9006 // ordinary boolean expression, so a comparison in it is already an
9007 // ABC condition through the token arm; the clause added a blanket one
9008 // on top, and `gt` scored 3 against its `if` analogue `iff`'s 2.
9009 //
9010 // `bare` and `call` are the controls that isolate the defect: same
9011 // clause shape, no operator for any other arm to count, so the clause
9012 // must still pay for its decision — 2 and 1 before the fix and after.
9013 // A fix that moved them would be attacking the clause count, which
9014 // was never wrong.
9015 //
9016 // `gt`, `bare` and `iff` sit one above cyclomatic's decision count
9017 // because ABC counts the `else` (Fitzpatrick Rule 5) and cyclomatic
9018 // does not; every other row is at `conditions == cyclomatic - 1`.
9019 // `cmt` puts a comment where the subject would go: an extra is not the
9020 // `value` field, so the `case` stays subject-less.
9021 #[cfg(feature = "ruby")]
9022 #[test]
9023 fn ruby_subjectless_case_when_counts_its_condition_once() {
9024 let src = "def gt(x)
9025 case
9026 when x > 5 then 1
9027 else 0
9028 end
9029end
9030def iff(x)
9031 if x > 5 then 1 else 0 end
9032end
9033def two(x)
9034 case
9035 when x > 5 then 1
9036 when x < 0 then 2
9037 end
9038end
9039def ge(x)
9040 case
9041 when x >= 5 then 1
9042 end
9043end
9044def andd(a, b)
9045 case
9046 when a > 1 && b < 2 then 1
9047 end
9048end
9049def orr(a, b)
9050 case
9051 when a || b then 1
9052 end
9053end
9054def kw_or(a, b)
9055 case
9056 when a or b then 1
9057 end
9058end
9059def bare(b)
9060 case
9061 when b then 1
9062 else 0
9063 end
9064end
9065def call(x)
9066 case
9067 when x.even? then 1
9068 end
9069end
9070def cmt(x)
9071 case # no subject
9072 when x > 5 then 1
9073 end
9074end
9075";
9076 assert_ruby_case_members(
9077 src,
9078 0,
9079 &[
9080 (Ruby::When as u16, 10, "`when` clauses"),
9081 (Ruby::GT as u16, 5, "`>` comparisons"),
9082 (Ruby::AMPAMP as u16, 1, "`andd`'s `&&`"),
9083 (Ruby::PIPEPIPE as u16, 1, "`orr`'s `||`"),
9084 (Ruby::Or as u16, 1, "`kw_or`'s `or`"),
9085 (Ruby::Comment as u16, 1, "`cmt`'s comment"),
9086 ],
9087 &[
9088 // (member, abc.conditions, cyclomatic)
9089 // Was 3: the `>` and the clause, for one decision.
9090 ("gt", 2, 2),
9091 ("iff", 2, 2),
9092 // Was 4.
9093 ("two", 2, 3),
9094 // Was 2: `>=` reaches the same token arm.
9095 ("ge", 1, 2),
9096 // Was 3. The compound condition keeps both comparisons —
9097 // suppressing the operators instead would collapse it.
9098 ("andd", 2, 3),
9099 // The Rule 9 walker scores both operands of a bare chain,
9100 // in both spellings; the clause adds nothing on top.
9101 ("orr", 2, 3),
9102 ("kw_or", 2, 3),
9103 ("bare", 2, 2),
9104 ("call", 1, 2),
9105 // Was 2.
9106 ("cmt", 1, 2),
9107 ],
9108 );
9109 }
9110
9111 // Wrappers and the shapes no other arm owns. `not_cmp`, `paren_cmp`,
9112 // `tern` and `test_pat` peel to an expression another arm already
9113 // scored, and each read one above parity before the fix. `paren` and
9114 // `notted` peel to a bare operand nothing else counts, so the clause
9115 // pays exactly as an `if` predicate would.
9116 //
9117 // `scope`, `neg`, `splat` and `arith` are why the clause asks "is this
9118 // already paid for" rather than "is this a known terminal": the `if`
9119 // slot asked the second question and scored each of these 0 until
9120 // #1520 moved it onto the clause's question — see
9121 // `ruby_condition_slot_pays_what_no_other_arm_counted`.
9122 #[cfg(feature = "ruby")]
9123 #[test]
9124 fn ruby_subjectless_case_when_condition_wrappers_count_once() {
9125 let src = "def paren(b)
9126 case
9127 when (b) then 1
9128 end
9129end
9130def notted(b)
9131 case
9132 when !b then 1
9133 end
9134end
9135def not_cmp(x)
9136 case
9137 when not(x > 1) then 1
9138 end
9139end
9140def paren_cmp(x)
9141 case
9142 when (x > 1) then 1
9143 end
9144end
9145def tern(x, a, b)
9146 case
9147 when x ? a : b then 1
9148 end
9149end
9150def test_pat(x)
9151 case
9152 when (x in Integer) then 1
9153 end
9154end
9155def scope
9156 case
9157 when Foo::Bar then 1
9158 end
9159end
9160def neg(x)
9161 case
9162 when -x then 1
9163 end
9164end
9165def splat(xs)
9166 case
9167 when *xs then 1
9168 end
9169end
9170def arith(x)
9171 case
9172 when x + 1 then 1
9173 end
9174end
9175";
9176 assert_ruby_case_members(
9177 src,
9178 0,
9179 &[
9180 (Ruby::When as u16, 10, "`when` clauses"),
9181 (
9182 Ruby::ParenthesizedStatements as u16,
9183 4,
9184 "parenthesised operands",
9185 ),
9186 (Ruby::Conditional as u16, 1, "`tern`'s ternary"),
9187 (Ruby::TestPattern as u16, 1, "`test_pat`'s pattern test"),
9188 (Ruby::ScopeResolution2 as u16, 1, "`scope`'s `Foo::Bar`"),
9189 (Ruby::SplatArgument as u16, 1, "`splat`'s `*xs`"),
9190 ],
9191 &[
9192 ("paren", 1, 2),
9193 ("notted", 1, 2),
9194 // Each was 2.
9195 ("not_cmp", 1, 2),
9196 ("paren_cmp", 1, 2),
9197 ("test_pat", 1, 2),
9198 // Was 3. The ternary's own `?` and its condition `x` are
9199 // its two conditions, as in `if x ? a : b`.
9200 ("tern", 2, 3),
9201 ("scope", 1, 2),
9202 ("neg", 1, 2),
9203 ("splat", 1, 2),
9204 ("arith", 1, 2),
9205 ],
9206 );
9207 }
9208
9209 // A clause may list several patterns: an implicit `||`, so each is
9210 // one operand of that chain and scores once unless its own arm already
9211 // counted it — every row here must equal its `if … || …` twin in the
9212 // `or_*` defs. `bare_first` and `cmp_first` differ only in pattern
9213 // order and must agree; reading the first pattern alone, as Kotlin's
9214 // `kotlin_count_when_entry` does, would score them 2 and 1. None can
9215 // reach `conditions == cyclomatic - 1`: cyclomatic scores the clause
9216 // as one decision, and the `||` twins sit just as far above it.
9217 #[cfg(feature = "ruby")]
9218 #[test]
9219 fn ruby_subjectless_case_when_multiple_patterns() {
9220 let src = "def bare_alts(a, b)
9221 case
9222 when a, b then 1
9223 end
9224end
9225def alts(x)
9226 case
9227 when x > 1, x < -1 then 1
9228 end
9229end
9230def bare_first(a, x)
9231 case
9232 when a, x > 1 then 1
9233 end
9234end
9235def cmp_first(a, x)
9236 case
9237 when x > 1, a then 1
9238 end
9239end
9240def or_bare(a, b)
9241 if a || b then 1 end
9242end
9243def or_cmp(x)
9244 if x > 1 || x < -1 then 1 end
9245end
9246def or_mixed(a, x)
9247 if a || x > 1 then 1 end
9248end
9249";
9250 assert_ruby_case_members(
9251 src,
9252 0,
9253 &[
9254 (Ruby::When as u16, 4, "`when` clauses"),
9255 (Ruby::Pattern as u16, 8, "two patterns per clause"),
9256 ],
9257 &[
9258 // Was 1: one pattern went unscored.
9259 ("bare_alts", 2, 2),
9260 ("or_bare", 2, 3),
9261 // Was 3.
9262 ("alts", 2, 2),
9263 ("or_cmp", 2, 3),
9264 ("bare_first", 2, 2),
9265 ("cmp_first", 2, 2),
9266 ("or_mixed", 2, 3),
9267 ],
9268 );
9269 }
9270
9271 // An `elsif` is Java's `else if`: one condition for the `else`
9272 // (Rule 5) plus an `if` predicate slot. It paid only the `else`, so
9273 // `eb`, `eself`, `eneg` and `eparen` scored 2 where
9274 // `if (a) {} else if (b) {}` scores 3, while `ecmp` matched only
9275 // because its `>` arm paid the predicate. Each row now equals its Java
9276 // twin (`eelse` 4 / 3: Rule 5 counts the `else` arm, cyclomatic
9277 // does not), and `ecmp` / `eand` must not move, or the slot is paying
9278 // on top of the operator arms (#1520).
9279 #[cfg(feature = "ruby")]
9280 #[test]
9281 fn ruby_elsif_predicate_is_a_condition_slot() {
9282 let src = "def eb(a, b)
9283 if a then 1 elsif b then 2 end
9284end
9285def ecmp(a, x)
9286 if a then 1 elsif x > 0 then 2 end
9287end
9288def eself(a)
9289 if a then 1 elsif self then 2 end
9290end
9291def eneg(a, b)
9292 if a then 1 elsif !b then 2 end
9293end
9294def eparen(a, b)
9295 if a then 1 elsif (b) then 2 end
9296end
9297def eand(a, b, c)
9298 if a then 1 elsif b && c then 2 end
9299end
9300def eelse(a, b)
9301 if a then 1 elsif b then 2 else 3 end
9302end
9303";
9304 assert_fixture_spells::<RubyParser>(
9305 src,
9306 "foo.rb",
9307 &[
9308 (Ruby::Elsif as u16, 7, "one `elsif` per member"),
9309 (Ruby::Else as u16, 1, "`eelse`'s `else`"),
9310 ],
9311 );
9312 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9313 assert_members_score(
9314 &space,
9315 &[
9316 // (member, abc.conditions, cyclomatic)
9317 // Was 2.
9318 ("eb", 3, 3),
9319 ("ecmp", 3, 3),
9320 // Was 2.
9321 ("eself", 3, 3),
9322 // Was 2.
9323 ("eneg", 3, 3),
9324 // Was 2.
9325 ("eparen", 3, 3),
9326 ("eand", 4, 4),
9327 // Was 3.
9328 ("eelse", 4, 3),
9329 ],
9330 );
9331 });
9332 }
9333
9334 // #1520: every Ruby condition slot — block and modifier `if` /
9335 // `unless` / `while` / `until`, a `case … in` guard, a ternary
9336 // condition — pays one condition unless another arm already charged
9337 // the same decision. The slot used to pay only for a predicate that
9338 // peeled down to a known boolean terminal, so the first block below
9339 // scored 0 for one decision each, while the same expression as a
9340 // subject-less `when` scored 1 (`ruby_subjectless_case_when_condition_
9341 // wrappers_count_once`). Every row sits at `conditions == cyclomatic
9342 // - 1` except `tern` and `ifelse`, which agree with each other one
9343 // above it: ABC counts the `?` / `else` arm and cyclomatic does not.
9344 // `chain` and `chain_neg` sit level with each other, as a Rule 9
9345 // chain scores one per operand.
9346 //
9347 // The second block must not move, or the slot is paying twice: each
9348 // predicate's comparison or chain operand is already counted by its
9349 // own arm. `begin_cmp` and `assign_cmp` are why the peel looks through
9350 // `begin … end` and an assignment's value — without that the slot
9351 // would see a non-terminal and pay on top of the `>`. `chain_neg`'s
9352 // operands are slots of their own: the Rule 9 walker pays for `-a`
9353 // and `-b` as it does for `a` and `b`, and the `if` adds nothing
9354 // (#1529; before it, the walker skipped both and the `if` paid one).
9355 #[cfg(feature = "ruby")]
9356 #[test]
9357 fn ruby_condition_slot_pays_what_no_other_arm_counted() {
9358 let src = "def scope
9359 if Foo::Bar then 1 end
9360end
9361def zelf
9362 if self then 1 end
9363end
9364def neg(x)
9365 if -x then 1 end
9366end
9367def defd(x)
9368 if defined?(x) then 1 end
9369end
9370def assign(x)
9371 if (y = x) then y end
9372end
9373def arith(x)
9374 if x + 1 then 1 end
9375end
9376def blk(b)
9377 g if begin b end
9378end
9379def unl
9380 g unless Foo::Bar
9381end
9382def til(x)
9383 g until -x
9384end
9385def whl
9386 while self do g end
9387end
9388def guard(x)
9389 case x
9390 in [y] if -y then 1
9391 end
9392end
9393def tern(x)
9394 -x ? 1 : 2
9395end
9396def ifelse(x)
9397 if -x then 1 else 2 end
9398end
9399def cmp(x)
9400 if x > 1 then 1 end
9401end
9402def begin_cmp(x)
9403 if begin x > 1 end then 1 end
9404end
9405def assign_cmp(x)
9406 if (y = x > 1) then y end
9407end
9408def notted(x)
9409 if !x then 1 end
9410end
9411def chain(a, b)
9412 if a && b then 1 end
9413end
9414def chain_neg(a, b)
9415 if -a && -b then 1 end
9416end
9417";
9418 assert_fixture_spells::<RubyParser>(
9419 src,
9420 "foo.rb",
9421 &[
9422 (Ruby::ScopeResolution2 as u16, 2, "`Foo::Bar`"),
9423 (Ruby::Zelf as u16, 2, "`self`"),
9424 (Ruby::Begin as u16, 2, "`begin … end`"),
9425 (Ruby::Assignment as u16, 2, "parenthesised assignments"),
9426 (Ruby::Conditional as u16, 1, "`tern`'s ternary"),
9427 (Ruby::IfGuard as u16, 1, "`guard`'s guard"),
9428 ],
9429 );
9430 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9431 assert_members_score(
9432 &space,
9433 &[
9434 // (member, abc.conditions, cyclomatic); each of the
9435 // first thirteen was one lower.
9436 ("scope", 1, 2),
9437 ("zelf", 1, 2),
9438 ("neg", 1, 2),
9439 ("defd", 1, 2),
9440 ("assign", 1, 2),
9441 ("arith", 1, 2),
9442 ("blk", 1, 2),
9443 ("unl", 1, 2),
9444 ("til", 1, 2),
9445 ("whl", 1, 2),
9446 ("guard", 2, 3),
9447 ("tern", 2, 2),
9448 ("ifelse", 2, 2),
9449 ("cmp", 1, 2),
9450 ("begin_cmp", 1, 2),
9451 ("assign_cmp", 1, 2),
9452 ("notted", 1, 2),
9453 ("chain", 2, 3),
9454 // Was 0, then 1 until #1529.
9455 ("chain_neg", 2, 3),
9456 ],
9457 );
9458 });
9459 }
9460
9461 // The other half of #1453: a subject-ful clause must not move. It
9462 // lists a pattern matched with `pattern === subject`, a comparison
9463 // written nowhere, so the clause itself pays for the decision.
9464 // `subj_cmp` is the subject-ful counterpart of `gt` above and keeps
9465 // the clause's count *and* its `>`: `y > 5` is a value compared
9466 // against `x`, two comparisons for one branch. `in_arms` (the
9467 // `InClause` arm), `unl` and `elsif_` share no code with `When` and
9468 // are here to pin that the fix left them alone.
9469 #[cfg(feature = "ruby")]
9470 #[test]
9471 fn ruby_subjectful_case_when_keeps_the_per_clause_count() {
9472 let src = "def konst(x)
9473 case x
9474 when 1 then 1
9475 when 2 then 2
9476 else 0
9477 end
9478end
9479def subj_alts(x)
9480 case x
9481 when 1, 2 then 1
9482 else 0
9483 end
9484end
9485def subj_cmp(x, y)
9486 case x
9487 when y > 5 then 1
9488 end
9489end
9490def in_arms(x)
9491 case x
9492 in Integer then 1
9493 in String then 2
9494 else 0
9495 end
9496end
9497def unl(x)
9498 unless x > 5 then 1 else 0 end
9499end
9500def elsif_(x)
9501 if x > 5 then 1 elsif x < 0 then 2 else 0 end
9502end
9503";
9504 // Three: `in_arms` is a `case_match`, a separate production.
9505 assert_ruby_case_members(
9506 src,
9507 3,
9508 &[
9509 (Ruby::CaseMatch as u16, 1, "`in_arms`'s `case`"),
9510 (Ruby::When as u16, 4, "`when` clauses"),
9511 (Ruby::InClause as u16, 2, "`in_arms`'s clauses"),
9512 (Ruby::GT as u16, 3, "`>` comparisons"),
9513 ],
9514 &[
9515 ("konst", 3, 3),
9516 ("subj_alts", 2, 2),
9517 ("subj_cmp", 2, 2),
9518 ("in_arms", 3, 3),
9519 ("unl", 2, 2),
9520 ("elsif_", 4, 3),
9521 ],
9522 );
9523 }
9524
9525 // ---------------------------------------------------------------
9526 // Default-impl placeholder smoke tests (audited in #188).
9527 //
9528 // These tests assert that the *current* default-impl languages
9529 // return ABC = 0/0/0 for source that DOES contain branches,
9530 // conditions, and assignments. When the real impl lands for any
9531 // of these languages, the corresponding assertion below will fire
9532 // — the implementer must update the expected values, which is the
9533 // gate. Tag the follow-up issue in each test.
9534 // ---------------------------------------------------------------
9535
9536 // --- Python ABC ---------------------------------------------------
9537
9538 #[cfg(feature = "python")]
9539 #[test]
9540 fn python_empty_module_zero() {
9541 check_metrics::<PythonParser>("", "empty.py", |metric| {
9542 assert_eq!(metric.abc.assignments_sum(), 0);
9543 assert_eq!(metric.abc.branches_sum(), 0);
9544 assert_eq!(metric.abc.conditions_sum(), 0);
9545 insta::assert_json_snapshot!(metric.abc);
9546 });
9547 }
9548
9549 #[cfg(feature = "python")]
9550 #[test]
9551 fn python_plain_assignments_count() {
9552 // Three plain `=` assignments → A=3. No branches, no conditions.
9553 check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
9554 assert_eq!(metric.abc.assignments_sum(), 3);
9555 assert_eq!(metric.abc.branches_sum(), 0);
9556 assert_eq!(metric.abc.conditions_sum(), 0);
9557 insta::assert_json_snapshot!(metric.abc);
9558 });
9559 }
9560
9561 #[cfg(feature = "python")]
9562 #[test]
9563 fn python_typed_assignment_counts_bare_annotation_does_not() {
9564 // `x: int = 1` carries an `=`, so it counts.
9565 // `y: int` is a bare annotation (no `=`) — declares a type but
9566 // binds nothing; it must NOT inflate the assignment count.
9567 check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
9568 assert_eq!(metric.abc.assignments_sum(), 1);
9569 insta::assert_json_snapshot!(metric.abc);
9570 });
9571 }
9572
9573 #[cfg(feature = "python")]
9574 #[test]
9575 fn python_augmented_assignments_count() {
9576 // Each augmented op counts once.
9577 check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
9578 // 1 plain `=` + 3 augmented = 4 assignments.
9579 assert_eq!(metric.abc.assignments_sum(), 4);
9580 insta::assert_json_snapshot!(metric.abc);
9581 });
9582 }
9583
9584 #[cfg(feature = "python")]
9585 #[test]
9586 fn python_walrus_counts_as_assignment() {
9587 // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
9588 // → one assignment under Fitzpatrick's rule.
9589 check_metrics::<PythonParser>("if (n := 10) > 5:\n pass\n", "foo.py", |metric| {
9590 // 1 assignment (walrus) + 1 condition (`> 5` is a
9591 // ComparisonOperator).
9592 assert_eq!(metric.abc.assignments_sum(), 1);
9593 assert_eq!(metric.abc.conditions_sum(), 1);
9594 insta::assert_json_snapshot!(metric.abc);
9595 });
9596 }
9597
9598 #[cfg(feature = "python")]
9599 #[test]
9600 fn python_calls_are_branches() {
9601 // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
9602 // → three branches.
9603 check_metrics::<PythonParser>(
9604 "def foo():\n pass\ndef bar():\n pass\nclass Baz:\n pass\nfoo()\nbar()\nBaz()\n",
9605 "foo.py",
9606 |metric| {
9607 assert_eq!(metric.abc.branches_sum(), 3);
9608 assert_eq!(metric.abc.assignments_sum(), 0);
9609 insta::assert_json_snapshot!(metric.abc);
9610 },
9611 );
9612 }
9613
9614 #[cfg(feature = "python")]
9615 #[test]
9616 fn python_comparisons_count_conditions() {
9617 // `x > 0`, `x == y`, `x is None` are each a single
9618 // `ComparisonOperator` node — three conditions.
9619 check_metrics::<PythonParser>(
9620 "def f(x, y):\n a = x > 0\n b = x == y\n c = x is None\n",
9621 "foo.py",
9622 |metric| {
9623 assert_eq!(metric.abc.conditions_sum(), 3);
9624 // 3 plain assignments; the comparisons are operands.
9625 assert_eq!(metric.abc.assignments_sum(), 3);
9626 insta::assert_json_snapshot!(metric.abc);
9627 },
9628 );
9629 }
9630
9631 #[cfg(feature = "python")]
9632 #[test]
9633 fn python_chained_comparison_counts_once() {
9634 // tree-sitter-python collapses `0 < x < 10` into a single
9635 // `ComparisonOperator` — one condition, not two.
9636 check_metrics::<PythonParser>("def f(x):\n return 0 < x < 10\n", "foo.py", |metric| {
9637 assert_eq!(metric.abc.conditions_sum(), 1);
9638 insta::assert_json_snapshot!(metric.abc);
9639 });
9640 }
9641
9642 #[cfg(feature = "python")]
9643 #[test]
9644 fn python_number_truthy_condition_counts() {
9645 // Regression for #772: Python treats every non-zero number as
9646 // truthy, so `if 5:` and `x and 5` should each count their
9647 // numeric literal as a Fitzpatrick unary condition. Pre-fix
9648 // the former `python_bool_terminal_kinds!()` listed `True` /
9649 // `False` but omitted `Integer` / `Float`, so the walker dropped every
9650 // numeric-truthy operand (mirrors the Lua `Number` fix).
9651 check_metrics::<PythonParser>(
9652 "def f(a):\n if 5:\n pass\n return a and 2\n",
9653 "foo.py",
9654 |metric| {
9655 // `if 5:` → walker counts the Integer literal (+1).
9656 // `a and 2` → `and` walker counts both operands:
9657 // identifier `a` (+1), Integer `2` (+1).
9658 // Total: 3.
9659 assert_eq!(metric.abc.conditions_sum(), 3);
9660 insta::assert_json_snapshot!(metric.abc);
9661 },
9662 );
9663 }
9664
9665 #[cfg(feature = "python")]
9666 #[test]
9667 fn python_boolean_operators_not_counted_directly() {
9668 // Python's `and` / `or` are not counted as conditions on
9669 // their own (Fitzpatrick Rule 5; #395). Each operand is
9670 // instead counted as a unary conditional by the walker
9671 // (Rule 9; #403). `if a and b or c:` parses left-to-right
9672 // with `or` lower precedence: `(a and b) or c`. Walker
9673 // tallies: inner `and` counts `a`, `b` (+2); outer `or`
9674 // counts only the new outer operand `c` (+1; the inner
9675 // `(a and b)` BooleanOperator is not a terminal). Total
9676 // C = 3.
9677 check_metrics::<PythonParser>(
9678 "def f(a, b, c):\n if a and b or c:\n pass\n",
9679 "foo.py",
9680 |metric| {
9681 assert_eq!(metric.abc.conditions_sum(), 3);
9682 insta::assert_json_snapshot!(metric.abc);
9683 },
9684 );
9685 }
9686
9687 /// Python's unary `not` operator parses as `NotOperator` and now
9688 /// counts as one condition, matching Java's `!x` rule. Closes
9689 /// the parity gap noted in #214: without this, `if not flag:`
9690 /// reported 0 conditions while the Java equivalent reports 1.
9691 #[cfg(feature = "python")]
9692 #[test]
9693 fn python_unary_not_counts_as_condition() {
9694 check_metrics::<PythonParser>(
9695 "def f(flag):\n if not flag:\n return 1\n return 0\n",
9696 "foo.py",
9697 |metric| {
9698 // One `NotOperator` -> 1 condition. The `if` itself
9699 // is structural and doesn't add an Abc condition.
9700 assert_eq!(metric.abc.conditions_sum(), 1);
9701 insta::assert_json_snapshot!(metric.abc);
9702 },
9703 );
9704 }
9705
9706 /// `return not flag` — the unary `not` is the entire return
9707 /// expression. Without `NotOperator` counted, this reports zero
9708 /// conditions; with it, one. Java's `return !flag;` is one.
9709 #[cfg(feature = "python")]
9710 #[test]
9711 fn python_return_unary_not_counts() {
9712 check_metrics::<PythonParser>("def f(flag):\n return not flag\n", "foo.py", |metric| {
9713 assert_eq!(metric.abc.conditions_sum(), 1);
9714 insta::assert_json_snapshot!(metric.abc);
9715 });
9716 }
9717
9718 /// `foo(not ready, value)` — the unary `not` inside an argument
9719 /// list still contributes. Mirrors Java's `java_count_negated`
9720 /// walk over argument lists.
9721 #[cfg(feature = "python")]
9722 #[test]
9723 fn python_unary_not_in_argument_list_counts() {
9724 check_metrics::<PythonParser>(
9725 "def f(ready, value):\n log(not ready, value)\n",
9726 "foo.py",
9727 |metric| {
9728 // 1 Call (log) -> 1 branch.
9729 // 1 NotOperator (not ready) -> 1 condition.
9730 assert_eq!(metric.abc.branches_sum(), 1);
9731 assert_eq!(metric.abc.conditions_sum(), 1);
9732 insta::assert_json_snapshot!(metric.abc);
9733 },
9734 );
9735 }
9736
9737 /// A negated comparison is one condition. `not (x > 0)` parses as
9738 /// `NotOperator(ParenthesizedExpression(ComparisonOperator))`; the
9739 /// comparison's arm pays and the `not` adds nothing, as Java's
9740 /// `!(x > 0)` scores 1 (#1526). This test asserted 2 on the false
9741 /// premise that Java scores 2.
9742 #[cfg(feature = "python")]
9743 #[test]
9744 fn python_unary_not_with_comparison_counts_each_once() {
9745 check_metrics::<PythonParser>(
9746 "def f(x):\n if not (x > 0):\n return 1\n return 0\n",
9747 "foo.py",
9748 |metric| {
9749 // ComparisonOperator (1); the `not` negates a counted
9750 // comparison and adds nothing.
9751 assert_eq!(metric.abc.conditions_sum(), 1);
9752 insta::assert_json_snapshot!(metric.abc);
9753 },
9754 );
9755 }
9756
9757 /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
9758 /// y)`. The `and` itself is NOT counted (Fitzpatrick Rule 5
9759 /// lists only comparison operators); the `NotOperator` is
9760 /// counted at the top level (Rule 7); and the `y` operand is
9761 /// counted by the Rule 9 walker (issue #403). Total: 2.
9762 /// `NotOperator` is intentionally not walked-into a second
9763 /// time — the walker skips it to avoid double-counting.
9764 #[cfg(feature = "python")]
9765 #[test]
9766 fn python_unary_not_with_boolean_combinator_counts_each() {
9767 check_metrics::<PythonParser>(
9768 "def f(x, y):\n if not x and y:\n return 1\n return 0\n",
9769 "foo.py",
9770 |metric| {
9771 // NotOperator (1) + walker on `and` finds `y` (1) = 2.
9772 assert_eq!(metric.abc.conditions_sum(), 2);
9773 insta::assert_json_snapshot!(metric.abc);
9774 },
9775 );
9776 }
9777
9778 #[cfg(feature = "python")]
9779 #[test]
9780 fn python_control_flow_arms_count_conditions() {
9781 // `elif`, `else`, `except`, `finally`, `case` each contribute
9782 // one condition. The comparisons in the `if`/`elif`/`while`
9783 // headers contribute their own ComparisonOperator counts.
9784 check_metrics::<PythonParser>(
9785 "def f(x):\n if x > 0:\n a = 1\n elif x > -1:\n a = 2\n else:\n a = 3\n",
9786 "foo.py",
9787 |metric| {
9788 // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
9789 // ElifClause + 1 ElseClause = 4 conditions.
9790 assert_eq!(metric.abc.conditions_sum(), 4);
9791 insta::assert_json_snapshot!(metric.abc);
9792 },
9793 );
9794 }
9795
9796 #[cfg(feature = "python")]
9797 #[test]
9798 fn python_ternary_counts_as_condition() {
9799 // `a if c else b` is `ConditionalExpression` → 1 condition.
9800 // `c > 0` adds 1 more (ComparisonOperator).
9801 check_metrics::<PythonParser>(
9802 "def f(c):\n return 1 if c > 0 else 0\n",
9803 "foo.py",
9804 |metric| {
9805 assert_eq!(metric.abc.conditions_sum(), 2);
9806 insta::assert_json_snapshot!(metric.abc);
9807 },
9808 );
9809 }
9810
9811 // Issue #1161. Python counted the `conditional_expression` node but
9812 // never its condition slot, so `a if c() else b` reported 1 where
9813 // the equivalent `c() ? a : b` reports 2 everywhere else — and
9814 // the former `python_inspect_container`'s `ConditionalExpression`
9815 // boolean-context seed was unreachable, no call site having passed
9816 // that parent.
9817 #[cfg(feature = "python")]
9818 #[test]
9819 fn python_ternary_condition_slot_counts_as_a_unary_condition() {
9820 // ternary (1) + condition `c()` (1) = 2. `c()` is a `Call`, a
9821 // boolean terminal; it also adds one *branch*, not a condition.
9822 check_metrics::<PythonParser>(
9823 "def f(a, b, c):\n return a if c() else b\n",
9824 "foo.py",
9825 |metric| {
9826 assert_eq!(metric.abc.conditions_sum(), 2);
9827 },
9828 );
9829 // A parenthesised condition: `(c)` is a `parenthesized_expression`,
9830 // so only the peel (`python_wrapper_operand`) can resolve it to
9831 // `c`, which the slot then pays for.
9832 // ternary (1) + `(c)` (1) = 2.
9833 check_metrics::<PythonParser>(
9834 "def f(a, b, c):\n return a if (c) else b\n",
9835 "foo.py",
9836 |metric| {
9837 assert_eq!(metric.abc.conditions_sum(), 2);
9838 },
9839 );
9840 // A negated condition is *not* counted by the new slot: it is a
9841 // `NotOperator`, which already has its own top-level dispatcher
9842 // arm. ternary (1) + `not c` (1) = 2, not 3.
9843 check_metrics::<PythonParser>(
9844 "def f(a, b, c):\n return a if not c else b\n",
9845 "foo.py",
9846 |metric| {
9847 assert_eq!(metric.abc.conditions_sum(), 2);
9848 },
9849 );
9850 // The cross-language reference case. `(not b) if a else (not c)`
9851 // is the exact semantic equivalent of `a ? !b : !c`, which every
9852 // other language reports as 4: ternary (1) + condition `a` (1) +
9853 // two `NotOperator`s (2). The negated operands come from
9854 // Python's own arm, the condition from the slot added here.
9855 //
9856 // #1161's resolution plan predicted this would stay 3, having
9857 // measured the condition slot's contribution against the pre-fix
9858 // total. 3 would have left Python disagreeing with every other
9859 // language on the reference expression — the gap the issue was
9860 // filed about.
9861 check_metrics::<PythonParser>(
9862 "def f(a, b, c):\n return (not b) if a else (not c)\n",
9863 "foo.py",
9864 |metric| {
9865 assert_eq!(metric.abc.conditions_sum(), 4);
9866 },
9867 );
9868 }
9869
9870 // The double-count pin, and the reason Python gets a condition-slot
9871 // helper rather than a copy of `cpp_walk_ternary`: Python's branch
9872 // operands are counted by the top-level `NotOperator` /
9873 // `ComparisonOperator` arms, a different mechanism from every other
9874 // language's walker. Routing the branch slots through a negated-
9875 // operand walk as the C family does (`python_inspect_container`
9876 // before #1526) would have counted a parenthesised operand that
9877 // the identical unparenthesised expression scores at zero.
9878 //
9879 // Both fixtures below are 2 today and were 4 under such a copy, so a
9880 // later "make Python consistent with the others" change cannot land
9881 // silently.
9882 #[cfg(feature = "python")]
9883 #[test]
9884 fn python_ternary_branch_operands_are_not_double_counted() {
9885 // ternary (1) + condition `a` (1) = 2. The two parenthesised
9886 // operands add nothing — an unnegated branch is type-free.
9887 check_metrics::<PythonParser>(
9888 "def f(a, b, c):\n return (b) if a else (c)\n",
9889 "foo.py",
9890 |metric| {
9891 assert_eq!(metric.abc.conditions_sum(), 2);
9892 },
9893 );
9894 // The unparenthesised form must agree: nothing about `(b)`
9895 // versus `b` is a condition.
9896 check_metrics::<PythonParser>(
9897 "def f(a, b, c):\n return b if a else c\n",
9898 "foo.py",
9899 |metric| {
9900 assert_eq!(metric.abc.conditions_sum(), 2);
9901 },
9902 );
9903 }
9904
9905 // Comments are tree-sitter `extras`, so they arrive as direct
9906 // children of `conditional_expression` and shift every positional
9907 // index after them. `python_count_ternary_condition` therefore
9908 // anchors on the `if` keyword and skips comments after it; both
9909 // halves are needed and each fixture below fails without one.
9910 #[cfg(feature = "python")]
9911 #[test]
9912 fn python_ternary_condition_survives_an_interposed_comment() {
9913 // Comment before the keyword: `child(2)` is the `if` token here,
9914 // so a positional lookup reads 1. ternary (1) + `f()` (1) = 2.
9915 check_metrics::<PythonParser>(
9916 "def f(b, c):\n return (b\n # why\n if f() else c)\n",
9917 "foo.py",
9918 |metric| {
9919 assert_eq!(metric.abc.conditions_sum(), 2);
9920 },
9921 );
9922 // Comment after the keyword: the child immediately following
9923 // `if` is the comment, so taking the first rather than the first
9924 // non-comment reads 1. ternary (1) + `f()` (1) = 2.
9925 check_metrics::<PythonParser>(
9926 "def f(b, c):\n return (b if\n # why\n f() else c)\n",
9927 "foo.py",
9928 |metric| {
9929 assert_eq!(metric.abc.conditions_sum(), 2);
9930 },
9931 );
9932 }
9933
9934 #[cfg(feature = "python")]
9935 #[test]
9936 fn python_try_except_finally_count_conditions() {
9937 // ExceptClause + FinallyClause → 2 conditions.
9938 check_metrics::<PythonParser>(
9939 "def f():\n try:\n pass\n except ValueError:\n pass\n finally:\n pass\n",
9940 "foo.py",
9941 |metric| {
9942 assert_eq!(metric.abc.conditions_sum(), 2);
9943 insta::assert_json_snapshot!(metric.abc);
9944 },
9945 );
9946 }
9947
9948 #[cfg(feature = "python")]
9949 #[test]
9950 fn python_match_case_counts_conditions() {
9951 // Each non-wildcard `CaseClause` → 1 condition. The bare
9952 // `case _:` arm is the language-neutral `default:` equivalent
9953 // and is excluded (matches Rust's bare-`_` MatchArm filter and
9954 // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
9955 // `case _:` (excluded) → C = 1.
9956 check_metrics::<PythonParser>(
9957 "def f(x):\n match x:\n case 1:\n pass\n case _:\n pass\n",
9958 "foo.py",
9959 |metric| {
9960 assert_eq!(metric.abc.conditions_sum(), 1);
9961 insta::assert_json_snapshot!(metric.abc);
9962 },
9963 );
9964 }
9965
9966 #[cfg(feature = "python")]
9967 #[test]
9968 fn python_match_case_guarded_wildcard_counts() {
9969 // `case _ if g:` is NOT a bare wildcard — the guard
9970 // contributes real branching, so the arm counts as a
9971 // condition. Mirrors Rust's `_ if g => ...` behavior.
9972 // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
9973 // wildcard, counts) + `case _:` (bare wildcard, excluded) →
9974 // C from CaseClause = 2; the guard's `x > 0` adds one
9975 // ComparisonOperator → total C = 3.
9976 check_metrics::<PythonParser>(
9977 "def f(x):\n match x:\n case 1:\n pass\n case _ if x > 0:\n pass\n case _:\n pass\n",
9978 "foo.py",
9979 |metric| {
9980 assert_eq!(metric.abc.conditions_sum(), 3);
9981 insta::assert_json_snapshot!(metric.abc);
9982 },
9983 );
9984 }
9985
9986 #[cfg(feature = "python")]
9987 #[test]
9988 fn python_complex_function_abc() {
9989 // Mixed-shape regression: assignments, calls, conditions all in
9990 // a single function.
9991 check_metrics::<PythonParser>(
9992 "def f(items, threshold):\n\
9993 \x20 result = []\n\
9994 \x20 for item in items:\n\
9995 \x20 if item > threshold:\n\
9996 \x20 result.append(item)\n\
9997 \x20 return result\n",
9998 "foo.py",
9999 |metric| {
10000 // assignments: `result = []` → 1
10001 // branches: `result.append(item)` is one call → 1
10002 // conditions: `item > threshold` is one
10003 // ComparisonOperator → 1
10004 assert_eq!(metric.abc.assignments_sum(), 1);
10005 assert_eq!(metric.abc.branches_sum(), 1);
10006 assert_eq!(metric.abc.conditions_sum(), 1);
10007 insta::assert_json_snapshot!(metric.abc);
10008 },
10009 );
10010 }
10011
10012 #[cfg(feature = "python")]
10013 #[test]
10014 fn python_if_multiple_conditions() {
10015 // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403).
10016 // - `if a or b or c or d:` → 4 (each operand counted once)
10017 // - `if a and b and c:` → 3
10018 // - `if not a and not b:` → 2 (two `NotOperator`s counted
10019 // by the top-level dispatcher arm; the walker SKIPS
10020 // `NotOperator` children to avoid double-counting)
10021 // Total: 4 + 3 + 2 = 9.
10022 check_metrics::<PythonParser>(
10023 "def f(a, b, c, d):\n\
10024 \x20 if a or b or c or d: # +4c\n\
10025 \x20 pass\n\
10026 \x20 if a and b and c: # +3c\n\
10027 \x20 pass\n\
10028 \x20 if not a and not b: # +2c (NotOperator x2)\n\
10029 \x20 pass\n",
10030 "foo.py",
10031 |metric| {
10032 assert_eq!(metric.abc.conditions_sum(), 9);
10033 insta::assert_json_snapshot!(metric.abc);
10034 },
10035 );
10036 }
10037
10038 #[cfg(feature = "python")]
10039 #[test]
10040 fn python_while_conditions() {
10041 // Python has no `do { ... } while(cond);` construct, so this
10042 // mirrors only the `while` half of the Java suite. The
10043 // walker fires on each `and` / `or` token inside the loop
10044 // header.
10045 check_metrics::<PythonParser>(
10046 "def f(a, b):\n\
10047 \x20 while a or b: # +2c\n\
10048 \x20 break\n\
10049 \x20 while a and not b: # +2c (a + NotOperator)\n\
10050 \x20 break\n",
10051 "foo.py",
10052 |metric| {
10053 assert_eq!(metric.abc.conditions_sum(), 4);
10054 insta::assert_json_snapshot!(metric.abc);
10055 },
10056 );
10057 }
10058
10059 #[cfg(feature = "python")]
10060 #[test]
10061 fn python_short_circuit_with_boolean_literal_operand() {
10062 // `a and True` reports 2 conditions: one identifier, one
10063 // True literal. Confirms `True` / `False` are in the walker
10064 // terminal set.
10065 check_metrics::<PythonParser>("def f(a):\n return a and True\n", "foo.py", |metric| {
10066 assert_eq!(metric.abc.conditions_sum(), 2);
10067 insta::assert_json_snapshot!(metric.abc);
10068 });
10069 }
10070
10071 #[cfg(feature = "python")]
10072 #[test]
10073 fn python_await_expression_condition_counts() {
10074 // Regression for findings.md round-2 #2 (Python):
10075 // `if await ready(): pass` parses with `await` as the
10076 // condition node. Adding `Python::Await` to the
10077 // terminal-bool set mirrors the C# reference (lesson 19).
10078 check_metrics::<PythonParser>(
10079 "async def ready(): return True\n\
10080 async def f():\n if await ready(): pass\n",
10081 "foo.py",
10082 |metric| {
10083 // ready() is a call (1 branch); await is the
10084 // condition (1).
10085 assert_eq!(metric.abc.branches_sum(), 1);
10086 assert_eq!(metric.abc.conditions_sum(), 1);
10087 insta::assert_json_snapshot!(metric.abc);
10088 },
10089 );
10090 }
10091
10092 #[cfg(feature = "python")]
10093 #[test]
10094 fn python_if_call_terminal_condition_counts_once() {
10095 // Pins the Phase-2B behaviour for Python's `Call` terminal-bool
10096 // kind: `if foo():` is a Fitzpatrick Rule 6 unary conditional
10097 // (a bare boolean-evaluating call as the if-condition). The
10098 // walker's terminal-at-top check fires once per call-condition;
10099 // the call itself separately contributes 1 branch. Surfaced
10100 // (and verified intentional) by the code-review pass on
10101 // Phase 2B.
10102 check_metrics::<PythonParser>("def f():\n if foo(): pass\n", "foo.py", |metric| {
10103 assert_eq!(metric.abc.branches_sum(), 1);
10104 assert_eq!(metric.abc.conditions_sum(), 1);
10105 insta::assert_json_snapshot!(metric.abc);
10106 });
10107 }
10108
10109 #[cfg(feature = "python")]
10110 #[test]
10111 fn python_if_boolean_literal_condition() {
10112 // Phase 2B (issue #403): bare-boolean conditions count once.
10113 // Python has no paren wrap around if-conditions, so the
10114 // condition node is checked directly. The existing
10115 // NotOperator / ComparisonOperator arms continue to fire
10116 // for those shapes; only the bare-terminal cases (Identifier,
10117 // True, False, etc.) are added by the new arm.
10118 check_metrics::<PythonParser>(
10119 "def f(a):\n\
10120 \x20 if True: pass # +1c\n\
10121 \x20 if False: pass # +1c\n\
10122 \x20 while True: break # +1c\n\
10123 \x20 if a: pass # +1c (Rule 6 — bare identifier as condition)\n",
10124 "foo.py",
10125 |metric| {
10126 assert_eq!(metric.abc.conditions_sum(), 4);
10127 insta::assert_json_snapshot!(metric.abc);
10128 },
10129 );
10130 }
10131
10132 #[cfg(feature = "python")]
10133 #[test]
10134 fn python_methods_arguments_with_conditions() {
10135 // `m(not a, not b)` reports 2 conditions — both `NotOperator`
10136 // nodes are counted by Python's pre-existing top-level
10137 // NotOperator dispatcher arm. The argument-list walker does
10138 // not need a separate Python arm.
10139 check_metrics::<PythonParser>(
10140 "def f(a, b):\n\
10141 \x20 m(a, b) # +1b\n\
10142 \x20 m(not a, not b) # +1b +2c\n",
10143 "foo.py",
10144 |metric| {
10145 assert_eq!(metric.abc.branches_sum(), 2);
10146 assert_eq!(metric.abc.conditions_sum(), 2);
10147 insta::assert_json_snapshot!(metric.abc);
10148 },
10149 );
10150 }
10151
10152 #[cfg(feature = "python")]
10153 #[test]
10154 fn python_return_with_conditions() {
10155 // Phase 2B (issue #403). Python uses the pre-existing top-
10156 // level NotOperator / ComparisonOperator arms for return
10157 // expressions; no dedicated ReturnStatement walker arm is
10158 // needed.
10159 check_metrics::<PythonParser>(
10160 "def m1(z): return not (z >= 0)\n\
10161 def m2(x): return (((not x)))\n\
10162 def m3(x, y): return x and y\n",
10163 "foo.py",
10164 |metric| {
10165 // m1: ComparisonOperator (1); the `not` adds nothing,
10166 // as Java's `return !(z >= 0)` scores 1 (#1526).
10167 // m2: NotOperator (1).
10168 // m3: walker on `and` counts both operands = 2.
10169 // Sum: 4.
10170 assert_eq!(metric.abc.conditions_sum(), 4);
10171 insta::assert_json_snapshot!(metric.abc);
10172 },
10173 );
10174 }
10175
10176 #[cfg(feature = "rust")]
10177 #[test]
10178 fn rust_empty_unit_zero() {
10179 // No code at all → A=B=C=0. Establishes the trait is wired up
10180 // and the per-language compute is reachable.
10181 check_metrics::<RustParser>("", "empty.rs", |metric| {
10182 assert_eq!(metric.abc.assignments_sum(), 0);
10183 assert_eq!(metric.abc.branches_sum(), 0);
10184 assert_eq!(metric.abc.conditions_sum(), 0);
10185 insta::assert_json_snapshot!(metric.abc);
10186 });
10187 }
10188
10189 #[cfg(feature = "rust")]
10190 #[test]
10191 fn rust_assignments_let_init_plain_and_compound() {
10192 // `let mut x = 0` is a `let_declaration` carrying an `=`
10193 // initializer → counts as 1 (matches Fitzpatrick's literal
10194 // "every `=` is an assignment" rule and the JS impl's
10195 // treatment of `let x = 5`). `x = 5` and `x = 7` are plain
10196 // `=` assignments → 2. `x += 2` is a compound assignment → 1.
10197 // Total A = 4.
10198 check_metrics::<RustParser>(
10199 "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
10200 "foo.rs",
10201 |metric| {
10202 assert_eq!(metric.abc.assignments_sum(), 4);
10203 assert_eq!(metric.abc.branches_sum(), 0);
10204 assert_eq!(metric.abc.conditions_sum(), 0);
10205 insta::assert_json_snapshot!(metric.abc);
10206 },
10207 );
10208 }
10209
10210 #[cfg(feature = "rust")]
10211 #[test]
10212 fn rust_let_without_initializer_does_not_count() {
10213 // `let a;` is a `let_declaration` with NO `=` and no `value`
10214 // field — the binding is uninitialised. The arm only fires
10215 // when `value` is present, so this contributes zero to A.
10216 // `let _b;` is the same shape (the `_` pattern is still a
10217 // pattern, not a wildcard suppression of the binding).
10218 // Regression test for issue #393: only `=` counts, not the
10219 // bare declaration.
10220 check_metrics::<RustParser>(
10221 "fn f() { let a: i32; let _b: i32; a = 5; }",
10222 "foo.rs",
10223 |metric| {
10224 // Only `a = 5` (assignment_expression) → A = 1.
10225 assert_eq!(metric.abc.assignments_sum(), 1);
10226 insta::assert_json_snapshot!(metric.abc);
10227 },
10228 );
10229 }
10230
10231 #[cfg(feature = "rust")]
10232 #[test]
10233 fn rust_let_initializers_immutable_and_mutable_count() {
10234 // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`,
10235 // `let mut d = 0;` are all `let_declaration` nodes carrying
10236 // an `=` initializer — each counts as 1 (Option B in the
10237 // issue body: literal Fitzpatrick, both `let` and `let mut`
10238 // count). `d = 5;` is one plain assignment_expression, `d
10239 // += 1;` is one compound. Total A = 4 + 1 + 1 = 6.
10240 check_metrics::<RustParser>(
10241 "fn f() { let a=1; let b=2; let c=a+b; let mut d=0; d=5; d+=1; }",
10242 "foo.rs",
10243 |metric| {
10244 assert_eq!(metric.abc.assignments_sum(), 6);
10245 insta::assert_json_snapshot!(metric.abc);
10246 },
10247 );
10248 }
10249
10250 #[cfg(feature = "rust")]
10251 #[test]
10252 fn rust_calls_are_branches() {
10253 // Free function call + method call (parses as call_expression
10254 // with a field_expression callee) + associated-fn call. All
10255 // three are `call_expression` → B = 3. Macro invocations like
10256 // `println!` parse as `macro_invocation`, NOT `call_expression`,
10257 // so they are not branches.
10258 check_metrics::<RustParser>(
10259 "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
10260 "foo.rs",
10261 |metric| {
10262 assert_eq!(metric.abc.branches_sum(), 3);
10263 assert_eq!(metric.abc.assignments_sum(), 0);
10264 assert_eq!(metric.abc.conditions_sum(), 0);
10265 insta::assert_json_snapshot!(metric.abc);
10266 },
10267 );
10268 }
10269
10270 #[cfg(feature = "rust")]
10271 #[test]
10272 fn rust_try_operator_is_branch() {
10273 // `?` parses as `try_expression` and counts as one branch
10274 // (short-circuit return on Err / None). The `Err(())` call
10275 // contributes one branch in addition (call_expression).
10276 check_metrics::<RustParser>(
10277 "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
10278 "foo.rs",
10279 |metric| {
10280 // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
10281 assert_eq!(metric.abc.branches_sum(), 3);
10282 insta::assert_json_snapshot!(metric.abc);
10283 },
10284 );
10285 }
10286
10287 #[cfg(feature = "rust")]
10288 #[test]
10289 fn rust_comparisons_count_conditions() {
10290 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
10291 // comparisons → C = 6.
10292 check_metrics::<RustParser>(
10293 "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
10294 "foo.rs",
10295 |metric| {
10296 assert_eq!(metric.abc.conditions_sum(), 6);
10297 insta::assert_json_snapshot!(metric.abc);
10298 },
10299 );
10300 }
10301
10302 #[cfg(feature = "rust")]
10303 #[test]
10304 fn rust_generic_brackets_not_conditions() {
10305 // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
10306 // comparison operators. The parent-check in the LT/GT arms
10307 // must filter them out. Expected C = 0.
10308 check_metrics::<RustParser>(
10309 "fn f() -> Vec<i32> { Vec::<i32>::new() }",
10310 "foo.rs",
10311 |metric| {
10312 assert_eq!(metric.abc.conditions_sum(), 0);
10313 insta::assert_json_snapshot!(metric.abc);
10314 },
10315 );
10316 }
10317
10318 #[cfg(feature = "rust")]
10319 #[test]
10320 fn rust_if_let_counts_as_condition() {
10321 // `if let Some(v) = opt { ... }` introduces a `let_condition`
10322 // → 1 condition. The `if` keyword itself does not add another
10323 // count — Fitzpatrick counts conditions, not branch keywords.
10324 check_metrics::<RustParser>(
10325 "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
10326 "foo.rs",
10327 |metric| {
10328 assert_eq!(metric.abc.conditions_sum(), 1);
10329 insta::assert_json_snapshot!(metric.abc);
10330 },
10331 );
10332 }
10333
10334 #[cfg(feature = "rust")]
10335 #[test]
10336 fn rust_while_let_counts_as_condition() {
10337 // `while let Some(y) = it.next() { ... }` is also a
10338 // `let_condition` (the `while` form). One condition; the
10339 // `it.next()` call adds one branch.
10340 check_metrics::<RustParser>(
10341 "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
10342 "foo.rs",
10343 |metric| {
10344 assert_eq!(metric.abc.conditions_sum(), 1);
10345 assert_eq!(metric.abc.branches_sum(), 1);
10346 insta::assert_json_snapshot!(metric.abc);
10347 },
10348 );
10349 }
10350
10351 #[cfg(feature = "rust")]
10352 #[test]
10353 fn rust_match_arms_count_conditions_wildcard_excluded() {
10354 // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
10355 // bare wildcard is the `default:` equivalent and is skipped.
10356 // The guarded arm has a `n if n > 0` pattern (more than one
10357 // child in the match_pattern) and still counts. Two non-wildcard
10358 // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
10359 // adds one more → C = 3.
10360 check_metrics::<RustParser>(
10361 "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
10362 "foo.rs",
10363 |metric| {
10364 assert_eq!(metric.abc.conditions_sum(), 3);
10365 insta::assert_json_snapshot!(metric.abc);
10366 },
10367 );
10368 }
10369
10370 #[cfg(feature = "rust")]
10371 #[test]
10372 fn rust_else_counts_as_condition() {
10373 // `if a > b { ... } else { ... }` → `a > b` is one condition,
10374 // `else` is one condition → C = 2.
10375 check_metrics::<RustParser>(
10376 "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
10377 "foo.rs",
10378 |metric| {
10379 assert_eq!(metric.abc.conditions_sum(), 2);
10380 insta::assert_json_snapshot!(metric.abc);
10381 },
10382 );
10383 }
10384
10385 #[cfg(feature = "rust")]
10386 #[test]
10387 fn rust_let_chain2_hidden_rule_drift_marker() {
10388 // Drift marker (findings.md round-2 #3): `Rust::LetChain2`
10389 // maps to the hidden grammar rule `_let_chain`. At the
10390 // pinned tree-sitter-rust version it is never emitted as a
10391 // concrete node — the visible `LetChain` (= 352) carries
10392 // every let-chain. We list `LetChain2` defensively in
10393 // `rust_condition_scores_itself` and `rust_count_chain_operands`
10394 // (lesson 34); if a future grammar bump promotes
10395 // `_let_chain` to a visible rule, this assertion fails
10396 // loudly so the maintainer knows to verify the walker still
10397 // counts correctly for the new shape.
10398 let src = "fn f(a: bool, b: Option<i32>) {\n\
10399 \x20 if a && let Some(_) = b { }\n\
10400 }\n";
10401 let parser = RustParser::new(
10402 src.as_bytes().to_vec(),
10403 &std::path::PathBuf::from("foo.rs"),
10404 None,
10405 );
10406 assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16));
10407 }
10408
10409 #[cfg(feature = "rust")]
10410 #[test]
10411 fn rust_scoped_identifier_condition_counts() {
10412 // Regression for findings.md round-2 #1 (Rust):
10413 // `if crate::FLAG {}` parses with `scoped_identifier` as the
10414 // condition node. Pre-fix, the former `rust_bool_terminal_kinds!()`
10415 // listed only `Identifier` so the walker reached the
10416 // `scoped_identifier` child, found it non-terminal /
10417 // non-paren / non-unary, and broke without counting.
10418 // Mirrors the C# fix in #372 (lesson 19) for
10419 // `MemberAccessExpression`.
10420 check_metrics::<RustParser>("fn f() { if crate::FLAG { } }\n", "foo.rs", |metric| {
10421 assert_eq!(metric.abc.conditions_sum(), 1);
10422 insta::assert_json_snapshot!(metric.abc);
10423 });
10424 }
10425
10426 #[cfg(feature = "rust")]
10427 #[test]
10428 fn rust_await_expression_condition_counts() {
10429 // Regression for findings.md round-2 #2 (Rust):
10430 // `if ready().await {}` parses with `await_expression` as
10431 // the condition node. Adding `Rust::AwaitExpression` to the
10432 // former terminal-bool set closed the parity gap with the C#
10433 // reference (the former `csharp_bool_terminal_kinds!()`).
10434 check_metrics::<RustParser>(
10435 "async fn ready() -> bool { true }\n\
10436 async fn f() { if ready().await { } }\n",
10437 "foo.rs",
10438 |metric| {
10439 // ready() is a call (1 branch); `ready().await` is
10440 // the unary boolean condition (1).
10441 assert_eq!(metric.abc.branches_sum(), 1);
10442 assert_eq!(metric.abc.conditions_sum(), 1);
10443 insta::assert_json_snapshot!(metric.abc);
10444 },
10445 );
10446 }
10447
10448 #[cfg(feature = "rust")]
10449 #[test]
10450 fn rust_complex_function_abc() {
10451 // Mixed-shape regression: assignments, calls, conditions, `?`,
10452 // `if let`, `match` in one body. Verified by hand:
10453 // - assignments: `let mut x = 0` (let init), `x = 5`, `x += 2`,
10454 // `let _ = ...` (let init), `let r: ... = Err(())` (let init),
10455 // `let _v = r?` (let init) → A = 6 (post-#393: every `=`
10456 // initializer in a `let_declaration` is one assignment, in
10457 // line with the literal Fitzpatrick reading).
10458 // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
10459 // (3 calls + 1 try).
10460 // - conditions: `if let Some(v) = opt` → 1, `match x` arms
10461 // `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
10462 // Total C = 4.
10463 check_metrics::<RustParser>(
10464 "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
10465 \x20 let mut x = 0;\n\
10466 \x20 x = 5;\n\
10467 \x20 x += 2;\n\
10468 \x20 if let Some(_v) = opt { }\n\
10469 \x20 let _ = xs.iter().next();\n\
10470 \x20 let r: Result<i32, ()> = Err(());\n\
10471 \x20 let _v = r?;\n\
10472 \x20 Ok(match x {\n\
10473 \x20 0 => 1,\n\
10474 \x20 n if n > 0 => n,\n\
10475 \x20 _ => -1,\n\
10476 \x20 })\n\
10477 }\n",
10478 "foo.rs",
10479 |metric| {
10480 assert_eq!(metric.abc.assignments_sum(), 6);
10481 // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
10482 // plus 1 try (`r?`) → 5 branches.
10483 assert_eq!(metric.abc.branches_sum(), 5);
10484 // 1 let_condition + 2 non-wildcard match_arms + 1
10485 // comparison (`n > 0`) → 4.
10486 assert_eq!(metric.abc.conditions_sum(), 4);
10487 insta::assert_json_snapshot!(metric.abc);
10488 },
10489 );
10490 }
10491
10492 #[cfg(feature = "rust")]
10493 #[test]
10494 fn rust_let_chain_bare_identifier_operand_counts() {
10495 // Regression: pre-fix, `if a && let Some(_z) = y { }` reported
10496 // 1 condition (only the LetCondition). The bare-identifier
10497 // `a` operand was lost because Rust 2024 wraps let-chain
10498 // `&&` operands in a `LetChain` node (not `BinaryExpression`)
10499 // and the former `rust_count_unary_conditions` only counted
10500 // terminals under a `BinaryExpression` parent. Allowing `LetChain` /
10501 // `LetChain2` as known-bool list parents fixed the loss.
10502 // Expected: LetCondition (1) + walker on `a` (1) = 2.
10503 check_metrics::<RustParser>(
10504 "fn f(a: bool, y: Option<i32>) {\n\
10505 \x20 if a && let Some(_z) = y { }\n\
10506 }\n",
10507 "foo.rs",
10508 |metric| {
10509 assert_eq!(metric.abc.conditions_sum(), 2);
10510 insta::assert_json_snapshot!(metric.abc);
10511 },
10512 );
10513 }
10514
10515 #[cfg(feature = "rust")]
10516 #[test]
10517 fn rust_if_multiple_conditions() {
10518 // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of
10519 // a `&&` / `||` chain is one condition. Mirrors
10520 // `java_if_multiple_conditions`. Rust's `if` head has no
10521 // parentheses, but the walker fires on each `&&` / `||` token
10522 // and walks the parent `binary_expression` regardless.
10523 check_metrics::<RustParser>(
10524 "fn f(a: bool, b: bool, c: bool, d: bool) -> i32 {\n\
10525 \x20 if a || b || c || d { return 1; } // +4c\n\
10526 \x20 if a && b && c { return 2; } // +3c\n\
10527 \x20 if !a && !b { return 3; } // +2c\n\
10528 \x20 0\n\
10529 }\n",
10530 "foo.rs",
10531 |metric| {
10532 // 4 + 3 + 2 = 9
10533 assert_eq!(metric.abc.conditions_sum(), 9);
10534 insta::assert_json_snapshot!(metric.abc);
10535 },
10536 );
10537 }
10538
10539 #[cfg(feature = "rust")]
10540 #[test]
10541 fn rust_while_conditions() {
10542 // Rust has no `do { ... } while(cond);` construct, so this
10543 // mirrors only the `while` half of `java_while_and_do_while_conditions`.
10544 // Each operand of the `&&` / `||` chain in the loop condition
10545 // counts as one Fitzpatrick condition (Rule 7).
10546 check_metrics::<RustParser>(
10547 "fn f(a: bool, b: bool) {\n\
10548 \x20 while a || b { break; } // +2c\n\
10549 \x20 while a && !b { break; } // +2c\n\
10550 }\n",
10551 "foo.rs",
10552 |metric| {
10553 assert_eq!(metric.abc.conditions_sum(), 4);
10554 insta::assert_json_snapshot!(metric.abc);
10555 },
10556 );
10557 }
10558
10559 #[cfg(feature = "rust")]
10560 #[test]
10561 fn rust_if_boolean_literal_condition() {
10562 // Phase 2B (issue #403): a condition whose entire body is a
10563 // boolean literal counts as one Fitzpatrick condition.
10564 // `if true {}` → 1, `if !false {}` → 1 (unary unwrap), and
10565 // `while true { break }` → 1.
10566 check_metrics::<RustParser>(
10567 "fn f() {\n\
10568 \x20 if true { } // +1c\n\
10569 \x20 if !false { } // +1c\n\
10570 \x20 while true { break; } // +1c\n\
10571 }\n",
10572 "foo.rs",
10573 |metric| {
10574 assert_eq!(metric.abc.conditions_sum(), 3);
10575 insta::assert_json_snapshot!(metric.abc);
10576 },
10577 );
10578 }
10579
10580 #[cfg(feature = "rust")]
10581 #[test]
10582 fn rust_methods_arguments_with_conditions() {
10583 // Phase 2B (issue #403): unary-conditional arguments to a
10584 // call each count once. `m(!a, !b)` → 2 conditions + 1
10585 // branch (the call itself). Bare identifier arguments do
10586 // NOT count (they reach the count_unary_conditions list with
10587 // list_kind = Arguments, not BinaryExpression).
10588 check_metrics::<RustParser>(
10589 "fn f(a: bool, b: bool) {\n\
10590 \x20 m(a, b); // +1b\n\
10591 \x20 m(!a, !b); // +1b +2c\n\
10592 \x20 m(!a, b, !a); // +1b +2c\n\
10593 }\n",
10594 "foo.rs",
10595 |metric| {
10596 assert_eq!(metric.abc.branches_sum(), 3);
10597 assert_eq!(metric.abc.conditions_sum(), 4);
10598 insta::assert_json_snapshot!(metric.abc);
10599 },
10600 );
10601 }
10602
10603 #[cfg(feature = "rust")]
10604 #[test]
10605 fn rust_return_with_conditions() {
10606 // Phase 2B (issue #403). Mirrors `java_return_with_conditions`
10607 // — `return !a` / `return x && y` count their unary
10608 // conditional operands. Per Fitzpatrick Rule 7, a `!`-wrapped
10609 // relational expression contributes ONE condition (the
10610 // relational op itself) — the `!` does not add a second
10611 // count when its operand is already a comparison.
10612 check_metrics::<RustParser>(
10613 "fn m1(z: i32) -> bool { return !(z >= 0); }\n\
10614 fn m2(x: bool) -> bool { return (((!x))); }\n\
10615 fn m3(x: bool, y: bool) -> bool { return x && y; }\n\
10616 fn m4(y: bool, z: i32) -> bool { return y || (z < 0); }\n",
10617 "foo.rs",
10618 |metric| {
10619 // m1: !(z >= 0) → the `>=` contributes 1; the unary
10620 // `!` wraps a paren'd BinaryExpression, which
10621 // inspect_container does not unwrap further →
10622 // no walker count. Total: 1.
10623 // m2: (((!x))) → ReturnExpression arm walks (((!x))).
10624 // inspect_container unwraps three parens and one
10625 // unary, reaches Identifier `x`, has_boolean_content
10626 // was seeded true by the unary-not flip. +1.
10627 // m3: x && y → `&&` walker counts both terminals → 2.
10628 // m4: y || (z < 0) → `||` walker counts `y` (terminal,
10629 // +1); the `<` contributes 1 via its own arm; the
10630 // paren'd BinaryExpression `(z < 0)` is not
10631 // terminal under the walker → no extra count.
10632 // Total: 2.
10633 // Sum: 1 + 1 + 2 + 2 = 6.
10634 assert_eq!(metric.abc.conditions_sum(), 6);
10635 insta::assert_json_snapshot!(metric.abc);
10636 },
10637 );
10638 }
10639
10640 #[cfg(feature = "rust")]
10641 #[test]
10642 fn rust_short_circuit_with_boolean_literal_operand() {
10643 // `if a && true` reports 2 conditions: one for the identifier
10644 // operand, one for the boolean-literal operand. Confirms the
10645 // walker terminal set includes `BooleanLiteral`.
10646 check_metrics::<RustParser>(
10647 "fn f(a: bool) -> bool { a && true }\n",
10648 "foo.rs",
10649 |metric| {
10650 assert_eq!(metric.abc.conditions_sum(), 2);
10651 insta::assert_json_snapshot!(metric.abc);
10652 },
10653 );
10654 }
10655
10656 // ----- Go -----
10657
10658 #[cfg(feature = "go")]
10659 #[test]
10660 fn go_empty_unit_zero() {
10661 // Package declaration only — no Fitzpatrick events. Confirms the
10662 // GoCode Abc trait is wired up and emits zero counts.
10663 check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
10664 assert_eq!(metric.abc.assignments_sum(), 0);
10665 assert_eq!(metric.abc.branches_sum(), 0);
10666 assert_eq!(metric.abc.conditions_sum(), 0);
10667 insta::assert_json_snapshot!(metric.abc);
10668 });
10669 }
10670
10671 #[cfg(feature = "go")]
10672 #[test]
10673 fn go_assignments_count_plain_compound_short_var_and_incdec() {
10674 // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
10675 // `x += 2` (compound), `x++` (inc), and the initialized
10676 // declaration `var y = 1` — which is counted, matching the Rust
10677 // and Java rules for `let y = 1` / `int y = 1` (both measured at
10678 // one assignment each). The comment here previously claimed the
10679 // opposite and pinned Go at 6 (#1278).
10680 check_metrics::<GoParser>(
10681 "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
10682 "foo.go",
10683 |metric| {
10684 // `_ = y` is itself an assignment_statement → +1.
10685 // var y=1 + _=y + x:= + x=5 + x+=2 + x=7 + x++ → 7
10686 assert_eq!(metric.abc.assignments_sum(), 7);
10687 assert_eq!(metric.abc.branches_sum(), 0);
10688 assert_eq!(metric.abc.conditions_sum(), 0);
10689 insta::assert_json_snapshot!(metric.abc);
10690 },
10691 );
10692 }
10693
10694 #[cfg(feature = "go")]
10695 #[test]
10696 fn go_var_declarations_count_only_when_initialized() {
10697 // Regression for #1278: a `var` declaration with an initializer is
10698 // a `var_spec` carrying a `value` field, not an
10699 // `assignment_statement` or `short_var_declaration`, so it scored
10700 // zero — `var x = 5` and `x := 5` are the same binding spelled two
10701 // ways. Both typed and untyped initializers count; an
10702 // uninitialized `var z int` and a `const` do not.
10703 // expected: 3 assignments — `var x = 5`, `var y int = 6`, `z := 7`;
10704 // `var w int` and `const c = 1` contribute nothing.
10705 check_metrics::<GoParser>(
10706 "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",
10707 "foo.go",
10708 |metric| {
10709 assert_eq!(metric.abc.assignments_sum(), 3);
10710 assert_eq!(metric.abc.branches_sum(), 0);
10711 },
10712 );
10713 }
10714
10715 #[cfg(feature = "go")]
10716 #[test]
10717 fn go_grouped_var_block_counts_each_initialized_spec() {
10718 // A grouped `var ( … )` block is one `var_declaration` holding one
10719 // `var_spec` per line, so matching the spec counts each initialized
10720 // line on its own. A multi-name spec is still one binding
10721 // statement, matching `p, q := 1, 2` (#1278).
10722 // expected: 3 assignments — `a = 1`, `p, q = 1, 2`, and `r := 0`;
10723 // the uninitialized `b int` contributes nothing.
10724 check_metrics::<GoParser>(
10725 "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",
10726 "foo.go",
10727 |metric| {
10728 // The trailing `_ = …` is itself an assignment_statement.
10729 assert_eq!(metric.abc.assignments_sum(), 4);
10730 },
10731 );
10732 }
10733
10734 #[cfg(feature = "go")]
10735 #[test]
10736 fn go_calls_are_branches() {
10737 // Three calls: free function `g()`, method call `r.Inc()`, and
10738 // builtin call `len(s)`. All parse as `call_expression` → B = 3.
10739 // Composite literal `Foo{}` is NOT a call.
10740 check_metrics::<GoParser>(
10741 "package main\n\
10742 type R struct{}\n\
10743 func (r R) Inc() {}\n\
10744 func g() {}\n\
10745 func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
10746 "foo.go",
10747 |metric| {
10748 assert_eq!(metric.abc.branches_sum(), 3);
10749 insta::assert_json_snapshot!(metric.abc);
10750 },
10751 );
10752 }
10753
10754 #[cfg(feature = "go")]
10755 #[test]
10756 fn go_comparisons_count_conditions() {
10757 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
10758 // comparisons → C = 6.
10759 check_metrics::<GoParser>(
10760 "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
10761 "foo.go",
10762 |metric| {
10763 assert_eq!(metric.abc.conditions_sum(), 6);
10764 insta::assert_json_snapshot!(metric.abc);
10765 },
10766 );
10767 }
10768
10769 #[cfg(feature = "go")]
10770 #[test]
10771 fn go_generic_brackets_not_conditions() {
10772 // Generic instantiation `Min[int](a, b)` puts `int` inside
10773 // `TypeArguments`, not `BinaryExpression`. The parent guard on
10774 // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
10775 check_metrics::<GoParser>(
10776 "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
10777 "foo.go",
10778 |metric| {
10779 assert_eq!(metric.abc.conditions_sum(), 0);
10780 assert_eq!(metric.abc.branches_sum(), 1);
10781 insta::assert_json_snapshot!(metric.abc);
10782 },
10783 );
10784 }
10785
10786 #[cfg(feature = "go")]
10787 #[test]
10788 fn go_switch_arms_count_conditions_default_excluded() {
10789 // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
10790 // bare `default` is the C/Java `default:` equivalent and is
10791 // excluded — 3 conditions from ExpressionCase. The switch
10792 // expression `x` is bare (no comparison), so no extra
10793 // condition from `==`-style operators.
10794 check_metrics::<GoParser>(
10795 "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",
10796 "foo.go",
10797 |metric| {
10798 assert_eq!(metric.abc.conditions_sum(), 3);
10799 insta::assert_json_snapshot!(metric.abc);
10800 },
10801 );
10802 }
10803
10804 #[cfg(feature = "go")]
10805 #[test]
10806 fn go_type_switch_arms_count_conditions() {
10807 // Type switch: `case int:`, `case string:`, `default:`. Two
10808 // non-default type-case arms → C = 2.
10809 check_metrics::<GoParser>(
10810 "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
10811 "foo.go",
10812 |metric| {
10813 assert_eq!(metric.abc.conditions_sum(), 2);
10814 insta::assert_json_snapshot!(metric.abc);
10815 },
10816 );
10817 }
10818
10819 #[cfg(feature = "go")]
10820 #[test]
10821 fn go_select_arms_count_conditions() {
10822 // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
10823 // Two non-default communication cases → C = 2.
10824 check_metrics::<GoParser>(
10825 "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
10826 "foo.go",
10827 |metric| {
10828 assert_eq!(metric.abc.conditions_sum(), 2);
10829 insta::assert_json_snapshot!(metric.abc);
10830 },
10831 );
10832 }
10833
10834 // Anchors the tag shape of a Go fixture before scoring its members:
10835 // `tagged` is how many expression switches carry a `value` field, so
10836 // a fixture edit that turns a tagless switch tagged (or back) fails
10837 // here rather than as a moved score.
10838 #[cfg(feature = "go")]
10839 fn assert_go_switch_members(
10840 src: &str,
10841 tagged: usize,
10842 kinds: &[(u16, usize, &str)],
10843 expected: &[(&str, u64, u64)],
10844 ) {
10845 let parser = GoParser::new(
10846 src.as_bytes().to_vec(),
10847 std::path::Path::new("foo.go"),
10848 None,
10849 );
10850 let found = parser
10851 .root()
10852 .preorder()
10853 .filter(|n| {
10854 n.kind_id() == Go::ExpressionSwitchStatement
10855 && n.child_by_field_name("value").is_some()
10856 })
10857 .count();
10858 assert_eq!(found, tagged, "tagged switches — the fixture moved");
10859 assert_fixture_spells::<GoParser>(src, "foo.go", kinds);
10860 check_func_space::<GoParser, _>(src, "foo.go", |space| {
10861 assert_members_score(&space, expected);
10862 });
10863 }
10864
10865 // #1523, transferring #1453's Ruby rule. A tagless switch is
10866 // `switch true`: each case expression is an ordinary boolean, so a
10867 // comparison in it is already paid by the token arm, and the case's
10868 // former blanket +1 counted `gt` twice against its `if` twin `iff`.
10869 // Every row now equals the `if` spelling of the same predicate.
10870 //
10871 // `bare`, `paren` and `neg` are the controls: no operator for any
10872 // other arm to count, so the case still pays through the `if` slot
10873 // (1 before the fix and after). `negcmp` peels to a comparison the
10874 // token arm owns. `init1` carries an `initializer` and no tag, and
10875 // `cmt` puts a comment where the tag would go: neither is the
10876 // `value` field, so both stay tagless. `nest2` is a tagless switch
10877 // inside a tagged one — each switch is judged by its own tag.
10878 #[cfg(feature = "go")]
10879 #[test]
10880 fn go_tagless_switch_case_counts_its_condition_once() {
10881 let src = "package main
10882func gt(x int) int { switch { case x > 5: return 1 }; return 0 }
10883func iff(x int) int { if x > 5 { return 1 }; return 0 }
10884func bare(b bool) int { switch { case b: return 1 }; return 0 }
10885func paren(b bool) int { switch { case (b): return 1 }; return 0 }
10886func neg(b bool) int { switch { case !b: return 1 }; return 0 }
10887func negcmp(x int) int { switch { case !(x > 5): return 1 }; return 0 }
10888func andd(a, b bool) int { switch { case a && b: return 1 }; return 0 }
10889func two(x int) int { switch { case x > 1: fallthrough; case x < 0: return 1; default: return 2 } }
10890func init1(s string) int { switch n := len(s); { case n > 1: return 1 }; return 0 }
10891func cmt(x int) int { switch /* no tag */ { case /* c */ x > 5: return 1 }; return 0 }
10892func nest(x, y int) int { switch { case x > 1: switch { case y > 1: return 1 } }; return 0 }
10893func nest2(x, y int) int { switch x { case 1: switch { case y > 1: return 1 } }; return 0 }
10894";
10895 assert_go_switch_members(
10896 src,
10897 1,
10898 &[
10899 (Go::ExpressionCase as u16, 14, "expression cases"),
10900 (Go::DefaultCase as u16, 1, "`two`'s `default`"),
10901 (Go::GT as u16, 9, "`>` comparisons"),
10902 (
10903 Go::ParenthesizedExpression as u16,
10904 2,
10905 "`paren` / `negcmp` parens",
10906 ),
10907 (Go::Comment as u16, 2, "`cmt`'s comments"),
10908 ],
10909 &[
10910 // (member, abc.conditions, cyclomatic)
10911 // Was 2: the `>` and the case, for one decision.
10912 ("gt", 1, 2),
10913 ("iff", 1, 2),
10914 ("bare", 1, 2),
10915 ("paren", 1, 2),
10916 ("neg", 1, 2),
10917 // Was 2.
10918 ("negcmp", 1, 2),
10919 // Was 3: the Rule 9 walker owns both operands.
10920 ("andd", 2, 3),
10921 // Was 4. `default` stays +0 and `fallthrough` adds nothing.
10922 ("two", 2, 3),
10923 // Was 2.
10924 ("init1", 1, 2),
10925 // Was 2. A comparison, not a bare operand, so a misread
10926 // tag (+1 per case on top of the `>`) shows as 2.
10927 ("cmt", 1, 2),
10928 // Was 4.
10929 ("nest", 2, 3),
10930 // Was 3: the tagged outer case keeps its 1.
10931 ("nest2", 2, 3),
10932 ],
10933 );
10934 }
10935
10936 // A pointer dereference or channel receive is a boolean operand the
10937 // way an identifier is: `*p` of a `*bool`, `<-ch` of a `chan bool`.
10938 // No operator arm counts either, so each slot pays for it — the `if`
10939 // slot, a tagless case (which #1523 routed through that slot, so
10940 // `swp` / `swr` fell from 1 to 0 until this), the `!` / paren peel,
10941 // and an `&&` operand. `neg` and `asg` are the controls: `-x`, `*&y`
10942 // and `b := *p` sit in no boolean slot and stay 0 (#1526).
10943 #[cfg(feature = "go")]
10944 #[test]
10945 fn go_deref_and_receive_are_boolean_operands() {
10946 let src = "package main
10947func ifp(p *bool) int { if *p { return 1 }; return 0 }
10948func ifr(ch chan bool) int { if <-ch { return 1 }; return 0 }
10949func swp(p *bool) int { switch { case *p: return 1 }; return 0 }
10950func swr(ch chan bool) int { switch { case <-ch: return 1 }; return 0 }
10951func andp(a bool, p *bool) int { if a && *p { return 1 }; return 0 }
10952func notp(p *bool) int { if !*p { return 1 }; return 0 }
10953func parp(p *bool) int { if (*p) { return 1 }; return 0 }
10954func neg(x int) int { y := -x; z := *&y; return z }
10955func asg(p *bool) bool { b := *p; return b }
10956";
10957 assert_go_switch_members(
10958 src,
10959 0,
10960 &[
10961 (Go::ExpressionCase as u16, 2, "`swp` / `swr` cases"),
10962 (Go::LTDASH as u16, 2, "`ifr` / `swr` receives"),
10963 (Go::ParenthesizedExpression as u16, 1, "`parp`'s parens"),
10964 ],
10965 &[
10966 // (member, abc.conditions, cyclomatic)
10967 ("ifp", 1, 2),
10968 ("ifr", 1, 2),
10969 ("swp", 1, 2),
10970 ("swr", 1, 2),
10971 ("andp", 2, 3),
10972 ("notp", 1, 2),
10973 ("parp", 1, 2),
10974 ("neg", 0, 1),
10975 ("asg", 0, 1),
10976 ],
10977 );
10978 }
10979
10980 // A case listing several expressions is an implicit `||` — Go's spec
10981 // defines a tagless switch as `switch true`, so `case a, b:` holds
10982 // when `a == true || b == true` — and each expression is scored as an
10983 // operand of that chain: once, unless its own arm already counted it.
10984 // `ab` and `mix` therefore read 2, exactly their `if a || b` /
10985 // `if a || x > 1` twins, and `xim` shows the order cannot matter.
10986 // This is #1453's Ruby rule (`when a, b` reads 2) rather than
10987 // Kotlin's first-alternative-only rule, which scores the two orders
10988 // differently.
10989 //
10990 // Parity consequence: cyclomatic scores the case as one decision, so
10991 // these rows sit one above `conditions == cyclomatic - 1`, while the
10992 // `||` twins sit at it — the same trade Ruby's `when a, b` makes.
10993 #[cfg(feature = "go")]
10994 #[test]
10995 fn go_tagless_switch_case_lists_are_an_implicit_or() {
10996 let src = "package main
10997func ab(a, b bool) int { switch { case a, b: return 1 }; return 0 }
10998func ifab(a, b bool) int { if a || b { return 1 }; return 0 }
10999func mix(a bool, x int) int { switch { case a, x > 1: return 1 }; return 0 }
11000func xim(a bool, x int) int { switch { case x > 1, a: return 1 }; return 0 }
11001func ifmix(a bool, x int) int { if a || x > 1 { return 1 }; return 0 }
11002func cmts(a, b bool) int { switch { case a, /* c */ b: return 1 }; return 0 }
11003";
11004 assert_go_switch_members(
11005 src,
11006 0,
11007 &[
11008 (Go::ExpressionCase as u16, 4, "expression cases"),
11009 (Go::PIPEPIPE as u16, 2, "the `||` twins"),
11010 (Go::Comment as u16, 1, "`cmts`'s comment"),
11011 ],
11012 &[
11013 // Was 1: only the case, for two operands.
11014 ("ab", 2, 2),
11015 ("ifab", 2, 3),
11016 ("mix", 2, 2),
11017 ("xim", 2, 2),
11018 ("ifmix", 2, 3),
11019 // Was 1: the comment is no operand.
11020 ("cmts", 2, 2),
11021 ],
11022 );
11023 }
11024
11025 // The controls that must not move. A tagged case lists values
11026 // compared with the tag, a comparison the source never spells, so the
11027 // case itself is the condition — `init2` carries an `initializer`
11028 // beside its tag, and `tagcmp`'s `x > 1` is a value compared with `b`
11029 // (`if b == (x > 1)` also scores 2). Type-switch and `select` cases
11030 // are not expression cases at all.
11031 #[cfg(feature = "go")]
11032 #[test]
11033 fn go_tagged_switch_keeps_the_per_case_count() {
11034 let src = "package main
11035func tag(x int) int { switch x { case 1, 2: return 1 }; return 0 }
11036func init2(s string) int { switch n := len(s); n { case 1: return 1 }; return 0 }
11037func tagcmp(b bool, x int) int { switch b { case x > 1: return 1 }; return 0 }
11038func ts(v any) int { switch v.(type) { case int: return 1 }; return 0 }
11039func sel(c chan int) int { select { case <-c: return 1 }; return 0 }
11040";
11041 assert_go_switch_members(
11042 src,
11043 3,
11044 &[
11045 (Go::ExpressionCase as u16, 3, "expression cases"),
11046 (Go::TypeCase as u16, 1, "`ts`'s type case"),
11047 (
11048 Go::CommunicationCase as u16,
11049 1,
11050 "`sel`'s communication case",
11051 ),
11052 ],
11053 &[
11054 ("tag", 1, 2),
11055 ("init2", 1, 2),
11056 ("tagcmp", 2, 2),
11057 ("ts", 1, 2),
11058 ("sel", 1, 2),
11059 ],
11060 );
11061 }
11062
11063 #[cfg(feature = "go")]
11064 #[test]
11065 fn go_else_counts_as_condition() {
11066 // `if a > b { ... } else { ... }` → `a > b` is one condition,
11067 // `else` is one condition → C = 2.
11068 check_metrics::<GoParser>(
11069 "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
11070 "foo.go",
11071 |metric| {
11072 assert_eq!(metric.abc.conditions_sum(), 2);
11073 insta::assert_json_snapshot!(metric.abc);
11074 },
11075 );
11076 }
11077
11078 #[cfg(feature = "go")]
11079 #[test]
11080 fn go_complex_function_abc() {
11081 // Mixed shape, verified by hand:
11082 // - Assignments: `var x = 10` (an initialized declaration, #1278),
11083 // `_ = x`, `n := 0`, `n = n + 1`, `n += 2`, `n++`,
11084 // `_ = len(s)` → A = 7. Every `_ = ...` IS counted as an
11085 // assignment_statement.
11086 // - Branches: `len(s)` → B = 1.
11087 // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
11088 // and `case 1:` (default excluded) → 2 → total C = 4.
11089 check_metrics::<GoParser>(
11090 "package main\nfunc f(s string) int {\n\
11091 \x20 var x = 10\n\
11092 \x20 _ = x\n\
11093 \x20 n := 0\n\
11094 \x20 if n < 10 { n = n + 1 } else { n += 2 }\n\
11095 \x20 n++\n\
11096 \x20 _ = len(s)\n\
11097 \x20 switch n {\n\
11098 \x20 case 0: return 0\n\
11099 \x20 case 1: return 1\n\
11100 \x20 default: return n\n\
11101 \x20 }\n\
11102 }\n",
11103 "foo.go",
11104 |metric| {
11105 assert_eq!(metric.abc.assignments_sum(), 7);
11106 assert_eq!(metric.abc.branches_sum(), 1);
11107 assert_eq!(metric.abc.conditions_sum(), 4);
11108 insta::assert_json_snapshot!(metric.abc);
11109 },
11110 );
11111 }
11112
11113 #[cfg(feature = "go")]
11114 #[test]
11115 fn go_if_multiple_conditions() {
11116 // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors
11117 // `rust_if_multiple_conditions`.
11118 check_metrics::<GoParser>(
11119 "package p\n\
11120 func F(a, b, c, d bool) int {\n\
11121 \x20 if a || b || c || d { return 1 } // +4c\n\
11122 \x20 if a && b && c { return 2 } // +3c\n\
11123 \x20 if !a && !b { return 3 } // +2c\n\
11124 \x20 return 0\n\
11125 }\n",
11126 "foo.go",
11127 |metric| {
11128 assert_eq!(metric.abc.conditions_sum(), 9);
11129 insta::assert_json_snapshot!(metric.abc);
11130 },
11131 );
11132 }
11133
11134 #[cfg(feature = "go")]
11135 #[test]
11136 fn go_for_with_conditions() {
11137 // Go has no `while` or `do { … } while(…);` — the `for` loop
11138 // header is the sole condition slot. Each operand of the
11139 // `&&` / `||` chain in the for-condition counts as one
11140 // Fitzpatrick condition.
11141 check_metrics::<GoParser>(
11142 "package p\n\
11143 func F(a, b bool) {\n\
11144 \x20 for a || b { break } // +2c\n\
11145 \x20 for a && !b { break } // +2c\n\
11146 }\n",
11147 "foo.go",
11148 |metric| {
11149 assert_eq!(metric.abc.conditions_sum(), 4);
11150 insta::assert_json_snapshot!(metric.abc);
11151 },
11152 );
11153 }
11154
11155 #[cfg(feature = "go")]
11156 #[test]
11157 fn go_for_bare_condition_counts() {
11158 // Regression for findings.md #1: `for true {}` / `for !ready {}`
11159 // are Go's only loop-condition slot. Pre-fix, the Phase-2B
11160 // dispatcher had no `G::ForStatement` arm, so bare-boolean
11161 // and `!`-wrapped `for` conditions silently reported zero.
11162 // `go_count_condition`'s terminal-bool / paren / unary filter
11163 // makes the walker safe across all three for-statement shapes:
11164 // bare condition, `for_clause` (init; cond; post) — whose own
11165 // `condition` field #1276 taught the walker to read — and
11166 // `range_clause`, which has no such field and contributes
11167 // nothing.
11168 check_metrics::<GoParser>(
11169 "package p\n\
11170 func F(ready bool) {\n\
11171 \x20 for true { break } // +1c\n\
11172 \x20 for !ready { break } // +1c\n\
11173 \x20 for i := 0; i < 3; i++ { _ = i } // +1c (the `<`)\n\
11174 }\n",
11175 "foo.go",
11176 |metric| {
11177 // `for true`: walker counts True (+1).
11178 // `for !ready`: walker on unary unwraps to `ready`
11179 // (+1).
11180 // `for_clause`'s condition is `i < 3`, a
11181 // `binary_expression` that `go_count_condition`
11182 // filters out; the `<` itself contributes 1 via the
11183 // pre-existing LT/GT arm.
11184 // Total: 3.
11185 assert_eq!(metric.abc.conditions_sum(), 3);
11186 insta::assert_json_snapshot!(metric.abc);
11187 },
11188 );
11189 }
11190
11191 // Issue #1276, Go's share. `for_statement`'s child(1) is the
11192 // condition only in the `for cond {}` spelling; the three-clause
11193 // form puts it one level down, in the `for_clause`'s `condition`
11194 // field. Letting the `for_clause` fall through — which the arm's
11195 // own comment used to call harmless — scored a bare three-clause
11196 // condition zero while `for a {}` scored one.
11197 #[cfg(feature = "go")]
11198 #[test]
11199 fn go_three_clause_for_condition_counts() {
11200 // Bare identifier in the three-clause header: no comparison
11201 // token, so only the `for_clause` lookup can count it.
11202 check_metrics::<GoParser>(
11203 "package p\n\
11204 func F(a bool) {\n\
11205 \x20 for i := 0; a; i++ { _ = i }\n\
11206 }\n",
11207 "foo.go",
11208 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11209 );
11210 // Negation, through `go_inspect_container`'s `!` unwrap.
11211 check_metrics::<GoParser>(
11212 "package p\n\
11213 func F(a bool) {\n\
11214 \x20 for i := 0; !a; i++ { _ = i }\n\
11215 }\n",
11216 "foo.go",
11217 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11218 );
11219 // Empty condition: the `for_clause` exposes no `condition`
11220 // field, so zero — the same answer as Go's bare `for {}` and
11221 // as every other language since #1276.
11222 check_metrics::<GoParser>(
11223 "package p\n\
11224 func F() {\n\
11225 \x20 for i := 0; ; i++ { break }\n\
11226 }\n",
11227 "foo.go",
11228 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
11229 );
11230 // A `range_clause` carries no condition either, and the walker
11231 // must not mistake the clause itself for one.
11232 check_metrics::<GoParser>(
11233 "package p\n\
11234 func F(xs []int) {\n\
11235 \x20 for _, v := range xs { _ = v }\n\
11236 }\n",
11237 "foo.go",
11238 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
11239 );
11240 }
11241
11242 // Go's `for_statement` is the one grammar here that exposes no
11243 // `condition` field, so its header slot is located structurally and
11244 // has to skip what the field-addressed siblings get for free. Two
11245 // things it must skip, each of which `node.child(1)` got wrong:
11246 // a leading comment (tree-sitter counts comments among a node's
11247 // children — the #1181 failure), and the body of a bare `for {}`,
11248 // which IS child(1) and would otherwise be offered to
11249 // `go_count_condition` as though it were a condition.
11250 #[cfg(feature = "go")]
11251 #[test]
11252 fn go_for_header_slot_skips_comments_and_the_body() {
11253 // Each pair is (source, expected conditions). The commented
11254 // spelling must agree with its bare twin.
11255 let cases = [
11256 ("for a { break }", 1),
11257 ("for /* n */ a { break }", 1),
11258 ("for i := 0; a; i++ { _ = i }", 1),
11259 ("for /* n */ i := 0; a; i++ { _ = i }", 1),
11260 ("for i := 0; /* n */ a; i++ { _ = i }", 1),
11261 // Bare infinite loop: the body is child(1) and must not be
11262 // read as the condition.
11263 ("for { break }", 0),
11264 ("for /* n */ { break }", 0),
11265 ("for _, v := range xs { _ = v }", 0),
11266 ("for /* n */ _, v := range xs { _ = v }", 0),
11267 ];
11268 let mut ran = 0;
11269 for (body, expected) in cases {
11270 let src = format!("package p\nfunc F(a bool, xs []int) {{\n\t{body}\n}}\n");
11271 assert_eq!(abc_conditions(LANG::Go, &src), expected, "`{body}`");
11272 ran += 1;
11273 }
11274 // Non-vacuity, both halves: the loop must actually have run
11275 // every row, and the rows must carry both answers — a walker
11276 // stuck at 0 or at 1 would otherwise pass half the table
11277 // silently.
11278 assert_eq!(ran, cases.len());
11279 assert!(cases.iter().any(|&(_, n)| n == 1));
11280 assert!(cases.iter().any(|&(_, n)| n == 0));
11281 }
11282
11283 #[cfg(feature = "go")]
11284 #[test]
11285 fn go_if_init_statement_condition_counts() {
11286 // Regression for the code-review finding: Go's
11287 // `if x := f(); x { ... }` init-statement form puts the
11288 // short-var declaration at child(1) and the condition at
11289 // child(2). Pre-fix, the dispatcher used child(1) and
11290 // counted zero conditions for this idiomatic Go shape.
11291 // The fix uses `child_by_field_name("condition")` which
11292 // returns the condition regardless of init presence.
11293 check_metrics::<GoParser>(
11294 "package p\nfunc F() { if x := g(); x { } }\n",
11295 "foo.go",
11296 |metric| {
11297 // `x` bare-identifier condition contributes 1
11298 // (Rule 6 — bare boolean identifier in if-condition).
11299 // `g()` call contributes 1 branch but no condition.
11300 assert_eq!(metric.abc.branches_sum(), 1);
11301 assert_eq!(metric.abc.conditions_sum(), 1);
11302 insta::assert_json_snapshot!(metric.abc);
11303 },
11304 );
11305 }
11306
11307 #[cfg(feature = "go")]
11308 #[test]
11309 fn go_if_boolean_literal_condition() {
11310 check_metrics::<GoParser>(
11311 "package p\n\
11312 func F() {\n\
11313 \x20 if true {} // +1c\n\
11314 \x20 if !false {} // +1c\n\
11315 }\n",
11316 "foo.go",
11317 |metric| {
11318 assert_eq!(metric.abc.conditions_sum(), 2);
11319 insta::assert_json_snapshot!(metric.abc);
11320 },
11321 );
11322 }
11323
11324 #[cfg(feature = "go")]
11325 #[test]
11326 fn go_methods_arguments_with_conditions() {
11327 check_metrics::<GoParser>(
11328 "package p\n\
11329 func F(a, b bool) {\n\
11330 \x20 m(a, b) // +1b\n\
11331 \x20 m(!a, !b) // +1b +2c\n\
11332 }\n",
11333 "foo.go",
11334 |metric| {
11335 assert_eq!(metric.abc.branches_sum(), 2);
11336 assert_eq!(metric.abc.conditions_sum(), 2);
11337 insta::assert_json_snapshot!(metric.abc);
11338 },
11339 );
11340 }
11341
11342 #[cfg(feature = "go")]
11343 #[test]
11344 fn go_return_with_conditions() {
11345 check_metrics::<GoParser>(
11346 "package p\n\
11347 func M1(z int) bool { return !(z >= 0) }\n\
11348 func M2(x bool) bool { return !x }\n\
11349 func M3(x, y bool) bool { return x && y }\n",
11350 "foo.go",
11351 |metric| {
11352 // M1: `>=` (1). `!(z >= 0)` walker on the unary
11353 // doesn't reach a terminal — stops at the
11354 // BinaryExpression z>=0 inside the parens. +1.
11355 // M2: walker on `!x` → 1.
11356 // M3: `&&` walker counts both → 2.
11357 // Sum: 1 + 1 + 2 = 4.
11358 assert_eq!(metric.abc.conditions_sum(), 4);
11359 insta::assert_json_snapshot!(metric.abc);
11360 },
11361 );
11362 }
11363
11364 #[cfg(feature = "go")]
11365 #[test]
11366 fn go_short_circuit_with_boolean_literal_operand() {
11367 // `a && true` reports 2 conditions: one identifier, one
11368 // boolean literal. Confirms the terminal set includes
11369 // `True` / `False`.
11370 check_metrics::<GoParser>(
11371 "package p\nfunc F(a bool) bool { return a && true }\n",
11372 "foo.go",
11373 |metric| {
11374 assert_eq!(metric.abc.conditions_sum(), 2);
11375 insta::assert_json_snapshot!(metric.abc);
11376 },
11377 );
11378 }
11379
11380 // ----- Elixir -----
11381
11382 // No top-level Calls and no operators → all three vectors are
11383 // zero. Uses a bare expression rather than a `defmodule` wrapper
11384 // (which would itself be a Call → 1 branch). Confirms the
11385 // ElixirCode Abc trait is wired up and the metric emits.
11386 #[cfg(feature = "elixir")]
11387 #[test]
11388 fn elixir_empty_unit_zero() {
11389 check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
11390 assert_eq!(metric.abc.assignments_sum(), 0);
11391 assert_eq!(metric.abc.branches_sum(), 0);
11392 assert_eq!(metric.abc.conditions_sum(), 0);
11393 insta::assert_json_snapshot!(metric.abc);
11394 });
11395 }
11396
11397 // An empty `defmodule Foo do ... end` is itself ONE `Call` →
11398 // Documents that module-/function-defining macros (`defmodule`,
11399 // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
11400 // directives (`alias`, `import`, `require`, `use`) are NOT
11401 // runtime dispatch and therefore do NOT inflate `branches`,
11402 // matching Cognitive's treatment.
11403 #[cfg(feature = "elixir")]
11404 #[test]
11405 fn elixir_defmodule_is_zero_branches() {
11406 check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
11407 assert_eq!(metric.abc.branches_sum(), 0);
11408 assert_eq!(metric.abc.assignments_sum(), 0);
11409 assert_eq!(metric.abc.conditions_sum(), 0);
11410 insta::assert_json_snapshot!(metric.abc);
11411 });
11412 }
11413
11414 // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
11415 // `defmodule` and `def` are declarative-Call wrappers and are
11416 // filtered out of branches; the assertion focuses on assignments
11417 // so we only pin that vector.
11418 #[cfg(feature = "elixir")]
11419 #[test]
11420 fn elixir_pattern_match_is_assignment() {
11421 check_metrics::<ElixirParser>(
11422 "defmodule Foo do\n def f do\n x = 1\n y = x + 1\n y\n end\nend\n",
11423 "foo.ex",
11424 |metric| {
11425 assert_eq!(metric.abc.assignments_sum(), 2);
11426 insta::assert_json_snapshot!(metric.abc);
11427 },
11428 );
11429 }
11430
11431 // `|>` pipeline operator: each `|>` token contributes one branch.
11432 // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
11433 // step also dispatches a Call (`String.upcase(...)`,
11434 // `String.trim(...)`) — these are wrapped inside the outer
11435 // pipeline Call tree, contributing additional Call branches.
11436 // The headline assertion confirms (a) `|>` is detected and (b)
11437 // pipeline steps are not silently dropped.
11438 #[cfg(feature = "elixir")]
11439 #[test]
11440 fn elixir_pipeline_each_step_is_branch() {
11441 check_metrics::<ElixirParser>(
11442 "defmodule Foo do\n def normalize(s) do\n s |> String.trim() |> String.upcase()\n end\nend\n",
11443 "foo.ex",
11444 |metric| {
11445 // Pipeline yields 2 `|>` branches plus Calls for
11446 // String.trim, String.upcase, and the outer pipeline
11447 // (which surfaces as a Call wrapping the binary
11448 // operator). `def` and `defmodule` are declarative
11449 // and excluded. Empirical total: B = 5.
11450 assert_eq!(metric.abc.branches_sum(), 5);
11451 assert_eq!(metric.abc.assignments_sum(), 0);
11452 insta::assert_json_snapshot!(metric.abc);
11453 },
11454 );
11455 }
11456
11457 // Comparison operators all count as conditions. Six comparisons
11458 // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
11459 #[cfg(feature = "elixir")]
11460 #[test]
11461 fn elixir_comparisons_are_conditions() {
11462 check_metrics::<ElixirParser>(
11463 "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",
11464 "foo.ex",
11465 |metric| {
11466 assert_eq!(metric.abc.conditions_sum(), 6);
11467 insta::assert_json_snapshot!(metric.abc);
11468 },
11469 );
11470 }
11471
11472 // Strict-equality operators `===` / `!==` count as conditions too.
11473 #[cfg(feature = "elixir")]
11474 #[test]
11475 fn elixir_strict_equality_is_condition() {
11476 check_metrics::<ElixirParser>(
11477 "defmodule Foo do\n def f(a, b) do\n a === b or a !== b\n end\nend\n",
11478 "foo.ex",
11479 |metric| {
11480 assert_eq!(metric.abc.conditions_sum(), 2);
11481 insta::assert_json_snapshot!(metric.abc);
11482 },
11483 );
11484 }
11485
11486 // #1461 moved every relational construct a grammar spells as its own
11487 // production onto an unconditional arm — scored by use, Fitzpatrick
11488 // Rule 5 — in C#, Java, Groovy, Kotlin and Ruby. Elixir's membership
11489 // and type tests were the same class and were not in that sweep, so
11490 // `a in b` scored zero where `a == b` scored one. The gap stayed
11491 // invisible while the `when` arm paid a flat one for every guard,
11492 // and surfaced the moment the guard became a slot: an
11493 // operator-spelled guard is owned by its operator's arm, and `in`
11494 // had none.
11495 //
11496 // `bare` / `negbare` are the by-use claim; `slot` is the §5 check
11497 // that a boolean slot does not now score the same operator twice;
11498 // `cmp` is the control the two membership rows are levelled against.
11499 // `in` rides the `<` / `>` `binary_operator` gate rather than
11500 // standing alone — it has the same three grammar positions — and
11501 // `elixir_operator_identifier_is_not_a_condition` pins that gate.
11502 #[cfg(feature = "elixir")]
11503 #[test]
11504 fn elixir_membership_is_a_condition_by_use() {
11505 check_func_space::<ElixirParser, _>(
11506 "defmodule Foo do\n\
11507 def bare(a, b), do: a in b\n\
11508 def negbare(a, b), do: a not in b\n\
11509 def slot(a, b) do\n\
11510 if a in b do\n\
11511 IO.puts(\"x\")\n\
11512 end\n\
11513 end\n\
11514 def cmp(a, b), do: a == b\n\
11515 end\n",
11516 "foo.ex",
11517 |space| {
11518 assert_members_score(
11519 &space.spaces[0],
11520 &[
11521 // Each was 0 before the arm.
11522 ("bare", 1, 1),
11523 ("negbare", 1, 1),
11524 // `in` (1) only — the `if` predicate slot is
11525 // already paid by the operator (#1527), exactly
11526 // as `if a == b` scores.
11527 ("slot", 1, 2),
11528 ("cmp", 1, 1),
11529 ],
11530 );
11531 },
11532 );
11533 }
11534
11535 // Guard `when` clause counts as a condition — exactly one, and here
11536 // it is the `>` that supplies it. The guard is a condition *slot*
11537 // (#1454), so an operator-spelled guard scores through the operator
11538 // arm that owns it and the slot itself adds nothing; see
11539 // `elixir_guard_is_a_decision_however_spelled` for the four-spelling
11540 // agreement this is one row of.
11541 //
11542 // Was 2, on a flat `+1` for the `when` token laid on top of the `>`:
11543 // the §5 double count a whole-branch review of the #1454 batch
11544 // found.
11545 #[cfg(feature = "elixir")]
11546 #[test]
11547 fn elixir_guard_when_is_condition() {
11548 check_metrics::<ElixirParser>(
11549 "defmodule Foo do\n def f(x) when x > 0 do\n :pos\n end\nend\n",
11550 "foo.ex",
11551 |metric| {
11552 // the guard slot (+0, the guard is an operator
11553 // application) + `>` (+1) = 1
11554 assert_eq!(metric.abc.conditions_sum(), 1);
11555 insta::assert_json_snapshot!(metric.abc);
11556 },
11557 );
11558 }
11559
11560 // A `case` pays one condition per clause (#1531) AND one branch for
11561 // the Call itself. The `_ ->` catch-all is the default arm and pays
11562 // nothing, so this two-clause `case` scores 1; the per-clause rule
11563 // is pinned by `elixir_clause_constructs_pay_per_clause`.
11564 #[cfg(feature = "elixir")]
11565 #[test]
11566 fn elixir_case_is_condition_and_branch() {
11567 check_metrics::<ElixirParser>(
11568 "defmodule Foo do\n def f(x) do\n case x do\n 1 -> :one\n _ -> :other\n end\n end\nend\n",
11569 "foo.ex",
11570 |metric| {
11571 // conditions: `1 ->` (+1) + `_ ->` default (+0) = 1
11572 assert_eq!(metric.abc.conditions_sum(), 1);
11573 insta::assert_json_snapshot!(metric.abc);
11574 },
11575 );
11576 }
11577
11578 // `cond` scores per clause, as its nested-`if` twin does (#1527): the
11579 // `x > 0` clause is a predicate slot its `>` already pays, and the
11580 // `true ->` catch-all is the default arm and pays nothing.
11581 #[cfg(feature = "elixir")]
11582 #[test]
11583 fn elixir_cond_is_condition() {
11584 check_metrics::<ElixirParser>(
11585 "defmodule Foo do\n def f(x) do\n cond do\n x > 0 -> :pos\n true -> :other\n end\n end\nend\n",
11586 "foo.ex",
11587 |metric| {
11588 // conditions: `x > 0` clause slot (+0) + `>` (+1) +
11589 // `true ->` default (+0) = 1, against cyclomatic 2
11590 assert_eq!(metric.abc.conditions_sum(), 1);
11591 insta::assert_json_snapshot!(metric.abc);
11592 },
11593 );
11594 }
11595
11596 // An `if` / `unless` predicate and each `cond` clause are condition
11597 // slots (#1527): one condition, unless an operator in the slot
11598 // already paid. Before, the keyword Call paid a flat one on top of
11599 // its predicate, so `if x > 5` scored 2 and `cond` paid once per
11600 // construct whatever its clause count.
11601 //
11602 // Expected values come from the twins, not the rule: each `if` row
11603 // is what Ruby and Java score for the same `if` (`if x > 5` 1,
11604 // `if b` 1, `if a && b` 2 — measured), every non-chain row sits at
11605 // `cyclomatic - 1`, and `cond3` equals Go's tagless
11606 // `switch { case x > 5: … case b: … default: … }` (2), whose
11607 // `default:` is as free as `cond`'s `true ->`. Its nested-`if` twin
11608 // `nested` pays Fitzpatrick Rule 5 for each `else` (#1531), as
11609 // Ruby's `if … elsif … else` and Java's `if … else if … else` do (4
11610 // against cyclomatic 3 — measured). The chain rows sit one under
11611 // parity exactly as Ruby's and Java's `if a && b` (2 / 3) do.
11612 //
11613 // Paths only one row exercises (§11): `cmp_comment` the comment
11614 // skip in a parenthesised peel, `assign` the match-`=` peel,
11615 // `negcmp` the `not` peel, `arith` a non-comparison operator the
11616 // slot pays for, `capture` a chain operand that no terminal list
11617 // held (`&f/1` scored one short of `a && b` on main), and the
11618 // `true ->` rows the default-arm gate shared with cyclomatic.
11619 #[cfg(feature = "elixir")]
11620 #[test]
11621 fn elixir_if_and_cond_predicates_are_slots() {
11622 check_func_space::<ElixirParser, _>(
11623 "defmodule Foo do
11624 def cmp(x), do: (if x > 5 do 1 end)
11625 def bare(b), do: (if b do 1 end)
11626 def kw(x), do: if(x > 5, do: 1)
11627 def unless_cmp(x), do: (unless x > 5 do 1 end)
11628 def cmp_comment(x) do
11629 if (x > 5 # c
11630 ) do
11631 1
11632 end
11633 end
11634 def assign(x), do: (if (y = x > 5) do y end)
11635 def negcmp(x), do: (if not(x > 5) do 1 end)
11636 def arith(x), do: (if x + 1 do 1 end)
11637 def chain(a, b), do: (if a && b do 1 end)
11638 def capture(a), do: (if a && &f/1 do 1 end)
11639 def cond_cmp(x) do
11640 cond do
11641 x > 5 -> 1
11642 true -> 2
11643 end
11644 end
11645 def cond_bare(b) do
11646 cond do
11647 b -> 1
11648 true -> 2
11649 end
11650 end
11651 def cond3(x, b) do
11652 cond do
11653 x > 5 -> 1
11654 b -> 2
11655 true -> 3
11656 end
11657 end
11658 def nested(x, b) do
11659 if x > 5 do
11660 1
11661 else
11662 if b do 2 else 3 end
11663 end
11664 end
11665 def cond_no_default(a, b, c) do
11666 cond do
11667 a -> 1
11668 b -> 2
11669 c -> 3
11670 end
11671 end
11672end
11673",
11674 "foo.ex",
11675 |space| {
11676 assert_members_score(
11677 &space.spaces[0],
11678 &[
11679 ("cmp", 1, 2),
11680 ("bare", 1, 2),
11681 ("kw", 1, 2),
11682 ("unless_cmp", 1, 2),
11683 ("cmp_comment", 1, 2),
11684 ("assign", 1, 2),
11685 ("negcmp", 1, 2),
11686 ("arith", 1, 2),
11687 ("chain", 2, 3),
11688 ("capture", 2, 3),
11689 ("cond_cmp", 1, 2),
11690 ("cond_bare", 1, 2),
11691 ("cond3", 2, 3),
11692 ("nested", 4, 3),
11693 ("cond_no_default", 3, 4),
11694 ],
11695 );
11696 },
11697 );
11698 }
11699
11700 // Every clause construct pays one condition per clause (#1531), as
11701 // `Cyclomatic` counts one decision per clause through the same gate.
11702 // `case` and `with` paid once per construct and the others not at
11703 // all, so a two-clause `case` scored 1 against Ruby's
11704 // `case x; when 1; when 2; end`, Go's `switch` and Rust's `match`
11705 // at 2 (measured).
11706 //
11707 // Expected values come from cyclomatic, which this fix did not move:
11708 // every row sits at `cyclomatic - 1`. Paths only one row exercises
11709 // (§11): `case_default` the free `_ ->`, `case_guard` a guarded
11710 // clause's flat one beside its `>`, `rescue_in` a sole pattern that
11711 // scores itself (the `in` arm), `rescue_alias` a sole pattern the
11712 // clause slot pays for, `catch_two` a multi-pattern clause, `recv` a
11713 // `receive`'s `after` timeout clause, and `with_else` the `else` of
11714 // a `with`, whose clauses pay rather than a Rule 5 `else`. Its `<-`
11715 // clause pays one more since #1535, in both metrics.
11716 #[cfg(feature = "elixir")]
11717 #[test]
11718 fn elixir_clause_constructs_pay_per_clause() {
11719 check_func_space::<ElixirParser, _>(
11720 "defmodule Foo do
11721 def case2(x) do
11722 case x do
11723 1 -> :a
11724 2 -> :b
11725 end
11726 end
11727 def case_default(x) do
11728 case x do
11729 1 -> :a
11730 2 -> :b
11731 _ -> :c
11732 end
11733 end
11734 def case_guard(x) do
11735 case x do
11736 y when y > 0 -> :a
11737 _ -> :b
11738 end
11739 end
11740 def with_else(x) do
11741 with {:ok, a} <- g(x) do
11742 a
11743 else
11744 {:error, e} -> e
11745 _ -> nil
11746 end
11747 end
11748 def recv(x) do
11749 receive do
11750 {:a, y} -> y
11751 {:b, z} -> z
11752 after
11753 100 -> x
11754 end
11755 end
11756 def rescue_in() do
11757 try do
11758 g()
11759 rescue
11760 e in RuntimeError -> e
11761 end
11762 end
11763 def rescue_alias() do
11764 try do
11765 g()
11766 rescue
11767 RuntimeError -> 1
11768 end
11769 end
11770 def catch_two() do
11771 try do
11772 g()
11773 catch
11774 :exit, r -> r
11775 end
11776 end
11777end
11778",
11779 "foo.ex",
11780 |space| {
11781 assert_members_score(
11782 &space.spaces[0],
11783 &[
11784 ("case2", 2, 3),
11785 ("case_default", 2, 3),
11786 ("case_guard", 2, 3),
11787 ("with_else", 2, 3),
11788 ("recv", 3, 4),
11789 ("rescue_in", 1, 2),
11790 ("rescue_alias", 1, 2),
11791 ("catch_two", 1, 2),
11792 ],
11793 );
11794 },
11795 );
11796 }
11797
11798 // A multi-clause anonymous fn dispatches like a `case`, less its head
11799 // clause, which is the closure's definition (#776): `fn x -> x end`
11800 // has no decision and `fn 0 -> :a; _ -> :b end` one — the trailing
11801 // `_ ->` included, since the head already took the free path.
11802 // Expected: each total equals the closure's cyclomatic decisions.
11803 #[cfg(feature = "elixir")]
11804 #[test]
11805 fn elixir_multi_clause_fn_pays_after_its_head() {
11806 for (source, conditions) in [
11807 ("f = fn x -> x end\n", 0),
11808 ("f = fn 0 -> :a; _ -> :b end\n", 1),
11809 ("f = fn 0 -> :a; 1 -> :b; _ -> :c end\n", 2),
11810 ] {
11811 let metrics =
11812 metrics_verbatim(LANG::Elixir, source.as_bytes(), MetricsOptions::default());
11813 assert_eq!(metrics.abc.conditions_sum(), conditions, "{source}");
11814 }
11815 }
11816
11817 // A typespec's function type (`(any -> any)`) is a `stab_clause` too,
11818 // under a parenthesised `block`, and is type syntax with no decision
11819 // behind it. Cyclomatic counted each one; the shared clause gate
11820 // would have carried that into ABC (#1531). Expected: nothing but the
11821 // unit's base path, as for the typespec `when` in
11822 // `elixir_typespec_when_is_not_a_guard`.
11823 #[cfg(feature = "elixir")]
11824 #[test]
11825 fn elixir_typespec_function_type_is_not_a_clause() {
11826 let metrics = metrics_verbatim(
11827 LANG::Elixir,
11828 b"@spec f(list, (any -> any)) :: list\n@type cb :: (integer, integer -> boolean)\n",
11829 MetricsOptions::default(),
11830 );
11831 assert_eq!(metrics.abc.conditions_sum(), 0);
11832 assert_eq!(metrics.cyclomatic.cyclomatic_sum(), 1);
11833 }
11834
11835 // The keyword form of each clause construct holds its clauses in a
11836 // parenthesised `block` under a `do:` / `else:` / `rescue:` pair, the
11837 // shape a typespec function type also has. Excluding `block` for the
11838 // typespec dropped every keyword-form clause (`ck` read 1/4, and `rk`
11839 // 0/3); each `*k` row must equal its `*b` block-form twin, `dk`'s
11840 // `true ->` included (it was a decision in the keyword form).
11841 #[cfg(feature = "elixir")]
11842 #[test]
11843 fn elixir_keyword_form_clauses_match_block_form() {
11844 let src = "defmodule M do
11845 def ck(x), do: case(x, do: (1 -> :a; 2 -> :b; 3 -> :c))
11846 def cb(x) do
11847 case x do
11848 1 -> :a
11849 2 -> :b
11850 3 -> :c
11851 end
11852 end
11853 def dk(x), do: cond(do: (x > 1 -> :a; x < 0 -> :b; true -> :c))
11854 def db(x) do
11855 cond do
11856 x > 1 -> :a
11857 x < 0 -> :b
11858 true -> :c
11859 end
11860 end
11861 def rk(), do: receive(do: ({:a, y} -> y; {:b, z} -> z))
11862 def rb() do
11863 receive do
11864 {:a, y} -> y
11865 {:b, z} -> z
11866 end
11867 end
11868 def tk(), do: try(do: g(), rescue: (e in RuntimeError -> e; _ -> 2))
11869 def tb() do
11870 try do
11871 g()
11872 rescue
11873 e in RuntimeError -> e
11874 _ -> 2
11875 end
11876 end
11877 def wk(x), do: with({:ok, a} <- x, do: a, else: ({:error, e} -> e; _ -> 0))
11878 def wb(x) do
11879 with {:ok, a} <- x do
11880 a
11881 else
11882 {:error, e} -> e
11883 _ -> 0
11884 end
11885 end
11886end
11887";
11888 assert_fixture_spells::<ElixirParser>(
11889 src,
11890 "foo.ex",
11891 &[
11892 (Elixir::Block as u16, 5, "the five keyword-form sections"),
11893 (Elixir::StabClause as u16, 24, "clauses, 12 per spelling"),
11894 ],
11895 );
11896 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
11897 assert_members_score(
11898 &space.spaces[0],
11899 &[
11900 ("ck", 3, 4),
11901 ("cb", 3, 4),
11902 ("dk", 2, 3),
11903 ("db", 2, 3),
11904 ("rk", 2, 3),
11905 ("rb", 2, 3),
11906 ("tk", 1, 2),
11907 ("tb", 1, 2),
11908 ("wk", 2, 3),
11909 ("wb", 2, 3),
11910 ],
11911 );
11912 });
11913 }
11914
11915 // Fitzpatrick Rule 5 counts the `else` of an `if` / `unless` (#1531),
11916 // in both of Elixir's spellings. Expected values are the twins':
11917 // Ruby's `if b … else … end`, Java's `if (b) {} else {}`, Python's
11918 // and Go's each score 2 against cyclomatic 2 (measured), one over
11919 // parity by the documented `else` arm. `kw_other` is the control
11920 // that the `else:` key is read only on an `if` / `unless`: on any
11921 // other call it is an ordinary keyword argument.
11922 #[cfg(feature = "elixir")]
11923 #[test]
11924 fn elixir_if_else_pays_rule_5() {
11925 check_func_space::<ElixirParser, _>(
11926 "defmodule Foo do
11927 def block(b) do
11928 if b do
11929 1
11930 else
11931 2
11932 end
11933 end
11934 def kw(b), do: if(b, do: 1, else: 2)
11935 def unless_block(b) do
11936 unless b do
11937 1
11938 else
11939 2
11940 end
11941 end
11942 def cmp(x) do
11943 if x > 5 do
11944 1
11945 else
11946 2
11947 end
11948 end
11949 def no_else(b) do
11950 if b do
11951 1
11952 end
11953 end
11954 def kw_no_else(b), do: if(b, do: 1)
11955 def kw_other(b), do: g(b, else: 1)
11956end
11957",
11958 "foo.ex",
11959 |space| {
11960 assert_members_score(
11961 &space.spaces[0],
11962 &[
11963 ("block", 2, 2),
11964 ("kw", 2, 2),
11965 ("unless_block", 2, 2),
11966 ("cmp", 2, 2),
11967 ("no_else", 1, 2),
11968 ("kw_no_else", 1, 2),
11969 ("kw_other", 0, 1),
11970 ],
11971 );
11972 },
11973 );
11974 }
11975
11976 // `for` is a comprehension/loop, NOT in the issue's condition
11977 // list. It is still a Call so it contributes one branch, but no
11978 // condition.
11979 #[cfg(feature = "elixir")]
11980 #[test]
11981 fn elixir_for_is_branch_not_condition() {
11982 check_metrics::<ElixirParser>(
11983 "defmodule Foo do\n def f(xs) do\n for x <- xs, do: x * 2\n end\nend\n",
11984 "foo.ex",
11985 |metric| {
11986 assert_eq!(metric.abc.conditions_sum(), 0);
11987 insta::assert_json_snapshot!(metric.abc);
11988 },
11989 );
11990 }
11991
11992 // Mixed shape, verified by hand: defmodule Call + def Call + if Call
11993 // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
11994 // - Assignments: `x = 1` → A = 1.
11995 // - Branches: `defmodule` and `def` are declarative and excluded;
11996 // `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
11997 // - Conditions: `x > 0` → 1; the `if` predicate slot it fills adds
11998 // nothing more (#1527) → C = 1.
11999 #[cfg(feature = "elixir")]
12000 #[test]
12001 fn elixir_mixed_abc() {
12002 check_metrics::<ElixirParser>(
12003 "defmodule Foo do\n def f do\n x = 1\n if x > 0 do\n side_effect()\n end\n end\nend\n",
12004 "foo.ex",
12005 |metric| {
12006 assert_eq!(metric.abc.assignments_sum(), 1);
12007 assert_eq!(metric.abc.branches_sum(), 2);
12008 assert_eq!(metric.abc.conditions_sum(), 1);
12009 insta::assert_json_snapshot!(metric.abc);
12010 },
12011 );
12012 }
12013
12014 #[cfg(feature = "elixir")]
12015 #[test]
12016 fn elixir_unary_conditions_in_chain() {
12017 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
12018 // `&&` / `||` chain is one condition. For `if a && b || c` the
12019 // walker adds a, b, c → 3, and the `if` predicate slot adds
12020 // nothing because the chain already scored it (#1527). expected:
12021 // 3, as Ruby and Java score the same `if`, against cyclomatic 4
12022 // (base 1 + if + && + ||).
12023 check_metrics::<ElixirParser>(
12024 "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",
12025 "foo.ex",
12026 |metric| {
12027 assert_eq!(metric.abc.conditions_sum(), 3);
12028 },
12029 );
12030 }
12031
12032 // Elixir spells negation two ways and the operand peel once
12033 // recognised only `!`, so `a && not b` scored 3 where `a && !b`
12034 // scored 4 — the `not` operand reached no terminal and vanished.
12035 // Both forms now read the same. `not` is the stricter of the two (it
12036 // raises on a non-boolean operand where `!` accepts any truthy
12037 // value), so it is at least as good a proof that what it wraps is
12038 // boolean, which is all the walker's flag claims.
12039 //
12040 // Both members in one fixture because the claim is that they agree:
12041 // asserting either alone says nothing about the pair (§11), and a
12042 // shared `check_metrics` total could not tell 3 + 4 from 4 + 3.
12043 //
12044 // `assert_members_score` rather than the parity-asserting sibling:
12045 // each member scores its two `&&` operands and nothing for the `if`
12046 // slot the chain fills (#1527) — 2 against cyclomatic
12047 // `base 1 + if + &&` = 3, as Ruby's `if a && !b` does.
12048 #[cfg(feature = "elixir")]
12049 #[test]
12050 fn elixir_keyword_not_negates_like_bang() {
12051 check_func_space::<ElixirParser, _>(
12052 "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",
12053 "foo.ex",
12054 |space| {
12055 assert_members_score(&space.spaces[0], &[("kw", 2, 3), ("bang", 2, 3)]);
12056 },
12057 );
12058 }
12059
12060 #[cfg(feature = "elixir")]
12061 #[test]
12062 fn elixir_comparison_operands_add_nothing() {
12063 // Isolation check: comparison operands of a `&&` chain are nested
12064 // `binary_operator` nodes, not bare boolean leaves, so the walker
12065 // adds nothing. expected: 2 = `>` (1) + `>` (1); the `&&` walker
12066 // and the `if` slot the chain fills contribute 0 (#1527).
12067 check_metrics::<ElixirParser>(
12068 "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",
12069 "foo.ex",
12070 |metric| {
12071 assert_eq!(metric.abc.conditions_sum(), 2);
12072 },
12073 );
12074 }
12075
12076 #[cfg(feature = "elixir")]
12077 #[test]
12078 fn elixir_keyword_and_or_chain_counts_operands() {
12079 // The keyword forms `and` / `or` get the same Rule 9 treatment as
12080 // `&&` / `||`. expected: 3 = operands a, b, c; the `if` slot the
12081 // chain fills adds nothing (#1527).
12082 check_metrics::<ElixirParser>(
12083 "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",
12084 "foo.ex",
12085 |metric| {
12086 assert_eq!(metric.abc.conditions_sum(), 3);
12087 },
12088 );
12089 }
12090
12091 // Sigil delimiter choice must not move ABC: `~s<hi>` and `~s(hi)`
12092 // are the same value spelled differently, but the `<` / `>`
12093 // delimiter tokens carry the comparison kind ids, so the unguarded
12094 // condition arm scored `~s<hi>` as 2 conditions and `~s(hi)` as 0.
12095 // The parent-is-`Sigil` guard (mirroring the Halstead getter's,
12096 // #1256) suppresses the delimiter case. expected, for each
12097 // spelling: A = 1 (the `x =` pattern match), B = 0 (a bare sigil
12098 // is not a `Call` node — verified by AST dump: `binary_operator`
12099 // wrapping `identifier`, `=`, `sigil`), C = 0 (no comparison, no
12100 // guard, no keyword Call).
12101 #[cfg(feature = "elixir")]
12102 #[test]
12103 fn elixir_sigil_delimiter_choice_is_abc_invariant() {
12104 for src in ["x = ~s<hi>\n", "x = ~s(hi)\n"] {
12105 check_metrics::<ElixirParser>(src, "foo.ex", |metric| {
12106 assert_eq!(metric.abc.assignments_sum(), 1);
12107 assert_eq!(metric.abc.branches_sum(), 0);
12108 assert_eq!(metric.abc.conditions_sum(), 0);
12109 });
12110 }
12111 }
12112
12113 // Control for the guard above: `<` *outside* a sigil is a genuine
12114 // comparison and must keep counting even with a `<`-delimited
12115 // sigil in the same unit. expected: A = 2 (`x =`, `y =`), C = 1
12116 // (only `a < b`; the sigil's `<` / `>` delimiters are guarded).
12117 #[cfg(feature = "elixir")]
12118 #[test]
12119 fn elixir_lt_comparison_still_counts_beside_sigil() {
12120 check_metrics::<ElixirParser>("x = ~s<hi>\ny = a < b\n", "foo.ex", |metric| {
12121 assert_eq!(metric.abc.assignments_sum(), 2);
12122 assert_eq!(metric.abc.conditions_sum(), 1);
12123 });
12124 }
12125
12126 // #1297's sweep converted six grammars from a denylist of
12127 // non-comparison `<` / `>` parents to an allowlist and recorded
12128 // Elixir as already covered by its sigil guard. It was not: an
12129 // operator *named* rather than applied — the capture `&</2` and the
12130 // qualified call `Kernel.<(a, b)` — puts the bare token under an
12131 // `operator_identifier`, which no denylist entry mentioned, so each
12132 // scored a condition against zero decisions. Same shape as the C#
12133 // `operator <` declaration the sweep did fix.
12134 //
12135 // Each function is its own space and its own row, so a fix that
12136 // reached one spelling and not the other fails here. `max/2` is the
12137 // control: a qualified call whose name is not an operator was
12138 // always 0, so a guard that merely stopped counting `<` everywhere
12139 // would pass this row and fail the two below it.
12140 //
12141 // The six equality and ordering tokens share the gate (#1531): they
12142 // sat on an ungated arm, so `&==/2` and `Kernel.==(a, b)` scored a
12143 // condition each and `caps` five. `eq` is their applied control.
12144 #[cfg(feature = "elixir")]
12145 #[test]
12146 fn elixir_operator_identifier_is_not_a_condition() {
12147 check_func_space::<ElixirParser, _>(
12148 "defmodule M do\n\
12149 def asc(l), do: Enum.sort(l, &</2)\n\
12150 def lt(a, b), do: Kernel.<(a, b)\n\
12151 def big(a, b), do: Kernel.max(a, b)\n\
12152 def cmp(a, b), do: a < b\n\
12153 def eq_cap(l), do: Enum.sort(l, &==/2)\n\
12154 def eq_remote(a, b), do: Kernel.==(a, b)\n\
12155 def caps(), do: {&===/2, &!=/2, &!==/2, &<=/2, &>=/2}\n\
12156 def eq(a, b), do: a == b\n\
12157 end\n",
12158 "foo.ex",
12159 |space| {
12160 let module = &space.spaces[0];
12161 let scored: Vec<(String, u64)> = module
12162 .spaces
12163 .iter()
12164 .map(|f| {
12165 (
12166 f.name.clone().unwrap_or_default(),
12167 f.metrics.abc.conditions(),
12168 )
12169 })
12170 .collect();
12171 assert_eq!(
12172 scored,
12173 vec![
12174 ("asc".to_owned(), 0),
12175 ("lt".to_owned(), 0),
12176 ("big".to_owned(), 0),
12177 ("cmp".to_owned(), 1),
12178 ("eq_cap".to_owned(), 0),
12179 ("eq_remote".to_owned(), 0),
12180 ("caps".to_owned(), 0),
12181 ("eq".to_owned(), 1),
12182 ],
12183 );
12184 },
12185 );
12186 }
12187
12188 // ----- C++ -----
12189
12190 #[cfg(feature = "cpp")]
12191 #[test]
12192 fn cpp_empty_unit_zero() {
12193 // No code → A=B=C=0. Wires up the trait and exercises the
12194 // per-language compute reachability.
12195 check_metrics::<CppParser>("", "empty.cpp", |metric| {
12196 assert_eq!(metric.abc.assignments_sum(), 0);
12197 assert_eq!(metric.abc.branches_sum(), 0);
12198 assert_eq!(metric.abc.conditions_sum(), 0);
12199 insta::assert_json_snapshot!(metric.abc);
12200 });
12201 }
12202
12203 #[cfg(feature = "cpp")]
12204 #[test]
12205 fn cpp_plain_and_compound_assignments_count() {
12206 // `int x = 0` is an `init_declarator` carrying an `=` token
12207 // and counts as 1 (post-#393: the literal Fitzpatrick rule
12208 // counts every `=` operator, matching the JS impl's
12209 // `let x = 5` treatment). `x = 5`, `x += 2`, `x = 7` all
12210 // parse as `assignment_expression` → 3. Total A = 4.
12211 check_metrics::<CppParser>(
12212 "void f() { int x = 0; x = 5; x += 2; x = 7; }",
12213 "foo.cpp",
12214 |metric| {
12215 assert_eq!(metric.abc.assignments_sum(), 4);
12216 assert_eq!(metric.abc.branches_sum(), 0);
12217 assert_eq!(metric.abc.conditions_sum(), 0);
12218 insta::assert_json_snapshot!(metric.abc);
12219 },
12220 );
12221 }
12222
12223 #[cfg(feature = "cpp")]
12224 #[test]
12225 fn cpp_increment_and_decrement_count_as_assignment() {
12226 // `x++` / `--x` / prefix and postfix forms each parse as
12227 // `update_expression` and count as 1 assignment per
12228 // Fitzpatrick — 4. `int x = 0` (init_declarator with `=`)
12229 // adds 1 (post-#393). Total A = 5.
12230 check_metrics::<CppParser>(
12231 "void f() { int x = 0; x++; --x; ++x; x--; }",
12232 "foo.cpp",
12233 |metric| {
12234 assert_eq!(metric.abc.assignments_sum(), 5);
12235 insta::assert_json_snapshot!(metric.abc);
12236 },
12237 );
12238 }
12239
12240 #[cfg(feature = "cpp")]
12241 #[test]
12242 fn cpp_init_declarators_count_as_assignments() {
12243 // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`,
12244 // `int d=0;` are all `init_declarator` nodes with `=` → 4
12245 // assignments. `d=5;` is one plain `assignment_expression`,
12246 // `d+=1;` is one compound. Total A = 4 + 1 + 1 = 6.
12247 check_metrics::<CppParser>(
12248 "void f() { int a=1; int b=2; int c=a+b; int d=0; d=5; d+=1; }",
12249 "foo.cpp",
12250 |metric| {
12251 assert_eq!(metric.abc.assignments_sum(), 6);
12252 insta::assert_json_snapshot!(metric.abc);
12253 },
12254 );
12255 }
12256
12257 #[cfg(feature = "cpp")]
12258 #[test]
12259 fn cpp_declaration_without_initializer_does_not_count() {
12260 // `int a;` parses as a plain declarator inside `declaration`,
12261 // NOT an `init_declarator` (the latter only appears when an
12262 // initializer is present). Regression test for issue #393:
12263 // un-initialised declarations contribute zero to A.
12264 check_metrics::<CppParser>("void f() { int a; a = 5; }", "foo.cpp", |metric| {
12265 // Only `a = 5` (assignment_expression) → A = 1.
12266 assert_eq!(metric.abc.assignments_sum(), 1);
12267 insta::assert_json_snapshot!(metric.abc);
12268 });
12269 }
12270
12271 #[cfg(feature = "cpp")]
12272 #[test]
12273 fn cpp_init_declarator_brace_paren_init_does_not_count() {
12274 // `init_declarator` has two grammar forms: `declarator = value`
12275 // (the `=` form) and `declarator argument_list_or_initializer_list`
12276 // (the `int x(5);` / `int x{5};` direct-init forms). Only the
12277 // first form contains an `=` token, so only it should count.
12278 // Regression test pinning that distinction so that
12279 // refactorings of the init_declarator arm don't accidentally
12280 // start counting direct-init too.
12281 check_metrics::<CppParser>(
12282 "void f() { int x(5); int y{7}; x = 1; }",
12283 "foo.cpp",
12284 |metric| {
12285 // Only `x = 1` (assignment_expression) → A = 1.
12286 assert_eq!(metric.abc.assignments_sum(), 1);
12287 insta::assert_json_snapshot!(metric.abc);
12288 },
12289 );
12290 }
12291
12292 #[cfg(feature = "cpp")]
12293 #[test]
12294 fn cpp_calls_are_branches() {
12295 // Free call + member-fn call (parses as `call_expression` with
12296 // a `field_expression` callee) + `new` allocation. All three
12297 // are branches → B = 3. `auto* p = new int(5)` is also an
12298 // `init_declarator` with `=` so it contributes one assignment
12299 // (post-#393); the snapshot pins that magnitude.
12300 check_metrics::<CppParser>(
12301 "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
12302 "foo.cpp",
12303 |metric| {
12304 assert_eq!(metric.abc.branches_sum(), 3);
12305 assert_eq!(metric.abc.assignments_sum(), 1);
12306 insta::assert_json_snapshot!(metric.abc);
12307 },
12308 );
12309 }
12310
12311 #[cfg(feature = "cpp")]
12312 #[test]
12313 fn cpp_comparisons_count_conditions() {
12314 // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
12315 // `<=>` each contribute one condition. The `||` short-
12316 // circuits add 0 (Fitzpatrick Rule 5, issue #395). Six
12317 // comparisons in the `||` chain plus `<=>` (1) plus the
12318 // outer `== 0` (1) → C = 8.
12319 check_metrics::<CppParser>(
12320 "#include <compare>\n\
12321 bool f(int a, int b) {\n\
12322 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
12323 }\n",
12324 "foo.cpp",
12325 |metric| {
12326 // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 comparisons
12327 // from the chained `||` expression. `(a <=> b) == 0`
12328 // adds the spaceship `<=>` (1) + the outer `== 0`
12329 // (1) → 8 total. The six `||` short-circuits add 0
12330 // (Fitzpatrick Rule 5; issue #395).
12331 assert_eq!(metric.abc.conditions_sum(), 8);
12332 insta::assert_json_snapshot!(metric.abc);
12333 },
12334 );
12335 }
12336
12337 #[cfg(feature = "cpp")]
12338 #[test]
12339 fn cpp_short_circuit_ops_not_counted_directly() {
12340 // `&&` and `||` do NOT count on their own (see the
12341 // module-level `Stats` doc-comment; #395). Phase-2 walker
12342 // counts each operand of a logical chain once (#403), but
12343 // when every operand is itself a relational expression
12344 // (`a == b`, `a > 0`, `b < 0`) the walker doesn't add
12345 // anything on top of the existing comparison-token tally
12346 // — a relational sub-expression scores itself
12347 // (`cpp_condition_scores_itself`), so its chain slot pays
12348 // nothing on top.
12349 check_metrics::<CppParser>(
12350 "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
12351 "foo.cpp",
12352 |metric| {
12353 // == 1, > 1, < 1; the walker on && and || finds
12354 // BinaryExpression operands (not terminal-bool) and
12355 // adds nothing. Total: 3.
12356 assert_eq!(metric.abc.conditions_sum(), 3);
12357 insta::assert_json_snapshot!(metric.abc);
12358 },
12359 );
12360 }
12361
12362 #[cfg(feature = "cpp")]
12363 #[test]
12364 fn cpp_generic_brackets_not_conditions() {
12365 // `<` / `>` in `std::vector<int>` are `template_argument_list`
12366 // delimiters, NOT comparison operators. The `binary_expression`
12367 // parent check must filter them out → C = 0.
12368 check_metrics::<CppParser>(
12369 "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
12370 "foo.cpp",
12371 |metric| {
12372 assert_eq!(metric.abc.conditions_sum(), 0);
12373 insta::assert_json_snapshot!(metric.abc);
12374 },
12375 );
12376 }
12377
12378 #[cfg(feature = "cpp")]
12379 #[test]
12380 fn cpp_else_and_ternary_count_conditions() {
12381 // `if (cond) ... else ...` + ternary `cond ? a : b`. The
12382 // `if`-keyword is NOT a condition (its condition is the
12383 // comparison inside, which counts separately). `else` adds 1,
12384 // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
12385 check_metrics::<CppParser>(
12386 "int f(int a, int b) {\n\
12387 if (a > b) { return a; } else { return b; }\n\
12388 return (b < 0) ? -b : b;\n\
12389 }\n",
12390 "foo.cpp",
12391 |metric| {
12392 assert_eq!(metric.abc.conditions_sum(), 4);
12393 insta::assert_json_snapshot!(metric.abc);
12394 },
12395 );
12396 }
12397
12398 // Issue #1102. A ternary's condition and both branch operands are
12399 // Fitzpatrick Rule 9 unary conditions, exactly as `java_walk_ternary`
12400 // has always counted them. Before the fix the C family scored
12401 // `a ? !b : !c` as 1 — the `?` token alone — against Java's 4.
12402 #[cfg(feature = "cpp")]
12403 #[test]
12404 fn cpp_ternary_operand_slots_count_as_unary_conditions() {
12405 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
12406 check_metrics::<CppParser>("void f() { x = a ? !b : !c; }", "foo.cpp", |metric| {
12407 assert_eq!(metric.abc.conditions_sum(), 4);
12408 });
12409 // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
12410 // fix. The parenthesised condition peels to a
12411 // `binary_expression`, which scores itself, and
12412 // neither branch is negated — the `!` is the type-free proxy for
12413 // "this operand is boolean", so an unnegated branch contributes
12414 // nothing.
12415 check_metrics::<CppParser>("void f() { x = (a > 0) ? b : -b; }", "foo.cpp", |metric| {
12416 assert_eq!(metric.abc.conditions_sum(), 2);
12417 });
12418 // Nested: outer `?` (1) + outer condition `a` (1) + inner `?`
12419 // (1) + inner condition `b` (1) = 4. The outer consequence is
12420 // the inner ternary — which scores itself, and is no
12421 // paren / `!` wrapper — so it adds nothing on its own and the
12422 // inner one is reached by the walk, not by descent.
12423 check_metrics::<CppParser>("void f() { x = a ? b ? c : d : e; }", "foo.cpp", |metric| {
12424 assert_eq!(metric.abc.conditions_sum(), 4);
12425 });
12426 // A negated *condition* is the input where the slot must peel a
12427 // `!`: `cpp_wrapper_operand` takes `!a` down to `a`, which does
12428 // not score itself, so the slot pays once. Every other condition
12429 // fixture in this file wraps a comparison, which scores itself
12430 // and so pays 0 from the slot. `?` (1) + `!a` (1) = 2.
12431 check_metrics::<CppParser>("void f() { x = !a ? b : c; }", "foo.cpp", |metric| {
12432 assert_eq!(metric.abc.conditions_sum(), 2);
12433 });
12434 }
12435
12436 // The GNU short-ternary `a ?: b` elides the consequence, so the
12437 // C-family grammar marks that field optional and the alternative
12438 // lands at child(3) rather than child(4). Addressing the operand
12439 // slots by grammar field name — never by index — is what keeps `!b`
12440 // counted here; a fixed `child(4)` reads `None` and scores 2.
12441 #[cfg(feature = "cpp")]
12442 #[test]
12443 fn cpp_elided_ternary_consequence_still_walks_the_alternative() {
12444 // `?` (1) + condition `a` (1) + `!b` (1) = 3.
12445 check_metrics::<CppParser>("void f() { x = a ?: !b; }", "foo.cpp", |metric| {
12446 assert_eq!(metric.abc.conditions_sum(), 3);
12447 });
12448 }
12449
12450 // `cpp_walk_ternary` is shared by the C, ObjC, and Mozcpp ABC impls
12451 // exactly as `cpp_count_condition` is, so each needs its own
12452 // dispatcher arm. Mozcpp owns no file extension and so gets no
12453 // integration-snapshot coverage at all — this parity assertion is
12454 // its only guard.
12455 //
12456 // The expected value is *derived from the C++ run*, not hardcoded,
12457 // so the four languages cannot silently drift apart if the C++
12458 // expectation ever legitimately moves.
12459 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12460 #[test]
12461 fn c_family_ternary_operand_slots_agree_with_cpp() {
12462 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12463 const SRC: &str = "void f() { x = a ? !b : !c; }\n";
12464 let conditions = abc_conditions;
12465
12466 let cpp = conditions(LANG::Cpp, SRC);
12467 // Non-degenerate: a zeroed reference would make every
12468 // comparison below vacuous.
12469 assert_eq!(cpp, 4, "C++ reference value for `a ? !b : !c`");
12470
12471 assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
12472 assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
12473 assert_eq!(
12474 conditions(
12475 LANG::Objc,
12476 "@implementation Foo\n\
12477 - (void)bar {\n\
12478 x = a ? !b : !c;\n\
12479 }\n\
12480 @end\n",
12481 ),
12482 cpp,
12483 "ObjC must match C++"
12484 );
12485 }
12486
12487 // Issue #1276, C-family half. `cpp_walk_for_statement` is the
12488 // `for` header's counterpart to the `if` / `while` arms: the slot
12489 // is a bare expression rather than a `condition_clause`, read by
12490 // the same slot rule `cpp_walk_ternary` uses. Every fixture is a
12491 // shape only that walker can classify —
12492 // a comparison-shaped condition proves nothing, the `<` token arm
12493 // counts it either way (grammar-dispatch §11).
12494 #[cfg(feature = "cpp")]
12495 #[test]
12496 fn cpp_for_condition_slot_counts_unary_conditions() {
12497 // Bare identifier: the whole condition, no operator token.
12498 check_metrics::<CppParser>("void f(int a) { for (; a; ) {} }", "foo.cpp", |metric| {
12499 assert_eq!(metric.abc.conditions_sum(), 1);
12500 });
12501 // Negation: the slot peels the `!` (`cpp_wrapper_operand`) and
12502 // pays for the operand beneath it.
12503 check_metrics::<CppParser>("void f(int a) { for (; !a; ) {} }", "foo.cpp", |metric| {
12504 assert_eq!(metric.abc.conditions_sum(), 1);
12505 });
12506 // Parentheses: counted because the slot peels them
12507 // (`cpp_wrapper_operand`) — the path #1276 found dead.
12508 check_metrics::<CppParser>("void f(int a) { for (; (a); ) {} }", "foo.cpp", |metric| {
12509 assert_eq!(metric.abc.conditions_sum(), 1);
12510 });
12511 // No-double-count pin: the `<` token arm already counted this
12512 // shape before the fix and the walker must not add a second.
12513 // The two assignments confirm the header parsed as the
12514 // three-clause form rather than degenerating.
12515 check_metrics::<CppParser>(
12516 "void f(int n) { for (int i = 0; i < n; i++) {} }",
12517 "foo.cpp",
12518 |metric| {
12519 assert_eq!(metric.abc.conditions_sum(), 1);
12520 assert_eq!(metric.abc.assignments_sum(), 2);
12521 },
12522 );
12523 // Empty condition: no `condition` field, no decision, zero.
12524 check_metrics::<CppParser>("void f() { for (;;) { break; } }", "foo.cpp", |metric| {
12525 assert_eq!(metric.abc.conditions_sum(), 0);
12526 });
12527 }
12528
12529 // `cpp_walk_for_statement` is shared by the C, ObjC and Mozcpp ABC
12530 // impls the way `cpp_walk_ternary` is, so each needs its own
12531 // dispatcher arm — and Mozcpp, which owns no file extension, has no
12532 // integration-snapshot coverage at all, making this its only guard.
12533 // The expected value is derived from the C++ run rather than
12534 // hardcoded, so the four cannot silently drift apart.
12535 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12536 #[test]
12537 fn c_family_for_condition_slot_agrees_with_cpp() {
12538 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12539 const SRC: &str = "void f(int a) { for (; !a; ) {} }\n";
12540 let conditions = abc_conditions;
12541
12542 let cpp = conditions(LANG::Cpp, SRC);
12543 // Non-degenerate: a zeroed reference makes every comparison
12544 // below vacuous.
12545 assert_eq!(cpp, 1, "C++ reference value for `for (; !a; )`");
12546
12547 assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
12548 assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
12549 assert_eq!(
12550 conditions(
12551 LANG::Objc,
12552 "@implementation Foo\n\
12553 - (void)bar {\n\
12554 for (; !a; ) {}\n\
12555 }\n\
12556 @end\n",
12557 ),
12558 cpp,
12559 "ObjC must match C++"
12560 );
12561 }
12562
12563 #[cfg(feature = "cpp")]
12564 #[test]
12565 fn cpp_switch_cases_count_default_excluded() {
12566 // `case 1`, `case 2` → 2 conditions. `default` is intentionally
12567 // excluded (the unconditional fallthrough, mirroring cyclomatic's
12568 // `Case`-only count). Since #469 every C-family language —
12569 // Java, C#, Groovy, JS, TS — agrees on this; C++ already did.
12570 // C = 2.
12571 check_metrics::<CppParser>(
12572 "void f(int x) {\n\
12573 switch (x) {\n\
12574 case 1: break;\n\
12575 case 2: break;\n\
12576 default: break;\n\
12577 }\n\
12578 }\n",
12579 "foo.cpp",
12580 |metric| {
12581 assert_eq!(metric.abc.conditions_sum(), 2);
12582 insta::assert_json_snapshot!(metric.abc);
12583 },
12584 );
12585 }
12586
12587 #[cfg(feature = "cpp")]
12588 #[test]
12589 fn cpp_try_catch_count_conditions() {
12590 // `try` and `catch` each add one condition (Fitzpatrick's rule;
12591 // Java's impl above counts them too).
12592 check_metrics::<CppParser>(
12593 "void f() { try { } catch (int) { } catch (...) { } }",
12594 "foo.cpp",
12595 |metric| {
12596 // 1 `try` + 2 `catch` arms = 3.
12597 assert_eq!(metric.abc.conditions_sum(), 3);
12598 insta::assert_json_snapshot!(metric.abc);
12599 },
12600 );
12601 }
12602
12603 #[cfg(feature = "cpp")]
12604 #[test]
12605 fn cpp_complex_function_abc() {
12606 // Mixed-shape regression: assignments, calls, conditions,
12607 // ternary, switch, new. Verified by hand:
12608 // - assignments: `int x = 0` (init_declarator with `=`),
12609 // `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`, `x = b`,
12610 // `auto* p = new int(5)` (init_declarator with `=`) → A = 7
12611 // (post-#393: every `=` in an init_declarator counts).
12612 // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
12613 // - conditions: `a == b` (1) + `a > 0` (1) inside the if;
12614 // `&&` itself is NOT a condition (Fitzpatrick Rule 5,
12615 // issue #395). `a > b` (1) + `?` (1) in the ternary.
12616 // `else` (1, from the `else if` keyword) + `a < b` (1)
12617 // in the else-if. `!x` contributes 1 via the unary-
12618 // conditional walker (Fitzpatrick Rule 9, issue #403):
12619 // the `||` walker treats `!x` as a unary boolean operand
12620 // and counts the wrapped Identifier once. `case 1`,
12621 // `case 2` → 2. `default` excluded. Total C = 9.
12622 check_metrics::<CppParser>(
12623 "int f(int a, int b) {\n\
12624 int x = 0;\n\
12625 x = 5;\n\
12626 x += 2;\n\
12627 x++;\n\
12628 if (a == b && a > 0) {\n\
12629 x = (a > b) ? a : b;\n\
12630 } else if (a < b || !x) {\n\
12631 x = b;\n\
12632 }\n\
12633 switch (x) {\n\
12634 case 1: break;\n\
12635 case 2: break;\n\
12636 default: break;\n\
12637 }\n\
12638 auto* p = new int(5);\n\
12639 return f(a, b);\n\
12640 }\n",
12641 "foo.cpp",
12642 |metric| {
12643 assert_eq!(metric.abc.assignments_sum(), 7);
12644 assert_eq!(metric.abc.branches_sum(), 2);
12645 assert_eq!(metric.abc.conditions_sum(), 9);
12646 insta::assert_json_snapshot!(metric.abc);
12647 },
12648 );
12649 }
12650
12651 #[cfg(feature = "cpp")]
12652 #[test]
12653 fn cpp_if_multiple_conditions() {
12654 // Fitzpatrick Rule 9 walker (issue #403): each operand of a
12655 // `&&` / `||` chain is one condition.
12656 check_metrics::<CppParser>(
12657 "void f(bool a, bool b, bool c, bool d) {\n\
12658 \x20 if (a || b || c || d) {} // +4c\n\
12659 \x20 if (a && b && c) {} // +3c\n\
12660 \x20 if (!a && !b) {} // +2c\n\
12661 }\n",
12662 "foo.cpp",
12663 |metric| {
12664 assert_eq!(metric.abc.conditions_sum(), 9);
12665 insta::assert_json_snapshot!(metric.abc);
12666 },
12667 );
12668 }
12669
12670 #[cfg(feature = "cpp")]
12671 #[test]
12672 fn cpp_while_and_do_while_conditions() {
12673 // Exercise both the WhileStatement and DoStatement arms via
12674 // the walker on the `&&` / `||` tokens inside their parens.
12675 check_metrics::<CppParser>(
12676 "void f(bool a, bool b) {\n\
12677 \x20 while (a || b) {} // +2c\n\
12678 \x20 do {} while (a && !b); // +2c\n\
12679 }\n",
12680 "foo.cpp",
12681 |metric| {
12682 assert_eq!(metric.abc.conditions_sum(), 4);
12683 insta::assert_json_snapshot!(metric.abc);
12684 },
12685 );
12686 }
12687
12688 #[cfg(feature = "cpp")]
12689 #[test]
12690 fn cpp_if_constexpr_condition_counts() {
12691 // Regression for the code-review finding: C++ `if constexpr
12692 // (cond)` puts the `constexpr` keyword at child(1) and the
12693 // condition_clause at child(2). Pre-fix, the dispatcher used
12694 // child(1) and counted zero conditions for the `constexpr`
12695 // form. The fix uses `child_by_field_name("condition")`
12696 // which returns the condition_clause regardless of the
12697 // optional `constexpr` keyword.
12698 check_metrics::<CppParser>(
12699 "template <int N> void f() {\n\
12700 \x20 if constexpr (true) { } // +1c\n\
12701 \x20 if (false) { } // +1c\n\
12702 }\n",
12703 "foo.cpp",
12704 |metric| {
12705 assert_eq!(metric.abc.conditions_sum(), 2);
12706 insta::assert_json_snapshot!(metric.abc);
12707 },
12708 );
12709 }
12710
12711 #[cfg(feature = "cpp")]
12712 #[test]
12713 fn cpp_cast_expression_in_logical_chain_counts() {
12714 // Regression for findings.md round-2 #1 (C++):
12715 // `if ((bool)ptr && ready) {}` had the `||` walker missing
12716 // the `(bool)ptr` operand because `CastExpression` was not
12717 // in the former `cpp_bool_terminal_kinds!()`. Mirrors C#'s
12718 // former `csharp_bool_terminal_kinds!()`, which listed
12719 // `CastExpression` (lesson 19, #372).
12720 check_metrics::<CppParser>(
12721 "void f(void* ptr, bool ready) { if ((bool)ptr && ready) { } }\n",
12722 "foo.cpp",
12723 |metric| {
12724 // `&&` walker counts both operands: `(bool)ptr` (1)
12725 // and `ready` (1). Total: 2.
12726 assert_eq!(metric.abc.conditions_sum(), 2);
12727 insta::assert_json_snapshot!(metric.abc);
12728 },
12729 );
12730 }
12731
12732 #[cfg(feature = "cpp")]
12733 #[test]
12734 fn cpp_qualified_identifier_condition_counts() {
12735 // Regression for findings.md #3 (C++): tree-sitter-cpp emits
12736 // `qualified_identifier` under four kind_ids (573..576) per
12737 // the production-rule path; runtime kind for `ns::flag` is
12738 // 574 (`QualifiedIdentifier2`). Pre-fix the
12739 // former `cpp_bool_terminal_kinds!()` macro listed neither the
12740 // primary nor any alias, so `if (n::flag) {}` reported zero
12741 // conditions. The slot now pays for every variant, since none
12742 // scores itself (lesson #2).
12743 check_metrics::<CppParser>(
12744 "namespace n { extern bool flag; }\n\
12745 void f() { if (n::flag) { } }\n",
12746 "foo.cpp",
12747 |metric| {
12748 assert_eq!(metric.abc.conditions_sum(), 1);
12749 insta::assert_json_snapshot!(metric.abc);
12750 },
12751 );
12752 }
12753
12754 #[cfg(feature = "cpp")]
12755 #[test]
12756 fn cpp_if_boolean_literal_condition() {
12757 check_metrics::<CppParser>(
12758 "void f() {\n\
12759 \x20 if (true) {} // +1c\n\
12760 \x20 if (!false) {} // +1c\n\
12761 \x20 while (true) {} // +1c\n\
12762 \x20 do {} while (false); // +1c\n\
12763 }\n",
12764 "foo.cpp",
12765 |metric| {
12766 assert_eq!(metric.abc.conditions_sum(), 4);
12767 insta::assert_json_snapshot!(metric.abc);
12768 },
12769 );
12770 }
12771
12772 #[cfg(feature = "cpp")]
12773 #[test]
12774 fn cpp_methods_arguments_with_conditions() {
12775 check_metrics::<CppParser>(
12776 "void f(bool a, bool b) {\n\
12777 \x20 m(a, b); // +1b\n\
12778 \x20 m(!a, !b); // +1b +2c\n\
12779 }\n",
12780 "foo.cpp",
12781 |metric| {
12782 assert_eq!(metric.abc.branches_sum(), 2);
12783 assert_eq!(metric.abc.conditions_sum(), 2);
12784 insta::assert_json_snapshot!(metric.abc);
12785 },
12786 );
12787 }
12788
12789 #[cfg(feature = "cpp")]
12790 #[test]
12791 fn cpp_return_with_conditions() {
12792 check_metrics::<CppParser>(
12793 "bool m1(int z) { return !(z >= 0); }\n\
12794 bool m2(bool x) { return (((!x))); }\n\
12795 bool m3(bool x, bool y) { return x && y; }\n",
12796 "foo.cpp",
12797 |metric| {
12798 // m1: !(z >= 0) → `>=` (1). `!` wraps a paren'd
12799 // BinaryExpression — inspect_container reaches
12800 // the inner BinaryExpression and stops, no
12801 // walker count. +1.
12802 // m2: (((!x))) → ReturnStatement → inspect_container
12803 // unwraps three parens + one unary → reaches `x`
12804 // in has_boolean_content=true (seeded by the
12805 // unary `!`). +1.
12806 // m3: x && y → `&&` walker counts both → +2.
12807 // Sum: 1 + 1 + 2 = 4.
12808 assert_eq!(metric.abc.conditions_sum(), 4);
12809 insta::assert_json_snapshot!(metric.abc);
12810 },
12811 );
12812 }
12813
12814 // #1547: `co_return` is a coroutine's `return`, so ABC reads its value
12815 // the same way. Each row's expected value is its `return` twin's. A
12816 // `co_yield` hands a value back without returning, as the JS family's
12817 // `yield` does, and scores no value slot either.
12818 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12819 fn assert_co_return_scores_like_return(lang: LANG) {
12820 let source = |body: &str| format!("task f(bool a) {{ {body} }}\n");
12821 let rows = [
12822 ("co_return !a;", "return !a;", 1),
12823 ("co_return /*c*/ !a;", "return /*c*/ !a;", 1),
12824 ("co_return a;", "return a;", 0),
12825 ("co_return;", "return;", 0),
12826 ];
12827 for (coroutine, twin, expected) in rows {
12828 assert_eq!(
12829 abc_conditions(lang, &source(twin)),
12830 expected,
12831 "{lang:?} `{twin}`"
12832 );
12833 assert_eq!(
12834 abc_conditions(lang, &source(coroutine)),
12835 expected,
12836 "{lang:?} `{coroutine}`"
12837 );
12838 }
12839 assert_eq!(
12840 abc_conditions(lang, &source("co_yield !a;")),
12841 0,
12842 "{lang:?} co_yield"
12843 );
12844 }
12845
12846 #[cfg(feature = "cpp")]
12847 #[test]
12848 fn cpp_co_return_value_scores_like_return_1547() {
12849 assert_co_return_scores_like_return(LANG::Cpp);
12850 }
12851
12852 // Mozcpp owns no file extension, so this is its only coverage.
12853 #[cfg(feature = "mozcpp")]
12854 #[test]
12855 fn mozcpp_co_return_value_scores_like_return_1547() {
12856 assert_co_return_scores_like_return(LANG::Mozcpp);
12857 }
12858
12859 #[cfg(feature = "cpp")]
12860 #[test]
12861 fn cpp_short_circuit_with_boolean_literal_operand() {
12862 // `a && true` reports 2 conditions: one for the identifier
12863 // operand, one for the `True` literal operand.
12864 check_metrics::<CppParser>(
12865 "bool f(bool a) { return a && true; }\n",
12866 "foo.cpp",
12867 |metric| {
12868 assert_eq!(metric.abc.conditions_sum(), 2);
12869 insta::assert_json_snapshot!(metric.abc);
12870 },
12871 );
12872 }
12873
12874 #[cfg(feature = "javascript")]
12875 #[test]
12876 fn javascript_empty_unit_zero() {
12877 // No code → A=B=C=0. Wires up the trait and exercises the
12878 // per-language compute reachability.
12879 check_metrics::<JavascriptParser>("", "empty.js", |metric| {
12880 assert_eq!(metric.abc.assignments_sum(), 0);
12881 assert_eq!(metric.abc.branches_sum(), 0);
12882 assert_eq!(metric.abc.conditions_sum(), 0);
12883 insta::assert_json_snapshot!(metric.abc);
12884 });
12885 }
12886
12887 #[cfg(feature = "javascript")]
12888 #[test]
12889 fn javascript_plain_and_compound_assignments_count() {
12890 // `let` / `var` declarations behave like TypeScript: only a
12891 // `const` initializer is suppressed. So `let x = 0` does count as
12892 // A=+1; only `const PI = 3.14` would be elided. Plain `x = 5`,
12893 // `x += 2`, `x = 7` all count → A = 4 total here.
12894 check_metrics::<JavascriptParser>(
12895 "function f() { let x = 0; x = 5; x += 2; x = 7; }",
12896 "foo.js",
12897 |metric| {
12898 assert_eq!(metric.abc.assignments_sum(), 4);
12899 assert_eq!(metric.abc.branches_sum(), 0);
12900 assert_eq!(metric.abc.conditions_sum(), 0);
12901 insta::assert_json_snapshot!(metric.abc);
12902 },
12903 );
12904 }
12905
12906 #[cfg(feature = "javascript")]
12907 #[test]
12908 fn javascript_const_initializer_not_assignment() {
12909 // `const PI = 3.14` must NOT count as an assignment — its `=`
12910 // initialises a `const` binding. `let x = 1` and `var y = 2`
12911 // still count (matches the TS impl: only `const` suppresses).
12912 check_metrics::<JavascriptParser>(
12913 "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
12914 "foo.js",
12915 |metric| {
12916 // `const PI` suppressed; `let x = 1`, `var y = 2`,
12917 // `x = 9` all count → A = 3.
12918 assert_eq!(metric.abc.assignments_sum(), 3);
12919 insta::assert_json_snapshot!(metric.abc);
12920 },
12921 );
12922 }
12923
12924 #[cfg(feature = "javascript")]
12925 #[test]
12926 fn javascript_asi_const_does_not_suppress_later_assignments() {
12927 // The issue #1277 reproducer verbatim. JavaScript half of the
12928 // cluster documented at
12929 // `typescript_asi_const_does_not_suppress_later_assignments`.
12930 check_metrics::<JavascriptParser>(
12931 "function f() {
12932 const a = 1
12933 x = 2
12934 return x
12935 }",
12936 "foo.js",
12937 |metric| {
12938 // Pre-#1277 this reported 0; the semicolon-terminated
12939 // spelling reported 1.
12940 assert_eq!(metric.abc.assignments_sum(), 1);
12941 },
12942 );
12943 }
12944
12945 #[cfg(feature = "javascript")]
12946 #[test]
12947 fn javascript_nested_arrow_const_does_not_leak() {
12948 // The ASI leak beside a nested space: the arrow body opens its
12949 // own space, so `y = 1` was always counted there, but
12950 // `const f = () => …` has no `;`, so `h`'s sentinel stayed live
12951 // and suppressed `z = 2`. The stack was per-space state that
12952 // `Stats::merge` never carried, so this is the terminator defect
12953 // and not a second route.
12954 check_metrics::<JavascriptParser>(
12955 "function h() {
12956 const f = () => { y = 1 }
12957 z = 2
12958 }",
12959 "foo.js",
12960 |metric| {
12961 // `y = 1` and `z = 2`; the `const` initializer is
12962 // suppressed. Pre-#1277: 1.
12963 assert_eq!(metric.abc.assignments_sum(), 2);
12964 },
12965 );
12966 }
12967
12968 #[cfg(feature = "javascript")]
12969 #[test]
12970 fn javascript_non_declarator_equals_still_count() {
12971 // An `=` that does not belong to a `const` declarator is always
12972 // an assignment: a class `field_definition` initializer, a
12973 // default-parameter `assignment_pattern`, and a destructured
12974 // parameter's defaults, whose climb crosses the same pattern
12975 // layers as a `const` pattern's but ends at `formal_parameters`.
12976 // A predicate that accepted any pattern ancestor as a declarator
12977 // would zero the last three.
12978 check_metrics::<JavascriptParser>(
12979 "class K { f = 1; g(p = 2, {q = 3} = {}) { let d = 4 } }",
12980 "foo.js",
12981 |metric| {
12982 // `f = 1`, `p = 2`, `q = 3`, `= {}`, `let d = 4`.
12983 assert_eq!(metric.abc.assignments_sum(), 5);
12984 },
12985 );
12986 }
12987
12988 #[cfg(feature = "javascript")]
12989 #[test]
12990 fn javascript_const_initializer_value_assignments_still_count() {
12991 // An `=` inside a `const` initializer's *value* is an
12992 // `assignment_expression`: its climb reaches no pattern layer and
12993 // no declarator, so it counts. The pre-#1277 sentinel
12994 // blanket-suppressed every `=` between `const` and `;`, which is
12995 // the shape that moved the pdf.js corpus snapshots
12996 // (`const bbox = (this.data.rect = …)`).
12997 check_metrics::<JavascriptParser>(
12998 "function m(o, a, b) { const x = (o.p = 1); const y = a || (b = 2); }",
12999 "foo.js",
13000 |metric| {
13001 // `o.p = 1` and `b = 2`; the two `const` initializers are
13002 // suppressed. Pre-#1277: 0.
13003 assert_eq!(metric.abc.assignments_sum(), 2);
13004 },
13005 );
13006 }
13007
13008 #[cfg(feature = "javascript")]
13009 #[test]
13010 fn javascript_const_declarator_shapes_stay_suppressed() {
13011 // JavaScript half of
13012 // `typescript_const_declarator_shapes_stay_suppressed`.
13013 check_metrics::<JavascriptParser>(
13014 "function f(o, xs) {
13015 const a = 1, b = 2
13016 const {c = 5, d: {e = 6} = {}} = o
13017 const [g = 7, ...[h = 8]] = xs
13018 let i = 3
13019 var j = 4
13020 for (const x of xs) { k(x) }
13021 }",
13022 "foo.js",
13023 |metric| {
13024 assert_eq!(metric.abc.assignments_sum(), 2);
13025 },
13026 );
13027 }
13028
13029 #[cfg(feature = "javascript")]
13030 #[test]
13031 fn javascript_increment_and_decrement_count_as_assignment() {
13032 // `x++` (post) and `--x` (pre) both update an lvalue and so
13033 // count as assignments. Combined with the `let x = 0`
13034 // initializer (which counts under the JS/TS rule — only `const`
13035 // suppresses), A = 3.
13036 check_metrics::<JavascriptParser>(
13037 "function f() { let x = 0; x++; --x; }",
13038 "foo.js",
13039 |metric| {
13040 assert_eq!(metric.abc.assignments_sum(), 3);
13041 insta::assert_json_snapshot!(metric.abc);
13042 },
13043 );
13044 }
13045
13046 #[cfg(feature = "javascript")]
13047 #[test]
13048 fn javascript_calls_are_branches() {
13049 // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
13050 // `new_expression` → B = 1. Total B = 2.
13051 check_metrics::<JavascriptParser>(
13052 "function f() { g(1); new Foo(2); }",
13053 "foo.js",
13054 |metric| {
13055 assert_eq!(metric.abc.branches_sum(), 2);
13056 assert_eq!(metric.abc.conditions_sum(), 0);
13057 insta::assert_json_snapshot!(metric.abc);
13058 },
13059 );
13060 }
13061
13062 #[cfg(feature = "javascript")]
13063 #[test]
13064 fn javascript_comparisons_count_conditions() {
13065 // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
13066 // once. The `&&` / `||` short-circuit operators are NOT
13067 // counted as conditions in this impl (matches the TS
13068 // precedent — short-circuit ops are folded into the
13069 // surrounding `if` / control-flow arm, not separately).
13070 // Total C = 8.
13071 check_metrics::<JavascriptParser>(
13072 "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
13073 "foo.js",
13074 |metric| {
13075 assert_eq!(metric.abc.conditions_sum(), 8);
13076 insta::assert_json_snapshot!(metric.abc);
13077 },
13078 );
13079 }
13080
13081 // #1297 in the `js_abc_compute!` expansion, where `LT` / `GT` had
13082 // no gate at all: plain JavaScript has no generics, but
13083 // tree-sitter-javascript parses JSX unconditionally, so the same
13084 // seven tag brackets that
13085 // `tsx_jsx_elements_are_not_conditions` covers scored seven
13086 // conditions in a `.js` file too. Same fixture minus the type
13087 // annotations, same discriminating numbers: 8 pre-fix, 1 after.
13088 #[cfg(feature = "javascript")]
13089 #[test]
13090 fn javascript_jsx_elements_are_not_conditions() {
13091 check_metrics::<JavascriptParser>(
13092 "function f(a, b) {
13093 if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
13094 return <br />;
13095 }",
13096 "foo.js",
13097 |metric| {
13098 assert_eq!(metric.abc.conditions_sum(), 1);
13099 // Fixture-presence anchor: the `className="x"` attribute
13100 // `=` is this fixture's only assignment, so the test
13101 // fails if a later edit drops the JSX. Without it the
13102 // surviving `a < b` alone also scores 1, and the
13103 // conditions assertion would pass while proving nothing.
13104 assert_eq!(metric.abc.assignments_sum(), 1);
13105 },
13106 );
13107 }
13108
13109 #[cfg(feature = "javascript")]
13110 #[test]
13111 fn javascript_number_truthy_condition_counts() {
13112 // Regression for #772: JS treats every non-zero number as
13113 // truthy, so `while (5)` and `x && 5` should each count their
13114 // numeric literal as a Fitzpatrick unary condition. Pre-fix
13115 // the former `javascript_bool_terminal_kinds!()` listed `True` /
13116 // `False` but omitted `Number`, so the walker dropped every numeric-
13117 // truthy operand (mirrors the Lua `Number` fix).
13118 check_metrics::<JavascriptParser>(
13119 "function f(x) { while (5) {} return x && 5; }",
13120 "foo.js",
13121 |metric| {
13122 // `while (5)` → Number literal (+1). `x && 5` → both
13123 // operands count: identifier `x` (+1), Number `5` (+1).
13124 // Total: 3.
13125 assert_eq!(metric.abc.conditions_sum(), 3);
13126 insta::assert_json_snapshot!(metric.abc);
13127 },
13128 );
13129 }
13130
13131 #[cfg(feature = "typescript")]
13132 #[test]
13133 fn typescript_number_truthy_condition_counts() {
13134 // Regression for #772: TS shares the JS truthy semantics. The
13135 // numeric *literal* `5` (kind `Number`) counts; the type-keyword
13136 // `number` (kind `Number2`, the `predefined_type`) must not —
13137 // see the former `typescript_bool_terminal_kinds!`.
13138 check_metrics::<TypescriptParser>(
13139 "function f(x: number) { while (5) {} return x && 5; }",
13140 "foo.ts",
13141 |metric| {
13142 // `while (5)` → +1; `x && 5` → `x` (+1) + `5` (+1).
13143 // Total: 3. The `: number` annotation contributes 0.
13144 assert_eq!(metric.abc.conditions_sum(), 3);
13145 insta::assert_json_snapshot!(metric.abc);
13146 },
13147 );
13148 }
13149
13150 #[cfg(feature = "javascript")]
13151 #[test]
13152 fn javascript_nullish_coalescing_counts_condition() {
13153 // `a ?? b` is one nullish-coalescing operator → C = 1.
13154 check_metrics::<JavascriptParser>(
13155 "function f(a, b) { return a ?? b; }",
13156 "foo.js",
13157 |metric| {
13158 assert_eq!(metric.abc.conditions_sum(), 1);
13159 insta::assert_json_snapshot!(metric.abc);
13160 },
13161 );
13162 }
13163
13164 #[cfg(feature = "javascript")]
13165 #[test]
13166 fn javascript_else_ternary_case_default_try_catch() {
13167 // `else`, `?` (ternary), `case`, `try`, `catch` all count.
13168 // `default` is the unconditional fallthrough → +0 (#469).
13169 // With the comparisons:
13170 // - `a > 0` → 1
13171 // - `else` opens an else_clause → 1
13172 // - `?` ternary → 1
13173 // - the ternary's bare-identifier condition `a` → 1 (#1102)
13174 // - `case 1` → 1
13175 // - `default` → 0 (fallthrough, #469)
13176 // - `try` + `catch` → 2
13177 // Total C = 7.
13178 check_metrics::<JavascriptParser>(
13179 "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
13180 "foo.js",
13181 |metric| {
13182 assert_eq!(metric.abc.conditions_sum(), 7);
13183 insta::assert_json_snapshot!(metric.abc);
13184 },
13185 );
13186 }
13187
13188 // Issue #1102, JS-family half. See
13189 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
13190 // rule; the two families were behind Java by the same three units.
13191 #[cfg(feature = "javascript")]
13192 #[test]
13193 fn javascript_ternary_operand_slots_count_as_unary_conditions() {
13194 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
13195 check_metrics::<JavascriptParser>(
13196 "function f() { x = a ? !b : !c; }",
13197 "foo.js",
13198 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13199 );
13200 // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
13201 // fix — the parenthesised condition unwraps to a
13202 // `binary_expression` (not a boolean terminal) and neither
13203 // branch is negated.
13204 check_metrics::<JavascriptParser>(
13205 "function f() { x = (a > 0) ? b : -b; }",
13206 "foo.js",
13207 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
13208 );
13209 // Nested: two `?` tokens plus the two bare-identifier
13210 // conditions = 4.
13211 check_metrics::<JavascriptParser>(
13212 "function f() { x = a ? b ? c : d : e; }",
13213 "foo.js",
13214 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13215 );
13216 // A negated condition is the only input reaching the walker's
13217 // `else` fallback — see the C++ sibling for why. `?` (1) +
13218 // `!a` (1) = 2.
13219 check_metrics::<JavascriptParser>("function f() { x = !a ? b : c; }", "foo.js", |metric| {
13220 assert_eq!(metric.abc.conditions_sum(), 2);
13221 });
13222 }
13223
13224 // TypeScript expands the same `ts_abc_compute!` arm from a separate
13225 // macro body than JavaScript's `js_abc_compute!`, so wiring one and
13226 // not the other is a live failure mode; TSX and Mozjs are clones of
13227 // these two.
13228 #[cfg(feature = "typescript")]
13229 #[test]
13230 fn typescript_ternary_operand_slots_count_as_unary_conditions() {
13231 check_metrics::<TypescriptParser>(
13232 "function f() { x = a ? !b : !c; }",
13233 "foo.ts",
13234 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13235 );
13236 check_metrics::<TypescriptParser>(
13237 "function f() { x = (a > 0) ? b : -b; }",
13238 "foo.ts",
13239 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
13240 );
13241 }
13242
13243 // Issue #1276, JS-family half. See
13244 // `cpp_for_condition_slot_counts_unary_conditions` for the rule.
13245 // The JS grammar marks the `condition` field on both the expression
13246 // and the `;` closing it, so `child_by_field_name` is the only
13247 // addressing that lands on the expression for every header shape.
13248 #[cfg(feature = "javascript")]
13249 #[test]
13250 fn javascript_for_condition_slot_counts_unary_conditions() {
13251 // Bare identifier: no operator token anywhere in the header.
13252 check_metrics::<JavascriptParser>("function f(a) { for (; a; ) {} }", "foo.js", |metric| {
13253 assert_eq!(metric.abc.conditions_sum(), 1);
13254 });
13255 // Negation, through the `!` unwrap.
13256 check_metrics::<JavascriptParser>(
13257 "function f(a) { for (; !a; ) {} }",
13258 "foo.js",
13259 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
13260 );
13261 // Parentheses: counts only via the `ForStatement`
13262 // boolean-context seed #1276 found dead.
13263 check_metrics::<JavascriptParser>(
13264 "function f(a) { for (; (a); ) {} }",
13265 "foo.js",
13266 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
13267 );
13268 // No-double-count pin: the `<` arm already counted this shape.
13269 // `let i = 0` and `i++` are the two assignments, which also
13270 // confirms the header parsed as the three-clause form.
13271 check_metrics::<JavascriptParser>(
13272 "function f(n) { for (let i = 0; i < n; i++) {} }",
13273 "foo.js",
13274 |metric| {
13275 assert_eq!(metric.abc.conditions_sum(), 1);
13276 assert_eq!(metric.abc.assignments_sum(), 2);
13277 },
13278 );
13279 // Empty condition: the slot holds an `empty_statement`, which
13280 // is neither a terminal nor a wrapper, so it counts nothing.
13281 check_metrics::<JavascriptParser>(
13282 "function f() { for (;;) { break; } }",
13283 "foo.js",
13284 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
13285 );
13286 }
13287
13288 // TypeScript expands the `ForStatement` arm from `ts_abc_compute!`,
13289 // a separate macro body from JavaScript's `js_abc_compute!`, so
13290 // wiring one and not the other is a live failure mode; TSX and
13291 // Mozjs are the clones of those two. The expected values are
13292 // derived from the JavaScript run rather than hardcoded.
13293 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13294 #[test]
13295 fn js_family_for_condition_slot_agrees_with_javascript() {
13296 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13297 const BARE: &str = "function f(a) { for (; a; ) {} }\n";
13298 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13299 const EMPTY: &str = "function f() { for (;;) { break; } }\n";
13300 let conditions = abc_conditions;
13301
13302 let bare = conditions(LANG::Javascript, BARE);
13303 // Non-degenerate: a zeroed reference makes the comparisons
13304 // below vacuous.
13305 assert_eq!(bare, 1, "JavaScript reference value for `for (; a; )`");
13306 assert_eq!(
13307 conditions(LANG::Javascript, EMPTY),
13308 0,
13309 "JavaScript `for (;;)`"
13310 );
13311
13312 for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
13313 assert_eq!(conditions(lang, BARE), bare, "{lang:?} bare for-condition");
13314 assert_eq!(conditions(lang, EMPTY), 0, "{lang:?} empty for-condition");
13315 }
13316 }
13317
13318 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13319 #[test]
13320 fn js_family_ternary_operand_slots_agree_with_javascript() {
13321 // `a ? !b : !c` is the one shape that tells the ternary walker
13322 // from the `for`-header walker: both read the `condition` field
13323 // and share the `(&Node, &mut f64)` signature, so a transposed
13324 // pair in a `ts_abc_compute!` / `js_abc_compute!` invocation
13325 // compiles, passes every condition-slot test, and drops only the
13326 // two branch operands. TypeScript alone pinned those before; the
13327 // other three expansions now do too.
13328 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13329 const SRC: &str = "function f(a, b, c) { x = a ? !b : !c; }\n";
13330 let javascript = abc_conditions(LANG::Javascript, SRC);
13331 // expected: 4 — the `?`, the `a` condition slot and both negated
13332 // branch operands; non-degenerate by construction.
13333 assert_eq!(
13334 javascript, 4,
13335 "JavaScript reference value for `a ? !b : !c`"
13336 );
13337 for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
13338 assert_eq!(
13339 abc_conditions(lang, SRC),
13340 javascript,
13341 "{lang:?} ternary operand slots"
13342 );
13343 }
13344 }
13345
13346 #[cfg(feature = "javascript")]
13347 #[test]
13348 fn javascript_instanceof_counts_condition() {
13349 // `x instanceof Foo` is a binary expression whose operator is
13350 // the `instanceof` keyword token → C = 1.
13351 check_metrics::<JavascriptParser>(
13352 "function f(x) { return x instanceof Foo; }",
13353 "foo.js",
13354 |metric| {
13355 assert_eq!(metric.abc.conditions_sum(), 1);
13356 insta::assert_json_snapshot!(metric.abc);
13357 },
13358 );
13359 }
13360
13361 #[cfg(feature = "javascript")]
13362 #[test]
13363 fn javascript_complex_function_abc() {
13364 // Mixed-shape regression. Verified by hand:
13365 // - assignments: `let x = 0` (a `let` initializer counts)
13366 // + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
13367 // `let p = ...` (likewise) → A = 7.
13368 // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
13369 // - conditions: `a == b`, `a > 0` → 2 inside the if header
13370 // (`&&` is not counted directly). `else` (1) + `a > b`,
13371 // `?` → 2 in the ternary. `a < b` → 1 in the else-if.
13372 // `!x` → 1 from the Fitzpatrick Rule 9 walker on `||`
13373 // (issue #403): the wrapped Identifier counts once.
13374 // `case 1` → 1 in the switch; `default` → 0 (fallthrough,
13375 // #469). Total C = 8.
13376 check_metrics::<JavascriptParser>(
13377 "function f(a, b) {\n\
13378 let x = 0;\n\
13379 x = 5;\n\
13380 x += 2;\n\
13381 x++;\n\
13382 if (a == b && a > 0) {\n\
13383 x = (a > b) ? a : b;\n\
13384 } else if (a < b || !x) {\n\
13385 x = b;\n\
13386 }\n\
13387 switch (x) {\n\
13388 case 1: break;\n\
13389 default: break;\n\
13390 }\n\
13391 let p = new Bar();\n\
13392 return f(a, b);\n\
13393 }\n",
13394 "foo.js",
13395 |metric| {
13396 assert_eq!(metric.abc.assignments_sum(), 7);
13397 assert_eq!(metric.abc.branches_sum(), 2);
13398 assert_eq!(metric.abc.conditions_sum(), 8);
13399 insta::assert_json_snapshot!(metric.abc);
13400 },
13401 );
13402 }
13403
13404 #[cfg(feature = "mozjs")]
13405 #[test]
13406 fn mozjs_asi_const_does_not_suppress_later_assignments() {
13407 // Mozjs half of the #1277 cluster; the fork carries its own
13408 // kind-id numbering, so it needs its own fixture. See
13409 // `typescript_asi_const_does_not_suppress_later_assignments`.
13410 check_metrics::<MozjsParser>(
13411 "function f() {
13412 const a = 1
13413 x = 2
13414 y = 3
13415 }",
13416 "foo.jsm",
13417 |metric| {
13418 assert_eq!(metric.abc.assignments_sum(), 2);
13419 },
13420 );
13421 }
13422
13423 #[cfg(feature = "mozjs")]
13424 #[test]
13425 fn mozjs_const_declarator_shapes_stay_suppressed() {
13426 // Mozjs half of
13427 // `javascript_const_declarator_shapes_stay_suppressed`.
13428 check_metrics::<MozjsParser>(
13429 "function f(o, xs) {
13430 const a = 1, b = 2
13431 const {c = 5, d: {e = 6} = {}} = o
13432 const [g = 7, ...[h = 8]] = xs
13433 let i = 3
13434 var j = 4
13435 for (const x of xs) { k(x) }
13436 }",
13437 "foo.jsm",
13438 |metric| {
13439 assert_eq!(metric.abc.assignments_sum(), 2);
13440 },
13441 );
13442 }
13443
13444 #[cfg(feature = "mozjs")]
13445 #[test]
13446 fn mozjs_complex_function_abc() {
13447 // Mozjs shares JavaScript's expression / statement vocabulary;
13448 // the `js_abc_compute!` macro expands identical token-level
13449 // rules for both. This test pins parity against the JS impl.
13450 check_metrics::<MozjsParser>(
13451 "function f(a, b) {\n\
13452 let x = 0;\n\
13453 x = 5;\n\
13454 x += 2;\n\
13455 x++;\n\
13456 if (a == b && a > 0) {\n\
13457 x = (a > b) ? a : b;\n\
13458 } else if (a < b || !x) {\n\
13459 x = b;\n\
13460 }\n\
13461 switch (x) {\n\
13462 case 1: break;\n\
13463 default: break;\n\
13464 }\n\
13465 let p = new Bar();\n\
13466 return f(a, b);\n\
13467 }\n",
13468 "foo.js",
13469 |metric| {
13470 assert_eq!(metric.abc.assignments_sum(), 7);
13471 assert_eq!(metric.abc.branches_sum(), 2);
13472 assert_eq!(metric.abc.conditions_sum(), 8);
13473 insta::assert_json_snapshot!(metric.abc);
13474 },
13475 );
13476 }
13477
13478 // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots -----
13479
13480 #[cfg(feature = "javascript")]
13481 #[test]
13482 fn javascript_await_expression_condition_counts() {
13483 // Regression for findings.md round-2 #2 (JS):
13484 // `if (await ready()) {}` parses with `await_expression` as
13485 // the condition node inside the `parenthesized_expression`.
13486 // The former `javascript_inspect_container` unwrapped the paren
13487 // but the await child was not in the terminal-bool set, so the
13488 // walker broke without counting. Mirrors C# (lesson 19).
13489 check_metrics::<JavascriptParser>(
13490 "async function ready() { return true; }\n\
13491 async function f() { if (await ready()) { } }\n",
13492 "foo.js",
13493 |metric| {
13494 assert_eq!(metric.abc.branches_sum(), 1);
13495 assert_eq!(metric.abc.conditions_sum(), 1);
13496 insta::assert_json_snapshot!(metric.abc);
13497 },
13498 );
13499 }
13500
13501 #[cfg(feature = "javascript")]
13502 #[test]
13503 fn javascript_member_expression_condition_counts() {
13504 // Regression for findings.md #3 (JS-family): tree-sitter-
13505 // javascript emits `member_expression` under three kind_ids
13506 // (191 primary, 208, 228 — `MemberExpression2/3`) depending
13507 // on the production rule path. The verifier in this audit
13508 // confirmed runtime kind for `o.x` is 208. Pre-fix the
13509 // shared former `js_family_bool_terminal_kinds!()` macro listed
13510 // only the primary, so every `if (o.x) {}` / `o.x && o.y`
13511 // condition silently reported zero. The slot now pays for all
13512 // three aliases, none of which scores itself (lesson #2).
13513 check_metrics::<JavascriptParser>(
13514 "function f(o) {\n\
13515 \x20 if (o.x) {} // +1c\n\
13516 \x20 return o.x && o.y; // +2c (walker on &&)\n\
13517 }\n",
13518 "foo.js",
13519 |metric| {
13520 assert_eq!(metric.abc.conditions_sum(), 3);
13521 insta::assert_json_snapshot!(metric.abc);
13522 },
13523 );
13524 }
13525
13526 #[cfg(feature = "javascript")]
13527 #[test]
13528 fn javascript_if_boolean_literal_condition() {
13529 check_metrics::<JavascriptParser>(
13530 "function f() {\n\
13531 \x20 if (true) {} // +1c\n\
13532 \x20 if (!false) {} // +1c\n\
13533 \x20 while (true) {} // +1c\n\
13534 \x20 do {} while (false); // +1c\n\
13535 }\n",
13536 "foo.js",
13537 |metric| {
13538 assert_eq!(metric.abc.conditions_sum(), 4);
13539 insta::assert_json_snapshot!(metric.abc);
13540 },
13541 );
13542 }
13543
13544 #[cfg(feature = "javascript")]
13545 #[test]
13546 fn javascript_methods_arguments_with_conditions() {
13547 check_metrics::<JavascriptParser>(
13548 "function f(a, b) {\n\
13549 \x20 m(a, b); // +1b\n\
13550 \x20 m(!a, !b); // +1b +2c\n\
13551 }\n",
13552 "foo.js",
13553 |metric| {
13554 assert_eq!(metric.abc.branches_sum(), 2);
13555 assert_eq!(metric.abc.conditions_sum(), 2);
13556 insta::assert_json_snapshot!(metric.abc);
13557 },
13558 );
13559 }
13560
13561 #[cfg(feature = "javascript")]
13562 #[test]
13563 fn javascript_return_with_conditions() {
13564 check_metrics::<JavascriptParser>(
13565 "function m1(z) { return !(z >= 0); }\n\
13566 function m2(x) { return (((!x))); }\n\
13567 function m3(x, y) { return x && y; }\n",
13568 "foo.js",
13569 |metric| {
13570 // m1: 1 (`>=`). m2: 1 (walker unwraps to `x`).
13571 // m3: 2 (`&&` walker counts both terminals).
13572 assert_eq!(metric.abc.conditions_sum(), 4);
13573 insta::assert_json_snapshot!(metric.abc);
13574 },
13575 );
13576 }
13577
13578 #[cfg(feature = "typescript")]
13579 #[test]
13580 fn typescript_if_boolean_literal_condition() {
13581 check_metrics::<TypescriptParser>(
13582 "function f() {\n\
13583 \x20 if (true) {}\n\
13584 \x20 if (!false) {}\n\
13585 \x20 while (true) {}\n\
13586 \x20 do {} while (false);\n\
13587 }\n",
13588 "foo.ts",
13589 |metric| {
13590 assert_eq!(metric.abc.conditions_sum(), 4);
13591 insta::assert_json_snapshot!(metric.abc);
13592 },
13593 );
13594 }
13595
13596 #[cfg(feature = "typescript")]
13597 #[test]
13598 fn typescript_methods_arguments_with_conditions() {
13599 check_metrics::<TypescriptParser>(
13600 "function f(a: boolean, b: boolean) {\n\
13601 \x20 m(a, b);\n\
13602 \x20 m(!a, !b);\n\
13603 }\n",
13604 "foo.ts",
13605 |metric| {
13606 assert_eq!(metric.abc.branches_sum(), 2);
13607 assert_eq!(metric.abc.conditions_sum(), 2);
13608 insta::assert_json_snapshot!(metric.abc);
13609 },
13610 );
13611 }
13612
13613 #[cfg(feature = "typescript")]
13614 #[test]
13615 fn typescript_return_with_conditions() {
13616 check_metrics::<TypescriptParser>(
13617 "function m1(z: number): boolean { return !(z >= 0); }\n\
13618 function m2(x: boolean): boolean { return (((!x))); }\n\
13619 function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
13620 "foo.ts",
13621 |metric| {
13622 assert_eq!(metric.abc.conditions_sum(), 4);
13623 insta::assert_json_snapshot!(metric.abc);
13624 },
13625 );
13626 }
13627
13628 #[cfg(feature = "typescript")]
13629 #[test]
13630 fn tsx_if_boolean_literal_condition() {
13631 check_metrics::<TsxParser>(
13632 "function f() {\n\
13633 \x20 if (true) {}\n\
13634 \x20 if (!false) {}\n\
13635 \x20 while (true) {}\n\
13636 \x20 do {} while (false);\n\
13637 }\n",
13638 "foo.tsx",
13639 |metric| {
13640 assert_eq!(metric.abc.conditions_sum(), 4);
13641 insta::assert_json_snapshot!(metric.abc);
13642 },
13643 );
13644 }
13645
13646 #[cfg(feature = "typescript")]
13647 #[test]
13648 fn tsx_methods_arguments_with_conditions() {
13649 check_metrics::<TsxParser>(
13650 "function f(a: boolean, b: boolean) {\n\
13651 \x20 m(a, b);\n\
13652 \x20 m(!a, !b);\n\
13653 }\n",
13654 "foo.tsx",
13655 |metric| {
13656 assert_eq!(metric.abc.branches_sum(), 2);
13657 assert_eq!(metric.abc.conditions_sum(), 2);
13658 insta::assert_json_snapshot!(metric.abc);
13659 },
13660 );
13661 }
13662
13663 #[cfg(feature = "typescript")]
13664 #[test]
13665 fn tsx_return_with_conditions() {
13666 check_metrics::<TsxParser>(
13667 "function m1(z: number): boolean { return !(z >= 0); }\n\
13668 function m2(x: boolean): boolean { return (((!x))); }\n\
13669 function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
13670 "foo.tsx",
13671 |metric| {
13672 assert_eq!(metric.abc.conditions_sum(), 4);
13673 insta::assert_json_snapshot!(metric.abc);
13674 },
13675 );
13676 }
13677
13678 #[cfg(feature = "mozjs")]
13679 #[test]
13680 fn mozjs_jsx_elements_are_not_conditions() {
13681 // #1297 in the second expansion of `js_abc_compute!`. The
13682 // vendored mozjs fork carries the same three JSX productions as
13683 // upstream tree-sitter-javascript with its own `kind_id`s, so
13684 // the gate is a distinct instantiation and needs its own
13685 // fixture — a passing JavaScript test says nothing about the
13686 // macro's other expansion. Same shape and same discriminating
13687 // numbers as `javascript_jsx_elements_are_not_conditions`: 8
13688 // pre-fix, 1 after.
13689 check_metrics::<MozjsParser>(
13690 "function f(a, b) {
13691 if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
13692 return <br />;
13693 }",
13694 "foo.js",
13695 |metric| {
13696 assert_eq!(metric.abc.conditions_sum(), 1);
13697 // Fixture-presence anchor: the `className="x"` attribute
13698 // `=` is this fixture's only assignment, so the test
13699 // fails if a later edit drops the JSX. Without it the
13700 // surviving `a < b` alone also scores 1, and the
13701 // conditions assertion would pass while proving nothing.
13702 assert_eq!(metric.abc.assignments_sum(), 1);
13703 },
13704 );
13705 }
13706
13707 #[cfg(feature = "mozjs")]
13708 #[test]
13709 fn mozjs_if_boolean_literal_condition() {
13710 check_metrics::<MozjsParser>(
13711 "function f() {\n\
13712 \x20 if (true) {}\n\
13713 \x20 if (!false) {}\n\
13714 \x20 while (true) {}\n\
13715 \x20 do {} while (false);\n\
13716 }\n",
13717 "foo.js",
13718 |metric| {
13719 assert_eq!(metric.abc.conditions_sum(), 4);
13720 insta::assert_json_snapshot!(metric.abc);
13721 },
13722 );
13723 }
13724
13725 #[cfg(feature = "mozjs")]
13726 #[test]
13727 fn mozjs_methods_arguments_with_conditions() {
13728 check_metrics::<MozjsParser>(
13729 "function f(a, b) {\n\
13730 \x20 m(a, b);\n\
13731 \x20 m(!a, !b);\n\
13732 }\n",
13733 "foo.js",
13734 |metric| {
13735 assert_eq!(metric.abc.branches_sum(), 2);
13736 assert_eq!(metric.abc.conditions_sum(), 2);
13737 insta::assert_json_snapshot!(metric.abc);
13738 },
13739 );
13740 }
13741
13742 #[cfg(feature = "mozjs")]
13743 #[test]
13744 fn mozjs_return_with_conditions() {
13745 check_metrics::<MozjsParser>(
13746 "function m1(z) { return !(z >= 0); }\n\
13747 function m2(x) { return (((!x))); }\n\
13748 function m3(x, y) { return x && y; }\n",
13749 "foo.js",
13750 |metric| {
13751 assert_eq!(metric.abc.conditions_sum(), 4);
13752 insta::assert_json_snapshot!(metric.abc);
13753 },
13754 );
13755 }
13756
13757 // ----- JS / TS / Tsx / Mozjs unary-conditional walker -----
13758
13759 #[cfg(feature = "javascript")]
13760 #[test]
13761 fn javascript_if_multiple_conditions() {
13762 check_metrics::<JavascriptParser>(
13763 "function f(a, b, c, d) {\n\
13764 \x20 if (a || b || c || d) {} // +4c\n\
13765 \x20 if (a && b && c) {} // +3c\n\
13766 \x20 if (!a && !b) {} // +2c\n\
13767 }\n",
13768 "foo.js",
13769 |metric| {
13770 assert_eq!(metric.abc.conditions_sum(), 9);
13771 insta::assert_json_snapshot!(metric.abc);
13772 },
13773 );
13774 }
13775
13776 #[cfg(feature = "javascript")]
13777 #[test]
13778 fn javascript_while_and_do_while_conditions() {
13779 check_metrics::<JavascriptParser>(
13780 "function f(a, b) {\n\
13781 \x20 while (a || b) {} // +2c\n\
13782 \x20 do {} while (a && !b); // +2c\n\
13783 }\n",
13784 "foo.js",
13785 |metric| {
13786 assert_eq!(metric.abc.conditions_sum(), 4);
13787 insta::assert_json_snapshot!(metric.abc);
13788 },
13789 );
13790 }
13791
13792 #[cfg(feature = "javascript")]
13793 #[test]
13794 fn javascript_short_circuit_with_boolean_literal_operand() {
13795 check_metrics::<JavascriptParser>(
13796 "function f(a) { return a && true; }\n",
13797 "foo.js",
13798 |metric| {
13799 assert_eq!(metric.abc.conditions_sum(), 2);
13800 insta::assert_json_snapshot!(metric.abc);
13801 },
13802 );
13803 }
13804
13805 #[cfg(feature = "typescript")]
13806 #[test]
13807 fn typescript_if_multiple_conditions() {
13808 check_metrics::<TypescriptParser>(
13809 "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
13810 \x20 if (a || b || c || d) {} // +4c\n\
13811 \x20 if (a && b && c) {} // +3c\n\
13812 \x20 if (!a && !b) {} // +2c\n\
13813 }\n",
13814 "foo.ts",
13815 |metric| {
13816 assert_eq!(metric.abc.conditions_sum(), 9);
13817 insta::assert_json_snapshot!(metric.abc);
13818 },
13819 );
13820 }
13821
13822 #[cfg(feature = "typescript")]
13823 #[test]
13824 fn typescript_while_and_do_while_conditions() {
13825 check_metrics::<TypescriptParser>(
13826 "function f(a: boolean, b: boolean) {\n\
13827 \x20 while (a || b) {} // +2c\n\
13828 \x20 do {} while (a && !b); // +2c\n\
13829 }\n",
13830 "foo.ts",
13831 |metric| {
13832 assert_eq!(metric.abc.conditions_sum(), 4);
13833 insta::assert_json_snapshot!(metric.abc);
13834 },
13835 );
13836 }
13837
13838 #[cfg(feature = "typescript")]
13839 #[test]
13840 fn typescript_short_circuit_with_boolean_literal_operand() {
13841 check_metrics::<TypescriptParser>(
13842 "function f(a: boolean): boolean { return a && true; }\n",
13843 "foo.ts",
13844 |metric| {
13845 assert_eq!(metric.abc.conditions_sum(), 2);
13846 insta::assert_json_snapshot!(metric.abc);
13847 },
13848 );
13849 }
13850
13851 #[cfg(feature = "typescript")]
13852 #[test]
13853 fn tsx_if_multiple_conditions() {
13854 check_metrics::<TsxParser>(
13855 "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
13856 \x20 if (a || b || c || d) {} // +4c\n\
13857 \x20 if (a && b && c) {} // +3c\n\
13858 \x20 if (!a && !b) {} // +2c\n\
13859 }\n",
13860 "foo.tsx",
13861 |metric| {
13862 assert_eq!(metric.abc.conditions_sum(), 9);
13863 insta::assert_json_snapshot!(metric.abc);
13864 },
13865 );
13866 }
13867
13868 #[cfg(feature = "typescript")]
13869 #[test]
13870 fn tsx_while_and_do_while_conditions() {
13871 check_metrics::<TsxParser>(
13872 "function f(a: boolean, b: boolean) {\n\
13873 \x20 while (a || b) {} // +2c\n\
13874 \x20 do {} while (a && !b); // +2c\n\
13875 }\n",
13876 "foo.tsx",
13877 |metric| {
13878 assert_eq!(metric.abc.conditions_sum(), 4);
13879 insta::assert_json_snapshot!(metric.abc);
13880 },
13881 );
13882 }
13883
13884 #[cfg(feature = "typescript")]
13885 #[test]
13886 fn tsx_short_circuit_with_boolean_literal_operand() {
13887 check_metrics::<TsxParser>(
13888 "function f(a: boolean): boolean { return a && true; }\n",
13889 "foo.tsx",
13890 |metric| {
13891 assert_eq!(metric.abc.conditions_sum(), 2);
13892 insta::assert_json_snapshot!(metric.abc);
13893 },
13894 );
13895 }
13896
13897 #[cfg(feature = "mozjs")]
13898 #[test]
13899 fn mozjs_if_multiple_conditions() {
13900 check_metrics::<MozjsParser>(
13901 "function f(a, b, c, d) {\n\
13902 \x20 if (a || b || c || d) {} // +4c\n\
13903 \x20 if (a && b && c) {} // +3c\n\
13904 \x20 if (!a && !b) {} // +2c\n\
13905 }\n",
13906 "foo.js",
13907 |metric| {
13908 assert_eq!(metric.abc.conditions_sum(), 9);
13909 insta::assert_json_snapshot!(metric.abc);
13910 },
13911 );
13912 }
13913
13914 #[cfg(feature = "mozjs")]
13915 #[test]
13916 fn mozjs_while_and_do_while_conditions() {
13917 check_metrics::<MozjsParser>(
13918 "function f(a, b) {\n\
13919 \x20 while (a || b) {} // +2c\n\
13920 \x20 do {} while (a && !b); // +2c\n\
13921 }\n",
13922 "foo.js",
13923 |metric| {
13924 assert_eq!(metric.abc.conditions_sum(), 4);
13925 insta::assert_json_snapshot!(metric.abc);
13926 },
13927 );
13928 }
13929
13930 #[cfg(feature = "mozjs")]
13931 #[test]
13932 fn mozjs_short_circuit_with_boolean_literal_operand() {
13933 check_metrics::<MozjsParser>(
13934 "function f(a) { return a && true; }\n",
13935 "foo.js",
13936 |metric| {
13937 assert_eq!(metric.abc.conditions_sum(), 2);
13938 insta::assert_json_snapshot!(metric.abc);
13939 },
13940 );
13941 }
13942
13943 // ---------- Perl ABC tests ----------
13944
13945 #[cfg(feature = "perl")]
13946 #[test]
13947 fn perl_empty_unit_zero() {
13948 // Empty source produces zero ABC magnitude — pins the trait
13949 // wiring without exercising any compute branch.
13950 check_metrics::<PerlParser>("", "empty.pl", |metric| {
13951 assert_eq!(metric.abc.assignments_sum(), 0);
13952 assert_eq!(metric.abc.branches_sum(), 0);
13953 assert_eq!(metric.abc.conditions_sum(), 0);
13954 insta::assert_json_snapshot!(metric.abc);
13955 });
13956 }
13957
13958 #[cfg(feature = "perl")]
13959 #[test]
13960 fn perl_plain_and_compound_assignments_count() {
13961 // `my $x = 0` parses as a `binary_expression` with an `=`
13962 // token, so the initialiser counts (Perl has no equivalent of
13963 // the JS `const` initialiser-suppression rule). Each
13964 // assignment operator token contributes one assignment:
13965 // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
13966 // from the `my $x = 0` initialiser and the later `$x = 5`
13967 // reassignment.
13968 check_metrics::<PerlParser>(
13969 "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
13970 "foo.pl",
13971 |metric| {
13972 assert_eq!(metric.abc.assignments_sum(), 5);
13973 assert_eq!(metric.abc.branches_sum(), 0);
13974 assert_eq!(metric.abc.conditions_sum(), 0);
13975 insta::assert_json_snapshot!(metric.abc);
13976 },
13977 );
13978 }
13979
13980 #[cfg(feature = "perl")]
13981 #[test]
13982 fn perl_calls_are_branches() {
13983 // `foo()` parses as `call_expression_with_args_with_brackets`
13984 // wrapping an inner `call_expression_with_bareword(foo)`;
13985 // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
13986 // appears as a standalone bareword. The bareword-inside-
13987 // wrapper case must NOT double-count — only the outer wrapper
13988 // contributes a branch. So B = 3 (foo, bar, shift), not 5.
13989 check_metrics::<PerlParser>(
13990 "sub f { foo(); bar 1, 2; my $a = shift; }",
13991 "foo.pl",
13992 |metric| {
13993 // shift's `my $a = shift` initialiser contributes one
13994 // assignment via the `=` token.
13995 assert_eq!(metric.abc.assignments_sum(), 1);
13996 assert_eq!(metric.abc.branches_sum(), 3);
13997 assert_eq!(metric.abc.conditions_sum(), 0);
13998 insta::assert_json_snapshot!(metric.abc);
13999 },
14000 );
14001 }
14002
14003 #[cfg(feature = "perl")]
14004 #[test]
14005 fn perl_method_invocation_counts_as_branch() {
14006 // `$obj->method(...)` parses as `method_invocation`. Any
14007 // arrow-dispatch counts as one branch regardless of how the
14008 // arguments are passed.
14009 check_metrics::<PerlParser>(
14010 "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
14011 "foo.pl",
14012 |metric| {
14013 // `my $obj = shift` → A=1, B=1 (shift bareword).
14014 // `$obj->run($x)` and `$obj->ping` → 2 more branches.
14015 assert_eq!(metric.abc.assignments_sum(), 1);
14016 assert_eq!(metric.abc.branches_sum(), 3);
14017 assert_eq!(metric.abc.conditions_sum(), 0);
14018 insta::assert_json_snapshot!(metric.abc);
14019 },
14020 );
14021 }
14022
14023 // #1297's own sweep cleared Perl, and it was wrong. `<STDIN>` does
14024 // lex as a single `standard_input` token and scores 0, which is
14025 // what the issue measured — but the filehandle and lexical-handle
14026 // readlines do not: `<FH>` is `standard_input_to_identifier` and
14027 // `<$fh>` is `standard_input_to_variable`, each a plain three-token
14028 // sequence whose brackets are the same bare `<` / `>` a comparison
14029 // uses, and the arm was ungated. Each scored two phantom
14030 // conditions.
14031 //
14032 // All three readline spellings against two genuine comparisons, so
14033 // every mis-aim lands on its own number: 6 pre-fix, 2 once the arm
14034 // allows `BinaryExpression`, 0 if it allows the wrong parent or the
14035 // fixture stops parsing. The assertion is also the grammar-dispatch
14036 // §8 pin — `cyclomatic()` is 3 on this space, so decisions is 2 and
14037 // the two counts agree exactly.
14038 #[cfg(feature = "perl")]
14039 #[test]
14040 fn perl_readline_angle_brackets_are_not_conditions() {
14041 check_func_space::<PerlParser, _>(
14042 "sub g {\n\
14043 my ($a, $b, $fh) = @_;\n\
14044 my $l = <FH>;\n\
14045 my $m = <$fh>;\n\
14046 my $n = <STDIN>;\n\
14047 if ($a < $b) { return 1; }\n\
14048 if ($a > $b) { return 2; }\n\
14049 return 0;\n\
14050 }",
14051 "foo.pl",
14052 |space| {
14053 assert_deepest_conditions_match_cyclomatic(&space, 2);
14054 // Fixture-presence anchor: the three readlines are
14055 // three of this sub's four assignments (the fourth is
14056 // the `my (...) = @_` unpack), so dropping them from
14057 // the fixture fails here rather than leaving a test
14058 // that still reports 2 conditions and proves nothing.
14059 assert_eq!(space.metrics.abc.assignments_sum(), 4);
14060 },
14061 );
14062 }
14063
14064 #[cfg(feature = "perl")]
14065 #[test]
14066 fn perl_numeric_and_string_comparisons_count_conditions() {
14067 // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
14068 // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
14069 // fire once per token. The sample below uses one of each →
14070 // C = 14. No assignments, no branches.
14071 check_metrics::<PerlParser>(
14072 "sub f {\n\
14073 my $r;\n\
14074 $r = $a == $b;\n\
14075 $r = $a != $b;\n\
14076 $r = $a < $b;\n\
14077 $r = $a > $b;\n\
14078 $r = $a <= $b;\n\
14079 $r = $a >= $b;\n\
14080 $r = $a <=> $b;\n\
14081 $r = $a eq $b;\n\
14082 $r = $a ne $b;\n\
14083 $r = $a lt $b;\n\
14084 $r = $a gt $b;\n\
14085 $r = $a le $b;\n\
14086 $r = $a ge $b;\n\
14087 $r = $a cmp $b;\n\
14088 }",
14089 "foo.pl",
14090 |metric| {
14091 // 15 `=` tokens: one declaration `my $r` (no `=`),
14092 // then 14 `$r = …` plus there's no `=` in `my $r;`.
14093 // Actually: `my $r;` has no `=`; the 14 `$r = …` are
14094 // 14 `=` tokens. So A=14, C=14.
14095 assert_eq!(metric.abc.assignments_sum(), 14);
14096 assert_eq!(metric.abc.branches_sum(), 0);
14097 assert_eq!(metric.abc.conditions_sum(), 14);
14098 insta::assert_json_snapshot!(metric.abc);
14099 },
14100 );
14101 }
14102
14103 #[cfg(feature = "perl")]
14104 #[test]
14105 fn perl_short_circuit_not_counted_directly_ternary_counts() {
14106 // `&&`, `||`, `//`, low-precedence `and`, `or` are NOT
14107 // counted as conditions on their own (Fitzpatrick Rule 5;
14108 // #395) — instead each operand is counted as a unary
14109 // conditional by the walker (Rule 9; #403). `xor` evaluates
14110 // both operands, so it is a value like `^` and, outside a
14111 // slot, scores nothing (#1536). The pinned
14112 // tree-sitter-perl parses the low-precedence `and` as a
14113 // two-operand `unary_expression` rather than a
14114 // `binary_expression`; the walker reads the operands of either
14115 // since #1526, which is what moved this count from 10 to 14.
14116 // The low-precedence forms bind looser than `=`, so
14117 // `$r = $a and $b` is `($r = $a) and $b`, and its left operand
14118 // pays through the assignment's value.
14119 // Net: 5 chain lines × 2 operands + 0 for the `xor` line + 1
14120 // ternary node + 1 for the ternary's bare `$a` condition operand
14121 // (#1102) = 12.
14122 check_metrics::<PerlParser>(
14123 "sub f {\n\
14124 my $r;\n\
14125 $r = $a && $b;\n\
14126 $r = $a || $b;\n\
14127 $r = $a // $b;\n\
14128 $r = $a and $b;\n\
14129 $r = $a or $b;\n\
14130 $r = $a xor $b;\n\
14131 $r = $a ? 1 : 2;\n\
14132 }",
14133 "foo.pl",
14134 |metric| {
14135 // 7 `=` tokens (one per reassignment line).
14136 assert_eq!(metric.abc.assignments_sum(), 7);
14137 assert_eq!(metric.abc.branches_sum(), 0);
14138 // 5 chain lines × 2 operands + 1 ternary node + 1 for
14139 // its bare `$a` condition operand = 12.
14140 assert_eq!(metric.abc.conditions_sum(), 12);
14141 insta::assert_json_snapshot!(metric.abc);
14142 },
14143 );
14144 }
14145
14146 // Issue #1102, Perl half. See
14147 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
14148 // rule. Like PHP, Perl's ABC dispatcher has no `?`-token arm — the
14149 // grammar does emit the token, but the `ternary_expression` node is
14150 // what carries the tally's +1. tree-sitter-perl names the branch
14151 // fields `true` / `false` rather than the C-family `consequence` /
14152 // `alternative`, so a copied C-family gate would match nothing.
14153 #[cfg(feature = "perl")]
14154 #[test]
14155 fn perl_ternary_operand_slots_count_as_unary_conditions() {
14156 // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
14157 check_metrics::<PerlParser>("sub f { my $x = $a ? !$b : !$c; }", "foo.pl", |metric| {
14158 assert_eq!(metric.abc.conditions_sum(), 4);
14159 });
14160 // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
14161 // the fix.
14162 check_metrics::<PerlParser>(
14163 "sub f { my $x = ($a > 0) ? $b : -$b; }",
14164 "foo.pl",
14165 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
14166 );
14167 // A negated *condition*: the slot peels the `!`
14168 // (`perl_wrapper_operand`) and pays for `$a`, which does not
14169 // score itself. ternary (1) + `!$a` (1) = 2.
14170 check_metrics::<PerlParser>("sub f { my $x = !$a ? $b : $c; }", "foo.pl", |metric| {
14171 assert_eq!(metric.abc.conditions_sum(), 2);
14172 });
14173 // Nested: two ternary nodes plus the two bare-variable
14174 // conditions = 4.
14175 check_metrics::<PerlParser>(
14176 "sub f { my $x = $a ? ($b ? $c : $d) : $e; }",
14177 "foo.pl",
14178 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
14179 );
14180 }
14181
14182 // Issue #1273. tree-sitter-perl 1.1.2 parses `$a > $b ? 1 : 2` as
14183 // `$a > ($b ? 1 : 2)`, and the ternary's condition slot then paid
14184 // for `$b`: 3 against JavaScript's 2 for `a > b ? 1 : 2`. Every
14185 // chaining comparison does it; each row must also equal its
14186 // parenthesised spelling, which parses correctly and so takes its
14187 // value from outside the fix.
14188 #[cfg(feature = "perl")]
14189 #[test]
14190 fn perl_comparison_conditioned_ternary_counts_the_comparison_once() {
14191 let operators = [
14192 "<", ">", "<=", ">=", "lt", "gt", "le", "ge", "==", "!=", "eq", "ne",
14193 ];
14194 for op in operators {
14195 let bare = format!("sub f {{ my $m = $a {op} $b ? 1 : 2; }}\n");
14196 let grouped = format!("sub f {{ my $m = ($a {op} $b) ? 1 : 2; }}\n");
14197 // The ternary (1) and the comparison token (1).
14198 assert_eq!(abc_conditions(LANG::Perl, &bare), 2, "`{op}`");
14199 assert_eq!(abc_conditions(LANG::Perl, &grouped), 2, "`({op})`");
14200 }
14201 }
14202
14203 // Issue #1273, the general shape: a chaining comparison swallows
14204 // every lower-precedence operator to its right, so a logical chain
14205 // misparses too — `$a > $b && $c` is `$a > ($b && $c)`, and the
14206 // chain walker paid for `$b`. Expected values are the JavaScript
14207 // twins (`a > b && c ? 1 : 2` = 3, …) or, where JavaScript has no
14208 // spelling, the parenthesised Perl one.
14209 #[cfg(feature = "perl")]
14210 #[test]
14211 fn perl_misparsed_comparison_operand_is_not_a_condition() {
14212 let cases = [
14213 // `<` `<` `?`; `($a < $b < $c) ? 1 : 2` is 3.
14214 ("my $m = $a < $b < $c ? 1 : 2;", 3),
14215 // `>` `?` `>` `?`, as `a > b ? (c > d ? 1 : 2) : 3`.
14216 ("my $m = $a > $b ? $c > $d ? 1 : 2 : 3;", 4),
14217 // Two ternaries and two comparisons, as the JS twin.
14218 ("if ($a > $b ? $c : $d) { } return $a == $b ? 1 : 0;", 4),
14219 // The chain is the ternary's condition: `>` `?` `$c`.
14220 ("my $m = $a > $b && $c ? 1 : 2;", 3),
14221 // The ternary hangs off the chain's right comparison.
14222 ("my $m = $x > 0 && $y > 0 ? 1 : 2;", 3),
14223 ("my $m = $x && $a > $b ? 1 : 2;", 3),
14224 ("if ($a > $b && $c) { }", 2),
14225 ("if ($a eq $b && $c eq $d) { }", 2),
14226 // `&&` is the first operand of `||`: the climb crosses it.
14227 ("if ($a > $b && $c || $d) { }", 3),
14228 ("if ($a > $b && $c && $d) { }", 3),
14229 // `and` is a two-operand `unary_expression`, which the
14230 // climb crosses too: `(($a > $b) && $c) and $d` is 3.
14231 ("if ($a > $b and $c) { }", 2),
14232 ("if ($a > $b && $c and $d) { }", 3),
14233 // A comment between the operator and the ternary is an
14234 // `extra` sibling, not the operator.
14235 ("my $m = $a > # c\n $b ? 1 : 2;", 2),
14236 ];
14237 for (body, expected) in cases {
14238 let src = format!("sub f {{ {body} }}\n");
14239 assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14240 }
14241 }
14242
14243 // Issue #1273: shapes the workaround must leave alone. The explicit
14244 // `$a > ($b ? 1 : 2)` puts an `array` between the comparison and
14245 // the ternary, so `$b` really is a condition: 3, as
14246 // `a > (b ? 1 : 2)` in JavaScript. `<=>`, `cmp` and `=~` do not
14247 // chain and parse correctly; those rows pin that grammar fact, not
14248 // the workaround's operator list — adding `<=>` to the list moves
14249 // nothing, since no tree ever puts a ternary straight after it.
14250 #[cfg(feature = "perl")]
14251 #[test]
14252 fn perl_correctly_parsed_ternaries_keep_their_condition_slot() {
14253 let cases = [
14254 ("my $m = ($a > $b) ? 1 : 2;", 2),
14255 ("my $m = $a <=> $b ? 1 : 2;", 2),
14256 ("my $m = $a cmp $b ? 1 : 2;", 2),
14257 ("my $m = $a =~ /x/ ? 1 : 2;", 2),
14258 ("my $m = $a > ($b ? 1 : 2);", 3),
14259 ("my $m = $a ? !$b : !$c;", 4),
14260 ("my $m = $a && $b ? 1 : 2;", 3),
14261 ];
14262 for (body, expected) in cases {
14263 let src = format!("sub f {{ {body} }}\n");
14264 assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14265 }
14266 }
14267
14268 #[cfg(feature = "perl")]
14269 #[test]
14270 fn perl_elsif_and_else_count_conditions() {
14271 // `if (… == …) { … } elsif (… < …) { … } else { … }` →
14272 // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
14273 // `else_clause` each + 1 → C = 4. Branches: 0 (only
14274 // assignments). Assignments: just the `=` initialisers /
14275 // reassignments — there are 4 here (`$x` init plus three
14276 // `$x = …` reassigns).
14277 check_metrics::<PerlParser>(
14278 "sub f {\n\
14279 my $x = 0;\n\
14280 if ($a == $b) {\n\
14281 $x = 1;\n\
14282 } elsif ($a < $b) {\n\
14283 $x = 2;\n\
14284 } else {\n\
14285 $x = 3;\n\
14286 }\n\
14287 }",
14288 "foo.pl",
14289 |metric| {
14290 assert_eq!(metric.abc.assignments_sum(), 4);
14291 assert_eq!(metric.abc.branches_sum(), 0);
14292 assert_eq!(metric.abc.conditions_sum(), 4);
14293 insta::assert_json_snapshot!(metric.abc);
14294 },
14295 );
14296 }
14297
14298 #[cfg(feature = "perl")]
14299 #[test]
14300 fn perl_regex_match_operators_count_conditions() {
14301 // `=~` and `!~` are pattern-match operators; we count both
14302 // as conditions because they evaluate the regex match in a
14303 // boolean context.
14304 check_metrics::<PerlParser>(
14305 "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
14306 "foo.pl",
14307 |metric| {
14308 // 3 `=` tokens, 0 branches except `shift` bareword.
14309 assert_eq!(metric.abc.assignments_sum(), 3);
14310 assert_eq!(metric.abc.branches_sum(), 1);
14311 assert_eq!(metric.abc.conditions_sum(), 2);
14312 insta::assert_json_snapshot!(metric.abc);
14313 },
14314 );
14315 }
14316
14317 #[cfg(feature = "perl")]
14318 #[test]
14319 fn perl_complex_function_abc() {
14320 // Mixed program exercising every category. Computed
14321 // expected:
14322 // Assignments: `my $i = 0` (1), `$i++` is a unary
14323 // increment — Perl's grammar emits `PLUSPLUS` not an `=`
14324 // operator, so it does NOT count under the operator-
14325 // token rule. The for-loop's `$i++` is similarly
14326 // uncounted.
14327 // Total A: 1 from `my $i = 0`, 1 from `$total += $i`
14328 // (the `+=` token) → A = 2.
14329 // Branches: `do_work($i)` → 1; `print "done\n"` is a
14330 // call_expression_with_spaced_args → 1; `return $total`
14331 // uses the `return` keyword not a call → 0. B = 2.
14332 // Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
14333 // 1; `else_clause` → 1. C = 3.
14334 check_metrics::<PerlParser>(
14335 "sub run {\n\
14336 my $total = 0;\n\
14337 for (my $i = 0; $i < 10; $i++) {\n\
14338 if ($i % 2 == 0) {\n\
14339 do_work($i);\n\
14340 } else {\n\
14341 $total += $i;\n\
14342 }\n\
14343 }\n\
14344 print \"done\\n\";\n\
14345 return $total;\n\
14346 }",
14347 "foo.pl",
14348 |metric| {
14349 // `my $total = 0` is one `=`; `my $i = 0` is another
14350 // `=`; `$total += $i` is one `+=`. Total = 3.
14351 assert_eq!(metric.abc.assignments_sum(), 3);
14352 assert_eq!(metric.abc.branches_sum(), 2);
14353 assert_eq!(metric.abc.conditions_sum(), 3);
14354 insta::assert_json_snapshot!(metric.abc);
14355 },
14356 );
14357 }
14358
14359 #[cfg(feature = "perl")]
14360 #[test]
14361 fn perl_if_multiple_conditions() {
14362 // Fitzpatrick Rule 9 walker (issue #403): each operand of a
14363 // `&&` / `||` / `//` / `and` / `or` chain is one
14364 // condition. ScalarVariable operands ($a, $b, …) qualify as
14365 // terminal-bool kinds for the walker.
14366 check_metrics::<PerlParser>(
14367 "sub f {\n\
14368 my ($a, $b, $c, $d) = @_;\n\
14369 if ($a || $b || $c || $d) { return 1; } # +4c\n\
14370 if ($a && $b && $c) { return 2; } # +3c\n\
14371 if (!$a && !$b) { return 3; } # +2c\n\
14372 return 0;\n\
14373 }",
14374 "foo.pl",
14375 |metric| {
14376 assert_eq!(metric.abc.conditions_sum(), 9);
14377 insta::assert_json_snapshot!(metric.abc);
14378 },
14379 );
14380 }
14381
14382 #[cfg(feature = "perl")]
14383 #[test]
14384 fn perl_while_and_until_conditions() {
14385 // Perl has no `do { ... } while(cond);` shape in this grammar
14386 // — `while` and `until` are the loop forms with a condition
14387 // slot. The walker fires on each `&&` / `||` token inside
14388 // those headers.
14389 check_metrics::<PerlParser>(
14390 "sub f {\n\
14391 my ($a, $b) = @_;\n\
14392 while ($a || $b) { last; } # +2c\n\
14393 until ($a && !$b) { last; } # +2c\n\
14394 }",
14395 "foo.pl",
14396 |metric| {
14397 assert_eq!(metric.abc.conditions_sum(), 4);
14398 insta::assert_json_snapshot!(metric.abc);
14399 },
14400 );
14401 }
14402
14403 #[cfg(feature = "perl")]
14404 #[test]
14405 fn perl_for_header_condition_slot_counts_unary_conditions() {
14406 // The Perl half of #1276. The C-style `for` header's condition
14407 // slot is a Rule 9 unary condition like the `if` slot: a bare
14408 // `$ok`, a negated `!$ok` and a parenthesised `($ok)` each count
14409 // one, an empty header and a `foreach` count zero, and a
14410 // comparison-shaped `$i < $n` stays at the one the `<` arm
14411 // already counts. Every C-style row is the three-clause spelling
14412 // because tree-sitter-perl does not parse an empty initializer.
14413 let cases = [
14414 ("for (my $i = 0; $ok; $i++) { }", 1),
14415 ("for (my $i = 0; !$ok; $i++) { }", 1),
14416 ("for (my $i = 0; ($ok); $i++) { }", 1),
14417 ("for (my $i = 0; $i < $n; $i++) { }", 1),
14418 ("for (my $i = 0; $ok && $j; $i++) { }", 2),
14419 ("for (;;) { last; }", 0),
14420 ("for my $x (@l) { }", 0),
14421 ];
14422 let mut ran = 0;
14423 for (body, expected) in cases {
14424 let src = format!("sub f {{ {body} }}\n");
14425 assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14426 ran += 1;
14427 }
14428 assert_eq!(ran, cases.len());
14429 assert!(cases.iter().any(|&(_, n)| n == 0));
14430 assert!(cases.iter().any(|&(_, n)| n == 2));
14431
14432 // The slot agrees with the `if` slot for every shape, which is
14433 // the property the fix restores; the reference is non-degenerate
14434 // by the table above.
14435 for condition in ["$ok", "!$ok", "($ok)", "$ok && $j"] {
14436 let in_for = format!("sub f {{ for (my $i = 0; {condition}; $i++) {{ }} }}\n");
14437 let in_if = format!("sub f {{ if ({condition}) {{ }} }}\n");
14438 assert_eq!(
14439 abc_conditions(LANG::Perl, &in_for),
14440 abc_conditions(LANG::Perl, &in_if),
14441 "`{condition}` must score alike in a `for` header and an `if`",
14442 );
14443 }
14444 }
14445
14446 #[cfg(feature = "perl")]
14447 #[test]
14448 fn perl_short_circuit_counts_scalar_variable_operands() {
14449 // `$a && $b` reports 2 conditions — one walker count per
14450 // `ScalarVariable` operand. Renamed from the cross-language
14451 // `_with_boolean_literal_operand` convention because Perl has
14452 // no readily-grammar-exposed boolean literal in an `&&`
14453 // operand slot at the pinned grammar version (the `Boolean`
14454 // kind only fires on the `boolean` pragma's named constants,
14455 // not bareword `1` / `0`). Two scalar variables are the
14456 // grammar-stable terminal-set witness for Perl.
14457 check_metrics::<PerlParser>(
14458 "sub f { my ($a) = @_; return $a && $b; }\n",
14459 "foo.pl",
14460 |metric| {
14461 assert_eq!(metric.abc.conditions_sum(), 2);
14462 insta::assert_json_snapshot!(metric.abc);
14463 },
14464 );
14465 }
14466
14467 #[cfg(feature = "perl")]
14468 #[test]
14469 fn perl_array_in_binary_operand_descends_to_scalar_context_value() {
14470 // Regression test for the code-review findings on the
14471 // Phase-2B Perl walker:
14472 // - Pre-fix-A: the former `perl_inspect_container` descended `Array`
14473 // via `node.child(1)` — the FIRST element — wrongly
14474 // attributing `$x` for `($x, $y)` (semantically `$y`
14475 // is the scalar-context value).
14476 // - Fix-A (the `array_is_paren` guard, 5db8078): dropped
14477 // Array-as-paren entirely in `BinaryExpression` operand
14478 // contexts to avoid the wrong attribution — but
14479 // regressed `$a || ($x)` (single paren-grouped operand)
14480 // to C=1 instead of 2.
14481 // - Fix-B (this change): keeps Array-as-paren unconditional
14482 // but descends via the LAST named child. `$a || ($x)`
14483 // reaches `$x` (count both operands → 2);
14484 // `$a || ($x, $y)` reaches `$y` (count `$a` + `$y` →
14485 // still 2, matching Fitzpatrick Rule 7 "one per
14486 // operand"); `if ($a)` still reaches `$a` (single-
14487 // element grouping → 1).
14488 check_metrics::<PerlParser>(
14489 "sub f { my ($a, $x, $y) = @_;\n\
14490 \x20 my $r = $a || ($x, $y); # +2c: $a + last-named $y\n\
14491 \x20 my $s = $a || ($x); # +2c: $a + only-named $x\n\
14492 \x20 $r + $s;\n\
14493 }\n",
14494 "foo.pl",
14495 |metric| {
14496 // 2 + 2 = 4 unary conditions from the two `||`s.
14497 assert_eq!(metric.abc.conditions_sum(), 4);
14498 insta::assert_json_snapshot!(metric.abc);
14499 },
14500 );
14501 }
14502
14503 #[cfg(feature = "perl")]
14504 #[test]
14505 fn perl_if_scalar_variable_condition() {
14506 // Renamed from the cross-language
14507 // `_if_boolean_literal_condition` convention because
14508 // Perl has no readily-grammar-exposed boolean literal in
14509 // an `if (cond)` slot at the pinned grammar version:
14510 // tree-sitter-perl's `Boolean` kind only fires for the
14511 // `boolean` pragma's named constants (not bareword `1` /
14512 // `0`, which surface as `Integer` / not in the
14513 // terminal-bool set). A scalar-variable condition is the
14514 // grammar-stable witness — `if ($a)` reaches
14515 // `scalar_variable` via the `Array` paren unwrap.
14516 check_metrics::<PerlParser>(
14517 "sub f { my ($a) = @_; if ($a) { return 1; } }\n",
14518 "foo.pl",
14519 |metric| {
14520 assert_eq!(metric.abc.conditions_sum(), 1);
14521 insta::assert_json_snapshot!(metric.abc);
14522 },
14523 );
14524 }
14525
14526 #[cfg(feature = "perl")]
14527 #[test]
14528 fn perl_methods_arguments_with_conditions() {
14529 // `call(!$a, !$b)` — argument list walker counts each
14530 // unary-conditional argument once. Cannot use `m(...)` as
14531 // the function name — tree-sitter-perl parses `m(...)` as
14532 // the regex-match operator, not a function call.
14533 check_metrics::<PerlParser>(
14534 "sub f { my ($a, $b) = @_; call($a, $b); call(!$a, !$b); }\n",
14535 "foo.pl",
14536 |metric| {
14537 // Two calls × 1 branch each = 2 branches.
14538 // `call(!$a, !$b)` contributes 2 walker conditions
14539 // (one per `!`-wrapped scalar-variable argument);
14540 // `call($a, $b)` contributes 0 (bare-args don't
14541 // count via the Arguments walker — list_kind !=
14542 // BinaryExpression).
14543 assert_eq!(metric.abc.branches_sum(), 2);
14544 assert_eq!(metric.abc.conditions_sum(), 2);
14545 insta::assert_json_snapshot!(metric.abc);
14546 },
14547 );
14548 }
14549
14550 #[cfg(feature = "perl")]
14551 #[test]
14552 fn perl_return_with_conditions() {
14553 // `return !$a` reports 1 condition via the walker (unary
14554 // unwrap to scalar-variable terminal). `return $a` reports
14555 // 0 (no paren / unary wrap, has_boolean_content stays
14556 // false from ReturnExpression parent).
14557 check_metrics::<PerlParser>(
14558 "sub m1 { my ($z) = @_; return !($z); }\n\
14559 sub m2 { my ($x) = @_; return (((!$x))); }\n\
14560 sub m3 { my ($x, $y) = @_; return $x && $y; }\n",
14561 "foo.pl",
14562 |metric| {
14563 // m1: !($z) → walker on `!` unwraps paren to $z (1).
14564 // m2: (((!$x))) → walker unwraps three parens + one
14565 // unary to $x (1).
14566 // m3: $x && $y → walker on `&&` counts both (2).
14567 // Sum: 4.
14568 assert_eq!(metric.abc.conditions_sum(), 4);
14569 insta::assert_json_snapshot!(metric.abc);
14570 },
14571 );
14572 }
14573
14574 // A bare `/re/` or `m{re}` is a match against `$_`, a relational
14575 // operator spelled with no operator token, so it scores by use
14576 // (#1467, the rule #1461 applied to five other languages). Each
14577 // `*_out` member and `ret` / `notm` / `grp` / `arg` / `smart` was 0
14578 // before the arm: only a boolean slot reached the pattern, through
14579 // the former `perl_bool_terminal_kinds!()`. `mbare_out` is the `m{}` spelling, a
14580 // sibling kind rather than an alias. `smart` levels a smartmatch
14581 // with its `=~` sibling: no arm counts `~~`, so the pattern does.
14582 //
14583 // The in-slot members are the §5 check that the slot stopped
14584 // counting the pattern in the change that gave it an arm: each
14585 // reads what it read before. `bound_out` / `nbound` pin the gate on
14586 // `=~` / `!~` — the operator token scores, the pattern under it does
14587 // not — and `ctrl_out` is the `==` control the rest level with.
14588 // `s///`, `tr///` and `qr//` are not matches and stay at 0; the
14589 // bound `s///` / `tr///` agree (#1540, the test below).
14590 #[cfg(feature = "perl")]
14591 #[test]
14592 fn perl_bare_match_is_a_condition_by_use() {
14593 check_func_space::<PerlParser, _>(
14594 "sub bare_out { my $r = /^#/; return $r; }\n\
14595 sub mbare_out { my $r = m{^#}; return $r; }\n\
14596 sub ret { return /x/; }\n\
14597 sub notm { my $r = !/x/; return $r; }\n\
14598 sub grp { my @a = @_; return grep { /x/ } @a; }\n\
14599 sub arg { foo(/x/); }\n\
14600 sub smart { my ($x) = @_; my $r = $x ~~ /re/; return $r; }\n\
14601 sub bare_in { if (/^#/) { return 1; } return 0; }\n\
14602 sub mbare_in { unless (m{^#}) { return 1; } return 0; }\n\
14603 sub modif { return 0 unless /x/; return 1; }\n\
14604 sub andy { my ($y) = @_; return /x/ && $y; }\n\
14605 sub oror { return /a/ || /b/; }\n\
14606 sub tern { return /x/ ? 1 : 0; }\n\
14607 sub forc { for (my $i = 0; /x/; $i++) { } }\n\
14608 sub bound_out { my ($x) = @_; my $r = ($x =~ /^#/); return $r; }\n\
14609 sub nbound { my ($x) = @_; my $r = ($x !~ /^#/); return $r; }\n\
14610 sub ctrl_out { my ($x) = @_; my $r = ($x == 1); return $r; }\n\
14611 sub subst { my $n = s/x/y/; return $n; }\n\
14612 sub tr1 { my $n = tr/a/b/; return $n; }\n\
14613 sub qrx { my $q = qr/x/; return $q; }\n",
14614 "foo.pl",
14615 |space| {
14616 assert_members_score(
14617 &space,
14618 &[
14619 ("bare_out", 1, 1),
14620 ("mbare_out", 1, 1),
14621 ("ret", 1, 1),
14622 // `!` adds nothing outside a slot; the match is
14623 // the one condition.
14624 ("notm", 1, 1),
14625 ("grp", 1, 1),
14626 ("arg", 1, 1),
14627 ("smart", 1, 1),
14628 ("bare_in", 1, 2),
14629 ("mbare_in", 1, 2),
14630 ("modif", 1, 2),
14631 // One per `&&` / `||` operand, the match included.
14632 ("andy", 2, 2),
14633 ("oror", 2, 2),
14634 // The ternary node plus its condition.
14635 ("tern", 2, 2),
14636 ("forc", 1, 2),
14637 ("bound_out", 1, 1),
14638 ("nbound", 1, 1),
14639 ("ctrl_out", 1, 1),
14640 ("subst", 0, 1),
14641 ("tr1", 0, 1),
14642 ("qrx", 0, 1),
14643 ],
14644 );
14645 },
14646 );
14647 }
14648
14649 // A substitution or transliteration bound with `=~` / `!~` is the
14650 // same edit as its bare form on `$_`, so it scores by use exactly as
14651 // that twin does: 0 (#1540). Each `*_s` / `*_tr` / `*_y` row below
14652 // was 1 through the operator token before the gate; `count` (an
14653 // assigned count) and `sr` (a `/r` copy, a value) are uses too. A
14654 // match keeps its 1 whatever spells it — a pattern, `qr//`, or a
14655 // variable holding one. The `if_*` rows are the slot side: each pays
14656 // once, as its bare twin `if_s` does, because
14657 // `perl_condition_scores_itself` stops claiming a rewrite binding in
14658 // the same change; `tern_s` and `neg_s` reach that predicate through
14659 // a ternary condition and a `!`-proved `return` operand.
14660 #[cfg(feature = "perl")]
14661 #[test]
14662 fn perl_bound_rewrite_scores_like_its_bare_twin() {
14663 check_func_space::<PerlParser, _>(
14664 "sub ctrl { my ($x) = @_; $x; return $x; }\n\
14665 sub m_bare { my ($x) = @_; /re/; return $x; }\n\
14666 sub m_bound { my ($x) = @_; $x =~ /re/; return $x; }\n\
14667 sub m_nbound { my ($x) = @_; $x !~ /re/; return $x; }\n\
14668 sub m_qr { my ($x) = @_; $x =~ qr/re/; return $x; }\n\
14669 sub m_var { my ($x, $q) = @_; $x =~ $q; return $x; }\n\
14670 sub s_bare { my ($x) = @_; s/a/b/; return $x; }\n\
14671 sub s_bound { my ($x) = @_; $x =~ s/a/b/; return $x; }\n\
14672 sub s_nbound { my ($x) = @_; $x !~ s/a/b/; return $x; }\n\
14673 sub tr_bare { my ($x) = @_; tr/a/b/; return $x; }\n\
14674 sub tr_bound { my ($x) = @_; $x =~ tr/a/b/; return $x; }\n\
14675 sub tr_nbound { my ($x) = @_; $x !~ tr/a/b/; return $x; }\n\
14676 sub y_bound { my ($x) = @_; $x =~ y/a/b/; return $x; }\n\
14677 sub count { my ($x) = @_; my $n = ($x =~ s/a/b/g); return $x; }\n\
14678 sub sr { my ($x) = @_; my $r = $x =~ s/a/b/r; return $r; }\n\
14679 sub if_s { my ($x) = @_; if (s/a/b/) { g(); } return $x; }\n\
14680 sub if_m { my ($x) = @_; if ($x =~ /re/) { g(); } return $x; }\n\
14681 sub if_bs { my ($x) = @_; if ($x =~ s/a/b/) { g(); } return $x; }\n\
14682 sub if_nbs { my ($x) = @_; if ($x !~ s/a/b/) { g(); } return $x; }\n\
14683 sub if_btr { my ($x) = @_; if ($x =~ tr/a/b/) { g(); } return $x; }\n\
14684 sub if_by { my ($x) = @_; if ($x =~ y/a/b/) { g(); } return $x; }\n\
14685 sub if_bsr { my ($x) = @_; if ($x =~ s/a/b/r) { g(); } return $x; }\n\
14686 sub tern_s { my ($x) = @_; return ($x =~ s/a/b/) ? 1 : 0; }\n\
14687 sub neg_s { my ($x) = @_; return !($x =~ s/a/b/); }\n",
14688 "foo.pl",
14689 |space| {
14690 assert_members_score(
14691 &space,
14692 &[
14693 ("ctrl", 0, 1),
14694 ("m_bare", 1, 1),
14695 ("m_bound", 1, 1),
14696 ("m_nbound", 1, 1),
14697 ("m_qr", 1, 1),
14698 ("m_var", 1, 1),
14699 ("s_bare", 0, 1),
14700 ("s_bound", 0, 1),
14701 ("s_nbound", 0, 1),
14702 ("tr_bare", 0, 1),
14703 ("tr_bound", 0, 1),
14704 ("tr_nbound", 0, 1),
14705 ("y_bound", 0, 1),
14706 ("count", 0, 1),
14707 ("sr", 0, 1),
14708 ("if_s", 1, 2),
14709 ("if_m", 1, 2),
14710 ("if_bs", 1, 2),
14711 ("if_nbs", 1, 2),
14712 ("if_btr", 1, 2),
14713 ("if_by", 1, 2),
14714 ("if_bsr", 1, 2),
14715 // The ternary node plus its condition slot.
14716 ("tern_s", 2, 2),
14717 ("neg_s", 1, 1),
14718 ],
14719 );
14720 },
14721 );
14722 }
14723
14724 // The pattern `split` takes first is its delimiter, not a match
14725 // against `$_`, so it is no condition (#1467) in any spelling: the
14726 // spaced and parenthesised call forms, the `m{}` pattern, and the
14727 // `CORE::` qualifier. Three discriminating members keep the exclusion
14728 // narrow: `spl2` puts the pattern in `split`'s *second* slot, where
14729 // it is an ordinary match whose result `split` receives, `grpe`
14730 // puts one first in a call that is not `split`, and `tsplit` puts
14731 // one first in a list whose grandparent holds a bare `split` without
14732 // being that call — the ternary node plus `$c` plus the match.
14733 #[cfg(feature = "perl")]
14734 #[test]
14735 fn perl_split_delimiter_is_not_a_condition() {
14736 use crate::{ParserTrait, Perl};
14737
14738 let src = "sub spl { my ($s) = @_; my @p = split /,/, $s; return @p; }\n\
14739 sub splp { my ($s) = @_; my @p = split(/,/, $s); return @p; }\n\
14740 sub splm { my ($s) = @_; my @p = split m{,}, $s; return @p; }\n\
14741 sub splc { my ($s) = @_; my @p = CORE::split(/,/, $s); return @p; }\n\
14742 sub spl2 { my @p = split ' ', /x/; return @p; }\n\
14743 sub grpe { my @a = @_; return grep /x/, @a; }\n\
14744 sub tsplit { my ($c) = @_; my @r = $c ? split : (/x/); return @r; }\n";
14745
14746 // Fixture census: each member but `tsplit` holds exactly one
14747 // pattern, as a direct argument of its call — so a zero below
14748 // is the exclusion at work and not a pattern the fixture lost.
14749 let parser = PerlParser::new(
14750 src.as_bytes().to_vec(),
14751 &std::path::PathBuf::from("foo.pl"),
14752 None,
14753 );
14754 let call_arguments = parser
14755 .root()
14756 .preorder()
14757 .filter(|n| {
14758 matches!(
14759 n.kind_id().into(),
14760 Perl::PatternMatcher | Perl::PatternMatcherM
14761 )
14762 })
14763 .filter(|n| {
14764 n.parent()
14765 .and_then(|list| list.parent())
14766 .is_some_and(|call| {
14767 matches!(
14768 call.kind_id().into(),
14769 Perl::CallExpressionWithSpacedArgs
14770 | Perl::CallExpressionWithArgsWithBrackets
14771 )
14772 })
14773 })
14774 .count();
14775 assert_eq!(call_arguments, 6, "one call-argument pattern per member");
14776
14777 check_func_space::<PerlParser, _>(src, "foo.pl", |space| {
14778 assert_members_score(
14779 &space,
14780 &[
14781 ("spl", 0, 1),
14782 ("splp", 0, 1),
14783 ("splm", 0, 1),
14784 ("splc", 0, 1),
14785 ("spl2", 1, 1),
14786 ("grpe", 1, 1),
14787 ("tsplit", 3, 2),
14788 ],
14789 );
14790 });
14791 }
14792
14793 // tree-sitter-perl names its separators, so a trailing `,` or `=>`
14794 // was read as the last element of a condition list and paid a slot
14795 // of its own: `if ($a > 1,)` scored 2. `perl -MO=Deparse` reduces
14796 // every `*_sep` row to its `*_twin`, which is where the expected
14797 // values come from; the chain rows put the list inside a `&&`
14798 // operand, which reaches the same peel through
14799 // `perl_count_chain_operands`.
14800 #[cfg(feature = "perl")]
14801 #[test]
14802 fn perl_trailing_separator_scores_like_its_bare_twin() {
14803 check_func_space::<PerlParser, _>(
14804 "sub cmp_twin { my ($a) = @_; if ($a > 1) { g(); } }\n\
14805 sub cmp_comma { my ($a) = @_; if ($a > 1,) { g(); } }\n\
14806 sub cmp_fat { my ($a) = @_; if ($a > 1 =>) { g(); } }\n\
14807 sub var_twin { my ($a) = @_; if ($a) { g(); } }\n\
14808 sub var_comma { my ($a) = @_; if ($a,) { g(); } }\n\
14809 sub chain_twin { my ($a, $b) = @_; if (($a > 1) && $b) { g(); } }\n\
14810 sub chain_comma { my ($a, $b) = @_; if (($a > 1,) && $b) { g(); } }\n",
14811 "foo.pl",
14812 |space| {
14813 assert_members_score(
14814 &space,
14815 &[
14816 ("cmp_twin", 1, 2),
14817 ("cmp_comma", 1, 2),
14818 ("cmp_fat", 1, 2),
14819 ("var_twin", 1, 2),
14820 ("var_comma", 1, 2),
14821 ("chain_twin", 2, 3),
14822 ("chain_comma", 2, 3),
14823 ],
14824 );
14825 },
14826 );
14827 }
14828
14829 // A pattern's role is read on the outermost `( … )` around it, which
14830 // is the value inside it, so each `*_paren` row scores as its bare
14831 // `*_twin` (#1467, #1540); `perl -MO=Deparse` prints each pair
14832 // identically. The reversed binding is the other direction: in
14833 // `/foo/ =~ $x` the pattern is the *left* operand, so it first
14834 // matches `$_` and that result is bound (Deparse: `/foo/ =~ /$x/`) —
14835 // the match and the `=~` both pay, as in the explicit `rev_twin`.
14836 #[cfg(feature = "perl")]
14837 #[test]
14838 fn perl_pattern_role_reads_through_parentheses() {
14839 check_func_space::<PerlParser, _>(
14840 "sub bind_twin { my ($x) = @_; return $x =~ /foo/; }\n\
14841 sub bind_paren { my ($x) = @_; return $x =~ (/foo/); }\n\
14842 sub bind_parens_m { my ($x) = @_; return $x =~ (((m{foo}))); }\n\
14843 sub bind_list { my ($x, $y) = @_; return $x =~ (/foo/, $y); }\n\
14844 sub bind_list_twin { my ($x, $y) = @_; /foo/; return $x =~ $y; }\n\
14845 sub split_twin { my ($x) = @_; my @p = split(/foo/, $x); return @p; }\n\
14846 sub split_paren { my ($x) = @_; my @p = split((/foo/), $x); return @p; }\n\
14847 sub split_sole { my @p = split(/foo/); return @p; }\n\
14848 sub subst_twin { my ($x) = @_; $x =~ s/a/b/; return $x; }\n\
14849 sub subst_paren { my ($x) = @_; $x =~ (s/a/b/); return $x; }\n\
14850 sub rev { my ($x) = @_; return /foo/ =~ $x; }\n\
14851 sub rev_twin { my ($x) = @_; my $m = /foo/; return $m =~ $x; }\n",
14852 "foo.pl",
14853 |space| {
14854 assert_members_score(
14855 &space,
14856 &[
14857 ("bind_twin", 1, 1),
14858 ("bind_paren", 1, 1),
14859 ("bind_parens_m", 1, 1),
14860 // A comma operator: `/foo/` matches `$_` and `$y`
14861 // is what binds, so the match pays as in the twin.
14862 ("bind_list", 2, 1),
14863 ("bind_list_twin", 2, 1),
14864 ("split_twin", 0, 1),
14865 ("split_paren", 0, 1),
14866 // The pattern is the whole argument list, which is
14867 // the call's own and no wrapper to climb through.
14868 ("split_sole", 0, 1),
14869 ("subst_twin", 0, 1),
14870 ("subst_paren", 0, 1),
14871 ("rev", 2, 1),
14872 ("rev_twin", 2, 1),
14873 ],
14874 );
14875 },
14876 );
14877 }
14878
14879 // Only the builtin `split` takes its first pattern as a delimiter:
14880 // `My::split` is a user sub, so the pattern it is handed is an
14881 // ordinary match, as it is for `My::other` (#1467). `CORE::split`
14882 // is the builtin by its full name.
14883 #[cfg(feature = "perl")]
14884 #[test]
14885 fn perl_split_exemption_is_the_builtin_only() {
14886 check_func_space::<PerlParser, _>(
14887 "sub mine { my ($x) = @_; my @p = My::split(/foo/, $x); return @p; }\n\
14888 sub other { my ($x) = @_; my @p = My::other(/foo/, $x); return @p; }\n\
14889 sub core { my ($x) = @_; my @p = CORE::split(/foo/, $x); return @p; }\n",
14890 "foo.pl",
14891 |space| {
14892 assert_members_score(&space, &[("mine", 1, 1), ("other", 1, 1), ("core", 0, 1)]);
14893 },
14894 );
14895 }
14896
14897 // ---------- Lua ABC tests ----------
14898
14899 #[cfg(feature = "lua")]
14900 #[test]
14901 fn lua_empty_unit_zero() {
14902 check_metrics::<LuaParser>("", "empty.lua", |metric| {
14903 assert_eq!(metric.abc.assignments_sum(), 0);
14904 assert_eq!(metric.abc.branches_sum(), 0);
14905 assert_eq!(metric.abc.conditions_sum(), 0);
14906 insta::assert_json_snapshot!(metric.abc);
14907 });
14908 }
14909
14910 #[cfg(feature = "lua")]
14911 #[test]
14912 fn lua_assignments_count_locals_and_plain() {
14913 // `local x = 0` wraps an `assignment_statement` under a
14914 // `variable_declaration`; the inner wrapper still counts.
14915 // Multi-target assignment `a, b = 1, 2` is a single
14916 // `assignment_statement` and contributes 1, NOT 2 — the
14917 // wrapper is the unit of counting (matches the Python rule:
14918 // one `Assignment` node, one assignment).
14919 check_metrics::<LuaParser>(
14920 "function f()\n\
14921 local x = 0\n\
14922 x = 1\n\
14923 local a, b = 1, 2\n\
14924 a, b = b, a\n\
14925 end",
14926 "foo.lua",
14927 |metric| {
14928 assert_eq!(metric.abc.assignments_sum(), 4);
14929 assert_eq!(metric.abc.branches_sum(), 0);
14930 assert_eq!(metric.abc.conditions_sum(), 0);
14931 insta::assert_json_snapshot!(metric.abc);
14932 },
14933 );
14934 }
14935
14936 #[cfg(feature = "lua")]
14937 #[test]
14938 fn lua_calls_are_branches() {
14939 // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
14940 // call form is a `function_call` node. The nested
14941 // `f(g(1))` counts as 2 branches (one per dispatch).
14942 check_metrics::<LuaParser>(
14943 "function r(x)\n\
14944 print(x)\n\
14945 obj.m(x)\n\
14946 obj:m(x)\n\
14947 return f(g(1))\n\
14948 end",
14949 "foo.lua",
14950 |metric| {
14951 assert_eq!(metric.abc.assignments_sum(), 0);
14952 assert_eq!(metric.abc.branches_sum(), 5);
14953 assert_eq!(metric.abc.conditions_sum(), 0);
14954 insta::assert_json_snapshot!(metric.abc);
14955 },
14956 );
14957 }
14958
14959 #[cfg(feature = "lua")]
14960 #[test]
14961 fn lua_comparisons_count_logical_ops_do_not() {
14962 // Each comparison token contributes one condition; `and` /
14963 // `or` are NOT counted on their own (Fitzpatrick Rule 5;
14964 // #395) — instead each operand is counted as a unary
14965 // conditional by the walker (Rule 9; #403). The two
14966 // `a and b` / `a or b` lines add 2 walker conditions each.
14967 check_metrics::<LuaParser>(
14968 "function f(a, b)\n\
14969 local r\n\
14970 r = a == b\n\
14971 r = a ~= b\n\
14972 r = a < b\n\
14973 r = a > b\n\
14974 r = a <= b\n\
14975 r = a >= b\n\
14976 r = a and b\n\
14977 r = a or b\n\
14978 end",
14979 "foo.lua",
14980 |metric| {
14981 // 8 `r = …` reassignments, plus `local r` (no `=`).
14982 assert_eq!(metric.abc.assignments_sum(), 8);
14983 assert_eq!(metric.abc.branches_sum(), 0);
14984 // 6 comparisons (+6) + 2 logical lines × 2 walker
14985 // operands (+4) = 10.
14986 assert_eq!(metric.abc.conditions_sum(), 10);
14987 insta::assert_json_snapshot!(metric.abc);
14988 },
14989 );
14990 }
14991
14992 // #1297: Lua 5.4 brackets a variable attribute with the same two
14993 // bare tokens a comparison uses, and the `<` / `>` arm had no gate,
14994 // so `local x <const> = 1` scored two conditions. Two attributes
14995 // against one genuine `a < b` separate every mis-aim: 5 pre-fix, 1
14996 // once the arm allows `BinaryExpression`, 0 if it allows the wrong
14997 // parent or the fixture stops parsing.
14998 //
14999 // No fixture-presence anchor is available, unlike the JSX and Perl
15000 // fixtures: once excluded, a Lua attribute contributes to no ABC
15001 // axis at all (`local x <const> = 1` and `local x = 1` score
15002 // identically on assignments, branches and conditions), and it
15003 // opens no space. Deleting the two attributes would leave this test
15004 // passing on the `a < b` alone. The coverage that the attributes
15005 // are reaching the arm is the revert test: removing the gate makes
15006 // this the only failing test in the suite.
15007 #[cfg(feature = "lua")]
15008 #[test]
15009 fn lua_variable_attributes_are_not_conditions() {
15010 check_metrics::<LuaParser>(
15011 "local function f(a, b)\n\
15012 local x <const> = 1\n\
15013 local y <close> = nil\n\
15014 if a < b then return x end\n\
15015 return y\n\
15016 end",
15017 "foo.lua",
15018 |metric| {
15019 assert_eq!(metric.abc.conditions_sum(), 1);
15020 },
15021 );
15022 }
15023
15024 #[cfg(feature = "lua")]
15025 #[test]
15026 fn lua_elseif_and_else_count_conditions() {
15027 // Each elseif / else arm of the if contributes one
15028 // condition, mirroring the Python rule.
15029 check_metrics::<LuaParser>(
15030 "function f(x)\n\
15031 if x > 0 then\n\
15032 return 1\n\
15033 elseif x < 0 then\n\
15034 return -1\n\
15035 else\n\
15036 return 0\n\
15037 end\n\
15038 end",
15039 "foo.lua",
15040 |metric| {
15041 // Comparisons: `>`, `<` → 2; elseif_statement → 1;
15042 // else_statement → 1. C = 4. No branches (no calls).
15043 assert_eq!(metric.abc.assignments_sum(), 0);
15044 assert_eq!(metric.abc.branches_sum(), 0);
15045 assert_eq!(metric.abc.conditions_sum(), 4);
15046 insta::assert_json_snapshot!(metric.abc);
15047 },
15048 );
15049 }
15050
15051 #[cfg(feature = "lua")]
15052 #[test]
15053 fn lua_complex_function_abc() {
15054 // Combines every category to pin the metric.
15055 check_metrics::<LuaParser>(
15056 "function run(n)\n\
15057 local total = 0\n\
15058 for i = 1, n do\n\
15059 if i % 2 == 0 then\n\
15060 do_work(i)\n\
15061 else\n\
15062 total = total + i\n\
15063 end\n\
15064 end\n\
15065 print(\"done\")\n\
15066 return total\n\
15067 end",
15068 "foo.lua",
15069 |metric| {
15070 // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
15071 // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
15072 // Conditions: `==` (1), `else_statement` (1) → 2.
15073 assert_eq!(metric.abc.assignments_sum(), 2);
15074 assert_eq!(metric.abc.branches_sum(), 2);
15075 assert_eq!(metric.abc.conditions_sum(), 2);
15076 insta::assert_json_snapshot!(metric.abc);
15077 },
15078 );
15079 }
15080
15081 #[cfg(feature = "lua")]
15082 #[test]
15083 fn lua_if_multiple_conditions() {
15084 // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or`
15085 // are keyword tokens inside a `binary_expression`.
15086 check_metrics::<LuaParser>(
15087 "function f(a, b, c, d)\n\
15088 if a or b or c or d then return 1 end -- +4c\n\
15089 if a and b and c then return 2 end -- +3c\n\
15090 if not a and not b then return 3 end -- +2c\n\
15091 return 0\n\
15092 end",
15093 "foo.lua",
15094 |metric| {
15095 assert_eq!(metric.abc.conditions_sum(), 9);
15096 insta::assert_json_snapshot!(metric.abc);
15097 },
15098 );
15099 }
15100
15101 #[cfg(feature = "lua")]
15102 #[test]
15103 fn lua_while_conditions() {
15104 // Lua has no `do { ... } while(cond);` — `while cond do …
15105 // end` and `repeat … until cond` are the loop forms.
15106 check_metrics::<LuaParser>(
15107 "function f(a, b)\n\
15108 while a or b do break end -- +2c\n\
15109 repeat break until a and not b -- +2c\n\
15110 end",
15111 "foo.lua",
15112 |metric| {
15113 assert_eq!(metric.abc.conditions_sum(), 4);
15114 insta::assert_json_snapshot!(metric.abc);
15115 },
15116 );
15117 }
15118
15119 #[cfg(feature = "lua")]
15120 #[test]
15121 fn lua_short_circuit_with_boolean_literal_operand() {
15122 // `a and true` reports 2 conditions: one Identifier, one
15123 // True keyword literal.
15124 check_metrics::<LuaParser>("function f(a) return a and true end", "foo.lua", |metric| {
15125 assert_eq!(metric.abc.conditions_sum(), 2);
15126 insta::assert_json_snapshot!(metric.abc);
15127 });
15128 }
15129
15130 #[cfg(feature = "lua")]
15131 #[test]
15132 fn lua_number_truthy_condition_counts() {
15133 // Regression for findings.md #2: Lua treats every non-nil,
15134 // non-false value as truthy, so `if 1 then ... end` and
15135 // `return a and 2` should each count their numeric literal
15136 // as a Fitzpatrick Rule 6 / 7 unary condition. Pre-fix,
15137 // the former `lua_bool_terminal_kinds!()` listed `True` /
15138 // `False` / `Nil` but omitted `Number`, so the walker dropped
15139 // every numeric-truthy operand. The walker comment at the top
15140 // of the former `lua_inspect_container` already promised
15141 // numbers were terminal-bool kinds; this commit closed the gap.
15142 check_metrics::<LuaParser>(
15143 "function f(a)\n\
15144 \x20 if 1 then return 1 end\n\
15145 \x20 return a and 2\n\
15146 end",
15147 "foo.lua",
15148 |metric| {
15149 // `if 1 then` → walker counts the Number literal (+1).
15150 // `a and 2` → `and` walker counts both operands:
15151 // identifier `a` (+1), Number `2` (+1).
15152 // Total: 3.
15153 assert_eq!(metric.abc.conditions_sum(), 3);
15154 insta::assert_json_snapshot!(metric.abc);
15155 },
15156 );
15157 }
15158
15159 #[cfg(feature = "lua")]
15160 #[test]
15161 fn lua_if_boolean_literal_condition() {
15162 check_metrics::<LuaParser>(
15163 "function f()\n\
15164 if true then end -- +1c\n\
15165 if not false then end -- +1c\n\
15166 while true do break end -- +1c\n\
15167 repeat break until false -- +1c\n\
15168 end",
15169 "foo.lua",
15170 |metric| {
15171 assert_eq!(metric.abc.conditions_sum(), 4);
15172 insta::assert_json_snapshot!(metric.abc);
15173 },
15174 );
15175 }
15176
15177 #[cfg(feature = "lua")]
15178 #[test]
15179 fn lua_methods_arguments_with_conditions() {
15180 // `m(not a, not b)` — argument list walker counts each
15181 // unary-conditional argument once. Bare-identifier args
15182 // (`m(a, b)`) do not count (list_kind != BinaryExpression).
15183 check_metrics::<LuaParser>(
15184 "function f(a, b) m(a, b); m(not a, not b) end",
15185 "foo.lua",
15186 |metric| {
15187 assert_eq!(metric.abc.branches_sum(), 2);
15188 assert_eq!(metric.abc.conditions_sum(), 2);
15189 insta::assert_json_snapshot!(metric.abc);
15190 },
15191 );
15192 }
15193
15194 #[cfg(feature = "lua")]
15195 #[test]
15196 fn lua_return_with_conditions() {
15197 // `return not (z >= 0)` → walker on `not` unwraps the paren
15198 // chain and reaches the inner BinaryExpression; the inner
15199 // `>=` comparison is the actual Fitzpatrick condition.
15200 check_metrics::<LuaParser>(
15201 "function m1(z) return not (z >= 0) end\n\
15202 function m2(x) return (((not x))) end\n\
15203 function m3(x, y) return x and y end",
15204 "foo.lua",
15205 |metric| {
15206 // m1: `>=` (1). `not` wraps a paren'd
15207 // BinaryExpression — `lua_count_negated` peels it
15208 // to the comparison, which scores itself, so the
15209 // return value pays nothing extra. +1.
15210 // m2: ReturnStatement → iterate expression_list →
15211 // `lua_count_negated` peels the parens and the
15212 // `not` (which proves boolean) down to `x`, which
15213 // does not score itself. +1.
15214 // m3: x and y → `and` walker counts both → +2.
15215 // Sum: 4.
15216 assert_eq!(metric.abc.conditions_sum(), 4);
15217 insta::assert_json_snapshot!(metric.abc);
15218 },
15219 );
15220 }
15221
15222 // ---------- Tcl ABC tests ----------
15223
15224 #[cfg(feature = "tcl")]
15225 #[test]
15226 fn tcl_empty_unit_zero() {
15227 check_metrics::<TclParser>("", "empty.tcl", |metric| {
15228 assert_eq!(metric.abc.assignments_sum(), 0);
15229 assert_eq!(metric.abc.branches_sum(), 0);
15230 assert_eq!(metric.abc.conditions_sum(), 0);
15231 insta::assert_json_snapshot!(metric.abc);
15232 });
15233 }
15234
15235 #[cfg(feature = "tcl")]
15236 #[test]
15237 fn tcl_set_command_counts_assignment() {
15238 // `set` has its own grammar production; each invocation is
15239 // one assignment.
15240 check_metrics::<TclParser>(
15241 "proc f {} {\n\
15242 set x 1\n\
15243 set y 2\n\
15244 set x [expr {$x + $y}]\n\
15245 }",
15246 "foo.tcl",
15247 |metric| {
15248 // 3 `set` invocations → A=3. The inner `expr` is a
15249 // sub-command (`command_substitution` + `expr_cmd`),
15250 // not a `command` node, so it doesn't add a branch.
15251 assert_eq!(metric.abc.assignments_sum(), 3);
15252 assert_eq!(metric.abc.branches_sum(), 0);
15253 assert_eq!(metric.abc.conditions_sum(), 0);
15254 insta::assert_json_snapshot!(metric.abc);
15255 },
15256 );
15257 }
15258
15259 #[cfg(feature = "tcl")]
15260 #[test]
15261 fn tcl_incr_append_lappend_count_assignment() {
15262 // Variable-mutation commands (`incr`, `append`, `lappend`)
15263 // are recognised by name and count as assignments, not
15264 // branches.
15265 check_metrics::<TclParser>(
15266 "proc f {} {\n\
15267 set x 0\n\
15268 incr x\n\
15269 append s \"hi\"\n\
15270 lappend lst 1\n\
15271 }",
15272 "foo.tcl",
15273 |metric| {
15274 // `set` (1) + `incr` (1) + `append` (1) + `lappend`
15275 // (1) → A=4. No branches, no conditions.
15276 assert_eq!(metric.abc.assignments_sum(), 4);
15277 assert_eq!(metric.abc.branches_sum(), 0);
15278 assert_eq!(metric.abc.conditions_sum(), 0);
15279 insta::assert_json_snapshot!(metric.abc);
15280 },
15281 );
15282 }
15283
15284 #[cfg(feature = "tcl")]
15285 #[test]
15286 fn tcl_qualified_mutator_commands_count_assignment() {
15287 // `::incr` is `incr` through the global namespace. Anchored on
15288 // `branches_sum() == 0` as well as the assignment total: an
15289 // unresolved name falls to the branch arm, so the two columns
15290 // move in opposite directions and no single total can mask the
15291 // regression (#1381 review).
15292 check_metrics::<TclParser>(
15293 "proc f {} {\n\
15294 ::incr x\n\
15295 ::append s \"hi\"\n\
15296 ::lappend lst 1\n\
15297 }",
15298 "foo.tcl",
15299 |metric| {
15300 assert_eq!(metric.abc.assignments_sum(), 3);
15301 assert_eq!(metric.abc.branches_sum(), 0);
15302 assert_eq!(metric.abc.conditions_sum(), 0);
15303 },
15304 );
15305 }
15306
15307 #[cfg(feature = "tcl")]
15308 #[test]
15309 fn tcl_namespaced_mutator_command_stays_a_branch() {
15310 // Control: only the *leading* qualifier names the core command,
15311 // so `ns::incr` is a user proc and keeps the branch column — the
15312 // direction `BRACED_WORD_VALUE_CASES` pins for `ns::eval`.
15313 check_metrics::<TclParser>(
15314 "proc f {} {\n\
15315 ns::incr x\n\
15316 }",
15317 "foo.tcl",
15318 |metric| {
15319 assert_eq!(metric.abc.assignments_sum(), 0);
15320 assert_eq!(metric.abc.branches_sum(), 1);
15321 },
15322 );
15323 }
15324
15325 #[cfg(feature = "tcl")]
15326 #[test]
15327 fn tcl_computed_command_name_is_not_an_assignment() {
15328 // A command whose leading word is computed (`$cmd args`) names no
15329 // builtin the parser can resolve, so it stays a branch. Pins the
15330 // field-addressed, `simple_word`-gated read the classifier shares
15331 // with the Cognitive / Cyclomatic detectors (grammar-dispatch §3):
15332 // the earlier `child(0)` byte-slice addressed the slot by position
15333 // and compared whatever literal text sat there, which agrees with
15334 // the gated read on today's grammar only because no mutator name is
15335 // spelled with a leading `$`. A grammar that moved the name out of
15336 // `child(0)`, or a mutator list that grew a computed-looking entry,
15337 // would diverge — this fixture is where that surfaces.
15338 check_metrics::<TclParser>(
15339 "proc f {cmd x} {\n\
15340 $cmd $x\n\
15341 incr x\n\
15342 }",
15343 "foo.tcl",
15344 |metric| {
15345 // `incr x` is the only assignment; `$cmd $x` is a branch.
15346 assert_eq!(metric.abc.assignments_sum(), 1);
15347 assert_eq!(metric.abc.branches_sum(), 1);
15348 assert_eq!(metric.abc.conditions_sum(), 0);
15349 },
15350 );
15351 }
15352
15353 #[cfg(feature = "tcl")]
15354 #[test]
15355 fn tcl_generic_commands_are_branches() {
15356 // Anything that isn't `set` or a known mutator command
15357 // counts as a branch — including builtins like `puts` and
15358 // `return`.
15359 check_metrics::<TclParser>(
15360 "proc f {} {\n\
15361 puts \"hello\"\n\
15362 do_work 1 2\n\
15363 return 0\n\
15364 }",
15365 "foo.tcl",
15366 |metric| {
15367 // 3 commands, all branches.
15368 assert_eq!(metric.abc.assignments_sum(), 0);
15369 assert_eq!(metric.abc.branches_sum(), 3);
15370 assert_eq!(metric.abc.conditions_sum(), 0);
15371 insta::assert_json_snapshot!(metric.abc);
15372 },
15373 );
15374 }
15375
15376 #[cfg(feature = "tcl")]
15377 #[test]
15378 fn tcl_comparisons_count_logical_ops_do_not() {
15379 // `expr` predicates expose comparison / logical tokens at
15380 // the leaf level. Each comparison token contributes one
15381 // condition; `&&` and `||` are NOT counted on their own
15382 // (Fitzpatrick Rule 5; #395) — instead each operand is
15383 // counted as a unary conditional by the walker (Rule 9;
15384 // #403). The two logical lines add 2 walker conditions
15385 // each (variable-substitution operands).
15386 check_metrics::<TclParser>(
15387 "proc f {a b} {\n\
15388 set r [expr {$a == $b}]\n\
15389 set r [expr {$a != $b}]\n\
15390 set r [expr {$a < $b}]\n\
15391 set r [expr {$a > $b}]\n\
15392 set r [expr {$a <= $b}]\n\
15393 set r [expr {$a >= $b}]\n\
15394 set r [expr {$a eq $b}]\n\
15395 set r [expr {$a ne $b}]\n\
15396 set r [expr {$a && $b}]\n\
15397 set r [expr {$a || $b}]\n\
15398 }",
15399 "foo.tcl",
15400 |metric| {
15401 // 10 `set` assignments.
15402 assert_eq!(metric.abc.assignments_sum(), 10);
15403 assert_eq!(metric.abc.branches_sum(), 0);
15404 // 8 comparisons (+8) + 2 logical lines × 2 walker
15405 // operands (+4) = 12.
15406 assert_eq!(metric.abc.conditions_sum(), 12);
15407 insta::assert_json_snapshot!(metric.abc);
15408 },
15409 );
15410 }
15411
15412 #[cfg(feature = "tcl")]
15413 #[test]
15414 fn tcl_ternary_counts_condition() {
15415 // The `ternary_expr` node is one condition and its condition
15416 // slot `$a` — a bare truthy test — is another, matching C++'s
15417 // `int r = a ? b : c;` (also 2). The two branch operands are
15418 // unnegated and so contribute nothing (#1180).
15419 check_metrics::<TclParser>(
15420 "proc f {a b c} {\n\
15421 set r [expr {$a ? $b : $c}]\n\
15422 }",
15423 "foo.tcl",
15424 |metric| {
15425 assert_eq!(metric.abc.assignments_sum(), 1);
15426 assert_eq!(metric.abc.branches_sum(), 0);
15427 assert_eq!(metric.abc.conditions_sum(), 2);
15428 insta::assert_json_snapshot!(metric.abc);
15429 },
15430 );
15431 }
15432
15433 /// The Tcl half of the ternary slot-location guard (#1180).
15434 ///
15435 /// `ternary_expr` exposes no grammar fields, so the slots are found
15436 /// relative to the `?` and `:` tokens. A *parenthesised* condition is
15437 /// the input that discriminates that from a fixed-index reading:
15438 /// `_expr` inlines `( … )` as anonymous children of `ternary_expr`,
15439 /// so `($a) ? !$b : !$c` shifts every operand right by one and
15440 /// `child(0)` / `child(2)` / `child(4)` land on `(`, `)` and `?`.
15441 /// Without this case the whole fixed-index revert passes.
15442 #[cfg(feature = "tcl")]
15443 #[test]
15444 fn tcl_parenthesised_ternary_condition_matches_the_bare_form() {
15445 let conditions = |source: &str| {
15446 crate::test_support::metrics_verbatim(
15447 crate::LANG::Tcl,
15448 source.as_bytes(),
15449 crate::MetricsOptions::default(),
15450 )
15451 .abc
15452 .conditions_sum()
15453 };
15454 let bare = conditions("proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}");
15455 assert_eq!(bare, 4, "the bare form is the documented reference value");
15456 assert_eq!(
15457 conditions("proc f {a b c} {\n set r [expr {($a) ? !$b : !$c}]\n}"),
15458 bare,
15459 "parenthesising the condition must not change the count"
15460 );
15461 }
15462
15463 /// A parenthesised operand under `!` counts the same as a bare one.
15464 ///
15465 /// `_expr` inlines `( … )` as anonymous children, so `!($a)` puts
15466 /// `(` where a positional read expects the operand. The walker's
15467 /// negation branch kept a fixed `child(1)` through the first draft of
15468 /// #1180 and scored these 0 while the unparenthesised forms scored 1
15469 /// — an inconsistency the fix itself introduced, since before it
15470 /// neither form counted. Found in review, not by the tests: the
15471 /// parenthesised fixtures added with #1180 covered the ternary
15472 /// *condition* slot only.
15473 #[cfg(feature = "tcl")]
15474 #[test]
15475 fn tcl_parenthesised_negated_operands_match_the_bare_form() {
15476 let conditions = |source: &str| {
15477 crate::test_support::metrics_verbatim(
15478 crate::LANG::Tcl,
15479 source.as_bytes(),
15480 crate::MetricsOptions::default(),
15481 )
15482 .abc
15483 .conditions_sum()
15484 };
15485 for (bare, parenthesised) in [
15486 (
15487 "proc f {a} {\n if {!$a} { puts x }\n}",
15488 "proc f {a} {\n if {!($a)} { puts x }\n}",
15489 ),
15490 (
15491 "proc f {a b} {\n if {$a && !$b} { puts x }\n}",
15492 "proc f {a b} {\n if {$a && !($b)} { puts x }\n}",
15493 ),
15494 (
15495 "proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}",
15496 "proc f {a b c} {\n set r [expr {$a ? !($b) : !$c}]\n}",
15497 ),
15498 ] {
15499 let want = conditions(bare);
15500 assert!(want > 0, "the bare form must count something: {bare}");
15501 assert_eq!(
15502 conditions(parenthesised),
15503 want,
15504 "parenthesising the negated operand changed the count\n bare: {bare}\n paren: {parenthesised}"
15505 );
15506 }
15507 }
15508
15509 #[cfg(feature = "irules")]
15510 #[test]
15511 fn irules_abc_parenthesised_negated_operands_match_the_bare_form() {
15512 let conditions = |source: &str| {
15513 crate::test_support::metrics_verbatim(
15514 crate::LANG::Irules,
15515 source.as_bytes(),
15516 crate::MetricsOptions::default(),
15517 )
15518 .abc
15519 .conditions_sum()
15520 };
15521 let want = conditions("when X {\n if { !$a } { log local0. hi }\n}\n");
15522 assert_eq!(want, 1);
15523 assert_eq!(
15524 conditions("when X {\n if { !($a) } { log local0. hi }\n}\n"),
15525 want
15526 );
15527 }
15528
15529 #[cfg(feature = "tcl")]
15530 #[test]
15531 fn tcl_bare_truthy_and_negated_predicates_count_one_condition() {
15532 // The headline #1180 fix, on the Tcl side: both were 0 before.
15533 let conditions = |source: &str| {
15534 crate::test_support::metrics_verbatim(
15535 crate::LANG::Tcl,
15536 source.as_bytes(),
15537 crate::MetricsOptions::default(),
15538 )
15539 .abc
15540 .conditions_sum()
15541 };
15542 assert_eq!(conditions("proc f {a} {\n if {$a} { puts x }\n}"), 1);
15543 assert_eq!(conditions("proc f {a} {\n if {!$a} { puts x }\n}"), 1);
15544 assert_eq!(conditions("proc f {a} {\n while {$a} { puts x }\n}"), 1);
15545 assert_eq!(conditions("proc f {a} {\n while {!$a} { puts x }\n}"), 1);
15546 }
15547
15548 #[cfg(feature = "tcl")]
15549 #[test]
15550 fn tcl_bare_truthy_elseif_predicate_counts_one_condition() {
15551 // The `Tcl::Elseif` arm routes its predicate through
15552 // `tcl_condition_expr` exactly as `If` / `While` do (#1180), but
15553 // a comparison predicate is counted by its leaf operator, so it
15554 // cannot tell the routing from its absence. Only a bare truthy
15555 // predicate can: `$b` scores through the routed walker or not at
15556 // all.
15557 let conditions = |source: &str| {
15558 crate::test_support::metrics_verbatim(
15559 crate::LANG::Tcl,
15560 source.as_bytes(),
15561 crate::MetricsOptions::default(),
15562 )
15563 .abc
15564 .conditions_sum()
15565 };
15566 // `$a` truthy (1) + `elseif` clause (1) + `$b` truthy (1).
15567 assert_eq!(
15568 conditions("proc f {a b} {\n if {$a} { puts x } elseif {$b} { puts y }\n}"),
15569 3
15570 );
15571 }
15572
15573 #[cfg(feature = "tcl")]
15574 #[test]
15575 fn tcl_elseif_and_else_count_conditions() {
15576 // `if` / `elseif` / `else` clause productions each
15577 // contribute one condition. The leaf comparison inside the
15578 // predicate is counted independently.
15579 check_metrics::<TclParser>(
15580 "proc f {x} {\n\
15581 if {$x > 0} {\n\
15582 return 1\n\
15583 } elseif {$x < 0} {\n\
15584 return -1\n\
15585 } else {\n\
15586 return 0\n\
15587 }\n\
15588 }",
15589 "foo.tcl",
15590 |metric| {
15591 // Branches: three `return` commands → 3.
15592 // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
15593 // (1) → 4.
15594 assert_eq!(metric.abc.assignments_sum(), 0);
15595 assert_eq!(metric.abc.branches_sum(), 3);
15596 assert_eq!(metric.abc.conditions_sum(), 4);
15597 insta::assert_json_snapshot!(metric.abc);
15598 },
15599 );
15600 }
15601
15602 #[cfg(feature = "tcl")]
15603 #[test]
15604 fn tcl_if_multiple_conditions() {
15605 // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot
15606 // exposes `&&` / `||` operands as variable substitutions
15607 // (`$a`, `$b`, …) inside a `binop_expr`.
15608 check_metrics::<TclParser>(
15609 "proc f {a b c d} {\n\
15610 if {[expr {$a || $b || $c || $d}]} { return 1 } \n\
15611 if {[expr {$a && $b && $c}]} { return 2 } \n\
15612 return 0\n\
15613 }",
15614 "foo.tcl",
15615 |metric| {
15616 // The two chains feed the walker: 4 + 3 = 7. Each `if`
15617 // predicate is additionally a bare truthy test of a
15618 // command substitution — `{[expr {…}]}` is structurally
15619 // `if {[somecmd]}`, which counts 1 exactly as `if {$a}`
15620 // does — so 7 + 2 = 9 (#1180). Written without the
15621 // redundant `[expr …]` wrapper, `if {$a || $b}` scores
15622 // 2, matching C++'s `if (a || b)`.
15623 assert_eq!(metric.abc.conditions_sum(), 9);
15624 insta::assert_json_snapshot!(metric.abc);
15625 },
15626 );
15627 }
15628
15629 #[cfg(feature = "tcl")]
15630 #[test]
15631 fn tcl_while_conditions() {
15632 // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is
15633 // the standard loop. The walker fires on `&&` / `||` tokens
15634 // inside the `expr` predicate.
15635 check_metrics::<TclParser>(
15636 "proc f {a b} {\n\
15637 while {[expr {$a || $b}]} { break } \n\
15638 while {[expr {$a && $b}]} { break } \n\
15639 }",
15640 "foo.tcl",
15641 |metric| {
15642 // 2 + 2 from the chains, plus one bare truthy test per
15643 // `while` predicate — see `tcl_if_multiple_conditions`
15644 // (#1180).
15645 assert_eq!(metric.abc.conditions_sum(), 6);
15646 insta::assert_json_snapshot!(metric.abc);
15647 },
15648 );
15649 }
15650
15651 #[cfg(feature = "tcl")]
15652 #[test]
15653 fn tcl_short_circuit_with_boolean_literal_operand() {
15654 // `$a && 1` reports 2 conditions: a VariableSubstitution
15655 // operand plus a Number-literal operand. Confirms `Number`
15656 // is in the walker terminal set. `true` / `false` Tcl
15657 // keywords are not literal tokens in tree-sitter-tcl —
15658 // they're emitted as the operator-context word, which is
15659 // captured separately by the `Tcl::Boolean` kind for
15660 // dedicated `expr {true}` predicates but not as a `&&`
15661 // operand at this iteration; using a numeric literal keeps
15662 // the assertion grammar-stable.
15663 check_metrics::<TclParser>(
15664 "proc f {a} { return [expr {$a && 1}] }\n",
15665 "foo.tcl",
15666 |metric| {
15667 assert_eq!(metric.abc.conditions_sum(), 2);
15668 insta::assert_json_snapshot!(metric.abc);
15669 },
15670 );
15671 }
15672
15673 #[cfg(feature = "tcl")]
15674 #[test]
15675 fn tcl_complex_function_abc() {
15676 // Mixed program covering every category. Tcl's grammar
15677 // re-parses braced content that looks command-shaped as a
15678 // nested `command` node, which inflates the branch count
15679 // relative to a naive read of the source — see breakdown.
15680 check_metrics::<TclParser>(
15681 "proc run {n} {\n\
15682 set total 0\n\
15683 for {set i 0} {$i < $n} {incr i} {\n\
15684 if {$i % 2 == 0} {\n\
15685 do_work $i\n\
15686 } else {\n\
15687 incr total $i\n\
15688 }\n\
15689 }\n\
15690 puts \"done\"\n\
15691 return $total\n\
15692 }",
15693 "foo.tcl",
15694 |metric| {
15695 // Assignments: `set total 0` (1), `set i 0` (1),
15696 // `incr i` (1), `incr total $i` (1) → A = 4.
15697 // Branches: the outer `for …` is one `command` node;
15698 // the `{$i < $n}` predicate ALSO re-parses as a
15699 // `command` node (tree-sitter-tcl treats braced
15700 // predicates as nested commands at the pinned
15701 // grammar version); plus `do_work $i`, `puts
15702 // "done"`, and `return $total`. The for-loop body's
15703 // `incr` and `incr total $i` are assignment commands
15704 // and don't add branches. Total B = 5.
15705 // Conditions: `==` (1) and `else` (1) → C = 2. The
15706 // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
15707 // that predicate re-parses as a `command`, the `<`
15708 // is emitted as `simple_word`. Only `<` inside a
15709 // real `expr` production becomes `Tcl::LT`.
15710 assert_eq!(metric.abc.assignments_sum(), 4);
15711 assert_eq!(metric.abc.branches_sum(), 5);
15712 assert_eq!(metric.abc.conditions_sum(), 2);
15713 insta::assert_json_snapshot!(metric.abc);
15714 },
15715 );
15716 }
15717
15718 /// The dedicated `set name value` production counts as one assignment.
15719 #[cfg(feature = "irules")]
15720 #[test]
15721 fn irules_abc_set_assignment() {
15722 check_metrics::<IrulesParser>("when X {\n set x 1\n}\n", "foo.irule", |metric| {
15723 assert_eq!(metric.abc.assignments_sum(), 1);
15724 assert_eq!(metric.abc.branches_sum(), 0);
15725 assert_eq!(metric.abc.conditions_sum(), 0);
15726 });
15727 }
15728
15729 /// Mutator commands (`incr` / `append` / `lappend`) count as
15730 /// assignments, not branches — iRules has no assignment operators, so
15731 /// mutation is always a command invocation.
15732 #[cfg(feature = "irules")]
15733 #[test]
15734 fn irules_abc_mutator_commands() {
15735 check_metrics::<IrulesParser>(
15736 "when X {\n incr x\n append s \"y\"\n lappend l 1\n}\n",
15737 "foo.irule",
15738 |metric| {
15739 assert_eq!(metric.abc.assignments_sum(), 3);
15740 assert_eq!(metric.abc.branches_sum(), 0);
15741 },
15742 );
15743 }
15744
15745 /// `::incr` is `incr` through the global namespace. iRules resolves
15746 /// the leading word in `irules_command_is_assignment` rather than
15747 /// through `tcl_command_name`, so this pins the strip on that second
15748 /// path (#1381 review).
15749 #[cfg(feature = "irules")]
15750 #[test]
15751 fn irules_abc_qualified_mutator_commands() {
15752 check_metrics::<IrulesParser>(
15753 "when X {\n ::incr x\n ::append s \"y\"\n ::lappend l 1\n}\n",
15754 "foo.irule",
15755 |metric| {
15756 assert_eq!(metric.abc.assignments_sum(), 3);
15757 assert_eq!(metric.abc.branches_sum(), 0);
15758 },
15759 );
15760 }
15761
15762 /// Control: `ns::incr` is a proc in `ns`, so it stays a branch.
15763 #[cfg(feature = "irules")]
15764 #[test]
15765 fn irules_abc_namespaced_mutator_command_stays_a_branch() {
15766 check_metrics::<IrulesParser>("when X {\n ns::incr x\n}\n", "foo.irule", |metric| {
15767 assert_eq!(metric.abc.assignments_sum(), 0);
15768 assert_eq!(metric.abc.branches_sum(), 1);
15769 });
15770 }
15771
15772 /// Generic (non-mutator) commands count as branches.
15773 #[cfg(feature = "irules")]
15774 #[test]
15775 fn irules_abc_branch_commands() {
15776 check_metrics::<IrulesParser>(
15777 "when X {\n log local0. hi\n pool p1\n}\n",
15778 "foo.irule",
15779 |metric| {
15780 assert_eq!(metric.abc.assignments_sum(), 0);
15781 assert_eq!(metric.abc.branches_sum(), 2);
15782 assert_eq!(metric.abc.conditions_sum(), 0);
15783 },
15784 );
15785 }
15786
15787 /// A numeric comparison (`==`) is one condition; the `log` inside the
15788 /// `if` body is one branch.
15789 #[cfg(feature = "irules")]
15790 #[test]
15791 fn irules_abc_comparison_condition() {
15792 check_metrics::<IrulesParser>(
15793 "when X {\n if { $a == 1 } { log local0. hi }\n}\n",
15794 "foo.irule",
15795 |metric| {
15796 assert_eq!(metric.abc.branches_sum(), 1);
15797 assert_eq!(metric.abc.conditions_sum(), 1);
15798 },
15799 );
15800 }
15801
15802 /// A word-form string comparator (`contains`) is a condition just like
15803 /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If
15804 /// `contains` were dropped from the condition set this would report 0.
15805 #[cfg(feature = "irules")]
15806 #[test]
15807 fn irules_abc_string_op_condition() {
15808 check_metrics::<IrulesParser>(
15809 "when X {\n if { $a contains \"x\" } { log local0. hi }\n}\n",
15810 "foo.irule",
15811 |metric| {
15812 assert_eq!(metric.abc.branches_sum(), 1);
15813 assert_eq!(metric.abc.conditions_sum(), 1);
15814 },
15815 );
15816 }
15817
15818 /// Each `elseif` / `else` clause is one condition; the three `set`s are
15819 /// assignments. The leading `if` is not itself a condition.
15820 #[cfg(feature = "irules")]
15821 #[test]
15822 fn irules_abc_elseif_else_conditions() {
15823 check_metrics::<IrulesParser>(
15824 "when X {\n if { $a } { set r 1 } elseif { $b } { set r 2 } else { set r 3 }\n}\n",
15825 "foo.irule",
15826 |metric| {
15827 assert_eq!(metric.abc.assignments_sum(), 3);
15828 // The `elseif` and `else` clauses are one condition each,
15829 // as before; #1180 adds the two bare truthy predicates
15830 // (`{ $a }`, `{ $b }`). C++'s
15831 // `if(a){} else if(b){} else {}` also scores 4.
15832 assert_eq!(metric.abc.conditions_sum(), 4);
15833 },
15834 );
15835 }
15836
15837 /// A ternary contributes its own condition plus the `>` comparison in
15838 /// its test: conditions 2; the `set` is one assignment.
15839 #[cfg(feature = "irules")]
15840 #[test]
15841 fn irules_abc_ternary_condition() {
15842 check_metrics::<IrulesParser>(
15843 "when X {\n set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
15844 "foo.irule",
15845 |metric| {
15846 assert_eq!(metric.abc.assignments_sum(), 1);
15847 assert_eq!(metric.abc.conditions_sum(), 2);
15848 },
15849 );
15850 }
15851
15852 /// Fitzpatrick Rule 9: the short-circuit `&&` is not itself a condition,
15853 /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards
15854 /// the `irules_count_condition` chain-operand walk — conditions 2.
15855 #[cfg(feature = "irules")]
15856 #[test]
15857 fn irules_abc_negated_operands_in_chain() {
15858 check_metrics::<IrulesParser>(
15859 "when X {\n if { !$a && !$b } { log local0. hi }\n}\n",
15860 "foo.irule",
15861 |metric| {
15862 assert_eq!(metric.abc.branches_sum(), 1);
15863 assert_eq!(metric.abc.conditions_sum(), 2);
15864 },
15865 );
15866 }
15867
15868 /// A bare-truthy `if {$a}` predicate is one condition (#1180).
15869 ///
15870 /// It carries no comparison, ternary or short-circuit operator, so
15871 /// before the Phase 2B slot routing landed nothing invoked the
15872 /// unary-conditional walker and the whole predicate scored 0 — this
15873 /// test previously pinned that absence, and the metrics book's ABC
15874 /// deviation table said so. Now the `if` node routes its `expr`
15875 /// predicate and the count matches C++'s `if (a)`, which is also 1.
15876 /// The `log` command remains the single branch.
15877 #[cfg(feature = "irules")]
15878 #[test]
15879 fn irules_abc_bare_truthy_counts_one_condition() {
15880 check_metrics::<IrulesParser>(
15881 "when X {\n if { $a } { log local0. hi }\n}\n",
15882 "foo.irule",
15883 |metric| {
15884 assert_eq!(metric.abc.branches_sum(), 1);
15885 assert_eq!(metric.abc.conditions_sum(), 1);
15886 },
15887 );
15888 }
15889
15890 /// The negated form of the same predicate, which was *also* 0 before
15891 /// #1180: `!$a` reached the walker but no parent seeded boolean
15892 /// context, so the terminal operand was never counted. Distinct from
15893 /// `irules_abc_negated_operands_in_chain`, whose `&&` supplied the
15894 /// seed the bare form lacked.
15895 #[cfg(feature = "irules")]
15896 #[test]
15897 fn irules_abc_negated_bare_truthy_counts_one_condition() {
15898 check_metrics::<IrulesParser>(
15899 "when X {\n if { !$a } { log local0. hi }\n}\n",
15900 "foo.irule",
15901 |metric| {
15902 assert_eq!(metric.abc.branches_sum(), 1);
15903 assert_eq!(metric.abc.conditions_sum(), 1);
15904 },
15905 );
15906 }
15907
15908 /// The ternary's three operand slots, located relative to the `?`
15909 /// and `:` tokens because the grammar exposes no fields (#1180).
15910 ///
15911 /// `$a ? !$b : !$c` is four: the `ternary_expr` node, the bare
15912 /// truthy condition, and one per negated branch — the same value
15913 /// Java, C#, Groovy, the C family, the JS family, PHP, Perl, Ruby
15914 /// and Python report for the identical expression.
15915 #[cfg(feature = "irules")]
15916 #[test]
15917 fn irules_abc_ternary_routes_its_operand_slots() {
15918 check_metrics::<IrulesParser>(
15919 "when X {\n set y [expr { $a ? !$b : !$c }]\n}\n",
15920 "foo.irule",
15921 |metric| {
15922 assert_eq!(metric.abc.conditions_sum(), 4);
15923 },
15924 );
15925 }
15926
15927 /// The #1161 control: a ternary whose condition is a *comparison*
15928 /// must not move. The `>` already supplied its condition and the
15929 /// branches are unnegated, so routing the slots adds nothing.
15930 #[cfg(feature = "irules")]
15931 #[test]
15932 fn irules_abc_comparison_ternary_is_unchanged_by_slot_routing() {
15933 check_metrics::<IrulesParser>(
15934 "when X {\n set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
15935 "foo.irule",
15936 |metric| {
15937 assert_eq!(metric.abc.conditions_sum(), 2);
15938 },
15939 );
15940 }
15941
15942 /// A parenthesised ternary condition scores the same as a bare one.
15943 ///
15944 /// The grammar inlines `( … )` as anonymous children of
15945 /// `ternary_expr` rather than wrapping them in a node, so a
15946 /// fixed-index reading of the slots would shift right by one and
15947 /// mis-assign every operand. This is the input that discriminates
15948 /// the token-relative location the fix uses.
15949 #[cfg(feature = "irules")]
15950 #[test]
15951 fn irules_abc_parenthesised_ternary_condition_matches_the_bare_form() {
15952 // `check_metrics` takes a bare `fn`, so it cannot carry the
15953 // first measurement into the second comparison; `metrics_verbatim`
15954 // returns a value instead.
15955 let conditions = |source: &str| {
15956 crate::test_support::metrics_verbatim(
15957 crate::LANG::Irules,
15958 source.as_bytes(),
15959 crate::MetricsOptions::default(),
15960 )
15961 .abc
15962 .conditions_sum()
15963 };
15964 let bare = conditions("when X {\n set y [expr { $a ? !$b : !$c }]\n}\n");
15965 assert_eq!(bare, 4, "the bare form is the documented reference value");
15966 assert_eq!(
15967 conditions("when X {\n set y [expr { ($a) ? !$b : !$c }]\n}\n"),
15968 bare,
15969 "parenthesising the condition must not change the count"
15970 );
15971 }
15972
15973 // #1454's five-language sweep of #1422's C# rule: a pattern-match
15974 // guard is a condition *slot*, so every spelling of one contributes
15975 // exactly one condition, and the guard is a cyclomatic decision the
15976 // arm it guards does not already pay for.
15977 //
15978 // Each fixture is the §11 triple — an operator guard (`> 5`, whose
15979 // comparison token already counted before the fix), a call guard,
15980 // and a bare-identifier guard (the two that counted nothing) —
15981 // plus a parenthesised guard, which is the only member that
15982 // exercises the boolean-context seed, plus an unguarded control
15983 // that must stay where it was. A single-spelling fixture cannot
15984 // show the slot works: before the fix the operator member already
15985 // read the right number.
15986 //
15987 // The controls are load-bearing twice over. `none` pins that the
15988 // slot fires on the guard and not on the arm — a `+1` on every arm
15989 // would satisfy every guarded row and break this one — and the
15990 // helper (`is_even` / `isEven`) pins that the call-guard row is
15991 // scoring its guard rather than its call, since a call is an ABC
15992 // *branch* everywhere and a condition nowhere.
15993 //
15994 // `assert_members_score` rather than `assert_every_member_scores`:
15995 // the guarded members and their control sit at different values,
15996 // which is the comparison these tests exist to make.
15997
15998 #[cfg(feature = "rust")]
15999 #[test]
16000 fn rust_match_guard_scores_one_condition_however_spelled() {
16001 let src = "fn is_even(n: i32) -> bool {
16002 n == 0
16003 }
16004 fn tok(x: i32) -> i32 {
16005 match x { n if n > 5 => 1, _ => 0 }
16006 }
16007 fn call(x: i32) -> i32 {
16008 match x { n if is_even(n) => 1, _ => 0 }
16009 }
16010 fn bare(x: i32, b: bool) -> i32 {
16011 match x { _ if b => 1, _ => 0 }
16012 }
16013 fn paren(x: i32, b: bool) -> i32 {
16014 match x { n if (b) => n, _ => 0 }
16015 }
16016 fn none(x: i32) -> i32 {
16017 match x { 1 => 1, _ => 0 }
16018 }";
16019 assert_fixture_spells::<RustParser>(
16020 src,
16021 "foo.rs",
16022 &[
16023 // The guard's own `if` keyword, one per guarded member.
16024 // It is also how Rust cyclomatic already saw the guard,
16025 // which is why only the ABC half moved here.
16026 (Rust::If as u16, 4, "match guards"),
16027 (Rust::GT as u16, 1, "`tok`'s `>`"),
16028 (Rust::CallExpression as u16, 1, "the `is_even` call"),
16029 (
16030 Rust::ParenthesizedExpression as u16,
16031 1,
16032 "`paren`'s parenthesised guard",
16033 ),
16034 // Two arms per member: the guarded one and its bare `_`.
16035 (Rust::MatchPattern as u16, 10, "match arms"),
16036 ],
16037 );
16038 check_func_space::<RustParser, _>(src, "foo.rs", |space| {
16039 assert_members_score(
16040 &space,
16041 &[
16042 ("is_even", 1, 1),
16043 ("tok", 2, 3),
16044 // Was 1 — the guard spelling nothing counted.
16045 ("call", 2, 3),
16046 ("bare", 2, 3),
16047 ("paren", 2, 3),
16048 ("none", 1, 2),
16049 ],
16050 );
16051 });
16052 }
16053
16054 #[cfg(feature = "java")]
16055 #[test]
16056 fn java_pattern_switch_guard_scores_one_condition_however_spelled() {
16057 let src = "class T {
16058 static boolean isEven(int i) { return i == 0; }
16059 int tok(Object o) { return switch (o) { case Integer i when i > 5 -> 1; default -> 0; }; }
16060 int call(Object o) { return switch (o) { case Integer i when isEven(i) -> 1; default -> 0; }; }
16061 int bare(Object o, boolean b) { return switch (o) { case Integer i when b -> 1; default -> 0; }; }
16062 int paren(Object o, boolean b) { return switch (o) { case Integer i when (b) -> 1; default -> 0; }; }
16063 int none(Object o) { return switch (o) { case Integer i -> 1; default -> 0; }; }
16064 }";
16065 assert_fixture_spells::<JavaParser>(
16066 src,
16067 "foo.java",
16068 &[
16069 (Java::Guard as u16, 4, "`when` guards"),
16070 (Java::GT as u16, 1, "`tok`'s `>`"),
16071 (Java::MethodInvocation as u16, 1, "the `isEven` call"),
16072 // Five `switch (o)` subjects plus `paren`'s `when (b)`.
16073 // Java wraps a switch subject in the same kind, so this
16074 // count moves if either is edited out.
16075 (
16076 Java::ParenthesizedExpression as u16,
16077 6,
16078 "the switch subjects and `paren`'s guard",
16079 ),
16080 ],
16081 );
16082 check_func_space::<JavaParser, _>(src, "foo.java", |space| {
16083 assert_members_score(
16084 &space.spaces[0],
16085 &[
16086 ("isEven", 1, 1),
16087 // Was 2 conditions against a *flat* cyclomatic of 2:
16088 // Java had neither half of the rule, so the guard
16089 // was invisible to cyclomatic and visible to ABC
16090 // only through whatever operator it spelled.
16091 ("tok", 2, 3),
16092 ("call", 2, 3),
16093 ("bare", 2, 3),
16094 ("paren", 2, 3),
16095 ("none", 1, 2),
16096 ],
16097 );
16098 });
16099 }
16100
16101 #[cfg(feature = "python")]
16102 #[test]
16103 fn python_case_guard_scores_one_condition_however_spelled() {
16104 let src = "def is_even(n):
16105 return n == 0
16106def tok(x):
16107 match x:
16108 case n if n > 5:
16109 return 1
16110 case _:
16111 return 0
16112def call(x):
16113 match x:
16114 case n if is_even(x):
16115 return 1
16116 case _:
16117 return 0
16118def bare(x, b):
16119 match x:
16120 case _ if b:
16121 return 1
16122 case _:
16123 return 0
16124def paren(x, b):
16125 match x:
16126 case n if (b):
16127 return n
16128 case _:
16129 return 0
16130def none(x):
16131 match x:
16132 case 1:
16133 return 1
16134 case _:
16135 return 0
16136";
16137 assert_fixture_spells::<PythonParser>(
16138 src,
16139 "foo.py",
16140 &[
16141 // The `case` guard clause. A comprehension filter is the
16142 // same kind in a different role, and the `guard` field
16143 // is what keeps it out of the slot — there is none in
16144 // this fixture, and `python_comprehension_if_clause_is_not_a_case_guard`
16145 // is where that separation is pinned.
16146 (Python::IfClause as u16, 4, "`case` guards"),
16147 (
16148 Python::ComparisonOperator as u16,
16149 2,
16150 "`tok`'s `>` and `is_even`'s `==`",
16151 ),
16152 (Python::Call as u16, 1, "the `is_even` call"),
16153 (
16154 Python::ParenthesizedExpression as u16,
16155 1,
16156 "`paren`'s parenthesised guard",
16157 ),
16158 ],
16159 );
16160 check_func_space::<PythonParser, _>(src, "foo.py", |space| {
16161 assert_members_score(
16162 &space,
16163 &[
16164 ("is_even", 1, 1),
16165 ("tok", 2, 3),
16166 ("call", 2, 3),
16167 ("bare", 2, 3),
16168 ("paren", 2, 3),
16169 ("none", 1, 2),
16170 ],
16171 );
16172 });
16173 }
16174
16175 // A comprehension's `if` filter is an `if_clause` too, and it is not
16176 // a `case` guard: the slot reads `case_clause`'s `guard` field, so
16177 // the two cannot be confused by construction. Python cyclomatic does
16178 // count the filter (through the same `If` keyword token it counts a
16179 // guard by), and ABC does not — a pre-existing divergence this
16180 // change deliberately leaves where it found it. The test is here so
16181 // that a later `IfClause` arm added without the field read fails
16182 // loudly rather than moving comprehensions silently.
16183 #[cfg(feature = "python")]
16184 #[test]
16185 fn python_comprehension_if_clause_is_not_a_case_guard() {
16186 let src = "def m(xs):
16187 return [x for x in xs if x]
16188";
16189 assert_fixture_spells::<PythonParser>(
16190 src,
16191 "foo.py",
16192 &[(Python::IfClause as u16, 1, "the comprehension filter")],
16193 );
16194 check_func_space::<PythonParser, _>(src, "foo.py", |space| {
16195 assert_members_score(&space, &[("m", 0, 3)]);
16196 });
16197 }
16198
16199 #[cfg(feature = "ruby")]
16200 #[test]
16201 fn ruby_in_clause_guard_scores_one_condition_however_spelled() {
16202 let src = "def is_even(x)
16203 x == 0
16204end
16205def tok(x)
16206 case x
16207 in [n] if n > 5 then 1
16208 in _ then 0
16209 end
16210end
16211def call(x)
16212 case x
16213 in [n] if n.even? then 1
16214 in _ then 0
16215 end
16216end
16217def bare(x, b)
16218 case x
16219 in [n] if b then 1
16220 in _ then 0
16221 end
16222end
16223def unguard(x, b)
16224 case x
16225 in [n] unless b then 1
16226 in _ then 0
16227 end
16228end
16229def paren(x, b)
16230 case x
16231 in [n] if (b) then 1
16232 in _ then 0
16233 end
16234end
16235def none(x)
16236 case x
16237 in [n] then 1
16238 in _ then 0
16239 end
16240end
16241";
16242 assert_fixture_spells::<RubyParser>(
16243 src,
16244 "foo.rb",
16245 &[
16246 (Ruby::IfGuard as u16, 4, "`if` guards"),
16247 (Ruby::UnlessGuard as u16, 1, "the `unless` guard"),
16248 (Ruby::GT as u16, 1, "`tok`'s `>`"),
16249 (
16250 Ruby::ParenthesizedStatements as u16,
16251 1,
16252 "`paren`'s parenthesised guard",
16253 ),
16254 ],
16255 );
16256 // `Ruby::Guard` (210) is the hidden `_guard` supertype: the two
16257 // dispatchers list it beside the concrete kinds so a grammar
16258 // that starts emitting it keeps working, and this pins that it
16259 // does not emit it today — otherwise the defensive arm is
16260 // indistinguishable from a dead one
16261 // (`.claude/rules/grammar-dispatch.md` §2).
16262 let parser = RubyParser::new(
16263 src.as_bytes().to_vec(),
16264 &std::path::PathBuf::from("foo.rb"),
16265 None,
16266 );
16267 assert!(
16268 !ast_has_kind_id(&parser, Ruby::Guard as u16),
16269 "`_guard` stopped being hidden — the defensive arms now fire"
16270 );
16271 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
16272 assert_members_score(
16273 &space,
16274 &[
16275 ("is_even", 1, 1),
16276 // Was 2 / 2: Ruby had neither half either, and the
16277 // arm's own condition supplied the 2 that made the
16278 // operator spelling look correct.
16279 ("tok", 2, 3),
16280 ("call", 2, 3),
16281 ("bare", 2, 3),
16282 ("unguard", 2, 3),
16283 ("paren", 2, 3),
16284 ("none", 1, 2),
16285 ],
16286 );
16287 });
16288 }
16289
16290 // Elixir is the inverse of the four above: ABC counted the `when`
16291 // token from the start and cyclomatic had no arm at all, so a guard
16292 // read as a condition with no decision behind it. #1454 adds the
16293 // decision, which is why `tok` / `call` / `bare` move on the
16294 // cyclomatic axis here and on the ABC axis everywhere else.
16295 //
16296 // Elixir was also the one language of the six that #1454 left
16297 // *without* the slot. Its `when` arm added a flat one on top of
16298 // whatever the guard's sub-structure already scored, so `tok` came
16299 // back 3 against `call` and `bare`'s 2 — the spelling-dependence the
16300 // batch exists to remove, and a `.claude/rules/grammar-dispatch.md`
16301 // §5 double count with the `>` token arm. A whole-branch review of
16302 // the batch found it; all three rows now read 2, level with the
16303 // sibling fixtures above and with their own decision counts.
16304 #[cfg(feature = "elixir")]
16305 #[test]
16306 fn elixir_guard_is_a_decision_however_spelled() {
16307 let src = "defmodule T do
16308 def is_even(x) do
16309 x == 0
16310 end
16311 def tok(x) do
16312 case x do
16313 n when n > 5 -> 1
16314 _ -> 0
16315 end
16316 end
16317 def call(x) do
16318 case x do
16319 n when is_integer(n) -> 1
16320 _ -> 0
16321 end
16322 end
16323 def bare(x, b) do
16324 case x do
16325 _n when b -> 1
16326 _ -> 0
16327 end
16328 end
16329 def none(x) do
16330 case x do
16331 1 -> 1
16332 _ -> 0
16333 end
16334 end
16335end
16336";
16337 assert_fixture_spells::<ElixirParser>(
16338 src,
16339 "foo.ex",
16340 &[
16341 (Elixir::When as u16, 3, "`when` guards"),
16342 (Elixir::GT as u16, 1, "`tok`'s `>`"),
16343 ],
16344 );
16345 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16346 assert_members_score(
16347 &space.spaces[0],
16348 &[
16349 ("is_even", 1, 1),
16350 // All three guarded members were cyclomatic 2 —
16351 // level with `none` — before the decision arm, and
16352 // `tok` was abc 3 before the slot.
16353 ("tok", 2, 3),
16354 ("call", 2, 3),
16355 ("bare", 2, 3),
16356 ("none", 1, 2),
16357 ],
16358 );
16359 });
16360 }
16361
16362 // A statement modifier's `condition` is the same boolean slot as the
16363 // block form's, so `g if (b)` must score what `if (b) then g end`
16364 // does. The modifier kinds were missing from the peel's seed, so a
16365 // parenthesised operand unwrapped into a context only a negation
16366 // could mark boolean and scored 0 (#1521). Every member here is one
16367 // decision, so each reads `conditions == cyclomatic - 1`; `bare` and
16368 // `block` are the analogues the modifier rows must match.
16369 #[cfg(feature = "ruby")]
16370 #[test]
16371 fn ruby_parenthesised_modifier_condition_scores_one() {
16372 let src = "def bare(b)
16373 g if b
16374end
16375def block(b)
16376 if (b) then g end
16377end
16378def if1(b)
16379 g if (b)
16380end
16381def if2(b)
16382 g if ((b))
16383end
16384def unless1(b)
16385 g unless (b)
16386end
16387def unless2(b)
16388 g unless ((b))
16389end
16390def while1(b)
16391 g while (b)
16392end
16393def while2(b)
16394 g while ((b))
16395end
16396def until1(b)
16397 g until (b)
16398end
16399def until2(b)
16400 g until ((b))
16401end
16402";
16403 assert_fixture_spells::<RubyParser>(
16404 src,
16405 "foo.rb",
16406 &[
16407 (Ruby::IfModifier as u16, 3, "`if` modifiers"),
16408 (Ruby::UnlessModifier as u16, 2, "`unless` modifiers"),
16409 (Ruby::WhileModifier as u16, 2, "`while` modifiers"),
16410 (Ruby::UntilModifier as u16, 2, "`until` modifiers"),
16411 (
16412 Ruby::ParenthesizedStatements as u16,
16413 13,
16414 "one layer per `(b)`, two per `((b))`",
16415 ),
16416 ],
16417 );
16418 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
16419 assert_members_score(
16420 &space,
16421 &[
16422 ("bare", 1, 2),
16423 ("block", 1, 2),
16424 ("if1", 1, 2),
16425 ("if2", 1, 2),
16426 ("unless1", 1, 2),
16427 ("unless2", 1, 2),
16428 ("while1", 1, 2),
16429 ("while2", 1, 2),
16430 ("until1", 1, 2),
16431 ("until2", 1, 2),
16432 ],
16433 );
16434 });
16435 }
16436
16437 // The slot's whole claim, on every spelling the Elixir grammar gives
16438 // a guard — and on `assert_every_member_scores`, which asserts §8
16439 // parity where the fixture above cannot (its unguarded control and
16440 // its bare-comparison member legitimately sit off their decision
16441 // counts, which is why that one takes a per-member table).
16442 //
16443 // Nine members, nine routes to the same 2:
16444 //
16445 // * `tok` / `eq` — the guard is an operator application, so the
16446 // slot adds nothing and the `>` / `==` token arm supplies the
16447 // one. These were 3 before the slot.
16448 // * `call` / `bare` — no operator counts a `Call` or an
16449 // `Identifier`, so the slot supplies the one directly.
16450 // * `paren` / `negated` — `block` and `unary_operator` are
16451 // wrappers, peeled by `elixir_wrapper_operand` before the slot
16452 // asks whether an operator already paid. `negated` covers the
16453 // keyword `not`, which the peel once did not recognise as a
16454 // negation.
16455 // * `membership` / `nonmembership` — `in` / `not in` are
16456 // relational operators, given the by-use arm #1461 gave the
16457 // other five languages. Without it these two would score zero,
16458 // since the slot correctly declines an operator application.
16459 //
16460 // A repeated guard (`when a when b`) is deliberately absent: it is
16461 // an or-chain and scores one per alternative, so it belongs with
16462 // `elixir_repeated_guard_matches_the_or_chain` below rather than in
16463 // a table whose every row reads 2.
16464 //
16465 // §11: each bullet is an independent path, and no other member can
16466 // stand in for it — deleting the `in` arm leaves `membership` and
16467 // `nonmembership` alone failing, deleting the `not` recognition
16468 // leaves `negated` alone failing.
16469 #[cfg(feature = "elixir")]
16470 #[test]
16471 fn elixir_guard_scores_one_however_spelled() {
16472 let src = "defmodule T do
16473 def tok(x) do
16474 case x do
16475 n when n > 5 -> 1
16476 _ -> 0
16477 end
16478 end
16479 def eq(x) do
16480 case x do
16481 n when n == 5 -> 1
16482 _ -> 0
16483 end
16484 end
16485 def call(x) do
16486 case x do
16487 n when is_integer(n) -> 1
16488 _ -> 0
16489 end
16490 end
16491 def bare(x, b) do
16492 case x do
16493 _n when b -> 1
16494 _ -> 0
16495 end
16496 end
16497 def paren(x, b) do
16498 case x do
16499 _n when (b) -> 1
16500 _ -> 0
16501 end
16502 end
16503 def negated(x, b) do
16504 case x do
16505 _n when not b -> 1
16506 _ -> 0
16507 end
16508 end
16509 def membership(x) do
16510 case x do
16511 n when n in [1, 2] -> 1
16512 _ -> 0
16513 end
16514 end
16515 def nonmembership(x) do
16516 case x do
16517 n when n not in [1, 2] -> 1
16518 _ -> 0
16519 end
16520 end
16521end
16522";
16523 assert_fixture_spells::<ElixirParser>(
16524 src,
16525 "foo.ex",
16526 &[
16527 (Elixir::When as u16, 8, "one `when` guard per member"),
16528 (Elixir::GT as u16, 1, "`tok`'s `>`"),
16529 (Elixir::EQEQ as u16, 1, "`eq`'s `==`"),
16530 (Elixir::Block as u16, 1, "`paren`'s parenthesised guard"),
16531 (Elixir::Not as u16, 1, "`negated`'s `not`"),
16532 (Elixir::In as u16, 1, "`membership`'s `in`"),
16533 (Elixir::Notin as u16, 1, "`nonmembership`'s `not in`"),
16534 ],
16535 );
16536 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16537 assert_every_member_scores(
16538 &space.spaces[0],
16539 8,
16540 2,
16541 "one for the `case` arm and one for the guard, however the guard is spelled",
16542 );
16543 });
16544 }
16545
16546 // Elixir's repeated guard is an or-chain, not a longer spelling of
16547 // one guard: each `when` expression is tried in turn and evaluation
16548 // moves to the next when the previous one is false *or raises*. So
16549 // `when a when b` carries the alternatives `when a or b` does, and
16550 // the two spellings have to score alike.
16551 //
16552 // They did not. The nesting is right-associative
16553 // (`head when (a when (b when c))`), only the outermost `when` sat
16554 // on the anchor `elixir_when_is_guard` tests, and the `Abc` slot
16555 // peeled to the last alternative and scored that one — so a
16556 // repeated guard read *identically to a single guard* on both axes
16557 // while the `or` form read one higher per alternative.
16558 //
16559 // Asserted as an equality against the `or` spelling rather than
16560 // against separate literals, so neither row can drift alone; the
16561 // literals are here too, because a pair of equal wrong numbers
16562 // would satisfy the equality on its own.
16563 //
16564 // Both anchors, because the climb has to reach each: a definition
16565 // `Call`'s `arguments` (`repeated` / `ored`) and a `stab_clause`'s
16566 // `left` (`clause` / `clause_or`). `single` is the control the
16567 // repeated rows must sit *above*, which is the comparison that
16568 // failed before.
16569 #[cfg(feature = "elixir")]
16570 #[test]
16571 fn elixir_repeated_guard_matches_the_or_chain() {
16572 let src = "defmodule T do
16573 def repeated(n) when is_integer(n) when is_float(n) when is_atom(n) do
16574 n
16575 end
16576 def ored(n) when is_integer(n) or is_float(n) or is_atom(n) do
16577 n
16578 end
16579 def single(n) when is_integer(n) do
16580 n
16581 end
16582 def clause(x) do
16583 case x do
16584 n when is_integer(n) when is_float(n) -> 1
16585 _ -> 0
16586 end
16587 end
16588 def clause_or(x) do
16589 case x do
16590 n when is_integer(n) or is_float(n) -> 1
16591 _ -> 0
16592 end
16593 end
16594end
16595";
16596 assert_fixture_spells::<ElixirParser>(
16597 src,
16598 "foo.ex",
16599 &[
16600 (
16601 Elixir::When as u16,
16602 8,
16603 "three repeated, one `or`-spelled, one single, two \
16604 repeated and one `or`-spelled in a clause",
16605 ),
16606 (Elixir::Or as u16, 3, "the two `or` spellings' operators"),
16607 ],
16608 );
16609 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16610 // Positional: a `def` whose head carries a guard parses its
16611 // name out of a `binary_operator` rather than a plain
16612 // `Call`, so `repeated`, `ored` and `single` all come back
16613 // `<anonymous>` (the same naming gap
16614 // `elixir_typespec_when_is_not_a_guard` reads around).
16615 let members: Vec<(u64, u64)> = space.spaces[0]
16616 .spaces
16617 .iter()
16618 .map(|m| {
16619 (
16620 m.metrics.abc.conditions(),
16621 m.metrics.cyclomatic.cyclomatic(),
16622 )
16623 })
16624 .collect();
16625 assert_eq!(
16626 members,
16627 vec![(3, 4), (3, 4), (1, 2), (3, 4), (3, 4)],
16628 "a repeated guard scores one alternative per `when`; the \
16629 single guard is the control one alternative lower"
16630 );
16631 assert_eq!(
16632 members[0], members[1],
16633 "`when a when b when c` is the or-chain `when a or b or c`"
16634 );
16635 assert_eq!(
16636 members[3], members[4],
16637 "and the same holds on a `stab_clause` anchor"
16638 );
16639 });
16640 }
16641
16642 // The gate that makes the Elixir arm safe. Elixir has no dedicated
16643 // guard production, and a typespec's binding clause spells the same
16644 // `when` token — so an ungated arm would have made type syntax a
16645 // decision. It was already an ABC condition against no decision
16646 // anywhere, which this removes.
16647 //
16648 // Both members carry the same `@spec`; only `guarded` carries a real
16649 // head guard, so the difference between the two rows is the guard
16650 // and nothing else.
16651 #[cfg(feature = "elixir")]
16652 #[test]
16653 fn elixir_typespec_when_is_not_a_guard() {
16654 let src = "defmodule T do
16655 @spec plain(a) :: a when a: integer
16656 def plain(x) do
16657 x
16658 end
16659 @spec guarded(a) :: a when a: integer
16660 def guarded(x) when is_integer(x) do
16661 x
16662 end
16663end
16664";
16665 assert_fixture_spells::<ElixirParser>(
16666 src,
16667 "foo.ex",
16668 &[(
16669 Elixir::When as u16,
16670 3,
16671 "two typespec `when`s and one head guard",
16672 )],
16673 );
16674 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16675 let module = &space.spaces[0];
16676 // Positional rather than by name: a `def` whose head carries
16677 // a guard parses its name out of a `binary_operator` instead
16678 // of a plain `Call` target, and the space comes back
16679 // `<anonymous>`. That naming gap predates this change and is
16680 // why `assert_members_score` cannot serve here.
16681 let members: Vec<(u64, u64)> = module
16682 .spaces
16683 .iter()
16684 .map(|m| {
16685 (
16686 m.metrics.abc.conditions(),
16687 m.metrics.cyclomatic.cyclomatic(),
16688 )
16689 })
16690 .collect();
16691 assert_eq!(
16692 members,
16693 vec![(0, 1), (1, 2)],
16694 "`plain` scores nothing; `guarded` scores its head guard \
16695 in both metrics"
16696 );
16697 // The typespecs sit in the module body, outside either
16698 // member, so their (non-)contribution has to be read off
16699 // the container. Was 2 before the gate: one per `@spec`.
16700 assert_eq!(
16701 module.metrics.abc.conditions(),
16702 0,
16703 "a typespec `when` is type syntax, not a guard"
16704 );
16705 });
16706 }
16707
16708 // The other half of that gate's allowlist. `elixir_when_is_guard`
16709 // accepts a definition `Call`'s `arguments` anchor when the call's
16710 // keyword is a method macro *or* one of `defguard` / `defguardp`,
16711 // and the second disjunct is its own path: `elixir_is_method_macro`
16712 // spells only `def` / `defp` / `defmacro` / `defmacrop`, so a
16713 // `defguard` head reaches the anchor test solely through the
16714 // `matches!`. Nothing in the suite spelled `defguard` before this,
16715 // which left the disjunct scored by no fixture at all.
16716 //
16717 // `defguard` is where a guard is most obviously a decision — the
16718 // macro exists to name one — so an ungated head would have scored
16719 // the construct zero on both axes while the `def f(x) when g`
16720 // spelling it expands into scores one.
16721 //
16722 // Read off the *module*, not off a member: `defguard` is not a
16723 // method macro, so it opens no function space and its guard lands
16724 // in the container's body. `plain` is the control that pins the
16725 // module's rows to the two guards and nothing else, and the guard
16726 // bodies are calls (`is_integer/1`, `is_atom/1`) rather than
16727 // comparisons so neither can supply a condition of its own.
16728 #[cfg(feature = "elixir")]
16729 #[test]
16730 fn elixir_defguard_head_is_a_guard() {
16731 let src = "defmodule T do
16732 defguard is_int(x) when is_integer(x)
16733 defguardp is_at(x) when is_atom(x)
16734 def plain(x) do
16735 x
16736 end
16737end
16738";
16739 assert_fixture_spells::<ElixirParser>(
16740 src,
16741 "foo.ex",
16742 &[(
16743 Elixir::When as u16,
16744 2,
16745 "one per `defguard` / `defguardp` head",
16746 )],
16747 );
16748 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16749 let module = &space.spaces[0];
16750 assert_eq!(
16751 (
16752 module.metrics.abc.conditions(),
16753 module.metrics.cyclomatic.cyclomatic()
16754 ),
16755 (2, 3),
16756 "each of `defguard` and `defguardp` carries one guard, on \
16757 both axes (cyclomatic counts from its base of 1)"
16758 );
16759 let members: Vec<(u64, u64)> = module
16760 .spaces
16761 .iter()
16762 .map(|m| {
16763 (
16764 m.metrics.abc.conditions(),
16765 m.metrics.cyclomatic.cyclomatic(),
16766 )
16767 })
16768 .collect();
16769 assert_eq!(
16770 members,
16771 vec![(0, 1)],
16772 "`defguard` opens no function space, so `plain` is the \
16773 module's only member and scores nothing"
16774 );
16775 });
16776 }
16777
16778 // #1461's structural half: a relational operator scores by *use*,
16779 // a value-bearing operand scores in a boolean *slot*.
16780 //
16781 // Until this, the relational constructs a grammar spells as their
16782 // own production — C#'s two `is` tests, Java's and Groovy's
16783 // `instanceof`, Groovy's `in`, Kotlin's `is` / `in`, Ruby's
16784 // one-line `in` — reached `stats.conditions` only through
16785 // `<lang>_bool_terminal_kinds!()`, which every walker consults
16786 // inside a boolean slot and nowhere else. The comparison token
16787 // beside each of them carried no such gate, so one language scored
16788 // two spellings of the same relational test differently by
16789 // position:
16790 //
16791 // val b = a == c // 1 condition
16792 // val b = a is String // 0 conditions
16793 //
16794 // Fitzpatrick Rule 5 scores a relational operator wherever it is
16795 // written, so the asymmetry was in the mechanism. Each construct
16796 // now has an unconditional arm in its language's `compute` and has
16797 // left the terminal set; listing it in both would score it twice
16798 // (`.claude/rules/grammar-dispatch.md` §5).
16799 //
16800 // Every fixture below carries the same four shapes, because they
16801 // are structurally independent paths and covering one says nothing
16802 // about the others (§11):
16803 //
16804 // - `out*` — the construct with no boolean slot above it. This is
16805 // the case the change exists for, and the one no prior fixture
16806 // covered: measured against the pre-change binary, every `out*`
16807 // member here scored **0**.
16808 // - `outCtl` — the same member spelling `==`. It read 1 before and
16809 // after, so it is the anchor that makes `out*`'s 1 a claim about
16810 // the construct rather than about the member shape
16811 // (`.claude/rules/testing.md`, "Perturb the fixture as well as
16812 // the production line").
16813 // - `in*` / `inAnd` — the predicate slot and the `&&`-chain walker,
16814 // the two paths that already owned the count. They must be
16815 // *unchanged*; a §5 double count surfaces here as a 2 against the
16816 // control's 1, which is how #1459 caught the Kotlin `as?`
16817 // collision.
16818 // - `forIn` (Kotlin / Groovy / Ruby) — the `for … in …` header,
16819 // which spells the same keyword from a different production and
16820 // must stay at 0. It is the reason those three count the node and
16821 // not the token.
16822
16823 #[cfg(feature = "csharp")]
16824 #[test]
16825 fn csharp_is_tests_score_outside_a_boolean_slot() {
16826 let src = "class A {
16827 bool outIs(object x) => x is int;
16828 bool outPat(object x) => x is null;
16829 bool outCtl(int x) => x == 1;
16830 int inIs(object x) { if (x is int) { return 1; } return 0; }
16831 int inAnd(object x, bool b) { if (x is int && b) { return 1; } return 0; }
16832 }";
16833 assert_csharp_fixture_spells(
16834 src,
16835 &[
16836 (Csharp::IsExpression as u16, 3, "the bare `is` type tests"),
16837 (
16838 Csharp::IsPatternExpression as u16,
16839 1,
16840 "`outPat`'s null pattern",
16841 ),
16842 (Csharp::EQEQ as u16, 1, "`outCtl`'s comparison"),
16843 ],
16844 );
16845 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
16846 assert_members_score(
16847 &space.spaces[0],
16848 &[
16849 // Both were 0, against `outCtl`'s 1 on the same
16850 // expression-bodied shape.
16851 ("outIs", 1, 1),
16852 ("outPat", 1, 1),
16853 ("outCtl", 1, 1),
16854 // Unchanged: the slot no longer counts the test, the
16855 // new arm does, and the total stays put.
16856 ("inIs", 1, 2),
16857 ("inAnd", 2, 3),
16858 ],
16859 );
16860 });
16861 }
16862
16863 #[cfg(feature = "java")]
16864 #[test]
16865 fn java_instanceof_scores_outside_a_boolean_slot() {
16866 let src = "class A {
16867 boolean outOf(Object x) { return x instanceof String; }
16868 boolean outCtl(int x) { return x == 1; }
16869 int inOf(Object x) { if (x instanceof String) { return 1; } return 0; }
16870 int inAnd(Object x, boolean b) { if (x instanceof String && b) { return 1; } return 0; }
16871 }";
16872 assert_fixture_spells::<JavaParser>(
16873 src,
16874 "foo.java",
16875 &[
16876 (
16877 Java::InstanceofExpression as u16,
16878 3,
16879 "the `instanceof` tests",
16880 ),
16881 (Java::EQEQ as u16, 1, "`outCtl`'s comparison"),
16882 ],
16883 );
16884 check_func_space::<JavaParser, _>(src, "foo.java", |space| {
16885 assert_members_score(
16886 &space.spaces[0],
16887 &[
16888 // Was 0, against `outCtl`'s 1 on the same
16889 // `return`-a-predicate shape.
16890 ("outOf", 1, 1),
16891 ("outCtl", 1, 1),
16892 ("inOf", 1, 2),
16893 ("inAnd", 2, 3),
16894 ],
16895 );
16896 });
16897 }
16898
16899 // Groovy carries item 4 as well as the structural pass. `<=>`
16900 // yields -1 / 0 / 1 rather than a boolean, which is why it never
16901 // belonged in the terminal set — that set holds operands — but it
16902 // is a relational operator, and `LTEQGT` is already a condition
16903 // token in Ruby, PHP, C++ and Mozcpp. Groovy was the outlier at 0.
16904 #[cfg(feature = "groovy")]
16905 #[test]
16906 fn groovy_relational_productions_score_outside_a_boolean_slot() {
16907 let src = "class A {
16908 def outIs(x) { def b = x instanceof String; return b }
16909 def outIn(x, l) { def b = x in l; return b }
16910 def outShip(a, c) { def r = a <=> c; return r }
16911 def outCtl(x) { def b = x == 1; return b }
16912 def inIs(x) { if (x instanceof String) { return 1 }; return 0 }
16913 def inIn(x, l) { if (x in l) { return 1 }; return 0 }
16914 def forIn(l) { for (q in l) { }; return 0 }
16915 }";
16916 assert_fixture_spells::<GroovyParser>(
16917 src,
16918 "foo.groovy",
16919 &[
16920 (
16921 Groovy::InstanceofExpression as u16,
16922 2,
16923 "the `instanceof` tests",
16924 ),
16925 (Groovy::MembershipExpression as u16, 2, "the `in` tests"),
16926 (Groovy::SpaceshipExpression as u16, 1, "`outShip`'s `<=>`"),
16927 (
16928 Groovy::ForInStatement as u16,
16929 1,
16930 "`forIn`'s loop header, which spells the same `in`",
16931 ),
16932 (Groovy::EQEQ as u16, 1, "`outCtl`'s comparison"),
16933 ],
16934 );
16935 check_func_space::<GroovyParser, _>(src, "foo.groovy", |space| {
16936 assert_members_score(
16937 &space.spaces[0],
16938 &[
16939 // All three were 0, against `outCtl`'s 1 on the
16940 // identical declare-and-return shape.
16941 ("outIs", 1, 1),
16942 ("outIn", 1, 1),
16943 ("outShip", 1, 1),
16944 ("outCtl", 1, 1),
16945 ("inIs", 1, 2),
16946 ("inIn", 1, 2),
16947 // The loop header is not a membership test and
16948 // stays at 0 — counting the `in` *token* would have
16949 // scored it.
16950 ("forIn", 0, 2),
16951 ],
16952 );
16953 });
16954 }
16955
16956 #[cfg(feature = "kotlin")]
16957 #[test]
16958 fn kotlin_is_and_in_score_outside_a_boolean_slot() {
16959 let src = "class K {
16960 fun outIs(a: Any): Boolean { val b = a is String; return b }
16961 fun outIn(a: Int): Boolean { val b = a in 1..2; return b }
16962 fun outCtl(a: Int): Boolean { val b = a == 1; return b }
16963 fun inIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
16964 fun forIn(l: List<Int>): Int { for (q in l) { }; return 0 }
16965 }";
16966 assert_kotlin_class_members(
16967 src,
16968 &[
16969 (Kotlin::IsExpression as u16, 2, "the `is` tests"),
16970 (Kotlin::InExpression as u16, 1, "`outIn`'s `in` test"),
16971 (
16972 Kotlin::ForStatement as u16,
16973 1,
16974 "`forIn`'s loop header, which spells the same `in`",
16975 ),
16976 (Kotlin::EQEQ as u16, 1, "`outCtl`'s comparison"),
16977 ],
16978 &[
16979 // Both were 0, against `outCtl`'s 1 on the identical
16980 // `val`-and-return shape.
16981 ("outIs", 1, 1),
16982 ("outIn", 1, 1),
16983 ("outCtl", 1, 1),
16984 ("inIs", 1, 2),
16985 ("forIn", 0, 2),
16986 ],
16987 );
16988 }
16989
16990 #[cfg(feature = "ruby")]
16991 #[test]
16992 fn ruby_test_pattern_scores_outside_a_boolean_slot() {
16993 let src = "def out_in(a)
16994 b = a in Integer
16995 b
16996end
16997def out_ctl(a)
16998 b = a == 1
16999 b
17000end
17001def in_in(a)
17002 if a in Integer
17003 return 1
17004 end
17005 0
17006end
17007def for_in(l)
17008 for q in l do end
17009 0
17010end
17011";
17012 assert_fixture_spells::<RubyParser>(
17013 src,
17014 "foo.rb",
17015 &[
17016 (Ruby::TestPattern as u16, 2, "the one-line `in` tests"),
17017 (
17018 Ruby::For as u16,
17019 1,
17020 "`for_in`'s loop header, which spells the same `in`",
17021 ),
17022 (Ruby::EQEQ as u16, 1, "`out_ctl`'s comparison"),
17023 ],
17024 );
17025 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
17026 assert_members_score(
17027 &space,
17028 &[
17029 // Was 0, against `out_ctl`'s 1 on the identical
17030 // assign-and-return shape.
17031 ("out_in", 1, 1),
17032 ("out_ctl", 1, 1),
17033 ("in_in", 1, 2),
17034 ("for_in", 0, 2),
17035 ],
17036 );
17037 });
17038 }
17039
17040 // #1461 item 1. The walrus is an *operand*, not an operator, so it
17041 // joined the former `python_bool_terminal_kinds!()` and stays slot-scoped —
17042 // `b = (n := g())` outside a predicate is a binding, not a
17043 // decision. Inside one the slot tests `g()`'s truth, and
17044 // `if (n := g()):` scored 0 where the `if g():` it is a
17045 // refactoring of scored 1.
17046 //
17047 // `assignments` is asserted beside `conditions` because the walrus
17048 // is the one construct in the survey that pays on two axes, and
17049 // that is deliberate rather than a §5 double count: ABC's axes are
17050 // independent measurements of the same source, and `if (n := g()):`
17051 // genuinely both binds a name and decides a branch. Without this
17052 // row a later reader has no way to tell the intent from an
17053 // oversight.
17054 #[cfg(feature = "python")]
17055 #[test]
17056 fn python_walrus_is_a_unary_condition() {
17057 let src = "def in_slot(g):
17058 if (n := g()):
17059 return 1
17060 return 0
17061
17062def chain(g, b):
17063 return b and (n := g())
17064
17065def ctrl(g):
17066 if g():
17067 return 1
17068 return 0
17069";
17070 assert_fixture_spells::<PythonParser>(
17071 src,
17072 "foo.py",
17073 &[(Python::NamedExpression as u16, 2, "the walrus bindings")],
17074 );
17075 check_func_space::<PythonParser, _>(src, "foo.py", |space| {
17076 assert_members_score(
17077 &space,
17078 &[
17079 // Was 0, against `ctrl`'s 1 for the same predicate
17080 // without the binding.
17081 ("in_slot", 1, 2),
17082 // The `and`-chain walker is the second, independent
17083 // path (§11): was 1, the bare `b` operand alone.
17084 ("chain", 2, 2),
17085 ("ctrl", 1, 2),
17086 ],
17087 );
17088 for (name, assignments) in [("in_slot", 1u64), ("chain", 1), ("ctrl", 0)] {
17089 assert_eq!(
17090 child_space(&space, name).metrics.abc.assignments(),
17091 assignments,
17092 "{name}: the walrus also pays on the A axis, on purpose"
17093 );
17094 }
17095 });
17096 }
17097
17098 // #1461 item 2. A macro in a boolean slot expands to a boolean
17099 // expression, and `if matches!(x, Some(_))` scored 0 against a
17100 // cyclomatic decision of 1.
17101 //
17102 // `cfg_slot` is in the fixture because the arm's *breadth* is the
17103 // decision, not a side effect: these sets discriminate on slot and
17104 // never on return type, so `cfg!` counts exactly as `matches!`
17105 // does. It is also the construct the corpus actually moves —
17106 // serde's one changed snapshot is `if cfg!(no_underscore_consts)`,
17107 // not a `matches!` — so asserting only `matches!` would leave the
17108 // measured case uncovered.
17109 #[cfg(feature = "rust")]
17110 #[test]
17111 fn rust_macro_invocation_is_a_unary_condition() {
17112 let src = "fn in_slot(x: Option<u8>) -> u8 { if matches!(x, Some(_)) { 1 } else { 0 } }
17113fn cfg_slot() -> u8 { if cfg!(unix) { 1 } else { 0 } }
17114fn chain(x: Option<u8>, b: bool) -> bool { b && matches!(x, Some(_)) }
17115fn ctrl(x: Option<u8>) -> u8 { if x.is_some() { 1 } else { 0 } }
17116";
17117 assert_fixture_spells::<RustParser>(
17118 src,
17119 "foo.rs",
17120 &[(
17121 Rust::MacroInvocation as u16,
17122 3,
17123 "the `matches!` / `cfg!` predicates",
17124 )],
17125 );
17126 check_func_space::<RustParser, _>(src, "foo.rs", |space| {
17127 assert_members_score(
17128 &space,
17129 &[
17130 // Each was 1 — the `else` alone — against `ctrl`'s
17131 // 2 for the same shape with a method call in the
17132 // predicate.
17133 ("in_slot", 2, 2),
17134 ("cfg_slot", 2, 2),
17135 // The `&&`-chain walker, the second independent
17136 // path (§11): was 1, the bare `b` operand alone.
17137 ("chain", 2, 2),
17138 ("ctrl", 2, 2),
17139 ],
17140 );
17141 });
17142 }
17143}
17144
17145/// A comment inside a ternary must not change its ABC conditions
17146/// (#1181).
17147///
17148/// Two opposite defects, one cause: tree-sitter counts a comment among a
17149/// node's children, so it is the operand's previous sibling *and* it
17150/// shifts every positional index.
17151///
17152/// * Languages whose seed asked "is my previous sibling `?` or `:`"
17153/// (C family, PHP, Perl, JS family) read the comment as "not a
17154/// ternary token", flipped the boolean-context seed on for a *branch*
17155/// slot, and **over**-counted: `a ? /*n*/ (b) : c` scored 3 where
17156/// `a ? (b) : c` scores 2.
17157/// * Languages whose branch walk read `child(2)` / `child(4)` (Java,
17158/// C#, Groovy) landed on the comment instead of the operand, never
17159/// inspected it, and **under**-counted: `a ? /*n*/ !b : c` scored 2
17160/// where `a ? !b : c` scores 3.
17161///
17162/// Both slots are now addressed by grammar field. The parenthesised
17163/// operand is the only input that discriminates the first defect and
17164/// the negated operand the only one that discriminates the second —
17165/// existing ternary fixtures use neither.
17166// Gated on the union of the features [`ternary_comment_invariance::cases`]
17167// has a row for, so a build enabling none of them — `--no-default-features
17168// --features go` — drops both tests rather than tripping their
17169// `checked > 0` guards (`.claude/rules/testing.md`, #1286). Both tests
17170// read the same table, so one gate on the module is exact for each.
17171#[cfg(test)]
17172#[cfg(any(
17173 feature = "c",
17174 feature = "cpp",
17175 feature = "csharp",
17176 feature = "groovy",
17177 feature = "java",
17178 feature = "javascript",
17179 feature = "mozcpp",
17180 feature = "mozjs",
17181 feature = "objc",
17182 feature = "perl",
17183 feature = "php",
17184 feature = "typescript"
17185))]
17186mod ternary_comment_invariance {
17187 use crate::test_support::metrics_verbatim;
17188 use crate::{LANG, MetricsOptions};
17189
17190 fn conditions(lang: LANG, source: &str) -> u64 {
17191 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17192 .abc
17193 .conditions_sum()
17194 }
17195
17196 /// `(base, with_comment)` for a parenthesised and a negated branch
17197 /// operand, per language.
17198 fn cases(lang: LANG) -> Option<[(String, String); 2]> {
17199 // `{}` marks the consequence slot; `/*n*/` the inserted comment.
17200 let (template, paren, negated, comment) = match lang {
17201 LANG::Cpp | LANG::C | LANG::Objc | LANG::Mozcpp => {
17202 ("int f(){ int x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
17203 }
17204 LANG::Java => (
17205 "class K{ void f(){ int x = a ? {} : c; } }",
17206 "(b)",
17207 "!b",
17208 "/*n*/ ",
17209 ),
17210 LANG::Csharp => (
17211 "class K{ void f(){ var x = a ? {} : c; } }",
17212 "(b)",
17213 "!b",
17214 "/*n*/ ",
17215 ),
17216 LANG::Groovy => ("def f(){ def x = a ? {} : c }", "(b)", "!b", "/*n*/ "),
17217 LANG::Javascript | LANG::Typescript | LANG::Tsx | LANG::Mozjs => {
17218 ("function f(){ var x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
17219 }
17220 LANG::Php => (
17221 "<?php function f(){ $x = $a ? {} : $c; }",
17222 "($b)",
17223 "!$b",
17224 "/*n*/ ",
17225 ),
17226 // Perl has no block comment: `#` runs to end of line, so the
17227 // comment must carry its own newline.
17228 LANG::Perl => ("sub f { my $x = $a ? {} : $c; }", "($b)", "!$b", "# n\n "),
17229 _ => return None,
17230 };
17231 let build = |operand: &str, with_comment: bool| {
17232 let slot = if with_comment {
17233 format!("{comment}{operand}")
17234 } else {
17235 operand.to_owned()
17236 };
17237 template.replace("{}", &slot)
17238 };
17239 Some([
17240 (build(paren, false), build(paren, true)),
17241 (build(negated, false), build(negated, true)),
17242 ])
17243 }
17244
17245 #[test]
17246 fn a_comment_before_a_branch_operand_changes_nothing() {
17247 let mut checked = 0;
17248 for lang in LANG::into_enum_iter() {
17249 if !lang.is_enabled() {
17250 continue;
17251 }
17252 let Some(pairs) = cases(lang) else { continue };
17253 checked += 1;
17254 for (base, commented) in pairs {
17255 assert_eq!(
17256 conditions(lang, &commented),
17257 conditions(lang, &base),
17258 "{lang:?}: a comment changed the ABC conditions of a ternary\n \
17259 without: {base}\n with: {commented}"
17260 );
17261 }
17262 }
17263 assert!(
17264 checked > 0,
17265 "no ternary language enabled; this test asserted nothing"
17266 );
17267 }
17268
17269 /// The absolute values the invariance test compares against, so a
17270 /// regression that moved *both* sides equally still fails.
17271 ///
17272 /// expected: `a ? (b) : c` counts the `?` marker plus the condition
17273 /// `a` in boolean context = 2. Negating the consequence adds one
17274 /// more, since `!b` establishes boolean content for that slot = 3.
17275 #[test]
17276 fn the_baseline_values_are_two_and_three() {
17277 let mut checked = 0;
17278 for lang in LANG::into_enum_iter() {
17279 if !lang.is_enabled() {
17280 continue;
17281 }
17282 let Some([(paren, _), (negated, _)]) = cases(lang) else {
17283 continue;
17284 };
17285 assert_eq!(
17286 conditions(lang, &paren),
17287 2,
17288 "{lang:?}: parenthesised branch"
17289 );
17290 assert_eq!(conditions(lang, &negated), 3, "{lang:?}: negated branch");
17291 checked += 1;
17292 }
17293 assert!(
17294 checked > 0,
17295 "no ternary language enabled; this test asserted nothing"
17296 );
17297 }
17298}
17299
17300/// A keyword negation must score like its symbolic twin (#1182).
17301///
17302/// `not b` and `!b` are the same negation — they differ in precedence,
17303/// not in meaning, and ABC counts the negation rather than the parse.
17304/// Ruby and Perl tested only the `!` token, so `if not b` scored 0
17305/// against `if !b`'s 1, and a `not` ternary scored 2 against the `!`
17306/// form's 4.
17307///
17308/// Lua and Elixir were checked in the same sweep and were already
17309/// correct: Lua's only negation keyword *is* `not` and it was the token
17310/// being tested, and Elixir reaches the same count by another path.
17311/// They are exercised here so a future edit cannot regress them
17312/// silently. Python counts `not` through its own dispatcher arm and has
17313/// no `!` spelling to compare against.
17314// Gated on the union of every row below — the three `pairs` languages
17315// plus Lua, whose test is separate — so a build enabling none of them
17316// drops the module instead of failing its guards
17317// (`.claude/rules/testing.md`, #1286 / #1411). The three tests do *not*
17318// share a row set, so each carries its own narrower gate: a Lua-only
17319// build must not be asked to run the Ruby/Perl baselines.
17320#[cfg(test)]
17321#[cfg(any(
17322 feature = "elixir",
17323 feature = "lua",
17324 feature = "perl",
17325 feature = "ruby"
17326))]
17327mod keyword_negation_parity {
17328 use crate::test_support::metrics_verbatim;
17329 use crate::{LANG, MetricsOptions};
17330
17331 fn conditions(lang: LANG, source: &str) -> u64 {
17332 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17333 .abc
17334 .conditions_sum()
17335 }
17336
17337 /// `(bang_form, keyword_form)` pairs that must score identically.
17338 #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
17339 fn pairs(lang: LANG) -> Option<Vec<(String, String)>> {
17340 let build =
17341 |t: &str| -> (String, String) { (t.replace("{NOT}", "!"), t.replace("{NOT}", "not ")) };
17342 let templates: &[&str] = match lang {
17343 LANG::Ruby => &[
17344 "def f(b)\n if {NOT}b\n 1\n end\nend\n",
17345 "def f(a, b, c)\n x = a ? ({NOT}b) : ({NOT}c)\nend\n",
17346 "def f(a, b, c)\n x = a ? b : ({NOT}c)\nend\n",
17347 ],
17348 LANG::Perl => &[
17349 "sub f { if ({NOT}$b) { 1; } }",
17350 "sub f { my $x = $a ? ({NOT}$b) : ({NOT}$c); }",
17351 "sub f { my $x = $a ? $b : ({NOT}$c); }",
17352 ],
17353 // Already correct before #1182; pinned so they stay that way.
17354 LANG::Elixir => &["def f(b) do\n if {NOT}b do\n 1\n end\nend\n"],
17355 _ => return None,
17356 };
17357 Some(templates.iter().map(|t| build(t)).collect())
17358 }
17359
17360 #[test]
17361 #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
17362 fn the_not_keyword_scores_like_bang() {
17363 let mut checked = 0;
17364 for lang in LANG::into_enum_iter() {
17365 if !lang.is_enabled() {
17366 continue;
17367 }
17368 let Some(pairs) = pairs(lang) else { continue };
17369 checked += 1;
17370 for (bang, keyword) in pairs {
17371 assert_eq!(
17372 conditions(lang, &keyword),
17373 conditions(lang, &bang),
17374 "{lang:?}: `not` and `!` scored differently\n bang: {bang}\n keyword: {keyword}"
17375 );
17376 }
17377 }
17378 assert!(
17379 checked > 0,
17380 "no language enabled; this test asserted nothing"
17381 );
17382 }
17383
17384 /// The absolute values, so a regression that moved both spellings
17385 /// equally still fails.
17386 ///
17387 /// expected: `if !b` is one condition — the negated bare operand.
17388 /// `a ? (!b) : (!c)` is four: the `?` marker, the condition `a` in
17389 /// boolean context, and one per negated branch operand.
17390 #[test]
17391 #[cfg(any(feature = "perl", feature = "ruby"))]
17392 fn the_baseline_values_are_one_and_four() {
17393 let mut checked = 0;
17394 for (lang, guard, ternary) in [
17395 (
17396 LANG::Ruby,
17397 "def f(b)\n if not b\n 1\n end\nend\n",
17398 "def f(a, b, c)\n x = a ? (not b) : (not c)\nend\n",
17399 ),
17400 (
17401 LANG::Perl,
17402 "sub f { if (not $b) { 1; } }",
17403 "sub f { my $x = $a ? (not $b) : (not $c); }",
17404 ),
17405 ] {
17406 if !lang.is_enabled() {
17407 continue;
17408 }
17409 assert_eq!(conditions(lang, guard), 1, "{lang:?}: `if not b`");
17410 assert_eq!(conditions(lang, ternary), 4, "{lang:?}: `not` ternary");
17411 checked += 1;
17412 }
17413 assert!(
17414 checked > 0,
17415 "no language enabled; this test asserted nothing"
17416 );
17417 }
17418
17419 /// Lua's only negation keyword is `not`, so it has no `!` twin to
17420 /// compare against — its guard is that the keyword counts at all.
17421 #[test]
17422 #[cfg(feature = "lua")]
17423 fn lua_counts_its_only_negation_keyword() {
17424 // Was a silent `if !is_enabled() { return; }`, which made the
17425 // whole test a no-op rather than absent whenever the runtime
17426 // check and the feature disagreed. The `cfg` above is the
17427 // absence half; this is the loud residual half (#1286).
17428 assert!(
17429 LANG::Lua.is_enabled(),
17430 "compiled under `feature = \"lua\"` but `LANG::Lua` reports disabled; \
17431 this test asserted nothing"
17432 );
17433 assert_eq!(
17434 conditions(
17435 LANG::Lua,
17436 "function f(b)\n if not b then return 1 end\nend\n"
17437 ),
17438 1
17439 );
17440 }
17441}
17442
17443/// A numeric literal used as a bare `&&` / `||` operand is a Fitzpatrick
17444/// Rule 9 unary condition in every truthy-valued language (#1379).
17445///
17446/// Ruby's terminal-bool set named `integer` and none of `float` /
17447/// `rational` / `complex`, Elixir's named `integer` and not `float`, and
17448/// Perl's named no numeric kind at all — so `a && 1.0` scored one
17449/// condition where `a && 1` scored two, and in Perl even `$a && 1`
17450/// scored one. Python had both kinds since #772 and is the control the
17451/// other three were brought level with.
17452///
17453/// #1410 closed the six sets #1379 deferred: PHP, Groovy, and the
17454/// name-keyed C / C++ / Mozcpp / Objective-C set, where the omission
17455/// was visible *within* one language — `if (true)` scored one condition
17456/// and `if (1)` none, because `"true"` was in the set and
17457/// `number_literal` was not.
17458///
17459/// The headline claim of each case is a *comparison*: a numeric operand
17460/// must score exactly what an identifier operand scores in the same slot.
17461/// That is what discriminates the defect — with the kind missing from the
17462/// set the numeric form drops while the identifier form does not — and it
17463/// gives a failure message naming both spellings.
17464/// `every_operand_scores_its_recorded_values` then pins the absolute
17465/// numbers, so a regression moving *both* sides equally still fails, and
17466/// carries the cross-metric anchor `.claude/rules/grammar-dispatch.md` §8
17467/// asks for.
17468///
17469/// Two slots per language, because the sets feed two independent walker
17470/// paths (grammar-dispatch §11) and Perl's defect showed in both: the
17471/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
17472/// only the first leaves `perl_count_condition` and `ruby_count_condition`
17473/// untested. Since #1520 Ruby's predicate pays for any operand no other
17474/// arm counts, terminal or not, so for Ruby only the chain half can tell
17475/// a kind missing from the set; the predicate half pins the slot itself.
17476///
17477/// The recorded cyclomatic figure is the same 3 for both slots — one file
17478/// space, one function space, one decision — while conditions differ (2
17479/// for the chain, 1 for the predicate, since ABC scores an `if` through
17480/// its predicate rather than the keyword). So §8's `conditions ==
17481/// cyclomatic - 1` identity is chain-specific arithmetic, not a law; what
17482/// generalises is that cyclomatic must not move when only the operand
17483/// spelling does, which is what makes a `conditions` move unambiguously
17484/// ABC's.
17485///
17486/// `for_each_case` guards the two ways this table could decay into
17487/// asserting nothing: an emptied `numerics` slice, and a template that
17488/// loses its `{}` slot (which would make every comparison `x == x`).
17489/// Both were reachable in the first draft and neither failed a test.
17490#[cfg(test)]
17491#[cfg(any(
17492 feature = "ruby",
17493 feature = "elixir",
17494 feature = "perl",
17495 feature = "python",
17496 feature = "lua",
17497 feature = "javascript",
17498 feature = "php",
17499 feature = "groovy",
17500 feature = "c",
17501 feature = "cpp",
17502 feature = "mozcpp",
17503 feature = "objc"
17504))]
17505mod numeric_bool_operands {
17506 use crate::test_support::metrics_verbatim;
17507 use crate::{LANG, MetricsOptions};
17508
17509 /// One fixture shape: a source template with a `{}` operand slot, and
17510 /// the `abc.conditions_sum` / `cyclomatic_sum` every spelling of that
17511 /// operand must produce.
17512 type Slot = (&'static str, u64, u64);
17513
17514 /// A language's two slots, its identifier baseline operand, the
17515 /// truthy literal operands that must score the same, and how many of
17516 /// those there should be. Every row but the C family's lists numeric
17517 /// literals only; that one adds `char_literal`, which is a second
17518 /// integral-literal kind rather than a second numeric spelling.
17519 ///
17520 /// The count is not bookkeeping. `for_each_case` counts *languages*,
17521 /// so trimming a row's operand list back to `&["1"]` — which is
17522 /// exactly the pre-#1379 fixture — left the whole module green when
17523 /// measured. Pinning the length makes that a deliberate two-line
17524 /// edit instead of a silent one.
17525 type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
17526
17527 fn conditions(lang: LANG, source: &str) -> u64 {
17528 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17529 .abc
17530 .conditions_sum()
17531 }
17532
17533 fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
17534 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17535 .cyclomatic
17536 .cyclomatic_sum()
17537 }
17538
17539 /// `([chain_slot, condition_slot], identifier, numerics)` per language.
17540 ///
17541 /// `{}` is the right-hand operand of a two-operand short-circuit chain
17542 /// in the first slot and the whole `if` predicate in the second, so
17543 /// every fixture differs from its own baseline in exactly one token.
17544 ///
17545 /// Each row lists **one spelling per numeric kind the grammar emits**,
17546 /// which is the unit that matters: #1379's misses were sibling rules
17547 /// under a shared supertype, not aliases of one rule.
17548 ///
17549 /// - Ruby: `integer` / `float` / `rational` / `complex`. `1r` parses
17550 /// to `rational(integer)`, `2i` to `complex(integer)` and `1ri` to
17551 /// `complex(rational(integer))` — verified by `bca dump` — and the
17552 /// walker cannot descend into any wrapper, so listing the wrapper
17553 /// is the only way each scores at all. Radix prefixes and `_`
17554 /// separators fold into `integer`.
17555 /// - Perl: all five members of the hidden `_numeric_literals` choice.
17556 /// `0xff` and `1.5e10` are here because the first cut of #1379
17557 /// named only `integer` and `floating_point` and left them scoring
17558 /// 1 against an identifier's 2. `017` is *not* a sixth row — it
17559 /// lexes as `integer`, and the `octal` kind is unreachable.
17560 /// - Elixir: `integer` / `float` / `char`. `?a` is the codepoint 97,
17561 /// a numeric literal wearing a sigil.
17562 /// - PHP: `integer` / `float`. Every radix prefix, `_` separator and
17563 /// exponent folds into those two, and the `float` *type* keyword
17564 /// `Float2` is a third id rendering to the same name that must
17565 /// stay out of the set (#1410).
17566 /// - Groovy: one consolidated `number_literal`, type suffixes
17567 /// (`1L`, `1.5f`, `1G`) included.
17568 /// - C, C++, Mozcpp and Objective-C: one consolidated
17569 /// `number_literal`, plus `char_literal` — not a numeric spelling
17570 /// but a second integral-literal *kind*, since `'c'` has type
17571 /// `int` in C and `char` in C++ and is truthy for the same reason.
17572 /// All four share one name-keyed set and so run the same fixture,
17573 /// which is the only coverage `Mozcpp` can have: it owns no file
17574 /// extension, and `metrics_verbatim` is the one entry point that
17575 /// reaches a `LANG` without going through one.
17576 ///
17577 /// Python, Lua and JavaScript were already correct and ride along as
17578 /// controls — they are what the first three were measured against,
17579 /// and a future edit that breaks them fails here too. Each folds
17580 /// every radix into one `Number` / `integer` kind, checked by
17581 /// measurement.
17582 fn cases(lang: LANG) -> Option<Case> {
17583 Some(match lang {
17584 LANG::Ruby => (
17585 [
17586 ("def f(a)\n a && {}\nend\n", 2, 3),
17587 ("def f\n if {}\n 1\n end\nend\n", 1, 3),
17588 ],
17589 "b",
17590 &["1", "1.0", "1r", "2i", "1ri"],
17591 5,
17592 ),
17593 LANG::Elixir => (
17594 [
17595 ("def f(a) do\n a && {}\nend\n", 2, 3),
17596 ("def f() do\n if {} do\n 1\n end\nend\n", 1, 3),
17597 ],
17598 "b",
17599 &["1", "1.0", "?a"],
17600 3,
17601 ),
17602 LANG::Perl => (
17603 [
17604 ("sub f {\n my $x = $a && {};\n}\n", 2, 3),
17605 ("sub f {\n if ({}) { 1; }\n}\n", 1, 3),
17606 ],
17607 "$b",
17608 &["1", "1.0", "1.5e10", "0xff"],
17609 4,
17610 ),
17611 LANG::Python => (
17612 [
17613 ("def f(a):\n return a and {}\n", 2, 3),
17614 ("def f():\n if {}:\n return 1\n", 1, 3),
17615 ],
17616 "b",
17617 &["1", "1.0", "0xff", "1j"],
17618 4,
17619 ),
17620 LANG::Lua => (
17621 [
17622 ("function f(a)\n return a and {}\nend\n", 2, 3),
17623 ("function f()\n if {} then return 1 end\nend\n", 1, 3),
17624 ],
17625 "b",
17626 &["1", "1.0", "0xff"],
17627 3,
17628 ),
17629 LANG::Javascript => (
17630 [
17631 ("function f(a) {\n return a && {};\n}\n", 2, 3),
17632 ("function f() {\n if ({}) { return 1; }\n}\n", 1, 3),
17633 ],
17634 "b",
17635 &["1", "1.0", "0xff"],
17636 3,
17637 ),
17638 LANG::Php => (
17639 [
17640 ("<?php\nfunction f($a) {\n return $a && {};\n}\n", 2, 3),
17641 ("<?php\nfunction f() {\n if ({}) { return 1; }\n}\n", 1, 3),
17642 ],
17643 "$b",
17644 &["1", "1.0"],
17645 2,
17646 ),
17647 LANG::Groovy => (
17648 [
17649 ("def f(a) {\n return a && {}\n}\n", 2, 3),
17650 ("def f() {\n if ({}) { return 1 }\n}\n", 1, 3),
17651 ],
17652 "b",
17653 &["1"],
17654 1,
17655 ),
17656 LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc => (
17657 [
17658 ("int f(int a) {\n return a && {};\n}\n", 2, 3),
17659 ("int f() {\n if ({}) { return 1; }\n}\n", 1, 3),
17660 ],
17661 "b",
17662 &["1", "'c'"],
17663 2,
17664 ),
17665 _ => return None,
17666 })
17667 }
17668
17669 /// Runs `check` once per enabled language that has a case, having
17670 /// first established that the case can still assert something.
17671 ///
17672 /// Three guards, all of which had to be added after the first draft
17673 /// shipped without them and a measured perturbation of each left the
17674 /// whole module green:
17675 ///
17676 /// - **`checked > 0`** is the non-vacuity half of the rule in
17677 /// `.claude/rules/testing.md`. The `#[cfg(any(feature = …))]` on
17678 /// this module makes the tests *absent* when no truthy-valued
17679 /// language is compiled in; this catches the residual case where
17680 /// the runtime `is_enabled()` check stops agreeing with the feature
17681 /// it compiled under.
17682 /// - **the `numerics` list is non-empty and keeps its recorded
17683 /// length** — `checked` counts *languages*, so trimming a row's
17684 /// numeric list back to `&["1"]` (the pre-#1379 fixture) or
17685 /// emptying it left every test passing when measured, with that
17686 /// language's coverage deleted. Two assertions, because the
17687 /// length check passes an emptied row whose count was zeroed with
17688 /// it.
17689 /// - **every template keeps its `{}`** — without the slot,
17690 /// `str::replace` is a no-op, baseline and candidate are computed
17691 /// from the same string, and the comparison degenerates to
17692 /// `x == x`.
17693 ///
17694 /// Sharing one driver means no test can lose any of the three.
17695 fn for_each_case(check: impl Fn(LANG, Case)) {
17696 let mut checked = 0;
17697 for lang in LANG::into_enum_iter() {
17698 if !lang.is_enabled() {
17699 continue;
17700 }
17701 let Some(case @ (slots, _, numerics, expected_kinds)) = cases(lang) else {
17702 continue;
17703 };
17704 // Emptying a row is the half the length check below cannot
17705 // see: `&[]` against an `expected_kinds` of 0 agrees with
17706 // itself, the inner loops run zero times, and `checked` —
17707 // which counts languages, not spellings — still reads
17708 // satisfied.
17709 assert!(
17710 !numerics.is_empty(),
17711 "{lang:?}: the numeric-operand list is empty; this language asserted nothing"
17712 );
17713 assert_eq!(
17714 numerics.len(),
17715 expected_kinds,
17716 "{lang:?}: the numeric-operand list no longer covers one spelling \
17717 per grammar numeric kind"
17718 );
17719 for (template, _, _) in slots {
17720 assert!(
17721 template.contains("{}"),
17722 "{lang:?}: template lost its `{{}}` operand slot: {template}"
17723 );
17724 }
17725 check(lang, case);
17726 checked += 1;
17727 }
17728 assert!(
17729 checked > 0,
17730 "no truthy-valued language enabled; this test asserted nothing"
17731 );
17732 }
17733
17734 #[test]
17735 fn a_numeric_operand_scores_like_an_identifier_operand() {
17736 for_each_case(|lang, (slots, identifier, numerics, _)| {
17737 for (template, _, _) in slots {
17738 let baseline = conditions(lang, &template.replace("{}", identifier));
17739 for numeric in numerics {
17740 let source = template.replace("{}", numeric);
17741 let scored = conditions(lang, &source);
17742 assert_eq!(
17743 scored, baseline,
17744 "{lang:?}: `{numeric}` scored {scored} unary conditions against \
17745 `{identifier}`'s {baseline}\n source: {source}"
17746 );
17747 }
17748 }
17749 });
17750 }
17751
17752 /// The absolute anchor under the comparison above: every operand
17753 /// spelling must produce the slot's recorded `conditions`, and must
17754 /// leave `cyclomatic` alone. The second half is what rules out a
17755 /// regression that moved both metrics together.
17756 #[test]
17757 fn every_operand_scores_its_recorded_values() {
17758 for_each_case(|lang, (slots, identifier, numerics, _)| {
17759 for (template, expected_conditions, expected_cyclomatic) in slots {
17760 for operand in std::iter::once(identifier).chain(numerics.iter().copied()) {
17761 let source = template.replace("{}", operand);
17762 assert_eq!(
17763 conditions(lang, &source),
17764 expected_conditions,
17765 "{lang:?}: `{operand}` conditions\n source: {source}"
17766 );
17767 assert_eq!(
17768 cyclomatic_sum(lang, &source),
17769 expected_cyclomatic,
17770 "{lang:?}: `{operand}` cyclomatic_sum\n source: {source}"
17771 );
17772 }
17773 }
17774 });
17775 }
17776}
17777
17778/// A relational construct with its own grammar production must score
17779/// like an identifier in the same boolean slot (#1449, #1461).
17780///
17781/// Every language here spells at least one boolean test as a dedicated
17782/// node rather than as a `binary_expression`, so no comparison-token
17783/// arm ever sees it. A kind missing from the former per-language
17784/// terminal-kind sets scored **zero**, silently, and zero is
17785/// indistinguishable from a legitimate zero — which is why the headline
17786/// claim of each case is a *comparison* against an identifier control
17787/// in the identical slot rather than an absolute number.
17788///
17789/// The constructs, and the route each fix took:
17790///
17791/// - **Groovy** `a in l` / `a !in l` (`membership_expression`), counted
17792/// as a node by `groovy_count_token_condition`. The other four —
17793/// `a === b` / `a !== b` (`identity_expression`) and `s =~ /p/` /
17794/// `s ==~ /p/` (`regex_find_expression`, `regex_match_expression`) —
17795/// are counted there as operator tokens, so all five score outside a
17796/// boolean slot as `==` does, and `groovy_condition_scores_itself`
17797/// lists each so the slot holding one does not pay again; a construct
17798/// takes exactly one of the two routes, never both
17799/// (`.claude/rules/grammar-dispatch.md` §5).
17800/// - **Perl** `/^#/` (`pattern_matcher`) and `m{^#}`
17801/// (`pattern_matcher_m`), the two spellings of a match against the
17802/// implicit `$_`. They are sibling rules, not aliases, so both are
17803/// covered — a fixture carrying only the first would have left the
17804/// `m{}` half at zero and read as covered. Since #1467 they take the
17805/// other route: a gated arm in `PerlCode::compute` counts them by use,
17806/// and they have left the former `perl_bool_terminal_kinds!()`.
17807/// - **Ruby** `a in Integer` (`test_pattern`), the one-line pattern
17808/// test. Decided against `match_pattern` (`expr => pat`), which
17809/// raises rather than yielding a boolean.
17810/// - **Objective-C** `@available(iOS 13.0, *)` and its
17811/// `__builtin_available` synonym (`available_expression`), the runtime
17812/// OS-version check. Not relational, but boolean by definition and
17813/// invisible to every comparison-token arm, so it measured short in
17814/// exactly the same way (#1457). Its entry landed in the *name*-keyed
17815/// former `cpp_bool_terminal_kinds!()` that C, C++ and Mozcpp shared, which is
17816/// why `the_c_family_grammars_do_not_emit_available_expression` below
17817/// pins the inertness the other three rely on.
17818///
17819/// Rust's `matches!(…)` measures short in the same way, and is
17820/// deliberately **not** here: its fix is `macro_invocation`, which also
17821/// catches `cfg!` / `dbg!` / `todo!`, and the one corpus snapshot it
17822/// moves is a `cfg!` rather than a `matches!`. That breadth is what
17823/// #1461 item 2 exists to decide, so it is left to that issue rather
17824/// than settled by inclusion here.
17825///
17826/// Two slots per language, because they are two independent walker
17827/// paths (§11) and every construct above measured short in **both**: the
17828/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
17829/// only one leaves the other untested.
17830///
17831/// Each row also carries the construct's negated spelling, which reaches
17832/// the construct only through the language's value peel: the `!…` /
17833/// `(…)` wrapper is peeled before `count_boolean_slot` asks
17834/// `<lang>_condition_scores_itself` whether the operand already paid.
17835/// Without a negated row the peel is the path no fixture here exercises.
17836///
17837/// Since #1526 no language here has a terminal-kind set: the chain
17838/// operands and the slots both score through the shared
17839/// `count_boolean_slot` rule, and Perl's rows score through the by-use
17840/// arm (#1467).
17841///
17842/// The `cyclomatic` half of `every_construct_scores_its_recorded_values`
17843/// is what rules out a regression that moved both metrics together:
17844/// cyclomatic must not move when only the operand spelling does, which
17845/// is what makes a `conditions` move unambiguously ABC's. The recorded
17846/// figures differ per slot (Rust's chain slot carries the extra `&&`
17847/// decision), so each slot pins its own pair rather than sharing one.
17848// Gated on the union of the three languages with rows, so a build
17849// enabling none of them drops the module instead of failing its
17850// guards (`.claude/rules/testing.md`, #1286 / #1411).
17851#[cfg(test)]
17852#[cfg(any(
17853 feature = "groovy",
17854 feature = "perl",
17855 feature = "ruby",
17856 feature = "objc"
17857))]
17858mod own_production_bool_constructs {
17859 use crate::test_support::metrics_verbatim;
17860 use crate::{LANG, MetricsOptions};
17861
17862 /// One fixture shape: a source template with a `{}` slot for the
17863 /// whole boolean expression, and the `abc.conditions_sum` /
17864 /// `cyclomatic_sum` every spelling in that slot must produce.
17865 type Slot = (&'static str, u64, u64);
17866
17867 /// A language's two slots, its identifier control, the constructs
17868 /// that must score the same, and how many of those there should be.
17869 ///
17870 /// The count is not bookkeeping. `for_each_case` counts *languages*,
17871 /// so trimming Groovy's list back to a single construct — or
17872 /// dropping Perl's `m{}` half, which is a separate grammar rule from
17873 /// its `//` half — leaves the module green with that coverage
17874 /// deleted. Pinning the length makes such a trim a deliberate
17875 /// two-line edit instead of a silent one.
17876 type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
17877
17878 fn conditions(lang: LANG, source: &str) -> u64 {
17879 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17880 .abc
17881 .conditions_sum()
17882 }
17883
17884 fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
17885 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17886 .cyclomatic
17887 .cyclomatic_sum()
17888 }
17889
17890 /// `([chain_slot, predicate_slot], identifier, constructs)` per
17891 /// language.
17892 ///
17893 /// `{}` is the left-hand operand of a two-operand short-circuit
17894 /// chain in the first slot and the whole `if` predicate in the
17895 /// second, so every fixture differs from its own control in exactly
17896 /// the construct under test.
17897 ///
17898 /// Ruby's chain slot parenthesises the slot because `in` binds
17899 /// looser than `&&` there; the control is parenthesised identically,
17900 /// so the parens cancel out of the comparison and the peel unwraps
17901 /// them for both sides alike.
17902 fn cases(lang: LANG) -> Option<Case> {
17903 Some(match lang {
17904 LANG::Groovy => (
17905 [
17906 ("def f(a, b, l, s) {\n return {} && b\n}\n", 2, 3),
17907 ("def f(a, b, l, s) {\n if ({}) { return 1 }\n}\n", 1, 3),
17908 ],
17909 "b",
17910 &[
17911 "a in l",
17912 "a !in l",
17913 "a === b",
17914 "a !== b",
17915 "s =~ /p/",
17916 "s ==~ /p/",
17917 "!(a in l)",
17918 // The indexing / navigation kinds added by #1466.
17919 // These two are the cyclomatic-neutral ones, so
17920 // they belong in this module's same-as-the-control
17921 // shape. The three *null-safe* spellings (`a?.b`,
17922 // `a??.b`, `l?[0]`) each short-circuit on a null
17923 // receiver and so add a cyclomatic decision, which
17924 // a row whose contract is "the control's
17925 // cyclomatic" cannot express; they get their own
17926 // test,
17927 // `groovy_safe_navigation_closes_the_two_below_gap`,
17928 // which anchors them on that axis explicitly.
17929 "l[0]",
17930 "a.@b",
17931 ],
17932 9,
17933 ),
17934 LANG::Perl => (
17935 [
17936 ("sub f {\n my $x = {} && $b;\n}\n", 2, 3),
17937 ("sub f {\n if ({}) { 1; }\n}\n", 1, 3),
17938 ],
17939 "$a",
17940 &["/^#/", "m{^#}", "!/^#/"],
17941 3,
17942 ),
17943 LANG::Ruby => (
17944 [
17945 ("def f(a)\n ({}) && b\nend\n", 2, 3),
17946 ("def f(a)\n if {}\n 1\n end\nend\n", 1, 3),
17947 ],
17948 "a",
17949 &["a in Integer", "!(a in Integer)"],
17950 2,
17951 ),
17952 // Objective-C's `@available` is not relational like the
17953 // constructs above, but it reaches the terminal set by the
17954 // route Groovy's `in` takes: a dedicated `available_expression`
17955 // production whose value is a boolean, seen by no
17956 // comparison-token arm (#1457). Both spellings are listed
17957 // because they are alternatives of one rule's leading token
17958 // rather than two rules — so unlike Perl's `m{}` above they
17959 // cannot drift apart, and the second row is here to keep that
17960 // claim measured rather than assumed.
17961 LANG::Objc => (
17962 [
17963 ("int f(int a) {\n return {} && b;\n}\n", 2, 3),
17964 ("int f() {\n if ({}) { return 1; }\n}\n", 1, 3),
17965 ],
17966 "a",
17967 &[
17968 "@available(iOS 13.0, *)",
17969 "__builtin_available(macOS 10.15, *)",
17970 "!@available(iOS 13.0, *)",
17971 ],
17972 3,
17973 ),
17974 _ => return None,
17975 })
17976 }
17977
17978 /// Runs `check` once per enabled language that has a case, having
17979 /// first established that the case can still assert something.
17980 ///
17981 /// The four guards mirror `numeric_bool_operands::for_each_case`,
17982 /// where each was added only after a measured perturbation of it
17983 /// left that module green: `checked > 0` for the runtime half of
17984 /// the feature gate, a non-empty construct list and its recorded
17985 /// count so a trimmed row cannot vanish silently, and the `{}` slot
17986 /// so `str::replace` does not degenerate into comparing a string
17987 /// with itself.
17988 fn for_each_case(check: impl Fn(LANG, Case)) {
17989 let mut checked = 0;
17990 for lang in LANG::into_enum_iter() {
17991 if !lang.is_enabled() {
17992 continue;
17993 }
17994 let Some(case @ (slots, _, constructs, expected_constructs)) = cases(lang) else {
17995 continue;
17996 };
17997 // Both halves are needed, and the second does not imply the
17998 // first. The length check alone only asserts that the list
17999 // and its recorded count *agree*, so emptying a row and
18000 // setting its count to 0 — the shape a careless "the test
18001 // failed, fix the number" edit takes — deleted that
18002 // language's coverage with both tests still green when
18003 // measured.
18004 assert!(
18005 !constructs.is_empty(),
18006 "{lang:?}: the construct list is empty; this language asserted nothing"
18007 );
18008 assert_eq!(
18009 constructs.len(),
18010 expected_constructs,
18011 "{lang:?}: the construct list no longer covers every own-production \
18012 boolean spelling this language was fixed for"
18013 );
18014 for (template, _, _) in slots {
18015 assert!(
18016 template.contains("{}"),
18017 "{lang:?}: template lost its `{{}}` slot: {template}"
18018 );
18019 }
18020 check(lang, case);
18021 checked += 1;
18022 }
18023 assert!(
18024 checked > 0,
18025 "no language with an own-production boolean construct enabled; \
18026 this test asserted nothing"
18027 );
18028 }
18029
18030 #[test]
18031 fn an_own_production_construct_scores_like_an_identifier() {
18032 for_each_case(|lang, (slots, identifier, constructs, _)| {
18033 for (template, _, _) in slots {
18034 let baseline = conditions(lang, &template.replace("{}", identifier));
18035 for construct in constructs {
18036 let source = template.replace("{}", construct);
18037 let scored = conditions(lang, &source);
18038 assert_eq!(
18039 scored, baseline,
18040 "{lang:?}: `{construct}` scored {scored} conditions against \
18041 `{identifier}`'s {baseline}\n source: {source}"
18042 );
18043 }
18044 }
18045 });
18046 }
18047
18048 /// Groovy's three null-safe spellings, on the cyclomatic axis.
18049 ///
18050 /// They cannot ride the rows above, whose contract is "scores the
18051 /// control's `conditions` *and* the control's `cyclomatic`":
18052 /// the boolean-slot rule (`groovy_count_condition`) does not move cyclomatic, but
18053 /// `?.` (`QMARKDOT`), `??.` (`QMARKQMARKDOT`) and `?[`
18054 /// (`QMARKLBRACK`) are already cyclomatic decisions in their own
18055 /// right (`src/metrics/cyclomatic/groovy.rs`), so each spelling
18056 /// scores the control's conditions against the control's cyclomatic
18057 /// **plus one**. That is the whole reason this family was the worst
18058 /// case in #1466: before the fix ABC sat *two* below its own
18059 /// decision count on `if (a?.b)`, against one below for every other
18060 /// spelling.
18061 ///
18062 /// `l?[0]` joined the family in #1471. It sat in the neutral rows
18063 /// above until then, passing only because Groovy cyclomatic had no
18064 /// `?[` arm — a fixture that would have rejected the fix for the
18065 /// very gap it recorded.
18066 ///
18067 /// Asserting the offset rather than the bare conditions is what
18068 /// makes this a §5 double-count guard as well. If a later change
18069 /// added `QMARKDOT` to `groovy_count_token_condition`'s arm while
18070 /// the wrapper stayed in the terminal set, `conditions` would go to
18071 /// `control + 1` and this test — not a snapshot — would say so.
18072 #[test]
18073 #[cfg(feature = "groovy")]
18074 fn groovy_safe_navigation_closes_the_two_below_gap() {
18075 for (template, control_conditions, control_cyclomatic) in [
18076 ("def f(a, b, l) {\n return {} && b\n}\n", 2, 3),
18077 ("def f(a, b, l) {\n if ({}) { return 1 }\n}\n", 1, 3),
18078 ] {
18079 let control = template.replace("{}", "b");
18080 assert_eq!(conditions(LANG::Groovy, &control), control_conditions);
18081 assert_eq!(cyclomatic_sum(LANG::Groovy, &control), control_cyclomatic);
18082
18083 for spelling in ["a?.b", "a??.b", "l?[0]"] {
18084 let source = template.replace("{}", spelling);
18085 assert_eq!(
18086 conditions(LANG::Groovy, &source),
18087 control_conditions,
18088 "`{spelling}` conditions\n source: {source}"
18089 );
18090 assert_eq!(
18091 cyclomatic_sum(LANG::Groovy, &source),
18092 control_cyclomatic + 1,
18093 "`{spelling}` cyclomatic_sum: the navigation operator's own \
18094 decision has moved\n source: {source}"
18095 );
18096 }
18097 }
18098 }
18099
18100 /// The `!` operand is read by field, so an `extra` cannot hide it.
18101 ///
18102 /// `groovy_wrapper_operand` took `child(1)` until #1466, which is
18103 /// the `!` operand only when nothing sits between the two. A comment
18104 /// is an `extra` and occupies that slot, so `if (! /*c*/ a)` scored
18105 /// zero conditions where `if (!a)` scored one — the positional-read
18106 /// class grammar-dispatch §3 is about, and the observable half of
18107 /// the peel rewrite.
18108 ///
18109 /// `parenthesized_expression` names nothing in node-types.json, so
18110 /// it kept the positional read, and the bug, until #1455 read it as
18111 /// the first operand that is not an extra.
18112 #[test]
18113 #[cfg(feature = "groovy")]
18114 fn groovy_negation_operand_survives_an_interposed_comment() {
18115 let template = "def f(a) {\n if ({}) { return 1 }\n}\n";
18116
18117 assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "!a")), 1);
18118 assert_eq!(
18119 conditions(LANG::Groovy, &template.replace("{}", "! /*c*/ a")),
18120 1,
18121 "the `!` operand is being read positionally again; a comment displaces it"
18122 );
18123
18124 assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "(a)")), 1);
18125 assert_eq!(
18126 conditions(LANG::Groovy, &template.replace("{}", "( /*c*/ a)")),
18127 1,
18128 "the parenthesised operand is being read positionally again; a comment \
18129 displaces it"
18130 );
18131 }
18132
18133 /// The arithmetic unary operators are values, not wrappers.
18134 ///
18135 /// `groovy_count_condition` routed every `unary_expression` to the
18136 /// peel while the peel handled only the `!` spelling, so the arm
18137 /// claimed `~a` / `-a` / `+a` and dropped them
18138 /// (grammar-dispatch §7). Since #1526 the slot pays for any
18139 /// predicate no other arm counts — Groovy truth makes `if (-a)` a
18140 /// decision — so the three spellings score what `a` does. Outside a
18141 /// slot they are no negation, so a call argument scores nothing for
18142 /// them, while `!-a` scores its negation once.
18143 #[test]
18144 #[cfg(feature = "groovy")]
18145 fn groovy_arithmetic_unary_is_a_value_not_a_wrapper() {
18146 let template = "def f(a) {\n if ({}) { return 1 }\n}\n";
18147 let call = "def f(a) {\n g({})\n}\n";
18148
18149 assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "a")), 1);
18150 for spelling in ["~a", "-a", "+a"] {
18151 assert_eq!(
18152 conditions(LANG::Groovy, &template.replace("{}", spelling)),
18153 1,
18154 "`{spelling}` in a slot pays as `a` does"
18155 );
18156 assert_eq!(
18157 conditions(LANG::Groovy, &call.replace("{}", spelling)),
18158 0,
18159 "`{spelling}` outside a slot is not a negation"
18160 );
18161 let negated = format!("!{spelling}");
18162 assert_eq!(
18163 conditions(LANG::Groovy, &call.replace("{}", &negated)),
18164 1,
18165 "`{negated}` outside a slot is one negation"
18166 );
18167 }
18168 }
18169
18170 /// The two Perl spellings must remain two distinct grammar kinds.
18171 ///
18172 /// The bare-match arm of `PerlCode::compute` lists `PatternMatcher`
18173 /// **and** `PatternMatcherM` on the stated grounds that `/^#/` and `m{^#}`
18174 /// are sibling rules rather than aliases of one. Nothing above pins
18175 /// that: if a grammar bump collapsed `m{}` onto `pattern_matcher`,
18176 /// both fixtures would keep scoring through the surviving entry,
18177 /// the `PatternMatcherM` arm would become dead code, and every test
18178 /// in this module would stay green — the silent-drift shape lesson
18179 /// 34 and grammar-dispatch §2 exist to catch.
18180 ///
18181 /// Asserting the ids differ is not enough on its own, since two
18182 /// distinct enum variants prove nothing about what the parser
18183 /// emits. So each spelling is parsed and required to produce its
18184 /// own kind and not the other's.
18185 #[test]
18186 #[cfg(feature = "perl")]
18187 fn the_two_perl_match_spellings_are_distinct_kinds() {
18188 use crate::{ParserTrait, Perl, PerlParser};
18189 use big_code_analysis_ast::test_support::ast_has_kind_id;
18190
18191 assert_ne!(
18192 Perl::PatternMatcher as u16,
18193 Perl::PatternMatcherM as u16,
18194 "the two spellings share one kind id; one bare-match arm entry is dead"
18195 );
18196
18197 for (src, present, absent) in [
18198 (
18199 "sub f { if (/^#/) { 1; } }",
18200 ("pattern_matcher", Perl::PatternMatcher as u16),
18201 ("pattern_matcher_m", Perl::PatternMatcherM as u16),
18202 ),
18203 (
18204 "sub f { if (m{^#}) { 1; } }",
18205 ("pattern_matcher_m", Perl::PatternMatcherM as u16),
18206 ("pattern_matcher", Perl::PatternMatcher as u16),
18207 ),
18208 ] {
18209 let parser = PerlParser::new(
18210 src.as_bytes().to_vec(),
18211 &std::path::PathBuf::from("f.pl"),
18212 None,
18213 );
18214 let (present_name, present_id) = present;
18215 let (absent_name, absent_id) = absent;
18216 assert!(
18217 ast_has_kind_id(&parser, present_id),
18218 "`{src}` no longer emits `{present_name}`"
18219 );
18220 assert!(
18221 !ast_has_kind_id(&parser, absent_id),
18222 "`{src}` now also emits `{absent_name}`; the two spellings have \
18223 collapsed and one bare-match arm entry is dead"
18224 );
18225 }
18226 }
18227
18228 /// The absolute anchor under the comparison above: every spelling
18229 /// must produce the slot's recorded `conditions`, and must leave
18230 /// `cyclomatic` alone.
18231 #[test]
18232 fn every_construct_scores_its_recorded_values() {
18233 for_each_case(|lang, (slots, identifier, constructs, _)| {
18234 for (template, expected_conditions, expected_cyclomatic) in slots {
18235 for spelling in std::iter::once(identifier).chain(constructs.iter().copied()) {
18236 let source = template.replace("{}", spelling);
18237 assert_eq!(
18238 conditions(lang, &source),
18239 expected_conditions,
18240 "{lang:?}: `{spelling}` conditions\n source: {source}"
18241 );
18242 assert_eq!(
18243 cyclomatic_sum(lang, &source),
18244 expected_cyclomatic,
18245 "{lang:?}: `{spelling}` cyclomatic_sum\n source: {source}"
18246 );
18247 }
18248 }
18249 });
18250 }
18251}
18252
18253/// A Perl statement modifier must score like the block form it is
18254/// shorthand for (#1464).
18255///
18256/// `return 1 if $x;` and `if ($x) { return 1; }` are the same decision
18257/// written two ways, and the modifier is the idiomatic Perl spelling —
18258/// `next unless $ok;`, `warn "..." if $debug;`. Perl's *cyclomatic*
18259/// dispatcher already counted all six modifier kinds, so this was a
18260/// straight ABC undercount against Perl's own decision count rather
18261/// than a policy disagreement between the two metrics.
18262///
18263/// Each pair is asserted equal **and** equal to a literal, so a
18264/// regression that zeroed both spellings still fails. The fixtures are
18265/// spell-anchored because the modifier is the only thing in them that
18266/// scores a condition: deleting `if $x` from the modifier fixture would
18267/// otherwise turn it into a silent copy of nothing at all
18268/// (`.claude/rules/testing.md`, "Perturb the fixture as well as the
18269/// production line").
18270#[cfg(test)]
18271#[cfg(feature = "perl")]
18272mod perl_statement_modifier_parity {
18273 use crate::test_support::{assert_perl_fixture_spells, metrics_verbatim};
18274 use crate::{LANG, MetricsOptions, Perl};
18275
18276 fn conditions(source: &str) -> u64 {
18277 metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default())
18278 .abc
18279 .conditions_sum()
18280 }
18281
18282 /// `(keyword, kind id, kind name)` — the four modifier keywords
18283 /// whose grammar slot is a `condition`. `for` / `foreach` is the
18284 /// fifth spelling and is covered by its own test below; `when` is
18285 /// the sixth and has no fixture at all, being unreachable from
18286 /// valid Perl (see `perl_walk_statement_modifier`).
18287 const CONDITION_MODIFIERS: [(&str, u16, &str); 4] = [
18288 ("if", Perl::IfSimpleStatement as u16, "if_simple_statement"),
18289 (
18290 "unless",
18291 Perl::UnlessSimpleStatement as u16,
18292 "unless_simple_statement",
18293 ),
18294 (
18295 "while",
18296 Perl::WhileSimpleStatement as u16,
18297 "while_simple_statement",
18298 ),
18299 (
18300 "until",
18301 Perl::UntilSimpleStatement as u16,
18302 "until_simple_statement",
18303 ),
18304 ];
18305
18306 #[test]
18307 fn a_modifier_scores_like_its_block_form() {
18308 assert!(
18309 LANG::Perl.is_enabled(),
18310 "compiled under `feature = \"perl\"` but `LANG::Perl` reports disabled; \
18311 this test asserted nothing"
18312 );
18313 for (keyword, kind, name) in CONDITION_MODIFIERS {
18314 let modifier = format!("sub f {{ my $x = shift; return 1 {keyword} $x; }}");
18315 let block = format!("sub f {{ my $x = shift; {keyword} ($x) {{ return 1; }} }}");
18316 assert_perl_fixture_spells(&modifier, &[(kind, 1, name)]);
18317 assert_eq!(
18318 conditions(&modifier),
18319 conditions(&block),
18320 "`{keyword}` modifier and block form disagree\n modifier: {modifier}\n block: {block}"
18321 );
18322 assert_eq!(
18323 conditions(&modifier),
18324 1,
18325 "`{keyword}` modifier: the bare scalar predicate is one condition"
18326 );
18327 }
18328 }
18329
18330 /// The modifier's `condition` field goes through the shared
18331 /// condition classifier, not a flat `+1`, so a compound predicate
18332 /// keeps its sub-structure — three shapes the block form already
18333 /// scores above and below one.
18334 #[test]
18335 fn a_modifier_condition_keeps_its_substructure() {
18336 for (predicate, expected) in [("!$x", 1), ("($x)", 1), ("$x && $x", 2), ("$x > 2", 1)] {
18337 let modifier = format!("sub f {{ my $x = shift; return 1 if {predicate}; }}");
18338 let block = format!("sub f {{ my $x = shift; if ({predicate}) {{ return 1; }} }}");
18339 assert_perl_fixture_spells(
18340 &modifier,
18341 &[(Perl::IfSimpleStatement as u16, 1, "if_simple_statement")],
18342 );
18343 assert_eq!(
18344 conditions(&modifier),
18345 expected,
18346 "`if {predicate}` modifier\n source: {modifier}"
18347 );
18348 assert_eq!(
18349 conditions(&block),
18350 expected,
18351 "`if ({predicate})` block form\n source: {block}"
18352 );
18353 }
18354 }
18355
18356 /// The `for` / `foreach` modifier iterates a list and is not a
18357 /// boolean test — the grammar names its slot `list`, not
18358 /// `condition`. Pinned against the `if` modifier so the zero reads
18359 /// as a decision rather than as an arm nothing reaches.
18360 ///
18361 /// Measured: simply *adding* `ForSimpleStatement` to the dispatch
18362 /// arm does not fail this test, because the walker reads the slot by
18363 /// field name and `for_simple_statement` exposes no `condition` —
18364 /// the exclusion is structural, not a listed omission. What it does
18365 /// catch is a rework that reaches the slot some other way (a child
18366 /// index, the `list` field) or a grammar that renames the field.
18367 #[test]
18368 fn the_for_modifier_is_not_a_condition() {
18369 for keyword in ["for", "foreach"] {
18370 let source = format!("sub f {{ my $x = shift; print 5 {keyword} @$x; }}");
18371 assert_perl_fixture_spells(
18372 &source,
18373 &[(Perl::ForSimpleStatement as u16, 1, "for_simple_statement")],
18374 );
18375 let abc =
18376 metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default()).abc;
18377 assert_eq!(abc.conditions_sum(), 0, "`{keyword}` modifier: {source}");
18378 // The expected value *is* the default, so the zero above
18379 // proves nothing on its own — a fixture that stopped
18380 // parsing would satisfy it too. `my $x = shift` is the
18381 // file's one assignment, and `shift` and `print 5` its two
18382 // branches; all three must survive.
18383 assert_eq!(
18384 abc.assignments_sum(),
18385 1,
18386 "`{keyword}`: fixture stopped scoring"
18387 );
18388 assert_eq!(
18389 abc.branches_sum(),
18390 2,
18391 "`{keyword}`: fixture stopped scoring"
18392 );
18393 }
18394 assert_eq!(
18395 conditions("sub f { my $x = shift; return 1 if $x; }"),
18396 1,
18397 "control: the `if` modifier still counts, so the zero above is a \
18398 policy decision and not a dead arm"
18399 );
18400 }
18401}
18402
18403/// A non-numeric literal in a boolean operand slot must score like an
18404/// identifier in the same slot (#1462).
18405///
18406/// The sibling module above pins the *numeric* half, closed by #1410.
18407/// The non-numeric literals were never swept and were missing from
18408/// every set the numerics were added to — same mechanism, same silent
18409/// zero, same file. Measured before the fix, every row below scored
18410/// exactly one condition short of its identifier control, in **both**
18411/// slots: `x || "default"` — the language's commonest truthy-default
18412/// idiom — reported 1 against `x || y`'s 2.
18413///
18414/// Scope is the eleven truthy-valued sets. #1462 shipped eight of them
18415/// and called that complete; a whole-branch review of the batch found
18416/// Perl, Ruby and Elixir in neither the swept list nor the
18417/// compile-error exemption, each already carrying `False` / `Nil`, and
18418/// each reproducing the gap exactly. They were swept on the same terms
18419/// and are rows here. Tcl and iRules are truthy too and needed nothing:
18420/// their `quoted_word` / `braced_word_simple` / `number` kinds already
18421/// cover every literal an `expr {…}` operand can hold, so they have no
18422/// row rather than a vacuous one.
18423///
18424/// C#, Java, Kotlin, Rust and Go name no literal kind at all and stay
18425/// that way: a bare literal in a boolean slot is a compile error there,
18426/// so there is nothing to count. The C family is integer-truthy, and
18427/// #1462 deferred it for its corpus exposure. #1526 then closed its gap
18428/// without naming a kind, by making every slot pay for an occupant no
18429/// other arm counts; its rows here (#1469) pin that answer, because a
18430/// kind `cpp_condition_scores_itself` wrongly claimed would drop a slot
18431/// exactly as the missing terminal kind once did. Its `true`, `false`
18432/// and `null` rows are the exception to "measured short": the former
18433/// set already named them, and they stay so the pin covers every kind.
18434///
18435/// Three things each row pins that a conditions comparison alone
18436/// cannot:
18437///
18438/// - **The `kind_id` each spelling parses to.** This is the whole bug
18439/// class (`.claude/rules/grammar-dispatch.md` §1): the grammars here
18440/// emit `string` under two ids in JavaScript, Mozjs and Tsx and under
18441/// three in Tsx counting the type keyword, and listing the wrong one
18442/// compiles, runs, and returns the unfixed number. Asserting the id
18443/// turns that measurement into a standing claim rather than a note in
18444/// an issue. It is also the fixture anchor `.claude/rules/testing.md`
18445/// asks for — editing `"s"` to `1` in a row now fails by name instead
18446/// of quietly turning that row into a copy of the numeric module.
18447/// - **`cyclomatic` does not move.** Only the operand spelling changes
18448/// between a row and its control, so a `conditions` difference is
18449/// unambiguously ABC's.
18450/// - **Both walker paths.** The sets feed two structurally independent
18451/// consumers (§11) — the operands of a `&&` / `and` chain, and the
18452/// predicate of an `if` — and every row measured short in both. A
18453/// fixture of only one leaves the other path invisible.
18454///
18455/// The JS-family rows go through `js_literals!` rather than four hand-
18456/// written arrays, because the four languages differ in exactly one
18457/// place — which `string` alias an operand slot carries — and making
18458/// that the macro's only argument states the asymmetry instead of
18459/// leaving a reader to diff four near-identical lists for it.
18460#[cfg(test)]
18461#[cfg(any(
18462 feature = "javascript",
18463 feature = "mozjs",
18464 feature = "typescript",
18465 feature = "python",
18466 feature = "lua",
18467 feature = "php",
18468 feature = "groovy",
18469 feature = "perl",
18470 feature = "ruby",
18471 feature = "elixir",
18472 feature = "c",
18473 feature = "cpp",
18474 feature = "mozcpp",
18475 feature = "objc"
18476))]
18477mod literal_bool_operands {
18478 use crate::test_support::{assert_fixture_spells, metrics_verbatim};
18479 use crate::{LANG, MetricsOptions};
18480
18481 /// One fixture shape: a source template with a `{}` operand slot,
18482 /// and the `abc.conditions_sum` / `cyclomatic_sum` every spelling of
18483 /// that operand must produce.
18484 type Slot = (&'static str, u64, u64);
18485
18486 /// One literal operand: its spelling, and the `kind_id` the grammar
18487 /// must emit for it.
18488 type Literal = (&'static str, u16);
18489
18490 /// A language's two slots, its identifier baseline operand, the
18491 /// literal operands that must score the same, and how many of those
18492 /// there should be.
18493 ///
18494 /// The count is not bookkeeping. `for_each_case` counts *languages*,
18495 /// so trimming a row's literal list — the pre-#1462 state is the
18496 /// empty list — would leave the whole module green.
18497 type Case = ([Slot; 2], &'static str, &'static [Literal], usize);
18498
18499 /// The seven non-numeric literal kinds of a JS-family grammar, with
18500 /// the `string` alias an operand slot carries passed in: `String2`
18501 /// for JavaScript, Mozjs and Tsx, `String` for TypeScript. That one
18502 /// argument is the entire difference between the four rows, and it
18503 /// is the same split `Checker::is_string` makes (§7).
18504 macro_rules! js_literals {
18505 ($Lang:ident, $string:ident) => {
18506 &[
18507 ("\"s\"", crate::$Lang::$string as u16),
18508 ("`t`", crate::$Lang::TemplateString as u16),
18509 ("/re/", crate::$Lang::Regex as u16),
18510 ("null", crate::$Lang::Null as u16),
18511 ("undefined", crate::$Lang::Undefined as u16),
18512 ("{}", crate::$Lang::Object as u16),
18513 ("[]", crate::$Lang::Array as u16),
18514 ]
18515 };
18516 }
18517
18518 const JS_SLOTS: [Slot; 2] = [
18519 ("function f(a) {\n return a && {};\n}\n", 2, 3),
18520 ("function f() {\n if ({}) { return 1; }\n}\n", 1, 3),
18521 ];
18522
18523 /// The five non-numeric literal kinds every C-family grammar shares,
18524 /// then the grammar's own. The string and null spellings are passed
18525 /// in because Objective-C writes its strings `@"s"` and C++ its null
18526 /// `nullptr`; each still parses to the shared `string_literal` /
18527 /// `null` kind, so the row stays one spelling per kind.
18528 macro_rules! c_family_literals {
18529 ($Lang:ident, $string:literal, $null:literal $(, $extra:expr)* $(,)?) => {
18530 &[
18531 ($string, crate::$Lang::StringLiteral as u16),
18532 ("\"a\" \"b\"", crate::$Lang::ConcatenatedString as u16),
18533 ("true", crate::$Lang::True as u16),
18534 ("false", crate::$Lang::False as u16),
18535 ($null, crate::$Lang::Null as u16),
18536 $($extra,)*
18537 ]
18538 };
18539 }
18540
18541 const C_FAMILY_SLOTS: [Slot; 2] = [
18542 ("int f(int a) {\n return a && {};\n}\n", 2, 3),
18543 ("int f() {\n if ({}) { return 1; }\n}\n", 1, 3),
18544 ];
18545
18546 fn conditions(lang: LANG, source: &str) -> u64 {
18547 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
18548 .abc
18549 .conditions_sum()
18550 }
18551
18552 fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
18553 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
18554 .cyclomatic
18555 .cyclomatic_sum()
18556 }
18557
18558 /// Asserts the fixture still spells the construct under test, by
18559 /// parsing it with the language's own parser. Split out of `cases`
18560 /// because only the parser types are feature-gated; the `kind_id`
18561 /// enums are compiled unconditionally.
18562 fn assert_spells(lang: LANG, source: &str, kinds: &[(u16, usize, &str)]) {
18563 match lang {
18564 #[cfg(feature = "javascript")]
18565 LANG::Javascript => {
18566 assert_fixture_spells::<crate::JavascriptParser>(source, "f.js", kinds);
18567 }
18568 #[cfg(feature = "mozjs")]
18569 LANG::Mozjs => assert_fixture_spells::<crate::MozjsParser>(source, "f.jsm", kinds),
18570 #[cfg(feature = "typescript")]
18571 LANG::Typescript => {
18572 assert_fixture_spells::<crate::TypescriptParser>(source, "f.ts", kinds);
18573 }
18574 #[cfg(feature = "typescript")]
18575 LANG::Tsx => assert_fixture_spells::<crate::TsxParser>(source, "f.tsx", kinds),
18576 #[cfg(feature = "python")]
18577 LANG::Python => assert_fixture_spells::<crate::PythonParser>(source, "f.py", kinds),
18578 #[cfg(feature = "lua")]
18579 LANG::Lua => assert_fixture_spells::<crate::LuaParser>(source, "f.lua", kinds),
18580 #[cfg(feature = "php")]
18581 LANG::Php => assert_fixture_spells::<crate::PhpParser>(source, "f.php", kinds),
18582 #[cfg(feature = "groovy")]
18583 LANG::Groovy => {
18584 assert_fixture_spells::<crate::GroovyParser>(source, "f.groovy", kinds);
18585 }
18586 #[cfg(feature = "perl")]
18587 LANG::Perl => assert_fixture_spells::<crate::PerlParser>(source, "f.pl", kinds),
18588 #[cfg(feature = "ruby")]
18589 LANG::Ruby => assert_fixture_spells::<crate::RubyParser>(source, "f.rb", kinds),
18590 #[cfg(feature = "elixir")]
18591 LANG::Elixir => assert_fixture_spells::<crate::ElixirParser>(source, "f.ex", kinds),
18592 #[cfg(feature = "c")]
18593 LANG::C => assert_fixture_spells::<crate::CParser>(source, "f.c", kinds),
18594 #[cfg(feature = "cpp")]
18595 LANG::Cpp => assert_fixture_spells::<crate::CppParser>(source, "f.cpp", kinds),
18596 #[cfg(feature = "mozcpp")]
18597 LANG::Mozcpp => assert_fixture_spells::<crate::MozcppParser>(source, "f.cpp", kinds),
18598 #[cfg(feature = "objc")]
18599 LANG::Objc => assert_fixture_spells::<crate::ObjcParser>(source, "f.m", kinds),
18600 other => panic!("{other:?} has a case row but no parser arm"),
18601 }
18602 }
18603
18604 /// `([chain_slot, condition_slot], identifier, literals)` per
18605 /// language, one spelling per literal *kind* the grammar emits.
18606 ///
18607 /// - JS family: `string`, `template_string`, `regex`, `null`,
18608 /// `undefined`, `object`, `array`. TypeScript's `string` / `object`
18609 /// type keywords (`a: string`) are different ids rendering to the
18610 /// same names and are deliberately not in the sets, so a row that
18611 /// accidentally named one would fail the id anchor here.
18612 /// - Python: `string` covers `'s'`, `"""s"""`, `f'x'` and `b'x'`,
18613 /// while the implicit-join `'a' 'b'` is the separate
18614 /// `concatenated_string` rule — the sibling-rule-under-a-supertype
18615 /// shape #1379's Perl numerals were, which an alias sweep cannot
18616 /// see. Then `none`, the four collection displays, and `ellipsis`.
18617 /// - Lua: one `string` kind for `"s"`, `'s'` and `[[s]]`, plus
18618 /// `table_constructor`. Lua's is the strongest case of the eight:
18619 /// everything but `false` and `nil` is truthy, which is why
18620 /// `cond and "a" or "b"` is the language's ternary.
18621 /// - PHP: `string` (single-quoted) and `encapsed_string`
18622 /// (interpolating) are separate rules, as are `heredoc` and
18623 /// `nowdoc`; then `array_creation_expression`, `null`, the
18624 /// backtick `shell_command_expression`, and `cast_expression` —
18625 /// the second finding of #1462, PHP being the only set in the
18626 /// Java / C# / Groovy / PHP group that named no cast kind.
18627 /// - Groovy: `string_literal` (which also covers the slashy `/re/`),
18628 /// `null_literal`, `list_literal`, `map_literal`.
18629 /// - Perl: the four string productions the grammar keeps separate
18630 /// (`'s'`, `q()`, `"s"`, `qq()`), the two command substitutions
18631 /// (`qx()`, backticks), the `qw()` / `[…]` / `{…}` collection
18632 /// literals, `qr//`, and `special_literal` (`__FILE__`).
18633 /// `heredoc_initializer` is the twelfth kind and cannot live in
18634 /// this table — a Perl heredoc body follows the *statement*, not
18635 /// the operand — so it has its own test below.
18636 /// - Ruby: `string` (covering `"s"`, `'s'`, `%q()`, `%Q()`),
18637 /// `chained_string` (the adjacent-literal join `"a" "b"`, a
18638 /// sibling rule an alias sweep cannot see), `subshell`, the four
18639 /// collection literals `array` / `hash` / `%w[]` / `%i[]`,
18640 /// `regex`, the one-character `?a`, and both symbol productions.
18641 /// `heredoc_beginning` is the twelfth and shares the Perl heredoc
18642 /// test for the same reason.
18643 /// - Elixir: `string` (one kind for `"s"` and the `"""` heredoc),
18644 /// `charlist`, `sigil` (one kind for `~r//`, `~s()`, `~w()`),
18645 /// `quoted_atom` — a separate production from `atom`, so
18646 /// `:"q a"` scored zero while `:atom` scored one — and the four
18647 /// collection literals `list` / `tuple` / `map` / `bitstring`.
18648 /// - C family: `string_literal` (one kind for `"s"`, `L"s"`, `u8"s"`
18649 /// and Objective-C's `@"s"`), `concatenated_string`, `true`,
18650 /// `false` and `null` (`NULL`, and C++'s `nullptr`, which the
18651 /// grammar wraps in a `null` node). C++ adds `raw_string_literal`
18652 /// and `user_defined_literal` (`"s"_x`, and `1.0_km` around a
18653 /// number); Objective-C adds its boxed forms — `array_literal`,
18654 /// `dictionary_literal`, `at_expression` (`@1`, `@YES`, `@(b)`,
18655 /// `@protocol(P)`), `selector_expression` and `encode_expression`.
18656 /// `char_literal` is numeric and pinned in the sibling module.
18657 /// Objective-C's `nil`, `YES` and `NO` parse as plain identifiers,
18658 /// so they need no row of their own.
18659 ///
18660 /// Shapes that measured short and are not rows here, none of them a
18661 /// literal: JavaScript's `this` and Groovy's
18662 /// `object_creation_expression` (recorded in #1462); Perl's
18663 /// `s///` and `tr///` (operations on `$_` evaluating to a count),
18664 /// `anonymous_function` and `array_dereference`; Ruby's `lambda`;
18665 /// and Elixir's `anonymous_function` and `&f/1` capture. Every one
18666 /// pays its slot since #1526 (Elixir's since #1527), and
18667 /// `slot_pays_tests` pins them against an identifier control (#1475).
18668 fn cases(lang: LANG) -> Option<Case> {
18669 Some(match lang {
18670 LANG::Javascript => (JS_SLOTS, "b", js_literals!(Javascript, String2), 7),
18671 LANG::Mozjs => (JS_SLOTS, "b", js_literals!(Mozjs, String2), 7),
18672 LANG::Typescript => (JS_SLOTS, "b", js_literals!(Typescript, String), 7),
18673 LANG::Tsx => (JS_SLOTS, "b", js_literals!(Tsx, String2), 7),
18674 LANG::Python => (
18675 [
18676 ("def f(a):\n return a and {}\n", 2, 3),
18677 ("def f():\n if {}:\n return 1\n", 1, 3),
18678 ],
18679 "b",
18680 &[
18681 ("'s'", crate::Python::String as u16),
18682 ("'a' 'b'", crate::Python::ConcatenatedString as u16),
18683 ("None", crate::Python::None as u16),
18684 ("[]", crate::Python::List as u16),
18685 ("{1}", crate::Python::Set as u16),
18686 ("()", crate::Python::Tuple as u16),
18687 ("{}", crate::Python::Dictionary as u16),
18688 ("...", crate::Python::Ellipsis as u16),
18689 ],
18690 8,
18691 ),
18692 LANG::Lua => (
18693 [
18694 ("function f(a)\n return a and {}\nend\n", 2, 3),
18695 ("function f()\n if {} then return 1 end\nend\n", 1, 3),
18696 ],
18697 "b",
18698 &[
18699 ("\"s\"", crate::Lua::String as u16),
18700 ("{}", crate::Lua::TableConstructor as u16),
18701 ],
18702 2,
18703 ),
18704 LANG::Php => (
18705 [
18706 ("<?php\nfunction f($a) {\n return $a && {};\n}\n", 2, 3),
18707 ("<?php\nfunction f() {\n if ({}) { return 1; }\n}\n", 1, 3),
18708 ],
18709 "$b",
18710 &[
18711 ("'s'", crate::Php::String as u16),
18712 ("\"x$b\"", crate::Php::EncapsedString as u16),
18713 ("<<<EOT\ns\nEOT", crate::Php::Heredoc as u16),
18714 ("<<<'EOT'\ns\nEOT", crate::Php::Nowdoc as u16),
18715 ("[]", crate::Php::ArrayCreationExpression as u16),
18716 ("null", crate::Php::Null as u16),
18717 ("`ls`", crate::Php::ShellCommandExpression as u16),
18718 ("(bool)$b", crate::Php::CastExpression as u16),
18719 ],
18720 8,
18721 ),
18722 LANG::Groovy => (
18723 [
18724 ("def f(a) {\n return a && {}\n}\n", 2, 3),
18725 ("def f() {\n if ({}) { return 1 }\n}\n", 1, 3),
18726 ],
18727 "b",
18728 &[
18729 ("\"s\"", crate::Groovy::StringLiteral as u16),
18730 ("null", crate::Groovy::NullLiteral as u16),
18731 ("[]", crate::Groovy::ListLiteral as u16),
18732 ("[:]", crate::Groovy::MapLiteral as u16),
18733 ],
18734 4,
18735 ),
18736 LANG::Perl => (
18737 [
18738 (
18739 "sub f {\n my ($a, $b) = @_;\n if ($a && {}) { print 1; }\n}\n",
18740 2,
18741 4,
18742 ),
18743 (
18744 "sub f {\n my ($a, $b) = @_;\n if ({}) { print 1; }\n}\n",
18745 1,
18746 3,
18747 ),
18748 ],
18749 "$b",
18750 &[
18751 ("'s'", crate::Perl::StringSingleQuoted as u16),
18752 ("q(s)", crate::Perl::StringQQuoted as u16),
18753 ("\"s\"", crate::Perl::StringDoubleQuoted as u16),
18754 ("qq(s)", crate::Perl::StringQqQuoted as u16),
18755 ("qx(ls)", crate::Perl::CommandQxQuoted as u16),
18756 ("`ls`", crate::Perl::BacktickQuoted as u16),
18757 ("qw(a b)", crate::Perl::WordListQw as u16),
18758 ("[1, 2]", crate::Perl::ArrayRef as u16),
18759 ("{ a => 1 }", crate::Perl::HashRef as u16),
18760 ("qr/re/", crate::Perl::RegexPatternQr as u16),
18761 ("__FILE__", crate::Perl::SpecialLiteral as u16),
18762 ],
18763 11,
18764 ),
18765 LANG::Ruby => (
18766 [
18767 ("def f(a, b)\n if a && {} then 1 else 0 end\nend\n", 3, 4),
18768 ("def f(a, b)\n if {} then 1 else 0 end\nend\n", 2, 3),
18769 ],
18770 "b",
18771 &[
18772 ("\"s\"", crate::Ruby::String as u16),
18773 ("\"a\" \"b\"", crate::Ruby::ChainedString as u16),
18774 ("`ls`", crate::Ruby::Subshell as u16),
18775 ("[1, 2]", crate::Ruby::Array as u16),
18776 ("{ a: 1 }", crate::Ruby::Hash as u16),
18777 ("%w[a b]", crate::Ruby::StringArray as u16),
18778 ("%i[a b]", crate::Ruby::SymbolArray as u16),
18779 ("/re/", crate::Ruby::Regex as u16),
18780 ("?a", crate::Ruby::Character as u16),
18781 (":sym", crate::Ruby::SimpleSymbol as u16),
18782 (":\"sym\"", crate::Ruby::DelimitedSymbol as u16),
18783 ],
18784 11,
18785 ),
18786 LANG::Elixir => (
18787 [
18788 (
18789 "defmodule M do\n def f(a, b) do\n if a && {} do\n a\n end\n end\nend\n",
18790 2,
18791 5,
18792 ),
18793 (
18794 "defmodule M do\n def f(a, b) do\n if {} do\n a\n end\n end\nend\n",
18795 1,
18796 4,
18797 ),
18798 ],
18799 "b",
18800 &[
18801 ("\"s\"", crate::Elixir::String as u16),
18802 ("'c'", crate::Elixir::Charlist as u16),
18803 ("~r/re/", crate::Elixir::Sigil as u16),
18804 (":\"q a\"", crate::Elixir::QuotedAtom as u16),
18805 ("[1, 2]", crate::Elixir::List as u16),
18806 ("{1, 2}", crate::Elixir::Tuple as u16),
18807 ("%{a: 1}", crate::Elixir::Map as u16),
18808 ("<<1>>", crate::Elixir::Bitstring as u16),
18809 ],
18810 8,
18811 ),
18812 LANG::C => (
18813 C_FAMILY_SLOTS,
18814 "b",
18815 c_family_literals!(C, "\"s\"", "NULL"),
18816 5,
18817 ),
18818 LANG::Cpp => (
18819 C_FAMILY_SLOTS,
18820 "b",
18821 c_family_literals!(
18822 Cpp,
18823 "\"s\"",
18824 "nullptr",
18825 ("R\"(x)\"", crate::Cpp::RawStringLiteral as u16),
18826 ("\"s\"_x", crate::Cpp::UserDefinedLiteral as u16),
18827 ),
18828 7,
18829 ),
18830 LANG::Mozcpp => (
18831 C_FAMILY_SLOTS,
18832 "b",
18833 c_family_literals!(
18834 Mozcpp,
18835 "\"s\"",
18836 "nullptr",
18837 ("R\"(x)\"", crate::Mozcpp::RawStringLiteral as u16),
18838 ("\"s\"_x", crate::Mozcpp::UserDefinedLiteral as u16),
18839 ),
18840 7,
18841 ),
18842 LANG::Objc => (
18843 C_FAMILY_SLOTS,
18844 "b",
18845 c_family_literals!(
18846 Objc,
18847 "@\"s\"",
18848 "NULL",
18849 ("@[]", crate::Objc::ArrayLiteral as u16),
18850 ("@{}", crate::Objc::DictionaryLiteral as u16),
18851 ("@1", crate::Objc::AtExpression as u16),
18852 ("@selector(x)", crate::Objc::SelectorExpression as u16),
18853 ("@encode(int)", crate::Objc::EncodeExpression as u16),
18854 ),
18855 10,
18856 ),
18857 _ => return None,
18858 })
18859 }
18860
18861 /// Runs `check` once per enabled language that has a case, having
18862 /// first established that the case can still assert something. The
18863 /// four guards are the sibling module's, for the same four ways
18864 /// this table could decay into asserting nothing: no language
18865 /// enabled, an emptied literal list, a list that no longer covers
18866 /// every kind, and a template that lost its `{}` slot (which makes
18867 /// every comparison `x == x`).
18868 fn for_each_case(check: impl Fn(LANG, Case)) {
18869 let mut checked = 0;
18870 for lang in LANG::into_enum_iter() {
18871 if !lang.is_enabled() {
18872 continue;
18873 }
18874 let Some(case @ (slots, _, literals, expected_kinds)) = cases(lang) else {
18875 continue;
18876 };
18877 // The length check does not imply this one: it asserts only
18878 // that the list and its recorded count *agree*, which an
18879 // emptied row with `expected_kinds` set to 0 satisfies —
18880 // the shape a "the test failed, fix the number" edit takes.
18881 // `checked` counts languages, so that row still increments
18882 // it and the non-vacuity guard below reads satisfied.
18883 assert!(
18884 !literals.is_empty(),
18885 "{lang:?}: the literal-operand list is empty; this language asserted nothing"
18886 );
18887 assert_eq!(
18888 literals.len(),
18889 expected_kinds,
18890 "{lang:?}: the literal-operand list no longer covers one spelling \
18891 per grammar literal kind"
18892 );
18893 for (template, _, _) in slots {
18894 assert!(
18895 template.contains("{}"),
18896 "{lang:?}: template lost its `{{}}` operand slot: {template}"
18897 );
18898 }
18899 check(lang, case);
18900 checked += 1;
18901 }
18902 assert!(
18903 checked > 0,
18904 "no truthy-valued language enabled; this test asserted nothing"
18905 );
18906 }
18907
18908 #[test]
18909 fn a_literal_operand_scores_like_an_identifier_operand() {
18910 for_each_case(|lang, (slots, identifier, literals, _)| {
18911 for (template, _, _) in slots {
18912 let baseline = conditions(lang, &template.replace("{}", identifier));
18913 for (literal, _) in literals {
18914 let source = template.replace("{}", literal);
18915 let scored = conditions(lang, &source);
18916 assert_eq!(
18917 scored, baseline,
18918 "{lang:?}: `{literal}` scored {scored} unary conditions against \
18919 `{identifier}`'s {baseline}\n source: {source}"
18920 );
18921 }
18922 }
18923 });
18924 }
18925
18926 /// The absolute anchor under the comparison above: every operand
18927 /// spelling must produce the slot's recorded `conditions`, and must
18928 /// leave `cyclomatic` alone.
18929 #[test]
18930 fn every_literal_operand_scores_its_recorded_values() {
18931 for_each_case(|lang, (slots, identifier, literals, _)| {
18932 for (template, expected_conditions, expected_cyclomatic) in slots {
18933 let operands = std::iter::once(identifier)
18934 .chain(literals.iter().map(|(spelling, _)| *spelling));
18935 for operand in operands {
18936 let source = template.replace("{}", operand);
18937 assert_eq!(
18938 conditions(lang, &source),
18939 expected_conditions,
18940 "{lang:?}: `{operand}` conditions\n source: {source}"
18941 );
18942 assert_eq!(
18943 cyclomatic_sum(lang, &source),
18944 expected_cyclomatic,
18945 "{lang:?}: `{operand}` cyclomatic_sum\n source: {source}"
18946 );
18947 }
18948 }
18949 });
18950 }
18951
18952 /// The twelfth Perl and Ruby literal kind, which the table above
18953 /// cannot hold: in both languages a heredoc body follows the
18954 /// *statement*, so the operand slot carries only the introducer and
18955 /// the rest of the literal lands on later lines. Substituting one
18956 /// into a single-line template produces an unterminated heredoc and
18957 /// an `ERROR` parse, not a measurement.
18958 ///
18959 /// That shape is also why exactly one kind is listed per language
18960 /// (§5): `heredoc_initializer` / `heredoc_beginning` is the node in
18961 /// the operand slot, while `heredoc_body_statement` /
18962 /// `heredoc_body` is a sibling the walker never reaches. Listing
18963 /// both would score one literal twice.
18964 #[cfg(any(feature = "perl", feature = "ruby"))]
18965 #[test]
18966 fn a_heredoc_operand_scores_like_an_identifier_operand() {
18967 #[cfg(feature = "perl")]
18968 {
18969 let heredoc = "sub f {\n my ($a, $b) = @_;\n if ($a && <<\"EOT\") { print 1; }\nhello\nEOT\n}\n";
18970 assert_spells(
18971 LANG::Perl,
18972 heredoc,
18973 &[(crate::Perl::HeredocInitializer as u16, 1, "<<\"EOT\"")],
18974 );
18975 assert_eq!(
18976 conditions(LANG::Perl, heredoc),
18977 conditions(
18978 LANG::Perl,
18979 "sub f {\n my ($a, $b) = @_;\n if ($a && $b) { print 1; }\n}\n"
18980 ),
18981 "Perl heredoc operand"
18982 );
18983 }
18984 #[cfg(feature = "ruby")]
18985 {
18986 let heredoc =
18987 "def f(a, b)\n if a && <<~TXT then 1 else 0 end\n hello\n TXT\nend\n";
18988 assert_spells(
18989 LANG::Ruby,
18990 heredoc,
18991 &[(crate::Ruby::HeredocBeginning as u16, 1, "<<~TXT")],
18992 );
18993 assert_eq!(
18994 conditions(LANG::Ruby, heredoc),
18995 conditions(
18996 LANG::Ruby,
18997 "def f(a, b)\n if a && b then 1 else 0 end\nend\n"
18998 ),
18999 "Ruby heredoc operand"
19000 );
19001 }
19002 }
19003
19004 /// Every spelling must parse to the `kind_id` its row names — the
19005 /// §1 alias claim, which is the one thing a conditions comparison
19006 /// cannot check: a row naming the wrong alias of a multi-id kind
19007 /// would simply keep reporting the unfixed number.
19008 #[test]
19009 fn every_literal_spelling_parses_to_the_kind_its_row_names() {
19010 for_each_case(|lang, (slots, _, literals, _)| {
19011 for (template, _, _) in slots {
19012 for (literal, kind) in literals {
19013 let source = template.replace("{}", literal);
19014 assert_spells(lang, &source, &[(*kind, 1, literal)]);
19015 }
19016 }
19017 });
19018 }
19019}
19020
19021/// The condition slot and the wrapper peel agree on which kinds are
19022/// wrappers, in each language whose slot used to restate the list
19023/// (#1470).
19024///
19025/// `<lang>_count_condition` decided which slot expressions to hand the
19026/// peel, and the former `<lang>_inspect_container` which it could descend.
19027/// Spelled as two lists they drifted three times — Kotlin (#1459),
19028/// Groovy (#1466) and C# (#1463) — each time scoring a valid predicate
19029/// zero. The six languages here now ask `<lang>_wrapper_operand`
19030/// instead, which removed the duplication without moving a number: the
19031/// wrapper matrix measured identical before and after, so a revert
19032/// cannot fail these tests. What they pin is the *answer* both halves
19033/// must keep giving:
19034///
19035/// - **Routed.** Every wrapper the peel accepts scores its slot once.
19036/// A slot that stops asking the peel scores these zero.
19037/// - **Declined.** A unary the peel does not accept (`-b`, or a valid
19038/// non-wrapper such as `b & b`) stops the peel: a slot pays for it
19039/// once (#1526; Go's terminal-kind slot still scores `-b` nothing),
19040/// and outside a slot it scores nothing, so a peel that starts
19041/// accepting arithmetic operators by accident is caught here rather
19042/// than in a corpus snapshot.
19043/// - **Proves boolean.** A negation counts its operand outside any
19044/// boolean slot and a parenthesis does not — the second half of the
19045/// peel's answer, which the slot never sees.
19046///
19047/// Each template's `@` is the operand under test.
19048#[cfg(test)]
19049#[cfg(any(
19050 feature = "rust",
19051 feature = "go",
19052 feature = "java",
19053 feature = "python",
19054 feature = "ruby",
19055 feature = "lua"
19056))]
19057mod wrapper_peel_routing {
19058 use crate::test_support::metrics_verbatim;
19059 use crate::{LANG, MetricsOptions};
19060
19061 fn conditions(lang: LANG, source: &str) -> u64 {
19062 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
19063 .abc
19064 .conditions_sum()
19065 }
19066
19067 /// Asserts `conditions` for every `(operand, expected)` row of one
19068 /// template.
19069 fn assert_operands(lang: LANG, template: &str, rows: &[(&str, u64)]) {
19070 assert!(template.contains('@'), "template has no operand slot");
19071 for &(operand, expected) in rows {
19072 let source = template.replace('@', operand);
19073 assert_eq!(
19074 conditions(lang, &source),
19075 expected,
19076 "`{operand}` in {lang:?}\n source: {source}"
19077 );
19078 }
19079 }
19080
19081 /// Rust's slot pays for any predicate no other arm counted (#1526),
19082 /// so the declined half reads the valid non-wrapper `b & b`: the
19083 /// peel stops on it, the slot pays for it, and the call argument
19084 /// below does not.
19085 #[test]
19086 #[cfg(feature = "rust")]
19087 fn rust_condition_slot_routes_what_the_peel_accepts() {
19088 assert_operands(
19089 LANG::Rust,
19090 "fn f(b: bool) { if @ {} }\n",
19091 &[
19092 ("b", 1),
19093 ("(b)", 1),
19094 ("!b", 1),
19095 ("!(b)", 1),
19096 ("b & b", 1),
19097 ("(b & b)", 1),
19098 ("!(b & b)", 1),
19099 ],
19100 );
19101 assert_operands(
19102 LANG::Rust,
19103 "fn f(b: bool) { g(@); }\n",
19104 &[("!b", 1), ("(b)", 0), ("b & b", 0), ("!(b & b)", 1)],
19105 );
19106 }
19107
19108 #[test]
19109 #[cfg(feature = "go")]
19110 fn go_condition_slot_routes_what_the_peel_accepts() {
19111 assert_operands(
19112 LANG::Go,
19113 "package p\nfunc f(b bool) { if @ {} }\n",
19114 &[
19115 ("b", 1),
19116 ("(b)", 1),
19117 ("!b", 1),
19118 ("!(b)", 1),
19119 ("-b", 0),
19120 ("(-b)", 0),
19121 ],
19122 );
19123 assert_operands(
19124 LANG::Go,
19125 "package p\nfunc f(b bool) { g(@) }\n",
19126 &[("!b", 1), ("(b)", 0), ("-b", 0)],
19127 );
19128 }
19129
19130 /// The ternary condition is a slot like the `if` one; the `?` itself
19131 /// is the base 1. Java's slot pays for any predicate no other arm
19132 /// counted (#1526), so the declined half reads the valid non-wrapper
19133 /// `b & b`: the peel stops on it, the slot pays for it, and the call
19134 /// argument below does not.
19135 #[test]
19136 #[cfg(feature = "java")]
19137 fn java_condition_slot_routes_what_the_peel_accepts() {
19138 assert_operands(
19139 LANG::Java,
19140 "class A { int f(boolean b) { return @ ? 1 : 2; } }\n",
19141 &[
19142 ("b", 2),
19143 ("(b)", 2),
19144 ("!b", 2),
19145 ("!(b)", 2),
19146 ("b & b", 2),
19147 ("(b & b)", 2),
19148 ],
19149 );
19150 assert_operands(
19151 LANG::Java,
19152 "class A { void f(boolean b) { g(@); } }\n",
19153 &[("!b", 1), ("(b)", 0), ("b & b", 0), ("!(b & b)", 1)],
19154 );
19155 }
19156
19157 /// Python peels parentheses and the walrus only: `not` is counted by
19158 /// its own `NotOperator` arm, so `python_wrapper_operand` carries no
19159 /// proves-boolean flag and there is no outside-a-slot half to pin.
19160 /// Python is truthy-valued, so the slot pays for any predicate no
19161 /// other arm counted, `-b` included (#1526), as Ruby's does.
19162 #[test]
19163 #[cfg(feature = "python")]
19164 fn python_condition_slot_routes_what_the_peel_accepts() {
19165 assert_operands(
19166 LANG::Python,
19167 "def f(b):\n if @:\n pass\n",
19168 &[("b", 1), ("(b)", 1), ("((b))", 1), ("-b", 1), ("(-b)", 1)],
19169 );
19170 }
19171
19172 /// Ruby has no counted call-argument slot, so the outside-a-slot
19173 /// half reads a ternary *branch*, which is type-free: only a
19174 /// negation there proves its operand boolean. That branch is also
19175 /// where the declined half lives for Ruby: its condition slot pays
19176 /// for any predicate no other arm counted, `-b` included (#1520), so
19177 /// `if -b` scores 1 rather than 0.
19178 #[test]
19179 #[cfg(feature = "ruby")]
19180 fn ruby_condition_slot_routes_what_the_peel_accepts() {
19181 assert_operands(
19182 LANG::Ruby,
19183 "def f(b)\n if @ then 1 end\nend\n",
19184 &[
19185 ("b", 1),
19186 ("(b)", 1),
19187 ("!b", 1),
19188 ("not b", 1),
19189 ("!(b)", 1),
19190 ("-b", 1),
19191 ("(-b)", 1),
19192 ],
19193 );
19194 assert_operands(
19195 LANG::Ruby,
19196 "def f(b, c)\n x = c ? 1 : @\nend\n",
19197 &[
19198 ("!b", 3),
19199 ("not b", 3),
19200 ("(b)", 2),
19201 ("-b", 2),
19202 // A negated branch is a slot whatever it negates (#1529).
19203 ("!self", 3),
19204 ("not -b", 3),
19205 ("!(b > 1)", 3),
19206 ],
19207 );
19208 // A statement sequence evaluates to its last statement, so that is
19209 // the operand the slot reads: `(b; b > 1)` is the comparison,
19210 // already counted by its own `>`, and `(b > 1; b)` is the bare `b`,
19211 // which the slot counts beside that `>`. Reading the first
19212 // statement inverted both. The subject-less `when` goes through
19213 // the same peel.
19214 let seqs: &[(&str, u64)] = &[("(b; b > 1)", 1), ("(b > 1; b)", 2)];
19215 assert_operands(LANG::Ruby, "def f(b)\n if @ then 1 end\nend\n", seqs);
19216 assert_operands(
19217 LANG::Ruby,
19218 "def f(b)\n case\n when @ then 1\n end\nend\n",
19219 seqs,
19220 );
19221 }
19222
19223 /// A subject-less `when` whose pattern is a chain adds nothing of its
19224 /// own: the Rule 9 walker scores every operand as a slot, so each
19225 /// chain already pays at least one (#1529). Before that the walker
19226 /// skipped an operand outside its terminal list, the clause had to
19227 /// pay for `-a && -b` (review of #1453), and a chain scored as many
19228 /// conditions as it had *listed* operands plus one at most. Each row
19229 /// now scores what its plain-identifier twin (`a && b`, `a || b ||
19230 /// c`) does. An assignment operand is its value (#1520), so
19231 /// `(a = 1) && (b = 2)` is `1 && 2`.
19232 #[test]
19233 #[cfg(feature = "ruby")]
19234 fn ruby_subjectless_when_chain_pays_per_operand() {
19235 assert_operands(
19236 LANG::Ruby,
19237 "def f(a, b, c)\n case\n when @ then 1\n end\nend\n",
19238 &[
19239 ("a", 1),
19240 ("-a", 1),
19241 ("a && b", 2),
19242 ("-a && -b", 2),
19243 ("(a = 1) && (b = 2)", 2),
19244 ("a || b || c", 3),
19245 ("-a || -b || -c", 3),
19246 ("-a && b", 2),
19247 ("a > 1 && b", 2),
19248 ("a > 1 && -b", 2),
19249 ("a && (b || c)", 3),
19250 ("-a && (-b || c)", 3),
19251 ],
19252 );
19253 }
19254
19255 /// #1529: an operand of a Ruby `&&` / `||` / `and` / `or` chain is
19256 /// scored by the rule #1520 gave every condition slot — one, unless
19257 /// another arm already counts it — wherever the chain is written.
19258 /// The walker used to pay only for an operand that peeled down to a
19259 /// fixed list of terminal kinds, so every spelling in `UNLISTED`
19260 /// scored one below its plain-identifier twin `b` in every position
19261 /// below. `CONTROLS` must not move: `!b` and `(b)` already scored
19262 /// their twin, and a comparison, a pattern test, or a negation of one
19263 /// is counted by its own arm, so paying the operand as well would
19264 /// count that decision twice.
19265 #[test]
19266 #[cfg(feature = "ruby")]
19267 fn ruby_chain_operand_pays_what_no_other_arm_counted() {
19268 const UNLISTED: &[&str] = &[
19269 "self",
19270 "Foo::Bar",
19271 "-b",
19272 "(-b)",
19273 "defined?(b)",
19274 "b + 1",
19275 "!self",
19276 "not self",
19277 "(y ||= b)",
19278 "(y = self)",
19279 "begin self end",
19280 ];
19281 const CONTROLS: &[&str] = &["!b", "(b)", "c > 1", "!(c > 1)", "(c in Integer)"];
19282 // (template, conditions with `b` in the `@` operand)
19283 let positions: &[(&str, u64)] = &[
19284 ("def f(a, b, c)\n g if a && @\nend\n", 2),
19285 ("def f(a, b, c)\n g if (a && @)\nend\n", 2),
19286 ("def f(a, b, c)\n x = a && @\nend\n", 2),
19287 ("def f(a, b, c)\n return a || @\nend\n", 2),
19288 ("def f(a, b, c)\n h(@ && a)\nend\n", 2),
19289 ("def f(a, b, c)\n x = (a and @)\nend\n", 2),
19290 ("def f(a, b, c)\n x = (a or @)\nend\n", 2),
19291 ("def f(a, b, c)\n y ||= a && @\nend\n", 2),
19292 ("def f(a, b, c)\n y &&= a || @\nend\n", 2),
19293 ("def f(a, b, c)\n x = a && (c || @)\nend\n", 3),
19294 // A comment is a named child of the `binary` too, and must not
19295 // pay as a third operand.
19296 ("def f(a, b, c)\n x = a && # c\n @\nend\n", 2),
19297 ("def f(a, b, c)\n while a && @ do g end\nend\n", 2),
19298 (
19299 "def f(a, b, c)\n if a then 1 elsif a && @ then 2 end\nend\n",
19300 4,
19301 ),
19302 (
19303 "def f(a, b, c)\n case\n when a && @ then 1\n end\nend\n",
19304 2,
19305 ),
19306 ];
19307 for &(template, twin) in positions {
19308 // The twin is measured, not only assumed, so a template that
19309 // stopped parsing as a chain fails here rather than agreeing
19310 // with every row at some other value.
19311 assert_operands(LANG::Ruby, template, &[("b", twin)]);
19312 let rows: Vec<_> = UNLISTED
19313 .iter()
19314 .chain(CONTROLS)
19315 .map(|&operand| (operand, twin))
19316 .collect();
19317 assert_operands(LANG::Ruby, template, &rows);
19318 }
19319 }
19320
19321 #[test]
19322 #[cfg(feature = "lua")]
19323 fn lua_condition_slot_routes_what_the_peel_accepts() {
19324 // Lua is truthy-valued, so the slot pays for any predicate no
19325 // other arm counted, `-b` and `#b` included (#1526).
19326 assert_operands(
19327 LANG::Lua,
19328 "function f(b)\n if @ then end\nend\n",
19329 &[
19330 ("b", 1),
19331 ("(b)", 1),
19332 ("not b", 1),
19333 ("not (b)", 1),
19334 ("-b", 1),
19335 ("#b", 1),
19336 ("(-b)", 1),
19337 ],
19338 );
19339 assert_operands(
19340 LANG::Lua,
19341 "function f(b)\n g(@)\nend\n",
19342 &[("not b", 1), ("(b)", 0), ("-b", 0)],
19343 );
19344 }
19345}
19346
19347/// A comment inside a condition or value slot changes nothing (#1455).
19348///
19349/// Every walker read its slots by fixed child index, and every peel
19350/// stepped into a `(…)` / negation with `child(1)` or the first named
19351/// child. tree-sitter counts a comment among a node's children and
19352/// marks it *named*, so `if (/*c*/ b)` handed the comment to the slot,
19353/// which declined it, and the condition scored zero. Each pair below is
19354/// one spelling with and without a comment; the two are asserted equal
19355/// to each other *and* to the expected literal, so a regression that
19356/// moved both sides alike — to zero, say — still fails.
19357///
19358/// The forms, per slot and per comment spelling of the language: a
19359/// comment before the operand, inside a nested parenthesis, before a
19360/// negation, between a negation and its operand, and — where the
19361/// language's syntax puts parentheses around the slot — before those
19362/// parentheses. Value slots (`return`, declarator, assignment, lambda
19363/// body) take the negated forms only, since a bare operand there is no
19364/// condition, and only the first comment spelling: a line comment after
19365/// `return` ends the statement in Go.
19366#[cfg(test)]
19367#[cfg(any(
19368 feature = "c",
19369 feature = "cpp",
19370 feature = "csharp",
19371 feature = "elixir",
19372 feature = "go",
19373 feature = "groovy",
19374 feature = "java",
19375 feature = "javascript",
19376 feature = "kotlin",
19377 feature = "lua",
19378 feature = "mozcpp",
19379 feature = "mozjs",
19380 feature = "objc",
19381 feature = "perl",
19382 feature = "php",
19383 feature = "python",
19384 feature = "ruby",
19385 feature = "rust",
19386 feature = "typescript"
19387))]
19388mod slot_comment_invariance {
19389 use crate::test_support::metrics_verbatim;
19390 use crate::{LANG, MetricsOptions};
19391
19392 fn conditions(lang: LANG, source: &str) -> u64 {
19393 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
19394 .abc
19395 .conditions_sum()
19396 }
19397
19398 /// How one language spells the slots under test.
19399 struct Spelling {
19400 /// Comment spellings, each carrying the whitespace — a newline
19401 /// for a line comment — the operand after it needs.
19402 comments: &'static [&'static str],
19403 operand: &'static str,
19404 /// `None` where the peel does not descend the negation: Python
19405 /// counts `not` through its own arm, and tree-sitter-python
19406 /// fails to parse the valid `(not # c⏎ b)` at the pinned grammar.
19407 negation: Option<&'static str>,
19408 /// The slot is written inside parentheses: either the syntax
19409 /// requires them (`if (…)`), or a line comment needs them to
19410 /// stay inside the expression (Python, Ruby, Elixir).
19411 parenthesised: bool,
19412 /// The parentheses belong to the slot's syntax, so a comment
19413 /// may also sit before them (`if /*c*/ (b)`).
19414 syntactic_parens: bool,
19415 /// `(template, expected)`; `@` marks a condition slot.
19416 conditions: &'static [(&'static str, u64)],
19417 /// Templates whose `@` is a value slot; a negation there scores 1.
19418 values: &'static [&'static str],
19419 }
19420
19421 const C_COMMENTS: &[&str] = &["/*c*/ ", "//c\n "];
19422
19423 const C_FAMILY: Spelling = Spelling {
19424 comments: C_COMMENTS,
19425 operand: "b",
19426 negation: Some("!"),
19427 parenthesised: true,
19428 syntactic_parens: true,
19429 conditions: &[
19430 ("int f(){ if @ { x(); } }", 1),
19431 ("int f(){ while @ { x(); } }", 1),
19432 ("int f(){ do { x(); } while @; }", 1),
19433 ],
19434 values: &["int f(){ return @; }"],
19435 };
19436
19437 const JS_FAMILY: Spelling = Spelling {
19438 comments: C_COMMENTS,
19439 operand: "b",
19440 negation: Some("!"),
19441 parenthesised: true,
19442 syntactic_parens: true,
19443 conditions: &[
19444 ("function f(){ if @ { x(); } }", 1),
19445 ("function f(){ while @ { x(); } }", 1),
19446 ("function f(){ do { x(); } while @; }", 1),
19447 ],
19448 values: &["function f(){ return @; }"],
19449 };
19450
19451 fn spelling(lang: LANG) -> Option<Spelling> {
19452 Some(match lang {
19453 LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc => C_FAMILY,
19454 LANG::Javascript | LANG::Mozjs | LANG::Typescript | LANG::Tsx => JS_FAMILY,
19455 LANG::Java => Spelling {
19456 conditions: &[
19457 ("class K{ void f(){ if @ { x(); } } }", 1),
19458 ("class K{ void f(){ while @ { x(); } } }", 1),
19459 ("class K{ void f(){ do { x(); } while @; } }", 1),
19460 ],
19461 values: &[
19462 "class K{ boolean f(){ return @; } }",
19463 "class K{ void f(){ boolean y = @; } }",
19464 "class K{ void f(){ y = @; } }",
19465 "class K{ void f(){ Supplier<Boolean> s = () -> @; } }",
19466 ],
19467 ..C_FAMILY
19468 },
19469 LANG::Csharp => Spelling {
19470 conditions: &[
19471 ("class K{ void f(){ if @ { x(); } } }", 1),
19472 ("class K{ void f(){ while @ { x(); } } }", 1),
19473 ("class K{ void f(){ do { x(); } while @; } }", 1),
19474 ],
19475 values: &[
19476 "class K{ bool f(){ return @; } }",
19477 "class K{ void f(){ var y = @; } }",
19478 "class K{ void f(){ y = @; } }",
19479 "class K{ void f(){ Func<bool> s = () => @; } }",
19480 ],
19481 ..C_FAMILY
19482 },
19483 LANG::Groovy => Spelling {
19484 conditions: &[
19485 ("def f(){ if @ { x() } }", 1),
19486 ("def f(){ while @ { x() } }", 1),
19487 ("def f(){ do { x() } while @ }", 1),
19488 ],
19489 values: &[
19490 "def f(){ return @ }",
19491 "def f(){ def y = @ }",
19492 "def f(){ y = @ }",
19493 ],
19494 ..C_FAMILY
19495 },
19496 LANG::Php => Spelling {
19497 comments: &["/*c*/ ", "//c\n ", "#c\n "],
19498 operand: "$b",
19499 conditions: &[
19500 ("<?php function f(){ if @ { x(); } }", 1),
19501 ("<?php function f(){ while @ { x(); } }", 1),
19502 ("<?php function f(){ do { x(); } while @; }", 1),
19503 ],
19504 values: &["<?php function f(){ return @; }"],
19505 ..C_FAMILY
19506 },
19507 LANG::Kotlin => Spelling {
19508 conditions: &[
19509 ("fun f(){ if @ { x() } }", 1),
19510 ("fun f(){ while @ { x() } }", 1),
19511 ("fun f(){ do { x() } while @ }", 1),
19512 ],
19513 values: &[],
19514 ..C_FAMILY
19515 },
19516 LANG::Perl => Spelling {
19517 comments: &["# c\n "],
19518 operand: "$b",
19519 conditions: &[
19520 ("sub f { if @ { x(); } }", 1),
19521 ("sub f { unless @ { x(); } }", 1),
19522 ("sub f { while @ { x(); } }", 1),
19523 ("sub f { until @ { x(); } }", 1),
19524 ],
19525 values: &["sub f { return @; }"],
19526 ..C_FAMILY
19527 },
19528 LANG::Rust => Spelling {
19529 parenthesised: false,
19530 syntactic_parens: false,
19531 conditions: &[
19532 ("fn f(){ if @ { x(); } }", 1),
19533 ("fn f(){ while @ { x(); } }", 1),
19534 ],
19535 values: &["fn f() -> bool { return @; }"],
19536 ..C_FAMILY
19537 },
19538 LANG::Go => Spelling {
19539 parenthesised: false,
19540 syntactic_parens: false,
19541 conditions: &[
19542 ("package p\nfunc f(){ if @ { x() } }", 1),
19543 ("package p\nfunc f(){ for @ { x() } }", 1),
19544 ],
19545 values: &["package p\nfunc f() bool { return @ }"],
19546 ..C_FAMILY
19547 },
19548 LANG::Lua => Spelling {
19549 comments: &["--[[c]] ", "--c\n "],
19550 negation: Some("not "),
19551 parenthesised: false,
19552 syntactic_parens: false,
19553 conditions: &[
19554 ("function f() if @ then x() end end", 1),
19555 ("function f() while @ do x() end end", 1),
19556 ("function f() repeat x() until @ end", 1),
19557 ],
19558 values: &["function f() return @ end"],
19559 ..C_FAMILY
19560 },
19561 LANG::Python => Spelling {
19562 comments: &["# c\n "],
19563 negation: None,
19564 syntactic_parens: false,
19565 conditions: &[
19566 ("def f():\n if @:\n x()\n", 1),
19567 ("def f():\n while @:\n x()\n", 1),
19568 ],
19569 values: &[],
19570 ..C_FAMILY
19571 },
19572 LANG::Ruby => Spelling {
19573 comments: &["# c\n "],
19574 syntactic_parens: false,
19575 conditions: &[
19576 ("def f\n if @\n x\n end\nend\n", 1),
19577 ("def f\n while @\n x\n end\nend\n", 1),
19578 ("def f\n case\n when @\n x\n end\nend\n", 1),
19579 ],
19580 values: &[],
19581 ..C_FAMILY
19582 },
19583 // Elixir's `if` slot scored its operand whatever the peel
19584 // returned, so the defect showed only where the peel decides:
19585 // an operand of a `&&` chain, which counts each operand.
19586 LANG::Elixir => Spelling {
19587 comments: &["# c\n "],
19588 syntactic_parens: false,
19589 conditions: &[
19590 (
19591 "defmodule M do\n def f do\n if @ do\n x()\n end\n end\nend\n",
19592 1,
19593 ),
19594 ("defmodule M do\n def f do\n y = a && @\n end\nend\n", 2),
19595 ],
19596 values: &[],
19597 ..C_FAMILY
19598 },
19599 _ => return None,
19600 })
19601 }
19602
19603 /// `(without, with, expected)` for every form of every slot.
19604 fn cases(s: &Spelling) -> Vec<(String, String, u64)> {
19605 let slot = |x: String| {
19606 if s.parenthesised { format!("({x})") } else { x }
19607 };
19608 let b = s.operand;
19609 let mut out = Vec::new();
19610 for &(template, expected) in s.conditions {
19611 let fill = |x: String| template.replace('@', &x);
19612 for c in s.comments {
19613 let mut push = |plain: String, commented: String| {
19614 out.push((fill(plain), fill(commented), expected));
19615 };
19616 push(slot(b.into()), slot(format!("{c}{b}")));
19617 push(slot(format!("({b})")), slot(format!("({c}{b})")));
19618 if let Some(n) = s.negation {
19619 push(slot(format!("{n}{b}")), slot(format!("{c}{n}{b}")));
19620 push(slot(format!("{n}{b}")), slot(format!("{n}{c}{b}")));
19621 }
19622 if s.syntactic_parens {
19623 push(slot(b.into()), format!("{c}{}", slot(b.into())));
19624 }
19625 }
19626 }
19627 if let (Some(n), Some(c)) = (s.negation, s.comments.first()) {
19628 for template in s.values {
19629 let fill = |x: String| template.replace('@', &x);
19630 out.push((fill(format!("{n}{b}")), fill(format!("{c}{n}{b}")), 1));
19631 out.push((fill(format!("{n}{b}")), fill(format!("{n}{c}{b}")), 1));
19632 }
19633 }
19634 out
19635 }
19636
19637 #[test]
19638 fn a_comment_in_a_slot_changes_nothing() {
19639 let mut checked = 0;
19640 for lang in LANG::into_enum_iter().filter(LANG::is_enabled) {
19641 let Some(spelling) = spelling(lang) else {
19642 continue;
19643 };
19644 for (plain, commented, expected) in cases(&spelling) {
19645 let without = conditions(lang, &plain);
19646 assert_eq!(
19647 conditions(lang, &commented),
19648 without,
19649 "{lang:?}: a comment changed the ABC conditions\n \
19650 without: {plain}\n with: {commented}"
19651 );
19652 assert_eq!(without, expected, "{lang:?}: {plain}");
19653 checked += 1;
19654 }
19655 }
19656 assert!(
19657 checked > 0,
19658 "no slot language enabled; this test asserted nothing"
19659 );
19660 }
19661}
19662
19663/// Slot occupants that no fixed index or first-named-child read finds
19664/// (#1455): a preamble before the slot, a comment *after* the operand
19665/// of a last-operand read, and the non-condition reads that were
19666/// positional too. Each row pairs a spelling with its plain twin.
19667#[cfg(test)]
19668#[cfg(any(
19669 feature = "cpp",
19670 feature = "elixir",
19671 feature = "mozcpp",
19672 feature = "perl",
19673 feature = "ruby",
19674 feature = "rust"
19675))]
19676mod slot_role_reads {
19677 use crate::test_support::metrics_verbatim;
19678 use crate::{LANG, MetricsOptions};
19679
19680 /// `(conditions, assignments)`.
19681 fn abc(lang: LANG, source: &str) -> (u64, u64) {
19682 let abc = metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default()).abc;
19683 (abc.conditions_sum(), abc.assignments_sum())
19684 }
19685
19686 /// `(language, plain, variant, (conditions, assignments))`.
19687 const ROWS: &[(LANG, &str, &str, (u64, u64))] = &[
19688 // A C++ `condition_clause` holds its condition in the `value`
19689 // field; an init-statement sits at child(1) before it.
19690 (
19691 LANG::Cpp,
19692 "int f(){ int y = g(); if (b) { x(); } }",
19693 "int f(){ if (int y = g(); b) { x(); } }",
19694 (1, 1),
19695 ),
19696 (
19697 LANG::Mozcpp,
19698 "int f(){ int y = g(); if (b) { x(); } }",
19699 "int f(){ if (int y = g(); b) { x(); } }",
19700 (1, 1),
19701 ),
19702 (
19703 LANG::Cpp,
19704 "int f(){ if constexpr (b) { x(); } }",
19705 "int f(){ if constexpr (/*c*/ b) { x(); } }",
19706 (1, 0),
19707 ),
19708 // A loop label is the `while` expression's first child.
19709 (
19710 LANG::Rust,
19711 "fn f(){ while b { x(); } }",
19712 "fn f(){ 'a: while b { x(); } }",
19713 (1, 0),
19714 ),
19715 // Perl's `(…)` evaluates to its last element, so the peel takes
19716 // the last operand — never a trailing comment.
19717 (
19718 LANG::Perl,
19719 "sub f { if ($b) { x(); } }",
19720 "sub f { if ($b # c\n) { x(); } }",
19721 (1, 0),
19722 ),
19723 (
19724 LANG::Perl,
19725 "sub f { x() if $b; }",
19726 "sub f { x() if $b # c\n; }",
19727 (1, 0),
19728 ),
19729 // `split`'s delimiter is its first *operand*; a comment before it
19730 // made the pattern read as an ordinary match.
19731 (
19732 LANG::Perl,
19733 "sub f { my @a = split(/,/, $s); }",
19734 "sub f { my @a = split( # sep\n /,/, $s); }",
19735 (0, 1),
19736 ),
19737 // A subject-less `when` scores its operand once; the comment
19738 // made the peel return it, so `x > 1` counted twice.
19739 (
19740 LANG::Ruby,
19741 "def f\n case\n when (x > 1)\n y\n end\nend\n",
19742 "def f\n case\n when ( # c\n x > 1)\n y\n end\nend\n",
19743 (1, 0),
19744 ),
19745 // Elixir's match operator, read at child(1), lost to a comment
19746 // before the `=` — an assignment rather than a condition.
19747 (
19748 LANG::Elixir,
19749 "defmodule M do\n def f do\n a = 1\n end\nend\n",
19750 "defmodule M do\n def f do\n a # c\n = 1\n end\nend\n",
19751 (0, 1),
19752 ),
19753 ];
19754
19755 #[test]
19756 fn a_slot_is_read_by_role() {
19757 let mut checked = 0;
19758 for &(lang, plain, variant, expected) in ROWS.iter().filter(|row| row.0.is_enabled()) {
19759 let without = abc(lang, plain);
19760 assert_eq!(
19761 abc(lang, variant),
19762 without,
19763 "{lang:?}: the variant scored differently\n plain: {plain}\n variant: {variant}"
19764 );
19765 assert_eq!(without, expected, "{lang:?}: {plain}");
19766 checked += 1;
19767 }
19768 assert!(
19769 checked > 0,
19770 "no row language enabled; this test asserted nothing"
19771 );
19772 }
19773}