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: impl 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// Fitzpatrick Rule 9 for a chain whose grammar names its two operands
531// `left` and `right`: each is scored by `score`, the language's
532// `count_boolean_slot` or `count_negated_operand` wrapper — a closure
533// where it needs the source bytes (Elixir's, #1574). Read by field rather
534// than as every operand child, so a comment beside the operator — a
535// named child too — does not pay. The caller owns the gate deciding
536// that `chain` is a chain at all.
537pub(super) fn count_field_operands(
538 chain: &Node,
539 score: impl Fn(&Node, &mut f64),
540 conditions: &mut f64,
541) {
542 for field in ["left", "right"] {
543 if let Some(operand) = chain.child_by_field_name(field) {
544 score(&operand, conditions);
545 }
546 }
547}
548
549// Each operand child of `list` — an argument list, a `return` value
550// list, a chain whose grammar names no operand field — scored by
551// `score`. `is_operand` keeps punctuation and comments out.
552pub(super) fn count_each_operand(
553 list: &Node,
554 score: impl Fn(&Node, &mut f64),
555 conditions: &mut f64,
556) {
557 for operand in list.children().filter(is_operand) {
558 score(&operand, conditions);
559 }
560}
561
562// Default no-op `Abc` impls. Audited in #188; the matrix below
563// records the rationale for every entry so the no-op default is a
564// deliberate choice, not scaffolding leftover.
565//
566// Real defaults (the language has no construct ABC measures, so the
567// metric is genuinely 0):
568// - PreprocCode, CcommentCode: no executable code (comments /
569// preprocessor lines only).
570implement_metric_trait!(Abc, PreprocCode, CcommentCode);
571
572#[cfg(test)]
573#[allow(
574 clippy::float_cmp,
575 clippy::cast_precision_loss,
576 clippy::cast_possible_truncation,
577 clippy::cast_sign_loss,
578 clippy::similar_names,
579 clippy::doc_markdown,
580 clippy::needless_raw_string_hashes,
581 clippy::too_many_lines
582)]
583mod tests {
584 #[cfg(feature = "csharp")]
585 use crate::test_support::assert_csharp_fixture_spells;
586 use crate::test_support::{
587 assert_fixture_spells, ast_has_kind_id, check_func_space_only_shim,
588 check_metrics_only_shim, child_space, metrics_verbatim,
589 };
590 // Hand-written, not derived. A trait is used through its methods, so
591 // nothing in a test body names `ParserTrait` and
592 // `check-test-lang-gates.py` derives no gate for an import at all,
593 // so an ungated one stays compiled into builds that never call
594 // `<Lang>Parser::new`. These seven are the languages that do; a new
595 // `XParser::new` test in this module needs its feature added here,
596 // and the leg that lacks it says so as an unused import.
597 #[cfg(any(
598 feature = "csharp",
599 feature = "groovy",
600 feature = "kotlin",
601 feature = "php",
602 feature = "ruby",
603 feature = "rust",
604 feature = "typescript",
605 ))]
606 use crate::traits::ParserTrait;
607
608 use super::*;
609
610 check_metrics_only_shim!(check_metrics, Abc);
611 // Every `check_func_space` caller in this module is an ABC-versus-
612 // cyclomatic parity test (`abc.conditions() == cyclomatic() - 1` on
613 // the same space), so the func-space shim carries Cyclomatic too.
614 check_func_space_only_shim!(check_func_space, Abc, Cyclomatic);
615
616 /// `abc.conditions_sum()` of `src` under a walk restricted to ABC
617 /// and the metrics it resolves, for the cross-language parity tests
618 /// that compare one construct across sibling grammars.
619 /// `check_metrics` expands to a plain `fn` and so cannot close over
620 /// a reference value; this can. Restricted rather than
621 /// `MetricsOptions::default()` per `metrics_verbatim`'s own doc
622 /// (#1127).
623 #[cfg(any(
624 feature = "c",
625 feature = "cpp",
626 feature = "csharp",
627 feature = "go",
628 feature = "java",
629 feature = "javascript",
630 feature = "mozcpp",
631 feature = "mozjs",
632 feature = "objc",
633 feature = "perl",
634 feature = "typescript",
635 ))]
636 fn abc_conditions(lang: LANG, src: &str) -> u64 {
637 metrics_verbatim(
638 lang,
639 src.as_bytes(),
640 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
641 )
642 .abc
643 .conditions_sum()
644 }
645
646 // Recurse to the innermost function space and assert the invariant
647 // named above there. `conditions()` is that one space's own count,
648 // so it pairs with `cyclomatic()` and never `cyclomatic_sum()`,
649 // which folds in a base of 1 per nested space.
650 #[cfg(any(
651 feature = "cpp",
652 feature = "csharp",
653 feature = "groovy",
654 feature = "java",
655 feature = "perl",
656 feature = "php",
657 feature = "typescript",
658 ))]
659 fn assert_deepest_conditions_match_cyclomatic(space: &crate::FuncSpace, expected: u64) {
660 let mut deepest = space;
661 while let Some(child) = deepest.spaces.last() {
662 deepest = child;
663 }
664 let decisions = deepest.metrics.cyclomatic.cyclomatic() - 1;
665 assert_eq!(decisions, expected);
666 assert_eq!(deepest.metrics.abc.conditions(), decisions);
667 }
668
669 // The sibling form of the helper above, for a fixture whose point is
670 // that several *different* spellings of one construct agree. The
671 // deepest-space walk cannot serve those: it follows `spaces.last()`
672 // and so inspects exactly one member, which is the shape #1383's
673 // over-count hid behind — a merged root total cannot tell a
674 // relational method scoring 2 from the constant control scoring 2.
675 #[cfg(any(feature = "csharp", feature = "elixir"))]
676 fn assert_every_member_scores(
677 container: &crate::FuncSpace,
678 members: usize,
679 expected: u64,
680 why: &str,
681 ) {
682 // The count is a parameter rather than a `!is_empty()` check so
683 // a caller cannot omit it: without it a fixture that lost every
684 // member but one still satisfies "every member scores N".
685 assert_eq!(
686 container.spaces.len(),
687 members,
688 "member count changed — the fixture moved, not the metric"
689 );
690 for member in &container.spaces {
691 let name = member.name.as_deref().unwrap_or("?");
692 assert_eq!(member.metrics.abc.conditions(), expected, "{name}: {why}");
693 assert_eq!(
694 member.metrics.abc.conditions(),
695 member.metrics.cyclomatic.cyclomatic() - 1,
696 "{name}: §8 parity with the cyclomatic decision count"
697 );
698 }
699 }
700
701 // The second sibling, for a fixture whose point is that a construct
702 // moves a member *off* the value its unguarded control scores.
703 // `assert_every_member_scores` cannot serve those: it takes one
704 // expected value for the whole container, and every #1422 guard
705 // fixture deliberately pairs guarded members with an unguarded
706 // control one lower — which is the comparison the test exists to
707 // make. Both tiers per member, because a guard is a decision in each
708 // and a fix that moved only one of them would satisfy neither claim
709 // on its own, and because a member may legitimately sit *above* its
710 // decision count — an `else` arm, or a comparison nested inside
711 // another comparison, is an ABC condition with no cyclomatic
712 // decision behind it (#1421).
713 #[cfg(any(
714 feature = "csharp",
715 feature = "elixir",
716 feature = "go",
717 feature = "groovy",
718 feature = "java",
719 feature = "kotlin",
720 feature = "perl",
721 feature = "python",
722 feature = "ruby",
723 feature = "rust",
724 ))]
725 #[track_caller]
726 fn assert_members_score(container: &crate::FuncSpace, expected: &[(&str, u64, u64)]) {
727 assert_eq!(
728 container.spaces.len(),
729 expected.len(),
730 "member count changed — the fixture moved, not the metric"
731 );
732 for (name, conditions, cyclomatic) in expected {
733 let member = child_space(container, name);
734 assert_eq!(
735 member.metrics.abc.conditions(),
736 *conditions,
737 "{name}: abc.conditions"
738 );
739 assert_eq!(
740 member.metrics.cyclomatic.cyclomatic(),
741 *cyclomatic,
742 "{name}: cyclomatic"
743 );
744 }
745 }
746
747 /// Regression for #227: a `Stats::default()` that never sees an
748 /// observation must not leak the `f64::MAX` sentinel for
749 /// `assignments_min`, `branches_min`, or `conditions_min`. All
750 /// three getters collapse the sentinel to `0.0` so JSON never
751 /// emits `1.7976931e308`.
752 #[test]
753 fn abc_empty_file_min_is_zero() {
754 let stats = Stats::default();
755 assert_eq!(stats.assignments_min(), 0);
756 assert_eq!(stats.branches_min(), 0);
757 assert_eq!(stats.conditions_min(), 0);
758 }
759
760 // The `EQ` arm of `java_count_token_assignment`: a plain `=` counts
761 // unless `java_eq_initializes_final_binding` finds it initialising a
762 // `final` binding, whether the declaration is a local or a field.
763 #[cfg(feature = "java")]
764 #[test]
765 fn java_eq_arm_counts_outside_final_declarations() {
766 check_metrics::<JavaParser>(
767 "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
768 "foo.java",
769 |metric| {
770 // `int x = 0;` is not `final`, so it counts like each
771 // `x = N;` that follows.
772 assert_eq!(metric.abc.assignments_sum(), 4);
773 },
774 );
775 }
776
777 #[cfg(feature = "java")]
778 #[test]
779 fn java_eq_arm_skips_final_initializers() {
780 check_metrics::<JavaParser>(
781 "class A {
782 final int X = 1;
783 @Deprecated private final int Y = 2;
784 void m() { final int Z = 3; }
785 }",
786 "foo.java",
787 |metric| {
788 // All three `=` tokens are `final` initializers — the
789 // second behind an annotation and an access modifier in
790 // the same `modifiers` node — so assignments are 0
791 // across all spaces.
792 assert_eq!(metric.abc.assignments_sum(), 0);
793 },
794 );
795 }
796
797 #[cfg(feature = "java")]
798 #[test]
799 fn java_final_initializer_does_not_suppress_the_assignments_inside_it() {
800 // The sentinel stack the structural predicate replaced stayed
801 // live from `final` to the next `;`, so every `=` nested in the
802 // initializer — a lambda body's, an array initializer's — was
803 // suppressed with the declarator's own. Java lambdas open no
804 // space, so nothing else separated them. Each row names the
805 // pre-fix value.
806 let cases = [
807 ("void m() { final Runnable r = () -> { x = 1; }; }", 1, 0),
808 (
809 "void m() { final Runnable r = () -> { x = 1; y = 2; }; }",
810 2,
811 1,
812 ),
813 ("void m() { final Runnable r = () -> x = 1; }", 1, 0),
814 ("void m() { final int[] a = { x = 1 }; }", 1, 0),
815 ("private final Runnable f = () -> { x = 1; };", 1, 0),
816 // The `;` that closed the leak is the reference: the same
817 // body after a `final` local counts as it always did.
818 ("void m() { final int q = 1; x = 1; }", 1, 1),
819 ];
820 let mut ran = 0;
821 for (body, expected, before) in cases {
822 let src = format!("class K {{ int x, y; {body} }}\n");
823 let assignments = metrics_verbatim(
824 LANG::Java,
825 src.as_bytes(),
826 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
827 )
828 .abc
829 .assignments_sum();
830 assert_eq!(assignments, expected, "`{body}` (pre-fix {before})");
831 ran += 1;
832 }
833 assert_eq!(ran, cases.len());
834 assert!(cases.iter().any(|&(_, now, before)| now != before));
835 }
836
837 // Constant declarations are not counted as assignments
838 #[cfg(feature = "java")]
839 #[test]
840 fn java_constant_declarations() {
841 check_metrics::<JavaParser>(
842 "class A {
843 private final int X1 = 0, Y1 = 0;
844 public final float PI = 3.14f;
845 final static String HELLO = \"Hello,\";
846 protected String world = \" world!\"; // +1a
847 public float e = 2.718f; // +1a
848 private int x2 = 1, y2 = 2; // +2a
849
850 void m() {
851 final int Z1 = 0, Z2 = 0, Z3 = 0;
852 final float T = 0.0f;
853 int z1 = 1, z2 = 2, z3 = 3; // +3a
854 float t = 60.0f; // +1a
855 }
856 }",
857 "foo.java",
858 |metric| {
859 // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
860 // space count: 3 (1 unit, 1 class and 1 method)
861 insta::assert_json_snapshot!(
862 metric.abc,
863 @r#"
864 {
865 "assignments": 8,
866 "branches": 0,
867 "conditions": 0,
868 "magnitude": 8.0,
869 "value": 0.0,
870 "assignments_average": 2.6666666666666665,
871 "branches_average": 0.0,
872 "conditions_average": 0.0,
873 "assignments_min": 0,
874 "assignments_max": 4,
875 "branches_min": 0,
876 "branches_max": 0,
877 "conditions_min": 0,
878 "conditions_max": 0
879 }
880 "#
881 );
882 },
883 );
884 }
885
886 // "In computer science, conditionals (that is, conditional statements, conditional expressions
887 // and conditional constructs,) are programming language commands for handling decisions."
888 // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
889 // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
890 // Variables, method invocations and true or false values used inside
891 // variable declarations and assignment expressions are not counted as conditions
892 #[cfg(feature = "java")]
893 #[test]
894 fn java_declarations_with_conditions() {
895 check_metrics::<JavaParser>(
896 "
897 boolean a = (1 > 2); // +1a +1c
898 boolean b = 3 > 4; // +1a +1c
899 boolean c = (1 > 2) && 3 > 4; // +1a +2c
900 boolean d = b && (x > 5) || c; // +1a +3c
901 boolean e = !d; // +1a +1c
902 boolean f = ((!false)); // +1a +1c
903 boolean g = !(!(true)); // +1a +1c
904 boolean h = true; // +1a
905 boolean i = (false); // +1a
906 boolean j = (((((true))))); // +1a
907 boolean k = (((((m()))))); // +1a +1b
908 boolean l = (((((!m()))))); // +1a +1b +1c
909 boolean m = (!(!((m())))); // +1a +1b +1c
910 List<String> n = null; // +1a (< and > used for generic types are not counted as conditions)
911 ",
912 "foo.java",
913 |metric| {
914 // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
915 // space count: 1 (1 unit)
916 insta::assert_json_snapshot!(
917 metric.abc,
918 @r#"
919 {
920 "assignments": 14,
921 "branches": 3,
922 "conditions": 12,
923 "magnitude": 18.681541692269406,
924 "value": 18.681541692269406,
925 "assignments_average": 14.0,
926 "branches_average": 3.0,
927 "conditions_average": 12.0,
928 "assignments_min": 14,
929 "assignments_max": 14,
930 "branches_min": 3,
931 "branches_max": 3,
932 "conditions_min": 12,
933 "conditions_max": 12
934 }
935 "#
936 );
937 },
938 );
939 }
940
941 // Conditions can be found in assignment expressions
942 #[cfg(feature = "java")]
943 #[test]
944 fn java_assignments_with_conditions() {
945 check_metrics::<JavaParser>(
946 "
947 a = 2 < 1; // +1a +1c
948 b = (4 >= 3) && 2 <= 1; // +1a +2c
949 c = a || (x != 10) && b; // +1a +3c
950 d = !false; // +1a +1c
951 e = (!false); // +1a +1c
952 f = !(false); // +1a +1c
953 g = (!(((true)))); // +1a +1c
954 h = ((true)); // +1a
955 i = !m(); // +1a +1b +1c
956 j = !((m())); // +1a +1b +1c
957 k = (!(m())); // +1a +1b +1c
958 l = ((!(m()))); // +1a +1b +1c
959 m = !B.<Integer>m(2); // +1a +1b +1c
960 n = !((B.<Integer>m(4))); // +1a +1b +1c
961 ",
962 "foo.java",
963 |metric| {
964 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
965 // space count: 1 (1 unit)
966 insta::assert_json_snapshot!(
967 metric.abc,
968 @r#"
969 {
970 "assignments": 14,
971 "branches": 6,
972 "conditions": 16,
973 "magnitude": 22.090722034374522,
974 "value": 22.090722034374522,
975 "assignments_average": 14.0,
976 "branches_average": 6.0,
977 "conditions_average": 16.0,
978 "assignments_min": 14,
979 "assignments_max": 14,
980 "branches_min": 6,
981 "branches_max": 6,
982 "conditions_min": 16,
983 "conditions_max": 16
984 }
985 "#
986 );
987 },
988 );
989 }
990
991 // Conditions can be found in method arguments
992 #[cfg(feature = "java")]
993 #[test]
994 fn java_methods_arguments_with_conditions() {
995 check_metrics::<JavaParser>(
996 "
997 m1(a); // +1b
998 m2(a, b); // +1b
999 m3(true, (false), (((true)))); // +1b
1000 m3(m1(false), m1(true), m1(false)); // +4b
1001 m1(!a); // +1b +1c
1002 m2((((a))), (!b)); // +1b +1c
1003 m3(!(a), b, !!!c); // +1b +2c
1004 m3(a, !b, m2(!a, !m2(!b, !m1(!c)))); // +4b +6c
1005 ",
1006 "foo.java",
1007 |metric| {
1008 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
1009 // space count: 1 (1 unit)
1010 insta::assert_json_snapshot!(
1011 metric.abc,
1012 @r#"
1013 {
1014 "assignments": 0,
1015 "branches": 14,
1016 "conditions": 10,
1017 "magnitude": 17.204650534085253,
1018 "value": 17.204650534085253,
1019 "assignments_average": 0.0,
1020 "branches_average": 14.0,
1021 "conditions_average": 10.0,
1022 "assignments_min": 0,
1023 "assignments_max": 0,
1024 "branches_min": 14,
1025 "branches_max": 14,
1026 "conditions_min": 10,
1027 "conditions_max": 10
1028 }
1029 "#
1030 );
1031 },
1032 );
1033 }
1034
1035 // "A unary conditional expression is an implicit condition that uses no relational operators."
1036 // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
1037 // https://www.softwarerenovation.com/Articles.aspx (page 5)
1038 #[cfg(feature = "java")]
1039 #[test]
1040 fn java_if_single_conditions() {
1041 check_metrics::<JavaParser>(
1042 "
1043 if ( a < 0 ) {} // +1c
1044 if ( ((a != 0)) ) {} // +1c
1045 if ( !(a > 0) ) {} // +1c
1046 if ( !(((a == 0))) ) {} // +1c
1047 if ( b.m1() ) {} // +1b +1c
1048 if ( !b.m1() ) {} // +1b +1c
1049 if ( !!b.m2() ) {} // +1b +1c
1050 if ( (!(b.m1())) ) {} // +1b +1c
1051 if ( (!(!b.m1())) ) {} // +1b +1c
1052 if ( ((b.m2())) ) {} // +1b +1c
1053 if ( ((b.m().m1())) ) {} // +2b +1c
1054 if ( c ) {} // +1c
1055 if ( !c ) {} // +1c
1056 if ( !!!!!!!!!!c ) {} // +1c
1057 if ( (((c))) ) {} // +1c
1058 if ( (((!c))) ) {} // +1c
1059 if ( ((!(c))) ) {} // +1c
1060 if ( true ) {} // +1c
1061 if ( !true ) {} // +1c
1062 if ( ((false)) ) {} // +1c
1063 if ( !(!(false)) ) {} // +1c
1064 if ( !!!false ) {} // +1c
1065 ",
1066 "foo.java",
1067 |metric| {
1068 // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
1069 // space count: 1 (1 unit)
1070 insta::assert_json_snapshot!(
1071 metric.abc,
1072 @r#"
1073 {
1074 "assignments": 0,
1075 "branches": 8,
1076 "conditions": 22,
1077 "magnitude": 23.40939982143925,
1078 "value": 23.40939982143925,
1079 "assignments_average": 0.0,
1080 "branches_average": 8.0,
1081 "conditions_average": 22.0,
1082 "assignments_min": 0,
1083 "assignments_max": 0,
1084 "branches_min": 8,
1085 "branches_max": 8,
1086 "conditions_min": 22,
1087 "conditions_max": 22
1088 }
1089 "#
1090 );
1091 },
1092 );
1093 }
1094
1095 #[cfg(feature = "java")]
1096 #[test]
1097 fn java_if_multiple_conditions() {
1098 check_metrics::<JavaParser>(
1099 "
1100 if ( a || b || c || d ) {} // +4c
1101 if ( a || b && c && d ) {} // +4c
1102 if ( x < y && a == b ) {} // +2c
1103 if ( ((z < (x + y))) ) {} // +1c
1104 if ( a || ((((b))) && c) ) {} // +3c
1105 if ( a && ((((a == b))) && c) ) {} // +3c
1106 if ( a || ((((a == b))) || ((c))) ) {} // +3c
1107 if ( x < y && B.m() ) {} // +1b +2c
1108 if ( x < y && !(((B.m()))) ) {} // +1b +2c
1109 if ( !(x < y) && !B.m() ) {} // +1b +2c
1110 if ( !!!(!!!(a)) && B.m() || // +1b +2c
1111 !B.m() && (((x > 4))) ) {} // +1b +2c
1112 ",
1113 "foo.java",
1114 |metric| {
1115 // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
1116 // space count: 1 (1 unit)
1117 insta::assert_json_snapshot!(
1118 metric.abc,
1119 @r#"
1120 {
1121 "assignments": 0,
1122 "branches": 5,
1123 "conditions": 30,
1124 "magnitude": 30.4138126514911,
1125 "value": 30.4138126514911,
1126 "assignments_average": 0.0,
1127 "branches_average": 5.0,
1128 "conditions_average": 30.0,
1129 "assignments_min": 0,
1130 "assignments_max": 0,
1131 "branches_min": 5,
1132 "branches_max": 5,
1133 "conditions_min": 30,
1134 "conditions_max": 30
1135 }
1136 "#
1137 );
1138 },
1139 );
1140 }
1141
1142 #[cfg(feature = "java")]
1143 #[test]
1144 fn java_while_and_do_while_conditions() {
1145 check_metrics::<JavaParser>(
1146 "
1147 while ( (!(!(!(a)))) ) {} // +1c
1148 while ( b || 1 > 2 ) {} // +2c
1149 while ( x.m() && (((c))) ) {} // +1b +2c
1150 do {} while ( !!!(((!!!a))) ); // +1c
1151 do {} while ( a || (b && c) ); // +3c
1152 do {} while ( !x.m() && 1 > 2 || !true ); // +1b +3c
1153 ",
1154 "foo.java",
1155 |metric| {
1156 // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
1157 // space count: 1 (1 unit)
1158 insta::assert_json_snapshot!(
1159 metric.abc,
1160 @r#"
1161 {
1162 "assignments": 0,
1163 "branches": 2,
1164 "conditions": 12,
1165 "magnitude": 12.165525060596439,
1166 "value": 12.165525060596439,
1167 "assignments_average": 0.0,
1168 "branches_average": 2.0,
1169 "conditions_average": 12.0,
1170 "assignments_min": 0,
1171 "assignments_max": 0,
1172 "branches_min": 2,
1173 "branches_max": 2,
1174 "conditions_min": 12,
1175 "conditions_max": 12
1176 }
1177 "#
1178 );
1179 },
1180 );
1181 }
1182
1183 // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
1184 // "These are cases where a single variable/field/value is treated as a boolean value.
1185 // Examples include `if (x)` and `return !ready`."
1186 // According to this definition, unary conditional expressions are counted also in function return values.
1187 // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
1188 // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
1189 #[cfg(feature = "java")]
1190 #[test]
1191 fn java_return_with_conditions() {
1192 check_metrics::<JavaParser>(
1193 "class A {
1194 boolean m1() {
1195 return !(z >= 0); // +1c
1196 }
1197 boolean m2() {
1198 return (((!x))); // +1c
1199 }
1200 boolean m3() {
1201 return x && y; // +2c
1202 }
1203 boolean m4() {
1204 return y || (z < 0); // +2c
1205 }
1206 boolean m5() {
1207 return x || y ? // +3c (two unary conditions and one ?)
1208 true : false;
1209 }
1210 }",
1211 "foo.java",
1212 |metric| {
1213 // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
1214 // space count: 7 (1 unit, 1 class and 5 methods)
1215 insta::assert_json_snapshot!(
1216 metric.abc,
1217 @r#"
1218 {
1219 "assignments": 0,
1220 "branches": 0,
1221 "conditions": 9,
1222 "magnitude": 9.0,
1223 "value": 0.0,
1224 "assignments_average": 0.0,
1225 "branches_average": 0.0,
1226 "conditions_average": 1.2857142857142858,
1227 "assignments_min": 0,
1228 "assignments_max": 0,
1229 "branches_min": 0,
1230 "branches_max": 0,
1231 "conditions_min": 0,
1232 "conditions_max": 3
1233 }
1234 "#
1235 );
1236 },
1237 );
1238 }
1239
1240 // Variables, method invocations, and true or false values
1241 // inside return statements are not counted as conditions
1242 #[cfg(feature = "java")]
1243 #[test]
1244 fn java_return_without_conditions() {
1245 check_metrics::<JavaParser>(
1246 "class A {
1247 boolean m1() {
1248 return x;
1249 }
1250 boolean m2() {
1251 return (x);
1252 }
1253 boolean m3() {
1254 return y.m(); // +1b
1255 }
1256 boolean m4() {
1257 return false;
1258 }
1259 void m5() {
1260 return;
1261 }
1262 }",
1263 "foo.java",
1264 |metric| {
1265 // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
1266 // space count: 7 (1 unit, 1 class and 5 methods)
1267 insta::assert_json_snapshot!(
1268 metric.abc,
1269 @r#"
1270 {
1271 "assignments": 0,
1272 "branches": 1,
1273 "conditions": 0,
1274 "magnitude": 1.0,
1275 "value": 0.0,
1276 "assignments_average": 0.0,
1277 "branches_average": 0.14285714285714285,
1278 "conditions_average": 0.0,
1279 "assignments_min": 0,
1280 "assignments_max": 0,
1281 "branches_min": 0,
1282 "branches_max": 1,
1283 "conditions_min": 0,
1284 "conditions_max": 0
1285 }
1286 "#
1287 );
1288 },
1289 );
1290 }
1291
1292 // Variables, method invocations, and true or false values
1293 // in lambda expression return values are not counted as conditions
1294 #[cfg(feature = "java")]
1295 #[test]
1296 fn java_lambda_expressions_return_with_conditions() {
1297 check_metrics::<JavaParser>(
1298 "
1299 Predicate<Boolean> p1 = a -> a; // +1a
1300 Predicate<Boolean> p2 = b -> true; // +1a
1301 Predicate<Boolean> p3 = c -> m(); // +1a
1302 Predicate<Integer> p4 = d -> d > 10; // +1a +1c
1303 Predicate<Boolean> p5 = (e) -> !e; // +1a +1c
1304 Predicate<Boolean> p6 = (f) -> !((!f)); // +1a +1c
1305 Predicate<Boolean> p7 = (g) -> !g && true; // +1a +2c
1306 BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
1307 BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
1308 return j || k; // +1a +2c
1309 };
1310 ",
1311 "foo.java",
1312 |metric| {
1313 // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
1314 // space count: 1 (1 unit)
1315 insta::assert_json_snapshot!(
1316 metric.abc,
1317 @r#"
1318 {
1319 "assignments": 9,
1320 "branches": 1,
1321 "conditions": 9,
1322 "magnitude": 12.767145334803704,
1323 "value": 12.767145334803704,
1324 "assignments_average": 9.0,
1325 "branches_average": 1.0,
1326 "conditions_average": 9.0,
1327 "assignments_min": 9,
1328 "assignments_max": 9,
1329 "branches_min": 1,
1330 "branches_max": 1,
1331 "conditions_min": 9,
1332 "conditions_max": 9
1333 }
1334 "#
1335 );
1336 },
1337 );
1338 }
1339
1340 #[cfg(feature = "java")]
1341 #[test]
1342 fn java_for_with_variable_declaration() {
1343 check_metrics::<JavaParser>(
1344 "
1345 for ( int i1 = 0; !(!(!(!a))); i1++ ) {} // +2a +1c
1346 for ( int i2 = 0; !B.m(); i2++ ) {} // +2a +1b +1c
1347 for ( int i3 = 0; a || false; i3++ ) {} // +2a +2c
1348 for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {} // +2a +1b +3c
1349 for ( int i5 = 0; true; i5++ ) {} // +2a +1c
1350 ",
1351 "foo.java",
1352 |metric| {
1353 // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
1354 // space count: 1 (1 unit)
1355 insta::assert_json_snapshot!(
1356 metric.abc,
1357 @r#"
1358 {
1359 "assignments": 10,
1360 "branches": 2,
1361 "conditions": 8,
1362 "magnitude": 12.96148139681572,
1363 "value": 12.96148139681572,
1364 "assignments_average": 10.0,
1365 "branches_average": 2.0,
1366 "conditions_average": 8.0,
1367 "assignments_min": 10,
1368 "assignments_max": 10,
1369 "branches_min": 2,
1370 "branches_max": 2,
1371 "conditions_min": 8,
1372 "conditions_max": 8
1373 }
1374 "#
1375 );
1376 },
1377 );
1378 }
1379
1380 #[cfg(feature = "java")]
1381 #[test]
1382 fn java_for_without_variable_declaration() {
1383 check_metrics::<JavaParser>(
1384 "class A{
1385 void m1() {
1386 for (i = 0; x < y; i++) {} // +2a +1c
1387 for (i = 0; ((x < y)); i++) {} // +2a +1c
1388 for (i = 0; !(!(x < y)); i++) {} // +2a +1c
1389 for (i = 0; true; i++) {} // +2a +1c
1390 }
1391 void m2() {
1392 for ( ; true; ) {} // +1c
1393 }
1394 void m3() {
1395 for ( ; ; ) {} // +0c — no condition to count (#1276)
1396 }
1397 }",
1398 "foo.java",
1399 |metric| {
1400 // magnitude: sqrt(64 + 0 + 25) = sqrt(89)
1401 // space count: 5 (1 unit, 1 class and 3 methods)
1402 insta::assert_json_snapshot!(
1403 metric.abc,
1404 @r#"
1405 {
1406 "assignments": 8,
1407 "branches": 0,
1408 "conditions": 5,
1409 "magnitude": 9.433981132056603,
1410 "value": 0.0,
1411 "assignments_average": 1.6,
1412 "branches_average": 0.0,
1413 "conditions_average": 1.0,
1414 "assignments_min": 0,
1415 "assignments_max": 8,
1416 "branches_min": 0,
1417 "branches_max": 0,
1418 "conditions_min": 0,
1419 "conditions_max": 4
1420 }
1421 "#
1422 );
1423 },
1424 );
1425 }
1426
1427 // Issue #1276 changed Java's answer here. `java_walk_for_statement`
1428 // used to read child(3), fall through to child(4) when that was the
1429 // `;` an expression initializer leaves behind, and count a `;` or
1430 // `)` landing there as a vacuously-true condition — so `for (;;)`
1431 // scored one. Nothing in Fitzpatrick's C dimension attributes a
1432 // condition to a `for` keyword; it counts conditional operators and
1433 // unary conditions that are present. Java and Groovy were the only
1434 // two impls disagreeing, and the field-addressed walker now reports
1435 // zero the way the C family, the JS family, PHP, C# and Go all do.
1436 #[cfg(feature = "java")]
1437 #[test]
1438 fn java_empty_for_condition_counts_nothing() {
1439 check_metrics::<JavaParser>(
1440 "class A { void m() { for (;;) { break; } } }",
1441 "foo.java",
1442 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1443 );
1444 // The other empty spelling, and the one the old cascade
1445 // actually mis-scored: with an **expression** initializer the
1446 // children are `for ( init ; ; update )`, so child(3) was the
1447 // separating `;` and child(4) the empty condition's `;`, which
1448 // the vacuous-true arm counted. The `for (int i = 0; ; i++)`
1449 // spelling scored 0 both before and after — Java's
1450 // `local_variable_declaration` swallows its own `;`, putting the
1451 // update expression at child(4), where it matched no arm — so it
1452 // would be a vacuous regression test and is deliberately not
1453 // used here.
1454 check_metrics::<JavaParser>(
1455 "class A { void m() { int i; for (i = 0; ; i++) { break; } } }",
1456 "foo.java",
1457 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
1458 );
1459 }
1460
1461 // The second defect the positional cascade carried: tree-sitter
1462 // counts comments among a node's children, so a comment anywhere in
1463 // the header shifted every index and the condition went unread —
1464 // the same failure #1181 removed from `java_walk_ternary`. Reading
1465 // the `condition` field cannot shift.
1466 #[cfg(feature = "java")]
1467 #[test]
1468 fn java_for_condition_survives_a_header_comment() {
1469 check_metrics::<JavaParser>(
1470 "class A { void m(boolean a) { for (; /* n */ a; ) { break; } } }",
1471 "foo.java",
1472 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1473 );
1474 // Unchanged control: the same loop without the comment. Both
1475 // spellings must agree, which is the property the positional
1476 // form broke.
1477 check_metrics::<JavaParser>(
1478 "class A { void m(boolean a) { for (; a; ) { break; } } }",
1479 "foo.java",
1480 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
1481 );
1482 }
1483
1484 // Variables, method invocations, and true or false values
1485 // in ternary expression return values are not counted as conditions
1486 #[cfg(feature = "java")]
1487 #[test]
1488 fn java_ternary_conditions() {
1489 check_metrics::<JavaParser>(
1490 "
1491 a = true; // +1a
1492 b = a ? true : false; // +1a +2c
1493 c = ((((a)))) ? !false : !b; // +1a +4c
1494 d = !this.m() ? !!a : (false); // +1a +1b +3c
1495 e = !(a) && b ? ((c)) : !d; // +1a +4c
1496 if ( this.m() ? a : !this.m() ) {} // +2b +3c
1497 if ( x > 0 ? !(false) : this.m() ) {} // +1b +3c
1498 if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {} // +5c
1499 ",
1500 "foo.java",
1501 |metric| {
1502 // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
1503 // space count: 1 (1 unit)
1504 insta::assert_json_snapshot!(
1505 metric.abc,
1506 @r#"
1507 {
1508 "assignments": 5,
1509 "branches": 4,
1510 "conditions": 24,
1511 "magnitude": 24.839484696748443,
1512 "value": 24.839484696748443,
1513 "assignments_average": 5.0,
1514 "branches_average": 4.0,
1515 "conditions_average": 24.0,
1516 "assignments_min": 5,
1517 "assignments_max": 5,
1518 "branches_min": 4,
1519 "branches_max": 4,
1520 "conditions_min": 24,
1521 "conditions_max": 24
1522 }
1523 "#
1524 );
1525 },
1526 );
1527 }
1528
1529 #[cfg(feature = "bash")]
1530 #[test]
1531 fn bash_assignments_only() {
1532 check_metrics::<BashParser>(
1533 "f() {
1534 a=1
1535 b=2
1536 c+=3
1537 }",
1538 "foo.sh",
1539 |metric| {
1540 insta::assert_json_snapshot!(
1541 metric.abc,
1542 @r#"
1543 {
1544 "assignments": 3,
1545 "branches": 0,
1546 "conditions": 0,
1547 "magnitude": 3.0,
1548 "value": 0.0,
1549 "assignments_average": 1.5,
1550 "branches_average": 0.0,
1551 "conditions_average": 0.0,
1552 "assignments_min": 0,
1553 "assignments_max": 3,
1554 "branches_min": 0,
1555 "branches_max": 0,
1556 "conditions_min": 0,
1557 "conditions_max": 0
1558 }
1559 "#
1560 );
1561 },
1562 );
1563 }
1564
1565 #[cfg(feature = "bash")]
1566 #[test]
1567 fn bash_commands_only() {
1568 check_metrics::<BashParser>(
1569 "f() {
1570 echo a
1571 ls
1572 }",
1573 "foo.sh",
1574 |metric| {
1575 insta::assert_json_snapshot!(
1576 metric.abc,
1577 @r#"
1578 {
1579 "assignments": 0,
1580 "branches": 2,
1581 "conditions": 0,
1582 "magnitude": 2.0,
1583 "value": 0.0,
1584 "assignments_average": 0.0,
1585 "branches_average": 1.0,
1586 "conditions_average": 0.0,
1587 "assignments_min": 0,
1588 "assignments_max": 0,
1589 "branches_min": 0,
1590 "branches_max": 2,
1591 "conditions_min": 0,
1592 "conditions_max": 0
1593 }
1594 "#
1595 );
1596 },
1597 );
1598 }
1599
1600 #[cfg(feature = "bash")]
1601 #[test]
1602 fn bash_control_flow_counts_conditions() {
1603 // Regression for #696: Bash control-flow branches are ABC
1604 // conditions (a Bash predicate is a command, so the branch keyword
1605 // is the only condition signal). Each mirrors a cyclomatic decision.
1606 //
1607 // expected: 4 conditions — `if` (1) + `elif` (1) + `while` (1) +
1608 // the non-wildcard case arm `a)` (1). The bare-`*)` wildcard arm is
1609 // the Bash analogue of `default:` and is excluded, exactly as the
1610 // cyclomatic standard count excludes it. No comparison / test
1611 // operators appear, so every condition here is control-flow.
1612 check_metrics::<BashParser>(
1613 "f() {
1614 if cmd; then
1615 echo a
1616 elif other; then
1617 echo b
1618 fi
1619 while running; do
1620 echo c
1621 done
1622 case \"$x\" in
1623 a) echo d ;;
1624 *) echo e ;;
1625 esac
1626 }",
1627 "foo.sh",
1628 |metric| {
1629 assert_eq!(metric.abc.conditions_sum(), 4);
1630 },
1631 );
1632 }
1633
1634 #[cfg(feature = "bash")]
1635 #[test]
1636 fn bash_conditions_mix() {
1637 // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
1638 // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
1639 // inside `[ ]`. Each `if` body's `echo` contributes a branch.
1640 //
1641 // expected: 8 conditions — each of the four `if`s contributes one
1642 // for the control-flow branch (#696) plus one for its comparison /
1643 // test operator (`==`, `!=`, `<`, `-z`). 4 branches (one `echo`
1644 // each). magnitude = sqrt(4² + 8²) = sqrt(80).
1645 check_metrics::<BashParser>(
1646 "f() {
1647 if [[ \"$a\" == \"$b\" ]]; then
1648 echo eq
1649 fi
1650 if [[ \"$x\" != \"$y\" ]]; then
1651 echo ne
1652 fi
1653 if (( $a < $b )); then
1654 echo lt
1655 fi
1656 if [ -z \"$x\" ]; then
1657 echo empty
1658 fi
1659 }",
1660 "foo.sh",
1661 |metric| {
1662 assert_eq!(metric.abc.conditions_sum(), 8);
1663 assert_eq!(metric.abc.branches_sum(), 4);
1664 insta::assert_json_snapshot!(
1665 metric.abc,
1666 @r#"
1667 {
1668 "assignments": 0,
1669 "branches": 4,
1670 "conditions": 8,
1671 "magnitude": 8.94427190999916,
1672 "value": 0.0,
1673 "assignments_average": 0.0,
1674 "branches_average": 2.0,
1675 "conditions_average": 4.0,
1676 "assignments_min": 0,
1677 "assignments_max": 0,
1678 "branches_min": 0,
1679 "branches_max": 4,
1680 "conditions_min": 0,
1681 "conditions_max": 8
1682 }
1683 "#
1684 );
1685 },
1686 );
1687 }
1688
1689 #[cfg(feature = "bash")]
1690 #[test]
1691 fn bash_redirection_is_not_a_condition() {
1692 // `>` and `<` spell an I/O redirection as well as a comparison, and
1693 // the grammar parents the redirection under `file_redirect` rather
1694 // than `binary_expression`. Ungated, every redirect in a script
1695 // scored a condition: this fixture measured 2 before the parent
1696 // gate — the Bash instance of #1280's positive-parent polarity.
1697 // expected: 0 conditions — no test, no `if`, no comparison.
1698 check_metrics::<BashParser>(
1699 "f() {\n echo hi > out.txt\n read x < in.txt\n}\n",
1700 "foo.sh",
1701 |metric| {
1702 assert_eq!(metric.abc.conditions_sum(), 0);
1703 // The two `echo` / `read` commands still count as branches,
1704 // so the zero above is the gate firing rather than the walk
1705 // skipping the function body.
1706 assert_eq!(metric.abc.branches_sum(), 2);
1707 },
1708 );
1709 }
1710
1711 #[cfg(feature = "bash")]
1712 #[test]
1713 fn bash_comparison_inside_an_arithmetic_or_test_context_is_a_condition() {
1714 // The positive control for the gate above: the same tokens under a
1715 // `binary_expression` are real comparisons in both the `[[ … ]]`
1716 // test form and the `(( … ))` arithmetic form, and still count.
1717 // expected: 4 — one `if` control-flow condition and one `>` per
1718 // function.
1719 check_metrics::<BashParser>(
1720 "f() {\n if [[ $a > $b ]]; then :; fi\n}\ng() {\n if (( a > b )); then :; fi\n}\n",
1721 "foo.sh",
1722 |metric| {
1723 assert_eq!(metric.abc.conditions_sum(), 4);
1724 },
1725 );
1726 }
1727
1728 #[cfg(feature = "bash")]
1729 #[test]
1730 fn bash_arithmetic_ternary_is_a_condition() {
1731 // The ABC half of #1268. Cyclomatic and cognitive both count Bash's
1732 // only ternary form; ABC did not, so the identical construct scored
1733 // 1 here against the C family's 2.
1734 // expected: 2 — the `>` comparison and the ternary itself, matching
1735 // `int m = a > b ? a : b;` in C.
1736 check_metrics::<BashParser>(
1737 "f() {\n local m=$(( a > b ? a : b ))\n}\n",
1738 "foo.sh",
1739 |metric| {
1740 assert_eq!(metric.abc.conditions_sum(), 2);
1741 },
1742 );
1743 }
1744
1745 #[cfg(feature = "bash")]
1746 #[test]
1747 fn bash_magnitude() {
1748 // Combined assignments + branches + conditions. The single `if`
1749 // contributes two conditions (the control-flow branch, #696, plus
1750 // the `==` operator), so magnitude = sqrt(2² + 1² + 2²) = sqrt(9).
1751 check_metrics::<BashParser>(
1752 "f() {
1753 a=1
1754 b=2
1755 if [[ \"$a\" == \"$b\" ]]; then
1756 echo eq
1757 fi
1758 }",
1759 "foo.sh",
1760 |metric| {
1761 assert_eq!(metric.abc.conditions_sum(), 2);
1762 insta::assert_json_snapshot!(
1763 metric.abc,
1764 @r#"
1765 {
1766 "assignments": 2,
1767 "branches": 1,
1768 "conditions": 2,
1769 "magnitude": 3.0,
1770 "value": 0.0,
1771 "assignments_average": 1.0,
1772 "branches_average": 0.5,
1773 "conditions_average": 1.0,
1774 "assignments_min": 0,
1775 "assignments_max": 2,
1776 "branches_min": 0,
1777 "branches_max": 1,
1778 "conditions_min": 0,
1779 "conditions_max": 2
1780 }
1781 "#
1782 );
1783 },
1784 );
1785 }
1786
1787 #[cfg(feature = "java")]
1788 #[test]
1789 fn java_malformed_parenthesized_no_panic() {
1790 check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
1791 // tree-sitter emits ERROR nodes for this malformed source, so no
1792 // IfStatement, branch, or condition is recognised — all counts are 0.
1793 // Primary goal: the unwrap-free path does not panic.
1794 assert_eq!(metric.abc.assignments(), 0);
1795 assert_eq!(metric.abc.branches(), 0);
1796 assert_eq!(metric.abc.conditions(), 0);
1797 assert_eq!(metric.abc.magnitude(), 0.0);
1798 });
1799 }
1800
1801 #[cfg(feature = "java")]
1802 #[test]
1803 fn java_bool_returning_terminal_kinds_count() {
1804 // Companion to `csharp_bool_returning_terminal_kinds_count`
1805 // (issue #372 / lesson #19). Java's grammar wraps every
1806 // if/while/do condition in `parenthesized_expression`, so
1807 // the gap lived in the former `java_inspect_container`'s
1808 // terminal-arm recognizer: `FieldAccess` (`cfg.flag`), `CastExpression`
1809 // (`(boolean)v`), `ArrayAccess` (`flags[0]`), and
1810 // `InstanceofExpression` (`x instanceof Foo`) were never
1811 // counted. Java has no `await` or `is_pattern` analogues,
1812 // so the C# fix's five-kind set collapses to four here.
1813 //
1814 // `instanceof` has since moved out of the terminal set to an
1815 // unconditional arm (#1461), so it no longer depends on the
1816 // slot to be counted. The total is unchanged, which is the
1817 // point of that change rather than an accident of it.
1818 //
1819 // expected: 4 conditions (one per `if`), 0 assignments,
1820 // 0 branches (no invocations).
1821 check_metrics::<JavaParser>(
1822 "class Cfg { boolean flag; }
1823 class A {
1824 void m(Object v, boolean[] flags, Cfg cfg) {
1825 if (cfg.flag) { }
1826 if ((boolean) v) { }
1827 if (v instanceof Cfg) { }
1828 if (flags[0]) { }
1829 }
1830 }",
1831 "foo.java",
1832 |metric| {
1833 assert_eq!(metric.abc.conditions_sum(), 4);
1834 assert_eq!(metric.abc.assignments_sum(), 0);
1835 assert_eq!(metric.abc.branches_sum(), 0);
1836 },
1837 );
1838 }
1839
1840 // Issue #1274: a generic *declaration* is type syntax, not a
1841 // decision. `java_count_token_condition` denied only
1842 // `type_arguments`, so `class Gen<T>` and `<U> U ident(U x)` — both
1843 // `type_parameters` — each scored two conditions. The fix counts
1844 // `<` / `>` only under `binary_expression`, the polarity C / C++ /
1845 // Rust / Go already use; a `grammar.json` sweep proves the token is
1846 // emitted from exactly three productions, so the two shapes agree on
1847 // every well-formed input.
1848 //
1849 // One fixture per production the arm must ignore: the class-level
1850 // `type_parameters` (`Gen<T …>`), its `extends` bound's nested
1851 // `type_arguments` (`Comparable<T>`), the method-level
1852 // `type_parameters` (`<U>`), and a nested generic
1853 // (`Map<String, List<T>>`) whose two closing `>` lex as separate
1854 // tokens under separate `type_arguments`. Pre-fix this file scored
1855 // 4 conditions — two per `type_parameters` bracket pair; expected 0,
1856 // the file contains no conditional construct at all.
1857 #[cfg(feature = "java")]
1858 #[test]
1859 fn java_generic_declarations_are_not_conditions() {
1860 check_metrics::<JavaParser>(
1861 "class Gen<T extends Comparable<T>> {
1862 <U> U ident(U x) { return x; }
1863 List<T> pick(Map<String, List<T>> m) { return m.get(\"k\"); }
1864 }",
1865 "foo.java",
1866 |metric| {
1867 assert_eq!(metric.abc.conditions_sum(), 0);
1868 // Non-vacuity guard: 0 is also what an unparsed file
1869 // scores, so pin a value only a walked body can produce
1870 // — the `m.get("k")` invocation.
1871 assert_eq!(metric.abc.branches_sum(), 1);
1872 },
1873 );
1874 }
1875
1876 // Second half of #1274, same premise and same match: a wildcard type
1877 // argument's `?` is type syntax, not a ternary. tree-sitter-java emits
1878 // a bare `?` from exactly two productions — `ternary_expression` and
1879 // `wildcard` — so the `QMARK` arm carries the same allowlist gate the
1880 // `<` / `>` arm does.
1881 //
1882 // The body carries a real ternary over a real comparison so the
1883 // expected total is 2, not 0. Asserting 0 on a wildcard-only fixture
1884 // would have been vacuous — an unparsable file scores 0 too, so the
1885 // test could not tell "the wildcard was ignored" from "the fixture
1886 // stopped being recognised". Each failure mode now lands on its own
1887 // number: 4 if the wildcard `?` counts again (the pre-fix value), 3
1888 // if the gate is aimed at `Wildcard` instead, 1 if it swallows the
1889 // genuine ternary, 0 if parsing breaks. Two wildcards against one
1890 // ternary is deliberate — with one of each, aiming the gate at the
1891 // wrong one of the two productions still totals 2 and the test
1892 // cannot see the difference.
1893 #[cfg(feature = "java")]
1894 #[test]
1895 fn java_generic_wildcard_is_not_a_condition() {
1896 check_metrics::<JavaParser>(
1897 "class A {
1898 int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
1899 int a, int b) { return a < b ? 1 : 2; }
1900 }",
1901 "foo.java",
1902 |metric| {
1903 assert_eq!(metric.abc.conditions_sum(), 2);
1904 },
1905 );
1906 }
1907
1908 // The other half of #1274: narrowing the `<` / `>` arm must not
1909 // swallow real comparisons. `List<String>` in the signature keeps a
1910 // generic in scope so the fixture proves both directions at once,
1911 // and the assertion is the grammar-dispatch §8 pin — on the
1912 // method's own space the ABC condition count must equal the
1913 // cyclomatic decision count (`cyclomatic()` minus the per-space
1914 // base of 1). Both are 2, one per `if`.
1915 #[cfg(feature = "java")]
1916 #[test]
1917 fn java_comparison_operators_still_count_alongside_generics() {
1918 check_func_space::<JavaParser, _>(
1919 "class A {
1920 int m(List<String> xs, int a, int b) {
1921 if (a < b) { return 1; }
1922 if (a > b) { return 2; }
1923 return 0;
1924 }
1925 }",
1926 "foo.java",
1927 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
1928 );
1929 }
1930
1931 #[cfg(feature = "java")]
1932 #[test]
1933 fn java_constructor_delegation_is_a_branch() {
1934 // Regression for #1279: `super(…)` / `this(…)` parse as
1935 // `explicit_constructor_invocation`, not `method_invocation`, so
1936 // each scored zero branches while Groovy scored one for identical
1937 // source. Both constructors delegate, and neither body contains any
1938 // other call.
1939 // expected: 2 branches — one delegation each.
1940 check_metrics::<JavaParser>(
1941 "class Sub extends Base {
1942 Sub() { super(1); }
1943 Sub(int a) { this(); }
1944 }",
1945 "foo.java",
1946 |metric| {
1947 assert_eq!(metric.abc.branches_sum(), 2);
1948 },
1949 );
1950 }
1951
1952 #[cfg(feature = "java")]
1953 #[test]
1954 fn java_constructor_delegation_does_not_double_count_arguments() {
1955 // The delegation node does not wrap a `method_invocation` for the
1956 // call itself, so `super(f())` is exactly two branches — the
1957 // delegation and the argument call — not three (#1279).
1958 // expected: 2 branches.
1959 check_metrics::<JavaParser>(
1960 "class Sub extends Base {
1961 Sub() { super(f()); }
1962 }",
1963 "foo.java",
1964 |metric| {
1965 assert_eq!(metric.abc.branches_sum(), 2);
1966 },
1967 );
1968 }
1969
1970 #[cfg(feature = "csharp")]
1971 #[test]
1972 fn csharp_constructor_initializer_is_a_branch() {
1973 // C# spells the same delegation as a `constructor_initializer`
1974 // (`: base(…)` / `: this(…)`), which likewise scored zero (#1279).
1975 // expected: 2 branches — one initializer each, no other calls.
1976 check_metrics::<CsharpParser>(
1977 "class Sub : Base {
1978 Sub() : base(1) { }
1979 Sub(int a) : this() { }
1980 }",
1981 "foo.cs",
1982 |metric| {
1983 assert_eq!(metric.abc.branches_sum(), 2);
1984 },
1985 );
1986 }
1987
1988 #[cfg(feature = "csharp")]
1989 #[test]
1990 fn csharp_primary_constructor_base_call_is_a_branch() {
1991 // The C# 12 primary-constructor superclass call invokes the base
1992 // constructor exactly as the `: base(…)` above it does, and scored
1993 // zero until #1406 — the C# sibling of Kotlin's #1384.
1994 //
1995 // tree-sitter-c-sharp 0.23.5 spells the `record` and `class`
1996 // families differently, so per `.claude/rules/grammar-dispatch.md`
1997 // §11 they are two independent paths and each needs its own
1998 // fixture: the record form nests the arguments under a
1999 // `primary_constructor_base_type`, the class form hangs them
2000 // straight off the `base_list`. A record-only fixture says nothing
2001 // about the class arm, and vice versa.
2002 //
2003 // `S` and `R3` are the negatives: a base type with no argument
2004 // list is not a call, and neither spelling emits an
2005 // `argument_list` for it.
2006 let src = "class Sub(int x) : Base(x) { }
2007 record R1(int x) : Base(x);
2008 record class R2(int x) : Base(x);
2009 class G<T>(int x) : GBase<T>(x) where T : class { }
2010 class Multi(int x) : Base(x), IBase { }
2011 struct S(int x) : IBase { }
2012 record R3(int x) : Base;";
2013 // Anchors every spelling against a fixture edit: with presence
2014 // checks only, deleting `(x)` from the class form leaves the
2015 // record form satisfying the arm and this test green. The last
2016 // two counts anchor the declarations that would otherwise decay
2017 // into duplicates of `Sub` — `Multi` owns the fixture's only
2018 // comma (`Base(x), IBase`) and `G` its only type argument list
2019 // (`GBase<T>`), so trimming either back fails here by name.
2020 assert_csharp_fixture_spells(
2021 src,
2022 &[
2023 (
2024 Csharp::PrimaryConstructorBaseType as u16,
2025 2,
2026 "`record` base calls",
2027 ),
2028 (Csharp::ArgumentList as u16, 5, "base-call argument lists"),
2029 (Csharp::BaseList2 as u16, 7, "base lists"),
2030 // The §2 drift marker for the arm's other half. Both ids
2031 // render to `"base_list"`; at this pin every declaration
2032 // family above emits 252 and none emits 246, so
2033 // `BaseList` is defensive and this zero says so. A
2034 // grammar bump that starts emitting it fails here rather
2035 // than silently changing which arm does the work.
2036 (Csharp::BaseList as u16, 0, "unaliased `base_list` nodes"),
2037 (Csharp::COMMA as u16, 1, "`Multi`'s trailing interface"),
2038 (Csharp::TypeArgumentList as u16, 1, "`G`'s generic base"),
2039 ],
2040 );
2041 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2042 for name in ["Sub", "R1", "R2", "G", "Multi"] {
2043 assert_eq!(
2044 child_space(&space, name).metrics.abc.branches(),
2045 1,
2046 "{name}: the primary-constructor base call is one branch"
2047 );
2048 }
2049 for name in ["S", "R3"] {
2050 assert_eq!(
2051 child_space(&space, name).metrics.abc.branches(),
2052 0,
2053 "{name}: a base type with no argument list is not a call"
2054 );
2055 }
2056 });
2057 }
2058
2059 #[cfg(feature = "csharp")]
2060 #[test]
2061 fn csharp_base_list_gate_excludes_other_argument_lists() {
2062 // The `base_list` parent gate on the `ArgumentList` arm is
2063 // load-bearing, not decoration: ungated it bills a branch for the
2064 // argument list of every call and allocation in the file, on top
2065 // of the `InvocationExpression` / `ObjectCreationExpression` the
2066 // arm already counts — doubling both (#1406).
2067 //
2068 // The fixture pairs one real base call with the four other
2069 // argument-carrying productions C# spells nearby: an attribute
2070 // (`attribute_argument_list`), an invocation, an object creation,
2071 // and an indexer (`bracketed_argument_list`) alongside a generic
2072 // (`type_argument_list`). Those last three are distinct kind ids
2073 // rather than `argument_list`, so they pin the §1 neighbour set
2074 // as much as the gate.
2075 let src = "[System.Obsolete(\"why\")]
2076 class A(int x) : Base(x) {
2077 void M(int y) { Helper(y); }
2078 object N() { return new Foo(1); }
2079 int P(System.Collections.Generic.List<int> l, int i) { return l[i]; }
2080 }";
2081 assert_csharp_fixture_spells(
2082 src,
2083 &[
2084 (
2085 Csharp::AttributeArgumentList as u16,
2086 1,
2087 "attribute argument lists",
2088 ),
2089 (
2090 Csharp::BracketedArgumentList as u16,
2091 1,
2092 "bracketed argument lists",
2093 ),
2094 (Csharp::TypeArgumentList as u16, 1, "type argument lists"),
2095 (Csharp::ArgumentList as u16, 3, "argument lists"),
2096 ],
2097 );
2098 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2099 let class = child_space(&space, "A");
2100 assert_eq!(
2101 class.metrics.abc.branches(),
2102 1,
2103 "`: Base(x)` alone — the attribute's arguments are not a call"
2104 );
2105 // The controls. 2 apiece would mean the gate let the
2106 // argument list through beside the call that owns it.
2107 assert_eq!(
2108 child_space(class, "M").metrics.abc.branches(),
2109 1,
2110 "`Helper(y)` is one branch, not one per argument list"
2111 );
2112 assert_eq!(
2113 child_space(class, "N").metrics.abc.branches(),
2114 1,
2115 "`new Foo(1)` is one branch, not one per argument list"
2116 );
2117 // `P` carries the indexer and the generic deliberately, but
2118 // gets no assertion: `bracketed_argument_list` and
2119 // `type_argument_list` are distinct kind ids, so `P` reads 0
2120 // with the gate, without it, and without this change
2121 // altogether. The census above is what actually pins them —
2122 // an assertion here would read as coverage it cannot provide.
2123 });
2124 }
2125
2126 #[cfg(feature = "csharp")]
2127 #[test]
2128 fn csharp_base_call_and_constructor_initializer_do_not_double_count() {
2129 // A class can spell *both* delegations at once: the primary
2130 // constructor's `: Base(x)` and a secondary constructor's
2131 // `: this(a)`. They are separate nodes in separate spaces, so the
2132 // file scores 2 — one each. 3 would mean the #1406 arm also fired
2133 // on something `ConstructorInitializer` already owned; 1 would
2134 // mean one of the two stopped counting.
2135 //
2136 // Java has the equivalent guard
2137 // (`java_constructor_delegation_does_not_double_count_arguments`);
2138 // C# now has two delegation spellings and this is the pairing
2139 // test for them.
2140 let src = "class Both(int x) : Base(x) {
2141 public Both(int a, int b) : this(a) { }
2142 }";
2143 assert_csharp_fixture_spells(
2144 src,
2145 &[
2146 (Csharp::ConstructorInitializer as u16, 1, "`: this(a)`"),
2147 (Csharp::BaseList2 as u16, 1, "the primary-constructor base"),
2148 ],
2149 );
2150 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
2151 let class = child_space(&space, "Both");
2152 assert_eq!(
2153 class.metrics.abc.branches(),
2154 1,
2155 "the class space owns `: Base(x)` and nothing else"
2156 );
2157 assert_eq!(
2158 child_space(class, "Both").metrics.abc.branches(),
2159 1,
2160 "the secondary constructor owns `: this(a)` and nothing else"
2161 );
2162 });
2163 }
2164
2165 #[cfg(feature = "kotlin")]
2166 #[test]
2167 fn kotlin_constructor_delegation_is_a_branch() {
2168 // Kotlin's secondary-constructor delegation is a
2169 // `constructor_delegation_call`, a distinct production from
2170 // `CallExpression`, so it scored zero for the same reason (#1279).
2171 // expected: 1 branch — the `: super(x)` delegation.
2172 check_metrics::<KotlinParser>(
2173 "class Sub : Base {
2174 constructor(x: Int) : super(x) { }
2175 }",
2176 "foo.kt",
2177 |metric| {
2178 assert_eq!(metric.abc.branches_sum(), 1);
2179 },
2180 );
2181 }
2182
2183 #[cfg(feature = "kotlin")]
2184 #[test]
2185 fn kotlin_primary_constructor_superclass_call_is_a_branch() {
2186 // Kotlin's *primary*-constructor superclass call is a
2187 // `constructor_invocation` under a `delegation_specifier` — a third
2188 // production, distinct from both `CallExpression` and the
2189 // `ConstructorDelegationCall` #1279 added, so it scored zero while
2190 // the secondary form beside it scored one (#1384). An object
2191 // expression's superclass call uses the same production.
2192 //
2193 // `class Plain : Marker` is the negative case: a delegation
2194 // specifier with no argument list is a plain `user_type`, so it
2195 // must stay at zero. Broadening the arm to bare
2196 // `DelegationSpecifier` fails here at `branches_sum` (5 against
2197 // 3), though not on `Plain`'s account alone: `class Classic :
2198 // Base` below is argument-less too, so deleting `Plain` still
2199 // leaves the broadening failing, at 4. What `Plain` buys is the
2200 // case stated outright rather than riding on the contrast fixture,
2201 // and since the per-space assertions below name it, trimming it
2202 // from the fixture fails in `child_space` instead of passing
2203 // silently as it did while this test read only the sum.
2204 //
2205 // `class Classic` carries the secondary spelling of the same call
2206 // alongside, which is what makes the per-space assertions below a
2207 // contrast rather than a census: a class header's superclass call
2208 // sits in `delegation_specifiers`, outside every member, so no
2209 // function space encloses it and the branch is the **class**
2210 // space's, while `constructor(x: Int) : super(x)` opens a function
2211 // space and takes the branch with it. `branches_sum` cannot see the
2212 // difference — the file totals 3 either way — so a codebase moving
2213 // to primary constructors shifts per-function `branches_max` /
2214 // `branches_average` and per-space `abc` thresholds without any
2215 // behaviour change. That is deliberate (there is no function to
2216 // attribute the call to), and C# has the identical split, pinned by
2217 // `csharp_base_call_and_constructor_initializer_do_not_double_count`
2218 // (#1456).
2219 //
2220 // expected: 3 branches — `Base(1, 2)`, `Base(3)` and `super(x)`.
2221 // Nothing else in the fixture is a call, so deleting any one
2222 // construction from the fixture moves both the total and the space
2223 // it belongs to.
2224 check_func_space::<KotlinParser, _>(
2225 "class Sub : Base(1, 2) { }
2226 class Plain : Marker { }
2227 fun make(): Any = object : Base(3) { }
2228 class Classic : Base {
2229 constructor(x: Int) : super(x) { }
2230 }",
2231 "foo.kt",
2232 |space| {
2233 assert_eq!(space.metrics.abc.branches_sum(), 3);
2234 assert_eq!(
2235 child_space(&space, "Sub").metrics.abc.branches(),
2236 1,
2237 "the primary-constructor superclass call is the class space's"
2238 );
2239 assert_eq!(
2240 child_space(&space, "Plain").metrics.abc.branches(),
2241 0,
2242 "a delegation specifier with no argument list is not a call"
2243 );
2244 // The object expression is its own Class space (#463), so
2245 // `make` holds nothing of its own — the same attribution
2246 // one level down.
2247 let make = child_space(&space, "make");
2248 assert_eq!(
2249 make.metrics.abc.branches(),
2250 0,
2251 "`make` owns no branch; `object : Base(3)` is its own space"
2252 );
2253 assert_eq!(
2254 child_space(make, "<anonymous>").metrics.abc.branches(),
2255 1,
2256 "the object expression's superclass call is that space's"
2257 );
2258 // The contrast. `Classic` spells the same call through a
2259 // secondary constructor, which *does* open a function
2260 // space, so the class owns nothing.
2261 let classic = child_space(&space, "Classic");
2262 assert_eq!(
2263 classic.metrics.abc.branches(),
2264 0,
2265 "a secondary constructor's `: super(x)` is not the class's"
2266 );
2267 assert_eq!(
2268 child_space(classic, "<anonymous>").metrics.abc.branches(),
2269 1,
2270 "it belongs to the constructor's own function space"
2271 );
2272 },
2273 );
2274 }
2275
2276 #[cfg(feature = "kotlin")]
2277 #[test]
2278 fn kotlin_annotation_arguments_are_not_a_branch() {
2279 // The parent gate on that arm is load-bearing, not decoration:
2280 // tree-sitter-kotlin-ng spells an annotation's argument list with
2281 // the *same* `constructor_invocation` production — `@Mark("a")`
2282 // parses as `annotation > constructor_invocation` and
2283 // `@file:Suppress(…)` as `file_annotation > constructor_invocation`.
2284 // Ungated, every argument-carrying annotation in a Kotlin file
2285 // would bill a branch, and annotations are everywhere (#1384).
2286 //
2287 // The fixture pairs three annotation spellings — file-level,
2288 // class-level and use-site-targeted — with one real superclass
2289 // call, so the total discriminates in both directions: 1 with the
2290 // gate, 4 without it, 0 without the arm at all.
2291 //
2292 // A correctly-excluded annotation contributes to no ABC axis, so
2293 // `branches_sum()` alone cannot notice the annotations being
2294 // trimmed out of the fixture — measured: dropping any of the three
2295 // leaves this test green, and with all three gone, removing the
2296 // production gate fails nothing. The node census below is the
2297 // anchor: it pins that the fixture really does hand the arm four
2298 // `constructor_invocation` nodes for it to score 1 out of, and
2299 // that the `file_annotation` parent — the spelling a denylist of
2300 // `Annotation` alone would have missed — is among them.
2301 //
2302 // expected: 1 branch — `Base(1)` only.
2303 let src = r#"@file:Suppress("unused")
2304 @Mark("a")
2305 class Ann : Base(1) {
2306 @get:Mark("b")
2307 val v: Int = 0
2308 }"#;
2309 let parser = KotlinParser::new(
2310 src.as_bytes().to_vec(),
2311 &std::path::PathBuf::from("foo.kt"),
2312 None,
2313 );
2314 assert_eq!(
2315 parser
2316 .root()
2317 .preorder()
2318 .filter(|n| n.kind_id() == Kotlin::ConstructorInvocation as u16)
2319 .count(),
2320 4,
2321 "fixture must keep three annotations plus the superclass call"
2322 );
2323 assert!(ast_has_kind_id(&parser, Kotlin::FileAnnotation as u16));
2324
2325 check_metrics::<KotlinParser>(src, "foo.kt", |metric| {
2326 assert_eq!(metric.abc.branches_sum(), 1);
2327 });
2328 }
2329
2330 #[cfg(feature = "groovy")]
2331 #[test]
2332 fn groovy_constructor_delegation_is_a_branch() {
2333 // Groovy already counted this shape before #1279; the assertion
2334 // pins the JVM-family parity the Java and Kotlin fixes restore.
2335 // expected: 1 branch — the `super(1)` delegation.
2336 check_metrics::<GroovyParser>(
2337 "class Sub extends Base {
2338 Sub() { super(1) }
2339 }",
2340 "foo.groovy",
2341 |metric| {
2342 assert_eq!(metric.abc.branches_sum(), 1);
2343 },
2344 );
2345 }
2346
2347 // The three tests below are one fix seen from three grammars (#1407):
2348 // an enum constant carrying constructor arguments is an object
2349 // construction and scored zero in every JVM-family language, which
2350 // #1279 and #1384 left behind because both looked at superclass
2351 // delegation only.
2352 //
2353 // Each fixture pairs an argument-carrying constant with a bare one in
2354 // the *same* enum, because that is the only shape that shows the gate
2355 // working: a fixture of only `A(1)` scores the same whether the arm is
2356 // gated on the argument list or matches every constant
2357 // (`.claude/rules/grammar-dispatch.md` §6). Each also carries a
2358 // constant whose argument is itself a call, which must score 2 rather
2359 // than 1 (§5).
2360 //
2361 // `branches_sum()` alone cannot notice the bare constant being trimmed
2362 // out — it contributes to no axis — so every fixture is anchored on a
2363 // node census that fails by name when a spelling is edited away
2364 // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
2365 // production line"). The two are coupled in one helper for the same
2366 // reason `assert_kotlin_class_members` couples its pair: a branch
2367 // total added without the census reads as coverage and is not.
2368
2369 #[cfg(any(feature = "groovy", feature = "java", feature = "kotlin"))]
2370 fn assert_enum_branches<P: MetricSuite>(
2371 src: &str,
2372 path: &str,
2373 kinds: &[(u16, usize, &str)],
2374 branches: u64,
2375 ) {
2376 assert_fixture_spells::<P>(src, path, kinds);
2377 check_func_space::<P, _>(src, path, |space| {
2378 assert_eq!(space.metrics.abc.branches_sum(), branches);
2379 });
2380 }
2381
2382 #[cfg(feature = "java")]
2383 #[test]
2384 fn java_enum_constant_with_arguments_is_a_branch() {
2385 // `@SuppressWarnings("x") B` is the discriminating case for the
2386 // gate's *kind*, not just its presence: the annotation carries an
2387 // argument list of its own, so an arm gated on "has some argument
2388 // list anywhere below" would score it. It cannot satisfy this one
2389 // twice over — `annotation_argument_list` is a distinct kind id
2390 // from `argument_list`, and it hangs off the constant's `modifiers`
2391 // child rather than the constant itself. Both verified with `bca
2392 // dump`.
2393 //
2394 // expected: 4 branches — `A(1)`, `K(2)`, `D(…)`, and the `f()`
2395 // inside `D`'s argument list. `B` scores 0.
2396 let src = "class Outer {
2397 static int f() { return 1; }
2398 enum E {
2399 A(1),
2400 @SuppressWarnings(\"x\") B,
2401 K(2),
2402 D(f());
2403 E(int v) {}
2404 }
2405 }";
2406 assert_enum_branches::<JavaParser>(
2407 src,
2408 "foo.java",
2409 &[
2410 (Java::EnumConstant as u16, 4, "enum constants"),
2411 (Java::ArgumentList as u16, 4, "argument lists"),
2412 (
2413 Java::AnnotationArgumentList as u16,
2414 1,
2415 "`@SuppressWarnings`'s argument list",
2416 ),
2417 (Java::MethodInvocation as u16, 1, "`D`'s nested `f()` call"),
2418 ],
2419 4,
2420 );
2421 }
2422
2423 #[cfg(feature = "kotlin")]
2424 #[test]
2425 fn kotlin_enum_entry_with_arguments_is_a_branch() {
2426 // Kotlin needs a defaulted primary-constructor parameter to spell
2427 // both cases in one enum: `enum class E(val v: Int)` obliges every
2428 // entry to pass an argument, so `= 0` is what lets the bare `B`
2429 // sit beside `A(1)`. `enum class P` covers the other shape the gate
2430 // has to leave alone — an enum with no constructor at all, whose
2431 // entries can never carry an argument list.
2432 //
2433 // expected: 3 branches — `A(1)`, `C(…)`, and the `g()` inside `C`'s
2434 // argument list. `B`, `X` and `Y` score 0.
2435 let src = "fun g(): Int = 1
2436 enum class E(val v: Int = 0) { A(1), B, C(g()) }
2437 enum class P { X, Y }";
2438 assert_enum_branches::<KotlinParser>(
2439 src,
2440 "foo.kt",
2441 &[
2442 (Kotlin::EnumEntry as u16, 5, "enum entries"),
2443 (Kotlin::ValueArguments as u16, 3, "argument lists"),
2444 (Kotlin::CallExpression as u16, 1, "`C`'s nested `g()` call"),
2445 ],
2446 3,
2447 );
2448 }
2449
2450 #[cfg(feature = "groovy")]
2451 #[test]
2452 fn groovy_enum_constant_with_arguments_is_a_branch() {
2453 // Groovy's enum has no annotated-constant case to cover: the
2454 // dekobon grammar cannot parse `@Deprecated A(1)` inside an enum
2455 // body and recovers into `ERROR` nodes, emitting no `enum_constant`
2456 // at all. The remaining two cases are the ones that matter.
2457 //
2458 // expected: 3 branches — `A(1)`, `D(…)`, and the `Outer.f()` inside
2459 // `D`'s argument list. `B` scores 0.
2460 let src = "class Outer { static int f() { 1 } }
2461 enum E { A(1), B, D(Outer.f()) }";
2462 assert_enum_branches::<GroovyParser>(
2463 src,
2464 "foo.groovy",
2465 &[
2466 (Groovy::EnumConstant as u16, 3, "enum constants"),
2467 (Groovy::ArgumentList as u16, 3, "argument lists"),
2468 (
2469 Groovy::MethodInvocation as u16,
2470 1,
2471 "`D`'s nested `Outer.f()` call",
2472 ),
2473 ],
2474 3,
2475 );
2476 }
2477
2478 #[cfg(feature = "groovy")]
2479 #[test]
2480 fn groovy_no_abc() {
2481 // Comment-only file has no executable code → all-zero ABC.
2482 check_metrics::<GroovyParser>(
2483 "// just a comment, no executable code",
2484 "foo.groovy",
2485 |metric| {
2486 assert_eq!(metric.abc.assignments_sum(), 0);
2487 assert_eq!(metric.abc.branches_sum(), 0);
2488 assert_eq!(metric.abc.conditions_sum(), 0);
2489 },
2490 );
2491 }
2492
2493 #[cfg(feature = "groovy")]
2494 #[test]
2495 fn groovy_single_assignment() {
2496 // `int x = 1` is a local-variable declaration whose `=` counts
2497 // as one assignment (matches Java's semantics).
2498 check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
2499 assert_eq!(metric.abc.assignments_sum(), 1);
2500 assert_eq!(metric.abc.branches_sum(), 0);
2501 assert_eq!(metric.abc.conditions_sum(), 0);
2502 });
2503 }
2504
2505 #[cfg(feature = "groovy")]
2506 #[test]
2507 fn groovy_assignments() {
2508 check_metrics::<GroovyParser>(
2509 "void f() {
2510 int a = 1
2511 int b = 2
2512 a = 3
2513 b = 4
2514 a += 1
2515 b -= 1
2516 }",
2517 "foo.groovy",
2518 |metric| {
2519 // Six `=` tokens total. The two `Final`-less local
2520 // var-decls (`int a = 1`, `int b = 2`) and the two
2521 // bare assignments (`a = 3`, `b = 4`) each contribute
2522 // one assignment via the `EQ` arm; the `+=` / `-=`
2523 // each contribute one via the compound-assign arm.
2524 assert_eq!(metric.abc.assignments_sum(), 6);
2525 },
2526 );
2527 }
2528
2529 #[cfg(feature = "groovy")]
2530 #[test]
2531 fn groovy_branches() {
2532 check_metrics::<GroovyParser>(
2533 "void f() {
2534 doStuff()
2535 helper.invoke()
2536 new Worker()
2537 }",
2538 "foo.groovy",
2539 |metric| {
2540 // 2 method invocations + 1 object creation = 3 branches
2541 assert_eq!(metric.abc.branches_sum(), 3);
2542 },
2543 );
2544 }
2545
2546 #[cfg(feature = "groovy")]
2547 #[test]
2548 fn groovy_conditions_in_if() {
2549 check_metrics::<GroovyParser>(
2550 "void f(int a) {
2551 if (a == 0) { println(a) }
2552 if (a >= 1) { println(a) }
2553 if (a != 2) { println(a) }
2554 }",
2555 "foo.groovy",
2556 |metric| {
2557 // Three relational ops = 3 conditions
2558 assert_eq!(metric.abc.conditions_sum(), 3);
2559 },
2560 );
2561 }
2562
2563 #[cfg(feature = "groovy")]
2564 #[test]
2565 fn groovy_branches_with_juxt_call() {
2566 // Groovy's parens-less call form `println foo` must be counted
2567 // as a branch (`JuxtFunctionCall`).
2568 check_metrics::<GroovyParser>(
2569 "void f() {
2570 println 'hi'
2571 println 'bye'
2572 }",
2573 "foo.groovy",
2574 |metric| {
2575 // 2 juxt calls = 2 branches.
2576 assert_eq!(metric.abc.branches_sum(), 2);
2577 },
2578 );
2579 }
2580
2581 #[cfg(feature = "groovy")]
2582 #[test]
2583 fn groovy_try_catch_conditions() {
2584 // Each `try` and `catch` keyword token contributes +1 to
2585 // conditions (mirrors Java).
2586 check_metrics::<GroovyParser>(
2587 "void f() {
2588 try {
2589 risky()
2590 } catch (Exception e) {
2591 handle(e)
2592 }
2593 }",
2594 "foo.groovy",
2595 |metric| {
2596 // try + catch = 2 conditions
2597 assert_eq!(metric.abc.conditions_sum(), 2);
2598 },
2599 );
2600 }
2601
2602 #[cfg(feature = "groovy")]
2603 #[test]
2604 fn groovy_ternary_conditions() {
2605 check_metrics::<GroovyParser>(
2606 "void f(int x) {
2607 def y = x > 0 ? 1 : 2
2608 }",
2609 "foo.groovy",
2610 |metric| {
2611 // QMARK alone is +1 condition, plus the `>` condition = 2.
2612 assert_eq!(metric.abc.conditions_sum(), 2);
2613 },
2614 );
2615 }
2616
2617 #[cfg(feature = "groovy")]
2618 #[test]
2619 fn groovy_constant_excluded_from_assignments() {
2620 // `final` declarations are not counted as assignments
2621 // (mirrors Java's `Final` handling).
2622 check_metrics::<GroovyParser>(
2623 "class A {
2624 final int CONST = 42
2625 int field = 0
2626 }",
2627 "foo.groovy",
2628 |metric| {
2629 // The `=` on `final int CONST = 42` is a constant
2630 // initialiser (skipped). Only `field = 0` counts.
2631 assert_eq!(metric.abc.assignments_sum(), 1);
2632 },
2633 );
2634 }
2635
2636 #[cfg(feature = "groovy")]
2637 #[test]
2638 fn groovy_malformed_parenthesized_no_panic() {
2639 // Regression: malformed Groovy input must not panic the ABC
2640 // walker; the `spaces.rs` Unit fallback (lesson 9) covers
2641 // structural recovery. amaanq's grammar treats `def x = (((`
2642 // as a `local_variable_declaration` whose initialiser is the
2643 // first opening paren — the `=` still fires the assignment
2644 // arm.
2645 check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
2646 assert_eq!(metric.abc.assignments_sum(), 1);
2647 });
2648 }
2649
2650 #[cfg(feature = "groovy")]
2651 #[test]
2652 fn groovy_bool_returning_terminal_kinds_count() {
2653 // Companion to `csharp_bool_returning_terminal_kinds_count`
2654 // (issue #372 / lesson #19). The dekobon Groovy grammar
2655 // shares Java's wrapping conventions for `FieldAccess` and
2656 // `InstanceofExpression`, but it splits casts into two
2657 // distinct kinds — `cast_expression` for the Groovy-idiomatic
2658 // `v as Boolean` and `parenthesized_type_cast` for the
2659 // Java-style `(boolean) v`. The grammar has no `await` or
2660 // `array_access` analogues, so the C# fix's five-kind set
2661 // collapses to four here (with the cast slot doubled).
2662 //
2663 // `instanceof` has since moved out of the terminal set to an
2664 // unconditional arm (#1461), so it no longer depends on the
2665 // slot to be counted. The total is unchanged, which is the
2666 // point of that change rather than an accident of it.
2667 //
2668 // expected: 4 conditions (one per `if`), 0 assignments,
2669 // 0 branches (no invocations).
2670 check_metrics::<GroovyParser>(
2671 "class Cfg { boolean flag }
2672 class A {
2673 void m(Object v, Cfg cfg) {
2674 if (cfg.flag) { }
2675 if ((boolean) v) { }
2676 if (v as Boolean) { }
2677 if (v instanceof Cfg) { }
2678 }
2679 }",
2680 "foo.groovy",
2681 |metric| {
2682 assert_eq!(metric.abc.conditions_sum(), 4);
2683 assert_eq!(metric.abc.assignments_sum(), 0);
2684 assert_eq!(metric.abc.branches_sum(), 0);
2685 },
2686 );
2687 }
2688
2689 // Groovy half of #1274 — see `java_generic_declarations_are_not_conditions`.
2690 // The class-level `type_parameters` and its bound's nested
2691 // `type_arguments` mirror the Java fixture; pre-fix this file scored
2692 // 2 conditions, expected 0.
2693 #[cfg(feature = "groovy")]
2694 #[test]
2695 fn groovy_generic_declarations_are_not_conditions() {
2696 check_metrics::<GroovyParser>(
2697 "class Gen<T extends Comparable<T>> {
2698 List<T> pick(Map<String, List<T>> m) { m.get(\"k\") }
2699 }",
2700 "foo.groovy",
2701 |metric| {
2702 assert_eq!(metric.abc.conditions_sum(), 0);
2703 // Non-vacuity guard — see the Java counterpart.
2704 assert_eq!(metric.abc.branches_sum(), 1);
2705 },
2706 );
2707 }
2708
2709 // Groovy counterpart of `java_generic_wildcard_is_not_a_condition`,
2710 // including its choice of a non-zero expected total and its
2711 // two-wildcards-one-ternary shape; the dekobon grammar emits the
2712 // same `wildcard` node. Pre-fix: 4.
2713 #[cfg(feature = "groovy")]
2714 #[test]
2715 fn groovy_generic_wildcard_is_not_a_condition() {
2716 check_metrics::<GroovyParser>(
2717 "class A {
2718 int m(List<? extends Number> xs, Map<String, ? extends Number> ys,
2719 int a, int b) { a < b ? 1 : 2 }
2720 }",
2721 "foo.groovy",
2722 |metric| {
2723 assert_eq!(metric.abc.conditions_sum(), 2);
2724 },
2725 );
2726 }
2727
2728 // A generic *method* gets its own fixture because the dekobon
2729 // grammar gives it its own production: `def <U> U ident(U x)` emits
2730 // `method_type_parameters`, not the `type_parameters` the class form
2731 // above uses. It is the fourth and last production from which that
2732 // grammar emits a bare `<` / `>` (the others being
2733 // `binary_expression`, `type_arguments` and `type_parameters`), and
2734 // the one a denylist extended only with `TypeParameters` — the fix
2735 // this issue's own plan proposed — would still miss. Revert-verified
2736 // in both directions: under that denylist this test still fails
2737 // while `groovy_generic_declarations_are_not_conditions` passes.
2738 //
2739 // The body carries a ternary over a comparison for the same
2740 // non-vacuity reason as the wildcard tests: expected 2, pre-fix 4
2741 // (the `<U>` bracket pair), 0 if the fixture stops parsing.
2742 #[cfg(feature = "groovy")]
2743 #[test]
2744 fn groovy_method_type_parameters_are_not_conditions() {
2745 check_metrics::<GroovyParser>(
2746 "class A {
2747 def <U> U ident(U x, int a, int b) { a < b ? x : null }
2748 }",
2749 "foo.groovy",
2750 |metric| {
2751 assert_eq!(metric.abc.conditions_sum(), 2);
2752 },
2753 );
2754 }
2755
2756 // Groovy counterpart of
2757 // `java_comparison_operators_still_count_alongside_generics`: the
2758 // narrowed arm keeps counting real comparisons, pinned against the
2759 // cyclomatic decision count on the method's own space (§8).
2760 #[cfg(feature = "groovy")]
2761 #[test]
2762 fn groovy_elvis_counts_one_condition_per_token() {
2763 // `a ?: c` is a short-circuit decision Groovy cyclomatic already
2764 // counts and the Kotlin ABC arm counts for its identical token;
2765 // Groovy's arm listed neither `?:` nor its `elvis_expression`, so
2766 // a method whose only branching is elvis chains reported
2767 // cyclomatic > 1 with zero conditions. One per token, so the
2768 // grammar-dispatch §8 invariant holds on the chain:
2769 // `conditions() == cyclomatic() - 1`.
2770 check_func_space::<GroovyParser, _>(
2771 "class K { def f(a, c) { return a ?: c } }",
2772 "foo.groovy",
2773 |space| assert_deepest_conditions_match_cyclomatic(&space, 1),
2774 );
2775 check_func_space::<GroovyParser, _>(
2776 "class K { def g(a, b, c) { return a ?: b ?: c } }",
2777 "foo.groovy",
2778 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2779 );
2780 }
2781
2782 #[cfg(feature = "groovy")]
2783 #[test]
2784 fn groovy_comparison_operators_still_count_alongside_generics() {
2785 check_func_space::<GroovyParser, _>(
2786 "class A {
2787 int m(List<String> xs, int a, int b) {
2788 if (a < b) { return 1 }
2789 if (a > b) { return 2 }
2790 return 0
2791 }
2792 }",
2793 "foo.groovy",
2794 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
2795 );
2796 }
2797
2798 #[cfg(feature = "groovy")]
2799 #[test]
2800 fn groovy_if_multiple_conditions() {
2801 // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
2802 // and parenthesised unary forms each contribute one
2803 // condition per primitive comparison; the inspect-container
2804 // pass picks up the unary `!a` / `!b` arguments inside the
2805 // `BinaryExpression` and counts them too.
2806 check_metrics::<GroovyParser>(
2807 "void f(boolean a, boolean b, boolean c) {
2808 if (a || b || c) { println(a) }
2809 if (a && b && c) { println(a) }
2810 if (!a && !b) { println(a) }
2811 }",
2812 "foo.groovy",
2813 |metric| {
2814 // Conditions counted via the AMPAMP/PIPEPIPE arms
2815 // (one count per identifier in the chain — three
2816 // for `||`, three for `&&`, two for the unary chain)
2817 // = 8.
2818 assert_eq!(metric.abc.conditions_sum(), 8);
2819 // Three `println a` juxt calls — each is a branch.
2820 assert_eq!(metric.abc.branches_sum(), 3);
2821 },
2822 );
2823 }
2824
2825 #[cfg(feature = "groovy")]
2826 #[test]
2827 fn groovy_while_and_do_while_conditions() {
2828 // Covers the WhileStatement and DoStatement arms in
2829 // `impl Abc for GroovyCode`. Each `while` / `do-while` has
2830 // its condition scored through `groovy_count_condition`.
2831 check_metrics::<GroovyParser>(
2832 "void f(boolean a, boolean b) {
2833 while (a) {
2834 a = false
2835 }
2836 do {
2837 b = !b
2838 } while (b)
2839 }",
2840 "foo.groovy",
2841 |metric| {
2842 // `while(a)` + `while(b)` each contribute one condition;
2843 // the unary `!b` on the do body's right-hand side adds
2844 // one more via the assignment-arm inspection = 3.
2845 assert_eq!(metric.abc.conditions_sum(), 3);
2846 // Two assignments to existing variables (`a = false`,
2847 // `b = !b`).
2848 assert_eq!(metric.abc.assignments_sum(), 2);
2849 },
2850 );
2851 }
2852
2853 #[cfg(feature = "groovy")]
2854 #[test]
2855 fn groovy_if_while_boolean_literal_condition() {
2856 // Regression for the Groovy half of #371-class bugs: the
2857 // dekobon tree-sitter-groovy grammar wraps a bare
2858 // `true` / `false` literal used as the condition of
2859 // `if` / `while` / `do` / `?:` in a `boolean_literal` node
2860 // (`Groovy::BooleanLiteral`, kind_id 270), not the leaf
2861 // `True` / `False` keyword tokens. `groovy_count_condition`
2862 // must therefore match `BooleanLiteral` (the wrapper).
2863 // Without that, every literal-condition statement silently
2864 // scored 0 conditions. Mirror of
2865 // `csharp_if_while_boolean_literal_condition`.
2866 check_metrics::<GroovyParser>(
2867 "void m() {
2868 if (true) { println 'a' }
2869 if (false) { println 'b' }
2870 while (true) { break }
2871 int t = true ? 1 : 0
2872 }",
2873 "foo.groovy",
2874 |metric| {
2875 // Four literal-condition statements contribute 4
2876 // `BooleanLiteral` conditions (if / if / while /
2877 // ternary), plus the ternary's `?` token adds one
2878 // more via `groovy_count_token_condition` → 5
2879 // total. The `println` calls contribute 2 branches
2880 // (the `while` body's `break` is not a branch).
2881 // The `int t = …` initializer contributes 1
2882 // assignment.
2883 assert_eq!(metric.abc.conditions_sum(), 5);
2884 assert_eq!(metric.abc.branches_sum(), 2);
2885 assert_eq!(metric.abc.assignments_sum(), 1);
2886 },
2887 );
2888 }
2889
2890 #[cfg(feature = "groovy")]
2891 #[test]
2892 fn groovy_return_unary_boolean_literal() {
2893 // Companion to `groovy_if_while_boolean_literal_condition`:
2894 // a `!true` / `!false` operand inside a `return` statement
2895 // routes through `groovy_count_negated`. The `!` proves
2896 // boolean context, and a `BooleanLiteral` does not score
2897 // itself (`groovy_condition_scores_itself`), so the
2898 // negated operand pays one condition.
2899 check_metrics::<GroovyParser>(
2900 "boolean f() {
2901 return !true
2902 }
2903 boolean g() {
2904 return !false
2905 }",
2906 "foo.groovy",
2907 |metric| {
2908 // Each `return !X` walks into `groovy_count_negated`
2909 // with a UnaryExpression wrapping a `BANG` +
2910 // BooleanLiteral. The peel through `!` proves boolean
2911 // context (a `return` is not a slot), then the
2912 // BooleanLiteral operand contributes one condition.
2913 // Two `return !X` → 2 conditions, no branches, no
2914 // assignments.
2915 assert_eq!(metric.abc.conditions_sum(), 2);
2916 assert_eq!(metric.abc.branches_sum(), 0);
2917 assert_eq!(metric.abc.assignments_sum(), 0);
2918 },
2919 );
2920 }
2921
2922 #[cfg(feature = "groovy")]
2923 #[test]
2924 fn groovy_short_circuit_with_boolean_literal_operand() {
2925 // Companion to `groovy_if_while_boolean_literal_condition`:
2926 // a bare `true` / `false` operand of `&&` / `||` is a
2927 // chain-operand slot scored through `groovy_count_condition`.
2928 // A `BooleanLiteral` does not score itself, so the slot pays
2929 // for it; otherwise the operand would silently score zero.
2930 check_metrics::<GroovyParser>(
2931 "void m(boolean x) {
2932 if (x && true) { println 'a' }
2933 if (false || x) { println 'b' }
2934 }",
2935 "foo.groovy",
2936 |metric| {
2937 // `&&` and `||` themselves are NOT in
2938 // `groovy_count_token_condition`'s match list —
2939 // they route through
2940 // `groovy_walk_for_conditions::AMPAMP|PIPEPIPE`,
2941 // which scores each operand of the parent
2942 // BinaryExpression through `groovy_count_condition`.
2943 // An operand pays one condition unless it scores
2944 // itself. For `x && true`: Identifier x
2945 // (+1) + BooleanLiteral true (+1) = 2. For
2946 // `false || x`: BooleanLiteral false (+1) +
2947 // Identifier x (+1) = 2. Total 4.
2948 assert_eq!(metric.abc.conditions_sum(), 4);
2949 assert_eq!(metric.abc.branches_sum(), 2);
2950 assert_eq!(metric.abc.assignments_sum(), 0);
2951 },
2952 );
2953 }
2954
2955 #[cfg(feature = "groovy")]
2956 #[test]
2957 fn groovy_methods_arguments_with_conditions() {
2958 // Mirror of `java_methods_arguments_with_conditions`: a
2959 // unary `!x` inside an argument list must count both the
2960 // method invocation as a branch AND the unary as a
2961 // condition. The `ArgumentList | ArgumentList2` arm in
2962 // `impl Abc for GroovyCode` is what exercises this.
2963 check_metrics::<GroovyParser>(
2964 "void f(boolean a, boolean b, boolean c) {
2965 m1(a)
2966 m1(!a)
2967 m2(!a, !b)
2968 }",
2969 "foo.groovy",
2970 |metric| {
2971 // 3 method invocations (m1, m1, m2) — each fires the
2972 // branches arm.
2973 assert_eq!(metric.abc.branches_sum(), 3);
2974 // Three `!` unaries — `m1(!a)` and the two args of
2975 // `m2(!a, !b)` — each contribute one condition via
2976 // the ArgumentList inspection.
2977 assert_eq!(metric.abc.conditions_sum(), 3);
2978 },
2979 );
2980 }
2981
2982 #[cfg(feature = "groovy")]
2983 #[test]
2984 fn groovy_return_with_conditions() {
2985 // Mirror of `java_return_with_conditions`: a parenthesised
2986 // or unary expression inside `return` flows through the
2987 // `ReturnStatement` arm to `groovy_count_negated`.
2988 check_metrics::<GroovyParser>(
2989 "boolean f(boolean a) {
2990 return (a)
2991 }
2992 boolean g(boolean a) {
2993 return !a
2994 }",
2995 "foo.groovy",
2996 |metric| {
2997 // Only one of the two return forms surfaces a
2998 // condition: `return !a` hits the UnaryExpression
2999 // path and adds one; `return (a)` reaches
3000 // `groovy_count_negated` but no `!` proves the inner
3001 // identifier `a` boolean, so no condition is added.
3002 assert_eq!(metric.abc.conditions_sum(), 1);
3003 },
3004 );
3005 }
3006
3007 #[cfg(feature = "groovy")]
3008 #[test]
3009 fn groovy_for_with_variable_declaration() {
3010 // Classical `for (int i = 0; cond; i++)` form. The init
3011 // slot's `int i = 0` is suppressed from assignments by the
3012 // `LocalVariableDeclaration` push/pop dance; the `i++` in
3013 // the update slot contributes one assignment via the
3014 // `PLUSPLUS` arm. The condition `i < 10` flows through the
3015 // `ForStatement` arm.
3016 check_metrics::<GroovyParser>(
3017 "void f() {
3018 for (int i = 0; i < 10; i++) {
3019 println(i)
3020 }
3021 }",
3022 "foo.groovy",
3023 |metric| {
3024 // `int i = 0` fires the EQ arm + `i++` fires the
3025 // PLUSPLUS arm = 2 assignments.
3026 assert_eq!(metric.abc.assignments_sum(), 2);
3027 // `i < 10` is one condition (the LT arm).
3028 assert_eq!(metric.abc.conditions_sum(), 1);
3029 },
3030 );
3031 }
3032
3033 /// The existing `for` test uses `i < 10`, which the `LT` token arm
3034 /// counts on its own — `groovy_walk_for_statement` never
3035 /// contributes there, so it stayed uncovered. A bare-identifier
3036 /// condition has no comparison token, so the count can only come
3037 /// from the walker.
3038 #[cfg(feature = "groovy")]
3039 #[test]
3040 fn groovy_for_with_bare_identifier_condition() {
3041 check_metrics::<GroovyParser>(
3042 "void f(boolean go) {
3043 for (int i = 0; go; i++) {
3044 println(i)
3045 }
3046 }",
3047 "foo.groovy",
3048 |metric| {
3049 // `go` is the whole condition and counts once.
3050 assert_eq!(metric.abc.conditions_sum(), 1);
3051 // `int i = 0` (EQ) + `i++` (PLUSPLUS) = 2, as in
3052 // `groovy_for_with_variable_declaration`.
3053 assert_eq!(metric.abc.assignments_sum(), 2);
3054 },
3055 );
3056 }
3057
3058 /// The same slot with the initialiser hoisted out of the header.
3059 /// Under the pre-#1276 positional cascade this was a distinct code
3060 /// path — the condition moved from child(4) to child(3) — and the
3061 /// pair is kept as a shape guard now that the walker reads the
3062 /// `condition` field and cannot see the difference.
3063 #[cfg(feature = "groovy")]
3064 #[test]
3065 fn groovy_for_with_empty_initializer_counts_the_condition() {
3066 check_metrics::<GroovyParser>(
3067 "void f(boolean go) {
3068 int i = 0
3069 for (; go; i++) {
3070 println(i)
3071 }
3072 }",
3073 "foo.groovy",
3074 |metric| {
3075 assert_eq!(metric.abc.conditions_sum(), 1);
3076 // `int i = 0` (EQ) + `i++` (PLUSPLUS), as above — the
3077 // initialiser just moved out of the loop header.
3078 assert_eq!(metric.abc.assignments_sum(), 2);
3079 },
3080 );
3081 }
3082
3083 /// Issue #1276 changed Groovy's answer here, exactly as it changed
3084 /// Java's: the positional cascade counted a `;` / `)` landing at
3085 /// child(4) as a vacuously-true condition, so `for (;;)` scored
3086 /// one. An omitted test is not a decision, and every other impl
3087 /// scores it zero. See `java_empty_for_condition_counts_nothing`.
3088 #[cfg(feature = "groovy")]
3089 #[test]
3090 fn groovy_empty_for_condition_counts_nothing() {
3091 check_metrics::<GroovyParser>("void f() { for (;;) { break } }", "foo.groovy", |metric| {
3092 assert_eq!(metric.abc.conditions_sum(), 0);
3093 });
3094 // The second spelling that moved. Unlike Java's, Groovy's
3095 // `local_variable_declaration` does not swallow its `;`, so the
3096 // old cascade found `;` at child(3) and `;` at child(4) here
3097 // too, and counted one.
3098 check_metrics::<GroovyParser>(
3099 "void f() { for (int i = 0; ; i++) { break } }",
3100 "foo.groovy",
3101 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
3102 );
3103 }
3104
3105 /// The cascade's other defect, shared with Java: a comment in the
3106 /// header shifted every child index, so the condition went unread.
3107 /// Reading the `condition` field cannot shift.
3108 #[cfg(feature = "groovy")]
3109 #[test]
3110 fn groovy_for_condition_survives_a_header_comment() {
3111 check_metrics::<GroovyParser>(
3112 "void f(boolean go) { for (; /* n */ go; ) { break } }",
3113 "foo.groovy",
3114 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
3115 );
3116 // Unchanged control: the same loop without the comment.
3117 check_metrics::<GroovyParser>(
3118 "void f(boolean go) { for (; go; ) { break } }",
3119 "foo.groovy",
3120 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
3121 );
3122 }
3123
3124 /// C#'s `csharp_walk_for_statement` reads the loop condition off
3125 /// the named `condition` field and scores a parenthesised or
3126 /// `!`-prefixed one through `csharp_count_condition`. Every other
3127 /// C# `for` test uses a comparison (`i < n`), which the `LT` token
3128 /// arm counts without entering the walker.
3129 #[cfg(feature = "csharp")]
3130 #[test]
3131 fn csharp_for_with_negated_condition() {
3132 check_metrics::<CsharpParser>(
3133 "class A {
3134 void M(bool done) {
3135 for (int i = 0; !done; i++) { System.Console.WriteLine(i); }
3136 }
3137 }",
3138 "foo.cs",
3139 |metric| {
3140 // `!done` unwraps to the `done` terminal: one condition,
3141 // and no comparison token to double-count it.
3142 assert_eq!(metric.abc.conditions_sum(), 1);
3143 // `int i = 0` + `i++`.
3144 assert_eq!(metric.abc.assignments_sum(), 2);
3145 assert_eq!(metric.abc.branches_sum(), 1);
3146 },
3147 );
3148 }
3149
3150 #[cfg(feature = "groovy")]
3151 #[test]
3152 fn groovy_eq_arm_counts_outside_final_declarations() {
3153 // Bare reassignment of an already-declared variable: the `=`
3154 // belongs to no `final` declaration, so it counts. Mirrors
3155 // `java_eq_arm_counts_outside_final_declarations`.
3156 check_metrics::<GroovyParser>(
3157 "void f(int x) {
3158 x = 42
3159 }",
3160 "foo.groovy",
3161 |metric| {
3162 assert_eq!(metric.abc.assignments_sum(), 1);
3163 assert_eq!(metric.abc.branches_sum(), 0);
3164 assert_eq!(metric.abc.conditions_sum(), 0);
3165 },
3166 );
3167 }
3168
3169 #[cfg(feature = "groovy")]
3170 #[test]
3171 fn groovy_final_field_initializer_does_not_suppress_the_closure_body() {
3172 // The Groovy spelling of
3173 // `java_final_initializer_does_not_suppress_the_assignments_inside_it`:
3174 // a `final` field's closure body opens no space, and the sentinel
3175 // stack suppressed its `x = 1` with the declarator's own `=`.
3176 check_metrics::<GroovyParser>(
3177 "class K {
3178 int x
3179 final Closure c = { x = 1 }
3180 Closure d = { x = 2 }
3181 final int q = 3
3182 }",
3183 "foo.groovy",
3184 |metric| {
3185 // `x = 1`, `d = {…}`, `x = 2`; the two `final`
3186 // initializers are suppressed. Pre-fix: 2.
3187 assert_eq!(metric.abc.assignments_sum(), 3);
3188 },
3189 );
3190 }
3191
3192 #[cfg(feature = "groovy")]
3193 #[test]
3194 fn groovy_final_local_is_an_error_at_the_pinned_grammar() {
3195 // `groovy_eq_initializes_final_binding` lists
3196 // `local_variable_declaration` for symmetry with Java, but at
3197 // dekobon-tree-sitter-groovy 0.3.0 a `final` local never
3198 // reaches it: the parser emits an `ERROR` node for the
3199 // declaration, with or without a terminator, so both of its `=`
3200 // tokens count. This pins the grammar limitation so a bump that
3201 // starts parsing the local shows up here rather than as a silent
3202 // change in what the predicate suppresses.
3203 let source = b"class K { int x; def m() { final int a = 0; x = 1 } }";
3204 let parser = GroovyParser::new(
3205 source.to_vec(),
3206 &std::path::PathBuf::from("foo.groovy"),
3207 None,
3208 );
3209 assert!(
3210 ast_has_kind_id(&parser, u16::MAX),
3211 "a `final` local should still parse to an ERROR node; if the grammar \
3212 now accepts it, re-derive the local half of the predicate",
3213 );
3214 check_metrics::<GroovyParser>(
3215 "class K { int x; def m() { final int a = 0; x = 1 } }",
3216 "foo.groovy",
3217 |metric| {
3218 assert_eq!(metric.abc.assignments_sum(), 2);
3219 },
3220 );
3221 }
3222
3223 #[cfg(feature = "csharp")]
3224 #[test]
3225 fn csharp_const_initializer_shapes() {
3226 // `csharp_eq_initializes_const_binding` reads the `const`
3227 // through the `modifier` node one hop above the
3228 // `variable_declaration`, for a local and a field alike, behind
3229 // other modifiers; `readonly` is not `const` and its initializer
3230 // counts. A lambda's body opens its own space in C#, so unlike
3231 // Java there was never a nested `=` to leak — the row pins that
3232 // the structural rule keeps the count the sentinel gave.
3233 check_metrics::<CsharpParser>(
3234 "class K {
3235 int x;
3236 private const int Q = 1;
3237 public static readonly int R = 2;
3238 void M() { const int q = 3; System.Action a = () => { x = 4; }; }
3239 }",
3240 "foo.cs",
3241 |metric| {
3242 // `R = 2`, `a = () => …`, `x = 4`.
3243 assert_eq!(metric.abc.assignments_sum(), 3);
3244 },
3245 );
3246 }
3247
3248 #[cfg(feature = "csharp")]
3249 #[test]
3250 fn csharp_constant_declarations() {
3251 check_metrics::<CsharpParser>(
3252 "class A {
3253 private const int X1 = 0, Y1 = 0;
3254 public const float PI = 3.14f;
3255 const string HELLO = \"Hello,\";
3256 protected string world = \" world!\";
3257 public float e = 2.718f;
3258 private int x2 = 1, y2 = 2;
3259 void M() {
3260 const int Z1 = 0, Z2 = 0, Z3 = 0;
3261 const float T = 0.0f;
3262 int z1 = 1, z2 = 2, z3 = 3;
3263 }
3264 }",
3265 "foo.cs",
3266 |metric| insta::assert_json_snapshot!(metric.abc),
3267 );
3268 }
3269
3270 #[cfg(feature = "csharp")]
3271 #[test]
3272 fn csharp_declarations_with_conditions() {
3273 check_metrics::<CsharpParser>(
3274 "class A {
3275 bool a = (1 == 2);
3276 bool b = (1 < 2);
3277 bool c = !true;
3278 bool d = !false;
3279 }",
3280 "foo.cs",
3281 |metric| insta::assert_json_snapshot!(metric.abc),
3282 );
3283 }
3284
3285 #[cfg(feature = "csharp")]
3286 #[test]
3287 fn csharp_assignments_with_conditions() {
3288 check_metrics::<CsharpParser>(
3289 "class A {
3290 void M() {
3291 int a = 0;
3292 a += 1;
3293 a -= 2;
3294 a *= 3;
3295 a /= 4;
3296 a %= 5;
3297 a++;
3298 a--;
3299 }
3300 }",
3301 "foo.cs",
3302 |metric| insta::assert_json_snapshot!(metric.abc),
3303 );
3304 }
3305
3306 #[cfg(feature = "csharp")]
3307 #[test]
3308 fn csharp_methods_arguments_with_conditions() {
3309 check_metrics::<CsharpParser>(
3310 "class A {
3311 void M(int x, int y) {
3312 F(x == y, x < y, !x.Equals(y));
3313 }
3314 void F(bool a, bool b, bool c) {}
3315 }",
3316 "foo.cs",
3317 |metric| {
3318 // expected: `==` +1, `<` +1, and the negated argument
3319 // `!x.Equals(y)` +1 — it scored 0 until #1537, when every
3320 // C# argument reached the peel still in its `argument`
3321 // wrapper.
3322 assert_eq!(metric.abc.conditions_sum(), 3);
3323 insta::assert_json_snapshot!(metric.abc);
3324 },
3325 );
3326 }
3327
3328 // #1537: every C# argument is wrapped in an `argument` node that the
3329 // negation peel declined, so a negated argument scored 0 where every
3330 // other C-family language scores 1. Each row is a C# call next to its
3331 // Java twin, which takes the expected value from outside the fix.
3332 #[cfg(all(feature = "csharp", feature = "java"))]
3333 #[test]
3334 fn csharp_negated_call_argument_matches_java() {
3335 let rows = [
3336 ("G(!a);", "g(!a);", 1),
3337 ("G((!a));", "g((!a));", 1),
3338 ("G(!a, !b);", "g(!a, !b);", 2),
3339 ("G(a, !b);", "g(a, !b);", 1),
3340 ("new B(!a);", "new B(!a);", 1),
3341 ("G(-a);", "g(-a);", 0),
3342 // A negated comparison pays once, for its `>`; a chain pays
3343 // per operand. Neither argument may pay again as a negation.
3344 ("G(!(a > b));", "g(!(a > b));", 1),
3345 ("G(!a && b);", "g(!a && b);", 2),
3346 ];
3347 for (csharp, java, expected) in rows {
3348 let cs = abc_conditions(
3349 LANG::Csharp,
3350 &format!("class A {{ void M() {{ {csharp} }} }}"),
3351 );
3352 let jv = abc_conditions(LANG::Java, &format!("class A {{ void m() {{ {java} }} }}"));
3353 assert_eq!(jv, expected, "Java twin `{java}`");
3354 assert_eq!(cs, expected, "C# `{csharp}` against Java `{java}`");
3355 }
3356 }
3357
3358 // The C#-only spellings around an argument. The value is the
3359 // `argument` wrapper's last operand, so a named-argument label, a
3360 // passing-mode keyword or a comment before it must not hide it.
3361 #[cfg(feature = "csharp")]
3362 #[test]
3363 fn csharp_negated_call_argument_spellings() {
3364 let rows = [
3365 // A named argument: the value follows `name` and `:`.
3366 ("class A { void M() { G(name: !a); } }", 1),
3367 // A comment between the label and the value sits inside the
3368 // `argument` wrapper, so a positional read of the value lands
3369 // on it. A comment before or after a whole argument is a
3370 // sibling of the wrapper instead and never reaches this read.
3371 ("class A { void M() { G(name: /*c*/ !a); } }", 1),
3372 // `ref` / `out` / `in` precede a variable, never a negation. A
3373 // control only: 0 is also what the pre-#1537 walk scored, so
3374 // this row cannot fail for any defect the fix addresses.
3375 ("class A { void M() { G(ref x, out var y, in z); } }", 0),
3376 // The lambda arm pays for its `!a` body; the argument holding
3377 // the lambda is not a negation and must not pay again.
3378 ("class A { void M() { G(x => !a); } }", 1),
3379 // `: base(!a)` — a `constructor_initializer`'s list reaches the
3380 // walk like any call's.
3381 ("class A : B { A(bool a) : base(!a) {} }", 1),
3382 // The two primary-constructor base calls: the record nests its
3383 // list in a `primary_constructor_base_type`, the class's list
3384 // is the branch node itself and is scored on the branch path.
3385 ("record R(bool a) : Base(!a);", 1),
3386 ("class S(bool a) : Base(!a) {}", 1),
3387 // An indexer's `bracketed_argument_list` is not a call, as
3388 // Java's array index is not.
3389 ("class A { void M() { var q = o[!a]; } }", 0),
3390 ];
3391 for (src, expected) in rows {
3392 assert_eq!(abc_conditions(LANG::Csharp, src), expected, "{src}");
3393 }
3394 }
3395
3396 #[cfg(feature = "csharp")]
3397 #[test]
3398 fn csharp_if_single_conditions() {
3399 check_metrics::<CsharpParser>(
3400 "class A {
3401 void M(int x) {
3402 if (x > 0) { System.Console.WriteLine(\"a\"); }
3403 if (x < 0) { System.Console.WriteLine(\"b\"); }
3404 if (x == 0) { System.Console.WriteLine(\"c\"); }
3405 }
3406 }",
3407 "foo.cs",
3408 |metric| insta::assert_json_snapshot!(metric.abc),
3409 );
3410 }
3411
3412 #[cfg(feature = "csharp")]
3413 #[test]
3414 fn csharp_if_multiple_conditions() {
3415 check_metrics::<CsharpParser>(
3416 "class A {
3417 void M(int x, int y) {
3418 if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
3419 if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
3420 }
3421 }",
3422 "foo.cs",
3423 |metric| insta::assert_json_snapshot!(metric.abc),
3424 );
3425 }
3426
3427 #[cfg(feature = "csharp")]
3428 #[test]
3429 fn csharp_while_and_do_while_conditions() {
3430 check_metrics::<CsharpParser>(
3431 "class A {
3432 void M(int x) {
3433 while (x > 0) { x--; }
3434 do { x++; } while (x < 10);
3435 }
3436 }",
3437 "foo.cs",
3438 |metric| insta::assert_json_snapshot!(metric.abc),
3439 );
3440 }
3441
3442 #[cfg(feature = "csharp")]
3443 #[test]
3444 fn csharp_return_with_conditions() {
3445 check_metrics::<CsharpParser>(
3446 "class A {
3447 bool M(int x) {
3448 return (x > 0);
3449 }
3450 bool N(int x) {
3451 return !(x < 0);
3452 }
3453 }",
3454 "foo.cs",
3455 |metric| insta::assert_json_snapshot!(metric.abc),
3456 );
3457 }
3458
3459 // C# `switch` *expression* arms scored zero ABC conditions before
3460 // #456 — they carry no `case` / `default` token, so the token-driven
3461 // `csharp_count_token_condition` never saw them, even though C#
3462 // cyclomatic counts each non-discard arm. Revert-verified: adding the
3463 // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The
3464 // bare `_ =>` discard arm is excluded (the `default:` analogue),
3465 // mirroring the cyclomatic gate (lesson 11).
3466 #[cfg(feature = "csharp")]
3467 #[test]
3468 fn csharp_switch_expression_arm_counts_condition() {
3469 check_metrics::<CsharpParser>(
3470 "class A {
3471 int M(int x) {
3472 return x switch { 1 => 10, 2 => 20, _ => 0 };
3473 }
3474 }",
3475 "foo.cs",
3476 |metric| {
3477 // arm `1 =>` (+1) + arm `2 =>` (+1) + `_ =>` discard (+0).
3478 assert_eq!(metric.abc.conditions_sum(), 2);
3479 },
3480 );
3481 }
3482
3483 // Cross-language parity (lesson 11): a C# `switch` expression and the
3484 // equivalent Java arrow-`switch` must report the same ABC condition
3485 // count on equivalent code. Both have two concrete case arms and no
3486 // fallback arm, so both must count exactly 2. `check_metrics` takes a
3487 // non-capturing `fn` pointer, so the shared expected value (2) is
3488 // asserted in each callback rather than compared across closures; the
3489 // matching constant is what enforces parity. This guards against the
3490 // C# fix drifting away from the Java arrow-case treatment.
3491 #[cfg(all(feature = "csharp", feature = "java"))]
3492 #[test]
3493 fn csharp_java_switch_arm_abc_parity() {
3494 // C# switch expression: two arms, no fallback → 2 conditions.
3495 check_metrics::<CsharpParser>(
3496 "class A {
3497 int M(int x) {
3498 return x switch { 1 => 10, 2 => 20 };
3499 }
3500 }",
3501 "foo.cs",
3502 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3503 );
3504
3505 // Equivalent Java arrow-`switch`: two case arms, no default → 2.
3506 check_metrics::<JavaParser>(
3507 "class A {
3508 int m(int x) {
3509 return switch (x) { case 1 -> 10; case 2 -> 20; };
3510 }
3511 }",
3512 "foo.java",
3513 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3514 );
3515 }
3516
3517 // Issue #469: the `default` arm of a C-family `switch` is the
3518 // unconditional fallthrough and must NOT count as an ABC condition,
3519 // mirroring cyclomatic — which counts only the `Case` arms, never
3520 // the `Default` token.
3521 //
3522 // expected: each fixture is a single function whose switch has two
3523 // concrete `case` arms plus one `default`. ABC must count exactly
3524 // the two case arms (conditions = 2), matching cyclomatic's two
3525 // case-arm decisions. Pre-fix, every language below scored 3 (the
3526 // `Default` token leaked into the condition tally) — revert-verified
3527 // against the pre-#469 condition arms. We anchor on the integer
3528 // `conditions_sum()` headline (the value the public JSON serializes;
3529 // float magnitude is bit-brittle and excluded by the snapshot
3530 // policy). The cyclomatic side is pinned separately in
3531 // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where
3532 // the per-space `cyclomatic()` decision count is isolated.
3533 #[cfg(feature = "java")]
3534 #[test]
3535 fn java_switch_default_not_a_condition() {
3536 // Classic statement `default:`.
3537 check_metrics::<JavaParser>(
3538 "class A {
3539 int m(int x) {
3540 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3541 }
3542 }",
3543 "foo.java",
3544 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3545 );
3546 // Arrow `default ->` — shares the same `Default` token.
3547 check_metrics::<JavaParser>(
3548 "class A {
3549 int m(int x) {
3550 return switch (x) { case 1 -> 1; case 2 -> 2; default -> 0; };
3551 }
3552 }",
3553 "foo.java",
3554 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3555 );
3556 }
3557
3558 // Over-exclusion guard (issue #469): a statement `switch` with two
3559 // `case` arms and NO `default` must still count both cases. This
3560 // pins that the fix excludes only the `Default` token, never a
3561 // `Case` arm — the count is identical before and after #469 (two
3562 // cases → 2), so it would catch a fix that over-eagerly dropped a
3563 // real case (e.g. treating the trailing case as a fallthrough).
3564 // expected: case 1 (+1) + case 2 (+1) = 2.
3565 #[cfg(feature = "java")]
3566 #[test]
3567 fn java_switch_without_default_counts_all_cases() {
3568 check_metrics::<JavaParser>(
3569 "class A {
3570 int m(int x) {
3571 switch (x) { case 1: return 1; case 2: return 2; }
3572 return -1;
3573 }
3574 }",
3575 "foo.java",
3576 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3577 );
3578 }
3579
3580 #[cfg(feature = "csharp")]
3581 #[test]
3582 fn csharp_switch_default_not_a_condition() {
3583 check_metrics::<CsharpParser>(
3584 "class A {
3585 int M(int x) {
3586 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3587 }
3588 }",
3589 "foo.cs",
3590 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3591 );
3592 }
3593
3594 /// Regression #1450 / #1451: `goto case 2;` spells the same `case`
3595 /// keyword token (id 114) as a real arm, from a second production
3596 /// (`goto_statement`), and scored an ABC condition it does not earn —
3597 /// an unconditional jump to an arm is not a decision.
3598 ///
3599 /// `jmp` differs from `ctl` by exactly one statement, so the
3600 /// `goto case` is the only thing that can separate their scores
3601 /// (`.claude/rules/grammar-dispatch.md` §11 — an arm and a
3602 /// `goto case` are independent paths into the same counter, and a
3603 /// fixture carrying only arms cannot show the gate works). `dfl` is
3604 /// the control that was already correct, by accident rather than
3605 /// design: `goto default;` spells `Default`, which neither metric
3606 /// counts because it is the switch's unconditional fallthrough
3607 /// (#456, #469).
3608 ///
3609 /// [`assert_every_member_scores`] covers cyclomatic as well as ABC,
3610 /// through its per-member §8 parity assertion, and would catch the
3611 /// gate being removed from *either* side alone: dropping ABC's puts
3612 /// `jmp` at conditions 3 against the asserted 2, and dropping
3613 /// cyclomatic's leaves conditions 2 against a decision count of 3.
3614 /// Removing both restores the parity at `3 == 4 - 1` and is caught by
3615 /// the conditions value, which is why 2 is named rather than derived.
3616 ///
3617 /// The `Case` anchor is the second axis. Every member scores 2 from
3618 /// its two arms alone, so deleting `goto case 2;` leaves every
3619 /// assertion below satisfied and the construct under test gone; the
3620 /// anchor's count of 7 — six arms plus the one jump — fails by name
3621 /// instead.
3622 #[cfg(feature = "csharp")]
3623 #[test]
3624 fn csharp_goto_case_is_not_a_condition() {
3625 let src = "class A {
3626 int ctl(int x) { switch (x) { case 1: return 1; case 2: return 2; default: return 0; } }
3627 int jmp(int x) { switch (x) { case 1: goto case 2; case 2: return 2; default: return 0; } }
3628 int dfl(int x) { switch (x) { case 1: goto default; case 2: return 2; default: return 0; } }
3629 }";
3630 assert_csharp_fixture_spells(
3631 src,
3632 &[
3633 (Csharp::Case as u16, 7, "`case` tokens (6 arms + 1 jump)"),
3634 (Csharp::SwitchSection as u16, 9, "switch arms"),
3635 (Csharp::GotoStatement as u16, 2, "`goto` statements"),
3636 (
3637 Csharp::Default as u16,
3638 4,
3639 "`default` tokens (3 arms + 1 jump)",
3640 ),
3641 ],
3642 );
3643 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
3644 assert_every_member_scores(
3645 &space.spaces[0],
3646 3,
3647 2,
3648 "one condition per `case` arm, none for a `goto` into one",
3649 );
3650 });
3651 }
3652
3653 #[cfg(feature = "cpp")]
3654 #[test]
3655 fn cpp_switch_default_not_a_condition() {
3656 // C++ (and plain C, which shares this grammar) already excluded
3657 // `default`; this pins the cross-language parity invariant.
3658 check_metrics::<CppParser>(
3659 "void f(int x) {
3660 switch (x) { case 1: return; case 2: return; default: return; }
3661 }",
3662 "foo.cpp",
3663 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3664 );
3665 }
3666
3667 #[cfg(feature = "objc")]
3668 #[test]
3669 fn objc_abc() {
3670 // ObjC ABC reuses the C/C++ walker with two additions: a message
3671 // send `[obj msg]` is a call (B), and `@try` / `@catch` count as
3672 // conditions (C) like C++ try/catch.
3673 // A: `int total = 0`, `int i = 0`, `i++`, `total = total + …`,
3674 // `total = -1` = 5.
3675 // B: `[self valueAt:i]`, `[self risky]` = 2 message sends.
3676 // C: `i < n`, `@try`, `@catch`, `total >= 0` = 4.
3677 check_metrics::<ObjcParser>(
3678 "@implementation Foo\n\
3679 - (int)bar:(int)n {\n\
3680 int total = 0;\n\
3681 for (int i = 0; i < n; i++) {\n\
3682 total = total + [self valueAt:i];\n\
3683 }\n\
3684 @try {\n\
3685 [self risky];\n\
3686 } @catch (NSException *e) {\n\
3687 total = -1;\n\
3688 }\n\
3689 if (total >= 0) {\n\
3690 return total;\n\
3691 }\n\
3692 return 0;\n\
3693 }\n\
3694 @end\n",
3695 "foo.m",
3696 |metric| {
3697 assert_eq!(metric.abc.assignments_sum(), 5);
3698 assert_eq!(metric.abc.branches_sum(), 2);
3699 assert_eq!(metric.abc.conditions_sum(), 4);
3700 },
3701 );
3702 }
3703
3704 #[cfg(feature = "objc")]
3705 #[test]
3706 fn objc_abc_conditions() {
3707 // Exercises the condition-slot arms shared with C/C++: a `while`
3708 // head, a `&&` chain, a `do … while` trailing condition, and a
3709 // `return <comparison>`. ObjC routes these through the same
3710 // grammar-agnostic `cpp_inspect_*` helpers.
3711 // A: `p++`, `p--` = 2. B: no calls = 0.
3712 // C: `p != 0`, `*p > 0`, `*p < 9`, `*p == 0` = 4.
3713 check_metrics::<ObjcParser>(
3714 "@implementation Foo\n\
3715 - (int)g:(int *)p {\n\
3716 while (p != 0 && *p > 0) {\n\
3717 p++;\n\
3718 }\n\
3719 do {\n\
3720 p--;\n\
3721 } while (*p < 9);\n\
3722 return *p == 0;\n\
3723 }\n\
3724 @end\n",
3725 "foo.m",
3726 |metric| {
3727 assert_eq!(metric.abc.assignments_sum(), 2);
3728 assert_eq!(metric.abc.branches_sum(), 0);
3729 assert_eq!(metric.abc.conditions_sum(), 4);
3730 },
3731 );
3732 }
3733
3734 #[cfg(feature = "objc")]
3735 #[test]
3736 fn objc_abc_message_send_unary_condition() {
3737 // A negated boolean passed as a message-send argument is a unary
3738 // condition (Fitzpatrick Rule 9), the same as in a C-call argument.
3739 // Message args are direct children of `message_expression` (no
3740 // `argument_list`), so they are inspected in the `MessageExpression`
3741 // arm. Here: `[self use:!a]` (1 call + 1 unary condition) +
3742 // `cFunc(!a)` (1 call + 1 unary condition) → B=2, C=2.
3743 check_metrics::<ObjcParser>(
3744 "@implementation Foo\n\
3745 - (void)bar:(int)a {\n\
3746 [self use:!a];\n\
3747 cFunc(!a);\n\
3748 }\n\
3749 @end\n",
3750 "foo.m",
3751 |metric| {
3752 assert_eq!(metric.abc.branches_sum(), 2);
3753 assert_eq!(metric.abc.conditions_sum(), 2);
3754 },
3755 );
3756 }
3757
3758 #[cfg(feature = "objc")]
3759 #[test]
3760 fn objc_message_send_is_a_bool_terminal_in_condition_slots() {
3761 // `[obj ok]` is Objective-C's call, so in a condition slot it is
3762 // the unary condition `ok()` is (Fitzpatrick Rule 9). Until
3763 // `message_expression` joined the former `cpp_bool_terminal_kinds!` every
3764 // row here scored zero conditions where its C-call twin scored
3765 // one — and #1276's `for` slot inherited the gap. Each row is a
3766 // message send as the *whole* slot (grammar-dispatch §11: a
3767 // comparison would be counted by its own operator arm anyway),
3768 // and branches are asserted beside conditions so the arm cannot
3769 // be read as counting the call twice. The last three are the
3770 // over-count guards: a value position counts nothing, as `ok()`'s
3771 // does not.
3772 let cases = [
3773 ("if ([self ok]) {}", 1, 1),
3774 ("while (![self ok]) {}", 1, 1),
3775 ("do {} while ([self ok]);", 1, 1),
3776 ("for (; [self ok]; ) {}", 1, 1),
3777 ("x = [self ok] ? 1 : 2;", 2, 1),
3778 ("if ([self ok] && [o ok]) {}", 2, 2),
3779 ("return [self ok];", 0, 1),
3780 ("[self use:[self ok]];", 0, 2),
3781 ("[self use:![self ok]];", 1, 2),
3782 ];
3783 let mut ran = 0;
3784 for (body, conditions, branches) in cases {
3785 let src = format!("@implementation Foo\n- (int)bar {{\n {body}\n}}\n@end\n");
3786 let abc = metrics_verbatim(
3787 LANG::Objc,
3788 src.as_bytes(),
3789 MetricsOptions::default().with_only(&[crate::Metric::Abc]),
3790 )
3791 .abc;
3792 assert_eq!(abc.conditions_sum(), conditions, "`{body}` conditions");
3793 assert_eq!(abc.branches_sum(), branches, "`{body}` branches");
3794 ran += 1;
3795 }
3796 assert_eq!(ran, cases.len());
3797 // Both answers are present, so a walker stuck at 0 or at 1 cannot
3798 // pass half the table silently.
3799 assert!(cases.iter().any(|&(_, c, _)| c == 0));
3800 assert!(cases.iter().any(|&(_, c, _)| c == 2));
3801 }
3802
3803 #[cfg(feature = "objc")]
3804 #[test]
3805 fn objc_message_send_condition_agrees_with_c_call() {
3806 // The intra-ObjC parity C++ cannot express: a message send and a
3807 // C call in the same slot score alike. Non-degenerate by the
3808 // `assert_eq!(…, 1)` on the reference.
3809 for (send, call) in [
3810 ("if ([a ok]) {}", "if (ok()) {}"),
3811 ("for (; [a ok]; ) {}", "for (; ok(); ) {}"),
3812 ("x = [a ok] ? 1 : 2;", "x = ok() ? 1 : 2;"),
3813 ] {
3814 let wrap =
3815 |body: &str| format!("@implementation Foo\n- (int)bar {{\n {body}\n}}\n@end\n");
3816 let reference = abc_conditions(LANG::Objc, &wrap(call));
3817 assert!(reference >= 1, "`{call}` must count at least the slot");
3818 assert_eq!(
3819 abc_conditions(LANG::Objc, &wrap(send)),
3820 reference,
3821 "`{send}`"
3822 );
3823 }
3824 }
3825
3826 #[cfg(feature = "groovy")]
3827 #[test]
3828 fn groovy_switch_default_not_a_condition() {
3829 check_metrics::<GroovyParser>(
3830 "class A {
3831 int m(int x) {
3832 switch (x) { case 1: return 1; case 2: return 2; default: return 0 }
3833 }
3834 }",
3835 "foo.groovy",
3836 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3837 );
3838 }
3839
3840 #[cfg(feature = "javascript")]
3841 #[test]
3842 fn js_switch_default_not_a_condition() {
3843 check_metrics::<JavascriptParser>(
3844 "function f(x) {
3845 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3846 }",
3847 "foo.js",
3848 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3849 );
3850 }
3851
3852 #[cfg(feature = "typescript")]
3853 #[test]
3854 fn ts_switch_default_not_a_condition() {
3855 check_metrics::<TypescriptParser>(
3856 "function f(x: number): number {
3857 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3858 }",
3859 "foo.ts",
3860 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3861 );
3862 }
3863
3864 // Cross-language parity (lesson 11): the equivalent statement-`switch`
3865 // with a `default` arm reports the same ABC condition count across
3866 // Java / C# / C++. All three have two concrete case arms plus a
3867 // fallthrough `default`, so all three must count exactly 2 conditions
3868 // (the `default` excluded). `check_metrics` takes a non-capturing
3869 // `fn` pointer, so the shared expected value is asserted in each
3870 // callback; the matching constant is what enforces parity.
3871 #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3872 #[test]
3873 fn java_csharp_cpp_switch_default_abc_parity() {
3874 check_metrics::<JavaParser>(
3875 "class A {
3876 int m(int x) {
3877 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3878 }
3879 }",
3880 "foo.java",
3881 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3882 );
3883 check_metrics::<CsharpParser>(
3884 "class A {
3885 int M(int x) {
3886 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3887 }
3888 }",
3889 "foo.cs",
3890 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3891 );
3892 check_metrics::<CppParser>(
3893 "void f(int x) {
3894 switch (x) { case 1: return; case 2: return; default: return; }
3895 }",
3896 "foo.cpp",
3897 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
3898 );
3899 }
3900
3901 // Pins the ABC-vs-cyclomatic agreement the fix is about (lesson 11):
3902 // on the method's own function space, the cyclomatic decision count
3903 // (`cyclomatic()` minus the per-space base of 1) must equal the ABC
3904 // `conditions()` for the same switch. Both must be 2 — the two case
3905 // arms — with the `default` excluded from each. Revert-verified: pre-
3906 // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2.
3907 #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))]
3908 #[test]
3909 fn java_csharp_cpp_switch_default_cyclomatic_parity() {
3910 check_func_space::<JavaParser, _>(
3911 "class A {
3912 int m(int x) {
3913 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3914 }
3915 }",
3916 "foo.java",
3917 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3918 );
3919 check_func_space::<CsharpParser, _>(
3920 "class A {
3921 int M(int x) {
3922 switch (x) { case 1: return 1; case 2: return 2; default: return 0; }
3923 }
3924 }",
3925 "foo.cs",
3926 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3927 );
3928 check_func_space::<CppParser, _>(
3929 "void f(int x) {
3930 switch (x) { case 1: return; case 2: return; default: return; }
3931 }",
3932 "foo.cpp",
3933 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3934 );
3935 }
3936
3937 // Issue #473: PHP `switch` `default:` (`DefaultStatement`) is the
3938 // unconditional fallthrough, not a condition. ABC `conditions()` must
3939 // equal the cyclomatic decision count (`cyclomatic() - 1`) on the
3940 // function's own space — both 2 for the two `case` arms, with the
3941 // `default` excluded. Revert-verified: re-adding `DefaultStatement` to
3942 // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic
3943 // stays at 2, failing the invariant.
3944 #[cfg(feature = "php")]
3945 #[test]
3946 fn php_switch_default_not_a_condition() {
3947 check_func_space::<PhpParser, _>(
3948 "<?php
3949 function f($x) {
3950 switch ($x) {
3951 case 1: return 1;
3952 case 2: return 2;
3953 default: return 0;
3954 }
3955 }",
3956 "foo.php",
3957 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3958 );
3959 }
3960
3961 // Issue #473: PHP `match` `default =>` (`MatchDefaultExpression`) is the
3962 // unconditional fallthrough, mirroring the switch `default:` case above.
3963 // ABC `conditions()` must equal the cyclomatic decision count
3964 // (`cyclomatic() - 1`) — both 2 for the two non-default match arms.
3965 // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC
3966 // condition arm makes `conditions()` 3 here while cyclomatic stays at 2.
3967 #[cfg(feature = "php")]
3968 #[test]
3969 fn php_match_default_not_a_condition() {
3970 check_func_space::<PhpParser, _>(
3971 "<?php
3972 function g($x) {
3973 return match ($x) {
3974 1 => \"a\",
3975 2 => \"b\",
3976 default => \"z\",
3977 };
3978 }",
3979 "foo.php",
3980 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
3981 );
3982 }
3983
3984 #[cfg(feature = "csharp")]
3985 #[test]
3986 fn csharp_if_bare_identifier_condition() {
3987 check_metrics::<CsharpParser>(
3988 "class A {
3989 void M(bool x) {
3990 if (x) { System.Console.WriteLine(\"a\"); }
3991 }
3992 }",
3993 "foo.cs",
3994 |metric| {
3995 // `if (x)` contributes 1 condition (bare identifier).
3996 // `System.Console.WriteLine(...)` is the only call → 1 branch.
3997 // `*_sum()` is what the public JSON serializes as the
3998 // headline value (see `crate::wire::Abc`).
3999 assert_eq!(metric.abc.conditions_sum(), 1);
4000 assert_eq!(metric.abc.branches_sum(), 1);
4001 assert_eq!(metric.abc.assignments_sum(), 0);
4002 },
4003 );
4004 }
4005
4006 #[cfg(feature = "csharp")]
4007 #[test]
4008 fn csharp_while_bare_identifier_condition() {
4009 check_metrics::<CsharpParser>(
4010 "class A {
4011 void M(bool x) {
4012 while (x) { x = false; }
4013 }
4014 }",
4015 "foo.cs",
4016 |metric| {
4017 // `while (x)` contributes 1 condition; `x = false` is 1 assignment.
4018 assert_eq!(metric.abc.conditions_sum(), 1);
4019 assert_eq!(metric.abc.assignments_sum(), 1);
4020 assert_eq!(metric.abc.branches_sum(), 0);
4021 },
4022 );
4023 }
4024
4025 #[cfg(feature = "csharp")]
4026 #[test]
4027 fn csharp_do_while_bare_identifier_condition() {
4028 check_metrics::<CsharpParser>(
4029 "class A {
4030 void M(bool x) {
4031 do { x = true; } while (x);
4032 }
4033 }",
4034 "foo.cs",
4035 |metric| {
4036 // `do { ... } while (x)` contributes 1 condition;
4037 // `x = true` is 1 assignment.
4038 assert_eq!(metric.abc.conditions_sum(), 1);
4039 assert_eq!(metric.abc.assignments_sum(), 1);
4040 assert_eq!(metric.abc.branches_sum(), 0);
4041 },
4042 );
4043 }
4044
4045 #[cfg(feature = "csharp")]
4046 #[test]
4047 fn csharp_if_unary_not_condition() {
4048 // Two cases share one test:
4049 //
4050 // if (!x) { … } — the `if` predicate is a boolean slot, so
4051 // `csharp_count_condition` pays for it once the peel
4052 // (`csharp_wrapper_operand`) has taken the `!` off; the
4053 // slot would pay without the `!` too.
4054 //
4055 // return !x; — a `return` value is not a slot, so the `!`
4056 // is the *only* thing that makes the operand count, through
4057 // `csharp_count_negated`. Asserting the `return !x;`
4058 // case isolates the negation path from the slot path.
4059 check_metrics::<CsharpParser>(
4060 "class A {
4061 void M(bool x) {
4062 if (!x) { System.Console.WriteLine(\"a\"); }
4063 }
4064 bool N(bool x) {
4065 return !x;
4066 }
4067 }",
4068 "foo.cs",
4069 |metric| {
4070 // `if (!x)` contributes 1 condition (the slot pays for the
4071 // peeled `x`). `return !x;` contributes 1 condition (not a
4072 // slot — the unary `!` is what makes `x` count).
4073 // → 2 conditions total. 1 branch from WriteLine().
4074 assert_eq!(metric.abc.conditions_sum(), 2);
4075 assert_eq!(metric.abc.branches_sum(), 1);
4076 assert_eq!(metric.abc.assignments_sum(), 0);
4077 },
4078 );
4079 }
4080
4081 #[cfg(feature = "csharp")]
4082 #[test]
4083 fn csharp_if_double_parenthesized_condition() {
4084 // Audit-tests follow-up: with only the
4085 // `csharp_prefix_unary_expr_kinds!()` arm covered by
4086 // `csharp_if_unary_not_condition`, the
4087 // `csharp_paren_expr_kinds!()` delegation arm in
4088 // `csharp_count_condition` was a pure dead-code candidate —
4089 // disabling it caused zero existing tests to fail (verified
4090 // 2026-05-26).
4091 //
4092 // `if ((x))` puts a `ParenthesizedExpression` at child(2) of
4093 // the IfStatement (child(1) is the literal `(`, child(2) is
4094 // the inner parenthesised expression, child(3) is the literal
4095 // `)`). `csharp_count_condition` must peel that parenthesis
4096 // (`csharp_wrapper_operand`) down to the inner Identifier,
4097 // which does not score itself, so the slot pays once.
4098 check_metrics::<CsharpParser>(
4099 "class A {
4100 void M(bool x) {
4101 if ((x)) { System.Console.WriteLine(\"a\"); }
4102 }
4103 }",
4104 "foo.cs",
4105 |metric| {
4106 assert_eq!(metric.abc.conditions_sum(), 1);
4107 assert_eq!(metric.abc.branches_sum(), 1);
4108 assert_eq!(metric.abc.assignments_sum(), 0);
4109 },
4110 );
4111 }
4112
4113 #[cfg(feature = "csharp")]
4114 #[test]
4115 fn csharp_bool_returning_terminal_kinds_count() {
4116 // Regression for issue #372 (lesson #19): before the fix,
4117 // `csharp_count_condition` / the former `csharp_inspect_container`
4118 // only recognised invocation / identifier / boolean literal as
4119 // terminal-bool operands, so the five idiomatic boolean
4120 // expressions in the `if (...)` slots below silently scored
4121 // zero conditions:
4122 //
4123 // - `cfg.flag` — MemberAccessExpression
4124 // - `await c.Check()` — AwaitExpression
4125 // - `(bool)v` — CastExpression
4126 // - `v is not null` — IsPatternExpression
4127 // - `flags[0]` — ElementAccessExpression
4128 //
4129 // The `is` test has since moved out of the former terminal set to an
4130 // unconditional arm (#1461), so it no longer depends on the
4131 // slot to be counted. The total is unchanged, which is the
4132 // point of that change rather than an accident of it.
4133 //
4134 // expected: 5 conditions (one per `if`), 0 assignments,
4135 // 1 branch (the single `c.Check()` invocation; the other
4136 // `if`-condition expressions are not invocations).
4137 check_metrics::<CsharpParser>(
4138 "using System.Threading.Tasks;
4139 class A {
4140 async Task M(object v, bool[] flags, Cfg cfg, C c) {
4141 if (cfg.flag) { }
4142 if (await c.Check()) { }
4143 if ((bool)v) { }
4144 if (v is not null) { }
4145 if (flags[0]) { }
4146 }
4147 }
4148 class Cfg { public bool flag; }
4149 class C { public Task<bool> Check() => null; }",
4150 "foo.cs",
4151 |metric| {
4152 assert_eq!(metric.abc.conditions_sum(), 5);
4153 assert_eq!(metric.abc.assignments_sum(), 0);
4154 assert_eq!(metric.abc.branches_sum(), 1);
4155 },
4156 );
4157 }
4158
4159 #[cfg(feature = "csharp")]
4160 #[test]
4161 fn csharp_if_method_call_condition() {
4162 check_metrics::<CsharpParser>(
4163 "class A {
4164 void M(string s) {
4165 if (s.StartsWith(\"x\")) { System.Console.WriteLine(\"a\"); }
4166 }
4167 }",
4168 "foo.cs",
4169 |metric| {
4170 // `if (s.StartsWith("x"))` contributes 1 condition
4171 // (InvocationExpression) plus 1 branch for the call itself,
4172 // plus 1 branch for WriteLine.
4173 assert_eq!(metric.abc.conditions_sum(), 1);
4174 assert_eq!(metric.abc.branches_sum(), 2);
4175 assert_eq!(metric.abc.assignments_sum(), 0);
4176 },
4177 );
4178 }
4179
4180 #[cfg(feature = "csharp")]
4181 #[test]
4182 fn csharp_if_while_boolean_literal_condition() {
4183 // Regression for #371: the tree-sitter-c-sharp grammar wraps a
4184 // bare `true` / `false` literal used as the condition of
4185 // `if` / `while` / `do` / `?:` in a `boolean_literal` node,
4186 // not the leaf `true` / `false` tokens. `csharp_count_condition`
4187 // must therefore match `BooleanLiteral` (the wrapper),
4188 // mirroring the existing `csharp_walk_for_statement` arm.
4189 // Without that, every literal-condition statement scored 0
4190 // conditions. The `&&` / `||` chain-operand slot is covered
4191 // separately by `csharp_short_circuit_with_boolean_literal_operand`
4192 // and the negated-operand path (`csharp_count_negated`) by
4193 // `csharp_declarations_with_conditions` (`!true` / `!false`).
4194 check_metrics::<CsharpParser>(
4195 "class A {
4196 void M() {
4197 if (true) { System.Console.WriteLine(\"a\"); }
4198 if (false) { System.Console.WriteLine(\"b\"); }
4199 while (true) { break; }
4200 do { break; } while (false);
4201 int t = true ? 1 : 0;
4202 }
4203 }",
4204 "foo.cs",
4205 |metric| {
4206 // Five literal-condition statements contribute 5
4207 // `BooleanLiteral` conditions (one per if/if/while/
4208 // do-while/ternary), plus the ternary's `?` token
4209 // adds one more via `csharp_count_token_condition`
4210 // → 6 total. The two `System.Console.WriteLine`
4211 // calls contribute 2 branches; the `int t = …`
4212 // initializer contributes 1 assignment.
4213 assert_eq!(metric.abc.conditions_sum(), 6);
4214 assert_eq!(metric.abc.branches_sum(), 2);
4215 assert_eq!(metric.abc.assignments_sum(), 1);
4216 },
4217 );
4218 }
4219
4220 #[cfg(feature = "csharp")]
4221 #[test]
4222 fn csharp_short_circuit_with_boolean_literal_operand() {
4223 // Regression for #371 (companion to
4224 // `csharp_if_while_boolean_literal_condition`): a bare
4225 // `true` / `false` operand of `&&` / `||` is a chain-operand
4226 // slot scored through `csharp_count_condition`. A
4227 // `BooleanLiteral` does not score itself, so the slot pays for
4228 // it — otherwise the operand silently scores zero.
4229 check_metrics::<CsharpParser>(
4230 "class A {
4231 void M(bool x) {
4232 if (x && true) { System.Console.WriteLine(\"a\"); }
4233 if (false || x) { System.Console.WriteLine(\"b\"); }
4234 }
4235 }",
4236 "foo.cs",
4237 |metric| {
4238 // `&&` and `||` themselves are NOT in
4239 // `csharp_count_token_condition`'s match list — they
4240 // route through `csharp_walk_for_conditions::AMPAMP|
4241 // PIPEPIPE`, which scores each operand of the parent
4242 // BinaryExpression through `csharp_count_condition`.
4243 // An operand pays one condition unless it scores
4244 // itself. For
4245 // `x && true`: 1 (Identifier x) + 1 (BooleanLiteral
4246 // true) = 2. For `false || x`: 1 (BooleanLiteral
4247 // false) + 1 (Identifier x) = 2. Total 4.
4248 assert_eq!(metric.abc.conditions_sum(), 4);
4249 assert_eq!(metric.abc.branches_sum(), 2);
4250 assert_eq!(metric.abc.assignments_sum(), 0);
4251 },
4252 );
4253 }
4254
4255 #[cfg(feature = "csharp")]
4256 #[test]
4257 fn csharp_return_without_conditions() {
4258 check_metrics::<CsharpParser>(
4259 "class A {
4260 int M() { return 42; }
4261 string N() { return \"hi\"; }
4262 }",
4263 "foo.cs",
4264 |metric| insta::assert_json_snapshot!(metric.abc),
4265 );
4266 }
4267
4268 #[cfg(feature = "csharp")]
4269 #[test]
4270 fn csharp_lambda_expressions_return_with_conditions() {
4271 check_metrics::<CsharpParser>(
4272 "class A {
4273 public void M() {
4274 System.Func<int, bool> f = x => (x > 0);
4275 System.Func<int, bool> g = x => !(x < 0);
4276 }
4277 }",
4278 "foo.cs",
4279 |metric| insta::assert_json_snapshot!(metric.abc),
4280 );
4281 }
4282
4283 #[cfg(feature = "csharp")]
4284 #[test]
4285 fn csharp_for_with_variable_declaration() {
4286 check_metrics::<CsharpParser>(
4287 "class A {
4288 void M() {
4289 for (int i = 0; i < 10; i++) {
4290 System.Console.WriteLine(i);
4291 }
4292 }
4293 }",
4294 "foo.cs",
4295 |metric| insta::assert_json_snapshot!(metric.abc),
4296 );
4297 }
4298
4299 #[cfg(feature = "csharp")]
4300 #[test]
4301 fn csharp_for_without_variable_declaration() {
4302 check_metrics::<CsharpParser>(
4303 "class A {
4304 void M() {
4305 int i;
4306 for (i = 0; i < 10; i++) {
4307 System.Console.WriteLine(i);
4308 }
4309 }
4310 }",
4311 "foo.cs",
4312 |metric| insta::assert_json_snapshot!(metric.abc),
4313 );
4314 }
4315
4316 #[cfg(feature = "csharp")]
4317 #[test]
4318 fn csharp_for_identifier_condition() {
4319 check_metrics::<CsharpParser>(
4320 "class A {
4321 void M(bool ready) {
4322 for (; ready ;) { }
4323 }
4324 }",
4325 "foo.cs",
4326 |metric| {
4327 // expected: assignments=0 (no `=` / `++` / `--`),
4328 // branches=0 (no invocation / object creation),
4329 // conditions=1 (bare-identifier for-loop condition).
4330 // Averages divide by 3 spaces (top-level + class + method).
4331 insta::assert_json_snapshot!(
4332 metric.abc,
4333 @r#"
4334 {
4335 "assignments": 0,
4336 "branches": 0,
4337 "conditions": 1,
4338 "magnitude": 1.0,
4339 "value": 0.0,
4340 "assignments_average": 0.0,
4341 "branches_average": 0.0,
4342 "conditions_average": 0.3333333333333333,
4343 "assignments_min": 0,
4344 "assignments_max": 0,
4345 "branches_min": 0,
4346 "branches_max": 0,
4347 "conditions_min": 0,
4348 "conditions_max": 1
4349 }
4350 "#
4351 );
4352 },
4353 );
4354 }
4355
4356 #[cfg(feature = "csharp")]
4357 #[test]
4358 fn csharp_for_invocation_condition() {
4359 check_metrics::<CsharpParser>(
4360 "class A {
4361 bool Ok() { return true; }
4362 void M() {
4363 for (; Ok() ;) { }
4364 }
4365 }",
4366 "foo.cs",
4367 |metric| {
4368 // expected: assignments=0, branches=1 (the `Ok()` call),
4369 // conditions=1 (invocation as for-loop condition).
4370 // Averages divide by 4 spaces (top-level + class + two
4371 // methods).
4372 insta::assert_json_snapshot!(
4373 metric.abc,
4374 @r#"
4375 {
4376 "assignments": 0,
4377 "branches": 1,
4378 "conditions": 1,
4379 "magnitude": 1.4142135623730951,
4380 "value": 0.0,
4381 "assignments_average": 0.0,
4382 "branches_average": 0.25,
4383 "conditions_average": 0.25,
4384 "assignments_min": 0,
4385 "assignments_max": 0,
4386 "branches_min": 0,
4387 "branches_max": 1,
4388 "conditions_min": 0,
4389 "conditions_max": 1
4390 }
4391 "#
4392 );
4393 },
4394 );
4395 }
4396
4397 // Regression coverage for #279: the C# grammar wraps a literal
4398 // `true` / `false` for-loop condition in a `boolean_literal` node.
4399 // The `BooleanLiteral` arm in the `ForStatement` dispatch must
4400 // attribute one condition; without it, `for (; true ;)` would
4401 // contribute 0 (the bug fixed by this commit also affected this
4402 // shape).
4403 #[cfg(feature = "csharp")]
4404 #[test]
4405 fn csharp_for_boolean_literal_condition() {
4406 check_metrics::<CsharpParser>(
4407 "class A {
4408 void M() {
4409 for (; true ;) { }
4410 }
4411 }",
4412 "foo.cs",
4413 |metric| {
4414 // expected: assignments=0, branches=0,
4415 // conditions=1 (the `true` literal as condition).
4416 assert_eq!(metric.abc.conditions_sum(), 1);
4417 assert_eq!(metric.abc.assignments_sum(), 0);
4418 assert_eq!(metric.abc.branches_sum(), 0);
4419 },
4420 );
4421 }
4422
4423 // Regression coverage for #279: an empty for-loop condition such as
4424 // `for (; ;) {}` must contribute 0 to conditions — there is no
4425 // condition node to count.
4426 #[cfg(feature = "csharp")]
4427 #[test]
4428 fn csharp_for_empty_condition() {
4429 check_metrics::<CsharpParser>(
4430 "class A {
4431 void M() {
4432 for (; ;) { }
4433 }
4434 }",
4435 "foo.cs",
4436 |metric| {
4437 // expected: assignments=0, branches=0, conditions=0
4438 // (no condition expression in `for (; ;)`).
4439 insta::assert_json_snapshot!(
4440 metric.abc,
4441 @r#"
4442 {
4443 "assignments": 0,
4444 "branches": 0,
4445 "conditions": 0,
4446 "magnitude": 0.0,
4447 "value": 0.0,
4448 "assignments_average": 0.0,
4449 "branches_average": 0.0,
4450 "conditions_average": 0.0,
4451 "assignments_min": 0,
4452 "assignments_max": 0,
4453 "branches_min": 0,
4454 "branches_max": 0,
4455 "conditions_min": 0,
4456 "conditions_max": 0
4457 }
4458 "#
4459 );
4460 },
4461 );
4462 }
4463
4464 #[cfg(feature = "csharp")]
4465 #[test]
4466 fn csharp_ternary_conditions() {
4467 check_metrics::<CsharpParser>(
4468 "class A {
4469 int Sign(int x) {
4470 return (x > 0) ? 1 : (x < 0 ? -1 : 0);
4471 }
4472 }",
4473 "foo.cs",
4474 |metric| insta::assert_json_snapshot!(metric.abc),
4475 );
4476 }
4477
4478 #[cfg(feature = "csharp")]
4479 #[test]
4480 fn csharp_malformed_parenthesized_no_panic() {
4481 check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
4482 // Don't panic on malformed source.
4483 assert_eq!(metric.abc.assignments(), 0);
4484 assert_eq!(metric.abc.branches(), 0);
4485 });
4486 }
4487
4488 #[cfg(feature = "csharp")]
4489 #[test]
4490 fn csharp_function_pointer_type_no_double_count() {
4491 // EC1 extension — `<` and `>` are also parameter-list delimiters
4492 // for unsafe function-pointer types. Since #1297 the arm is an
4493 // allowlist, so `FunctionPointerType` is excluded by being
4494 // absent from it rather than by being named in a deny set;
4495 // either way these brackets must not accumulate `conditions`.
4496 check_metrics::<CsharpParser>(
4497 "unsafe class A {
4498 public delegate*<int, int, int> Adder;
4499 public delegate*<string, void> Logger;
4500 }",
4501 "foo.cs",
4502 |metric| {
4503 assert_eq!(
4504 metric.abc.conditions(),
4505 0,
4506 "function-pointer-type angle brackets must not count"
4507 );
4508 },
4509 );
4510 }
4511
4512 #[cfg(feature = "csharp")]
4513 #[test]
4514 fn csharp_generic_type_args_no_double_count() {
4515 // EC1 — `<` and `>` inside TypeArgumentList must not count as
4516 // boolean conditions.
4517 check_metrics::<CsharpParser>(
4518 "class A {
4519 void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
4520 System.Console.WriteLine(d);
4521 }
4522 }",
4523 "foo.cs",
4524 |metric| insta::assert_json_snapshot!(metric.abc),
4525 );
4526 }
4527
4528 // #1297: `operator <` names the operator a type *defines*; applying
4529 // one is what a decision looks like. tree-sitter-c-sharp spells the
4530 // declaration's operator with the same bare token as a comparison,
4531 // and the previous denylist named only `type_argument_list`,
4532 // `type_parameter_list` and `function_pointer_type`, so every
4533 // comparison-operator overload scored a condition.
4534 //
4535 // The fixture carries the two overloads plus one `a < b` inside a
4536 // `binary_expression` and one `x is > 0 ? 2 : 3`, which the grammar
4537 // parses as a `relational_pattern` whose operand is the ternary (see
4538 // the assertion). Every mis-aim lands on its own number: 7
4539 // with neither gate, 5 if `RelationalPattern` is readmitted to the
4540 // allowlist (#1383 dropped it), 3 if the gate swallows
4541 // `BinaryExpression` too (only the ternary's `?` and slot and the
4542 // `is` test survive), 0 if the fixture stops parsing. Each is two
4543 // higher than before #1461 and #1526, which added the `is` test and
4544 // the ternary's slot as counts no gate on the `<` / `>` token can
4545 // reach.
4546 #[cfg(feature = "csharp")]
4547 #[test]
4548 fn csharp_operator_declaration_is_not_a_condition() {
4549 check_func_space::<CsharpParser, _>(
4550 "class V {
4551 public static bool operator <(V a, V b) { return true; }
4552 public static bool operator >(V a, V b) { return true; }
4553 int m(int a, int b, int x) {
4554 if (a < b) { return 1; }
4555 return x is > 0 ? 2 : 3;
4556 }
4557 }",
4558 "foo.cs",
4559 |space| {
4560 // Assert the claim per space rather than through the
4561 // file total, which is 2 with the two overloads and 2
4562 // without them — an aggregate assertion would pass on a
4563 // fixture that had lost the very construct under test.
4564 let class = &space.spaces[0];
4565 assert_eq!(class.spaces.len(), 3, "two overloads plus `m`");
4566 for (i, member) in class.spaces.iter().enumerate().take(2) {
4567 assert_eq!(
4568 member.metrics.abc.conditions(),
4569 0,
4570 "operator declaration {i} must score no condition"
4571 );
4572 }
4573 // 4: the `a < b` comparison, the ternary `?`, its
4574 // condition slot, and the `is` test. tree-sitter-c-sharp
4575 // 0.23.5 parses `x is > 0 ? 2 : 3` as `x is > (0 ? 2 :
4576 // 3)` — the ternary is the pattern's operand, so the
4577 // pattern sits in no decision slot and its `>` still
4578 // scores nothing (#1383), while the ternary's condition
4579 // is the literal `0`, which pays as a slot since #1526.
4580 // The fourth count is the enclosing
4581 // `is_pattern_expression` itself, which since #1461
4582 // scores by use rather than only inside a boolean slot.
4583 // C# itself binds the source `(x is > 0) ? 2 : 3`; a
4584 // grammar that agrees reads 3 here, because the `is`
4585 // test in the condition slot already pays for it.
4586 assert_eq!(class.spaces[2].metrics.abc.conditions(), 4);
4587 },
4588 );
4589 }
4590
4591 // #1420, the other four spellings of the test above. C# overloads six
4592 // comparison operators and #1297 gated two: `<=` `>=` `==` `!=` are
4593 // distinct tokens that reach a different arm of
4594 // `csharp_count_token_condition` and each still scored one spurious
4595 // condition on its declaration — measured 1 apiece against `<` / `>`'s
4596 // 0, on a class where `cyclomatic()` is 1 for all six.
4597 //
4598 // Each token gets its own member so a partial fix names the spelling
4599 // it missed: gating `EQEQ | BANGEQ` and forgetting `GTEQ | LTEQ` fails
4600 // here by operator, where the file total cannot tell the two apart —
4601 // it reads 4 before the fix and 0 after, so any two-of-four fix halves
4602 // it and still looks like movement.
4603 //
4604 // `n` is the deliberate control (§11): the same four tokens *applied*,
4605 // inside `binary_expression` parents, which must keep scoring one
4606 // condition each. Without it the gate could be satisfied by refusing
4607 // to count these tokens at all.
4608 #[cfg(feature = "csharp")]
4609 #[test]
4610 fn csharp_comparison_operator_overloads_are_not_conditions() {
4611 let src = "class V {
4612 public static bool operator <=(V a, V b) { return true; }
4613 public static bool operator >=(V a, V b) { return true; }
4614 public static bool operator ==(V a, V b) { return true; }
4615 public static bool operator !=(V a, V b) { return true; }
4616 int n(int a, int b) {
4617 if (a <= b) { return 1; }
4618 if (a >= b) { return 2; }
4619 if (a == b) { return 3; }
4620 if (a != b) { return 4; }
4621 return 0;
4622 }
4623 }";
4624 // Two of each token: one declaring, one applying. Trimming either
4625 // half out collapses this test into the other half's control and
4626 // is what this anchor exists to catch.
4627 assert_csharp_fixture_spells(
4628 src,
4629 &[
4630 (
4631 Csharp::OperatorDeclaration as u16,
4632 4,
4633 "operator declarations",
4634 ),
4635 (Csharp::LTEQ as u16, 2, "`<=`"),
4636 (Csharp::GTEQ as u16, 2, "`>=`"),
4637 (Csharp::EQEQ as u16, 2, "`==`"),
4638 (Csharp::BANGEQ as u16, 2, "`!=`"),
4639 ],
4640 );
4641 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
4642 let class = &space.spaces[0];
4643 assert_eq!(class.spaces.len(), 5, "four overloads plus `n`");
4644 for (member, spelling) in class.spaces.iter().zip(["<=", ">=", "==", "!="]) {
4645 assert_eq!(
4646 member.metrics.abc.conditions(),
4647 0,
4648 "`operator {spelling}` declares an operator, it does not apply one"
4649 );
4650 assert_eq!(
4651 member.metrics.abc.conditions(),
4652 member.metrics.cyclomatic.cyclomatic() - 1,
4653 "`operator {spelling}`: §8 parity with the cyclomatic decision count"
4654 );
4655 }
4656 // The control, found by name so a reordered fixture cannot
4657 // silently re-point it at an overload. 0 here would mean the
4658 // gate swallowed the applying form along with the declaring
4659 // one.
4660 let control = child_space(class, "n");
4661 assert_eq!(
4662 control.metrics.abc.conditions(),
4663 4,
4664 "`n` applies all four tokens: one condition each"
4665 );
4666 });
4667 }
4668
4669 // The allowlist #1420 installed is the only thing keeping `#if A == B`
4670 // a condition, and it keeps it for a reason no arm states: the grammar
4671 // aliases `preproc_binary_expression` onto `binary_expression`, so the
4672 // preprocessor's `==` arrives with kind id `BinaryExpression` (369)
4673 // and not with one of its own. Before #1420 the token was ungated and
4674 // no fact about its parent mattered; now a grammar bump that gave the
4675 // alias a distinct id would stop counting every `#if` equality with
4676 // nothing red. This is that pin.
4677 //
4678 // `BinaryExpression2` (469) is the pre-alias symbol the arm also
4679 // lists, defensively, per `.claude/rules/grammar-dispatch.md` §1. §2
4680 // asks that such an entry be kept *and* have its unreachability
4681 // asserted, so that a pin promoting the symbol changes behaviour
4682 // loudly rather than invisibly.
4683 #[cfg(feature = "csharp")]
4684 #[test]
4685 fn csharp_preproc_equality_counts_through_the_binary_expression_alias() {
4686 let src = "class P {
4687 #if A == B
4688 int x;
4689 #endif
4690 #if C != D
4691 int y;
4692 #endif
4693 }";
4694 let parser = CsharpParser::new(
4695 src.as_bytes().to_vec(),
4696 std::path::Path::new("foo.cs"),
4697 None,
4698 );
4699 assert!(
4700 ast_has_kind_id(&parser, Csharp::BinaryExpression as u16),
4701 "the `#if` expressions must parse as `binary_expression` — \
4702 the alias is what the arm's allowlist matches"
4703 );
4704 assert!(
4705 !ast_has_kind_id(&parser, Csharp::BinaryExpression2 as u16),
4706 "`BinaryExpression2` is the pre-alias symbol and must stay \
4707 unreachable; if the grammar starts emitting it, re-derive the \
4708 allowlist rather than trusting this arm"
4709 );
4710 // 2, one per `#if` expression. The two directives carry no
4711 // declarations the walk would score, so nothing else contributes.
4712 check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
4713 assert_eq!(metric.abc.conditions_sum(), 2);
4714 });
4715 }
4716
4717 // #1448, the C++ / Mozcpp instance of #1420. C++ overloads seven
4718 // comparison operators; before the fix only `<` / `>` were gated on a
4719 // `binary_expression` parent, so `operator<=` `>=` `==` `!=` and the
4720 // spaceship `operator<=>` each scored one spurious condition on a
4721 // struct where every member has `cyclomatic()` 1.
4722 //
4723 // Members are looked up by name, so a partial fix fails on the
4724 // spelling it missed rather than on a total that merely moved. `n`
4725 // is the §11 control: the same seven tokens applied, which must
4726 // keep scoring one condition each. The census anchors both halves —
4727 // trimming an overload or an application out of the fixture fails
4728 // by name instead of decaying into its counterpart.
4729 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4730 const CPP_COMPARISON_OVERLOADS: &str = "struct V {
4731 bool operator<(const V& o) const { return true; }
4732 bool operator>(const V& o) const { return true; }
4733 bool operator<=(const V& o) const { return true; }
4734 bool operator>=(const V& o) const { return true; }
4735 bool operator==(const V& o) const { return true; }
4736 bool operator!=(const V& o) const { return true; }
4737 auto operator<=>(const V& o) const { return 0; }
4738 };
4739 struct W {
4740 int n;
4741 bool operator==(const W&) const = default;
4742 };
4743 int n(int a, int b) {
4744 return (a < b) + (a > b) + (a <= b) + (a >= b) + (a == b) + (a != b) + (a <=> b);
4745 }";
4746
4747 // `operator_name` id, then the seven token ids in source order, for
4748 // whichever of the two grammars `P` parses.
4749 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4750 fn assert_cpp_comparison_overloads_are_not_conditions<P: MetricSuite>(
4751 operator_name: u16,
4752 tokens: [u16; 7],
4753 ) {
4754 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4755 const SPELLINGS: [&str; 7] = ["<", ">", "<=", ">=", "==", "!=", "<=>"];
4756 let mut census = vec![(operator_name, 8, "operator names")];
4757 // Two of each token, one declaring and one applying, plus the
4758 // third `==` that `W`'s defaulted comparison declares.
4759 census.extend(
4760 tokens
4761 .iter()
4762 .zip(SPELLINGS)
4763 .map(|(&id, s)| (id, if s == "==" { 3 } else { 2 }, s)),
4764 );
4765 assert_fixture_spells::<P>(CPP_COMPARISON_OVERLOADS, "foo.cpp", &census);
4766 check_func_space::<P, _>(CPP_COMPARISON_OVERLOADS, "foo.cpp", |space| {
4767 let class = child_space(&space, "V");
4768 assert_eq!(class.spaces.len(), 7, "seven overloads");
4769 for spelling in SPELLINGS {
4770 let member = child_space(class, &format!("operator{spelling}"));
4771 assert_eq!(
4772 member.metrics.abc.conditions(),
4773 0,
4774 "`operator{spelling}` declares an operator, it does not apply one"
4775 );
4776 assert_eq!(
4777 member.metrics.abc.conditions(),
4778 member.metrics.cyclomatic.cyclomatic() - 1,
4779 "`operator{spelling}`: §8 parity with the cyclomatic decision count"
4780 );
4781 }
4782 assert_eq!(
4783 child_space(&space, "n").metrics.abc.conditions(),
4784 7,
4785 "`n` applies all seven tokens: one condition each"
4786 );
4787 // A defaulted C++20 comparison declares the operator with no
4788 // body; it is the common modern spelling and must score
4789 // nothing anywhere in the file.
4790 assert_eq!(
4791 space.metrics.abc.conditions_sum(),
4792 7,
4793 "only `n`'s seven applications are conditions"
4794 );
4795 });
4796 }
4797
4798 #[cfg(feature = "cpp")]
4799 #[test]
4800 fn cpp_comparison_operator_overloads_are_not_conditions() {
4801 assert_cpp_comparison_overloads_are_not_conditions::<CppParser>(
4802 Cpp::OperatorName as u16,
4803 [
4804 Cpp::LT,
4805 Cpp::GT,
4806 Cpp::LTEQ,
4807 Cpp::GTEQ,
4808 Cpp::EQEQ,
4809 Cpp::BANGEQ,
4810 Cpp::LTEQGT,
4811 ]
4812 .map(|k| k as u16),
4813 );
4814 }
4815
4816 // Mozcpp owns no file extension, so this is its only coverage of the
4817 // arm: no integration snapshot routes to it.
4818 #[cfg(feature = "mozcpp")]
4819 #[test]
4820 fn mozcpp_comparison_operator_overloads_are_not_conditions() {
4821 use crate::Mozcpp;
4822 assert_cpp_comparison_overloads_are_not_conditions::<crate::MozcppParser>(
4823 Mozcpp::OperatorName as u16,
4824 [
4825 Mozcpp::LT,
4826 Mozcpp::GT,
4827 Mozcpp::LTEQ,
4828 Mozcpp::GTEQ,
4829 Mozcpp::EQEQ,
4830 Mozcpp::BANGEQ,
4831 Mozcpp::LTEQGT,
4832 ]
4833 .map(|k| k as u16),
4834 );
4835 }
4836
4837 // ISO C++ spells `!`, `&&`, `||` and `!=` equally as `not`, `and`,
4838 // `or` and `not_eq` ([lex.digraph]), and the C++ grammars give each
4839 // alternative token a kind of its own. ABC tested only the symbols,
4840 // so `if (not b)` scored 0 against `if (!b)`'s 1, `b and c` scored 0
4841 // against `b && c`'s 2, and `b not_eq c` scored 0 against `b != c`'s
4842 // 1. Each row pairs a symbolic spelling with its alternative and the
4843 // value both must give; the census proves the alternative fixture
4844 // really carries the alternative token rather than parsing it as an
4845 // identifier, which is what tree-sitter-c does with the same text.
4846 //
4847 // Arguments: the `not`, `and`, `or` and `not_eq` ids of `P`'s grammar.
4848 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4849 fn assert_cpp_alternative_tokens_score_like_symbols<P: MetricSuite>(alt: [u16; 4]) {
4850 let [not, and, or, not_eq] = alt;
4851 let rows = [
4852 ("if (!b) {}", "if (not b) {}", not, 1),
4853 ("bool x = !b && c;", "bool x = not b && c;", not, 2),
4854 ("bool x = c ? !b : c;", "bool x = c ? not b : c;", not, 3),
4855 ("bool x = b && c;", "bool x = b and c;", and, 2),
4856 ("if (b || c) {}", "if (b or c) {}", or, 2),
4857 ("if (b != c) {}", "if (b not_eq c) {}", not_eq, 1),
4858 ];
4859 for (symbolic, alternative, token, expected) in rows {
4860 let alt_source = format!("void f(bool b, bool c) {{\n {alternative}\n}}\n");
4861 assert_fixture_spells::<P>(&alt_source, "foo.cpp", &[(token, 1, alternative)]);
4862 for source in [symbolic, alternative] {
4863 let source = format!("void f(bool b, bool c) {{\n {source}\n}}\n");
4864 check_func_space::<P, _>(&source, "foo.cpp", |space| {
4865 assert_eq!(
4866 space.metrics.abc.conditions_sum(),
4867 expected,
4868 "`{source}` must score like its symbolic spelling `{symbolic}`"
4869 );
4870 });
4871 }
4872 }
4873 }
4874
4875 #[cfg(feature = "cpp")]
4876 #[test]
4877 fn cpp_alternative_tokens_score_like_symbols() {
4878 assert_cpp_alternative_tokens_score_like_symbols::<CppParser>(
4879 [Cpp::Not, Cpp::And, Cpp::Or, Cpp::NotEq].map(|k| k as u16),
4880 );
4881 }
4882
4883 // Mozcpp owns no file extension, so this is its only coverage of the
4884 // alternative tokens: no integration snapshot routes to it.
4885 #[cfg(feature = "mozcpp")]
4886 #[test]
4887 fn mozcpp_alternative_tokens_score_like_symbols() {
4888 use crate::Mozcpp;
4889 assert_cpp_alternative_tokens_score_like_symbols::<crate::MozcppParser>(
4890 [Mozcpp::Not, Mozcpp::And, Mozcpp::Or, Mozcpp::NotEq].map(|k| k as u16),
4891 );
4892 }
4893
4894 // The two other productions #1448's allowlist decides, alongside the
4895 // operator overloads above.
4896 //
4897 // `#if A <= B` is a real decision and must keep counting: the grammar
4898 // aliases `preproc_binary_expression` onto `binary_expression`, and
4899 // the allowlist is the only thing that admits it now that `<=` / `!=`
4900 // are gated. The fixture's two directives are the root's only
4901 // conditions.
4902 //
4903 // A fold over a comparison operator counts once, through the fold's
4904 // `operator` field: before the fix `lt` scored 0, `le` 1 and the
4905 // binary fold `eq` 2 (one per spelling of its single operator).
4906 // `idiom` is the control — its `==` sits in a `binary_expression`
4907 // inside the fold and counts there, while the fold's own `&&` is not
4908 // a comparison: it scores what its twin `(a == 0) && (a == 0)` does,
4909 // 2 (#1533). `neg` is the same twice-spelled operator under the `||`
4910 // chain arm: it scores what `true || !a` does, 2, where walking the
4911 // fold once per spelling would pay 4.
4912 //
4913 // The census also pins §2: every `binary_expression` here carries the
4914 // `BinaryExpression2` id and the pre-alias `BinaryExpression` is never
4915 // emitted. `cpp_operator_is_applied` matches the kind *name*, which
4916 // covers both, so a grammar that starts emitting the pre-alias id
4917 // changes nothing — this census is what would notice it.
4918 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
4919 fn assert_cpp_comparison_folds_and_preproc_operands<P: MetricSuite>(
4920 binary_expression: u16,
4921 binary_expression2: u16,
4922 fold_expression: u16,
4923 ) {
4924 let src = "#if A <= B
4925 #endif
4926 #if C != D
4927 #endif
4928 template <typename... T> bool lt(T... a) { return (a < ...); }
4929 template <typename... T> bool le(T... a) { return (a <= ...); }
4930 template <typename... T> bool eq(T... a) { return (0 == ... == a); }
4931 template <typename... T> bool idiom(T... a) { return ((a == 0) && ...); }
4932 template <typename... T> bool neg(T... a) { return (true || ... || !a); }";
4933 assert_fixture_spells::<P>(
4934 src,
4935 "foo.cpp",
4936 &[
4937 (fold_expression, 5, "fold expressions"),
4938 (binary_expression2, 3, "binary expressions"),
4939 (binary_expression, 0, "pre-alias binary expressions"),
4940 ],
4941 );
4942 check_func_space::<P, _>(src, "foo.cpp", |space| {
4943 for (name, want) in [("lt", 1), ("le", 1), ("eq", 1), ("idiom", 2), ("neg", 2)] {
4944 assert_eq!(
4945 child_space(&space, name).metrics.abc.conditions(),
4946 want,
4947 "fold in `{name}`"
4948 );
4949 }
4950 assert_eq!(
4951 space.metrics.abc.conditions(),
4952 2,
4953 "one condition per `#if` comparison"
4954 );
4955 });
4956 }
4957
4958 #[cfg(feature = "cpp")]
4959 #[test]
4960 fn cpp_comparison_folds_and_preproc_operands() {
4961 assert_cpp_comparison_folds_and_preproc_operands::<CppParser>(
4962 Cpp::BinaryExpression as u16,
4963 Cpp::BinaryExpression2 as u16,
4964 Cpp::FoldExpression as u16,
4965 );
4966 }
4967
4968 #[cfg(feature = "mozcpp")]
4969 #[test]
4970 fn mozcpp_comparison_folds_and_preproc_operands() {
4971 use crate::Mozcpp;
4972 assert_cpp_comparison_folds_and_preproc_operands::<crate::MozcppParser>(
4973 Mozcpp::BinaryExpression as u16,
4974 Mozcpp::BinaryExpression2 as u16,
4975 Mozcpp::FoldExpression as u16,
4976 );
4977 }
4978
4979 // #1383: a `relational_pattern`'s operator is not a condition of
4980 // its own — the `switch_expression_arm` that owns it already scores
4981 // the decision, exactly as it does for the constant arm `5 => 1`.
4982 // Counting both charged a relational arm twice, and twice C#'s own
4983 // cyclomatic decision count.
4984 //
4985 // Per `.claude/rules/grammar-dispatch.md` §11 the `operator <`
4986 // fixture above cannot prove this alone: its `is > 0` sits beside a
4987 // `binary_expression` comparison that supplies a plausible total on
4988 // its own. Here the pattern operators are the *only* `<` / `>` /
4989 // `>=` / `<=` in the file, so a readmitted `RelationalPattern`
4990 // parent is the only thing that can lift the count.
4991 //
4992 // Both spellings are covered, and asserted per member rather than
4993 // through a total, because they reached two different arms when this
4994 // was written: `n`'s `>` / `<` were gated by a `GT | LT` parent
4995 // allowlist and `g`'s `>=` / `<=` by a separate `RelationalPattern`
4996 // denial, so fixing one and not the other left the *other method* at
4997 // 4. #1420 merged all six comparison tokens onto the one allowlist,
4998 // which removes that particular way to get it half right — but the
4999 // per-member shape still earns its keep, since a total of 6 is what
5000 // both `{2, 2, 2}` and a regressed `{4, 2, 0}` report:
5001 //
5002 // | state | `n` | `g` | `c` |
5003 // |---|---|---|---|
5004 // | `relational_pattern` excluded (shipped) | 2 | 2 | 2 |
5005 // | readmitted to the allowlist | 4 | 4 | 2 |
5006 //
5007 // `c` is the constant-pattern control and reads 2 in both rows: it is
5008 // what the relational methods are supposed to agree with.
5009 #[cfg(feature = "csharp")]
5010 #[test]
5011 fn csharp_relational_pattern_does_not_double_count_its_arm() {
5012 let src = "class A {
5013 int n(int x) => x switch { > 5 => 1, < 0 => 2, _ => 3 };
5014 int g(int x) => x switch { >= 5 => 1, <= 0 => 2, _ => 3 };
5015 int c(int x) => x switch { 5 => 1, 0 => 2, _ => 3 };
5016 }";
5017 assert_csharp_fixture_spells(
5018 src,
5019 &[
5020 (Csharp::RelationalPattern as u16, 4, "relational patterns"),
5021 (Csharp::GT as u16, 1, "`>`"),
5022 (Csharp::LT as u16, 1, "`<`"),
5023 (Csharp::GTEQ as u16, 1, "`>=`"),
5024 (Csharp::LTEQ as u16, 1, "`<=`"),
5025 ],
5026 );
5027 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5028 // Per space, not through the root: the root can only see
5029 // the sum, and a `{4, 2, 0}` regression sums to the same 6
5030 // as the correct `{2, 2, 2}`. `cyclomatic()` parity is also
5031 // only expressible per space — `cyclomatic_sum()` folds in
5032 // a base of 1 for each one.
5033 assert_every_member_scores(&space.spaces[0], 3, 2, "one condition per non-discard arm");
5034 });
5035 }
5036
5037 // The `is` half of #1383, and the one whose over-count came from a
5038 // different owner: `if (x is > 0)` scored the `IfStatement`
5039 // condition slot *and* the pattern's `>`. `f`'s guard is the
5040 // deliberate control — a `binary_expression` comparison in the same
5041 // slot still scores exactly one, so the fix cannot be read as
5042 // "stop counting `if` conditions".
5043 //
5044 // The two combinator methods pin the compound forms the issue asks
5045 // about. `and` / `or` are keyword tokens C# ABC never listed and C#
5046 // cyclomatic never counts, so the arm stays the single decision
5047 // however many relational operands it carries — which is exactly
5048 // what would regress if a later fix re-derived the gate from the
5049 // operand instead of the parent.
5050 #[cfg(feature = "csharp")]
5051 #[test]
5052 fn csharp_is_pattern_and_combinators_score_one_decision() {
5053 let src = "class A {
5054 int m(int x) { if (x is > 0) { return 1; } return 0; }
5055 int f(int x) { if (x > 0) { return 1; } return 0; }
5056 int a(int x) { if (x is > 0 and < 9) { return 1; } return 0; }
5057 int o(int x) { if (x is < 0 or >= 100) { return 1; } return 0; }
5058 int n(int x) { if (x is not > 5) { return 1; } return 0; }
5059 }";
5060 assert_csharp_fixture_spells(
5061 src,
5062 &[
5063 (Csharp::RelationalPattern as u16, 6, "relational patterns"),
5064 (Csharp::IsPatternExpression as u16, 4, "`is` pattern tests"),
5065 (Csharp::AndPattern as u16, 1, "`and`"),
5066 (Csharp::OrPattern as u16, 1, "`or`"),
5067 (Csharp::NegatedPattern as u16, 1, "`not`"),
5068 (Csharp::GTEQ as u16, 1, "`>=` (only `o` carries one)"),
5069 ],
5070 );
5071 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5072 assert_every_member_scores(&space.spaces[0], 5, 1, "the `if` is the only decision");
5073 });
5074 }
5075
5076 // #1383's second, quieter effect, and the one its issue does not
5077 // mention: a relational pattern outside any decision slot went from
5078 // 1 to 0 as well, because the gate is on the operator's parent and
5079 // not on what encloses the pattern. That left `q` — the equivalent
5080 // binary comparison `x > 5` in the same slot — scoring 1 against
5081 // these four's 0, so a relational pattern read one *lower* than the
5082 // comparison it is sugar for. #1383 recorded that as a deliberate
5083 // exception to Fitzpatrick Rule 5 and kept it, because the only
5084 // route it had was to readmit the pattern's `<` / `>` token, which
5085 // would have scored the decision-slot case twice.
5086 //
5087 // #1461 closed it from the other end, and the test now pins the
5088 // agreement rather than the exception. The enclosing
5089 // `is_pattern_expression` scores by use, so `x is > 5` reads 1
5090 // wherever it is written — level with `q`, and level with the plain
5091 // type test `x is int` (`t`, whose `is_expression` moved for the
5092 // same reason). The pattern's own `>` still scores nothing, which
5093 // is what keeps the whole test at 1 rather than 2: one relational
5094 // operation, one condition.
5095 //
5096 // The four probes stay because they are four different enclosings —
5097 // a declarator, a bare `return`, an argument, a type test — and the
5098 // point was never that the pattern is special in one of them.
5099 #[cfg(feature = "csharp")]
5100 #[test]
5101 fn csharp_relational_pattern_scores_one_wherever_it_is_written() {
5102 let src = "class A {
5103 static bool M(bool b) { return b; }
5104 bool p(int x) { bool b = x is > 5; return b; }
5105 bool q(int x) { bool b = x > 5; return b; }
5106 bool r(int x) { return x is > 5; }
5107 bool s(int x) { return M(x is > 5); }
5108 bool t(object x) { bool b = x is int; return b; }
5109 }";
5110 assert_csharp_fixture_spells(
5111 src,
5112 &[
5113 (Csharp::RelationalPattern as u16, 3, "relational patterns"),
5114 (Csharp::IsPatternExpression as u16, 3, "`is` pattern tests"),
5115 (Csharp::IsExpression as u16, 1, "the `is int` control"),
5116 ],
5117 );
5118 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5119 let class = &space.spaces[0];
5120 assert_eq!(class.spaces.len(), 6, "`M` plus five probes");
5121 let by_name = |n: &str| {
5122 class
5123 .spaces
5124 .iter()
5125 .find(|s| s.name.as_deref() == Some(n))
5126 .unwrap_or_else(|| panic!("fixture lost `{n}`"))
5127 .metrics
5128 .abc
5129 .conditions()
5130 };
5131 // Named rather than indexed: the claim is about which
5132 // spelling scores what, so a reordering of the fixture must
5133 // not silently re-point the assertions.
5134 for probe in ["p", "r", "s"] {
5135 assert_eq!(by_name(probe), 1, "`{probe}`: the `is` test, once");
5136 }
5137 assert_eq!(by_name("t"), 1, "the bare type test, the same once");
5138 assert_eq!(by_name("q"), 1, "the comparison it is sugar for");
5139 assert_eq!(by_name("M"), 0, "no relational operator anywhere");
5140 });
5141 }
5142
5143 // The boundary of #1383: the pattern's operator stops counting, a
5144 // *binary* operator in the arm's `when` guard keeps counting (`w`).
5145 // Both sit under the same `switch_expression_arm`, so this is what
5146 // stops a later "patterns don't count" pass from suppressing the
5147 // guard too.
5148 //
5149 // A relational *pattern* in the guard (`g`) is the other side of that
5150 // line: the operator itself still scores nothing, because the gate is
5151 // on the operator's parent — but since #1422 the guard is a condition
5152 // slot, and an `is` test scores itself (`csharp_condition_scores_itself`),
5153 // so the slot does not pay again. That is what closed the gap this test used to
5154 // carry a `FIXME(#1422)` for: `when n is > 5` now reads level with
5155 // `when n > 5` instead of one below it.
5156 //
5157 // §8 holds throughout, which it did not before #1422. The old table
5158 // here recorded the defect: neither metric modelled the guard, so
5159 // ABC's count was whatever operator token happened to sit inside it
5160 // and cyclomatic's was nothing at all. Both tiers measured, before
5161 // and after:
5162 //
5163 // | guard | `conditions()` | `cyclomatic() - 1` |
5164 // |---|---|---|
5165 // | `when x % 2 == 0` (this fixture) | 3 → 3 | 2 → 3 |
5166 // | `when x > 2` | 3 → 3 | 2 → 3 |
5167 // | `when IsEven(x)` | 2 → 3 | 2 → 3 |
5168 //
5169 // The `==` is the guard's own operator and the only `==` in the
5170 // file, so trimming the `when` clause out of `w` drops its count to
5171 // 2 rather than leaving the assertion satisfied by something else;
5172 // `assert_csharp_fixture_spells` pins both guards by kind against
5173 // the same decay.
5174 #[cfg(feature = "csharp")]
5175 #[test]
5176 fn csharp_switch_arm_guard_operator_still_counts() {
5177 let src = "class A {
5178 int w(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
5179 int g(int x) => x switch { int n when n is > 5 => 1, _ => 0 };
5180 }";
5181 assert_csharp_fixture_spells(
5182 src,
5183 &[
5184 (Csharp::WhenClause as u16, 2, "`when` guards"),
5185 (Csharp::EQEQ as u16, 1, "the guard's `==`"),
5186 (Csharp::RelationalPattern as u16, 3, "relational patterns"),
5187 ],
5188 );
5189 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5190 let m = &space.spaces[0].spaces[0];
5191 assert_eq!(m.name.as_deref(), Some("w"));
5192 // 4 if the guard slot started counting a `binary_expression`
5193 // on top of its `==`, 2 if the `==` were suppressed along
5194 // with the pattern operators — both are live regressions.
5195 assert_eq!(
5196 m.metrics.abc.conditions(),
5197 3,
5198 "two arms plus the guard's `==`"
5199 );
5200 assert_eq!(m.metrics.cyclomatic.cyclomatic(), 4);
5201
5202 let g = &space.spaces[0].spaces[1];
5203 assert_eq!(g.name.as_deref(), Some("g"));
5204 assert_eq!(
5205 g.metrics.abc.conditions(),
5206 2,
5207 "one arm plus the guard slot, which scores the `is` test"
5208 );
5209 assert_eq!(g.metrics.cyclomatic.cyclomatic(), 3);
5210 });
5211 }
5212
5213 // #1422's headline: four spellings of one guard, previously worth
5214 // two different ABC numbers because nothing modelled the guard and
5215 // ABC counted whatever operator token happened to sit inside it —
5216 // `when x % 2 == 0` and `when x > 2` scored 1 through the
5217 // comparison-token arm, `when IsEven(x)` scored 0. Modelled as a
5218 // condition slot each contributes exactly 1, and the cyclomatic
5219 // `WhenClause` arm lifts the decision count to match.
5220 //
5221 // `none` is the unguarded control and the anchor: a two-arm switch
5222 // over the same relational patterns carrying no guard at all, so
5223 // every member here is exactly one guard away from it. Without that
5224 // row a fix that scored the *arms* differently would satisfy the
5225 // four guarded rows alike. Its arms run `> 2` before `> 0` because
5226 // the reverse order is a C# compile error (CS8510, the later arm
5227 // subsumed) — the guarded members escape that only because a guard
5228 // can fail, which is what makes their repeated `> 0` legal.
5229 //
5230 // `paren` is not a fourth spelling for its own sake — it is the only
5231 // member whose guard reaches the slot as a
5232 // `parenthesized_expression` for the peel (`csharp_wrapper_operand`)
5233 // to take off, since `when_clause` wraps a parenthesised guard in a
5234 // real `parenthesized_expression` rather than the anonymous parens
5235 // `catch_filter_clause` uses.
5236 //
5237 // `nullc` is #1459's row and the counter-example that falsified the
5238 // claim above when it was first written. The slot leaves an
5239 // operator guard to the arm that already counts the operator, and
5240 // no ABC arm counted `??`, so this one spelling read 2 where the
5241 // other four read 3. Counting the token levels it without touching
5242 // the slot; an unconditional `+1` in the slot instead would have
5243 // taken `cmp` to 4, re-creating the double count #1422 removed.
5244 //
5245 // It is the one guarded member whose cyclomatic is not 4, and that
5246 // is not a discrepancy to fix: C# cyclomatic counts `??` as a
5247 // decision *in addition to* the guard clause, so the arm below
5248 // narrows the ABC-versus-cyclomatic gap from two to one rather than
5249 // closing it. The remaining one is the slot's standing policy of
5250 // leaving a counted operator to its arm, and `cmp` pays it
5251 // too — it just happens to break even there.
5252 #[cfg(feature = "csharp")]
5253 #[test]
5254 fn csharp_switch_arm_guard_scores_one_condition_however_spelled() {
5255 let src = "class A {
5256 static bool IsEven(int x) { return true; }
5257 int tok(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
5258 int cmp(int x) => x switch { > 0 when x > 2 => 1, > 0 => 2, _ => 3 };
5259 int call(int x) => x switch { > 0 when IsEven(x) => 1, > 0 => 2, _ => 3 };
5260 int paren(int x) => x switch { > 0 when (IsEven(x)) => 1, > 0 => 2, _ => 3 };
5261 int nullc(int x, bool? b) => x switch { > 0 when b ?? false => 1, > 0 => 2, _ => 3 };
5262 int none(int x) => x switch { > 2 => 1, > 0 => 2, _ => 3 };
5263 }";
5264 assert_csharp_fixture_spells(
5265 src,
5266 &[
5267 (Csharp::WhenClause as u16, 5, "`when` guards"),
5268 (Csharp::EQEQ as u16, 1, "`tok`'s `==`"),
5269 (Csharp::QMARKQMARK as u16, 1, "`nullc`'s `??`"),
5270 (
5271 Csharp::ParenthesizedExpression as u16,
5272 1,
5273 "`paren`'s parenthesised guard",
5274 ),
5275 // By the call's *argument list*, which carries no
5276 // numeric-suffix alias, rather than by
5277 // `invocation_expression`, which has three
5278 // (`InvocationExpression` / `2` / `3`) and would pin this
5279 // anchor to whichever one the pinned grammar happens to
5280 // emit. `call` and `paren` hold the only two argument
5281 // lists in the fixture — `IsEven(int x)` is a
5282 // `parameter_list`.
5283 (Csharp::ArgumentList as u16, 2, "the `IsEven` calls"),
5284 ],
5285 );
5286 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5287 assert_members_score(
5288 &space.spaces[0],
5289 &[
5290 ("IsEven", 0, 1),
5291 ("tok", 3, 4),
5292 ("cmp", 3, 4),
5293 ("call", 3, 4),
5294 ("paren", 3, 4),
5295 // Was 2 — the guard spelling nothing counted.
5296 // Cyclomatic 5 rather than 4 because `??` is a
5297 // decision there on top of the guard clause.
5298 ("nullc", 3, 5),
5299 ("none", 2, 3),
5300 ],
5301 );
5302 });
5303 }
5304
5305 // The bare type test `x is int` is `is_expression` (391); only once
5306 // a pattern is involved (`x is int y`, `x is null`) does the grammar
5307 // emit `is_pattern_expression` (392). They are distinct kinds, and
5308 // the former `csharp_bool_terminal_kinds!()` listed only the
5309 // second, so the two spellings of one test disagreed: `if (x is int)` scored zero
5310 // conditions against a cyclomatic decision of one while
5311 // `if (x is int y)` scored one.
5312 //
5313 // Found by the whole-branch review of this batch, and it falsified
5314 // #1422's own claim in the arm above that a guard "scores one
5315 // however it is spelled" — `when x is int` read 1 where
5316 // `when IsEven(x)` read 2, which is the spelling-dependence that
5317 // fix exists to remove. It is the C# half of the gap #1421 closed
5318 // for Kotlin by adding `IsExpression | InExpression` there. Both
5319 // languages' type tests have since moved out of the terminal set to
5320 // an unconditional arm (#1461), which leaves every number here
5321 // unchanged — the slot no longer counts the test and the arm does.
5322 //
5323 // The plain `if` members are the control that keeps this honest: a
5324 // guard-only fixture could be satisfied by a guard-specific rule,
5325 // and the defect was never guard-specific. `q` and `iq` are the
5326 // already-correct `is_pattern_expression` twins, so a regression
5327 // that reintroduced the asymmetry fails on the pair rather than on
5328 // an absolute number.
5329 #[cfg(feature = "csharp")]
5330 #[test]
5331 fn csharp_bare_is_type_test_scores_one_condition() {
5332 let src = "class A {
5333 static bool IsEven(int x) { return true; }
5334 int g(object x) => x switch { object o when x is int => 1, _ => 0 };
5335 int gq(object x) => x switch { object o when x is int y => 1, _ => 0 };
5336 int gc(object x) => x switch { object o when IsEven(1) => 1, _ => 0 };
5337 int p(object x) { if (x is int) { return 1; } return 0; }
5338 int q(object x) { if (x is int y) { return 1; } return 0; }
5339 }";
5340 assert_csharp_fixture_spells(
5341 src,
5342 &[
5343 (Csharp::IsExpression as u16, 2, "the bare `is` type tests"),
5344 (
5345 Csharp::IsPatternExpression as u16,
5346 2,
5347 "the designation-pattern twins",
5348 ),
5349 (Csharp::WhenClause as u16, 3, "`when` guards"),
5350 ],
5351 );
5352 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5353 assert_members_score(
5354 &space.spaces[0],
5355 &[
5356 ("IsEven", 0, 1),
5357 // Level with `gq` and `gc`: every guard spelling
5358 // scores one, which is what #1422 claims.
5359 ("g", 2, 3),
5360 ("gq", 2, 3),
5361 ("gc", 2, 3),
5362 // Level with `q`: the two spellings of one type
5363 // test agree outside a guard too.
5364 ("p", 1, 2),
5365 ("q", 1, 2),
5366 ],
5367 );
5368 });
5369 }
5370
5371 // The null-forgiving `b!` is transparent: it scores whatever the
5372 // operand it wraps scores (#1463).
5373 //
5374 // `postfix_unary_expression` was in neither
5375 // the former `csharp_bool_terminal_kinds!()` nor the wrapper peel,
5376 // so every spelling below scored **zero** conditions against a cyclomatic
5377 // decision of one, while the bare `b` control scored one. In a
5378 // codebase with nullable reference types enabled the suffix is
5379 // everywhere, so the gap is not an exotic corner.
5380 //
5381 // Both walker paths, per `.claude/rules/grammar-dispatch.md` §11:
5382 // `p*` go through the `if` predicate slot
5383 // (`csharp_count_condition`) and `c*` through the `&&` chain slot
5384 // (also `csharp_count_condition`, per chain operand), which reach
5385 // the peel independently. A fixture of only one
5386 // reads correct with the other path dead.
5387 //
5388 // The controls are load-bearing in both directions. `p` / `c` pin
5389 // what the wrapper must score, so a regression fails on the pair
5390 // rather than on an absolute number; and the cyclomatic column pins
5391 // that only `conditions` moved — the suffix is not a decision in any
5392 // language, so a fix that moved both together would be a different
5393 // bug.
5394 #[cfg(feature = "csharp")]
5395 #[test]
5396 fn csharp_null_forgiving_operand_scores_like_its_operand() {
5397 let src = "class A {
5398 int p(bool b) { if (b) { return 1; } return 0; }
5399 int pn(bool b) { if (b!) { return 1; } return 0; }
5400 int pp(bool b) { if ((b!)) { return 1; } return 0; }
5401 int pd(bool b) { if (b!!) { return 1; } return 0; }
5402 int pb(bool b) { if (!b!) { return 1; } return 0; }
5403 int c(bool b) { if (b && b) { return 1; } return 0; }
5404 int cn(bool b) { if (b! && b) { return 1; } return 0; }
5405 int cp(bool b) { if ((b!) && b) { return 1; } return 0; }
5406 }";
5407 assert_csharp_fixture_spells(
5408 src,
5409 &[
5410 // Seven, not five: `b!!` is two nested nodes, and each
5411 // chain member carries one. Trimming a `!` out of any
5412 // member fails here by name instead of turning that
5413 // member into a silent copy of its control.
5414 (
5415 Csharp::PostfixUnaryExpression as u16,
5416 7,
5417 "the null-forgiving suffixes",
5418 ),
5419 // The `if (…)` parens are anonymous tokens rather than
5420 // nodes in this grammar, so these two are `pp`'s and
5421 // `cp`'s explicit ones — the wrappers that make the peel
5422 // chain rather than peel once.
5423 (
5424 Csharp::ParenthesizedExpression as u16,
5425 2,
5426 "the explicit parenthesised spellings",
5427 ),
5428 (Csharp::PrefixUnaryExpression as u16, 1, "`pb`'s `!`"),
5429 ],
5430 );
5431 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5432 assert_members_score(
5433 &space.spaces[0],
5434 &[
5435 ("p", 1, 2),
5436 ("pn", 1, 2),
5437 ("pp", 1, 2),
5438 ("pd", 1, 2),
5439 ("pb", 1, 2),
5440 ("c", 2, 3),
5441 ("cn", 2, 3),
5442 ("cp", 2, 3),
5443 ],
5444 );
5445 });
5446 }
5447
5448 // The null-forgiving operand is read by position, but from the end
5449 // the grammar cannot move: a `postfix_unary_expression` *starts* at
5450 // its operand, so `child(0)` is the operand whatever `extra` sits
5451 // between it and the operator. The operator is identified by
5452 // membership rather than by index for the same reason.
5453 //
5454 // The other two wrappers name no field either — the C# grammar gives
5455 // `parenthesized_expression`, `prefix_unary_expression` and
5456 // `postfix_unary_expression` an empty `fields` map in
5457 // node-types.json — so `child(1)` was a comment whenever one was
5458 // written there, and `nc` / `rc` scored zero. They read their
5459 // operand as the first one that is not an extra since #1455, so all
5460 // three pairs agree with their controls.
5461 #[cfg(feature = "csharp")]
5462 #[test]
5463 fn csharp_null_forgiving_operand_survives_an_interposed_comment() {
5464 let src = "class A {
5465 int p(bool b) { if (b!) { return 1; } return 0; }
5466 int pc(bool b) { if (b /*c*/ !) { return 1; } return 0; }
5467 int n(bool b) { if (!b) { return 1; } return 0; }
5468 int nc(bool b) { if (! /*c*/ b) { return 1; } return 0; }
5469 int r(bool b) { if ((b)) { return 1; } return 0; }
5470 int rc(bool b) { if (( /*c*/ b)) { return 1; } return 0; }
5471 }";
5472 assert_csharp_fixture_spells(
5473 src,
5474 &[
5475 (
5476 Csharp::Comment as u16,
5477 3,
5478 "the interposed comments — without them every row is its own control",
5479 ),
5480 (Csharp::PostfixUnaryExpression as u16, 2, "`p` and `pc`"),
5481 (Csharp::PrefixUnaryExpression as u16, 2, "`n` and `nc`"),
5482 (
5483 Csharp::ParenthesizedExpression as u16,
5484 2,
5485 "`r` and `rc` — the `if` parens are anonymous tokens",
5486 ),
5487 ],
5488 );
5489 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5490 assert_members_score(
5491 &space.spaces[0],
5492 &[
5493 ("p", 1, 2),
5494 // The row #1463 fixes.
5495 ("pc", 1, 2),
5496 ("n", 1, 2),
5497 // #1455: `child(1)` of the prefix wrapper is the
5498 // comment.
5499 ("nc", 1, 2),
5500 ("r", 1, 2),
5501 // #1455 again, through the paren wrapper.
5502 ("rc", 1, 2),
5503 ],
5504 );
5505 });
5506 }
5507
5508 // The statement `switch` reaches the same `when_clause` kind through
5509 // a different parent — `switch_section` rather than
5510 // `switch_expression_arm` — which makes it an independent path in
5511 // the sense of `.claude/rules/grammar-dispatch.md` §11: the arms
5512 // themselves are counted by two different owners (a `Case` token
5513 // here, the `SwitchExpressionArm` node there), so a guard rule
5514 // written against one shape could be dead for the other and every
5515 // expression-form fixture would still read correct.
5516 #[cfg(feature = "csharp")]
5517 #[test]
5518 fn csharp_statement_switch_section_guard_counts() {
5519 let src = "class A {
5520 static bool IsEven(int x) { return true; }
5521 int guarded(int x) {
5522 switch (x) {
5523 case > 0 when IsEven(x): return 1;
5524 case > 0: return 2;
5525 default: return 3;
5526 }
5527 }
5528 int plain(int x) {
5529 switch (x) {
5530 case > 2: return 1;
5531 case > 0: return 2;
5532 default: return 3;
5533 }
5534 }
5535 }";
5536 assert_csharp_fixture_spells(
5537 src,
5538 &[
5539 (Csharp::WhenClause as u16, 1, "the `case … when` guard"),
5540 (Csharp::SwitchSection as u16, 6, "switch sections"),
5541 (
5542 Csharp::SwitchExpressionArm as u16,
5543 0,
5544 "expression arms (this fixture is the statement form)",
5545 ),
5546 ],
5547 );
5548 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5549 assert_members_score(
5550 &space.spaces[0],
5551 &[("IsEven", 0, 1), ("guarded", 3, 4), ("plain", 2, 3)],
5552 );
5553 });
5554 }
5555
5556 // The sibling #1422's issue body does not mention, measured to have
5557 // the identical defect: `catch (E e) when (…)` is a second guard
5558 // spelling on its own `catch_filter_clause` kind, and it scored
5559 // conditions 3 / 2 / 2 for the comparison / call spellings and the
5560 // unfiltered control while `cyclomatic()` sat at 2 for all three.
5561 //
5562 // ABC runs one above `cyclomatic() - 1` on every row, guarded and
5563 // unguarded alike, and that offset is not this fix's: C# ABC counts
5564 // the `Try` token as a condition (Fitzpatrick Rule 5, the same
5565 // family as `else`) where cyclomatic counts only the `catch`. The
5566 // guard moves both tiers by exactly one, which is what `none`
5567 // anchors — the offset is a constant here, not something a guard
5568 // introduces.
5569 //
5570 // `paren` carries the *double* parenthesis on purpose. The grammar
5571 // spells `catch_filter_clause`'s own parentheses as anonymous tokens
5572 // the way `if_statement` does, so `when (IsEven(x))` hands the slot a
5573 // bare `invocation_expression` with no parenthesis for the peel
5574 // (`csharp_wrapper_operand`) to take off; only a
5575 // second pair of parentheses produces a `parenthesized_expression`
5576 // there.
5577 #[cfg(feature = "csharp")]
5578 #[test]
5579 fn csharp_catch_filter_guard_scores_one_condition_however_spelled() {
5580 let src = "class A {
5581 static bool IsEven(int x) { return true; }
5582 int tok(int x) { try { return 1; } catch (System.Exception e) when (x % 2 == 0) { return 2; } }
5583 int cmp(int x) { try { return 1; } catch (System.Exception e) when (x > 0) { return 2; } }
5584 int call(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5585 int paren(int x) { try { return 1; } catch (System.Exception e) when ((IsEven(x))) { return 2; } }
5586 int none(int x) { try { return 1; } catch (System.Exception e) { return 2; } }
5587 }";
5588 assert_csharp_fixture_spells(
5589 src,
5590 &[
5591 (
5592 Csharp::CatchFilterClause as u16,
5593 4,
5594 "`catch … when` filters",
5595 ),
5596 (Csharp::CatchClause as u16, 5, "catch clauses"),
5597 (Csharp::WhenClause as u16, 0, "switch guards (none here)"),
5598 (
5599 Csharp::ParenthesizedExpression as u16,
5600 1,
5601 "`paren`'s inner parentheses",
5602 ),
5603 ],
5604 );
5605 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5606 assert_members_score(
5607 &space.spaces[0],
5608 &[
5609 ("IsEven", 0, 1),
5610 ("tok", 3, 3),
5611 ("cmp", 3, 3),
5612 ("call", 3, 3),
5613 ("paren", 3, 3),
5614 ("none", 2, 2),
5615 ],
5616 );
5617 });
5618 }
5619
5620 // A guarded discard is where the two rules meet. `_ when g =>` is
5621 // already exempt from the `default:` exclusion —
5622 // `csharp_switch_expression_arm_is_bare_discard` looks for a
5623 // `when_clause` for exactly this reason — so after #1422 it scores
5624 // twice: once as an arm that is no longer unconditional, once for
5625 // the guard that makes it conditional. `bare` is the control that
5626 // keeps the exclusion itself pinned: drop the guard and the arm goes
5627 // back to costing nothing.
5628 #[cfg(feature = "csharp")]
5629 #[test]
5630 fn csharp_guarded_discard_arm_scores_arm_and_guard() {
5631 let src = "class A {
5632 static bool IsEven(int x) { return true; }
5633 int guarded(int x) => x switch { > 0 => 1, _ when IsEven(x) => 2, _ => 3 };
5634 int bare(int x) => x switch { > 0 => 1, _ => 3 };
5635 }";
5636 assert_csharp_fixture_spells(
5637 src,
5638 &[
5639 (Csharp::WhenClause as u16, 1, "the guarded discard"),
5640 (Csharp::Discard as u16, 3, "discard patterns"),
5641 ],
5642 );
5643 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5644 assert_members_score(
5645 &space.spaces[0],
5646 &[("IsEven", 0, 1), ("guarded", 3, 4), ("bare", 1, 2)],
5647 );
5648 });
5649 }
5650
5651 // A comment inside the guard must not cost it its condition.
5652 // tree-sitter `extra`s are *named* nodes, so `comment` can be the
5653 // first named child of either guard clause — and a slot that read
5654 // only the first named child scored `when /*c*/ IsEven(x)` as 1
5655 // where `when IsEven(x)` scores 2, silently reinstating the very
5656 // spelling-dependence #1422 removes and breaking the §8 parity the
5657 // fix establishes. Found in review of #1422 before it shipped.
5658 //
5659 // This is the C# instance of the class #1181 fixed for the ternary,
5660 // where a comment between a token and its operand shifted every
5661 // positional read. Each commented member is asserted against the
5662 // same literal pair as its uncommented twin in the same table, so a
5663 // regression that moved only the commented spelling fails here; the
5664 // literals themselves are pinned by
5665 // `csharp_switch_arm_guard_scores_one_condition_however_spelled` and
5666 // its catch-filter sibling.
5667 #[cfg(feature = "csharp")]
5668 #[test]
5669 fn csharp_guard_keeps_its_condition_across_a_comment() {
5670 let src = "class A {
5671 static bool IsEven(int x) => true;
5672 int plain(int x) => x switch { > 0 when IsEven(x) => 1, _ => 0 };
5673 int cmt(int x) => x switch { > 0 when /*c*/ IsEven(x) => 1, _ => 0 };
5674 int cplain(int x) { try { return 1; } catch (System.Exception e) when (IsEven(x)) { return 2; } }
5675 int ccmt(int x) { try { return 1; } catch (System.Exception e) when (/*c*/ IsEven(x)) { return 2; } }
5676 }";
5677 assert_csharp_fixture_spells(
5678 src,
5679 &[
5680 (Csharp::Comment as u16, 2, "the in-guard comments"),
5681 (Csharp::WhenClause as u16, 2, "switch guards"),
5682 (Csharp::CatchFilterClause as u16, 2, "catch filters"),
5683 ],
5684 );
5685 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5686 assert_members_score(
5687 &space.spaces[0],
5688 &[
5689 ("IsEven", 0, 1),
5690 ("plain", 2, 3),
5691 ("cmt", 2, 3),
5692 ("cplain", 3, 3),
5693 ("ccmt", 3, 3),
5694 ],
5695 );
5696 });
5697 }
5698
5699 // Why the guard is a condition *slot* and not a flat +1: a compound
5700 // guard keeps its sub-structure. `when a > 1 && b < 2` scores the
5701 // two comparisons through the token arm and nothing extra from the
5702 // slot, because a `&&` chain scores itself
5703 // (`csharp_condition_scores_itself`) — so it reads one above `single`'s
5704 // lone comparison rather than collapsing to the same number, and
5705 // cyclomatic agrees because it counts the `&&`.
5706 #[cfg(feature = "csharp")]
5707 #[test]
5708 fn csharp_compound_guard_keeps_its_sub_structure() {
5709 let src = "class A {
5710 int compound(int a, int b) => a switch { > 0 when a > 1 && b < 2 => 1, _ => 0 };
5711 int single(int a, int b) => a switch { > 0 when a > 1 => 1, _ => 0 };
5712 }";
5713 assert_csharp_fixture_spells(
5714 src,
5715 &[
5716 (Csharp::WhenClause as u16, 2, "`when` guards"),
5717 (Csharp::AMPAMP as u16, 1, "`compound`'s `&&`"),
5718 (Csharp::LT as u16, 1, "`compound`'s `<`"),
5719 ],
5720 );
5721 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5722 assert_members_score(&space.spaces[0], &[("compound", 3, 4), ("single", 2, 3)]);
5723 });
5724 }
5725
5726 // #1275: tree-sitter-c-sharp spells `int?`, `where T : class?`, the
5727 // ternary and `a?.b` with one and the same bare `?` token, so the
5728 // unguarded `QMARK` arm scored the two type-syntax forms as
5729 // decisions.
5730 //
5731 // The fixture carries two `nullable_type` `?`, one
5732 // `type_parameter_constraint` `?`, and one real ternary over a real
5733 // `>` comparison, so every way of getting the gate wrong lands on
5734 // its own number: 5 pre-fix, 3 if only `NullableType` is denied, 4
5735 // if only `TypeParameterConstraint` is, 1 if the gate swallows the
5736 // genuine ternary, 0 if the fixture stops parsing. Asserting 0 on a
5737 // nullable-only body would have been vacuous — an unparsable file
5738 // scores 0 too.
5739 #[cfg(feature = "csharp")]
5740 #[test]
5741 fn csharp_nullable_type_syntax_is_not_a_condition() {
5742 check_metrics::<CsharpParser>(
5743 "class A {
5744 int M<T>(int? p, int a, int b) where T : class? {
5745 int? q = null;
5746 return a > b ? 1 : 2;
5747 }
5748 }",
5749 "foo.cs",
5750 |metric| {
5751 assert_eq!(metric.abc.conditions_sum(), 2);
5752 },
5753 );
5754 }
5755
5756 // The polarity pin for #1275's C# half. `csharp_count_token_condition`
5757 // denies the two type-syntax parents rather than allowing the two
5758 // decision-bearing ones, so that `a?.b` and `a?[0]` keep counting by
5759 // construction: both spell their operator as the same bare `?` under
5760 // `conditional_access_expression`, and C# cyclomatic counts that node
5761 // (`safe_navigation_chain_parity` pins C# at one decision per
5762 // operator alongside every other safe-navigation language).
5763 //
5764 // This test is what stops a later "make all three languages
5765 // consistent" pass from flipping C# to a `ConditionalExpression`
5766 // allowlist: that would silently drop both `?` counts here while
5767 // every other ABC test still passed. The third condition is the
5768 // `??`, which #1459 added to close the gap this comment used to
5769 // record as out of scope for #1275 — so a flipped `QMARK` polarity
5770 // now reads 1 rather than 0, and the discrimination is unchanged.
5771 #[cfg(feature = "csharp")]
5772 #[test]
5773 fn csharp_conditional_access_still_counts_as_a_condition() {
5774 let src = "class A {
5775 object M(string s, int[] xs) {
5776 return s?.Length ?? xs?[0];
5777 }
5778 }";
5779 // The total is now fed by two token arms rather than one, so it
5780 // carries the per-source claim only alongside these counts: 3 is
5781 // two bare `?` plus one `??`, and without them a `QMARK` arm
5782 // that had stopped counting `a?[0]` while `??` gained a second
5783 // count elsewhere would still read 3.
5784 assert_csharp_fixture_spells(
5785 src,
5786 &[
5787 (Csharp::QMARK as u16, 2, "the `?.` and `?[` operators"),
5788 (Csharp::QMARKQMARK as u16, 1, "the `??`"),
5789 ],
5790 );
5791 check_metrics::<CsharpParser>(src, "foo.cs", |metric| {
5792 assert_eq!(metric.abc.conditions_sum(), 3);
5793 });
5794 }
5795
5796 // The `QMARKQMARK` arm away from any guard, where nothing else can
5797 // supply the count (#1459). `coalesce`'s body holds no comparison,
5798 // no call and no control-flow keyword, so its one condition is the
5799 // `??` and nothing else; it read 0 before the arm while C#
5800 // cyclomatic scored the decision, which is the direction ABC is not
5801 // allowed to sit on.
5802 //
5803 // `plain` is the control that keeps this from being an assertion
5804 // about `return` or about the method shape: same body without the
5805 // operator, 0 conditions and 1 decision.
5806 #[cfg(feature = "csharp")]
5807 #[test]
5808 fn csharp_null_coalescing_is_a_condition() {
5809 let src = "class A {
5810 int coalesce(int? x) { return x ?? 0; }
5811 int plain(int x) { return x; }
5812 }";
5813 assert_csharp_fixture_spells(src, &[(Csharp::QMARKQMARK as u16, 1, "the `??`")]);
5814 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
5815 assert_members_score(&space.spaces[0], &[("coalesce", 1, 2), ("plain", 0, 1)]);
5816 });
5817 }
5818
5819 // The other direction of #1275's C# gate, isolated: narrowing
5820 // `QMARK` must not swallow a real ternary. The fixture carries no
5821 // comparison operator at all, so the ternary is the only thing that
5822 // can put the count above the walker's own contribution — in the
5823 // test above, a surviving `>` would have masked an over-suppressed
5824 // `?` at 1 instead of 0 (grammar-dispatch §11). A nullable
5825 // parameter keeps the denied production live in the same parse, so
5826 // one fixture proves both directions: 3 pre-fix, 2 after, 1 if the
5827 // allowlist/denylist is aimed at `ConditionalExpression`.
5828 //
5829 // The 2 is the `?` token plus the ternary's condition slot, which
5830 // `csharp_walk_for_conditions` counts as a Fitzpatrick unary
5831 // condition. That is why this is not written as an
5832 // `assert_deepest_conditions_match_cyclomatic` parity pin the way
5833 // the Java `<` / `>` sibling is: C# cyclomatic scores this method 1
5834 // decision, and the divergence is the pre-existing unary-condition
5835 // rule, not the `?` gate.
5836 #[cfg(feature = "csharp")]
5837 #[test]
5838 fn csharp_ternary_still_counts_alongside_nullable_types() {
5839 check_metrics::<CsharpParser>(
5840 "class A {
5841 int M(int? n, bool c) {
5842 return c ? 1 : 2;
5843 }
5844 }",
5845 "foo.cs",
5846 |metric| {
5847 assert_eq!(metric.abc.conditions_sum(), 2);
5848 },
5849 );
5850 }
5851
5852 #[cfg(feature = "csharp")]
5853 #[test]
5854 fn csharp_aliased_invocation_expression_branches() {
5855 // Regression for issue #94 (lesson #2): the C# grammar emits three
5856 // aliased `kind_id`s for `invocation_expression`. Code that matches
5857 // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
5858 // branches whenever the AST emits an aliased variant. The three
5859 // method calls live in `M`, so the per-method maximum (visible at
5860 // the unit-space aggregate as `branches_max`) must be 3.
5861 check_metrics::<CsharpParser>(
5862 "class A {
5863 void M() {
5864 System.Console.WriteLine(1);
5865 System.Console.WriteLine(2);
5866 System.Console.WriteLine(3);
5867 }
5868 }",
5869 "foo.cs",
5870 |metric| {
5871 assert_eq!(metric.abc.branches_max(), 3);
5872 assert_eq!(metric.abc.conditions_max(), 0);
5873 },
5874 );
5875 }
5876
5877 #[cfg(feature = "php")]
5878 #[test]
5879 fn php_zero_abc() {
5880 check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
5881 assert_eq!(metric.abc.assignments_sum(), 0);
5882 assert_eq!(metric.abc.branches_sum(), 0);
5883 assert_eq!(metric.abc.conditions_sum(), 0);
5884 insta::assert_json_snapshot!(metric.abc);
5885 });
5886 }
5887
5888 #[cfg(feature = "php")]
5889 #[test]
5890 fn php_simple_assignment() {
5891 check_metrics::<PhpParser>(
5892 "<?php
5893function f(): void {
5894 $a = 1;
5895 $b = 2;
5896}",
5897 "foo.php",
5898 |metric| insta::assert_json_snapshot!(metric.abc),
5899 );
5900 }
5901
5902 #[cfg(feature = "php")]
5903 #[test]
5904 fn php_augmented_assignment() {
5905 check_metrics::<PhpParser>(
5906 "<?php
5907function f(int $x): int {
5908 $a = 0;
5909 $a += $x;
5910 $a -= 1;
5911 $a *= 2;
5912 return $a;
5913}",
5914 "foo.php",
5915 |metric| insta::assert_json_snapshot!(metric.abc),
5916 );
5917 }
5918
5919 #[cfg(feature = "php")]
5920 #[test]
5921 fn php_const_excluded() {
5922 // Constant declarations and enum cases are NOT counted as
5923 // assignments — they declare immutable values.
5924 check_metrics::<PhpParser>(
5925 "<?php
5926class A {
5927 const PI = 3.14;
5928 const E = 2.71;
5929}
5930enum Color {
5931 case Red;
5932 case Green;
5933}",
5934 "foo.php",
5935 |metric| insta::assert_json_snapshot!(metric.abc),
5936 );
5937 }
5938
5939 #[cfg(feature = "php")]
5940 #[test]
5941 fn php_function_call() {
5942 check_metrics::<PhpParser>(
5943 "<?php
5944function f(): void {
5945 foo();
5946 bar(1, 2);
5947}",
5948 "foo.php",
5949 |metric| insta::assert_json_snapshot!(metric.abc),
5950 );
5951 }
5952
5953 #[cfg(feature = "php")]
5954 #[test]
5955 fn php_method_call() {
5956 check_metrics::<PhpParser>(
5957 "<?php
5958function f($obj): void {
5959 $obj->m1();
5960 $obj->m2(1);
5961}",
5962 "foo.php",
5963 |metric| insta::assert_json_snapshot!(metric.abc),
5964 );
5965 }
5966
5967 #[cfg(feature = "php")]
5968 #[test]
5969 fn php_static_call() {
5970 check_metrics::<PhpParser>(
5971 "<?php
5972function f(): void {
5973 Foo::bar();
5974 Foo::baz(1);
5975}",
5976 "foo.php",
5977 |metric| insta::assert_json_snapshot!(metric.abc),
5978 );
5979 }
5980
5981 #[cfg(feature = "php")]
5982 #[test]
5983 fn php_nullsafe_call() {
5984 check_metrics::<PhpParser>(
5985 "<?php
5986function f($obj): void {
5987 $obj?->m1();
5988 $obj?->m2(1);
5989}",
5990 "foo.php",
5991 |metric| insta::assert_json_snapshot!(metric.abc),
5992 );
5993 }
5994
5995 #[cfg(feature = "php")]
5996 #[test]
5997 fn php_object_creation() {
5998 check_metrics::<PhpParser>(
5999 "<?php
6000function f(): void {
6001 new Foo();
6002 new Bar(1);
6003}",
6004 "foo.php",
6005 |metric| insta::assert_json_snapshot!(metric.abc),
6006 );
6007 }
6008
6009 #[cfg(feature = "php")]
6010 #[test]
6011 fn php_comparison_eq() {
6012 check_metrics::<PhpParser>(
6013 "<?php
6014function f(int $a, int $b): bool {
6015 return $a == $b || $a != $b;
6016}",
6017 "foo.php",
6018 |metric| insta::assert_json_snapshot!(metric.abc),
6019 );
6020 }
6021
6022 #[cfg(feature = "php")]
6023 #[test]
6024 fn php_comparison_strict() {
6025 check_metrics::<PhpParser>(
6026 "<?php
6027function f(int $a, int $b): bool {
6028 return $a === $b || $a !== $b;
6029}",
6030 "foo.php",
6031 |metric| insta::assert_json_snapshot!(metric.abc),
6032 );
6033 }
6034
6035 #[cfg(feature = "php")]
6036 #[test]
6037 fn php_spaceship() {
6038 check_metrics::<PhpParser>(
6039 "<?php
6040function f(int $a, int $b): int {
6041 return $a <=> $b;
6042}",
6043 "foo.php",
6044 |metric| insta::assert_json_snapshot!(metric.abc),
6045 );
6046 }
6047
6048 #[cfg(feature = "php")]
6049 #[test]
6050 fn php_instanceof() {
6051 check_metrics::<PhpParser>(
6052 "<?php
6053function f($x): bool {
6054 return $x instanceof Foo;
6055}",
6056 "foo.php",
6057 |metric| insta::assert_json_snapshot!(metric.abc),
6058 );
6059 }
6060
6061 #[cfg(feature = "php")]
6062 #[test]
6063 fn php_complex_function() {
6064 // One snippet exercising A, B, C buckets together.
6065 check_metrics::<PhpParser>(
6066 "<?php
6067function f(int $a, int $b): int {
6068 $sum = $a + $b;
6069 $prod = $a * $b;
6070 if ($sum > 0 && $prod === 0) {
6071 return foo($sum);
6072 }
6073 return bar()->double();
6074}",
6075 "foo.php",
6076 |metric| insta::assert_json_snapshot!(metric.abc),
6077 );
6078 }
6079
6080 #[cfg(feature = "php")]
6081 #[test]
6082 fn php_if_boolean_literal_condition() {
6083 check_metrics::<PhpParser>(
6084 "<?php\n\
6085 function f() {\n\
6086 \x20 if (true) {} // +1c\n\
6087 \x20 if (!false) {} // +1c\n\
6088 \x20 while (true) {} // +1c\n\
6089 \x20 do {} while (false); // +1c\n\
6090 }\n",
6091 "foo.php",
6092 |metric| {
6093 assert_eq!(metric.abc.conditions_sum(), 4);
6094 insta::assert_json_snapshot!(metric.abc);
6095 },
6096 );
6097 }
6098
6099 #[cfg(feature = "php")]
6100 #[test]
6101 fn php_methods_arguments_with_conditions() {
6102 check_metrics::<PhpParser>(
6103 "<?php\n\
6104 function f($a, $b) {\n\
6105 \x20 m($a, $b); // +1b\n\
6106 \x20 m(!$a, !$b); // +1b +2c\n\
6107 }\n",
6108 "foo.php",
6109 |metric| {
6110 assert_eq!(metric.abc.branches_sum(), 2);
6111 assert_eq!(metric.abc.conditions_sum(), 2);
6112 insta::assert_json_snapshot!(metric.abc);
6113 },
6114 );
6115 }
6116
6117 #[cfg(feature = "php")]
6118 #[test]
6119 fn php_return_with_conditions() {
6120 check_metrics::<PhpParser>(
6121 "<?php\n\
6122 function m1($z) { return !($z >= 0); }\n\
6123 function m2($x) { return (((!$x))); }\n\
6124 function m3($x, $y) { return $x && $y; }\n",
6125 "foo.php",
6126 |metric| {
6127 // m1: `>=` (1). m2: walker unwraps to $x (1).
6128 // m3: `&&` walker counts both (2). Sum: 4.
6129 assert_eq!(metric.abc.conditions_sum(), 4);
6130 insta::assert_json_snapshot!(metric.abc);
6131 },
6132 );
6133 }
6134
6135 #[cfg(feature = "php")]
6136 #[test]
6137 fn php_name2_hidden_rule_drift_marker() {
6138 // Drift marker (findings.md round-2 #3): `Php::Name2` maps
6139 // to the hidden grammar rule `_name`. At the pinned
6140 // tree-sitter-php version it is never emitted as a concrete
6141 // node — the visible `Name` (= 1) carries every name.
6142 // The former `php_bool_terminal_kinds!()` listed `Name2`
6143 // defensively (lesson 34); if a future grammar bump promotes `_name`
6144 // to a visible rule, this assertion fails loudly.
6145 let src = "<?php\nfunction f($x) { if ($x) { foo($x); } }\n";
6146 let parser = PhpParser::new(
6147 src.as_bytes().to_vec(),
6148 &std::path::PathBuf::from("foo.php"),
6149 None,
6150 );
6151 assert!(!ast_has_kind_id(&parser, Php::Name2 as u16));
6152 }
6153
6154 #[cfg(feature = "php")]
6155 #[test]
6156 fn php_scoped_property_access_condition_counts() {
6157 // Regression for findings.md round-2 #1 (PHP):
6158 // `if (Config::$enabled) {}` parses with
6159 // `scoped_property_access_expression` as the condition
6160 // node (kind_id 333 at the pinned grammar version — the
6161 // `*2` alias). Pre-fix, neither `ScopedPropertyAccessExpression`
6162 // nor its alias was in the former `php_bool_terminal_kinds!()`.
6163 // The walker reached the access node, found it non-terminal,
6164 // and broke. Mirrors C#'s `MemberAccessExpression` rule
6165 // (lesson 19, #372).
6166 check_metrics::<PhpParser>(
6167 "<?php\n\
6168 class Config { public static $enabled = true; }\n\
6169 function f() { if (Config::$enabled) { } }\n",
6170 "foo.php",
6171 |metric| {
6172 assert_eq!(metric.abc.conditions_sum(), 1);
6173 insta::assert_json_snapshot!(metric.abc);
6174 },
6175 );
6176 }
6177
6178 #[cfg(feature = "php")]
6179 #[test]
6180 fn php_named_argument_unary_conditional_counts() {
6181 // Regression for the code-review finding: PHP 8 named-argument
6182 // syntax `m(name: !$a)` parses as `argument(name, ':',
6183 // unary_op_expression)`. Pre-fix, the count walker took
6184 // `argument.child(0)` (the name) and missed the value at the
6185 // last child. Now it picks the last named child as the value.
6186 check_metrics::<PhpParser>(
6187 "<?php\nfunction f($a) { m(name: !$a); }\n",
6188 "foo.php",
6189 |metric| {
6190 // 1 call (branch) + 1 unary-conditional named argument.
6191 assert_eq!(metric.abc.branches_sum(), 1);
6192 assert_eq!(metric.abc.conditions_sum(), 1);
6193 insta::assert_json_snapshot!(metric.abc);
6194 },
6195 );
6196 }
6197
6198 #[cfg(feature = "php")]
6199 #[test]
6200 fn php_low_precedence_keyword_logical_ops_trigger_walker() {
6201 // Regression: pre-fix, `$a or $b` reported 0 conditions
6202 // because the dispatcher only handled `AMPAMP|PIPEPIPE`,
6203 // skipping the PHP-specific `and` / `or` keyword forms even
6204 // though they parse under the same `binary_expression` shape.
6205 // (`xor` is no chain: it is a value since #1536.)
6206 check_metrics::<PhpParser>(
6207 "<?php\n\
6208 function f($a, $b) {\n\
6209 \x20 return $a or $b;\n\
6210 }\n",
6211 "foo.php",
6212 |metric| {
6213 assert_eq!(metric.abc.conditions_sum(), 2);
6214 insta::assert_json_snapshot!(metric.abc);
6215 },
6216 );
6217 }
6218
6219 #[cfg(feature = "php")]
6220 #[test]
6221 fn php_if_multiple_conditions() {
6222 check_metrics::<PhpParser>(
6223 "<?php\n\
6224 function f($a, $b, $c, $d) {\n\
6225 \x20 if ($a || $b || $c || $d) {} // +4c\n\
6226 \x20 if ($a && $b && $c) {} // +3c\n\
6227 \x20 if (!$a && !$b) {} // +2c\n\
6228 }\n",
6229 "foo.php",
6230 |metric| {
6231 assert_eq!(metric.abc.conditions_sum(), 9);
6232 insta::assert_json_snapshot!(metric.abc);
6233 },
6234 );
6235 }
6236
6237 #[cfg(feature = "php")]
6238 #[test]
6239 fn php_while_and_do_while_conditions() {
6240 check_metrics::<PhpParser>(
6241 "<?php\n\
6242 function f($a, $b) {\n\
6243 \x20 while ($a || $b) {} // +2c\n\
6244 \x20 do {} while ($a && !$b); // +2c\n\
6245 }\n",
6246 "foo.php",
6247 |metric| {
6248 assert_eq!(metric.abc.conditions_sum(), 4);
6249 insta::assert_json_snapshot!(metric.abc);
6250 },
6251 );
6252 }
6253
6254 #[cfg(feature = "php")]
6255 #[test]
6256 fn php_short_circuit_with_boolean_literal_operand() {
6257 check_metrics::<PhpParser>(
6258 "<?php\nfunction f($a) { return $a && true; }\n",
6259 "foo.php",
6260 |metric| {
6261 assert_eq!(metric.abc.conditions_sum(), 2);
6262 insta::assert_json_snapshot!(metric.abc);
6263 },
6264 );
6265 }
6266
6267 // Issue #1102, PHP half. See
6268 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
6269 // rule. PHP's ABC dispatcher has no `?`-token arm — the grammar
6270 // does emit the token, but the `conditional_expression` node is
6271 // what carries the tally's +1 — so the arm keeps that increment and
6272 // adds the operand slots.
6273 #[cfg(feature = "php")]
6274 #[test]
6275 fn php_ternary_operand_slots_count_as_unary_conditions() {
6276 // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
6277 check_metrics::<PhpParser>(
6278 "<?php\nfunction f() { $x = $a ? !$b : !$c; }\n",
6279 "foo.php",
6280 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
6281 );
6282 // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
6283 // the fix.
6284 check_metrics::<PhpParser>(
6285 "<?php\nfunction f() { $x = ($a > 0) ? $b : -$b; }\n",
6286 "foo.php",
6287 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
6288 );
6289 // Nested (PHP 8 requires the inner ternary parenthesised): two
6290 // ternary nodes plus the two bare-variable conditions = 4.
6291 check_metrics::<PhpParser>(
6292 "<?php\nfunction f() { $x = $a ? ($b ? $c : $d) : $e; }\n",
6293 "foo.php",
6294 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
6295 );
6296 // A negated condition is the only input reaching the walker's
6297 // `else` fallback — see the C++ sibling for why. ternary (1) +
6298 // `!$a` (1) = 2.
6299 check_metrics::<PhpParser>(
6300 "<?php\nfunction f() { $x = !$a ? $b : $c; }\n",
6301 "foo.php",
6302 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
6303 );
6304 }
6305
6306 // PHP's short ternary `$a ?: $b` elides the consequence, which the
6307 // grammar names `body` (not `consequence`) and marks optional. The
6308 // alternative lands at child(3), so addressing the slot by field
6309 // name rather than a fixed child(4) is what keeps `!$b` counted.
6310 #[cfg(feature = "php")]
6311 #[test]
6312 fn php_elided_ternary_body_still_walks_the_alternative() {
6313 // ternary (1) + condition `$a` (1) + `!$b` (1) = 3.
6314 check_metrics::<PhpParser>(
6315 "<?php\nfunction f() { $x = $a ?: !$b; }\n",
6316 "foo.php",
6317 |metric| assert_eq!(metric.abc.conditions_sum(), 3),
6318 );
6319 }
6320
6321 // Issue #1276, PHP half. The `for` header's condition slot was the
6322 // one condition slot `PhpCode::compute` never dispatched, so a
6323 // bare / negated / parenthesised loop condition scored zero while
6324 // the identical predicate in an `if` header scored one. Each
6325 // fixture below is a shape only the new arm can classify — a
6326 // comparison-shaped condition proves nothing here, because the `<`
6327 // token arm counts it either way (grammar-dispatch §11).
6328 #[cfg(feature = "php")]
6329 #[test]
6330 fn php_for_condition_slot_counts_unary_conditions() {
6331 // Bare variable: the whole condition, no operator token.
6332 check_metrics::<PhpParser>(
6333 "<?php\nfunction f($a) { for (; $a; ) {} }\n",
6334 "foo.php",
6335 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6336 );
6337 // Negation: the slot peels the `!` (`php_wrapper_operand`) and
6338 // pays for the operand beneath it.
6339 check_metrics::<PhpParser>(
6340 "<?php\nfunction f($a) { for (; !$a; ) {} }\n",
6341 "foo.php",
6342 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6343 );
6344 // Parentheses: counted because the slot peels them
6345 // (`php_wrapper_operand`), the path #1276 found dead.
6346 check_metrics::<PhpParser>(
6347 "<?php\nfunction f($a) { for (; ($a); ) {} }\n",
6348 "foo.php",
6349 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
6350 );
6351 // No-double-count pin: the `<` token arm already counted this
6352 // shape before the fix, and the walker must not add a second.
6353 // Two assignments (`$i = 0`, `$i++`) confirm the header parsed
6354 // as the three-clause form rather than degenerating.
6355 check_metrics::<PhpParser>(
6356 "<?php\nfunction f($n) { for ($i = 0; $i < $n; $i++) {} }\n",
6357 "foo.php",
6358 |metric| {
6359 assert_eq!(metric.abc.conditions_sum(), 1);
6360 assert_eq!(metric.abc.assignments_sum(), 2);
6361 },
6362 );
6363 // Empty condition: no `condition` field, no decision, zero.
6364 check_metrics::<PhpParser>(
6365 "<?php\nfunction f() { for (;;) { break; } }\n",
6366 "foo.php",
6367 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
6368 );
6369 }
6370
6371 // --- Kotlin ABC tests -------------------------------------------------
6372
6373 #[cfg(feature = "kotlin")]
6374 #[test]
6375 fn kotlin_empty_class() {
6376 check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
6377 assert_eq!(metric.abc.assignments_sum(), 0);
6378 assert_eq!(metric.abc.branches_sum(), 0);
6379 assert_eq!(metric.abc.conditions_sum(), 0);
6380 insta::assert_json_snapshot!(metric.abc);
6381 });
6382 }
6383
6384 #[cfg(feature = "kotlin")]
6385 #[test]
6386 fn kotlin_val_declarations_are_not_assignments() {
6387 // `val` introduces an immutable binding — the `=` initialising it
6388 // is not an assignment in the ABC sense.
6389 check_metrics::<KotlinParser>(
6390 "class C {
6391 val a: Int = 1
6392 val b: Int = 2
6393 val c: Int = 3
6394 }",
6395 "foo.kt",
6396 |metric| {
6397 assert_eq!(metric.abc.assignments_sum(), 0);
6398 assert_eq!(metric.abc.branches_sum(), 0);
6399 insta::assert_json_snapshot!(metric.abc);
6400 },
6401 );
6402 }
6403
6404 #[cfg(feature = "kotlin")]
6405 #[test]
6406 fn kotlin_var_declarations_count_assignment() {
6407 // `var` initialisers count as assignments (mutable binding).
6408 check_metrics::<KotlinParser>(
6409 "class C {
6410 var a: Int = 1
6411 var b: Int = 2
6412 }",
6413 "foo.kt",
6414 |metric| {
6415 assert_eq!(metric.abc.assignments_sum(), 2);
6416 insta::assert_json_snapshot!(metric.abc);
6417 },
6418 );
6419 }
6420
6421 #[cfg(feature = "kotlin")]
6422 #[test]
6423 fn kotlin_val_then_assignments_count() {
6424 // Regression for #455: a `val` initialiser must not suppress the
6425 // standalone `=` assignments that follow it. tree-sitter-kotlin
6426 // emits no `SEMI` token (even for explicit semicolons), so the
6427 // pre-#455 `SEMI`-cleared declaration stack never cleared and the
6428 // immutable-`val` sentinel leaked, reporting A=0 here.
6429 check_metrics::<KotlinParser>(
6430 "fun f() {
6431 val cfg = 0
6432 a = 1
6433 b = 2
6434 }",
6435 "foo.kt",
6436 |metric| {
6437 // val initialiser suppressed; `a = 1` and `b = 2` count.
6438 assert_eq!(metric.abc.assignments_sum(), 2);
6439 insta::assert_json_snapshot!(metric.abc);
6440 },
6441 );
6442 }
6443
6444 #[cfg(feature = "kotlin")]
6445 #[test]
6446 fn kotlin_var_then_assignments_count() {
6447 // Companion to the #455 regression: a `var` declaration leaves a
6448 // mutable-binding sentinel that *permits* the `=` — this path
6449 // accidentally masked the leak (its `Var` sentinel never suppressed
6450 // anything), so it must keep counting both the initialiser and the
6451 // following standalone assignments.
6452 check_metrics::<KotlinParser>(
6453 "fun f() {
6454 var cfg = 0
6455 a = 1
6456 b = 2
6457 }",
6458 "foo.kt",
6459 |metric| {
6460 // var initialiser (+1) plus `a = 1` and `b = 2` (+2).
6461 assert_eq!(metric.abc.assignments_sum(), 3);
6462 insta::assert_json_snapshot!(metric.abc);
6463 },
6464 );
6465 }
6466
6467 #[cfg(feature = "kotlin")]
6468 #[test]
6469 fn kotlin_augmented_assignments_count() {
6470 // Augmented operators (+=, -=, etc.) and ++/-- always count.
6471 check_metrics::<KotlinParser>(
6472 "fun m() {
6473 var x = 0
6474 x += 1
6475 x -= 2
6476 x *= 3
6477 x++
6478 --x
6479 }",
6480 "foo.kt",
6481 |metric| {
6482 // var declaration (var x = 0): +1
6483 // x += 1, x -= 2, x *= 3, x++, --x: +5
6484 assert_eq!(metric.abc.assignments_sum(), 6);
6485 insta::assert_json_snapshot!(metric.abc);
6486 },
6487 );
6488 }
6489
6490 #[cfg(feature = "kotlin")]
6491 #[test]
6492 fn kotlin_branches_call_expression() {
6493 check_metrics::<KotlinParser>(
6494 "fun m() {
6495 println(\"a\")
6496 println(\"b\")
6497 println(\"c\")
6498 }",
6499 "foo.kt",
6500 |metric| {
6501 assert_eq!(metric.abc.branches_sum(), 3);
6502 insta::assert_json_snapshot!(metric.abc);
6503 },
6504 );
6505 }
6506
6507 #[cfg(feature = "kotlin")]
6508 #[test]
6509 fn kotlin_object_construction_branch() {
6510 // Kotlin's object construction is just `Foo()` — a `CallExpression`.
6511 check_metrics::<KotlinParser>(
6512 "class P(val x: Int)
6513 fun m(): P = P(1)",
6514 "foo.kt",
6515 |metric| {
6516 assert_eq!(metric.abc.branches_sum(), 1);
6517 insta::assert_json_snapshot!(metric.abc);
6518 },
6519 );
6520 }
6521
6522 #[cfg(feature = "kotlin")]
6523 #[test]
6524 fn kotlin_comparisons_count_conditions() {
6525 check_metrics::<KotlinParser>(
6526 "fun m(a: Int, b: Int): Boolean {
6527 val r1 = a < b
6528 val r2 = a > b
6529 val r3 = a <= b
6530 val r4 = a >= b
6531 val r5 = a == b
6532 val r6 = a != b
6533 return r1 || r2 || r3 || r4 || r5 || r6
6534 }",
6535 "foo.kt",
6536 |metric| {
6537 // Six comparison operators in the `val` initialisers
6538 // (<, >, <=, >=, ==, !=) → 6, plus the six bare-identifier
6539 // operands of the `r1 || … || r6` return chain, each a
6540 // Fitzpatrick Rule 9 unary condition (issue #557) → 6.
6541 // Total 12. Before the Kotlin walker was wired the chain
6542 // operands were silently dropped and this read 6.
6543 assert_eq!(metric.abc.conditions_sum(), 12);
6544 insta::assert_json_snapshot!(metric.abc);
6545 },
6546 );
6547 }
6548
6549 #[cfg(feature = "kotlin")]
6550 #[test]
6551 fn kotlin_identity_equality_conditions() {
6552 // `===` / `!==` are referential equality in Kotlin; they count too.
6553 check_metrics::<KotlinParser>(
6554 "fun m(a: Any, b: Any): Boolean {
6555 return a === b || a !== b
6556 }",
6557 "foo.kt",
6558 |metric| {
6559 assert_eq!(metric.abc.conditions_sum(), 2);
6560 insta::assert_json_snapshot!(metric.abc);
6561 },
6562 );
6563 }
6564
6565 #[cfg(feature = "kotlin")]
6566 #[test]
6567 fn kotlin_else_branch_counts() {
6568 check_metrics::<KotlinParser>(
6569 "fun m(x: Int): Int {
6570 return if (x > 0) 1 else -1
6571 }",
6572 "foo.kt",
6573 |metric| {
6574 // condition: > (1) + else (1) = 2
6575 assert_eq!(metric.abc.conditions_sum(), 2);
6576 insta::assert_json_snapshot!(metric.abc);
6577 },
6578 );
6579 }
6580
6581 #[cfg(feature = "kotlin")]
6582 #[test]
6583 fn kotlin_when_entries_count() {
6584 check_metrics::<KotlinParser>(
6585 "fun m(x: Int): Int {
6586 return when (x) {
6587 1 -> 10
6588 2 -> 20
6589 else -> 0
6590 }
6591 }",
6592 "foo.kt",
6593 |metric| {
6594 // Non-`else` WhenEntry arms count; the `else ->` fallback
6595 // arm does not (issue #456). Two case arms + zero for the
6596 // `else` arm = 2.
6597 assert_eq!(metric.abc.conditions_sum(), 2);
6598 insta::assert_json_snapshot!(metric.abc);
6599 },
6600 );
6601 }
6602
6603 // Pins the `else ->` exclusion directly: a `when` whose only fallback
6604 // is `else ->` must not count that arm. Revert-verified — gating the
6605 // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this
6606 // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate.
6607 #[cfg(feature = "kotlin")]
6608 #[test]
6609 fn kotlin_when_else_not_a_condition() {
6610 check_metrics::<KotlinParser>(
6611 "fun m(x: Int): Int {
6612 return when (x) { 1 -> 10; 2 -> 20; else -> 0 }
6613 }",
6614 "foo.kt",
6615 |metric| {
6616 // case `1 ->` (+1) + case `2 ->` (+1) + `else ->` (+0) = 2.
6617 assert_eq!(metric.abc.conditions_sum(), 2);
6618 },
6619 );
6620 }
6621
6622 // The Kotlin binding of the two assertions every #1421 fixture needs,
6623 // coupled on purpose. A per-member score table is worth only as much
6624 // as the anchor proving the fixture still spells what it claims to
6625 // (`.claude/rules/testing.md`, "Perturb the fixture as well as the
6626 // production line"), and the `when_subject` count is the anchor no
6627 // score can stand in for: a `when` that grew a subject scores its
6628 // entries the pre-#1421 way with every other row still satisfied.
6629 // Each fixture is one class, so the members are `spaces[0]`'s.
6630 #[cfg(feature = "kotlin")]
6631 #[track_caller]
6632 fn assert_kotlin_class_members(
6633 src: &str,
6634 kinds: &[(u16, usize, &str)],
6635 expected: &[(&str, u64, u64)],
6636 ) {
6637 assert_fixture_spells::<KotlinParser>(src, "foo.kt", kinds);
6638 check_func_space::<KotlinParser, _>(src, "foo.kt", |space| {
6639 assert_members_score(&space.spaces[0], expected);
6640 });
6641 }
6642
6643 // #1421's headline. A subject-less `when` arm's condition is an
6644 // ordinary boolean expression, so its comparison operator is already
6645 // an ABC condition through the token arms; the `WhenEntry` arm added
6646 // a second one on top, and `two` reported 4 against a decision count
6647 // of 2.
6648 //
6649 // `bare` is the control that isolates the defect rather than merely
6650 // observing it: same arm count, same entry shape, no comparison
6651 // operator anywhere — so it scored the right 2 before the fix and
6652 // after it. A fix that changed both members alike would be attacking
6653 // the entry count, which was never wrong.
6654 //
6655 // `ge` and `eq` exist because the issue named `<` and `>` only. All
6656 // six comparison spellings share the defect: `>=` and `==` reach
6657 // `stats.conditions` through the `LTEQ | GTEQ | EQEQ | …` arm rather
6658 // than the parent-gated `LT | GT` arm, and both scored 2 against a
6659 // decision count of 1.
6660 //
6661 // `iff` is the member that legitimately sits *above* its decision
6662 // count, and it is here so the table cannot be read as "§8 parity
6663 // everywhere". ABC counts the `else` (Fitzpatrick Rule 5) and
6664 // cyclomatic does not, so 2 conditions against 1 decision is correct
6665 // and unchanged by this fix.
6666 //
6667 // The `when_subject` anchor is the load-bearing one: the whole test
6668 // turns on these `when`s having no subject, and nothing in the
6669 // measured numbers would notice a fixture that grew one — a
6670 // subject-ful `when (x) { … }` scores its entries the old way and
6671 // `two` would read 4 again with every other row still satisfied.
6672 #[cfg(feature = "kotlin")]
6673 #[test]
6674 fn kotlin_subjectless_when_arm_counts_its_condition_once() {
6675 let src = "class K {
6676 fun f(): Boolean = true
6677 fun two(x: Int): Int = when { x > 5 -> 1; x < 0 -> 2; else -> 0 }
6678 fun bare(x: Boolean, y: Boolean): Int = when { x -> 1; y -> 2; else -> 0 }
6679 fun ge(x: Int): Int = when { x >= 5 -> 1; else -> 0 }
6680 fun eq(x: Int): Int = when { x == 5 -> 1; else -> 0 }
6681 fun andd(a: Int, b: Int): Int = when { a > 1 && b < 2 -> 1; else -> 0 }
6682 fun call(): Int = when { f() -> 1; else -> 0 }
6683 fun iff(x: Int): Int = if (x > 5) 1 else 0
6684 }";
6685 assert_kotlin_class_members(
6686 src,
6687 &[
6688 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6689 (Kotlin::WhenEntry as u16, 14, "`when` entries"),
6690 (
6691 Kotlin::BinaryExpression as u16,
6692 8,
6693 "comparison / chain expressions",
6694 ),
6695 (Kotlin::AMPAMP as u16, 1, "`andd`'s `&&`"),
6696 ],
6697 &[
6698 // (member, abc.conditions, cyclomatic)
6699 ("f", 0, 1),
6700 // Was 4: two entries, two comparisons, counted twice.
6701 ("two", 2, 3),
6702 // The no-comparison control — 2 before and after.
6703 ("bare", 2, 3),
6704 // Was 2 each: the `GTEQ` / `EQEQ` spellings.
6705 ("ge", 1, 2),
6706 ("eq", 1, 2),
6707 // Was 3. A compound condition keeps both of its
6708 // comparisons — suppressing the operators instead
6709 // would have collapsed this one to 1.
6710 ("andd", 2, 3),
6711 // The condition slot scores a call directly, so a
6712 // spelling with no operator in it still reads 1.
6713 ("call", 1, 2),
6714 // Above its decision count on purpose: ABC counts
6715 // the `else`, cyclomatic does not.
6716 ("iff", 2, 2),
6717 ],
6718 );
6719 }
6720
6721 // #1459, the regression #1421 left behind. Turning the `when`
6722 // entry's blanket count into a condition slot moved the payment from
6723 // the entry to the condition — so every shape the slot cannot
6724 // classify went from 1 to 0, silently. Two shapes did:
6725 //
6726 // - `a!!`. `unary_expression` is one kind for *both* Kotlin unary
6727 // spellings, and the slot routed it to the peel, so this reads as
6728 // handled at every call site. The peel then looked for the operand
6729 // at child(1) — right for the prefix `!x`, wrong for a postfix
6730 // `x!!`, whose child(1) is the `!!` token — and stopped.
6731 // - `a as Boolean`. `as_expression` was not a wrapper the peel knew
6732 // at all, so the slot fell off the end of its `else if`.
6733 //
6734 // The controls are `bare` / `cmp` / `call`, one per surviving path
6735 // (terminal, comparison token, invocation): a fix that moved the
6736 // entry count back would take those to 2 and fail here, which is
6737 // what distinguishes repairing the peel from reverting #1421.
6738 //
6739 // `safe` is the §5 row and the one measurement decided rather than
6740 // taste. `as?` is already a condition *token*
6741 // (`src/metrics/abc/kotlin.rs`), so a peel that treated `as` and
6742 // `as?` alike would score it 2 — the double count that arm's own
6743 // fix was avoiding. `kotlin_wrapper_operand` excludes `as?` for
6744 // that reason, and this member is what fails if the exclusion goes:
6745 // `(a as? Boolean)!!` is valid Kotlin whose peel reaches the cast
6746 // through two other wrappers, so it needs no invalid fixture to
6747 // discriminate.
6748 //
6749 // `iff` carries the same `a!!` through the `if` slot. That half is
6750 // not #1421 fallout — `if (a!!)` has read 0 since the slot landed in
6751 // #773 — but it is the same peel and the same edit, and a fix
6752 // narrowed to `when` entries would leave it at 0.
6753 //
6754 // Every member is valid Kotlin: `a!!` on a `Boolean?`, `as` on an
6755 // `Any`, and `(a as? Boolean)!!` back to a `Boolean`. The kind
6756 // anchors are what stop a later edit from trimming a spelling out
6757 // and turning its member into a silent copy of `bare`.
6758 //
6759 // Since #1533 the slot pays for any operand it cannot see a
6760 // comparison or chain inside, so `bang`, `paren`, `cast` and `chain`
6761 // score 1 with or without the peel. `bangCmp` and `castCmp` are what
6762 // still need it: unpeeled, the wrapper pays as a value on top of the
6763 // `>` it hides, and they read 2.
6764 #[cfg(feature = "kotlin")]
6765 #[test]
6766 fn kotlin_condition_slot_peels_null_assertions_and_casts() {
6767 let src = "class K {
6768 fun g(x: Boolean): Boolean = true
6769 fun bare(a: Boolean): Int = when { a -> 1; else -> 0 }
6770 fun cmp(a: Int): Int = when { a > 5 -> 1; else -> 0 }
6771 fun call(a: Boolean): Int = when { g(a) -> 1; else -> 0 }
6772 fun bang(a: Boolean?): Int = when { a!! -> 1; else -> 0 }
6773 fun paren(a: Boolean?): Int = when { (a!!) -> 1; else -> 0 }
6774 fun cast(a: Any): Int = when { a as Boolean -> 1; else -> 0 }
6775 fun chain(a: Any?): Int = when { a!! as Boolean -> 1; else -> 0 }
6776 fun safe(a: Any): Int = when { (a as? Boolean)!! -> 1; else -> 0 }
6777 fun cmt(a: Boolean): Int = when { ! /*c*/ a -> 1; else -> 0 }
6778 fun iff(a: Boolean?): Int { if (a!!) { return 1 }; return 0 }
6779 fun bangCmp(a: Int): Int = when { (a > 5)!! -> 1; else -> 0 }
6780 fun castCmp(a: Int): Int = when { (a > 5) as Boolean -> 1; else -> 0 }
6781 }";
6782 assert_kotlin_class_members(
6783 src,
6784 &[
6785 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6786 (
6787 Kotlin::BANGBANG as u16,
6788 6,
6789 "the `!!` null assertions of `bang` / `paren` / `chain` / `safe` / `iff` / \
6790 `bangCmp`",
6791 ),
6792 (
6793 Kotlin::AsExpression as u16,
6794 4,
6795 "the casts of `cast` / `chain` / `safe` / `castCmp`",
6796 ),
6797 (Kotlin::AsQMARK as u16, 1, "`safe`'s safe cast"),
6798 (
6799 Kotlin::BlockComment as u16,
6800 1,
6801 "`cmt`'s interposed comment, spaces and all",
6802 ),
6803 (
6804 Kotlin::ParenthesizedExpression as u16,
6805 4,
6806 "the parenthesised operands of `paren` / `safe` / `bangCmp` / `castCmp`",
6807 ),
6808 ],
6809 &[
6810 // (member, abc.conditions, cyclomatic)
6811 ("g", 0, 1),
6812 // The three controls, unchanged by this fix.
6813 ("bare", 1, 2),
6814 ("cmp", 1, 2),
6815 ("call", 1, 2),
6816 // Each was 0: the peel stopped on the wrapper.
6817 ("bang", 1, 2),
6818 ("paren", 1, 2),
6819 ("cast", 1, 2),
6820 ("chain", 1, 2),
6821 // 1, not 2 — the `as?` token owns this one, and the
6822 // peel declines to charge for it a second time.
6823 ("safe", 1, 2),
6824 // The peel reads its operand through the `operator` /
6825 // `argument` fields, so an `extra` between the two no
6826 // longer displaces it. The spacing is load-bearing and
6827 // must not be tidied away: `!/*c*/a` unspaced parses
6828 // with the comment somewhere a `child(1)` read still
6829 // skips, so only the spaced spelling can tell the field
6830 // read from the positional one. Measured both ways.
6831 ("cmt", 1, 2),
6832 // The `if` slot, which has read 0 since #773.
6833 ("iff", 1, 2),
6834 // The `>` pays; the peel keeps the wrapper from paying too.
6835 ("bangCmp", 1, 2),
6836 ("castCmp", 1, 2),
6837 ],
6838 );
6839 }
6840
6841 // The other half of #1421: a subject-ful entry must not move. Its
6842 // condition is a *pattern* matched against the subject, not an
6843 // independent boolean expression, so the implicit `subject ==
6844 // pattern` is the decision and the entry itself pays for it —
6845 // `range_test`, `type_test` and a bare constant carry no token the
6846 // comparison arms would count.
6847 //
6848 // `cmp` is the interesting row and the reason `nested` sits beside
6849 // it. `when (x) { y > 5 -> … }` is legal when `x` is `Boolean`, and
6850 // it reads 2 conditions against 1 decision — the entry's implicit
6851 // equality plus the `>` inside the operand. That is not the #1421
6852 // double-count reappearing: `nested` spells the same thing as an
6853 // `if` (`x == (y > 5)`) and ABC has always scored it 2 against the
6854 // same 1 decision. A comparison nested inside a comparison is two
6855 // comparisons; only one of them is a branch.
6856 #[cfg(feature = "kotlin")]
6857 #[test]
6858 fn kotlin_subjectful_when_arms_keep_the_per_entry_count() {
6859 let src = "class K {
6860 fun inn(a: Int): Int = when (a) { in 1..2 -> 1; else -> 0 }
6861 fun notin(a: Int): Int = when (a) { !in 1..2 -> 1; else -> 0 }
6862 fun iss(a: Any): Int = when (a) { is String -> 1; else -> 0 }
6863 fun konst(a: Int): Int = when (a) { 1 -> 10; 2 -> 20; else -> 0 }
6864 fun cmp(x: Boolean, y: Int): Int = when (x) { y > 5 -> 1; else -> 0 }
6865 fun nested(x: Boolean, y: Int): Int { if (x == (y > 5)) { return 1 }; return 0 }
6866 }";
6867 assert_kotlin_class_members(
6868 src,
6869 &[
6870 (Kotlin::WhenSubject as u16, 5, "`when` subjects"),
6871 (Kotlin::WhenEntry as u16, 11, "`when` entries"),
6872 (Kotlin::RangeTest as u16, 2, "`in` / `!in` patterns"),
6873 (Kotlin::TypeTest as u16, 1, "the `is` pattern"),
6874 (
6875 Kotlin::BinaryExpression as u16,
6876 3,
6877 "`cmp`'s and `nested`'s comparisons",
6878 ),
6879 ],
6880 &[
6881 ("inn", 1, 2),
6882 ("notin", 1, 2),
6883 ("iss", 1, 2),
6884 ("konst", 2, 3),
6885 // Above its decision count, like `nested` below it
6886 // and for the same reason.
6887 ("cmp", 2, 2),
6888 ("nested", 2, 2),
6889 ],
6890 );
6891 }
6892
6893 // `kotlin_enclosing_when_has_subject` stops scanning at the `{`, so
6894 // that a subject-less `when` does not walk all N arms once per arm.
6895 // The bound has to clear the header first, and the header can carry
6896 // an `extra`: tree-sitter hangs `when /*c*/ (x)`'s comment off the
6897 // `when_expression` between the keyword and the subject, so a bound
6898 // one child tighter — `child(1)`, or a stop at the first *named*
6899 // child — reads `cmtSubject` as subject-less. It would then score
6900 // its `1` arm through the slot, where a `number_literal` is not a
6901 // Kotlin terminal, and report 0 conditions instead of 1. That is
6902 // what discriminates here; `cmtNone` is the other direction, where
6903 // the brace is all the bound has to stop on.
6904 //
6905 // The `block_comment` census is the fixture anchor: delete either
6906 // comment and the members still read 1, because a plain `when (x)`
6907 // and a plain `when {` both do. The count fails by name instead.
6908 #[cfg(feature = "kotlin")]
6909 #[test]
6910 fn kotlin_when_subject_scan_clears_a_comment_before_the_brace() {
6911 let src = "class K {
6912 fun cmtSubject(x: Int): Int = when /*c*/ (x) { 1 -> 1; else -> 0 }
6913 fun cmtNone(a: Boolean): Int = when /*c*/ { a -> 1; else -> 0 }
6914 }";
6915 assert_kotlin_class_members(
6916 src,
6917 &[
6918 (Kotlin::BlockComment as u16, 2, "the two header comments"),
6919 (Kotlin::WhenSubject as u16, 1, "`cmtSubject`'s subject"),
6920 (Kotlin::WhenEntry as u16, 4, "`when` entries"),
6921 ],
6922 &[("cmtSubject", 1, 2), ("cmtNone", 1, 2)],
6923 );
6924 }
6925
6926 // A subject-less condition reaches the slot through the same
6927 // `kotlin_wrapper_operand` peel an `if` predicate does, which
6928 // is what separates this design from suppressing the entry's
6929 // operator. `notted` and `paren` unwrap to a bare terminal and score
6930 // through the slot; `notCmp` and `parenCmp` unwrap to a
6931 // `binary_expression`, score nothing from the slot, and take their
6932 // one condition from the `>` the token arm already owned. Both pairs
6933 // land on 1, which no blanket `+1` can produce for the second pair —
6934 // they read 2 before the fix.
6935 //
6936 // `paren` once reached the slot only through a `WhenEntry` seed in the
6937 // retired walker's boolean-context set, and dropped to 0 without it.
6938 // On the shared slot (#1533) it pins that a `when` entry peels a
6939 // parenthesis as `if ((a))` does; `notted` cannot stand in for it,
6940 // since a `!` is a wrapper of its own.
6941 #[cfg(feature = "kotlin")]
6942 #[test]
6943 fn kotlin_subjectless_when_condition_wrappers_count_once() {
6944 let src = "class K {
6945 fun notted(a: Boolean): Int = when { !a -> 1; else -> 0 }
6946 fun paren(a: Boolean): Int = when { (a) -> 1; else -> 0 }
6947 fun notCmp(a: Int): Int = when { !(a > 1) -> 1; else -> 0 }
6948 fun parenCmp(a: Int): Int = when { (a > 1) -> 1; else -> 0 }
6949 fun nest(x: Int, y: Int): Int =
6950 when { x > 1 -> when { y > 2 -> 1; else -> 2 }; else -> 0 }
6951 fun blockBody(x: Int): Int { when { x > 5 -> { return 1 } }; return 0 }
6952 }";
6953 assert_kotlin_class_members(
6954 src,
6955 &[
6956 // Paired, not bare: "no subjects" alone is satisfied by a
6957 // fixture that lost its `when`s altogether.
6958 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
6959 (Kotlin::WhenEntry as u16, 13, "`when` entries"),
6960 (Kotlin::UnaryExpression as u16, 2, "the two `!` operands"),
6961 (
6962 Kotlin::ParenthesizedExpression as u16,
6963 3,
6964 "the three parenthesised operands",
6965 ),
6966 (Kotlin::BinaryExpression as u16, 5, "comparisons"),
6967 ],
6968 &[
6969 ("notted", 1, 2),
6970 ("paren", 1, 2),
6971 // Both were 2: the entry's blanket count plus the `>`.
6972 ("notCmp", 1, 2),
6973 ("parenCmp", 1, 2),
6974 // Was 4 — one entry and one comparison per `when`,
6975 // each counted twice.
6976 ("nest", 2, 3),
6977 // The `when`-as-statement shape: a `block` body and no
6978 // `else` arm at all. Reading the condition from the
6979 // `condition` field rather than by position is what keeps
6980 // the block out of the slot; it was 2 before the fix.
6981 ("blockBody", 1, 2),
6982 ],
6983 );
6984 }
6985
6986 // The one place this design deliberately does not reach parity, put
6987 // under test so the choice cannot drift unremarked. A `when` entry
6988 // may list alternatives (`when { a, b -> … }`, an implicit `or`), and
6989 // the `condition` field is `multiple` — but cyclomatic scores the
6990 // whole entry as one decision, so `bareAlts` is at parity only
6991 // because the slot reads the *first* alternative and stops. Routing
6992 // every alternative through the slot would push it to 2.
6993 //
6994 // `alts` is the row that cannot be brought to parity from the ABC
6995 // side at all: the comparison-token arms count `>` and `<` wherever
6996 // they appear, so it reads 2 against 1 decision no matter what the
6997 // entry slot does. It was 3 before #1421 and the remaining gap is
6998 // cyclomatic's single count per multi-alternative entry, not a
6999 // double-count here.
7000 //
7001 // `subjAlts` is the subject-ful control, unchanged and at parity: its
7002 // alternatives are constants carrying no token to count.
7003 #[cfg(feature = "kotlin")]
7004 #[test]
7005 fn kotlin_subjectless_when_multi_alternative_entry() {
7006 let src = "class K {
7007 fun alts(x: Int, y: Int): Int = when { x > 5, y < 0 -> 1; else -> 0 }
7008 fun bareAlts(x: Boolean, y: Boolean): Int = when { x, y -> 1; else -> 0 }
7009 fun subjAlts(a: Int): Int = when (a) { 1, 2 -> 10; else -> 0 }
7010 }";
7011 assert_kotlin_class_members(
7012 src,
7013 &[
7014 (Kotlin::WhenSubject as u16, 1, "`subjAlts`'s subject"),
7015 (Kotlin::WhenEntry as u16, 6, "`when` entries"),
7016 (
7017 Kotlin::BinaryExpression as u16,
7018 2,
7019 "`alts`'s two comparisons",
7020 ),
7021 ],
7022 &[
7023 // Above its decision count: cyclomatic scores the entry
7024 // once, the token arms score both comparisons. Was 3.
7025 ("alts", 2, 2),
7026 ("bareAlts", 1, 2),
7027 ("subjAlts", 1, 2),
7028 ],
7029 );
7030 }
7031
7032 // `is_expression` and `in_expression` are the two relational forms
7033 // tree-sitter-kotlin-ng spells as their own production rather than as
7034 // a `binary_expression`, so no comparison-token arm sees them and
7035 // nothing counted them: `if (a is String)` scored 0 against a
7036 // decision count of 1, and the same test in a subject-less `when`
7037 // scored 1 only because the entry's blanket `+1` happened to cover
7038 // it. Removing that blanket without counting these two somewhere
7039 // would have regressed `whenIs` and `whenIn` to 0, and `isAnd` —
7040 // where the `is` test is one operand of a `&&` chain and so reaches
7041 // the walker rather than the slot — from 2 to 1.
7042 //
7043 // The `if` members are here because the fix was never `when`-
7044 // specific: they are the call sites that prove it reaches the `if` /
7045 // `while` predicate slot too.
7046 //
7047 // #1421 counted them through the former
7048 // `kotlin_bool_terminal_kinds!()` and
7049 // #1461 moved them to an unconditional arm, which leaves every
7050 // number below unchanged — the slot no longer counts the test and
7051 // the arm does. `kotlin_is_and_in_score_outside_a_boolean_slot` is
7052 // the half this fixture cannot see, every member of it being inside
7053 // a slot.
7054 #[cfg(feature = "kotlin")]
7055 #[test]
7056 fn kotlin_is_and_in_expressions_are_unary_conditions() {
7057 let src = "class K {
7058 fun ifIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
7059 fun ifIn(a: Int): Int { if (a in 1..2) { return 1 }; return 0 }
7060 fun whenIs(a: Any): Int = when { a is String -> 1; else -> 0 }
7061 fun whenIn(a: Int): Int = when { a in 1..2 -> 1; else -> 0 }
7062 fun isAnd(a: Any, b: Boolean): Int = when { a is String && b -> 1; else -> 0 }
7063 }";
7064 assert_kotlin_class_members(
7065 src,
7066 &[
7067 // Paired, not bare — see the wrappers test above.
7068 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
7069 (Kotlin::WhenEntry as u16, 6, "`when` entries"),
7070 (Kotlin::IsExpression as u16, 3, "`is` tests"),
7071 (Kotlin::InExpression as u16, 2, "`in` tests"),
7072 (Kotlin::AMPAMP as u16, 1, "`isAnd`'s `&&`"),
7073 ],
7074 &[
7075 // Both were 0 — the predicate slot saw a kind it did
7076 // not classify and the token arms saw no operator.
7077 ("ifIs", 1, 2),
7078 ("ifIn", 1, 2),
7079 ("whenIs", 1, 2),
7080 ("whenIn", 1, 2),
7081 ("isAnd", 2, 3),
7082 ],
7083 );
7084 }
7085
7086 // Kotlin spells boolean `and` / `or` / `xor` as infix *functions*, so
7087 // `a and b` parses as `infix_expression` — not `binary_expression`,
7088 // and not any token arm. A slot pays for it for the same reason it
7089 // pays for a `call_expression`: `a and b` is `a.and(b)`.
7090 //
7091 // This was a regression of #1421, not a pre-existing gap, and it is
7092 // the third shape the blanket per-entry count turned out to be
7093 // propping up — after `is` / `in` and bare parens, which that fix
7094 // caught. Found by the whole-branch review. Measured on both sides:
7095 // `when { a and b -> … }` scored 1 before and 0 after, while the
7096 // `if (a and b)` it is supposed to agree with still scored 1.
7097 //
7098 // The `if` members are the anchor rather than a second `when`: the
7099 // whole premise of #1421 is that a subject-less arm scores like an
7100 // `if` predicate, so a fixture where the two disagree is the
7101 // statement of the bug. `andAmp` holds the pair level against the
7102 // `&&` spelling, which never regressed and would otherwise be the
7103 // only form under test.
7104 #[cfg(feature = "kotlin")]
7105 #[test]
7106 fn kotlin_infix_boolean_functions_are_unary_conditions() {
7107 let src = "class K {
7108 fun whenAnd(a: Boolean, b: Boolean): Int = when { a and b -> 1; else -> 0 }
7109 fun whenOr(a: Boolean, b: Boolean): Int = when { a or b -> 1; else -> 0 }
7110 fun whenXor(a: Boolean, b: Boolean): Int = when { a xor b -> 1; else -> 0 }
7111 fun whenAmp(a: Boolean, b: Boolean): Int = when { a && b -> 1; else -> 0 }
7112 fun ifAnd(a: Boolean, b: Boolean): Int { if (a and b) { return 1 }; return 0 }
7113 }";
7114 assert_kotlin_class_members(
7115 src,
7116 &[
7117 (Kotlin::WhenSubject as u16, 0, "`when` subjects"),
7118 (Kotlin::InfixExpression as u16, 4, "the infix calls"),
7119 (Kotlin::AMPAMP as u16, 1, "`whenAmp`'s `&&`"),
7120 ],
7121 &[
7122 // 0 before this fix: the predicate slot saw a kind it
7123 // did not classify and no token arm matched.
7124 ("whenAnd", 1, 2),
7125 ("whenOr", 1, 2),
7126 ("whenXor", 1, 2),
7127 // Never regressed — `&&` is a `binary_expression` whose
7128 // operator the token arm owns. Holds the others level.
7129 ("whenAmp", 2, 3),
7130 // The agreement #1421 exists to establish: a
7131 // subject-less arm scores what the equivalent `if`
7132 // predicate scores.
7133 ("ifAnd", 1, 2),
7134 ],
7135 );
7136 }
7137
7138 #[cfg(feature = "kotlin")]
7139 #[test]
7140 fn kotlin_catch_block_counts() {
7141 check_metrics::<KotlinParser>(
7142 "fun m() {
7143 try {
7144 println(\"ok\")
7145 } catch (e: Exception) {
7146 println(\"err\")
7147 }
7148 }",
7149 "foo.kt",
7150 |metric| {
7151 // `try` (+1) and `catch` (+1) each contribute one condition,
7152 // matching Java / C# / C++ / Groovy (Fitzpatrick counts both
7153 // keywords). Before #696 Kotlin counted only the catch block.
7154 assert_eq!(metric.abc.conditions_sum(), 2);
7155 insta::assert_json_snapshot!(metric.abc);
7156 },
7157 );
7158 }
7159
7160 #[cfg(feature = "kotlin")]
7161 #[test]
7162 fn kotlin_elvis_and_safe_cast() {
7163 // `?:` (elvis) and `as?` (safe cast) are condition-like.
7164 check_metrics::<KotlinParser>(
7165 "fun m(s: String?): Int {
7166 val n = (s as? Int) ?: 0
7167 return n
7168 }",
7169 "foo.kt",
7170 |metric| {
7171 // as? (+1) + ?: (+1) = 2 conditions.
7172 assert_eq!(metric.abc.conditions_sum(), 2);
7173 insta::assert_json_snapshot!(metric.abc);
7174 },
7175 );
7176 }
7177
7178 #[cfg(feature = "kotlin")]
7179 #[test]
7180 fn kotlin_generic_brackets_not_conditions() {
7181 // `<` / `>` used as type-parameter brackets must not be counted.
7182 check_metrics::<KotlinParser>(
7183 "class Box<T>(val v: T)
7184 fun <T> wrap(x: T): Box<T> = Box(x)",
7185 "foo.kt",
7186 |metric| {
7187 // No comparisons — only generic brackets.
7188 assert_eq!(metric.abc.conditions_sum(), 0);
7189 insta::assert_json_snapshot!(metric.abc);
7190 },
7191 );
7192 }
7193
7194 // #1297: a qualified super call disambiguates its supertype with
7195 // the same two bare tokens a comparison uses — `super<A>.g()` is a
7196 // `super_expression`, which the previous denylist
7197 // (`type_arguments` / `type_parameters`) did not name, so it scored
7198 // two conditions. One `super<A>` against one genuine `a < b`
7199 // separates every mis-aim: 3 pre-fix, 1 once the arm allows
7200 // `BinaryExpression`, 0 if it allows the wrong parent or the
7201 // fixture stops parsing.
7202 #[cfg(feature = "kotlin")]
7203 #[test]
7204 fn kotlin_super_type_argument_is_not_a_condition() {
7205 check_metrics::<KotlinParser>(
7206 "class B : A() {
7207 override fun g(a: Int, b: Int): Int {
7208 if (a < b) { return super<A>.g(a, b) }
7209 return 0
7210 }
7211 }",
7212 "foo.kt",
7213 |metric| {
7214 assert_eq!(metric.abc.conditions_sum(), 1);
7215 // Non-vacuity guard: 1 is also what a body whose `if`
7216 // survived but whose super call did not would score, so
7217 // pin the call itself. Measured: dropping
7218 // `super<A>.g(a, b)` takes `branches_sum()` from 2 to 1 —
7219 // the remaining branch is the `A()` primary-constructor
7220 // delegation in the class header, which #1384 taught this
7221 // metric to count.
7222 assert_eq!(metric.abc.branches_sum(), 2);
7223 },
7224 );
7225 }
7226
7227 #[cfg(feature = "kotlin")]
7228 #[test]
7229 fn kotlin_class_with_methods_and_branches() {
7230 check_metrics::<KotlinParser>(
7231 "class C {
7232 var counter: Int = 0
7233 fun bump() {
7234 counter += 1
7235 println(counter)
7236 }
7237 }",
7238 "foo.kt",
7239 |metric| {
7240 // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
7241 // branches: println(counter) = 1
7242 assert_eq!(metric.abc.assignments_sum(), 2);
7243 assert_eq!(metric.abc.branches_sum(), 1);
7244 assert_eq!(metric.abc.conditions_sum(), 0);
7245 insta::assert_json_snapshot!(metric.abc);
7246 },
7247 );
7248 }
7249
7250 #[cfg(feature = "kotlin")]
7251 #[test]
7252 fn kotlin_object_singleton_abc() {
7253 check_metrics::<KotlinParser>(
7254 "object Util {
7255 fun work(x: Int): Int {
7256 var y = x
7257 y += 1
7258 if (y > 0) {
7259 return y
7260 }
7261 return -1
7262 }
7263 }",
7264 "foo.kt",
7265 |metric| {
7266 // assignments: var y = x (+1), y += 1 (+1) = 2
7267 // branches: 0 (return is not a call)
7268 // conditions: y > 0 (+1) = 1
7269 assert_eq!(metric.abc.assignments_sum(), 2);
7270 assert_eq!(metric.abc.branches_sum(), 0);
7271 assert_eq!(metric.abc.conditions_sum(), 1);
7272 insta::assert_json_snapshot!(metric.abc);
7273 },
7274 );
7275 }
7276
7277 #[cfg(feature = "kotlin")]
7278 #[test]
7279 fn kotlin_interface_abc() {
7280 // Pure-abstract interface with no bodies — all-zero.
7281 check_metrics::<KotlinParser>(
7282 "interface I {
7283 fun work(): Int
7284 fun describe(): String
7285 }",
7286 "foo.kt",
7287 |metric| {
7288 assert_eq!(metric.abc.assignments_sum(), 0);
7289 assert_eq!(metric.abc.branches_sum(), 0);
7290 assert_eq!(metric.abc.conditions_sum(), 0);
7291 insta::assert_json_snapshot!(metric.abc);
7292 },
7293 );
7294 }
7295
7296 #[cfg(feature = "kotlin")]
7297 #[test]
7298 fn kotlin_nested_class_abc() {
7299 check_metrics::<KotlinParser>(
7300 "class Outer {
7301 var o: Int = 0
7302 class Nested {
7303 var n: Int = 0
7304 fun bump() { n += 1 }
7305 }
7306 }",
7307 "foo.kt",
7308 |metric| {
7309 // Outer: var o = 0 (+1)
7310 // Nested: var n = 0 (+1), n += 1 (+1) = 2
7311 // total assignments = 3
7312 assert_eq!(metric.abc.assignments_sum(), 3);
7313 insta::assert_json_snapshot!(metric.abc);
7314 },
7315 );
7316 }
7317
7318 #[cfg(feature = "kotlin")]
7319 #[test]
7320 fn kotlin_data_class_abc() {
7321 // `data class` with primary-constructor `val`s — no assignments
7322 // (vals don't count) and no body conditions.
7323 check_metrics::<KotlinParser>(
7324 "data class Point(val x: Int, val y: Int)",
7325 "foo.kt",
7326 |metric| {
7327 assert_eq!(metric.abc.assignments_sum(), 0);
7328 assert_eq!(metric.abc.branches_sum(), 0);
7329 assert_eq!(metric.abc.conditions_sum(), 0);
7330 insta::assert_json_snapshot!(metric.abc);
7331 },
7332 );
7333 }
7334
7335 #[cfg(feature = "kotlin")]
7336 #[test]
7337 fn kotlin_primary_constructor_default_value_not_assignment() {
7338 // Regression: default values on primary-constructor `val`
7339 // parameters are initialisers, not assignments. Without
7340 // `ClassParameter` pushing a declaration sentinel, the `=` token
7341 // here would be counted unconditionally as a standalone
7342 // assignment.
7343 check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
7344 // `val a = 5` → suppressed (Const sentinel).
7345 assert_eq!(metric.abc.assignments_sum(), 0);
7346 insta::assert_json_snapshot!(metric.abc);
7347 });
7348 }
7349
7350 #[cfg(feature = "kotlin")]
7351 #[test]
7352 fn kotlin_unary_conditions_in_chain() {
7353 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
7354 // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
7355 // contribute one; the `&&` / `||` operators contribute nothing.
7356 // expected: 3 unary conditions, no comparisons, no `if`-keyword
7357 // condition in Kotlin (matches the Java byte-equivalent of 3).
7358 check_metrics::<KotlinParser>(
7359 "fun f(a: Boolean, b: Boolean, c: Boolean) {
7360 if (a && b || c) { println(\"x\") }
7361 }",
7362 "foo.kt",
7363 |metric| {
7364 assert_eq!(metric.abc.conditions_sum(), 3);
7365 },
7366 );
7367 }
7368
7369 #[cfg(feature = "kotlin")]
7370 #[test]
7371 fn kotlin_comparison_operands_add_nothing() {
7372 // Isolation check: comparison operands of a `&&` chain are nested
7373 // `binary_expression` nodes, not bare boolean leaves, so the
7374 // walker adds nothing — only the two `>` comparisons count.
7375 // expected: 2 (the two `>` tokens), walker contributes 0.
7376 check_metrics::<KotlinParser>(
7377 "fun g(x: Int, y: Int) {
7378 if (x > 0 && y > 0) { println(\"x\") }
7379 }",
7380 "foo.kt",
7381 |metric| {
7382 assert_eq!(metric.abc.conditions_sum(), 2);
7383 },
7384 );
7385 }
7386
7387 #[cfg(feature = "kotlin")]
7388 #[test]
7389 fn kotlin_negated_operand_is_unary_condition() {
7390 // A `!`-negated operand is still a unary condition: `a && !b`
7391 // unwraps the `unary_expression` to reach the inner identifier.
7392 // expected: 2 (`a` and the `!b` operand).
7393 check_metrics::<KotlinParser>(
7394 "fun f(a: Boolean, b: Boolean) {
7395 if (a && !b) { println(\"x\") }
7396 }",
7397 "foo.kt",
7398 |metric| {
7399 assert_eq!(metric.abc.conditions_sum(), 2);
7400 },
7401 );
7402 }
7403
7404 // The wrapper peel has two callers, and #1459's fixture members all
7405 // reach it through the *slot* (`kotlin_count_condition`). This is the
7406 // other one: the `&&` / `||` walker (`count_field_operands`),
7407 // which hands each operand to the same slot. The two wrappers #1459
7408 // taught it therefore have a
7409 // second, structurally independent path into the count, and a
7410 // fixture covering only the slot leaves it untested
7411 // (`.claude/rules/grammar-dispatch.md` §11).
7412 //
7413 // Each chain is one identifier plus one wrapped comparison, so 2 is
7414 // "the walker peeled the wrapper to the comparison, which pays for
7415 // itself" and 3 is "it paid the wrapper as a value as well". Since
7416 // the slot pays for any operand it cannot see a comparison inside
7417 // (#1533), a wrapped bare identifier scores 2 either way, so only a
7418 // wrapped comparison tells the peel from its absence. The bare
7419 // `a && b` control is `kotlin_unary_conditions_in_chain`'s shape at
7420 // 2, so a regression cannot be read as the chain itself changing.
7421 #[cfg(feature = "kotlin")]
7422 #[test]
7423 fn kotlin_chain_operands_peel_null_assertions_and_casts() {
7424 // Anchored per row, because the assertion alone cannot tell the
7425 // wrapper from its operand: `a && x > 1` scores the same 2 / 3 as
7426 // every row here, so trimming the `!!` or the cast out of a
7427 // fixture would leave this green with its subject gone
7428 // (`.claude/rules/testing.md`, "Perturb the fixture as well as
7429 // the production line").
7430 for (chain, spelling, anchors) in [
7431 (
7432 "a && (x > 1)!!",
7433 "null assertion",
7434 &[(Kotlin::BANGBANG as u16, 1usize, "the `!!`")][..],
7435 ),
7436 (
7437 "a && (x > 1) as Boolean",
7438 "cast",
7439 &[(Kotlin::AsExpression as u16, 1, "the `as` cast")][..],
7440 ),
7441 (
7442 "a && ((x > 1)!!)",
7443 "parenthesised null assertion",
7444 &[
7445 (Kotlin::BANGBANG as u16, 1, "the `!!`"),
7446 (Kotlin::ParenthesizedExpression as u16, 2, "the parens"),
7447 ][..],
7448 ),
7449 ] {
7450 let src = format!("fun f(a: Boolean, x: Int) {{ if ({chain}) {{ println(\"x\") }} }}");
7451 assert_fixture_spells::<KotlinParser>(&src, "foo.kt", anchors);
7452 check_func_space::<KotlinParser, _>(&src, "foo.kt", |space| {
7453 let f = child_space(&space, "f");
7454 assert_eq!(
7455 f.metrics.abc.conditions(),
7456 2,
7457 "`{chain}`: the identifier plus the comparison under the {spelling}"
7458 );
7459 assert_eq!(f.metrics.cyclomatic.cyclomatic(), 3, "`{chain}`: decisions");
7460 });
7461 }
7462 }
7463
7464 #[cfg(feature = "kotlin")]
7465 #[test]
7466 fn kotlin_bare_if_predicate_is_one_condition() {
7467 // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one
7468 // Fitzpatrick unary condition. Before the Phase-2B arm it counted
7469 // 0, so `if (flag) 1 else -1` scored 1 (only the `else`) instead of
7470 // 2, dropping below Kotlin's own cyclomatic decision count.
7471 // expected: predicate (1) + else (1) = 2.
7472 check_metrics::<KotlinParser>(
7473 "fun m(flag: Boolean): Int { return if (flag) 1 else -1 }",
7474 "foo.kt",
7475 |metric| {
7476 assert_eq!(metric.abc.conditions_sum(), 2);
7477 },
7478 );
7479 }
7480
7481 #[cfg(feature = "kotlin")]
7482 #[test]
7483 fn kotlin_bare_while_predicate_is_one_condition() {
7484 // Issue #773: the bare predicate of a `while` loop counts one
7485 // condition via the `condition` field. expected: 1.
7486 check_metrics::<KotlinParser>(
7487 "fun m(running: Boolean) { while (running) { println(\"x\") } }",
7488 "foo.kt",
7489 |metric| {
7490 assert_eq!(metric.abc.conditions_sum(), 1);
7491 },
7492 );
7493 }
7494
7495 #[cfg(feature = "kotlin")]
7496 #[test]
7497 fn kotlin_bare_do_while_predicate_is_one_condition() {
7498 // Issue #773: the bare predicate of a `do`/`while` loop counts one
7499 // condition. expected: 1.
7500 check_metrics::<KotlinParser>(
7501 "fun m(ok: Boolean) { do { println(\"x\") } while (ok) }",
7502 "foo.kt",
7503 |metric| {
7504 assert_eq!(metric.abc.conditions_sum(), 1);
7505 },
7506 );
7507 }
7508
7509 #[cfg(feature = "kotlin")]
7510 #[test]
7511 fn kotlin_comparison_predicate_not_double_counted() {
7512 // Double-count guard (#773): a comparison predicate (`if (a == b)`)
7513 // is a nested `binary_expression` already counted by the `==` token
7514 // arm, so the Phase-2B condition-slot arm must add nothing here.
7515 // expected: `==` (1) + else (1) = 2 — unchanged by the new arm.
7516 check_metrics::<KotlinParser>(
7517 "fun m(a: Int, b: Int): Int { return if (a == b) 1 else -1 }",
7518 "foo.kt",
7519 |metric| {
7520 assert_eq!(metric.abc.conditions_sum(), 2);
7521 },
7522 );
7523 }
7524
7525 #[cfg(feature = "kotlin")]
7526 #[test]
7527 fn kotlin_short_circuit_predicate_not_double_counted() {
7528 // Double-count guard (#773): an `&&`/`||` predicate is counted by
7529 // the Rule 9 chain walker (each operand once); the Phase-2B arm
7530 // must add nothing for it. expected: `x` (1) + `y` (1) + else (1)
7531 // = 3 — unchanged by the new arm.
7532 check_metrics::<KotlinParser>(
7533 "fun m(x: Boolean, y: Boolean): Int { return if (x && y) 1 else -1 }",
7534 "foo.kt",
7535 |metric| {
7536 assert_eq!(metric.abc.conditions_sum(), 3);
7537 },
7538 );
7539 }
7540
7541 #[cfg(feature = "kotlin")]
7542 #[test]
7543 fn kotlin_parenthesised_bare_predicate_is_one_condition() {
7544 // A parenthesised bare predicate (`if ((flag))`) is unwrapped by
7545 // `kotlin_wrapper_operand` and still counts one condition (#773).
7546 // expected: 1.
7547 check_metrics::<KotlinParser>(
7548 "fun m(flag: Boolean) { if ((flag)) { println(\"x\") } }",
7549 "foo.kt",
7550 |metric| {
7551 assert_eq!(metric.abc.conditions_sum(), 1);
7552 },
7553 );
7554 }
7555
7556 // --- TypeScript / TSX ABC tests --------------------------------------
7557 //
7558 // Assignment, branch, condition counting per Fitzpatrick:
7559 // - Augmented assignment / `++` / `--` always count.
7560 // - Plain `=` counts unless inside `const` declaration.
7561 // - `call_expression` / `new_expression` count as branches.
7562 // - Comparison / equality operators, ternary `?`, `??`, control-flow
7563 // arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
7564 // and `<`/`>` (outside `type_arguments` / `type_parameters`) count
7565 // as conditions.
7566
7567 #[cfg(feature = "typescript")]
7568 #[test]
7569 fn typescript_assignments_basic() {
7570 check_metrics::<TypescriptParser>(
7571 "class C {
7572 m(): void {
7573 let x = 0; // +1 — only a `const` initializer is suppressed
7574 x = 1; // +1
7575 x += 2; // +1
7576 x++; // +1
7577 }
7578 }",
7579 "foo.ts",
7580 |metric| {
7581 assert_eq!(metric.abc.assignments_sum(), 4);
7582 insta::assert_json_snapshot!(metric.abc);
7583 },
7584 );
7585 }
7586
7587 #[cfg(feature = "typescript")]
7588 #[test]
7589 fn typescript_const_excluded_from_assignments() {
7590 check_metrics::<TypescriptParser>(
7591 "class C {
7592 m(): void {
7593 const a = 1; // suppressed (`const` initializer)
7594 const b = 2; // suppressed
7595 let c = 3; // +1 — `let` initializers count
7596 }
7597 }",
7598 "foo.ts",
7599 |metric| {
7600 assert_eq!(metric.abc.assignments_sum(), 1);
7601 insta::assert_json_snapshot!(metric.abc);
7602 },
7603 );
7604 }
7605
7606 // Regression cluster for #1277. The pre-fix implementation decided
7607 // "is this `=` a `const` initializer?" from a sentinel stack cleared
7608 // only on a `SEMI` token, so the answer for one statement depended on
7609 // the *previous* statement's terminator. Automatic semicolon
7610 // insertion makes that terminator optional in all four JS-family
7611 // languages. The replacement is structural — see
7612 // `impl_js_family_const_binding!` in `src/metrics/abc/js_family.rs`.
7613
7614 #[cfg(feature = "typescript")]
7615 #[test]
7616 fn typescript_asi_const_does_not_suppress_later_assignments() {
7617 check_metrics::<TypescriptParser>(
7618 "function f() {
7619 const a = 1
7620 x = 2
7621 y = 3
7622 }",
7623 "foo.ts",
7624 |metric| {
7625 // `const a = 1` suppressed; `x = 2` and `y = 3` count.
7626 // Pre-#1277 this reported 0: the unterminated `const`
7627 // never popped its sentinel.
7628 assert_eq!(metric.abc.assignments_sum(), 2);
7629 },
7630 );
7631 }
7632
7633 #[cfg(feature = "typescript")]
7634 #[test]
7635 fn typescript_semicolon_const_does_not_suppress_later_assignments() {
7636 // The semicolon-terminated spelling of the fixture above, which
7637 // must score the same as it. Pre-#1277 they scored 2 and 0 — the
7638 // pair is what pins the terminator out of the answer.
7639 check_metrics::<TypescriptParser>(
7640 "function f() {
7641 const a = 1;
7642 x = 2;
7643 y = 3;
7644 }",
7645 "foo.ts",
7646 |metric| {
7647 assert_eq!(metric.abc.assignments_sum(), 2);
7648 },
7649 );
7650 }
7651
7652 #[cfg(feature = "typescript")]
7653 #[test]
7654 fn typescript_as_const_does_not_suppress_later_assignments() {
7655 // The sentinel stack was also reachable from the other side: the
7656 // `const` token of a TypeScript `x as const` assertion promoted a
7657 // live `let` slot to `Const` and suppressed every `=` until the
7658 // next `;`. Structurally that `const` is a child of an
7659 // `as_expression`, not a declaration keyword.
7660 check_metrics::<TypescriptParser>(
7661 "function f(x: number) {
7662 let y = x as const
7663 w = 3
7664 }",
7665 "foo.ts",
7666 |metric| {
7667 // `let` initializer (+1) and `w = 3` (+1); pre-#1277: 1.
7668 assert_eq!(metric.abc.assignments_sum(), 2);
7669 },
7670 );
7671 }
7672
7673 #[cfg(feature = "typescript")]
7674 #[test]
7675 fn typescript_const_declarator_shapes_stay_suppressed() {
7676 // Shapes the sentinel stack handled implicitly, which the
7677 // structural predicate must reproduce: *every* declarator under a
7678 // `const`-bearing `lexical_declaration` is suppressed, including
7679 // the second element of a multi-declarator list, a destructuring
7680 // pattern, and the defaults inside one — `c = 5`, the nested
7681 // `e = 6` and its `= {}`, `g = 7`, and the rest element's `h = 8`,
7682 // each one more pattern layer the predicate's climb has to cross
7683 // — while `for (const x of xs)` carries no `=` at all. Only
7684 // `let i = 3` and `var j = 4` count — the deliberate deviation
7685 // documented on `js_abc_compute!`. Gating on
7686 // `variable_declaration` instead of `lexical_declaration`,
7687 // dropping the `const` check, or stopping the climb at the
7688 // declarator's own `=`, each flips one of these rows. The
7689 // `for`-of row is the exception: it carries no `=`, so no change
7690 // to the predicate can move it. It pins the grammar shape instead
7691 // — a future grammar emitting a `variable_declarator` there would
7692 // start suppressing something that never counted.
7693 check_metrics::<TypescriptParser>(
7694 "function f(o: any, xs: number[]) {
7695 const a = 1, b = 2
7696 const {c = 5, d: {e = 6} = {}} = o
7697 const [g = 7, ...[h = 8]] = xs
7698 let i = 3
7699 var j = 4
7700 for (const x of xs) { k(x) }
7701 }",
7702 "foo.ts",
7703 |metric| {
7704 assert_eq!(metric.abc.assignments_sum(), 2);
7705 },
7706 );
7707 }
7708
7709 #[cfg(feature = "typescript")]
7710 #[test]
7711 fn typescript_const_initializer_value_assignments_still_count() {
7712 // TypeScript half of
7713 // `javascript_const_initializer_value_assignments_still_count`.
7714 check_metrics::<TypescriptParser>(
7715 "function m(o: any, a: any, b: any) { const x = (o.p = 1); const y = a || (b = 2); }",
7716 "foo.ts",
7717 |metric| {
7718 assert_eq!(metric.abc.assignments_sum(), 2);
7719 },
7720 );
7721 }
7722
7723 #[cfg(feature = "typescript")]
7724 #[test]
7725 fn typescript_branches_function_calls() {
7726 check_metrics::<TypescriptParser>(
7727 "class C {
7728 m(): void {
7729 foo(); // +1
7730 bar(1, 2); // +1
7731 new Date(); // +1
7732 }
7733 }",
7734 "foo.ts",
7735 |metric| {
7736 assert_eq!(metric.abc.branches_sum(), 3);
7737 insta::assert_json_snapshot!(metric.abc);
7738 },
7739 );
7740 }
7741
7742 #[cfg(feature = "typescript")]
7743 #[test]
7744 fn typescript_conditions_comparison_operators() {
7745 check_metrics::<TypescriptParser>(
7746 "class C {
7747 m(x: number, y: number): boolean {
7748 return x == y // +1
7749 || x === y // +1
7750 || x != y // +1
7751 || x !== y // +1
7752 || x < y // +1
7753 || x <= y // +1
7754 || x > y // +1
7755 || x >= y; // +1
7756 }
7757 }",
7758 "foo.ts",
7759 |metric| {
7760 assert_eq!(metric.abc.conditions_sum(), 8);
7761 insta::assert_json_snapshot!(metric.abc);
7762 },
7763 );
7764 }
7765
7766 #[cfg(feature = "typescript")]
7767 #[test]
7768 fn typescript_conditions_control_flow_arms() {
7769 check_metrics::<TypescriptParser>(
7770 "class C {
7771 m(x: number): number {
7772 try { // +1 (try)
7773 if (x > 0) { // +1 (>)
7774 return 1;
7775 } else { // +1 (else)
7776 return -1;
7777 }
7778 } catch (e) { // +1 (catch)
7779 return 0;
7780 }
7781 }
7782 }",
7783 "foo.ts",
7784 |metric| {
7785 assert_eq!(metric.abc.conditions_sum(), 4);
7786 insta::assert_json_snapshot!(metric.abc);
7787 },
7788 );
7789 }
7790
7791 #[cfg(feature = "typescript")]
7792 #[test]
7793 fn typescript_conditions_switch_case() {
7794 check_metrics::<TypescriptParser>(
7795 "class C {
7796 m(x: number): number {
7797 switch (x) {
7798 case 1: // +1
7799 return 1;
7800 case 2: // +1
7801 return 2;
7802 default: // +0 (fallthrough, #469)
7803 return 0;
7804 }
7805 }
7806 }",
7807 "foo.ts",
7808 |metric| {
7809 assert_eq!(metric.abc.conditions_sum(), 2);
7810 insta::assert_json_snapshot!(metric.abc);
7811 },
7812 );
7813 }
7814
7815 #[cfg(feature = "typescript")]
7816 #[test]
7817 fn typescript_ternary_and_nullish() {
7818 check_metrics::<TypescriptParser>(
7819 "class C {
7820 m(x: number | null): number {
7821 return x !== null // +1 (!==)
7822 ? x // +1 (ternary ?)
7823 : 0;
7824 }
7825 n(x: number | null): number {
7826 return x ?? 0; // +1 (??)
7827 }
7828 }",
7829 "foo.ts",
7830 |metric| {
7831 assert_eq!(metric.abc.conditions_sum(), 3);
7832 insta::assert_json_snapshot!(metric.abc);
7833 },
7834 );
7835 }
7836
7837 #[cfg(feature = "typescript")]
7838 #[test]
7839 fn typescript_instanceof_counts_as_condition() {
7840 check_metrics::<TypescriptParser>(
7841 "class C {
7842 m(o: unknown): boolean {
7843 return o instanceof C; // +1
7844 }
7845 }",
7846 "foo.ts",
7847 |metric| {
7848 assert_eq!(metric.abc.conditions_sum(), 1);
7849 insta::assert_json_snapshot!(metric.abc);
7850 },
7851 );
7852 }
7853
7854 #[cfg(feature = "typescript")]
7855 #[test]
7856 fn typescript_generic_lt_gt_not_a_condition() {
7857 // `<T>` in `class C<T>` and `Array<number>` should not contribute
7858 // to conditions even though the tokens are `<` and `>`.
7859 check_metrics::<TypescriptParser>(
7860 "class C<T> {
7861 xs: Array<number> = [];
7862 m(): void {
7863 const arr: Array<string> = []; // suppressed const
7864 void arr;
7865 }
7866 }",
7867 "foo.ts",
7868 |metric| {
7869 assert_eq!(metric.abc.conditions_sum(), 0);
7870 insta::assert_json_snapshot!(metric.abc);
7871 },
7872 );
7873 }
7874
7875 // #1297, TypeScript half: the `<` / `>` arm flipped from denying
7876 // `type_arguments` / `type_parameters` to allowing
7877 // `binary_expression`, and this pins the direction that flip could
7878 // have broken. `Array<string>` keeps a generic in scope so the
7879 // fixture proves both sides at once, and the assertion is the
7880 // grammar-dispatch §8 pin: on the function's own space the ABC
7881 // condition count equals the cyclomatic decision count
7882 // (`cyclomatic()` minus the per-space base of 1). Both are 2, one
7883 // per `if`.
7884 //
7885 // As with the Lua fixture, `Array<string>` leaves no ABC trace once
7886 // excluded, so nothing here fails if a later edit drops the
7887 // generic; that the generic reaches the arm is established by the
7888 // revert test, which fails this test when the gate is removed.
7889 //
7890 // TypeScript gets no JSX fixture because the `.ts` dialect has no
7891 // JSX to exercise: it lexes `return <div>…</div>` as a type
7892 // assertion and a comparison chain, sometimes with `ERROR` nodes and
7893 // sometimes without, depending on what follows the tag. Either way
7894 // the tokens land under `type_arguments` and `binary_expression`
7895 // rather than the `jsx_*` productions, so the construct that
7896 // motivated #1297 is only reachable through
7897 // `tsx_jsx_elements_are_not_conditions` below.
7898 //
7899 // That mis-parse still over-counts — a `.ts` file containing JSX
7900 // scores conditions this gate cannot exclude, because the grammar
7901 // genuinely reports a `binary_expression`. It is a wrong-dialect
7902 // input rather than a defect in this arm, and no allowlist can
7903 // distinguish it; see the same caveat on Kotlin's arm.
7904 #[cfg(feature = "typescript")]
7905 #[test]
7906 fn typescript_comparison_operators_still_count_alongside_generics() {
7907 check_func_space::<TypescriptParser, _>(
7908 "function m(xs: Array<string>, a: number, b: number): number {
7909 if (a < b) { return 1; }
7910 if (a > b) { return 2; }
7911 void xs;
7912 return 0;
7913 }",
7914 "foo.ts",
7915 |space| assert_deepest_conditions_match_cyclomatic(&space, 2),
7916 );
7917 }
7918
7919 // #1275, TypeScript half. Eleven grammar productions emit a bare
7920 // `?` and only `ternary_expression` is a decision; the other ten are
7921 // type syntax. This fixture exercises five of them — `optional_
7922 // parameter`, `property_signature`, `method_signature`,
7923 // `abstract_method_signature` and `public_field_definition`, plus an
7924 // `optional_type` in a tuple — against one real ternary over one
7925 // real `>`.
7926 //
7927 // Six type-syntax `?` means the numbers separate cleanly: 8 pre-fix,
7928 // 2 once the allowlist is aimed at `TernaryExpression`, 1 if it is
7929 // aimed at anything else (the ternary stops counting too), 0 if the
7930 // fixture stops parsing. Any partial gate — one that named some of
7931 // the type-syntax parents in a denylist instead — lands between 3
7932 // and 7 and is equally visible.
7933 #[cfg(feature = "typescript")]
7934 #[test]
7935 fn typescript_optional_type_syntax_is_not_a_condition() {
7936 check_metrics::<TypescriptParser>(
7937 "interface I { a?: string; m?(x: number): void; }
7938 abstract class K { f?: number; abstract g?(): void; }
7939 type Tup = [number, string?];
7940 function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
7941 "foo.ts",
7942 |metric| {
7943 assert_eq!(metric.abc.conditions_sum(), 2);
7944 },
7945 );
7946 }
7947
7948 // The explicit half of #1275's TypeScript decision: a conditional
7949 // type (`T extends U ? X : Y`) is resolved by the type checker and
7950 // erased before runtime, so its `?` is not a condition. That falls
7951 // out of the `TernaryExpression` allowlist rather than being named,
7952 // which is exactly why it needs its own test — the choice reads as
7953 // an omission otherwise, and nothing else here would notice if a
7954 // later edit added `ConditionalType` to the allowlist "for
7955 // symmetry".
7956 //
7957 // The real ternary below keeps the expectation off zero: 3 pre-fix,
7958 // 2 with the conditional type excluded, 1 if the ternary is
7959 // swallowed too.
7960 #[cfg(feature = "typescript")]
7961 #[test]
7962 fn typescript_conditional_type_is_not_a_condition() {
7963 check_metrics::<TypescriptParser>(
7964 "type Cond<T> = T extends string ? number : boolean;
7965 function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
7966 "foo.ts",
7967 |metric| {
7968 assert_eq!(metric.abc.conditions_sum(), 2);
7969 },
7970 );
7971 }
7972
7973 // grammar-dispatch §2. `Typescript::QMARK2` is the enum entry for
7974 // the external scanner's `_ternary_qmark` token, and
7975 // tree-sitter-typescript's public symbol map folds it back onto
7976 // `anon_sym_QMARK`, so `kind_id()` never reports it — every ternary
7977 // `?` in the fixture below arrives as plain `QMARK`. Listing
7978 // `QMARK2` in the gated arm would therefore be dead code, and
7979 // leaving it out is safe only for as long as that mapping holds.
7980 // This pins both halves so a grammar bump that starts exposing the
7981 // alias fails here rather than silently zeroing every TypeScript
7982 // ternary.
7983 //
7984 // The fixture's only `?` is the ternary's, deliberately. An optional
7985 // parameter (`b?: number`) would emit a `QMARK` of its own and
7986 // satisfy the positive assertion on its own, leaving it true no
7987 // matter what id the ternary's `?` came back as — decoration rather
7988 // than the non-vacuity guard it is here for.
7989 #[cfg(feature = "typescript")]
7990 #[test]
7991 fn typescript_ternary_qmark_alias_stays_unreachable() {
7992 let parser = TypescriptParser::new(
7993 "function f(a: boolean): number { return a ? 1 : 2; }\n"
7994 .as_bytes()
7995 .to_vec(),
7996 std::path::Path::new("foo.ts"),
7997 None,
7998 );
7999 assert!(ast_has_kind_id(&parser, Typescript::QMARK as u16));
8000 assert!(!ast_has_kind_id(&parser, Typescript::QMARK2 as u16));
8001 }
8002
8003 #[cfg(feature = "typescript")]
8004 #[test]
8005 fn typescript_abstract_class_abc() {
8006 // Abstract methods have no body — they contribute nothing.
8007 check_metrics::<TypescriptParser>(
8008 "abstract class C {
8009 abstract a(): void;
8010 m(x: number): number {
8011 if (x > 0) return 1; // +1 condition
8012 return 0;
8013 }
8014 }",
8015 "foo.ts",
8016 |metric| {
8017 assert_eq!(metric.abc.conditions_sum(), 1);
8018 assert_eq!(metric.abc.branches_sum(), 0);
8019 insta::assert_json_snapshot!(metric.abc);
8020 },
8021 );
8022 }
8023
8024 #[cfg(feature = "typescript")]
8025 #[test]
8026 fn typescript_interface_abc_zero() {
8027 check_metrics::<TypescriptParser>(
8028 "interface I {
8029 a(): void;
8030 b(): number;
8031 p: string;
8032 }",
8033 "foo.ts",
8034 |metric| {
8035 assert_eq!(metric.abc.assignments_sum(), 0);
8036 assert_eq!(metric.abc.branches_sum(), 0);
8037 assert_eq!(metric.abc.conditions_sum(), 0);
8038 insta::assert_json_snapshot!(metric.abc);
8039 },
8040 );
8041 }
8042
8043 #[cfg(feature = "typescript")]
8044 #[test]
8045 fn typescript_arrow_field_contributes_abc() {
8046 // Arrow function class members are function spaces; their
8047 // assignments/branches/conditions are counted.
8048 check_metrics::<TypescriptParser>(
8049 "class C {
8050 arrow = (x: number) => {
8051 if (x > 0) { // +1 condition
8052 return foo(); // +1 branch
8053 }
8054 return 0;
8055 };
8056 }",
8057 "foo.ts",
8058 |metric| {
8059 assert_eq!(metric.abc.conditions_sum(), 1);
8060 assert_eq!(metric.abc.branches_sum(), 1);
8061 insta::assert_json_snapshot!(metric.abc);
8062 },
8063 );
8064 }
8065
8066 #[cfg(feature = "typescript")]
8067 #[test]
8068 fn typescript_parameter_property_init_not_assignment() {
8069 // Parameter properties don't introduce a `=` token themselves;
8070 // only the explicit `let z = 0` body assignment is counted.
8071 // The class field initializer `f: number = 0` likewise has a `=`
8072 // that DOES count (matches `typescript_assignments_basic`).
8073 check_metrics::<TypescriptParser>(
8074 "class C {
8075 f: number = 0;
8076 constructor(public x: number, private y: string) {
8077 let z = 0;
8078 }
8079 }",
8080 "foo.ts",
8081 |metric| {
8082 // f's initializer + `let z = 0` = 2 assignments; the
8083 // parameter properties contribute zero.
8084 assert_eq!(metric.abc.assignments_sum(), 2);
8085 insta::assert_json_snapshot!(metric.abc);
8086 },
8087 );
8088 }
8089
8090 // TSX parity
8091
8092 #[cfg(feature = "typescript")]
8093 #[test]
8094 fn tsx_assignments_basic() {
8095 check_metrics::<TsxParser>(
8096 "class C {
8097 m(): void {
8098 let x = 0;
8099 x = 1;
8100 x += 2;
8101 x++;
8102 }
8103 }",
8104 "foo.tsx",
8105 |metric| {
8106 assert_eq!(metric.abc.assignments_sum(), 4);
8107 insta::assert_json_snapshot!(metric.abc);
8108 },
8109 );
8110 }
8111
8112 #[cfg(feature = "typescript")]
8113 #[test]
8114 fn tsx_const_excluded_from_assignments() {
8115 check_metrics::<TsxParser>(
8116 "class C {
8117 m(): void {
8118 const a = 1;
8119 let b = 2;
8120 }
8121 }",
8122 "foo.tsx",
8123 |metric| {
8124 assert_eq!(metric.abc.assignments_sum(), 1);
8125 insta::assert_json_snapshot!(metric.abc);
8126 },
8127 );
8128 }
8129
8130 #[cfg(feature = "typescript")]
8131 #[test]
8132 fn tsx_branches_function_calls() {
8133 check_metrics::<TsxParser>(
8134 "class C {
8135 m(): void {
8136 foo();
8137 new Date();
8138 }
8139 }",
8140 "foo.tsx",
8141 |metric| {
8142 assert_eq!(metric.abc.branches_sum(), 2);
8143 insta::assert_json_snapshot!(metric.abc);
8144 },
8145 );
8146 }
8147
8148 #[cfg(feature = "typescript")]
8149 #[test]
8150 fn tsx_conditions_comparison_operators() {
8151 check_metrics::<TsxParser>(
8152 "class C {
8153 m(x: number, y: number): boolean {
8154 return x == y || x < y || x >= y;
8155 }
8156 }",
8157 "foo.tsx",
8158 |metric| {
8159 assert_eq!(metric.abc.conditions_sum(), 3);
8160 insta::assert_json_snapshot!(metric.abc);
8161 },
8162 );
8163 }
8164
8165 #[cfg(feature = "typescript")]
8166 #[test]
8167 fn tsx_conditions_control_flow_arms() {
8168 check_metrics::<TsxParser>(
8169 "class C {
8170 m(x: number): number {
8171 try {
8172 if (x > 0) return 1;
8173 else return -1;
8174 } catch (e) {
8175 return 0;
8176 }
8177 }
8178 }",
8179 "foo.tsx",
8180 |metric| {
8181 assert_eq!(metric.abc.conditions_sum(), 4);
8182 insta::assert_json_snapshot!(metric.abc);
8183 },
8184 );
8185 }
8186
8187 #[cfg(feature = "typescript")]
8188 #[test]
8189 fn tsx_conditions_switch_case() {
8190 check_metrics::<TsxParser>(
8191 "class C {
8192 m(x: number): number {
8193 switch (x) {
8194 case 1: return 1; // +1
8195 case 2: return 2; // +1
8196 default: return 0; // +0 (fallthrough, #469)
8197 }
8198 }
8199 }",
8200 "foo.tsx",
8201 |metric| {
8202 assert_eq!(metric.abc.conditions_sum(), 2);
8203 insta::assert_json_snapshot!(metric.abc);
8204 },
8205 );
8206 }
8207
8208 #[cfg(feature = "typescript")]
8209 #[test]
8210 fn tsx_ternary_and_nullish() {
8211 check_metrics::<TsxParser>(
8212 "class C {
8213 m(x: number | null): number {
8214 return x !== null ? x : 0;
8215 }
8216 n(x: number | null): number { return x ?? 0; }
8217 }",
8218 "foo.tsx",
8219 |metric| {
8220 assert_eq!(metric.abc.conditions_sum(), 3);
8221 insta::assert_json_snapshot!(metric.abc);
8222 },
8223 );
8224 }
8225
8226 #[cfg(feature = "typescript")]
8227 #[test]
8228 fn tsx_instanceof_counts_as_condition() {
8229 check_metrics::<TsxParser>(
8230 "class C { m(o: unknown): boolean { return o instanceof C; } }",
8231 "foo.tsx",
8232 |metric| {
8233 assert_eq!(metric.abc.conditions_sum(), 1);
8234 insta::assert_json_snapshot!(metric.abc);
8235 },
8236 );
8237 }
8238
8239 #[cfg(feature = "typescript")]
8240 #[test]
8241 fn tsx_generic_lt_gt_not_a_condition() {
8242 check_metrics::<TsxParser>(
8243 "class C<T> { xs: Array<number> = []; }",
8244 "foo.tsx",
8245 |metric| {
8246 assert_eq!(metric.abc.conditions_sum(), 0);
8247 insta::assert_json_snapshot!(metric.abc);
8248 },
8249 );
8250 }
8251
8252 // #1297: a JSX tag delimiter is markup punctuation, not a
8253 // comparison. The `<` / `>` arm denied only `type_arguments` and
8254 // `type_parameters`, so every tag bracket scored a condition —
8255 // `jsx_opening_element` contributes both tokens, `jsx_closing_element`
8256 // a `>` (its `</` is one token) and `jsx_self_closing_element` a `<`
8257 // (its `/>` is one token).
8258 //
8259 // The fixture pairs seven such brackets — two elements' opening and
8260 // closing tags, plus one self-closing tag — against one genuine
8261 // `a < b`, so every way of mis-aiming the gate lands on its own
8262 // number: 8 pre-fix, 1 once it allows `BinaryExpression`, 0 if it
8263 // allows the wrong parent or the fixture stops parsing. Asserting 0
8264 // on a JSX-only body would not have separated those last two.
8265 #[cfg(feature = "typescript")]
8266 #[test]
8267 fn tsx_jsx_elements_are_not_conditions() {
8268 check_metrics::<TsxParser>(
8269 "function f(a: number, b: number) {
8270 if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
8271 return <br />;
8272 }",
8273 "foo.tsx",
8274 |metric| {
8275 assert_eq!(metric.abc.conditions_sum(), 1);
8276 // Fixture-presence anchor: the `className="x"` attribute
8277 // `=` is this fixture's only assignment, so the test
8278 // fails if a later edit drops the JSX. Without it the
8279 // surviving `a < b` alone also scores 1, and the
8280 // conditions assertion would pass while proving nothing.
8281 assert_eq!(metric.abc.assignments_sum(), 1);
8282 },
8283 );
8284 }
8285
8286 // #1275 in the second expansion of `ts_abc_compute!`. TSX shares
8287 // TypeScript's `?` productions and its own `TernaryExpression` /
8288 // `QMARK` ids, so the gate is a distinct instantiation and needs its
8289 // own fixture — a passing TypeScript test says nothing about the
8290 // macro's other expansion. Four type-syntax `?` plus one `>` and one
8291 // ternary: 6 pre-fix, 2 after, 1 if the allowlist is misaimed.
8292 #[cfg(feature = "typescript")]
8293 #[test]
8294 fn tsx_optional_type_syntax_is_not_a_condition() {
8295 check_metrics::<TsxParser>(
8296 "interface I { a?: string; m?(x: number): void; }
8297 class K { f?: number; }
8298 function h(x?: number, y: number = 0): number { return y > 1 ? 1 : 2; }",
8299 "foo.tsx",
8300 |metric| {
8301 assert_eq!(metric.abc.conditions_sum(), 2);
8302 },
8303 );
8304 }
8305
8306 // The TSX half of `typescript_conditional_type_is_not_a_condition`.
8307 // `ts_abc_compute!` expands twice and the two expansions are
8308 // independent code (grammar-dispatch §11): adding `ConditionalType`
8309 // to the allowlist "for symmetry" fails only the TypeScript test
8310 // without this one, which reads as the TSX expansion being fine
8311 // rather than untested. `conditional_type` is in the tsx grammar's
8312 // `?` set exactly as it is in typescript's.
8313 #[cfg(feature = "typescript")]
8314 #[test]
8315 fn tsx_conditional_type_is_not_a_condition() {
8316 check_metrics::<TsxParser>(
8317 "type Cond<T> = T extends string ? number : boolean;
8318 function pick(a: number, b: number): number { return a > b ? 1 : 2; }",
8319 "foo.tsx",
8320 |metric| {
8321 assert_eq!(metric.abc.conditions_sum(), 2);
8322 },
8323 );
8324 }
8325
8326 // The TSX half of `typescript_ternary_qmark_alias_stays_unreachable`
8327 // — the tsx grammar declares the same `_ternary_qmark` external and
8328 // maps it back onto `anon_sym_QMARK` at its own id. Same
8329 // single-`?` fixture rule; see that test for why.
8330 #[cfg(feature = "typescript")]
8331 #[test]
8332 fn tsx_ternary_qmark_alias_stays_unreachable() {
8333 let parser = TsxParser::new(
8334 "function f(a: boolean): number { return a ? 1 : 2; }\n"
8335 .as_bytes()
8336 .to_vec(),
8337 std::path::Path::new("foo.tsx"),
8338 None,
8339 );
8340 assert!(ast_has_kind_id(&parser, Tsx::QMARK as u16));
8341 assert!(!ast_has_kind_id(&parser, Tsx::QMARK2 as u16));
8342 }
8343
8344 #[cfg(feature = "typescript")]
8345 #[test]
8346 fn tsx_abstract_class_abc() {
8347 check_metrics::<TsxParser>(
8348 "abstract class C {
8349 abstract a(): void;
8350 m(x: number): number {
8351 if (x > 0) return 1;
8352 return 0;
8353 }
8354 }",
8355 "foo.tsx",
8356 |metric| {
8357 assert_eq!(metric.abc.conditions_sum(), 1);
8358 assert_eq!(metric.abc.branches_sum(), 0);
8359 insta::assert_json_snapshot!(metric.abc);
8360 },
8361 );
8362 }
8363
8364 #[cfg(feature = "typescript")]
8365 #[test]
8366 fn tsx_interface_abc_zero() {
8367 check_metrics::<TsxParser>(
8368 "interface I { a(): void; p: string; }",
8369 "foo.tsx",
8370 |metric| {
8371 assert_eq!(metric.abc.assignments_sum(), 0);
8372 assert_eq!(metric.abc.branches_sum(), 0);
8373 assert_eq!(metric.abc.conditions_sum(), 0);
8374 insta::assert_json_snapshot!(metric.abc);
8375 },
8376 );
8377 }
8378
8379 #[cfg(feature = "typescript")]
8380 #[test]
8381 fn tsx_arrow_field_contributes_abc() {
8382 check_metrics::<TsxParser>(
8383 "class C {
8384 arrow = (x: number) => {
8385 if (x > 0) return foo();
8386 return 0;
8387 };
8388 }",
8389 "foo.tsx",
8390 |metric| {
8391 assert_eq!(metric.abc.conditions_sum(), 1);
8392 assert_eq!(metric.abc.branches_sum(), 1);
8393 insta::assert_json_snapshot!(metric.abc);
8394 },
8395 );
8396 }
8397
8398 #[cfg(feature = "typescript")]
8399 #[test]
8400 fn tsx_asi_const_does_not_suppress_later_assignments() {
8401 // TSX half of the #1277 cluster; see
8402 // `typescript_asi_const_does_not_suppress_later_assignments`.
8403 // TSX has its own `Const` / `VariableDeclarator` /
8404 // `LexicalDeclaration` kind ids, so the predicate is generated
8405 // separately and needs its own fixture.
8406 check_metrics::<TsxParser>(
8407 "function f() {
8408 const a = 1
8409 x = 2
8410 y = 3
8411 }",
8412 "foo.tsx",
8413 |metric| {
8414 assert_eq!(metric.abc.assignments_sum(), 2);
8415 },
8416 );
8417 }
8418
8419 #[cfg(feature = "typescript")]
8420 #[test]
8421 fn tsx_const_declarator_shapes_stay_suppressed() {
8422 // TSX half of `typescript_const_declarator_shapes_stay_suppressed`.
8423 check_metrics::<TsxParser>(
8424 "function f(o: any, xs: number[]) {
8425 const a = 1, b = 2
8426 const {c = 5, d: {e = 6} = {}} = o
8427 const [g = 7, ...[h = 8]] = xs
8428 let i = 3
8429 var j = 4
8430 for (const x of xs) { k(x) }
8431 }",
8432 "foo.tsx",
8433 |metric| {
8434 assert_eq!(metric.abc.assignments_sum(), 2);
8435 },
8436 );
8437 }
8438
8439 #[cfg(feature = "typescript")]
8440 #[test]
8441 fn tsx_parameter_property_init_not_assignment() {
8442 // Parameter properties contribute no `=`; the body's `let z = 0`
8443 // and the field initializer do.
8444 check_metrics::<TsxParser>(
8445 "class C {
8446 f: number = 0;
8447 constructor(public x: number) { let z = 0; }
8448 }",
8449 "foo.tsx",
8450 |metric| {
8451 assert_eq!(metric.abc.assignments_sum(), 2);
8452 insta::assert_json_snapshot!(metric.abc);
8453 },
8454 );
8455 }
8456
8457 // --- Ruby ABC tests ---------------------------------------------------
8458 //
8459 // Each Ruby `assignment` / `operator_assignment` is one assignment
8460 // regardless of whether the LHS is a local, instance, or class
8461 // variable. Every `call` / `super` / `yield` is one branch. Every
8462 // comparison-operator token inside a `binary` node plus each
8463 // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
8464 // one condition.
8465
8466 #[cfg(feature = "ruby")]
8467 #[test]
8468 fn ruby_zero_abc() {
8469 check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
8470 assert_eq!(metric.abc.assignments_sum(), 0);
8471 assert_eq!(metric.abc.branches_sum(), 0);
8472 assert_eq!(metric.abc.conditions_sum(), 0);
8473 insta::assert_json_snapshot!(metric.abc);
8474 });
8475 }
8476
8477 #[cfg(feature = "ruby")]
8478 #[test]
8479 fn ruby_simple_assignment() {
8480 check_metrics::<RubyParser>("def f\n a = 1\n b = 2\nend\n", "foo.rb", |metric| {
8481 assert_eq!(metric.abc.assignments_sum(), 2);
8482 assert_eq!(metric.abc.branches_sum(), 0);
8483 assert_eq!(metric.abc.conditions_sum(), 0);
8484 insta::assert_json_snapshot!(metric.abc);
8485 });
8486 }
8487
8488 #[cfg(feature = "ruby")]
8489 #[test]
8490 fn ruby_augmented_assignment() {
8491 // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
8492 // one assignment. Plain `=` to set the initial value adds one
8493 // more.
8494 check_metrics::<RubyParser>(
8495 "def f(x)\n a = 0\n a += x\n a -= 1\n a *= 2\nend\n",
8496 "foo.rb",
8497 |metric| {
8498 assert_eq!(metric.abc.assignments_sum(), 4);
8499 insta::assert_json_snapshot!(metric.abc);
8500 },
8501 );
8502 }
8503
8504 #[cfg(feature = "ruby")]
8505 #[test]
8506 fn ruby_logical_augmented_assignment() {
8507 // `||=` and `&&=` are also `operator_assignment` nodes.
8508 check_metrics::<RubyParser>("def f\n @x ||= 0\n @x &&= 1\nend\n", "foo.rb", |metric| {
8509 assert_eq!(metric.abc.assignments_sum(), 2);
8510 insta::assert_json_snapshot!(metric.abc);
8511 });
8512 }
8513
8514 #[cfg(feature = "ruby")]
8515 #[test]
8516 fn ruby_method_call_branch() {
8517 // Each method invocation is one branch.
8518 check_metrics::<RubyParser>(
8519 "def f(obj)\n foo()\n obj.bar(1)\nend\n",
8520 "foo.rb",
8521 |metric| {
8522 assert_eq!(metric.abc.branches_sum(), 2);
8523 insta::assert_json_snapshot!(metric.abc);
8524 },
8525 );
8526 }
8527
8528 #[cfg(feature = "ruby")]
8529 #[test]
8530 fn ruby_super_and_yield_branches() {
8531 // `super` and `yield` both count as branches (control-pass).
8532 check_metrics::<RubyParser>("def f\n super\n yield\nend\n", "foo.rb", |metric| {
8533 assert_eq!(metric.abc.branches_sum(), 2);
8534 assert_eq!(metric.abc.assignments_sum(), 0);
8535 insta::assert_json_snapshot!(metric.abc);
8536 });
8537 }
8538
8539 #[cfg(feature = "ruby")]
8540 #[test]
8541 fn ruby_attr_macro_is_branch() {
8542 // `attr_accessor` is a `Call3` node and registers as a branch
8543 // like any method invocation.
8544 check_metrics::<RubyParser>("class A\n attr_accessor :x\nend\n", "foo.rb", |metric| {
8545 assert_eq!(metric.abc.branches_sum(), 1);
8546 insta::assert_json_snapshot!(metric.abc);
8547 });
8548 }
8549
8550 #[cfg(feature = "ruby")]
8551 #[test]
8552 fn ruby_comparison_conditions() {
8553 // Each comparison operator is one condition.
8554 check_metrics::<RubyParser>(
8555 "def f(a, b)\n a == b\n a != b\n a < b\n a > b\n a <= b\n a >= b\nend\n",
8556 "foo.rb",
8557 |metric| {
8558 assert_eq!(metric.abc.conditions_sum(), 6);
8559 insta::assert_json_snapshot!(metric.abc);
8560 },
8561 );
8562 }
8563
8564 #[cfg(feature = "ruby")]
8565 #[test]
8566 fn ruby_superclass_clause_is_not_a_condition() {
8567 // Regression for #1280: a superclass clause spells its `<` with the
8568 // same `LT` token as a comparison, but parents it under
8569 // `superclass` rather than `binary`, so the parent gate excludes
8570 // it. Before the gate every subclass declaration scored a phantom
8571 // condition.
8572 // expected: 0 conditions — the file contains no conditional at all;
8573 // the single assignment is `x = 1`.
8574 check_metrics::<RubyParser>(
8575 "class Foo < Bar\n def plain\n x = 1\n end\nend\n",
8576 "foo.rb",
8577 |metric| {
8578 assert_eq!(metric.abc.conditions_sum(), 0);
8579 assert_eq!(metric.abc.assignments_sum(), 1);
8580 },
8581 );
8582 }
8583
8584 #[cfg(feature = "ruby")]
8585 #[test]
8586 fn ruby_operator_method_name_is_not_a_condition() {
8587 // The `<` naming an operator method parents under `operator`, which
8588 // the `binary` gate likewise excludes (#1280). The body carries a
8589 // real comparison so the expected value is not the all-zero default:
8590 // 1 discriminates "only the name token is excluded" from both "the
8591 // gate excludes everything" (0) and "nothing is gated" (2).
8592 // expected: 1 condition — the `@v < other` comparison, not the `def <`.
8593 check_metrics::<RubyParser>("def <(other)\n @v < other\nend\n", "foo.rb", |metric| {
8594 assert_eq!(metric.abc.conditions_sum(), 1);
8595 });
8596 }
8597
8598 #[cfg(feature = "ruby")]
8599 #[test]
8600 fn ruby_every_comparison_operator_method_name_is_not_a_condition() {
8601 // The sibling half of #1280. `<` is not special: every comparison
8602 // and equality token Ruby lets you `def` parents under `operator`
8603 // in that position, so gating only `LT` / `GT` left `def ==`,
8604 // `def <=`, `def >=`, `def <=>`, `def !=` and `def =~` each
8605 // scoring a phantom condition — measured at 1 apiece for a body
8606 // containing no conditional at all.
8607 // expected: 0 conditions per definition; only the name token is on
8608 // the line, so a single non-zero total localises the regression.
8609 check_metrics::<RubyParser>(
8610 "def ==(o)\n 1\nend\n\
8611 def !=(o)\n 1\nend\n\
8612 def <=(o)\n 1\nend\n\
8613 def >=(o)\n 1\nend\n\
8614 def <=>(o)\n 1\nend\n\
8615 def =~(o)\n 1\nend\n\
8616 def <(o)\n 1\nend\n\
8617 def >(o)\n 1\nend\n",
8618 "foo.rb",
8619 |metric| {
8620 assert_eq!(metric.abc.conditions_sum(), 0);
8621 },
8622 );
8623 // The positive control: the same tokens inside a `binary` are real
8624 // comparisons and still count, so the gate is not blanket
8625 // suppression.
8626 check_metrics::<RubyParser>(
8627 "def cmp(a, b)\n a == b || a <= b || a <=> b\nend\n",
8628 "foo.rb",
8629 |metric| {
8630 assert_eq!(metric.abc.conditions_sum(), 3);
8631 },
8632 );
8633 }
8634
8635 #[cfg(feature = "ruby")]
8636 #[test]
8637 fn ruby_case_match_in_arms_are_conditions() {
8638 // Regression for #977: each non-wildcard `case … in` arm is one
8639 // ABC condition, matching Python's `case_clause` handling. Using
8640 // literal patterns (no comparison operators) isolates the
8641 // `in_clause` contribution from any operand tokens.
8642 // expected: 2 conditions — one per `in 1` / `in 2` arm.
8643 check_metrics::<RubyParser>(
8644 "def f(x)\n case x\n in 1 then :one\n in 2 then :two\n end\nend\n",
8645 "foo.rb",
8646 |metric| {
8647 assert_eq!(metric.abc.conditions_sum(), 2);
8648 },
8649 );
8650 }
8651
8652 #[cfg(feature = "ruby")]
8653 #[test]
8654 fn ruby_case_match_guarded_wildcard_is_a_condition() {
8655 // Regression for #977: a guarded wildcard arm `in _ if x` is not a
8656 // bare default and counts as one ABC condition, while the trailing
8657 // bare `in _` adds none.
8658 // expected: 2 conditions — the guarded `in _ if x` arm, plus its
8659 // guard slot. The guard predicate is a bare identifier, so before
8660 // #1454 it contributed nothing and the total was 1; the slot is
8661 // what makes `if x`, `if x.even?` and `if x > 0` agree.
8662 check_metrics::<RubyParser>(
8663 "def f(x)\n case x\n in _ if x then :y\n in _ then :default\n end\nend\n",
8664 "foo.rb",
8665 |metric| {
8666 assert_eq!(metric.abc.conditions_sum(), 2);
8667 },
8668 );
8669 }
8670
8671 #[cfg(feature = "ruby")]
8672 #[test]
8673 fn ruby_case_match_bare_wildcard_is_not_a_condition() {
8674 // Regression for #977: a `case … in` whose only arm is the bare
8675 // wildcard `in _` (no guard) is the default arm and contributes no
8676 // ABC condition, keeping ABC and cyclomatic in lockstep on the
8677 // same construct.
8678 // expected: 0 conditions.
8679 check_metrics::<RubyParser>(
8680 "def f(x)\n case x\n in _ then :default\n end\nend\n",
8681 "foo.rb",
8682 |metric| {
8683 assert_eq!(metric.abc.conditions_sum(), 0);
8684 },
8685 );
8686 }
8687
8688 #[cfg(feature = "ruby")]
8689 #[test]
8690 fn ruby_bare_predicate_control_flow_counts_one_condition() {
8691 // Regression for #696: idiomatic Ruby bare predicates
8692 // (`if flag` / `while flag` / `unless flag` / `until flag`) each
8693 // count one unary condition, matching Rust / C# / PHP / Python. The
8694 // condition field is read for both block and modifier forms.
8695 //
8696 // expected: 8 conditions — four block forms (`if`/`unless`/`while`/
8697 // `until`) plus the same four as modifiers, one each.
8698 check_metrics::<RubyParser>(
8699 "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",
8700 "foo.rb",
8701 |metric| {
8702 assert_eq!(metric.abc.conditions_sum(), 8);
8703 },
8704 );
8705 }
8706
8707 #[cfg(feature = "ruby")]
8708 #[test]
8709 fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() {
8710 // `if a == b` counts only the `==` comparison (the condition field
8711 // is a `binary` node, adding nothing). `if a && b` counts the two
8712 // chain operands via the `&&` walker, again with the condition-field
8713 // arm adding nothing — so neither shape is double-counted (#696).
8714 //
8715 // expected: 3 — `==` (1) + the `a`,`b` operands of `&&` (2).
8716 check_metrics::<RubyParser>(
8717 "def f(a, b)\n if a == b\n x\n end\n if a && b\n y\n end\nend\n",
8718 "foo.rb",
8719 |metric| {
8720 assert_eq!(metric.abc.conditions_sum(), 3);
8721 },
8722 );
8723 }
8724
8725 #[cfg(feature = "ruby")]
8726 #[test]
8727 fn ruby_spaceship_and_case_equality() {
8728 // `<=>` and `===` are comparison operators (conditions).
8729 check_metrics::<RubyParser>(
8730 "def f(a, b)\n a <=> b\n a === b\nend\n",
8731 "foo.rb",
8732 |metric| {
8733 assert_eq!(metric.abc.conditions_sum(), 2);
8734 insta::assert_json_snapshot!(metric.abc);
8735 },
8736 );
8737 }
8738
8739 #[cfg(feature = "ruby")]
8740 #[test]
8741 fn ruby_ternary_condition() {
8742 // The `?` ternary marker is one condition; the inner `==` is
8743 // another.
8744 check_metrics::<RubyParser>("def f(x)\n x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
8745 assert_eq!(metric.abc.conditions_sum(), 2);
8746 insta::assert_json_snapshot!(metric.abc);
8747 });
8748 }
8749
8750 // Issue #1161. Ruby's ternary carried only the `?` token arm, so
8751 // `a ? !b : !c` scored 1 against the 4 that Java, C#, Groovy, the C
8752 // family, the JS family, PHP and Perl all report for the same
8753 // expression (#1102).
8754 //
8755 // Every expectation below is the value its C++ sibling
8756 // (`cpp_ternary_operand_slots_count_as_unary_conditions`) already
8757 // asserts for the same expression, so the two read as one table.
8758 #[cfg(feature = "ruby")]
8759 #[test]
8760 fn ruby_ternary_operand_slots_count_as_unary_conditions() {
8761 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
8762 check_metrics::<RubyParser>("def f\n x = a ? !b : !c\nend\n", "foo.rb", |metric| {
8763 assert_eq!(metric.abc.conditions_sum(), 4);
8764 });
8765 // No-double-count pin, and the assertion that catches the trap
8766 // this grammar sets: `-b` and `!b` are the SAME node kind
8767 // (`unary:284`), separated only by their operator. Scoring a
8768 // branch only when the peel proves it boolean — a `!` / `not`
8769 // layer, not the `unary` kind — is what keeps this at 2. An
8770 // implementation keying on `Unary` reads 3 here.
8771 // `?` (1) + `>` (1) = 2, unchanged by the fix: the parenthesised
8772 // condition unwraps to a comparison its own token already
8773 // counts, and neither branch is negated.
8774 check_metrics::<RubyParser>("def f\n x = (a > 0) ? b : -b\nend\n", "foo.rb", |metric| {
8775 assert_eq!(metric.abc.conditions_sum(), 2);
8776 });
8777 // Nested — Ruby needs the inner ternary parenthesised. Outer `?`
8778 // (1) + outer condition `a` (1) + inner `?` (1) + inner
8779 // condition `b` (1) = 4. The outer consequence unwraps to the
8780 // inner `conditional`, which proves nothing boolean and is no
8781 // further paren / `!` layer, so it adds nothing on its own; the
8782 // inner ternary is reached by the walk, not by descent.
8783 check_metrics::<RubyParser>(
8784 "def f\n x = a ? (b ? c : d) : e\nend\n",
8785 "foo.rb",
8786 |metric| {
8787 assert_eq!(metric.abc.conditions_sum(), 4);
8788 },
8789 );
8790 // A parenthesised condition, pinning the paren unwrap on the
8791 // condition slot: `(a)` is `parenthesized_statements`, which the
8792 // peel must look through.
8793 // `?` (1) + `(a)` (1) + `!b` (1) + `!c` (1) = 4.
8794 check_metrics::<RubyParser>("def f\n x = (a) ? !b : !c\nend\n", "foo.rb", |metric| {
8795 assert_eq!(metric.abc.conditions_sum(), 4);
8796 });
8797 // A negated condition takes the same fallback through the `!`
8798 // unwrap rather than the paren one. `?` (1) + `!a` (1) = 2.
8799 check_metrics::<RubyParser>("def f\n x = !a ? b : c\nend\n", "foo.rb", |metric| {
8800 assert_eq!(metric.abc.conditions_sum(), 2);
8801 });
8802 }
8803
8804 // The boolean-context seed must discriminate between the condition
8805 // slot and the two branch slots — not merely exist. None of the
8806 // fixtures above can tell the difference: their branches are either
8807 // `!`-unaries (which set the flag inside the unwrap loop regardless
8808 // of the seed) or kinds the loop breaks on before any terminal test.
8809 // A seed that returned `true` for every slot of a `Conditional`
8810 // leaves all five at their asserted values and fails nothing.
8811 //
8812 // A parenthesised *branch* is the input that separates them: the
8813 // unwrap reaches a bare terminal, so only the seed decides whether
8814 // it counts. `?` (1) + condition `a` (1) = 2 in both directions.
8815 #[cfg(feature = "ruby")]
8816 #[test]
8817 fn ruby_ternary_branch_operands_are_not_double_counted() {
8818 check_metrics::<RubyParser>("def f\n x = a ? (b) : c\nend\n", "foo.rb", |metric| {
8819 assert_eq!(metric.abc.conditions_sum(), 2);
8820 });
8821 check_metrics::<RubyParser>("def f\n x = a ? b : (c)\nend\n", "foo.rb", |metric| {
8822 assert_eq!(metric.abc.conditions_sum(), 2);
8823 });
8824 // The same pair with a comment before the operand. Comments are
8825 // tree-sitter `extras`, so they become the branch's previous
8826 // sibling — which is why the seed asks the grammar which child
8827 // is the `condition` field rather than testing that sibling for
8828 // `?` / `:` as the C family does. Under the token form both of
8829 // these read 3.
8830 check_metrics::<RubyParser>(
8831 "def f\n x = a ?\n # note\n (b) : c\nend\n",
8832 "foo.rb",
8833 |metric| {
8834 assert_eq!(metric.abc.conditions_sum(), 2);
8835 },
8836 );
8837 check_metrics::<RubyParser>(
8838 "def f\n x = a ? b :\n # note\n (c)\nend\n",
8839 "foo.rb",
8840 |metric| {
8841 assert_eq!(metric.abc.conditions_sum(), 2);
8842 },
8843 );
8844 }
8845
8846 #[cfg(feature = "ruby")]
8847 #[test]
8848 fn ruby_case_when_arms() {
8849 // Each `when` named clause and the `else` clause count as one
8850 // condition each; the `case` head and the implicit `then`
8851 // wrappers do not.
8852 check_metrics::<RubyParser>(
8853 "def f(x)\n case x\n when 1 then 'one'\n when 2 then 'two'\n else 'other'\n end\nend\n",
8854 "foo.rb",
8855 |metric| {
8856 // 2 `when` + 1 `else` = 3 conditions.
8857 assert_eq!(metric.abc.conditions_sum(), 3);
8858 insta::assert_json_snapshot!(metric.abc);
8859 },
8860 );
8861 }
8862
8863 #[cfg(feature = "ruby")]
8864 #[test]
8865 fn ruby_elsif_and_else() {
8866 // `elsif` and `else` named clauses are conditions; their inner
8867 // `then` wrappers are not.
8868 check_metrics::<RubyParser>(
8869 "def f(x)\n if x > 0\n 1\n elsif x < 0\n -1\n else\n 0\n end\nend\n",
8870 "foo.rb",
8871 |metric| {
8872 // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
8873 assert_eq!(metric.abc.conditions_sum(), 4);
8874 insta::assert_json_snapshot!(metric.abc);
8875 },
8876 );
8877 }
8878
8879 #[cfg(feature = "ruby")]
8880 #[test]
8881 fn ruby_rescue_clause_condition() {
8882 // The `rescue` named clause is one condition; the `rescue`
8883 // keyword token (`Rescue2`) is not counted on its own.
8884 // `do_it` without parens is an `identifier`, not a `call`, so
8885 // it contributes no branch. `handle(e)` is a `call` (1 branch).
8886 check_metrics::<RubyParser>(
8887 "def f\n begin\n do_it\n rescue StandardError => e\n handle(e)\n end\nend\n",
8888 "foo.rb",
8889 |metric| {
8890 assert_eq!(metric.abc.conditions_sum(), 1);
8891 assert_eq!(metric.abc.branches_sum(), 1);
8892 insta::assert_json_snapshot!(metric.abc);
8893 },
8894 );
8895 }
8896
8897 #[cfg(feature = "ruby")]
8898 #[test]
8899 fn ruby_class_complex_function() {
8900 // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
8901 check_metrics::<RubyParser>(
8902 "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",
8903 "foo.rb",
8904 |metric| {
8905 assert_eq!(metric.abc.assignments_sum(), 1);
8906 assert_eq!(metric.abc.branches_sum(), 1);
8907 // `>`(1) + `==`(1) = 2 conditions. `if` is not a
8908 // token; `&&` is `AMPAMP` and is not counted (see
8909 // the module-level `Stats` doc-comment for the
8910 // cross-language policy; #395, walker tracked in
8911 // #403).
8912 assert_eq!(metric.abc.conditions_sum(), 2);
8913 insta::assert_json_snapshot!(metric.abc);
8914 },
8915 );
8916 }
8917
8918 #[cfg(feature = "ruby")]
8919 #[test]
8920 fn ruby_unary_conditions_in_chain() {
8921 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
8922 // `&&` / `||` chain is one condition. `a && b || c` → a, b, c each
8923 // contribute one. Ruby's `if` keyword is not a condition token.
8924 // expected: 3 unary conditions (matches the Java byte-equivalent).
8925 check_metrics::<RubyParser>(
8926 "def f(a, b, c)\n if a && b || c\n puts \"x\"\n end\nend\n",
8927 "foo.rb",
8928 |metric| {
8929 assert_eq!(metric.abc.conditions_sum(), 3);
8930 },
8931 );
8932 }
8933
8934 #[cfg(feature = "ruby")]
8935 #[test]
8936 fn ruby_keyword_and_or_chain_counts_operands() {
8937 // The keyword forms `and` / `or` get the same Rule 9 treatment as
8938 // `&&` / `||`. expected: 3 unary conditions (a, b, c).
8939 check_metrics::<RubyParser>(
8940 "def f(a, b, c)\n if a and b or c\n puts \"x\"\n end\nend\n",
8941 "foo.rb",
8942 |metric| {
8943 assert_eq!(metric.abc.conditions_sum(), 3);
8944 },
8945 );
8946 }
8947
8948 #[cfg(feature = "ruby")]
8949 #[test]
8950 fn ruby_negated_operand_is_unary_condition() {
8951 // A `!`-negated operand unwraps the `unary` node to the inner
8952 // identifier. expected: 2 (`a` and the `!b` operand).
8953 check_metrics::<RubyParser>(
8954 "def f(a, b)\n if a && !b\n puts \"x\"\n end\nend\n",
8955 "foo.rb",
8956 |metric| {
8957 assert_eq!(metric.abc.conditions_sum(), 2);
8958 },
8959 );
8960 }
8961
8962 #[cfg(feature = "ruby")]
8963 #[test]
8964 fn ruby_comparison_operands_add_nothing() {
8965 // Isolation for Rule 9 (issue #557): when the `&&` operands are
8966 // themselves comparisons, the unary-condition walker must add
8967 // nothing on top of the two `>` comparisons already counted as
8968 // conditions — distinguishing the gap (bare boolean operands)
8969 // from ordinary relational conditions. Mirrors the Kotlin and
8970 // Elixir isolation tests. expected: 2 (the two `>` comparisons).
8971 check_metrics::<RubyParser>(
8972 "def f(x, y)\n if x > 0 && y > 0\n puts \"x\"\n end\nend\n",
8973 "foo.rb",
8974 |metric| {
8975 assert_eq!(metric.abc.conditions_sum(), 2);
8976 },
8977 );
8978 }
8979
8980 // The two anchors every #1453 fixture needs, coupled on purpose as
8981 // `assert_kotlin_class_members` couples them for #1421. `subjects` is
8982 // the load-bearing one: the whole distinction is whether a `case`
8983 // carries a `value` field, no node kind spells that, and a fixture
8984 // that grew a subject would score its clauses the pre-#1453 way with
8985 // every other row still satisfied. Each fixture is top-level `def`s,
8986 // so the members are the unit space's.
8987 #[cfg(feature = "ruby")]
8988 fn assert_ruby_case_members(
8989 src: &str,
8990 subjects: usize,
8991 kinds: &[(u16, usize, &str)],
8992 expected: &[(&str, u64, u64)],
8993 ) {
8994 let parser = RubyParser::new(
8995 src.as_bytes().to_vec(),
8996 std::path::Path::new("foo.rb"),
8997 None,
8998 );
8999 let found = parser
9000 .root()
9001 .preorder()
9002 .filter(|n| n.kind_id() == Ruby::Case && n.child_by_field_name("value").is_some())
9003 .count();
9004 assert_eq!(found, subjects, "`case` subjects — the fixture moved");
9005 assert_fixture_spells::<RubyParser>(src, "foo.rb", kinds);
9006 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9007 assert_members_score(&space, expected);
9008 });
9009 }
9010
9011 // #1453's headline. A subject-less `when` clause's condition is an
9012 // ordinary boolean expression, so a comparison in it is already an
9013 // ABC condition through the token arm; the clause added a blanket one
9014 // on top, and `gt` scored 3 against its `if` analogue `iff`'s 2.
9015 //
9016 // `bare` and `call` are the controls that isolate the defect: same
9017 // clause shape, no operator for any other arm to count, so the clause
9018 // must still pay for its decision — 2 and 1 before the fix and after.
9019 // A fix that moved them would be attacking the clause count, which
9020 // was never wrong.
9021 //
9022 // `gt`, `bare` and `iff` sit one above cyclomatic's decision count
9023 // because ABC counts the `else` (Fitzpatrick Rule 5) and cyclomatic
9024 // does not; every other row is at `conditions == cyclomatic - 1`.
9025 // `cmt` puts a comment where the subject would go: an extra is not the
9026 // `value` field, so the `case` stays subject-less.
9027 #[cfg(feature = "ruby")]
9028 #[test]
9029 fn ruby_subjectless_case_when_counts_its_condition_once() {
9030 let src = "def gt(x)
9031 case
9032 when x > 5 then 1
9033 else 0
9034 end
9035end
9036def iff(x)
9037 if x > 5 then 1 else 0 end
9038end
9039def two(x)
9040 case
9041 when x > 5 then 1
9042 when x < 0 then 2
9043 end
9044end
9045def ge(x)
9046 case
9047 when x >= 5 then 1
9048 end
9049end
9050def andd(a, b)
9051 case
9052 when a > 1 && b < 2 then 1
9053 end
9054end
9055def orr(a, b)
9056 case
9057 when a || b then 1
9058 end
9059end
9060def kw_or(a, b)
9061 case
9062 when a or b then 1
9063 end
9064end
9065def bare(b)
9066 case
9067 when b then 1
9068 else 0
9069 end
9070end
9071def call(x)
9072 case
9073 when x.even? then 1
9074 end
9075end
9076def cmt(x)
9077 case # no subject
9078 when x > 5 then 1
9079 end
9080end
9081";
9082 assert_ruby_case_members(
9083 src,
9084 0,
9085 &[
9086 (Ruby::When as u16, 10, "`when` clauses"),
9087 (Ruby::GT as u16, 5, "`>` comparisons"),
9088 (Ruby::AMPAMP as u16, 1, "`andd`'s `&&`"),
9089 (Ruby::PIPEPIPE as u16, 1, "`orr`'s `||`"),
9090 (Ruby::Or as u16, 1, "`kw_or`'s `or`"),
9091 (Ruby::Comment as u16, 1, "`cmt`'s comment"),
9092 ],
9093 &[
9094 // (member, abc.conditions, cyclomatic)
9095 // Was 3: the `>` and the clause, for one decision.
9096 ("gt", 2, 2),
9097 ("iff", 2, 2),
9098 // Was 4.
9099 ("two", 2, 3),
9100 // Was 2: `>=` reaches the same token arm.
9101 ("ge", 1, 2),
9102 // Was 3. The compound condition keeps both comparisons —
9103 // suppressing the operators instead would collapse it.
9104 ("andd", 2, 3),
9105 // The Rule 9 walker scores both operands of a bare chain,
9106 // in both spellings; the clause adds nothing on top.
9107 ("orr", 2, 3),
9108 ("kw_or", 2, 3),
9109 ("bare", 2, 2),
9110 ("call", 1, 2),
9111 // Was 2.
9112 ("cmt", 1, 2),
9113 ],
9114 );
9115 }
9116
9117 // Wrappers and the shapes no other arm owns. `not_cmp`, `paren_cmp`,
9118 // `tern` and `test_pat` peel to an expression another arm already
9119 // scored, and each read one above parity before the fix. `paren` and
9120 // `notted` peel to a bare operand nothing else counts, so the clause
9121 // pays exactly as an `if` predicate would.
9122 //
9123 // `scope`, `neg`, `splat` and `arith` are why the clause asks "is this
9124 // already paid for" rather than "is this a known terminal": the `if`
9125 // slot asked the second question and scored each of these 0 until
9126 // #1520 moved it onto the clause's question — see
9127 // `ruby_condition_slot_pays_what_no_other_arm_counted`.
9128 #[cfg(feature = "ruby")]
9129 #[test]
9130 fn ruby_subjectless_case_when_condition_wrappers_count_once() {
9131 let src = "def paren(b)
9132 case
9133 when (b) then 1
9134 end
9135end
9136def notted(b)
9137 case
9138 when !b then 1
9139 end
9140end
9141def not_cmp(x)
9142 case
9143 when not(x > 1) then 1
9144 end
9145end
9146def paren_cmp(x)
9147 case
9148 when (x > 1) then 1
9149 end
9150end
9151def tern(x, a, b)
9152 case
9153 when x ? a : b then 1
9154 end
9155end
9156def test_pat(x)
9157 case
9158 when (x in Integer) then 1
9159 end
9160end
9161def scope
9162 case
9163 when Foo::Bar then 1
9164 end
9165end
9166def neg(x)
9167 case
9168 when -x then 1
9169 end
9170end
9171def splat(xs)
9172 case
9173 when *xs then 1
9174 end
9175end
9176def arith(x)
9177 case
9178 when x + 1 then 1
9179 end
9180end
9181";
9182 assert_ruby_case_members(
9183 src,
9184 0,
9185 &[
9186 (Ruby::When as u16, 10, "`when` clauses"),
9187 (
9188 Ruby::ParenthesizedStatements as u16,
9189 4,
9190 "parenthesised operands",
9191 ),
9192 (Ruby::Conditional as u16, 1, "`tern`'s ternary"),
9193 (Ruby::TestPattern as u16, 1, "`test_pat`'s pattern test"),
9194 (Ruby::ScopeResolution2 as u16, 1, "`scope`'s `Foo::Bar`"),
9195 (Ruby::SplatArgument as u16, 1, "`splat`'s `*xs`"),
9196 ],
9197 &[
9198 ("paren", 1, 2),
9199 ("notted", 1, 2),
9200 // Each was 2.
9201 ("not_cmp", 1, 2),
9202 ("paren_cmp", 1, 2),
9203 ("test_pat", 1, 2),
9204 // Was 3. The ternary's own `?` and its condition `x` are
9205 // its two conditions, as in `if x ? a : b`.
9206 ("tern", 2, 3),
9207 ("scope", 1, 2),
9208 ("neg", 1, 2),
9209 ("splat", 1, 2),
9210 ("arith", 1, 2),
9211 ],
9212 );
9213 }
9214
9215 // A clause may list several patterns: an implicit `||`, so each is
9216 // one operand of that chain and scores once unless its own arm already
9217 // counted it — every row here must equal its `if … || …` twin in the
9218 // `or_*` defs. `bare_first` and `cmp_first` differ only in pattern
9219 // order and must agree; reading the first pattern alone, as Kotlin's
9220 // `kotlin_count_when_entry` does, would score them 2 and 1. None can
9221 // reach `conditions == cyclomatic - 1`: cyclomatic scores the clause
9222 // as one decision, and the `||` twins sit just as far above it.
9223 #[cfg(feature = "ruby")]
9224 #[test]
9225 fn ruby_subjectless_case_when_multiple_patterns() {
9226 let src = "def bare_alts(a, b)
9227 case
9228 when a, b then 1
9229 end
9230end
9231def alts(x)
9232 case
9233 when x > 1, x < -1 then 1
9234 end
9235end
9236def bare_first(a, x)
9237 case
9238 when a, x > 1 then 1
9239 end
9240end
9241def cmp_first(a, x)
9242 case
9243 when x > 1, a then 1
9244 end
9245end
9246def or_bare(a, b)
9247 if a || b then 1 end
9248end
9249def or_cmp(x)
9250 if x > 1 || x < -1 then 1 end
9251end
9252def or_mixed(a, x)
9253 if a || x > 1 then 1 end
9254end
9255";
9256 assert_ruby_case_members(
9257 src,
9258 0,
9259 &[
9260 (Ruby::When as u16, 4, "`when` clauses"),
9261 (Ruby::Pattern as u16, 8, "two patterns per clause"),
9262 ],
9263 &[
9264 // Was 1: one pattern went unscored.
9265 ("bare_alts", 2, 2),
9266 ("or_bare", 2, 3),
9267 // Was 3.
9268 ("alts", 2, 2),
9269 ("or_cmp", 2, 3),
9270 ("bare_first", 2, 2),
9271 ("cmp_first", 2, 2),
9272 ("or_mixed", 2, 3),
9273 ],
9274 );
9275 }
9276
9277 // An `elsif` is Java's `else if`: one condition for the `else`
9278 // (Rule 5) plus an `if` predicate slot. It paid only the `else`, so
9279 // `eb`, `eself`, `eneg` and `eparen` scored 2 where
9280 // `if (a) {} else if (b) {}` scores 3, while `ecmp` matched only
9281 // because its `>` arm paid the predicate. Each row now equals its Java
9282 // twin (`eelse` 4 / 3: Rule 5 counts the `else` arm, cyclomatic
9283 // does not), and `ecmp` / `eand` must not move, or the slot is paying
9284 // on top of the operator arms (#1520).
9285 #[cfg(feature = "ruby")]
9286 #[test]
9287 fn ruby_elsif_predicate_is_a_condition_slot() {
9288 let src = "def eb(a, b)
9289 if a then 1 elsif b then 2 end
9290end
9291def ecmp(a, x)
9292 if a then 1 elsif x > 0 then 2 end
9293end
9294def eself(a)
9295 if a then 1 elsif self then 2 end
9296end
9297def eneg(a, b)
9298 if a then 1 elsif !b then 2 end
9299end
9300def eparen(a, b)
9301 if a then 1 elsif (b) then 2 end
9302end
9303def eand(a, b, c)
9304 if a then 1 elsif b && c then 2 end
9305end
9306def eelse(a, b)
9307 if a then 1 elsif b then 2 else 3 end
9308end
9309";
9310 assert_fixture_spells::<RubyParser>(
9311 src,
9312 "foo.rb",
9313 &[
9314 (Ruby::Elsif as u16, 7, "one `elsif` per member"),
9315 (Ruby::Else as u16, 1, "`eelse`'s `else`"),
9316 ],
9317 );
9318 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9319 assert_members_score(
9320 &space,
9321 &[
9322 // (member, abc.conditions, cyclomatic)
9323 // Was 2.
9324 ("eb", 3, 3),
9325 ("ecmp", 3, 3),
9326 // Was 2.
9327 ("eself", 3, 3),
9328 // Was 2.
9329 ("eneg", 3, 3),
9330 // Was 2.
9331 ("eparen", 3, 3),
9332 ("eand", 4, 4),
9333 // Was 3.
9334 ("eelse", 4, 3),
9335 ],
9336 );
9337 });
9338 }
9339
9340 // #1520: every Ruby condition slot — block and modifier `if` /
9341 // `unless` / `while` / `until`, a `case … in` guard, a ternary
9342 // condition — pays one condition unless another arm already charged
9343 // the same decision. The slot used to pay only for a predicate that
9344 // peeled down to a known boolean terminal, so the first block below
9345 // scored 0 for one decision each, while the same expression as a
9346 // subject-less `when` scored 1 (`ruby_subjectless_case_when_condition_
9347 // wrappers_count_once`). Every row sits at `conditions == cyclomatic
9348 // - 1` except `tern` and `ifelse`, which agree with each other one
9349 // above it: ABC counts the `?` / `else` arm and cyclomatic does not.
9350 // `chain` and `chain_neg` sit level with each other, as a Rule 9
9351 // chain scores one per operand.
9352 //
9353 // The second block must not move, or the slot is paying twice: each
9354 // predicate's comparison or chain operand is already counted by its
9355 // own arm. `begin_cmp` and `assign_cmp` are why the peel looks through
9356 // `begin … end` and an assignment's value — without that the slot
9357 // would see a non-terminal and pay on top of the `>`. `chain_neg`'s
9358 // operands are slots of their own: the Rule 9 walker pays for `-a`
9359 // and `-b` as it does for `a` and `b`, and the `if` adds nothing
9360 // (#1529; before it, the walker skipped both and the `if` paid one).
9361 #[cfg(feature = "ruby")]
9362 #[test]
9363 fn ruby_condition_slot_pays_what_no_other_arm_counted() {
9364 let src = "def scope
9365 if Foo::Bar then 1 end
9366end
9367def zelf
9368 if self then 1 end
9369end
9370def neg(x)
9371 if -x then 1 end
9372end
9373def defd(x)
9374 if defined?(x) then 1 end
9375end
9376def assign(x)
9377 if (y = x) then y end
9378end
9379def arith(x)
9380 if x + 1 then 1 end
9381end
9382def blk(b)
9383 g if begin b end
9384end
9385def unl
9386 g unless Foo::Bar
9387end
9388def til(x)
9389 g until -x
9390end
9391def whl
9392 while self do g end
9393end
9394def guard(x)
9395 case x
9396 in [y] if -y then 1
9397 end
9398end
9399def tern(x)
9400 -x ? 1 : 2
9401end
9402def ifelse(x)
9403 if -x then 1 else 2 end
9404end
9405def cmp(x)
9406 if x > 1 then 1 end
9407end
9408def begin_cmp(x)
9409 if begin x > 1 end then 1 end
9410end
9411def assign_cmp(x)
9412 if (y = x > 1) then y end
9413end
9414def notted(x)
9415 if !x then 1 end
9416end
9417def chain(a, b)
9418 if a && b then 1 end
9419end
9420def chain_neg(a, b)
9421 if -a && -b then 1 end
9422end
9423";
9424 assert_fixture_spells::<RubyParser>(
9425 src,
9426 "foo.rb",
9427 &[
9428 (Ruby::ScopeResolution2 as u16, 2, "`Foo::Bar`"),
9429 (Ruby::Zelf as u16, 2, "`self`"),
9430 (Ruby::Begin as u16, 2, "`begin … end`"),
9431 (Ruby::Assignment as u16, 2, "parenthesised assignments"),
9432 (Ruby::Conditional as u16, 1, "`tern`'s ternary"),
9433 (Ruby::IfGuard as u16, 1, "`guard`'s guard"),
9434 ],
9435 );
9436 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
9437 assert_members_score(
9438 &space,
9439 &[
9440 // (member, abc.conditions, cyclomatic); each of the
9441 // first thirteen was one lower.
9442 ("scope", 1, 2),
9443 ("zelf", 1, 2),
9444 ("neg", 1, 2),
9445 ("defd", 1, 2),
9446 ("assign", 1, 2),
9447 ("arith", 1, 2),
9448 ("blk", 1, 2),
9449 ("unl", 1, 2),
9450 ("til", 1, 2),
9451 ("whl", 1, 2),
9452 ("guard", 2, 3),
9453 ("tern", 2, 2),
9454 ("ifelse", 2, 2),
9455 ("cmp", 1, 2),
9456 ("begin_cmp", 1, 2),
9457 ("assign_cmp", 1, 2),
9458 ("notted", 1, 2),
9459 ("chain", 2, 3),
9460 // Was 0, then 1 until #1529.
9461 ("chain_neg", 2, 3),
9462 ],
9463 );
9464 });
9465 }
9466
9467 // The other half of #1453: a subject-ful clause must not move. It
9468 // lists a pattern matched with `pattern === subject`, a comparison
9469 // written nowhere, so the clause itself pays for the decision.
9470 // `subj_cmp` is the subject-ful counterpart of `gt` above and keeps
9471 // the clause's count *and* its `>`: `y > 5` is a value compared
9472 // against `x`, two comparisons for one branch. `in_arms` (the
9473 // `InClause` arm), `unl` and `elsif_` share no code with `When` and
9474 // are here to pin that the fix left them alone.
9475 #[cfg(feature = "ruby")]
9476 #[test]
9477 fn ruby_subjectful_case_when_keeps_the_per_clause_count() {
9478 let src = "def konst(x)
9479 case x
9480 when 1 then 1
9481 when 2 then 2
9482 else 0
9483 end
9484end
9485def subj_alts(x)
9486 case x
9487 when 1, 2 then 1
9488 else 0
9489 end
9490end
9491def subj_cmp(x, y)
9492 case x
9493 when y > 5 then 1
9494 end
9495end
9496def in_arms(x)
9497 case x
9498 in Integer then 1
9499 in String then 2
9500 else 0
9501 end
9502end
9503def unl(x)
9504 unless x > 5 then 1 else 0 end
9505end
9506def elsif_(x)
9507 if x > 5 then 1 elsif x < 0 then 2 else 0 end
9508end
9509";
9510 // Three: `in_arms` is a `case_match`, a separate production.
9511 assert_ruby_case_members(
9512 src,
9513 3,
9514 &[
9515 (Ruby::CaseMatch as u16, 1, "`in_arms`'s `case`"),
9516 (Ruby::When as u16, 4, "`when` clauses"),
9517 (Ruby::InClause as u16, 2, "`in_arms`'s clauses"),
9518 (Ruby::GT as u16, 3, "`>` comparisons"),
9519 ],
9520 &[
9521 ("konst", 3, 3),
9522 ("subj_alts", 2, 2),
9523 ("subj_cmp", 2, 2),
9524 ("in_arms", 3, 3),
9525 ("unl", 2, 2),
9526 ("elsif_", 4, 3),
9527 ],
9528 );
9529 }
9530
9531 // ---------------------------------------------------------------
9532 // Default-impl placeholder smoke tests (audited in #188).
9533 //
9534 // These tests assert that the *current* default-impl languages
9535 // return ABC = 0/0/0 for source that DOES contain branches,
9536 // conditions, and assignments. When the real impl lands for any
9537 // of these languages, the corresponding assertion below will fire
9538 // — the implementer must update the expected values, which is the
9539 // gate. Tag the follow-up issue in each test.
9540 // ---------------------------------------------------------------
9541
9542 // --- Python ABC ---------------------------------------------------
9543
9544 #[cfg(feature = "python")]
9545 #[test]
9546 fn python_empty_module_zero() {
9547 check_metrics::<PythonParser>("", "empty.py", |metric| {
9548 assert_eq!(metric.abc.assignments_sum(), 0);
9549 assert_eq!(metric.abc.branches_sum(), 0);
9550 assert_eq!(metric.abc.conditions_sum(), 0);
9551 insta::assert_json_snapshot!(metric.abc);
9552 });
9553 }
9554
9555 #[cfg(feature = "python")]
9556 #[test]
9557 fn python_plain_assignments_count() {
9558 // Three plain `=` assignments → A=3. No branches, no conditions.
9559 check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
9560 assert_eq!(metric.abc.assignments_sum(), 3);
9561 assert_eq!(metric.abc.branches_sum(), 0);
9562 assert_eq!(metric.abc.conditions_sum(), 0);
9563 insta::assert_json_snapshot!(metric.abc);
9564 });
9565 }
9566
9567 #[cfg(feature = "python")]
9568 #[test]
9569 fn python_typed_assignment_counts_bare_annotation_does_not() {
9570 // `x: int = 1` carries an `=`, so it counts.
9571 // `y: int` is a bare annotation (no `=`) — declares a type but
9572 // binds nothing; it must NOT inflate the assignment count.
9573 check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
9574 assert_eq!(metric.abc.assignments_sum(), 1);
9575 insta::assert_json_snapshot!(metric.abc);
9576 });
9577 }
9578
9579 #[cfg(feature = "python")]
9580 #[test]
9581 fn python_augmented_assignments_count() {
9582 // Each augmented op counts once.
9583 check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
9584 // 1 plain `=` + 3 augmented = 4 assignments.
9585 assert_eq!(metric.abc.assignments_sum(), 4);
9586 insta::assert_json_snapshot!(metric.abc);
9587 });
9588 }
9589
9590 #[cfg(feature = "python")]
9591 #[test]
9592 fn python_walrus_counts_as_assignment() {
9593 // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
9594 // → one assignment under Fitzpatrick's rule.
9595 check_metrics::<PythonParser>("if (n := 10) > 5:\n pass\n", "foo.py", |metric| {
9596 // 1 assignment (walrus) + 1 condition (`> 5` is a
9597 // ComparisonOperator).
9598 assert_eq!(metric.abc.assignments_sum(), 1);
9599 assert_eq!(metric.abc.conditions_sum(), 1);
9600 insta::assert_json_snapshot!(metric.abc);
9601 });
9602 }
9603
9604 #[cfg(feature = "python")]
9605 #[test]
9606 fn python_calls_are_branches() {
9607 // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
9608 // → three branches.
9609 check_metrics::<PythonParser>(
9610 "def foo():\n pass\ndef bar():\n pass\nclass Baz:\n pass\nfoo()\nbar()\nBaz()\n",
9611 "foo.py",
9612 |metric| {
9613 assert_eq!(metric.abc.branches_sum(), 3);
9614 assert_eq!(metric.abc.assignments_sum(), 0);
9615 insta::assert_json_snapshot!(metric.abc);
9616 },
9617 );
9618 }
9619
9620 #[cfg(feature = "python")]
9621 #[test]
9622 fn python_comparisons_count_conditions() {
9623 // `x > 0`, `x == y`, `x is None` are each a single
9624 // `ComparisonOperator` node — three conditions.
9625 check_metrics::<PythonParser>(
9626 "def f(x, y):\n a = x > 0\n b = x == y\n c = x is None\n",
9627 "foo.py",
9628 |metric| {
9629 assert_eq!(metric.abc.conditions_sum(), 3);
9630 // 3 plain assignments; the comparisons are operands.
9631 assert_eq!(metric.abc.assignments_sum(), 3);
9632 insta::assert_json_snapshot!(metric.abc);
9633 },
9634 );
9635 }
9636
9637 #[cfg(feature = "python")]
9638 #[test]
9639 fn python_chained_comparison_counts_once() {
9640 // tree-sitter-python collapses `0 < x < 10` into a single
9641 // `ComparisonOperator` — one condition, not two.
9642 check_metrics::<PythonParser>("def f(x):\n return 0 < x < 10\n", "foo.py", |metric| {
9643 assert_eq!(metric.abc.conditions_sum(), 1);
9644 insta::assert_json_snapshot!(metric.abc);
9645 });
9646 }
9647
9648 #[cfg(feature = "python")]
9649 #[test]
9650 fn python_number_truthy_condition_counts() {
9651 // Regression for #772: Python treats every non-zero number as
9652 // truthy, so `if 5:` and `x and 5` should each count their
9653 // numeric literal as a Fitzpatrick unary condition. Pre-fix
9654 // the former `python_bool_terminal_kinds!()` listed `True` /
9655 // `False` but omitted `Integer` / `Float`, so the walker dropped every
9656 // numeric-truthy operand (mirrors the Lua `Number` fix).
9657 check_metrics::<PythonParser>(
9658 "def f(a):\n if 5:\n pass\n return a and 2\n",
9659 "foo.py",
9660 |metric| {
9661 // `if 5:` → walker counts the Integer literal (+1).
9662 // `a and 2` → `and` walker counts both operands:
9663 // identifier `a` (+1), Integer `2` (+1).
9664 // Total: 3.
9665 assert_eq!(metric.abc.conditions_sum(), 3);
9666 insta::assert_json_snapshot!(metric.abc);
9667 },
9668 );
9669 }
9670
9671 #[cfg(feature = "python")]
9672 #[test]
9673 fn python_boolean_operators_not_counted_directly() {
9674 // Python's `and` / `or` are not counted as conditions on
9675 // their own (Fitzpatrick Rule 5; #395). Each operand is
9676 // instead counted as a unary conditional by the walker
9677 // (Rule 9; #403). `if a and b or c:` parses left-to-right
9678 // with `or` lower precedence: `(a and b) or c`. Walker
9679 // tallies: inner `and` counts `a`, `b` (+2); outer `or`
9680 // counts only the new outer operand `c` (+1; the inner
9681 // `(a and b)` BooleanOperator is not a terminal). Total
9682 // C = 3.
9683 check_metrics::<PythonParser>(
9684 "def f(a, b, c):\n if a and b or c:\n pass\n",
9685 "foo.py",
9686 |metric| {
9687 assert_eq!(metric.abc.conditions_sum(), 3);
9688 insta::assert_json_snapshot!(metric.abc);
9689 },
9690 );
9691 }
9692
9693 /// Python's unary `not` operator parses as `NotOperator` and now
9694 /// counts as one condition, matching Java's `!x` rule. Closes
9695 /// the parity gap noted in #214: without this, `if not flag:`
9696 /// reported 0 conditions while the Java equivalent reports 1.
9697 #[cfg(feature = "python")]
9698 #[test]
9699 fn python_unary_not_counts_as_condition() {
9700 check_metrics::<PythonParser>(
9701 "def f(flag):\n if not flag:\n return 1\n return 0\n",
9702 "foo.py",
9703 |metric| {
9704 // One `NotOperator` -> 1 condition. The `if` itself
9705 // is structural and doesn't add an Abc condition.
9706 assert_eq!(metric.abc.conditions_sum(), 1);
9707 insta::assert_json_snapshot!(metric.abc);
9708 },
9709 );
9710 }
9711
9712 /// `return not flag` — the unary `not` is the entire return
9713 /// expression. Without `NotOperator` counted, this reports zero
9714 /// conditions; with it, one. Java's `return !flag;` is one.
9715 #[cfg(feature = "python")]
9716 #[test]
9717 fn python_return_unary_not_counts() {
9718 check_metrics::<PythonParser>("def f(flag):\n return not flag\n", "foo.py", |metric| {
9719 assert_eq!(metric.abc.conditions_sum(), 1);
9720 insta::assert_json_snapshot!(metric.abc);
9721 });
9722 }
9723
9724 /// `foo(not ready, value)` — the unary `not` inside an argument
9725 /// list still contributes. Mirrors Java's `java_count_negated`
9726 /// walk over argument lists.
9727 #[cfg(feature = "python")]
9728 #[test]
9729 fn python_unary_not_in_argument_list_counts() {
9730 check_metrics::<PythonParser>(
9731 "def f(ready, value):\n log(not ready, value)\n",
9732 "foo.py",
9733 |metric| {
9734 // 1 Call (log) -> 1 branch.
9735 // 1 NotOperator (not ready) -> 1 condition.
9736 assert_eq!(metric.abc.branches_sum(), 1);
9737 assert_eq!(metric.abc.conditions_sum(), 1);
9738 insta::assert_json_snapshot!(metric.abc);
9739 },
9740 );
9741 }
9742
9743 /// A negated comparison is one condition. `not (x > 0)` parses as
9744 /// `NotOperator(ParenthesizedExpression(ComparisonOperator))`; the
9745 /// comparison's arm pays and the `not` adds nothing, as Java's
9746 /// `!(x > 0)` scores 1 (#1526). This test asserted 2 on the false
9747 /// premise that Java scores 2.
9748 #[cfg(feature = "python")]
9749 #[test]
9750 fn python_unary_not_with_comparison_counts_each_once() {
9751 check_metrics::<PythonParser>(
9752 "def f(x):\n if not (x > 0):\n return 1\n return 0\n",
9753 "foo.py",
9754 |metric| {
9755 // ComparisonOperator (1); the `not` negates a counted
9756 // comparison and adds nothing.
9757 assert_eq!(metric.abc.conditions_sum(), 1);
9758 insta::assert_json_snapshot!(metric.abc);
9759 },
9760 );
9761 }
9762
9763 /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
9764 /// y)`. The `and` itself is NOT counted (Fitzpatrick Rule 5
9765 /// lists only comparison operators); the `NotOperator` is
9766 /// counted at the top level (Rule 7); and the `y` operand is
9767 /// counted by the Rule 9 walker (issue #403). Total: 2.
9768 /// `NotOperator` is intentionally not walked-into a second
9769 /// time — the walker skips it to avoid double-counting.
9770 #[cfg(feature = "python")]
9771 #[test]
9772 fn python_unary_not_with_boolean_combinator_counts_each() {
9773 check_metrics::<PythonParser>(
9774 "def f(x, y):\n if not x and y:\n return 1\n return 0\n",
9775 "foo.py",
9776 |metric| {
9777 // NotOperator (1) + walker on `and` finds `y` (1) = 2.
9778 assert_eq!(metric.abc.conditions_sum(), 2);
9779 insta::assert_json_snapshot!(metric.abc);
9780 },
9781 );
9782 }
9783
9784 #[cfg(feature = "python")]
9785 #[test]
9786 fn python_control_flow_arms_count_conditions() {
9787 // `elif`, `else`, `except`, `finally`, `case` each contribute
9788 // one condition. The comparisons in the `if`/`elif`/`while`
9789 // headers contribute their own ComparisonOperator counts.
9790 check_metrics::<PythonParser>(
9791 "def f(x):\n if x > 0:\n a = 1\n elif x > -1:\n a = 2\n else:\n a = 3\n",
9792 "foo.py",
9793 |metric| {
9794 // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
9795 // ElifClause + 1 ElseClause = 4 conditions.
9796 assert_eq!(metric.abc.conditions_sum(), 4);
9797 insta::assert_json_snapshot!(metric.abc);
9798 },
9799 );
9800 }
9801
9802 #[cfg(feature = "python")]
9803 #[test]
9804 fn python_ternary_counts_as_condition() {
9805 // `a if c else b` is `ConditionalExpression` → 1 condition.
9806 // `c > 0` adds 1 more (ComparisonOperator).
9807 check_metrics::<PythonParser>(
9808 "def f(c):\n return 1 if c > 0 else 0\n",
9809 "foo.py",
9810 |metric| {
9811 assert_eq!(metric.abc.conditions_sum(), 2);
9812 insta::assert_json_snapshot!(metric.abc);
9813 },
9814 );
9815 }
9816
9817 // Issue #1161. Python counted the `conditional_expression` node but
9818 // never its condition slot, so `a if c() else b` reported 1 where
9819 // the equivalent `c() ? a : b` reports 2 everywhere else — and
9820 // the former `python_inspect_container`'s `ConditionalExpression`
9821 // boolean-context seed was unreachable, no call site having passed
9822 // that parent.
9823 #[cfg(feature = "python")]
9824 #[test]
9825 fn python_ternary_condition_slot_counts_as_a_unary_condition() {
9826 // ternary (1) + condition `c()` (1) = 2. `c()` is a `Call`, a
9827 // boolean terminal; it also adds one *branch*, not a condition.
9828 check_metrics::<PythonParser>(
9829 "def f(a, b, c):\n return a if c() else b\n",
9830 "foo.py",
9831 |metric| {
9832 assert_eq!(metric.abc.conditions_sum(), 2);
9833 },
9834 );
9835 // A parenthesised condition: `(c)` is a `parenthesized_expression`,
9836 // so only the peel (`python_wrapper_operand`) can resolve it to
9837 // `c`, which the slot then pays for.
9838 // ternary (1) + `(c)` (1) = 2.
9839 check_metrics::<PythonParser>(
9840 "def f(a, b, c):\n return a if (c) else b\n",
9841 "foo.py",
9842 |metric| {
9843 assert_eq!(metric.abc.conditions_sum(), 2);
9844 },
9845 );
9846 // A negated condition is *not* counted by the new slot: it is a
9847 // `NotOperator`, which already has its own top-level dispatcher
9848 // arm. ternary (1) + `not c` (1) = 2, not 3.
9849 check_metrics::<PythonParser>(
9850 "def f(a, b, c):\n return a if not c else b\n",
9851 "foo.py",
9852 |metric| {
9853 assert_eq!(metric.abc.conditions_sum(), 2);
9854 },
9855 );
9856 // The cross-language reference case. `(not b) if a else (not c)`
9857 // is the exact semantic equivalent of `a ? !b : !c`, which every
9858 // other language reports as 4: ternary (1) + condition `a` (1) +
9859 // two `NotOperator`s (2). The negated operands come from
9860 // Python's own arm, the condition from the slot added here.
9861 //
9862 // #1161's resolution plan predicted this would stay 3, having
9863 // measured the condition slot's contribution against the pre-fix
9864 // total. 3 would have left Python disagreeing with every other
9865 // language on the reference expression — the gap the issue was
9866 // filed about.
9867 check_metrics::<PythonParser>(
9868 "def f(a, b, c):\n return (not b) if a else (not c)\n",
9869 "foo.py",
9870 |metric| {
9871 assert_eq!(metric.abc.conditions_sum(), 4);
9872 },
9873 );
9874 }
9875
9876 // The double-count pin, and the reason Python gets a condition-slot
9877 // helper rather than a copy of `cpp_walk_ternary`: Python's branch
9878 // operands are counted by the top-level `NotOperator` /
9879 // `ComparisonOperator` arms, a different mechanism from every other
9880 // language's walker. Routing the branch slots through a negated-
9881 // operand walk as the C family does (`python_inspect_container`
9882 // before #1526) would have counted a parenthesised operand that
9883 // the identical unparenthesised expression scores at zero.
9884 //
9885 // Both fixtures below are 2 today and were 4 under such a copy, so a
9886 // later "make Python consistent with the others" change cannot land
9887 // silently.
9888 #[cfg(feature = "python")]
9889 #[test]
9890 fn python_ternary_branch_operands_are_not_double_counted() {
9891 // ternary (1) + condition `a` (1) = 2. The two parenthesised
9892 // operands add nothing — an unnegated branch is type-free.
9893 check_metrics::<PythonParser>(
9894 "def f(a, b, c):\n return (b) if a else (c)\n",
9895 "foo.py",
9896 |metric| {
9897 assert_eq!(metric.abc.conditions_sum(), 2);
9898 },
9899 );
9900 // The unparenthesised form must agree: nothing about `(b)`
9901 // versus `b` is a condition.
9902 check_metrics::<PythonParser>(
9903 "def f(a, b, c):\n return b if a else c\n",
9904 "foo.py",
9905 |metric| {
9906 assert_eq!(metric.abc.conditions_sum(), 2);
9907 },
9908 );
9909 }
9910
9911 // Comments are tree-sitter `extras`, so they arrive as direct
9912 // children of `conditional_expression` and shift every positional
9913 // index after them. `python_count_ternary_condition` therefore
9914 // anchors on the `if` keyword and skips comments after it; both
9915 // halves are needed and each fixture below fails without one.
9916 #[cfg(feature = "python")]
9917 #[test]
9918 fn python_ternary_condition_survives_an_interposed_comment() {
9919 // Comment before the keyword: `child(2)` is the `if` token here,
9920 // so a positional lookup reads 1. ternary (1) + `f()` (1) = 2.
9921 check_metrics::<PythonParser>(
9922 "def f(b, c):\n return (b\n # why\n if f() else c)\n",
9923 "foo.py",
9924 |metric| {
9925 assert_eq!(metric.abc.conditions_sum(), 2);
9926 },
9927 );
9928 // Comment after the keyword: the child immediately following
9929 // `if` is the comment, so taking the first rather than the first
9930 // non-comment reads 1. ternary (1) + `f()` (1) = 2.
9931 check_metrics::<PythonParser>(
9932 "def f(b, c):\n return (b if\n # why\n f() else c)\n",
9933 "foo.py",
9934 |metric| {
9935 assert_eq!(metric.abc.conditions_sum(), 2);
9936 },
9937 );
9938 }
9939
9940 #[cfg(feature = "python")]
9941 #[test]
9942 fn python_try_except_finally_count_conditions() {
9943 // ExceptClause + FinallyClause → 2 conditions.
9944 check_metrics::<PythonParser>(
9945 "def f():\n try:\n pass\n except ValueError:\n pass\n finally:\n pass\n",
9946 "foo.py",
9947 |metric| {
9948 assert_eq!(metric.abc.conditions_sum(), 2);
9949 insta::assert_json_snapshot!(metric.abc);
9950 },
9951 );
9952 }
9953
9954 #[cfg(feature = "python")]
9955 #[test]
9956 fn python_match_case_counts_conditions() {
9957 // Each non-wildcard `CaseClause` → 1 condition. The bare
9958 // `case _:` arm is the language-neutral `default:` equivalent
9959 // and is excluded (matches Rust's bare-`_` MatchArm filter and
9960 // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
9961 // `case _:` (excluded) → C = 1.
9962 check_metrics::<PythonParser>(
9963 "def f(x):\n match x:\n case 1:\n pass\n case _:\n pass\n",
9964 "foo.py",
9965 |metric| {
9966 assert_eq!(metric.abc.conditions_sum(), 1);
9967 insta::assert_json_snapshot!(metric.abc);
9968 },
9969 );
9970 }
9971
9972 #[cfg(feature = "python")]
9973 #[test]
9974 fn python_match_case_guarded_wildcard_counts() {
9975 // `case _ if g:` is NOT a bare wildcard — the guard
9976 // contributes real branching, so the arm counts as a
9977 // condition. Mirrors Rust's `_ if g => ...` behavior.
9978 // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
9979 // wildcard, counts) + `case _:` (bare wildcard, excluded) →
9980 // C from CaseClause = 2; the guard's `x > 0` adds one
9981 // ComparisonOperator → total C = 3.
9982 check_metrics::<PythonParser>(
9983 "def f(x):\n match x:\n case 1:\n pass\n case _ if x > 0:\n pass\n case _:\n pass\n",
9984 "foo.py",
9985 |metric| {
9986 assert_eq!(metric.abc.conditions_sum(), 3);
9987 insta::assert_json_snapshot!(metric.abc);
9988 },
9989 );
9990 }
9991
9992 #[cfg(feature = "python")]
9993 #[test]
9994 fn python_complex_function_abc() {
9995 // Mixed-shape regression: assignments, calls, conditions all in
9996 // a single function.
9997 check_metrics::<PythonParser>(
9998 "def f(items, threshold):\n\
9999 \x20 result = []\n\
10000 \x20 for item in items:\n\
10001 \x20 if item > threshold:\n\
10002 \x20 result.append(item)\n\
10003 \x20 return result\n",
10004 "foo.py",
10005 |metric| {
10006 // assignments: `result = []` → 1
10007 // branches: `result.append(item)` is one call → 1
10008 // conditions: `item > threshold` is one
10009 // ComparisonOperator → 1
10010 assert_eq!(metric.abc.assignments_sum(), 1);
10011 assert_eq!(metric.abc.branches_sum(), 1);
10012 assert_eq!(metric.abc.conditions_sum(), 1);
10013 insta::assert_json_snapshot!(metric.abc);
10014 },
10015 );
10016 }
10017
10018 #[cfg(feature = "python")]
10019 #[test]
10020 fn python_if_multiple_conditions() {
10021 // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403).
10022 // - `if a or b or c or d:` → 4 (each operand counted once)
10023 // - `if a and b and c:` → 3
10024 // - `if not a and not b:` → 2 (two `NotOperator`s counted
10025 // by the top-level dispatcher arm; the walker SKIPS
10026 // `NotOperator` children to avoid double-counting)
10027 // Total: 4 + 3 + 2 = 9.
10028 check_metrics::<PythonParser>(
10029 "def f(a, b, c, d):\n\
10030 \x20 if a or b or c or d: # +4c\n\
10031 \x20 pass\n\
10032 \x20 if a and b and c: # +3c\n\
10033 \x20 pass\n\
10034 \x20 if not a and not b: # +2c (NotOperator x2)\n\
10035 \x20 pass\n",
10036 "foo.py",
10037 |metric| {
10038 assert_eq!(metric.abc.conditions_sum(), 9);
10039 insta::assert_json_snapshot!(metric.abc);
10040 },
10041 );
10042 }
10043
10044 #[cfg(feature = "python")]
10045 #[test]
10046 fn python_while_conditions() {
10047 // Python has no `do { ... } while(cond);` construct, so this
10048 // mirrors only the `while` half of the Java suite. The
10049 // walker fires on each `and` / `or` token inside the loop
10050 // header.
10051 check_metrics::<PythonParser>(
10052 "def f(a, b):\n\
10053 \x20 while a or b: # +2c\n\
10054 \x20 break\n\
10055 \x20 while a and not b: # +2c (a + NotOperator)\n\
10056 \x20 break\n",
10057 "foo.py",
10058 |metric| {
10059 assert_eq!(metric.abc.conditions_sum(), 4);
10060 insta::assert_json_snapshot!(metric.abc);
10061 },
10062 );
10063 }
10064
10065 #[cfg(feature = "python")]
10066 #[test]
10067 fn python_short_circuit_with_boolean_literal_operand() {
10068 // `a and True` reports 2 conditions: one identifier, one
10069 // True literal. Confirms `True` / `False` are in the walker
10070 // terminal set.
10071 check_metrics::<PythonParser>("def f(a):\n return a and True\n", "foo.py", |metric| {
10072 assert_eq!(metric.abc.conditions_sum(), 2);
10073 insta::assert_json_snapshot!(metric.abc);
10074 });
10075 }
10076
10077 #[cfg(feature = "python")]
10078 #[test]
10079 fn python_await_expression_condition_counts() {
10080 // Regression for findings.md round-2 #2 (Python):
10081 // `if await ready(): pass` parses with `await` as the
10082 // condition node. Adding `Python::Await` to the
10083 // terminal-bool set mirrors the C# reference (lesson 19).
10084 check_metrics::<PythonParser>(
10085 "async def ready(): return True\n\
10086 async def f():\n if await ready(): pass\n",
10087 "foo.py",
10088 |metric| {
10089 // ready() is a call (1 branch); await is the
10090 // condition (1).
10091 assert_eq!(metric.abc.branches_sum(), 1);
10092 assert_eq!(metric.abc.conditions_sum(), 1);
10093 insta::assert_json_snapshot!(metric.abc);
10094 },
10095 );
10096 }
10097
10098 #[cfg(feature = "python")]
10099 #[test]
10100 fn python_if_call_terminal_condition_counts_once() {
10101 // Pins the Phase-2B behaviour for Python's `Call` terminal-bool
10102 // kind: `if foo():` is a Fitzpatrick Rule 6 unary conditional
10103 // (a bare boolean-evaluating call as the if-condition). The
10104 // walker's terminal-at-top check fires once per call-condition;
10105 // the call itself separately contributes 1 branch. Surfaced
10106 // (and verified intentional) by the code-review pass on
10107 // Phase 2B.
10108 check_metrics::<PythonParser>("def f():\n if foo(): pass\n", "foo.py", |metric| {
10109 assert_eq!(metric.abc.branches_sum(), 1);
10110 assert_eq!(metric.abc.conditions_sum(), 1);
10111 insta::assert_json_snapshot!(metric.abc);
10112 });
10113 }
10114
10115 #[cfg(feature = "python")]
10116 #[test]
10117 fn python_if_boolean_literal_condition() {
10118 // Phase 2B (issue #403): bare-boolean conditions count once.
10119 // Python has no paren wrap around if-conditions, so the
10120 // condition node is checked directly. The existing
10121 // NotOperator / ComparisonOperator arms continue to fire
10122 // for those shapes; only the bare-terminal cases (Identifier,
10123 // True, False, etc.) are added by the new arm.
10124 check_metrics::<PythonParser>(
10125 "def f(a):\n\
10126 \x20 if True: pass # +1c\n\
10127 \x20 if False: pass # +1c\n\
10128 \x20 while True: break # +1c\n\
10129 \x20 if a: pass # +1c (Rule 6 — bare identifier as condition)\n",
10130 "foo.py",
10131 |metric| {
10132 assert_eq!(metric.abc.conditions_sum(), 4);
10133 insta::assert_json_snapshot!(metric.abc);
10134 },
10135 );
10136 }
10137
10138 #[cfg(feature = "python")]
10139 #[test]
10140 fn python_methods_arguments_with_conditions() {
10141 // `m(not a, not b)` reports 2 conditions — both `NotOperator`
10142 // nodes are counted by Python's pre-existing top-level
10143 // NotOperator dispatcher arm. The argument-list walker does
10144 // not need a separate Python arm.
10145 check_metrics::<PythonParser>(
10146 "def f(a, b):\n\
10147 \x20 m(a, b) # +1b\n\
10148 \x20 m(not a, not b) # +1b +2c\n",
10149 "foo.py",
10150 |metric| {
10151 assert_eq!(metric.abc.branches_sum(), 2);
10152 assert_eq!(metric.abc.conditions_sum(), 2);
10153 insta::assert_json_snapshot!(metric.abc);
10154 },
10155 );
10156 }
10157
10158 #[cfg(feature = "python")]
10159 #[test]
10160 fn python_return_with_conditions() {
10161 // Phase 2B (issue #403). Python uses the pre-existing top-
10162 // level NotOperator / ComparisonOperator arms for return
10163 // expressions; no dedicated ReturnStatement walker arm is
10164 // needed.
10165 check_metrics::<PythonParser>(
10166 "def m1(z): return not (z >= 0)\n\
10167 def m2(x): return (((not x)))\n\
10168 def m3(x, y): return x and y\n",
10169 "foo.py",
10170 |metric| {
10171 // m1: ComparisonOperator (1); the `not` adds nothing,
10172 // as Java's `return !(z >= 0)` scores 1 (#1526).
10173 // m2: NotOperator (1).
10174 // m3: walker on `and` counts both operands = 2.
10175 // Sum: 4.
10176 assert_eq!(metric.abc.conditions_sum(), 4);
10177 insta::assert_json_snapshot!(metric.abc);
10178 },
10179 );
10180 }
10181
10182 #[cfg(feature = "rust")]
10183 #[test]
10184 fn rust_empty_unit_zero() {
10185 // No code at all → A=B=C=0. Establishes the trait is wired up
10186 // and the per-language compute is reachable.
10187 check_metrics::<RustParser>("", "empty.rs", |metric| {
10188 assert_eq!(metric.abc.assignments_sum(), 0);
10189 assert_eq!(metric.abc.branches_sum(), 0);
10190 assert_eq!(metric.abc.conditions_sum(), 0);
10191 insta::assert_json_snapshot!(metric.abc);
10192 });
10193 }
10194
10195 #[cfg(feature = "rust")]
10196 #[test]
10197 fn rust_assignments_let_init_plain_and_compound() {
10198 // `let mut x = 0` is a `let_declaration` carrying an `=`
10199 // initializer → counts as 1 (matches Fitzpatrick's literal
10200 // "every `=` is an assignment" rule and the JS impl's
10201 // treatment of `let x = 5`). `x = 5` and `x = 7` are plain
10202 // `=` assignments → 2. `x += 2` is a compound assignment → 1.
10203 // Total A = 4.
10204 check_metrics::<RustParser>(
10205 "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
10206 "foo.rs",
10207 |metric| {
10208 assert_eq!(metric.abc.assignments_sum(), 4);
10209 assert_eq!(metric.abc.branches_sum(), 0);
10210 assert_eq!(metric.abc.conditions_sum(), 0);
10211 insta::assert_json_snapshot!(metric.abc);
10212 },
10213 );
10214 }
10215
10216 #[cfg(feature = "rust")]
10217 #[test]
10218 fn rust_let_without_initializer_does_not_count() {
10219 // `let a;` is a `let_declaration` with NO `=` and no `value`
10220 // field — the binding is uninitialised. The arm only fires
10221 // when `value` is present, so this contributes zero to A.
10222 // `let _b;` is the same shape (the `_` pattern is still a
10223 // pattern, not a wildcard suppression of the binding).
10224 // Regression test for issue #393: only `=` counts, not the
10225 // bare declaration.
10226 check_metrics::<RustParser>(
10227 "fn f() { let a: i32; let _b: i32; a = 5; }",
10228 "foo.rs",
10229 |metric| {
10230 // Only `a = 5` (assignment_expression) → A = 1.
10231 assert_eq!(metric.abc.assignments_sum(), 1);
10232 insta::assert_json_snapshot!(metric.abc);
10233 },
10234 );
10235 }
10236
10237 #[cfg(feature = "rust")]
10238 #[test]
10239 fn rust_let_initializers_immutable_and_mutable_count() {
10240 // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`,
10241 // `let mut d = 0;` are all `let_declaration` nodes carrying
10242 // an `=` initializer — each counts as 1 (Option B in the
10243 // issue body: literal Fitzpatrick, both `let` and `let mut`
10244 // count). `d = 5;` is one plain assignment_expression, `d
10245 // += 1;` is one compound. Total A = 4 + 1 + 1 = 6.
10246 check_metrics::<RustParser>(
10247 "fn f() { let a=1; let b=2; let c=a+b; let mut d=0; d=5; d+=1; }",
10248 "foo.rs",
10249 |metric| {
10250 assert_eq!(metric.abc.assignments_sum(), 6);
10251 insta::assert_json_snapshot!(metric.abc);
10252 },
10253 );
10254 }
10255
10256 #[cfg(feature = "rust")]
10257 #[test]
10258 fn rust_calls_are_branches() {
10259 // Free function call + method call (parses as call_expression
10260 // with a field_expression callee) + associated-fn call. All
10261 // three are `call_expression` → B = 3. Macro invocations like
10262 // `println!` parse as `macro_invocation`, NOT `call_expression`,
10263 // so they are not branches.
10264 check_metrics::<RustParser>(
10265 "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
10266 "foo.rs",
10267 |metric| {
10268 assert_eq!(metric.abc.branches_sum(), 3);
10269 assert_eq!(metric.abc.assignments_sum(), 0);
10270 assert_eq!(metric.abc.conditions_sum(), 0);
10271 insta::assert_json_snapshot!(metric.abc);
10272 },
10273 );
10274 }
10275
10276 #[cfg(feature = "rust")]
10277 #[test]
10278 fn rust_try_operator_is_branch() {
10279 // `?` parses as `try_expression` and counts as one branch
10280 // (short-circuit return on Err / None). The `Err(())` call
10281 // contributes one branch in addition (call_expression).
10282 check_metrics::<RustParser>(
10283 "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
10284 "foo.rs",
10285 |metric| {
10286 // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
10287 assert_eq!(metric.abc.branches_sum(), 3);
10288 insta::assert_json_snapshot!(metric.abc);
10289 },
10290 );
10291 }
10292
10293 #[cfg(feature = "rust")]
10294 #[test]
10295 fn rust_comparisons_count_conditions() {
10296 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
10297 // comparisons → C = 6.
10298 check_metrics::<RustParser>(
10299 "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
10300 "foo.rs",
10301 |metric| {
10302 assert_eq!(metric.abc.conditions_sum(), 6);
10303 insta::assert_json_snapshot!(metric.abc);
10304 },
10305 );
10306 }
10307
10308 #[cfg(feature = "rust")]
10309 #[test]
10310 fn rust_generic_brackets_not_conditions() {
10311 // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
10312 // comparison operators. The parent-check in the LT/GT arms
10313 // must filter them out. Expected C = 0.
10314 check_metrics::<RustParser>(
10315 "fn f() -> Vec<i32> { Vec::<i32>::new() }",
10316 "foo.rs",
10317 |metric| {
10318 assert_eq!(metric.abc.conditions_sum(), 0);
10319 insta::assert_json_snapshot!(metric.abc);
10320 },
10321 );
10322 }
10323
10324 #[cfg(feature = "rust")]
10325 #[test]
10326 fn rust_if_let_counts_as_condition() {
10327 // `if let Some(v) = opt { ... }` introduces a `let_condition`
10328 // → 1 condition. The `if` keyword itself does not add another
10329 // count — Fitzpatrick counts conditions, not branch keywords.
10330 check_metrics::<RustParser>(
10331 "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
10332 "foo.rs",
10333 |metric| {
10334 assert_eq!(metric.abc.conditions_sum(), 1);
10335 insta::assert_json_snapshot!(metric.abc);
10336 },
10337 );
10338 }
10339
10340 #[cfg(feature = "rust")]
10341 #[test]
10342 fn rust_while_let_counts_as_condition() {
10343 // `while let Some(y) = it.next() { ... }` is also a
10344 // `let_condition` (the `while` form). One condition; the
10345 // `it.next()` call adds one branch.
10346 check_metrics::<RustParser>(
10347 "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
10348 "foo.rs",
10349 |metric| {
10350 assert_eq!(metric.abc.conditions_sum(), 1);
10351 assert_eq!(metric.abc.branches_sum(), 1);
10352 insta::assert_json_snapshot!(metric.abc);
10353 },
10354 );
10355 }
10356
10357 #[cfg(feature = "rust")]
10358 #[test]
10359 fn rust_match_arms_count_conditions_wildcard_excluded() {
10360 // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
10361 // bare wildcard is the `default:` equivalent and is skipped.
10362 // The guarded arm has a `n if n > 0` pattern (more than one
10363 // child in the match_pattern) and still counts. Two non-wildcard
10364 // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
10365 // adds one more → C = 3.
10366 check_metrics::<RustParser>(
10367 "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
10368 "foo.rs",
10369 |metric| {
10370 assert_eq!(metric.abc.conditions_sum(), 3);
10371 insta::assert_json_snapshot!(metric.abc);
10372 },
10373 );
10374 }
10375
10376 #[cfg(feature = "rust")]
10377 #[test]
10378 fn rust_else_counts_as_condition() {
10379 // `if a > b { ... } else { ... }` → `a > b` is one condition,
10380 // `else` is one condition → C = 2.
10381 check_metrics::<RustParser>(
10382 "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
10383 "foo.rs",
10384 |metric| {
10385 assert_eq!(metric.abc.conditions_sum(), 2);
10386 insta::assert_json_snapshot!(metric.abc);
10387 },
10388 );
10389 }
10390
10391 #[cfg(feature = "rust")]
10392 #[test]
10393 fn rust_let_chain2_hidden_rule_drift_marker() {
10394 // Drift marker (findings.md round-2 #3): `Rust::LetChain2`
10395 // maps to the hidden grammar rule `_let_chain`. At the
10396 // pinned tree-sitter-rust version it is never emitted as a
10397 // concrete node — the visible `LetChain` (= 352) carries
10398 // every let-chain. We list `LetChain2` defensively in
10399 // `rust_condition_scores_itself` and `rust_count_chain_operands`
10400 // (lesson 34); if a future grammar bump promotes
10401 // `_let_chain` to a visible rule, this assertion fails
10402 // loudly so the maintainer knows to verify the walker still
10403 // counts correctly for the new shape.
10404 let src = "fn f(a: bool, b: Option<i32>) {\n\
10405 \x20 if a && let Some(_) = b { }\n\
10406 }\n";
10407 let parser = RustParser::new(
10408 src.as_bytes().to_vec(),
10409 &std::path::PathBuf::from("foo.rs"),
10410 None,
10411 );
10412 assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16));
10413 }
10414
10415 #[cfg(feature = "rust")]
10416 #[test]
10417 fn rust_scoped_identifier_condition_counts() {
10418 // Regression for findings.md round-2 #1 (Rust):
10419 // `if crate::FLAG {}` parses with `scoped_identifier` as the
10420 // condition node. Pre-fix, the former `rust_bool_terminal_kinds!()`
10421 // listed only `Identifier` so the walker reached the
10422 // `scoped_identifier` child, found it non-terminal /
10423 // non-paren / non-unary, and broke without counting.
10424 // Mirrors the C# fix in #372 (lesson 19) for
10425 // `MemberAccessExpression`.
10426 check_metrics::<RustParser>("fn f() { if crate::FLAG { } }\n", "foo.rs", |metric| {
10427 assert_eq!(metric.abc.conditions_sum(), 1);
10428 insta::assert_json_snapshot!(metric.abc);
10429 });
10430 }
10431
10432 #[cfg(feature = "rust")]
10433 #[test]
10434 fn rust_await_expression_condition_counts() {
10435 // Regression for findings.md round-2 #2 (Rust):
10436 // `if ready().await {}` parses with `await_expression` as
10437 // the condition node. Adding `Rust::AwaitExpression` to the
10438 // former terminal-bool set closed the parity gap with the C#
10439 // reference (the former `csharp_bool_terminal_kinds!()`).
10440 check_metrics::<RustParser>(
10441 "async fn ready() -> bool { true }\n\
10442 async fn f() { if ready().await { } }\n",
10443 "foo.rs",
10444 |metric| {
10445 // ready() is a call (1 branch); `ready().await` is
10446 // the unary boolean condition (1).
10447 assert_eq!(metric.abc.branches_sum(), 1);
10448 assert_eq!(metric.abc.conditions_sum(), 1);
10449 insta::assert_json_snapshot!(metric.abc);
10450 },
10451 );
10452 }
10453
10454 #[cfg(feature = "rust")]
10455 #[test]
10456 fn rust_complex_function_abc() {
10457 // Mixed-shape regression: assignments, calls, conditions, `?`,
10458 // `if let`, `match` in one body. Verified by hand:
10459 // - assignments: `let mut x = 0` (let init), `x = 5`, `x += 2`,
10460 // `let _ = ...` (let init), `let r: ... = Err(())` (let init),
10461 // `let _v = r?` (let init) → A = 6 (post-#393: every `=`
10462 // initializer in a `let_declaration` is one assignment, in
10463 // line with the literal Fitzpatrick reading).
10464 // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
10465 // (3 calls + 1 try).
10466 // - conditions: `if let Some(v) = opt` → 1, `match x` arms
10467 // `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
10468 // Total C = 4.
10469 check_metrics::<RustParser>(
10470 "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
10471 \x20 let mut x = 0;\n\
10472 \x20 x = 5;\n\
10473 \x20 x += 2;\n\
10474 \x20 if let Some(_v) = opt { }\n\
10475 \x20 let _ = xs.iter().next();\n\
10476 \x20 let r: Result<i32, ()> = Err(());\n\
10477 \x20 let _v = r?;\n\
10478 \x20 Ok(match x {\n\
10479 \x20 0 => 1,\n\
10480 \x20 n if n > 0 => n,\n\
10481 \x20 _ => -1,\n\
10482 \x20 })\n\
10483 }\n",
10484 "foo.rs",
10485 |metric| {
10486 assert_eq!(metric.abc.assignments_sum(), 6);
10487 // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
10488 // plus 1 try (`r?`) → 5 branches.
10489 assert_eq!(metric.abc.branches_sum(), 5);
10490 // 1 let_condition + 2 non-wildcard match_arms + 1
10491 // comparison (`n > 0`) → 4.
10492 assert_eq!(metric.abc.conditions_sum(), 4);
10493 insta::assert_json_snapshot!(metric.abc);
10494 },
10495 );
10496 }
10497
10498 #[cfg(feature = "rust")]
10499 #[test]
10500 fn rust_let_chain_bare_identifier_operand_counts() {
10501 // Regression: pre-fix, `if a && let Some(_z) = y { }` reported
10502 // 1 condition (only the LetCondition). The bare-identifier
10503 // `a` operand was lost because Rust 2024 wraps let-chain
10504 // `&&` operands in a `LetChain` node (not `BinaryExpression`)
10505 // and the former `rust_count_unary_conditions` only counted
10506 // terminals under a `BinaryExpression` parent. Allowing `LetChain` /
10507 // `LetChain2` as known-bool list parents fixed the loss.
10508 // Expected: LetCondition (1) + walker on `a` (1) = 2.
10509 check_metrics::<RustParser>(
10510 "fn f(a: bool, y: Option<i32>) {\n\
10511 \x20 if a && let Some(_z) = y { }\n\
10512 }\n",
10513 "foo.rs",
10514 |metric| {
10515 assert_eq!(metric.abc.conditions_sum(), 2);
10516 insta::assert_json_snapshot!(metric.abc);
10517 },
10518 );
10519 }
10520
10521 #[cfg(feature = "rust")]
10522 #[test]
10523 fn rust_if_multiple_conditions() {
10524 // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of
10525 // a `&&` / `||` chain is one condition. Mirrors
10526 // `java_if_multiple_conditions`. Rust's `if` head has no
10527 // parentheses, but the walker fires on each `&&` / `||` token
10528 // and walks the parent `binary_expression` regardless.
10529 check_metrics::<RustParser>(
10530 "fn f(a: bool, b: bool, c: bool, d: bool) -> i32 {\n\
10531 \x20 if a || b || c || d { return 1; } // +4c\n\
10532 \x20 if a && b && c { return 2; } // +3c\n\
10533 \x20 if !a && !b { return 3; } // +2c\n\
10534 \x20 0\n\
10535 }\n",
10536 "foo.rs",
10537 |metric| {
10538 // 4 + 3 + 2 = 9
10539 assert_eq!(metric.abc.conditions_sum(), 9);
10540 insta::assert_json_snapshot!(metric.abc);
10541 },
10542 );
10543 }
10544
10545 #[cfg(feature = "rust")]
10546 #[test]
10547 fn rust_while_conditions() {
10548 // Rust has no `do { ... } while(cond);` construct, so this
10549 // mirrors only the `while` half of `java_while_and_do_while_conditions`.
10550 // Each operand of the `&&` / `||` chain in the loop condition
10551 // counts as one Fitzpatrick condition (Rule 7).
10552 check_metrics::<RustParser>(
10553 "fn f(a: bool, b: bool) {\n\
10554 \x20 while a || b { break; } // +2c\n\
10555 \x20 while a && !b { break; } // +2c\n\
10556 }\n",
10557 "foo.rs",
10558 |metric| {
10559 assert_eq!(metric.abc.conditions_sum(), 4);
10560 insta::assert_json_snapshot!(metric.abc);
10561 },
10562 );
10563 }
10564
10565 #[cfg(feature = "rust")]
10566 #[test]
10567 fn rust_if_boolean_literal_condition() {
10568 // Phase 2B (issue #403): a condition whose entire body is a
10569 // boolean literal counts as one Fitzpatrick condition.
10570 // `if true {}` → 1, `if !false {}` → 1 (unary unwrap), and
10571 // `while true { break }` → 1.
10572 check_metrics::<RustParser>(
10573 "fn f() {\n\
10574 \x20 if true { } // +1c\n\
10575 \x20 if !false { } // +1c\n\
10576 \x20 while true { break; } // +1c\n\
10577 }\n",
10578 "foo.rs",
10579 |metric| {
10580 assert_eq!(metric.abc.conditions_sum(), 3);
10581 insta::assert_json_snapshot!(metric.abc);
10582 },
10583 );
10584 }
10585
10586 #[cfg(feature = "rust")]
10587 #[test]
10588 fn rust_methods_arguments_with_conditions() {
10589 // Phase 2B (issue #403): unary-conditional arguments to a
10590 // call each count once. `m(!a, !b)` → 2 conditions + 1
10591 // branch (the call itself). Bare identifier arguments do
10592 // NOT count (they reach the count_unary_conditions list with
10593 // list_kind = Arguments, not BinaryExpression).
10594 check_metrics::<RustParser>(
10595 "fn f(a: bool, b: bool) {\n\
10596 \x20 m(a, b); // +1b\n\
10597 \x20 m(!a, !b); // +1b +2c\n\
10598 \x20 m(!a, b, !a); // +1b +2c\n\
10599 }\n",
10600 "foo.rs",
10601 |metric| {
10602 assert_eq!(metric.abc.branches_sum(), 3);
10603 assert_eq!(metric.abc.conditions_sum(), 4);
10604 insta::assert_json_snapshot!(metric.abc);
10605 },
10606 );
10607 }
10608
10609 #[cfg(feature = "rust")]
10610 #[test]
10611 fn rust_return_with_conditions() {
10612 // Phase 2B (issue #403). Mirrors `java_return_with_conditions`
10613 // — `return !a` / `return x && y` count their unary
10614 // conditional operands. Per Fitzpatrick Rule 7, a `!`-wrapped
10615 // relational expression contributes ONE condition (the
10616 // relational op itself) — the `!` does not add a second
10617 // count when its operand is already a comparison.
10618 check_metrics::<RustParser>(
10619 "fn m1(z: i32) -> bool { return !(z >= 0); }\n\
10620 fn m2(x: bool) -> bool { return (((!x))); }\n\
10621 fn m3(x: bool, y: bool) -> bool { return x && y; }\n\
10622 fn m4(y: bool, z: i32) -> bool { return y || (z < 0); }\n",
10623 "foo.rs",
10624 |metric| {
10625 // m1: !(z >= 0) → the `>=` contributes 1; the unary
10626 // `!` wraps a paren'd BinaryExpression, which
10627 // inspect_container does not unwrap further →
10628 // no walker count. Total: 1.
10629 // m2: (((!x))) → ReturnExpression arm walks (((!x))).
10630 // inspect_container unwraps three parens and one
10631 // unary, reaches Identifier `x`, has_boolean_content
10632 // was seeded true by the unary-not flip. +1.
10633 // m3: x && y → `&&` walker counts both terminals → 2.
10634 // m4: y || (z < 0) → `||` walker counts `y` (terminal,
10635 // +1); the `<` contributes 1 via its own arm; the
10636 // paren'd BinaryExpression `(z < 0)` is not
10637 // terminal under the walker → no extra count.
10638 // Total: 2.
10639 // Sum: 1 + 1 + 2 + 2 = 6.
10640 assert_eq!(metric.abc.conditions_sum(), 6);
10641 insta::assert_json_snapshot!(metric.abc);
10642 },
10643 );
10644 }
10645
10646 #[cfg(feature = "rust")]
10647 #[test]
10648 fn rust_short_circuit_with_boolean_literal_operand() {
10649 // `if a && true` reports 2 conditions: one for the identifier
10650 // operand, one for the boolean-literal operand. Confirms the
10651 // walker terminal set includes `BooleanLiteral`.
10652 check_metrics::<RustParser>(
10653 "fn f(a: bool) -> bool { a && true }\n",
10654 "foo.rs",
10655 |metric| {
10656 assert_eq!(metric.abc.conditions_sum(), 2);
10657 insta::assert_json_snapshot!(metric.abc);
10658 },
10659 );
10660 }
10661
10662 // ----- Go -----
10663
10664 #[cfg(feature = "go")]
10665 #[test]
10666 fn go_empty_unit_zero() {
10667 // Package declaration only — no Fitzpatrick events. Confirms the
10668 // GoCode Abc trait is wired up and emits zero counts.
10669 check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
10670 assert_eq!(metric.abc.assignments_sum(), 0);
10671 assert_eq!(metric.abc.branches_sum(), 0);
10672 assert_eq!(metric.abc.conditions_sum(), 0);
10673 insta::assert_json_snapshot!(metric.abc);
10674 });
10675 }
10676
10677 #[cfg(feature = "go")]
10678 #[test]
10679 fn go_assignments_count_plain_compound_short_var_and_incdec() {
10680 // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
10681 // `x += 2` (compound), `x++` (inc), and the initialized
10682 // declaration `var y = 1` — which is counted, matching the Rust
10683 // and Java rules for `let y = 1` / `int y = 1` (both measured at
10684 // one assignment each). The comment here previously claimed the
10685 // opposite and pinned Go at 6 (#1278).
10686 check_metrics::<GoParser>(
10687 "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
10688 "foo.go",
10689 |metric| {
10690 // `_ = y` is itself an assignment_statement → +1.
10691 // var y=1 + _=y + x:= + x=5 + x+=2 + x=7 + x++ → 7
10692 assert_eq!(metric.abc.assignments_sum(), 7);
10693 assert_eq!(metric.abc.branches_sum(), 0);
10694 assert_eq!(metric.abc.conditions_sum(), 0);
10695 insta::assert_json_snapshot!(metric.abc);
10696 },
10697 );
10698 }
10699
10700 #[cfg(feature = "go")]
10701 #[test]
10702 fn go_var_declarations_count_only_when_initialized() {
10703 // Regression for #1278: a `var` declaration with an initializer is
10704 // a `var_spec` carrying a `value` field, not an
10705 // `assignment_statement` or `short_var_declaration`, so it scored
10706 // zero — `var x = 5` and `x := 5` are the same binding spelled two
10707 // ways. Both typed and untyped initializers count; an
10708 // uninitialized `var z int` and a `const` do not.
10709 // expected: 3 assignments — `var x = 5`, `var y int = 6`, `z := 7`;
10710 // `var w int` and `const c = 1` contribute nothing.
10711 check_metrics::<GoParser>(
10712 "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",
10713 "foo.go",
10714 |metric| {
10715 assert_eq!(metric.abc.assignments_sum(), 3);
10716 assert_eq!(metric.abc.branches_sum(), 0);
10717 },
10718 );
10719 }
10720
10721 #[cfg(feature = "go")]
10722 #[test]
10723 fn go_grouped_var_block_counts_each_initialized_spec() {
10724 // A grouped `var ( … )` block is one `var_declaration` holding one
10725 // `var_spec` per line, so matching the spec counts each initialized
10726 // line on its own. A multi-name spec is still one binding
10727 // statement, matching `p, q := 1, 2` (#1278).
10728 // expected: 3 assignments — `a = 1`, `p, q = 1, 2`, and `r := 0`;
10729 // the uninitialized `b int` contributes nothing.
10730 check_metrics::<GoParser>(
10731 "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",
10732 "foo.go",
10733 |metric| {
10734 // The trailing `_ = …` is itself an assignment_statement.
10735 assert_eq!(metric.abc.assignments_sum(), 4);
10736 },
10737 );
10738 }
10739
10740 #[cfg(feature = "go")]
10741 #[test]
10742 fn go_calls_are_branches() {
10743 // Three calls: free function `g()`, method call `r.Inc()`, and
10744 // builtin call `len(s)`. All parse as `call_expression` → B = 3.
10745 // Composite literal `Foo{}` is NOT a call.
10746 check_metrics::<GoParser>(
10747 "package main\n\
10748 type R struct{}\n\
10749 func (r R) Inc() {}\n\
10750 func g() {}\n\
10751 func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
10752 "foo.go",
10753 |metric| {
10754 assert_eq!(metric.abc.branches_sum(), 3);
10755 insta::assert_json_snapshot!(metric.abc);
10756 },
10757 );
10758 }
10759
10760 #[cfg(feature = "go")]
10761 #[test]
10762 fn go_comparisons_count_conditions() {
10763 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
10764 // comparisons → C = 6.
10765 check_metrics::<GoParser>(
10766 "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
10767 "foo.go",
10768 |metric| {
10769 assert_eq!(metric.abc.conditions_sum(), 6);
10770 insta::assert_json_snapshot!(metric.abc);
10771 },
10772 );
10773 }
10774
10775 #[cfg(feature = "go")]
10776 #[test]
10777 fn go_generic_brackets_not_conditions() {
10778 // Generic instantiation `Min[int](a, b)` puts `int` inside
10779 // `TypeArguments`, not `BinaryExpression`. The parent guard on
10780 // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
10781 check_metrics::<GoParser>(
10782 "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
10783 "foo.go",
10784 |metric| {
10785 assert_eq!(metric.abc.conditions_sum(), 0);
10786 assert_eq!(metric.abc.branches_sum(), 1);
10787 insta::assert_json_snapshot!(metric.abc);
10788 },
10789 );
10790 }
10791
10792 #[cfg(feature = "go")]
10793 #[test]
10794 fn go_switch_arms_count_conditions_default_excluded() {
10795 // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
10796 // bare `default` is the C/Java `default:` equivalent and is
10797 // excluded — 3 conditions from ExpressionCase. The switch
10798 // expression `x` is bare (no comparison), so no extra
10799 // condition from `==`-style operators.
10800 check_metrics::<GoParser>(
10801 "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",
10802 "foo.go",
10803 |metric| {
10804 assert_eq!(metric.abc.conditions_sum(), 3);
10805 insta::assert_json_snapshot!(metric.abc);
10806 },
10807 );
10808 }
10809
10810 #[cfg(feature = "go")]
10811 #[test]
10812 fn go_type_switch_arms_count_conditions() {
10813 // Type switch: `case int:`, `case string:`, `default:`. Two
10814 // non-default type-case arms → C = 2.
10815 check_metrics::<GoParser>(
10816 "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
10817 "foo.go",
10818 |metric| {
10819 assert_eq!(metric.abc.conditions_sum(), 2);
10820 insta::assert_json_snapshot!(metric.abc);
10821 },
10822 );
10823 }
10824
10825 #[cfg(feature = "go")]
10826 #[test]
10827 fn go_select_arms_count_conditions() {
10828 // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
10829 // Two non-default communication cases → C = 2.
10830 check_metrics::<GoParser>(
10831 "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
10832 "foo.go",
10833 |metric| {
10834 assert_eq!(metric.abc.conditions_sum(), 2);
10835 insta::assert_json_snapshot!(metric.abc);
10836 },
10837 );
10838 }
10839
10840 // Anchors the tag shape of a Go fixture before scoring its members:
10841 // `tagged` is how many expression switches carry a `value` field, so
10842 // a fixture edit that turns a tagless switch tagged (or back) fails
10843 // here rather than as a moved score.
10844 #[cfg(feature = "go")]
10845 fn assert_go_switch_members(
10846 src: &str,
10847 tagged: usize,
10848 kinds: &[(u16, usize, &str)],
10849 expected: &[(&str, u64, u64)],
10850 ) {
10851 let parser = GoParser::new(
10852 src.as_bytes().to_vec(),
10853 std::path::Path::new("foo.go"),
10854 None,
10855 );
10856 let found = parser
10857 .root()
10858 .preorder()
10859 .filter(|n| {
10860 n.kind_id() == Go::ExpressionSwitchStatement
10861 && n.child_by_field_name("value").is_some()
10862 })
10863 .count();
10864 assert_eq!(found, tagged, "tagged switches — the fixture moved");
10865 assert_fixture_spells::<GoParser>(src, "foo.go", kinds);
10866 check_func_space::<GoParser, _>(src, "foo.go", |space| {
10867 assert_members_score(&space, expected);
10868 });
10869 }
10870
10871 // #1523, transferring #1453's Ruby rule. A tagless switch is
10872 // `switch true`: each case expression is an ordinary boolean, so a
10873 // comparison in it is already paid by the token arm, and the case's
10874 // former blanket +1 counted `gt` twice against its `if` twin `iff`.
10875 // Every row now equals the `if` spelling of the same predicate.
10876 //
10877 // `bare`, `paren` and `neg` are the controls: no operator for any
10878 // other arm to count, so the case still pays through the `if` slot
10879 // (1 before the fix and after). `negcmp` peels to a comparison the
10880 // token arm owns. `init1` carries an `initializer` and no tag, and
10881 // `cmt` puts a comment where the tag would go: neither is the
10882 // `value` field, so both stay tagless. `nest2` is a tagless switch
10883 // inside a tagged one — each switch is judged by its own tag.
10884 #[cfg(feature = "go")]
10885 #[test]
10886 fn go_tagless_switch_case_counts_its_condition_once() {
10887 let src = "package main
10888func gt(x int) int { switch { case x > 5: return 1 }; return 0 }
10889func iff(x int) int { if x > 5 { return 1 }; return 0 }
10890func bare(b bool) int { switch { case b: return 1 }; return 0 }
10891func paren(b bool) int { switch { case (b): return 1 }; return 0 }
10892func neg(b bool) int { switch { case !b: return 1 }; return 0 }
10893func negcmp(x int) int { switch { case !(x > 5): return 1 }; return 0 }
10894func andd(a, b bool) int { switch { case a && b: return 1 }; return 0 }
10895func two(x int) int { switch { case x > 1: fallthrough; case x < 0: return 1; default: return 2 } }
10896func init1(s string) int { switch n := len(s); { case n > 1: return 1 }; return 0 }
10897func cmt(x int) int { switch /* no tag */ { case /* c */ x > 5: return 1 }; return 0 }
10898func nest(x, y int) int { switch { case x > 1: switch { case y > 1: return 1 } }; return 0 }
10899func nest2(x, y int) int { switch x { case 1: switch { case y > 1: return 1 } }; return 0 }
10900";
10901 assert_go_switch_members(
10902 src,
10903 1,
10904 &[
10905 (Go::ExpressionCase as u16, 14, "expression cases"),
10906 (Go::DefaultCase as u16, 1, "`two`'s `default`"),
10907 (Go::GT as u16, 9, "`>` comparisons"),
10908 (
10909 Go::ParenthesizedExpression as u16,
10910 2,
10911 "`paren` / `negcmp` parens",
10912 ),
10913 (Go::Comment as u16, 2, "`cmt`'s comments"),
10914 ],
10915 &[
10916 // (member, abc.conditions, cyclomatic)
10917 // Was 2: the `>` and the case, for one decision.
10918 ("gt", 1, 2),
10919 ("iff", 1, 2),
10920 ("bare", 1, 2),
10921 ("paren", 1, 2),
10922 ("neg", 1, 2),
10923 // Was 2.
10924 ("negcmp", 1, 2),
10925 // Was 3: the Rule 9 walker owns both operands.
10926 ("andd", 2, 3),
10927 // Was 4. `default` stays +0 and `fallthrough` adds nothing.
10928 ("two", 2, 3),
10929 // Was 2.
10930 ("init1", 1, 2),
10931 // Was 2. A comparison, not a bare operand, so a misread
10932 // tag (+1 per case on top of the `>`) shows as 2.
10933 ("cmt", 1, 2),
10934 // Was 4.
10935 ("nest", 2, 3),
10936 // Was 3: the tagged outer case keeps its 1.
10937 ("nest2", 2, 3),
10938 ],
10939 );
10940 }
10941
10942 // A pointer dereference or channel receive is a boolean operand the
10943 // way an identifier is: `*p` of a `*bool`, `<-ch` of a `chan bool`.
10944 // No operator arm counts either, so each slot pays for it — the `if`
10945 // slot, a tagless case (which #1523 routed through that slot, so
10946 // `swp` / `swr` fell from 1 to 0 until this), the `!` / paren peel,
10947 // and an `&&` operand. `neg` and `asg` are the controls: `-x`, `*&y`
10948 // and `b := *p` sit in no boolean slot and stay 0 (#1526).
10949 #[cfg(feature = "go")]
10950 #[test]
10951 fn go_deref_and_receive_are_boolean_operands() {
10952 let src = "package main
10953func ifp(p *bool) int { if *p { return 1 }; return 0 }
10954func ifr(ch chan bool) int { if <-ch { return 1 }; return 0 }
10955func swp(p *bool) int { switch { case *p: return 1 }; return 0 }
10956func swr(ch chan bool) int { switch { case <-ch: return 1 }; return 0 }
10957func andp(a bool, p *bool) int { if a && *p { return 1 }; return 0 }
10958func notp(p *bool) int { if !*p { return 1 }; return 0 }
10959func parp(p *bool) int { if (*p) { return 1 }; return 0 }
10960func neg(x int) int { y := -x; z := *&y; return z }
10961func asg(p *bool) bool { b := *p; return b }
10962";
10963 assert_go_switch_members(
10964 src,
10965 0,
10966 &[
10967 (Go::ExpressionCase as u16, 2, "`swp` / `swr` cases"),
10968 (Go::LTDASH as u16, 2, "`ifr` / `swr` receives"),
10969 (Go::ParenthesizedExpression as u16, 1, "`parp`'s parens"),
10970 ],
10971 &[
10972 // (member, abc.conditions, cyclomatic)
10973 ("ifp", 1, 2),
10974 ("ifr", 1, 2),
10975 ("swp", 1, 2),
10976 ("swr", 1, 2),
10977 ("andp", 2, 3),
10978 ("notp", 1, 2),
10979 ("parp", 1, 2),
10980 ("neg", 0, 1),
10981 ("asg", 0, 1),
10982 ],
10983 );
10984 }
10985
10986 // A case listing several expressions is an implicit `||` — Go's spec
10987 // defines a tagless switch as `switch true`, so `case a, b:` holds
10988 // when `a == true || b == true` — and each expression is scored as an
10989 // operand of that chain: once, unless its own arm already counted it.
10990 // `ab` and `mix` therefore read 2, exactly their `if a || b` /
10991 // `if a || x > 1` twins, and `xim` shows the order cannot matter.
10992 // This is #1453's Ruby rule (`when a, b` reads 2) rather than
10993 // Kotlin's first-alternative-only rule, which scores the two orders
10994 // differently.
10995 //
10996 // Parity consequence: cyclomatic scores the case as one decision, so
10997 // these rows sit one above `conditions == cyclomatic - 1`, while the
10998 // `||` twins sit at it — the same trade Ruby's `when a, b` makes.
10999 #[cfg(feature = "go")]
11000 #[test]
11001 fn go_tagless_switch_case_lists_are_an_implicit_or() {
11002 let src = "package main
11003func ab(a, b bool) int { switch { case a, b: return 1 }; return 0 }
11004func ifab(a, b bool) int { if a || b { return 1 }; return 0 }
11005func mix(a bool, x int) int { switch { case a, x > 1: return 1 }; return 0 }
11006func xim(a bool, x int) int { switch { case x > 1, a: return 1 }; return 0 }
11007func ifmix(a bool, x int) int { if a || x > 1 { return 1 }; return 0 }
11008func cmts(a, b bool) int { switch { case a, /* c */ b: return 1 }; return 0 }
11009";
11010 assert_go_switch_members(
11011 src,
11012 0,
11013 &[
11014 (Go::ExpressionCase as u16, 4, "expression cases"),
11015 (Go::PIPEPIPE as u16, 2, "the `||` twins"),
11016 (Go::Comment as u16, 1, "`cmts`'s comment"),
11017 ],
11018 &[
11019 // Was 1: only the case, for two operands.
11020 ("ab", 2, 2),
11021 ("ifab", 2, 3),
11022 ("mix", 2, 2),
11023 ("xim", 2, 2),
11024 ("ifmix", 2, 3),
11025 // Was 1: the comment is no operand.
11026 ("cmts", 2, 2),
11027 ],
11028 );
11029 }
11030
11031 // The controls that must not move. A tagged case lists values
11032 // compared with the tag, a comparison the source never spells, so the
11033 // case itself is the condition — `init2` carries an `initializer`
11034 // beside its tag, and `tagcmp`'s `x > 1` is a value compared with `b`
11035 // (`if b == (x > 1)` also scores 2). Type-switch and `select` cases
11036 // are not expression cases at all.
11037 #[cfg(feature = "go")]
11038 #[test]
11039 fn go_tagged_switch_keeps_the_per_case_count() {
11040 let src = "package main
11041func tag(x int) int { switch x { case 1, 2: return 1 }; return 0 }
11042func init2(s string) int { switch n := len(s); n { case 1: return 1 }; return 0 }
11043func tagcmp(b bool, x int) int { switch b { case x > 1: return 1 }; return 0 }
11044func ts(v any) int { switch v.(type) { case int: return 1 }; return 0 }
11045func sel(c chan int) int { select { case <-c: return 1 }; return 0 }
11046";
11047 assert_go_switch_members(
11048 src,
11049 3,
11050 &[
11051 (Go::ExpressionCase as u16, 3, "expression cases"),
11052 (Go::TypeCase as u16, 1, "`ts`'s type case"),
11053 (
11054 Go::CommunicationCase as u16,
11055 1,
11056 "`sel`'s communication case",
11057 ),
11058 ],
11059 &[
11060 ("tag", 1, 2),
11061 ("init2", 1, 2),
11062 ("tagcmp", 2, 2),
11063 ("ts", 1, 2),
11064 ("sel", 1, 2),
11065 ],
11066 );
11067 }
11068
11069 #[cfg(feature = "go")]
11070 #[test]
11071 fn go_else_counts_as_condition() {
11072 // `if a > b { ... } else { ... }` → `a > b` is one condition,
11073 // `else` is one condition → C = 2.
11074 check_metrics::<GoParser>(
11075 "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
11076 "foo.go",
11077 |metric| {
11078 assert_eq!(metric.abc.conditions_sum(), 2);
11079 insta::assert_json_snapshot!(metric.abc);
11080 },
11081 );
11082 }
11083
11084 #[cfg(feature = "go")]
11085 #[test]
11086 fn go_complex_function_abc() {
11087 // Mixed shape, verified by hand:
11088 // - Assignments: `var x = 10` (an initialized declaration, #1278),
11089 // `_ = x`, `n := 0`, `n = n + 1`, `n += 2`, `n++`,
11090 // `_ = len(s)` → A = 7. Every `_ = ...` IS counted as an
11091 // assignment_statement.
11092 // - Branches: `len(s)` → B = 1.
11093 // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
11094 // and `case 1:` (default excluded) → 2 → total C = 4.
11095 check_metrics::<GoParser>(
11096 "package main\nfunc f(s string) int {\n\
11097 \x20 var x = 10\n\
11098 \x20 _ = x\n\
11099 \x20 n := 0\n\
11100 \x20 if n < 10 { n = n + 1 } else { n += 2 }\n\
11101 \x20 n++\n\
11102 \x20 _ = len(s)\n\
11103 \x20 switch n {\n\
11104 \x20 case 0: return 0\n\
11105 \x20 case 1: return 1\n\
11106 \x20 default: return n\n\
11107 \x20 }\n\
11108 }\n",
11109 "foo.go",
11110 |metric| {
11111 assert_eq!(metric.abc.assignments_sum(), 7);
11112 assert_eq!(metric.abc.branches_sum(), 1);
11113 assert_eq!(metric.abc.conditions_sum(), 4);
11114 insta::assert_json_snapshot!(metric.abc);
11115 },
11116 );
11117 }
11118
11119 #[cfg(feature = "go")]
11120 #[test]
11121 fn go_if_multiple_conditions() {
11122 // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors
11123 // `rust_if_multiple_conditions`.
11124 check_metrics::<GoParser>(
11125 "package p\n\
11126 func F(a, b, c, d bool) int {\n\
11127 \x20 if a || b || c || d { return 1 } // +4c\n\
11128 \x20 if a && b && c { return 2 } // +3c\n\
11129 \x20 if !a && !b { return 3 } // +2c\n\
11130 \x20 return 0\n\
11131 }\n",
11132 "foo.go",
11133 |metric| {
11134 assert_eq!(metric.abc.conditions_sum(), 9);
11135 insta::assert_json_snapshot!(metric.abc);
11136 },
11137 );
11138 }
11139
11140 #[cfg(feature = "go")]
11141 #[test]
11142 fn go_for_with_conditions() {
11143 // Go has no `while` or `do { … } while(…);` — the `for` loop
11144 // header is the sole condition slot. Each operand of the
11145 // `&&` / `||` chain in the for-condition counts as one
11146 // Fitzpatrick condition.
11147 check_metrics::<GoParser>(
11148 "package p\n\
11149 func F(a, b bool) {\n\
11150 \x20 for a || b { break } // +2c\n\
11151 \x20 for a && !b { break } // +2c\n\
11152 }\n",
11153 "foo.go",
11154 |metric| {
11155 assert_eq!(metric.abc.conditions_sum(), 4);
11156 insta::assert_json_snapshot!(metric.abc);
11157 },
11158 );
11159 }
11160
11161 #[cfg(feature = "go")]
11162 #[test]
11163 fn go_for_bare_condition_counts() {
11164 // Regression for findings.md #1: `for true {}` / `for !ready {}`
11165 // are Go's only loop-condition slot. Pre-fix, the Phase-2B
11166 // dispatcher had no `G::ForStatement` arm, so bare-boolean
11167 // and `!`-wrapped `for` conditions silently reported zero.
11168 // `go_count_condition`'s terminal-bool / paren / unary filter
11169 // makes the walker safe across all three for-statement shapes:
11170 // bare condition, `for_clause` (init; cond; post) — whose own
11171 // `condition` field #1276 taught the walker to read — and
11172 // `range_clause`, which has no such field and contributes
11173 // nothing.
11174 check_metrics::<GoParser>(
11175 "package p\n\
11176 func F(ready bool) {\n\
11177 \x20 for true { break } // +1c\n\
11178 \x20 for !ready { break } // +1c\n\
11179 \x20 for i := 0; i < 3; i++ { _ = i } // +1c (the `<`)\n\
11180 }\n",
11181 "foo.go",
11182 |metric| {
11183 // `for true`: walker counts True (+1).
11184 // `for !ready`: walker on unary unwraps to `ready`
11185 // (+1).
11186 // `for_clause`'s condition is `i < 3`, a
11187 // `binary_expression` that `go_count_condition`
11188 // filters out; the `<` itself contributes 1 via the
11189 // pre-existing LT/GT arm.
11190 // Total: 3.
11191 assert_eq!(metric.abc.conditions_sum(), 3);
11192 insta::assert_json_snapshot!(metric.abc);
11193 },
11194 );
11195 }
11196
11197 // Issue #1276, Go's share. `for_statement`'s child(1) is the
11198 // condition only in the `for cond {}` spelling; the three-clause
11199 // form puts it one level down, in the `for_clause`'s `condition`
11200 // field. Letting the `for_clause` fall through — which the arm's
11201 // own comment used to call harmless — scored a bare three-clause
11202 // condition zero while `for a {}` scored one.
11203 #[cfg(feature = "go")]
11204 #[test]
11205 fn go_three_clause_for_condition_counts() {
11206 // Bare identifier in the three-clause header: no comparison
11207 // token, so only the `for_clause` lookup can count it.
11208 check_metrics::<GoParser>(
11209 "package p\n\
11210 func F(a bool) {\n\
11211 \x20 for i := 0; a; i++ { _ = i }\n\
11212 }\n",
11213 "foo.go",
11214 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11215 );
11216 // Negation, through `go_inspect_container`'s `!` unwrap.
11217 check_metrics::<GoParser>(
11218 "package p\n\
11219 func F(a bool) {\n\
11220 \x20 for i := 0; !a; i++ { _ = i }\n\
11221 }\n",
11222 "foo.go",
11223 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
11224 );
11225 // Empty condition: the `for_clause` exposes no `condition`
11226 // field, so zero — the same answer as Go's bare `for {}` and
11227 // as every other language since #1276.
11228 check_metrics::<GoParser>(
11229 "package p\n\
11230 func F() {\n\
11231 \x20 for i := 0; ; i++ { break }\n\
11232 }\n",
11233 "foo.go",
11234 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
11235 );
11236 // A `range_clause` carries no condition either, and the walker
11237 // must not mistake the clause itself for one.
11238 check_metrics::<GoParser>(
11239 "package p\n\
11240 func F(xs []int) {\n\
11241 \x20 for _, v := range xs { _ = v }\n\
11242 }\n",
11243 "foo.go",
11244 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
11245 );
11246 }
11247
11248 // Go's `for_statement` is the one grammar here that exposes no
11249 // `condition` field, so its header slot is located structurally and
11250 // has to skip what the field-addressed siblings get for free. Two
11251 // things it must skip, each of which `node.child(1)` got wrong:
11252 // a leading comment (tree-sitter counts comments among a node's
11253 // children — the #1181 failure), and the body of a bare `for {}`,
11254 // which IS child(1) and would otherwise be offered to
11255 // `go_count_condition` as though it were a condition.
11256 #[cfg(feature = "go")]
11257 #[test]
11258 fn go_for_header_slot_skips_comments_and_the_body() {
11259 // Each pair is (source, expected conditions). The commented
11260 // spelling must agree with its bare twin.
11261 let cases = [
11262 ("for a { break }", 1),
11263 ("for /* n */ a { break }", 1),
11264 ("for i := 0; a; i++ { _ = i }", 1),
11265 ("for /* n */ i := 0; a; i++ { _ = i }", 1),
11266 ("for i := 0; /* n */ a; i++ { _ = i }", 1),
11267 // Bare infinite loop: the body is child(1) and must not be
11268 // read as the condition.
11269 ("for { break }", 0),
11270 ("for /* n */ { break }", 0),
11271 ("for _, v := range xs { _ = v }", 0),
11272 ("for /* n */ _, v := range xs { _ = v }", 0),
11273 ];
11274 let mut ran = 0;
11275 for (body, expected) in cases {
11276 let src = format!("package p\nfunc F(a bool, xs []int) {{\n\t{body}\n}}\n");
11277 assert_eq!(abc_conditions(LANG::Go, &src), expected, "`{body}`");
11278 ran += 1;
11279 }
11280 // Non-vacuity, both halves: the loop must actually have run
11281 // every row, and the rows must carry both answers — a walker
11282 // stuck at 0 or at 1 would otherwise pass half the table
11283 // silently.
11284 assert_eq!(ran, cases.len());
11285 assert!(cases.iter().any(|&(_, n)| n == 1));
11286 assert!(cases.iter().any(|&(_, n)| n == 0));
11287 }
11288
11289 #[cfg(feature = "go")]
11290 #[test]
11291 fn go_if_init_statement_condition_counts() {
11292 // Regression for the code-review finding: Go's
11293 // `if x := f(); x { ... }` init-statement form puts the
11294 // short-var declaration at child(1) and the condition at
11295 // child(2). Pre-fix, the dispatcher used child(1) and
11296 // counted zero conditions for this idiomatic Go shape.
11297 // The fix uses `child_by_field_name("condition")` which
11298 // returns the condition regardless of init presence.
11299 check_metrics::<GoParser>(
11300 "package p\nfunc F() { if x := g(); x { } }\n",
11301 "foo.go",
11302 |metric| {
11303 // `x` bare-identifier condition contributes 1
11304 // (Rule 6 — bare boolean identifier in if-condition).
11305 // `g()` call contributes 1 branch but no condition.
11306 assert_eq!(metric.abc.branches_sum(), 1);
11307 assert_eq!(metric.abc.conditions_sum(), 1);
11308 insta::assert_json_snapshot!(metric.abc);
11309 },
11310 );
11311 }
11312
11313 #[cfg(feature = "go")]
11314 #[test]
11315 fn go_if_boolean_literal_condition() {
11316 check_metrics::<GoParser>(
11317 "package p\n\
11318 func F() {\n\
11319 \x20 if true {} // +1c\n\
11320 \x20 if !false {} // +1c\n\
11321 }\n",
11322 "foo.go",
11323 |metric| {
11324 assert_eq!(metric.abc.conditions_sum(), 2);
11325 insta::assert_json_snapshot!(metric.abc);
11326 },
11327 );
11328 }
11329
11330 #[cfg(feature = "go")]
11331 #[test]
11332 fn go_methods_arguments_with_conditions() {
11333 check_metrics::<GoParser>(
11334 "package p\n\
11335 func F(a, b bool) {\n\
11336 \x20 m(a, b) // +1b\n\
11337 \x20 m(!a, !b) // +1b +2c\n\
11338 }\n",
11339 "foo.go",
11340 |metric| {
11341 assert_eq!(metric.abc.branches_sum(), 2);
11342 assert_eq!(metric.abc.conditions_sum(), 2);
11343 insta::assert_json_snapshot!(metric.abc);
11344 },
11345 );
11346 }
11347
11348 #[cfg(feature = "go")]
11349 #[test]
11350 fn go_return_with_conditions() {
11351 check_metrics::<GoParser>(
11352 "package p\n\
11353 func M1(z int) bool { return !(z >= 0) }\n\
11354 func M2(x bool) bool { return !x }\n\
11355 func M3(x, y bool) bool { return x && y }\n",
11356 "foo.go",
11357 |metric| {
11358 // M1: `>=` (1). `!(z >= 0)` walker on the unary
11359 // doesn't reach a terminal — stops at the
11360 // BinaryExpression z>=0 inside the parens. +1.
11361 // M2: walker on `!x` → 1.
11362 // M3: `&&` walker counts both → 2.
11363 // Sum: 1 + 1 + 2 = 4.
11364 assert_eq!(metric.abc.conditions_sum(), 4);
11365 insta::assert_json_snapshot!(metric.abc);
11366 },
11367 );
11368 }
11369
11370 #[cfg(feature = "go")]
11371 #[test]
11372 fn go_short_circuit_with_boolean_literal_operand() {
11373 // `a && true` reports 2 conditions: one identifier, one
11374 // boolean literal. Confirms the terminal set includes
11375 // `True` / `False`.
11376 check_metrics::<GoParser>(
11377 "package p\nfunc F(a bool) bool { return a && true }\n",
11378 "foo.go",
11379 |metric| {
11380 assert_eq!(metric.abc.conditions_sum(), 2);
11381 insta::assert_json_snapshot!(metric.abc);
11382 },
11383 );
11384 }
11385
11386 // ----- Elixir -----
11387
11388 // No top-level Calls and no operators → all three vectors are
11389 // zero. Uses a bare expression rather than a `defmodule` wrapper
11390 // (which would itself be a Call → 1 branch). Confirms the
11391 // ElixirCode Abc trait is wired up and the metric emits.
11392 #[cfg(feature = "elixir")]
11393 #[test]
11394 fn elixir_empty_unit_zero() {
11395 check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
11396 assert_eq!(metric.abc.assignments_sum(), 0);
11397 assert_eq!(metric.abc.branches_sum(), 0);
11398 assert_eq!(metric.abc.conditions_sum(), 0);
11399 insta::assert_json_snapshot!(metric.abc);
11400 });
11401 }
11402
11403 // An empty `defmodule Foo do ... end` is itself ONE `Call` →
11404 // Documents that module-/function-defining macros (`defmodule`,
11405 // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
11406 // directives (`alias`, `import`, `require`, `use`) are NOT
11407 // runtime dispatch and therefore do NOT inflate `branches`,
11408 // matching Cognitive's treatment.
11409 #[cfg(feature = "elixir")]
11410 #[test]
11411 fn elixir_defmodule_is_zero_branches() {
11412 check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
11413 assert_eq!(metric.abc.branches_sum(), 0);
11414 assert_eq!(metric.abc.assignments_sum(), 0);
11415 assert_eq!(metric.abc.conditions_sum(), 0);
11416 insta::assert_json_snapshot!(metric.abc);
11417 });
11418 }
11419
11420 // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
11421 // `defmodule` and `def` are declarative-Call wrappers and are
11422 // filtered out of branches; the assertion focuses on assignments
11423 // so we only pin that vector.
11424 #[cfg(feature = "elixir")]
11425 #[test]
11426 fn elixir_pattern_match_is_assignment() {
11427 check_metrics::<ElixirParser>(
11428 "defmodule Foo do\n def f do\n x = 1\n y = x + 1\n y\n end\nend\n",
11429 "foo.ex",
11430 |metric| {
11431 assert_eq!(metric.abc.assignments_sum(), 2);
11432 insta::assert_json_snapshot!(metric.abc);
11433 },
11434 );
11435 }
11436
11437 // `|>` pipeline operator: each `|>` token contributes one branch.
11438 // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
11439 // step also dispatches a Call (`String.upcase(...)`,
11440 // `String.trim(...)`) — these are wrapped inside the outer
11441 // pipeline Call tree, contributing additional Call branches.
11442 // The headline assertion confirms (a) `|>` is detected and (b)
11443 // pipeline steps are not silently dropped.
11444 #[cfg(feature = "elixir")]
11445 #[test]
11446 fn elixir_pipeline_each_step_is_branch() {
11447 check_metrics::<ElixirParser>(
11448 "defmodule Foo do\n def normalize(s) do\n s |> String.trim() |> String.upcase()\n end\nend\n",
11449 "foo.ex",
11450 |metric| {
11451 // Pipeline yields 2 `|>` branches plus Calls for
11452 // String.trim, String.upcase, and the outer pipeline
11453 // (which surfaces as a Call wrapping the binary
11454 // operator). `def` and `defmodule` are declarative
11455 // and excluded. Empirical total: B = 5.
11456 assert_eq!(metric.abc.branches_sum(), 5);
11457 assert_eq!(metric.abc.assignments_sum(), 0);
11458 insta::assert_json_snapshot!(metric.abc);
11459 },
11460 );
11461 }
11462
11463 // Comparison operators all count as conditions. Six comparisons
11464 // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
11465 #[cfg(feature = "elixir")]
11466 #[test]
11467 fn elixir_comparisons_are_conditions() {
11468 check_metrics::<ElixirParser>(
11469 "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",
11470 "foo.ex",
11471 |metric| {
11472 assert_eq!(metric.abc.conditions_sum(), 6);
11473 insta::assert_json_snapshot!(metric.abc);
11474 },
11475 );
11476 }
11477
11478 // Strict-equality operators `===` / `!==` count as conditions too.
11479 #[cfg(feature = "elixir")]
11480 #[test]
11481 fn elixir_strict_equality_is_condition() {
11482 check_metrics::<ElixirParser>(
11483 "defmodule Foo do\n def f(a, b) do\n a === b or a !== b\n end\nend\n",
11484 "foo.ex",
11485 |metric| {
11486 assert_eq!(metric.abc.conditions_sum(), 2);
11487 insta::assert_json_snapshot!(metric.abc);
11488 },
11489 );
11490 }
11491
11492 // #1461 moved every relational construct a grammar spells as its own
11493 // production onto an unconditional arm — scored by use, Fitzpatrick
11494 // Rule 5 — in C#, Java, Groovy, Kotlin and Ruby. Elixir's membership
11495 // and type tests were the same class and were not in that sweep, so
11496 // `a in b` scored zero where `a == b` scored one. The gap stayed
11497 // invisible while the `when` arm paid a flat one for every guard,
11498 // and surfaced the moment the guard became a slot: an
11499 // operator-spelled guard is owned by its operator's arm, and `in`
11500 // had none.
11501 //
11502 // `bare` / `negbare` are the by-use claim; `slot` is the §5 check
11503 // that a boolean slot does not now score the same operator twice;
11504 // `cmp` is the control the two membership rows are levelled against.
11505 // `in` rides the `<` / `>` `binary_operator` gate rather than
11506 // standing alone — it has the same three grammar positions — and
11507 // `elixir_operator_identifier_is_not_a_condition` pins that gate.
11508 #[cfg(feature = "elixir")]
11509 #[test]
11510 fn elixir_membership_is_a_condition_by_use() {
11511 check_func_space::<ElixirParser, _>(
11512 "defmodule Foo do\n\
11513 def bare(a, b), do: a in b\n\
11514 def negbare(a, b), do: a not in b\n\
11515 def slot(a, b) do\n\
11516 if a in b do\n\
11517 IO.puts(\"x\")\n\
11518 end\n\
11519 end\n\
11520 def cmp(a, b), do: a == b\n\
11521 end\n",
11522 "foo.ex",
11523 |space| {
11524 assert_members_score(
11525 &space.spaces[0],
11526 &[
11527 // Each was 0 before the arm.
11528 ("bare", 1, 1),
11529 ("negbare", 1, 1),
11530 // `in` (1) only — the `if` predicate slot is
11531 // already paid by the operator (#1527), exactly
11532 // as `if a == b` scores.
11533 ("slot", 1, 2),
11534 ("cmp", 1, 1),
11535 ],
11536 );
11537 },
11538 );
11539 }
11540
11541 // Guard `when` clause counts as a condition — exactly one, and here
11542 // it is the `>` that supplies it. The guard is a condition *slot*
11543 // (#1454), so an operator-spelled guard scores through the operator
11544 // arm that owns it and the slot itself adds nothing; see
11545 // `elixir_guard_is_a_decision_however_spelled` for the four-spelling
11546 // agreement this is one row of.
11547 //
11548 // Was 2, on a flat `+1` for the `when` token laid on top of the `>`:
11549 // the §5 double count a whole-branch review of the #1454 batch
11550 // found.
11551 #[cfg(feature = "elixir")]
11552 #[test]
11553 fn elixir_guard_when_is_condition() {
11554 check_metrics::<ElixirParser>(
11555 "defmodule Foo do\n def f(x) when x > 0 do\n :pos\n end\nend\n",
11556 "foo.ex",
11557 |metric| {
11558 // the guard slot (+0, the guard is an operator
11559 // application) + `>` (+1) = 1
11560 assert_eq!(metric.abc.conditions_sum(), 1);
11561 insta::assert_json_snapshot!(metric.abc);
11562 },
11563 );
11564 }
11565
11566 // A `case` pays one condition per clause (#1531) AND one branch for
11567 // the Call itself. The `_ ->` catch-all is the default arm and pays
11568 // nothing, so this two-clause `case` scores 1; the per-clause rule
11569 // is pinned by `elixir_clause_constructs_pay_per_clause`.
11570 #[cfg(feature = "elixir")]
11571 #[test]
11572 fn elixir_case_is_condition_and_branch() {
11573 check_metrics::<ElixirParser>(
11574 "defmodule Foo do\n def f(x) do\n case x do\n 1 -> :one\n _ -> :other\n end\n end\nend\n",
11575 "foo.ex",
11576 |metric| {
11577 // conditions: `1 ->` (+1) + `_ ->` default (+0) = 1
11578 assert_eq!(metric.abc.conditions_sum(), 1);
11579 insta::assert_json_snapshot!(metric.abc);
11580 },
11581 );
11582 }
11583
11584 // `cond` scores per clause, as its nested-`if` twin does (#1527): the
11585 // `x > 0` clause is a predicate slot its `>` already pays, and the
11586 // `true ->` catch-all is the default arm and pays nothing.
11587 #[cfg(feature = "elixir")]
11588 #[test]
11589 fn elixir_cond_is_condition() {
11590 check_metrics::<ElixirParser>(
11591 "defmodule Foo do\n def f(x) do\n cond do\n x > 0 -> :pos\n true -> :other\n end\n end\nend\n",
11592 "foo.ex",
11593 |metric| {
11594 // conditions: `x > 0` clause slot (+0) + `>` (+1) +
11595 // `true ->` default (+0) = 1, against cyclomatic 2
11596 assert_eq!(metric.abc.conditions_sum(), 1);
11597 insta::assert_json_snapshot!(metric.abc);
11598 },
11599 );
11600 }
11601
11602 // An `if` / `unless` predicate and each `cond` clause are condition
11603 // slots (#1527): one condition, unless an operator in the slot
11604 // already paid. Before, the keyword Call paid a flat one on top of
11605 // its predicate, so `if x > 5` scored 2 and `cond` paid once per
11606 // construct whatever its clause count.
11607 //
11608 // Expected values come from the twins, not the rule: each `if` row
11609 // is what Ruby and Java score for the same `if` (`if x > 5` 1,
11610 // `if b` 1, `if a && b` 2 — measured), every non-chain row sits at
11611 // `cyclomatic - 1`, and `cond3` equals Go's tagless
11612 // `switch { case x > 5: … case b: … default: … }` (2), whose
11613 // `default:` is as free as `cond`'s `true ->`. Its nested-`if` twin
11614 // `nested` pays Fitzpatrick Rule 5 for each `else` (#1531), as
11615 // Ruby's `if … elsif … else` and Java's `if … else if … else` do (4
11616 // against cyclomatic 3 — measured). The chain rows sit one under
11617 // parity exactly as Ruby's and Java's `if a && b` (2 / 3) do.
11618 //
11619 // Paths only one row exercises (§11): `cmp_comment` the comment
11620 // skip in a parenthesised peel, `assign` the match-`=` peel,
11621 // `negcmp` the `not` peel, `arith` a non-comparison operator the
11622 // slot pays for, `capture` a chain operand that no terminal list
11623 // held (`&f/1` scored one short of `a && b` on main), and the
11624 // `true ->` rows the default-arm gate shared with cyclomatic.
11625 #[cfg(feature = "elixir")]
11626 #[test]
11627 fn elixir_if_and_cond_predicates_are_slots() {
11628 check_func_space::<ElixirParser, _>(
11629 "defmodule Foo do
11630 def cmp(x), do: (if x > 5 do 1 end)
11631 def bare(b), do: (if b do 1 end)
11632 def kw(x), do: if(x > 5, do: 1)
11633 def unless_cmp(x), do: (unless x > 5 do 1 end)
11634 def cmp_comment(x) do
11635 if (x > 5 # c
11636 ) do
11637 1
11638 end
11639 end
11640 def assign(x), do: (if (y = x > 5) do y end)
11641 def negcmp(x), do: (if not(x > 5) do 1 end)
11642 def arith(x), do: (if x + 1 do 1 end)
11643 def chain(a, b), do: (if a && b do 1 end)
11644 def capture(a), do: (if a && &f/1 do 1 end)
11645 def cond_cmp(x) do
11646 cond do
11647 x > 5 -> 1
11648 true -> 2
11649 end
11650 end
11651 def cond_bare(b) do
11652 cond do
11653 b -> 1
11654 true -> 2
11655 end
11656 end
11657 def cond3(x, b) do
11658 cond do
11659 x > 5 -> 1
11660 b -> 2
11661 true -> 3
11662 end
11663 end
11664 def nested(x, b) do
11665 if x > 5 do
11666 1
11667 else
11668 if b do 2 else 3 end
11669 end
11670 end
11671 def cond_no_default(a, b, c) do
11672 cond do
11673 a -> 1
11674 b -> 2
11675 c -> 3
11676 end
11677 end
11678end
11679",
11680 "foo.ex",
11681 |space| {
11682 assert_members_score(
11683 &space.spaces[0],
11684 &[
11685 ("cmp", 1, 2),
11686 ("bare", 1, 2),
11687 ("kw", 1, 2),
11688 ("unless_cmp", 1, 2),
11689 ("cmp_comment", 1, 2),
11690 ("assign", 1, 2),
11691 ("negcmp", 1, 2),
11692 ("arith", 1, 2),
11693 ("chain", 2, 3),
11694 ("capture", 2, 3),
11695 ("cond_cmp", 1, 2),
11696 ("cond_bare", 1, 2),
11697 ("cond3", 2, 3),
11698 ("nested", 4, 3),
11699 ("cond_no_default", 3, 4),
11700 ],
11701 );
11702 },
11703 );
11704 }
11705
11706 // Every clause construct pays one condition per clause (#1531), as
11707 // `Cyclomatic` counts one decision per clause through the same gate.
11708 // `case` and `with` paid once per construct and the others not at
11709 // all, so a two-clause `case` scored 1 against Ruby's
11710 // `case x; when 1; when 2; end`, Go's `switch` and Rust's `match`
11711 // at 2 (measured).
11712 //
11713 // Expected values come from cyclomatic, which this fix did not move:
11714 // every row sits at `cyclomatic - 1`. Paths only one row exercises
11715 // (§11): `case_default` the free `_ ->`, `case_guard` a guarded
11716 // clause's flat one beside its `>`, `rescue_in` a sole pattern that
11717 // scores itself (the `in` arm), `rescue_alias` a sole pattern the
11718 // clause slot pays for, `catch_two` a multi-pattern clause, `recv` a
11719 // `receive`'s `after` timeout clause — free since #1581, as the
11720 // default arm, so `recv` scores what `case2` does — and `with_else`
11721 // the `else` of a `with`, whose clauses pay rather than a Rule 5
11722 // `else`. Its `<-` clause pays one more since #1535, in both metrics.
11723 #[cfg(feature = "elixir")]
11724 #[test]
11725 fn elixir_clause_constructs_pay_per_clause() {
11726 check_func_space::<ElixirParser, _>(
11727 "defmodule Foo do
11728 def case2(x) do
11729 case x do
11730 1 -> :a
11731 2 -> :b
11732 end
11733 end
11734 def case_default(x) do
11735 case x do
11736 1 -> :a
11737 2 -> :b
11738 _ -> :c
11739 end
11740 end
11741 def case_guard(x) do
11742 case x do
11743 y when y > 0 -> :a
11744 _ -> :b
11745 end
11746 end
11747 def with_else(x) do
11748 with {:ok, a} <- g(x) do
11749 a
11750 else
11751 {:error, e} -> e
11752 _ -> nil
11753 end
11754 end
11755 def recv(x) do
11756 receive do
11757 {:a, y} -> y
11758 {:b, z} -> z
11759 after
11760 100 -> x
11761 end
11762 end
11763 def rescue_in() do
11764 try do
11765 g()
11766 rescue
11767 e in RuntimeError -> e
11768 end
11769 end
11770 def rescue_alias() do
11771 try do
11772 g()
11773 rescue
11774 RuntimeError -> 1
11775 end
11776 end
11777 def catch_two() do
11778 try do
11779 g()
11780 catch
11781 :exit, r -> r
11782 end
11783 end
11784end
11785",
11786 "foo.ex",
11787 |space| {
11788 assert_members_score(
11789 &space.spaces[0],
11790 &[
11791 ("case2", 2, 3),
11792 ("case_default", 2, 3),
11793 ("case_guard", 2, 3),
11794 ("with_else", 2, 3),
11795 ("recv", 2, 3),
11796 ("rescue_in", 1, 2),
11797 ("rescue_alias", 1, 2),
11798 ("catch_two", 1, 2),
11799 ],
11800 );
11801 },
11802 );
11803 }
11804
11805 // A multi-clause anonymous fn dispatches like a `case`, less its head
11806 // clause, which is the closure's definition (#776): `fn x -> x end`
11807 // has no decision and `fn 0 -> :a; _ -> :b end` one — the trailing
11808 // `_ ->` included, since the head already took the free path.
11809 // Expected: each total equals the closure's cyclomatic decisions.
11810 #[cfg(feature = "elixir")]
11811 #[test]
11812 fn elixir_multi_clause_fn_pays_after_its_head() {
11813 for (source, conditions) in [
11814 ("f = fn x -> x end\n", 0),
11815 ("f = fn 0 -> :a; _ -> :b end\n", 1),
11816 ("f = fn 0 -> :a; 1 -> :b; _ -> :c end\n", 2),
11817 ] {
11818 let metrics =
11819 metrics_verbatim(LANG::Elixir, source.as_bytes(), MetricsOptions::default());
11820 assert_eq!(metrics.abc.conditions_sum(), conditions, "{source}");
11821 }
11822 }
11823
11824 // A typespec's function type (`(any -> any)`) is a `stab_clause` too,
11825 // under a parenthesised `block`, and is type syntax with no decision
11826 // behind it. Cyclomatic counted each one; the shared clause gate
11827 // would have carried that into ABC (#1531). Expected: nothing but the
11828 // unit's base path, as for the typespec `when` in
11829 // `elixir_typespec_when_is_not_a_guard`.
11830 #[cfg(feature = "elixir")]
11831 #[test]
11832 fn elixir_typespec_function_type_is_not_a_clause() {
11833 let metrics = metrics_verbatim(
11834 LANG::Elixir,
11835 b"@spec f(list, (any -> any)) :: list\n@type cb :: (integer, integer -> boolean)\n",
11836 MetricsOptions::default(),
11837 );
11838 assert_eq!(metrics.abc.conditions_sum(), 0);
11839 assert_eq!(metrics.cyclomatic.cyclomatic_sum(), 1);
11840 }
11841
11842 // The keyword form of each clause construct holds its clauses in a
11843 // parenthesised `block` under a `do:` / `else:` / `rescue:` pair, the
11844 // shape a typespec function type also has. Excluding `block` for the
11845 // typespec dropped every keyword-form clause (`ck` read 1/4, and `rk`
11846 // 0/3); each `*k` row must equal its `*b` block-form twin, `dk`'s
11847 // `true ->` included (it was a decision in the keyword form).
11848 #[cfg(feature = "elixir")]
11849 #[test]
11850 fn elixir_keyword_form_clauses_match_block_form() {
11851 let src = "defmodule M do
11852 def ck(x), do: case(x, do: (1 -> :a; 2 -> :b; 3 -> :c))
11853 def cb(x) do
11854 case x do
11855 1 -> :a
11856 2 -> :b
11857 3 -> :c
11858 end
11859 end
11860 def dk(x), do: cond(do: (x > 1 -> :a; x < 0 -> :b; true -> :c))
11861 def db(x) do
11862 cond do
11863 x > 1 -> :a
11864 x < 0 -> :b
11865 true -> :c
11866 end
11867 end
11868 def rk(), do: receive(do: ({:a, y} -> y; {:b, z} -> z))
11869 def rb() do
11870 receive do
11871 {:a, y} -> y
11872 {:b, z} -> z
11873 end
11874 end
11875 def tk(), do: try(do: g(), rescue: (e in RuntimeError -> e; _ -> 2))
11876 def tb() do
11877 try do
11878 g()
11879 rescue
11880 e in RuntimeError -> e
11881 _ -> 2
11882 end
11883 end
11884 def wk(x), do: with({:ok, a} <- x, do: a, else: ({:error, e} -> e; _ -> 0))
11885 def wb(x) do
11886 with {:ok, a} <- x do
11887 a
11888 else
11889 {:error, e} -> e
11890 _ -> 0
11891 end
11892 end
11893end
11894";
11895 assert_fixture_spells::<ElixirParser>(
11896 src,
11897 "foo.ex",
11898 &[
11899 (Elixir::Block as u16, 5, "the five keyword-form sections"),
11900 (Elixir::StabClause as u16, 24, "clauses, 12 per spelling"),
11901 ],
11902 );
11903 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
11904 assert_members_score(
11905 &space.spaces[0],
11906 &[
11907 ("ck", 3, 4),
11908 ("cb", 3, 4),
11909 ("dk", 2, 3),
11910 ("db", 2, 3),
11911 ("rk", 2, 3),
11912 ("rb", 2, 3),
11913 ("tk", 1, 2),
11914 ("tb", 1, 2),
11915 ("wk", 2, 3),
11916 ("wb", 2, 3),
11917 ],
11918 );
11919 });
11920 }
11921
11922 // Fitzpatrick Rule 5 counts the `else` of an `if` / `unless` (#1531),
11923 // in both of Elixir's spellings. Expected values are the twins':
11924 // Ruby's `if b … else … end`, Java's `if (b) {} else {}`, Python's
11925 // and Go's each score 2 against cyclomatic 2 (measured), one over
11926 // parity by the documented `else` arm. `kw_other` is the control
11927 // that the `else:` key is read only on an `if` / `unless`: on any
11928 // other call it is an ordinary keyword argument.
11929 #[cfg(feature = "elixir")]
11930 #[test]
11931 fn elixir_if_else_pays_rule_5() {
11932 check_func_space::<ElixirParser, _>(
11933 "defmodule Foo do
11934 def block(b) do
11935 if b do
11936 1
11937 else
11938 2
11939 end
11940 end
11941 def kw(b), do: if(b, do: 1, else: 2)
11942 def unless_block(b) do
11943 unless b do
11944 1
11945 else
11946 2
11947 end
11948 end
11949 def cmp(x) do
11950 if x > 5 do
11951 1
11952 else
11953 2
11954 end
11955 end
11956 def no_else(b) do
11957 if b do
11958 1
11959 end
11960 end
11961 def kw_no_else(b), do: if(b, do: 1)
11962 def kw_other(b), do: g(b, else: 1)
11963end
11964",
11965 "foo.ex",
11966 |space| {
11967 assert_members_score(
11968 &space.spaces[0],
11969 &[
11970 ("block", 2, 2),
11971 ("kw", 2, 2),
11972 ("unless_block", 2, 2),
11973 ("cmp", 2, 2),
11974 ("no_else", 1, 2),
11975 ("kw_no_else", 1, 2),
11976 ("kw_other", 0, 1),
11977 ],
11978 );
11979 },
11980 );
11981 }
11982
11983 // `for` is a comprehension/loop, NOT in the issue's condition
11984 // list. It is still a Call so it contributes one branch, but no
11985 // condition.
11986 #[cfg(feature = "elixir")]
11987 #[test]
11988 fn elixir_for_is_branch_not_condition() {
11989 check_metrics::<ElixirParser>(
11990 "defmodule Foo do\n def f(xs) do\n for x <- xs, do: x * 2\n end\nend\n",
11991 "foo.ex",
11992 |metric| {
11993 assert_eq!(metric.abc.conditions_sum(), 0);
11994 insta::assert_json_snapshot!(metric.abc);
11995 },
11996 );
11997 }
11998
11999 // Mixed shape, verified by hand: defmodule Call + def Call + if Call
12000 // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
12001 // - Assignments: `x = 1` → A = 1.
12002 // - Branches: `defmodule` and `def` are declarative and excluded;
12003 // `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
12004 // - Conditions: `x > 0` → 1; the `if` predicate slot it fills adds
12005 // nothing more (#1527) → C = 1.
12006 #[cfg(feature = "elixir")]
12007 #[test]
12008 fn elixir_mixed_abc() {
12009 check_metrics::<ElixirParser>(
12010 "defmodule Foo do\n def f do\n x = 1\n if x > 0 do\n side_effect()\n end\n end\nend\n",
12011 "foo.ex",
12012 |metric| {
12013 assert_eq!(metric.abc.assignments_sum(), 1);
12014 assert_eq!(metric.abc.branches_sum(), 2);
12015 assert_eq!(metric.abc.conditions_sum(), 1);
12016 insta::assert_json_snapshot!(metric.abc);
12017 },
12018 );
12019 }
12020
12021 #[cfg(feature = "elixir")]
12022 #[test]
12023 fn elixir_unary_conditions_in_chain() {
12024 // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a
12025 // `&&` / `||` chain is one condition. For `if a && b || c` the
12026 // walker adds a, b, c → 3, and the `if` predicate slot adds
12027 // nothing because the chain already scored it (#1527). expected:
12028 // 3, as Ruby and Java score the same `if`, against cyclomatic 4
12029 // (base 1 + if + && + ||).
12030 check_metrics::<ElixirParser>(
12031 "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",
12032 "foo.ex",
12033 |metric| {
12034 assert_eq!(metric.abc.conditions_sum(), 3);
12035 },
12036 );
12037 }
12038
12039 // Elixir spells negation two ways and the operand peel once
12040 // recognised only `!`, so `a && not b` scored 3 where `a && !b`
12041 // scored 4 — the `not` operand reached no terminal and vanished.
12042 // Both forms now read the same. `not` is the stricter of the two (it
12043 // raises on a non-boolean operand where `!` accepts any truthy
12044 // value), so it is at least as good a proof that what it wraps is
12045 // boolean, which is all the walker's flag claims.
12046 //
12047 // Both members in one fixture because the claim is that they agree:
12048 // asserting either alone says nothing about the pair (§11), and a
12049 // shared `check_metrics` total could not tell 3 + 4 from 4 + 3.
12050 //
12051 // `assert_members_score` rather than the parity-asserting sibling:
12052 // each member scores its two `&&` operands and nothing for the `if`
12053 // slot the chain fills (#1527) — 2 against cyclomatic
12054 // `base 1 + if + &&` = 3, as Ruby's `if a && !b` does.
12055 #[cfg(feature = "elixir")]
12056 #[test]
12057 fn elixir_keyword_not_negates_like_bang() {
12058 check_func_space::<ElixirParser, _>(
12059 "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",
12060 "foo.ex",
12061 |space| {
12062 assert_members_score(&space.spaces[0], &[("kw", 2, 3), ("bang", 2, 3)]);
12063 },
12064 );
12065 }
12066
12067 #[cfg(feature = "elixir")]
12068 #[test]
12069 fn elixir_comparison_operands_add_nothing() {
12070 // Isolation check: comparison operands of a `&&` chain are nested
12071 // `binary_operator` nodes, not bare boolean leaves, so the walker
12072 // adds nothing. expected: 2 = `>` (1) + `>` (1); the `&&` walker
12073 // and the `if` slot the chain fills contribute 0 (#1527).
12074 check_metrics::<ElixirParser>(
12075 "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",
12076 "foo.ex",
12077 |metric| {
12078 assert_eq!(metric.abc.conditions_sum(), 2);
12079 },
12080 );
12081 }
12082
12083 #[cfg(feature = "elixir")]
12084 #[test]
12085 fn elixir_keyword_and_or_chain_counts_operands() {
12086 // The keyword forms `and` / `or` get the same Rule 9 treatment as
12087 // `&&` / `||`. expected: 3 = operands a, b, c; the `if` slot the
12088 // chain fills adds nothing (#1527).
12089 check_metrics::<ElixirParser>(
12090 "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",
12091 "foo.ex",
12092 |metric| {
12093 assert_eq!(metric.abc.conditions_sum(), 3);
12094 },
12095 );
12096 }
12097
12098 // Sigil delimiter choice must not move ABC: `~s<hi>` and `~s(hi)`
12099 // are the same value spelled differently, but the `<` / `>`
12100 // delimiter tokens carry the comparison kind ids, so the unguarded
12101 // condition arm scored `~s<hi>` as 2 conditions and `~s(hi)` as 0.
12102 // The parent-is-`Sigil` guard (mirroring the Halstead getter's,
12103 // #1256) suppresses the delimiter case. expected, for each
12104 // spelling: A = 1 (the `x =` pattern match), B = 0 (a bare sigil
12105 // is not a `Call` node — verified by AST dump: `binary_operator`
12106 // wrapping `identifier`, `=`, `sigil`), C = 0 (no comparison, no
12107 // guard, no keyword Call).
12108 #[cfg(feature = "elixir")]
12109 #[test]
12110 fn elixir_sigil_delimiter_choice_is_abc_invariant() {
12111 for src in ["x = ~s<hi>\n", "x = ~s(hi)\n"] {
12112 check_metrics::<ElixirParser>(src, "foo.ex", |metric| {
12113 assert_eq!(metric.abc.assignments_sum(), 1);
12114 assert_eq!(metric.abc.branches_sum(), 0);
12115 assert_eq!(metric.abc.conditions_sum(), 0);
12116 });
12117 }
12118 }
12119
12120 // Control for the guard above: `<` *outside* a sigil is a genuine
12121 // comparison and must keep counting even with a `<`-delimited
12122 // sigil in the same unit. expected: A = 2 (`x =`, `y =`), C = 1
12123 // (only `a < b`; the sigil's `<` / `>` delimiters are guarded).
12124 #[cfg(feature = "elixir")]
12125 #[test]
12126 fn elixir_lt_comparison_still_counts_beside_sigil() {
12127 check_metrics::<ElixirParser>("x = ~s<hi>\ny = a < b\n", "foo.ex", |metric| {
12128 assert_eq!(metric.abc.assignments_sum(), 2);
12129 assert_eq!(metric.abc.conditions_sum(), 1);
12130 });
12131 }
12132
12133 // #1297's sweep converted six grammars from a denylist of
12134 // non-comparison `<` / `>` parents to an allowlist and recorded
12135 // Elixir as already covered by its sigil guard. It was not: an
12136 // operator *named* rather than applied — the capture `&</2` and the
12137 // qualified call `Kernel.<(a, b)` — puts the bare token under an
12138 // `operator_identifier`, which no denylist entry mentioned, so each
12139 // scored a condition against zero decisions. Same shape as the C#
12140 // `operator <` declaration the sweep did fix.
12141 //
12142 // Each function is its own space and its own row, so a fix that
12143 // reached one spelling and not the other fails here. `max/2` is the
12144 // control: a qualified call whose name is not an operator was
12145 // always 0, so a guard that merely stopped counting `<` everywhere
12146 // would pass this row and fail the two below it.
12147 //
12148 // The six equality and ordering tokens share the gate (#1531): they
12149 // sat on an ungated arm, so `&==/2` and `Kernel.==(a, b)` scored a
12150 // condition each and `caps` five. `eq` is their applied control.
12151 #[cfg(feature = "elixir")]
12152 #[test]
12153 fn elixir_operator_identifier_is_not_a_condition() {
12154 check_func_space::<ElixirParser, _>(
12155 "defmodule M do\n\
12156 def asc(l), do: Enum.sort(l, &</2)\n\
12157 def lt(a, b), do: Kernel.<(a, b)\n\
12158 def big(a, b), do: Kernel.max(a, b)\n\
12159 def cmp(a, b), do: a < b\n\
12160 def eq_cap(l), do: Enum.sort(l, &==/2)\n\
12161 def eq_remote(a, b), do: Kernel.==(a, b)\n\
12162 def caps(), do: {&===/2, &!=/2, &!==/2, &<=/2, &>=/2}\n\
12163 def eq(a, b), do: a == b\n\
12164 end\n",
12165 "foo.ex",
12166 |space| {
12167 let module = &space.spaces[0];
12168 let scored: Vec<(String, u64)> = module
12169 .spaces
12170 .iter()
12171 .map(|f| {
12172 (
12173 f.name.clone().unwrap_or_default(),
12174 f.metrics.abc.conditions(),
12175 )
12176 })
12177 .collect();
12178 assert_eq!(
12179 scored,
12180 vec![
12181 ("asc".to_owned(), 0),
12182 ("lt".to_owned(), 0),
12183 ("big".to_owned(), 0),
12184 ("cmp".to_owned(), 1),
12185 ("eq_cap".to_owned(), 0),
12186 ("eq_remote".to_owned(), 0),
12187 ("caps".to_owned(), 0),
12188 ("eq".to_owned(), 1),
12189 ],
12190 );
12191 },
12192 );
12193 }
12194
12195 // ----- C++ -----
12196
12197 #[cfg(feature = "cpp")]
12198 #[test]
12199 fn cpp_empty_unit_zero() {
12200 // No code → A=B=C=0. Wires up the trait and exercises the
12201 // per-language compute reachability.
12202 check_metrics::<CppParser>("", "empty.cpp", |metric| {
12203 assert_eq!(metric.abc.assignments_sum(), 0);
12204 assert_eq!(metric.abc.branches_sum(), 0);
12205 assert_eq!(metric.abc.conditions_sum(), 0);
12206 insta::assert_json_snapshot!(metric.abc);
12207 });
12208 }
12209
12210 #[cfg(feature = "cpp")]
12211 #[test]
12212 fn cpp_plain_and_compound_assignments_count() {
12213 // `int x = 0` is an `init_declarator` carrying an `=` token
12214 // and counts as 1 (post-#393: the literal Fitzpatrick rule
12215 // counts every `=` operator, matching the JS impl's
12216 // `let x = 5` treatment). `x = 5`, `x += 2`, `x = 7` all
12217 // parse as `assignment_expression` → 3. Total A = 4.
12218 check_metrics::<CppParser>(
12219 "void f() { int x = 0; x = 5; x += 2; x = 7; }",
12220 "foo.cpp",
12221 |metric| {
12222 assert_eq!(metric.abc.assignments_sum(), 4);
12223 assert_eq!(metric.abc.branches_sum(), 0);
12224 assert_eq!(metric.abc.conditions_sum(), 0);
12225 insta::assert_json_snapshot!(metric.abc);
12226 },
12227 );
12228 }
12229
12230 #[cfg(feature = "cpp")]
12231 #[test]
12232 fn cpp_increment_and_decrement_count_as_assignment() {
12233 // `x++` / `--x` / prefix and postfix forms each parse as
12234 // `update_expression` and count as 1 assignment per
12235 // Fitzpatrick — 4. `int x = 0` (init_declarator with `=`)
12236 // adds 1 (post-#393). Total A = 5.
12237 check_metrics::<CppParser>(
12238 "void f() { int x = 0; x++; --x; ++x; x--; }",
12239 "foo.cpp",
12240 |metric| {
12241 assert_eq!(metric.abc.assignments_sum(), 5);
12242 insta::assert_json_snapshot!(metric.abc);
12243 },
12244 );
12245 }
12246
12247 #[cfg(feature = "cpp")]
12248 #[test]
12249 fn cpp_init_declarators_count_as_assignments() {
12250 // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`,
12251 // `int d=0;` are all `init_declarator` nodes with `=` → 4
12252 // assignments. `d=5;` is one plain `assignment_expression`,
12253 // `d+=1;` is one compound. Total A = 4 + 1 + 1 = 6.
12254 check_metrics::<CppParser>(
12255 "void f() { int a=1; int b=2; int c=a+b; int d=0; d=5; d+=1; }",
12256 "foo.cpp",
12257 |metric| {
12258 assert_eq!(metric.abc.assignments_sum(), 6);
12259 insta::assert_json_snapshot!(metric.abc);
12260 },
12261 );
12262 }
12263
12264 #[cfg(feature = "cpp")]
12265 #[test]
12266 fn cpp_declaration_without_initializer_does_not_count() {
12267 // `int a;` parses as a plain declarator inside `declaration`,
12268 // NOT an `init_declarator` (the latter only appears when an
12269 // initializer is present). Regression test for issue #393:
12270 // un-initialised declarations contribute zero to A.
12271 check_metrics::<CppParser>("void f() { int a; a = 5; }", "foo.cpp", |metric| {
12272 // Only `a = 5` (assignment_expression) → A = 1.
12273 assert_eq!(metric.abc.assignments_sum(), 1);
12274 insta::assert_json_snapshot!(metric.abc);
12275 });
12276 }
12277
12278 #[cfg(feature = "cpp")]
12279 #[test]
12280 fn cpp_init_declarator_brace_paren_init_does_not_count() {
12281 // `init_declarator` has two grammar forms: `declarator = value`
12282 // (the `=` form) and `declarator argument_list_or_initializer_list`
12283 // (the `int x(5);` / `int x{5};` direct-init forms). Only the
12284 // first form contains an `=` token, so only it should count.
12285 // Regression test pinning that distinction so that
12286 // refactorings of the init_declarator arm don't accidentally
12287 // start counting direct-init too.
12288 check_metrics::<CppParser>(
12289 "void f() { int x(5); int y{7}; x = 1; }",
12290 "foo.cpp",
12291 |metric| {
12292 // Only `x = 1` (assignment_expression) → A = 1.
12293 assert_eq!(metric.abc.assignments_sum(), 1);
12294 insta::assert_json_snapshot!(metric.abc);
12295 },
12296 );
12297 }
12298
12299 #[cfg(feature = "cpp")]
12300 #[test]
12301 fn cpp_calls_are_branches() {
12302 // Free call + member-fn call (parses as `call_expression` with
12303 // a `field_expression` callee) + `new` allocation. All three
12304 // are branches → B = 3. `auto* p = new int(5)` is also an
12305 // `init_declarator` with `=` so it contributes one assignment
12306 // (post-#393); the snapshot pins that magnitude.
12307 check_metrics::<CppParser>(
12308 "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
12309 "foo.cpp",
12310 |metric| {
12311 assert_eq!(metric.abc.branches_sum(), 3);
12312 assert_eq!(metric.abc.assignments_sum(), 1);
12313 insta::assert_json_snapshot!(metric.abc);
12314 },
12315 );
12316 }
12317
12318 #[cfg(feature = "cpp")]
12319 #[test]
12320 fn cpp_comparisons_count_conditions() {
12321 // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
12322 // `<=>` each contribute one condition. The `||` short-
12323 // circuits add 0 (Fitzpatrick Rule 5, issue #395). Six
12324 // comparisons in the `||` chain plus `<=>` (1) plus the
12325 // outer `== 0` (1) → C = 8.
12326 check_metrics::<CppParser>(
12327 "#include <compare>\n\
12328 bool f(int a, int b) {\n\
12329 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
12330 }\n",
12331 "foo.cpp",
12332 |metric| {
12333 // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 comparisons
12334 // from the chained `||` expression. `(a <=> b) == 0`
12335 // adds the spaceship `<=>` (1) + the outer `== 0`
12336 // (1) → 8 total. The six `||` short-circuits add 0
12337 // (Fitzpatrick Rule 5; issue #395).
12338 assert_eq!(metric.abc.conditions_sum(), 8);
12339 insta::assert_json_snapshot!(metric.abc);
12340 },
12341 );
12342 }
12343
12344 #[cfg(feature = "cpp")]
12345 #[test]
12346 fn cpp_short_circuit_ops_not_counted_directly() {
12347 // `&&` and `||` do NOT count on their own (see the
12348 // module-level `Stats` doc-comment; #395). Phase-2 walker
12349 // counts each operand of a logical chain once (#403), but
12350 // when every operand is itself a relational expression
12351 // (`a == b`, `a > 0`, `b < 0`) the walker doesn't add
12352 // anything on top of the existing comparison-token tally
12353 // — a relational sub-expression scores itself
12354 // (`cpp_condition_scores_itself`), so its chain slot pays
12355 // nothing on top.
12356 check_metrics::<CppParser>(
12357 "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
12358 "foo.cpp",
12359 |metric| {
12360 // == 1, > 1, < 1; the walker on && and || finds
12361 // BinaryExpression operands (not terminal-bool) and
12362 // adds nothing. Total: 3.
12363 assert_eq!(metric.abc.conditions_sum(), 3);
12364 insta::assert_json_snapshot!(metric.abc);
12365 },
12366 );
12367 }
12368
12369 #[cfg(feature = "cpp")]
12370 #[test]
12371 fn cpp_generic_brackets_not_conditions() {
12372 // `<` / `>` in `std::vector<int>` are `template_argument_list`
12373 // delimiters, NOT comparison operators. The `binary_expression`
12374 // parent check must filter them out → C = 0.
12375 check_metrics::<CppParser>(
12376 "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
12377 "foo.cpp",
12378 |metric| {
12379 assert_eq!(metric.abc.conditions_sum(), 0);
12380 insta::assert_json_snapshot!(metric.abc);
12381 },
12382 );
12383 }
12384
12385 #[cfg(feature = "cpp")]
12386 #[test]
12387 fn cpp_else_and_ternary_count_conditions() {
12388 // `if (cond) ... else ...` + ternary `cond ? a : b`. The
12389 // `if`-keyword is NOT a condition (its condition is the
12390 // comparison inside, which counts separately). `else` adds 1,
12391 // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
12392 check_metrics::<CppParser>(
12393 "int f(int a, int b) {\n\
12394 if (a > b) { return a; } else { return b; }\n\
12395 return (b < 0) ? -b : b;\n\
12396 }\n",
12397 "foo.cpp",
12398 |metric| {
12399 assert_eq!(metric.abc.conditions_sum(), 4);
12400 insta::assert_json_snapshot!(metric.abc);
12401 },
12402 );
12403 }
12404
12405 // Issue #1102. A ternary's condition and both branch operands are
12406 // Fitzpatrick Rule 9 unary conditions, exactly as `java_walk_ternary`
12407 // has always counted them. Before the fix the C family scored
12408 // `a ? !b : !c` as 1 — the `?` token alone — against Java's 4.
12409 #[cfg(feature = "cpp")]
12410 #[test]
12411 fn cpp_ternary_operand_slots_count_as_unary_conditions() {
12412 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
12413 check_metrics::<CppParser>("void f() { x = a ? !b : !c; }", "foo.cpp", |metric| {
12414 assert_eq!(metric.abc.conditions_sum(), 4);
12415 });
12416 // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
12417 // fix. The parenthesised condition peels to a
12418 // `binary_expression`, which scores itself, and
12419 // neither branch is negated — the `!` is the type-free proxy for
12420 // "this operand is boolean", so an unnegated branch contributes
12421 // nothing.
12422 check_metrics::<CppParser>("void f() { x = (a > 0) ? b : -b; }", "foo.cpp", |metric| {
12423 assert_eq!(metric.abc.conditions_sum(), 2);
12424 });
12425 // Nested: outer `?` (1) + outer condition `a` (1) + inner `?`
12426 // (1) + inner condition `b` (1) = 4. The outer consequence is
12427 // the inner ternary — which scores itself, and is no
12428 // paren / `!` wrapper — so it adds nothing on its own and the
12429 // inner one is reached by the walk, not by descent.
12430 check_metrics::<CppParser>("void f() { x = a ? b ? c : d : e; }", "foo.cpp", |metric| {
12431 assert_eq!(metric.abc.conditions_sum(), 4);
12432 });
12433 // A negated *condition* is the input where the slot must peel a
12434 // `!`: `cpp_wrapper_operand` takes `!a` down to `a`, which does
12435 // not score itself, so the slot pays once. Every other condition
12436 // fixture in this file wraps a comparison, which scores itself
12437 // and so pays 0 from the slot. `?` (1) + `!a` (1) = 2.
12438 check_metrics::<CppParser>("void f() { x = !a ? b : c; }", "foo.cpp", |metric| {
12439 assert_eq!(metric.abc.conditions_sum(), 2);
12440 });
12441 }
12442
12443 // The GNU short-ternary `a ?: b` elides the consequence, so the
12444 // C-family grammar marks that field optional and the alternative
12445 // lands at child(3) rather than child(4). Addressing the operand
12446 // slots by grammar field name — never by index — is what keeps `!b`
12447 // counted here; a fixed `child(4)` reads `None` and scores 2.
12448 #[cfg(feature = "cpp")]
12449 #[test]
12450 fn cpp_elided_ternary_consequence_still_walks_the_alternative() {
12451 // `?` (1) + condition `a` (1) + `!b` (1) = 3.
12452 check_metrics::<CppParser>("void f() { x = a ?: !b; }", "foo.cpp", |metric| {
12453 assert_eq!(metric.abc.conditions_sum(), 3);
12454 });
12455 }
12456
12457 // `cpp_walk_ternary` is shared by the C, ObjC, and Mozcpp ABC impls
12458 // exactly as `cpp_count_condition` is, so each needs its own
12459 // dispatcher arm. Mozcpp owns no file extension and so gets no
12460 // integration-snapshot coverage at all — this parity assertion is
12461 // its only guard.
12462 //
12463 // The expected value is *derived from the C++ run*, not hardcoded,
12464 // so the four languages cannot silently drift apart if the C++
12465 // expectation ever legitimately moves.
12466 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12467 #[test]
12468 fn c_family_ternary_operand_slots_agree_with_cpp() {
12469 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12470 const SRC: &str = "void f() { x = a ? !b : !c; }\n";
12471 let conditions = abc_conditions;
12472
12473 let cpp = conditions(LANG::Cpp, SRC);
12474 // Non-degenerate: a zeroed reference would make every
12475 // comparison below vacuous.
12476 assert_eq!(cpp, 4, "C++ reference value for `a ? !b : !c`");
12477
12478 assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
12479 assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
12480 assert_eq!(
12481 conditions(
12482 LANG::Objc,
12483 "@implementation Foo\n\
12484 - (void)bar {\n\
12485 x = a ? !b : !c;\n\
12486 }\n\
12487 @end\n",
12488 ),
12489 cpp,
12490 "ObjC must match C++"
12491 );
12492 }
12493
12494 // Issue #1276, C-family half. `cpp_walk_for_statement` is the
12495 // `for` header's counterpart to the `if` / `while` arms: the slot
12496 // is a bare expression rather than a `condition_clause`, read by
12497 // the same slot rule `cpp_walk_ternary` uses. Every fixture is a
12498 // shape only that walker can classify —
12499 // a comparison-shaped condition proves nothing, the `<` token arm
12500 // counts it either way (grammar-dispatch §11).
12501 #[cfg(feature = "cpp")]
12502 #[test]
12503 fn cpp_for_condition_slot_counts_unary_conditions() {
12504 // Bare identifier: the whole condition, no operator token.
12505 check_metrics::<CppParser>("void f(int a) { for (; a; ) {} }", "foo.cpp", |metric| {
12506 assert_eq!(metric.abc.conditions_sum(), 1);
12507 });
12508 // Negation: the slot peels the `!` (`cpp_wrapper_operand`) and
12509 // pays for the operand beneath it.
12510 check_metrics::<CppParser>("void f(int a) { for (; !a; ) {} }", "foo.cpp", |metric| {
12511 assert_eq!(metric.abc.conditions_sum(), 1);
12512 });
12513 // Parentheses: counted because the slot peels them
12514 // (`cpp_wrapper_operand`) — the path #1276 found dead.
12515 check_metrics::<CppParser>("void f(int a) { for (; (a); ) {} }", "foo.cpp", |metric| {
12516 assert_eq!(metric.abc.conditions_sum(), 1);
12517 });
12518 // No-double-count pin: the `<` token arm already counted this
12519 // shape before the fix and the walker must not add a second.
12520 // The two assignments confirm the header parsed as the
12521 // three-clause form rather than degenerating.
12522 check_metrics::<CppParser>(
12523 "void f(int n) { for (int i = 0; i < n; i++) {} }",
12524 "foo.cpp",
12525 |metric| {
12526 assert_eq!(metric.abc.conditions_sum(), 1);
12527 assert_eq!(metric.abc.assignments_sum(), 2);
12528 },
12529 );
12530 // Empty condition: no `condition` field, no decision, zero.
12531 check_metrics::<CppParser>("void f() { for (;;) { break; } }", "foo.cpp", |metric| {
12532 assert_eq!(metric.abc.conditions_sum(), 0);
12533 });
12534 }
12535
12536 // `cpp_walk_for_statement` is shared by the C, ObjC and Mozcpp ABC
12537 // impls the way `cpp_walk_ternary` is, so each needs its own
12538 // dispatcher arm — and Mozcpp, which owns no file extension, has no
12539 // integration-snapshot coverage at all, making this its only guard.
12540 // The expected value is derived from the C++ run rather than
12541 // hardcoded, so the four cannot silently drift apart.
12542 #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12543 #[test]
12544 fn c_family_for_condition_slot_agrees_with_cpp() {
12545 #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
12546 const SRC: &str = "void f(int a) { for (; !a; ) {} }\n";
12547 let conditions = abc_conditions;
12548
12549 let cpp = conditions(LANG::Cpp, SRC);
12550 // Non-degenerate: a zeroed reference makes every comparison
12551 // below vacuous.
12552 assert_eq!(cpp, 1, "C++ reference value for `for (; !a; )`");
12553
12554 assert_eq!(conditions(LANG::C, SRC), cpp, "C must match C++");
12555 assert_eq!(conditions(LANG::Mozcpp, SRC), cpp, "Mozcpp must match C++");
12556 assert_eq!(
12557 conditions(
12558 LANG::Objc,
12559 "@implementation Foo\n\
12560 - (void)bar {\n\
12561 for (; !a; ) {}\n\
12562 }\n\
12563 @end\n",
12564 ),
12565 cpp,
12566 "ObjC must match C++"
12567 );
12568 }
12569
12570 #[cfg(feature = "cpp")]
12571 #[test]
12572 fn cpp_switch_cases_count_default_excluded() {
12573 // `case 1`, `case 2` → 2 conditions. `default` is intentionally
12574 // excluded (the unconditional fallthrough, mirroring cyclomatic's
12575 // `Case`-only count). Since #469 every C-family language —
12576 // Java, C#, Groovy, JS, TS — agrees on this; C++ already did.
12577 // C = 2.
12578 check_metrics::<CppParser>(
12579 "void f(int x) {\n\
12580 switch (x) {\n\
12581 case 1: break;\n\
12582 case 2: break;\n\
12583 default: break;\n\
12584 }\n\
12585 }\n",
12586 "foo.cpp",
12587 |metric| {
12588 assert_eq!(metric.abc.conditions_sum(), 2);
12589 insta::assert_json_snapshot!(metric.abc);
12590 },
12591 );
12592 }
12593
12594 #[cfg(feature = "cpp")]
12595 #[test]
12596 fn cpp_try_catch_count_conditions() {
12597 // `try` and `catch` each add one condition (Fitzpatrick's rule;
12598 // Java's impl above counts them too).
12599 check_metrics::<CppParser>(
12600 "void f() { try { } catch (int) { } catch (...) { } }",
12601 "foo.cpp",
12602 |metric| {
12603 // 1 `try` + 2 `catch` arms = 3.
12604 assert_eq!(metric.abc.conditions_sum(), 3);
12605 insta::assert_json_snapshot!(metric.abc);
12606 },
12607 );
12608 }
12609
12610 #[cfg(feature = "cpp")]
12611 #[test]
12612 fn cpp_complex_function_abc() {
12613 // Mixed-shape regression: assignments, calls, conditions,
12614 // ternary, switch, new. Verified by hand:
12615 // - assignments: `int x = 0` (init_declarator with `=`),
12616 // `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`, `x = b`,
12617 // `auto* p = new int(5)` (init_declarator with `=`) → A = 7
12618 // (post-#393: every `=` in an init_declarator counts).
12619 // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
12620 // - conditions: `a == b` (1) + `a > 0` (1) inside the if;
12621 // `&&` itself is NOT a condition (Fitzpatrick Rule 5,
12622 // issue #395). `a > b` (1) + `?` (1) in the ternary.
12623 // `else` (1, from the `else if` keyword) + `a < b` (1)
12624 // in the else-if. `!x` contributes 1 via the unary-
12625 // conditional walker (Fitzpatrick Rule 9, issue #403):
12626 // the `||` walker treats `!x` as a unary boolean operand
12627 // and counts the wrapped Identifier once. `case 1`,
12628 // `case 2` → 2. `default` excluded. Total C = 9.
12629 check_metrics::<CppParser>(
12630 "int f(int a, int b) {\n\
12631 int x = 0;\n\
12632 x = 5;\n\
12633 x += 2;\n\
12634 x++;\n\
12635 if (a == b && a > 0) {\n\
12636 x = (a > b) ? a : b;\n\
12637 } else if (a < b || !x) {\n\
12638 x = b;\n\
12639 }\n\
12640 switch (x) {\n\
12641 case 1: break;\n\
12642 case 2: break;\n\
12643 default: break;\n\
12644 }\n\
12645 auto* p = new int(5);\n\
12646 return f(a, b);\n\
12647 }\n",
12648 "foo.cpp",
12649 |metric| {
12650 assert_eq!(metric.abc.assignments_sum(), 7);
12651 assert_eq!(metric.abc.branches_sum(), 2);
12652 assert_eq!(metric.abc.conditions_sum(), 9);
12653 insta::assert_json_snapshot!(metric.abc);
12654 },
12655 );
12656 }
12657
12658 #[cfg(feature = "cpp")]
12659 #[test]
12660 fn cpp_if_multiple_conditions() {
12661 // Fitzpatrick Rule 9 walker (issue #403): each operand of a
12662 // `&&` / `||` chain is one condition.
12663 check_metrics::<CppParser>(
12664 "void f(bool a, bool b, bool c, bool d) {\n\
12665 \x20 if (a || b || c || d) {} // +4c\n\
12666 \x20 if (a && b && c) {} // +3c\n\
12667 \x20 if (!a && !b) {} // +2c\n\
12668 }\n",
12669 "foo.cpp",
12670 |metric| {
12671 assert_eq!(metric.abc.conditions_sum(), 9);
12672 insta::assert_json_snapshot!(metric.abc);
12673 },
12674 );
12675 }
12676
12677 #[cfg(feature = "cpp")]
12678 #[test]
12679 fn cpp_while_and_do_while_conditions() {
12680 // Exercise both the WhileStatement and DoStatement arms via
12681 // the walker on the `&&` / `||` tokens inside their parens.
12682 check_metrics::<CppParser>(
12683 "void f(bool a, bool b) {\n\
12684 \x20 while (a || b) {} // +2c\n\
12685 \x20 do {} while (a && !b); // +2c\n\
12686 }\n",
12687 "foo.cpp",
12688 |metric| {
12689 assert_eq!(metric.abc.conditions_sum(), 4);
12690 insta::assert_json_snapshot!(metric.abc);
12691 },
12692 );
12693 }
12694
12695 #[cfg(feature = "cpp")]
12696 #[test]
12697 fn cpp_if_constexpr_condition_counts() {
12698 // Regression for the code-review finding: C++ `if constexpr
12699 // (cond)` puts the `constexpr` keyword at child(1) and the
12700 // condition_clause at child(2). Pre-fix, the dispatcher used
12701 // child(1) and counted zero conditions for the `constexpr`
12702 // form. The fix uses `child_by_field_name("condition")`
12703 // which returns the condition_clause regardless of the
12704 // optional `constexpr` keyword.
12705 check_metrics::<CppParser>(
12706 "template <int N> void f() {\n\
12707 \x20 if constexpr (true) { } // +1c\n\
12708 \x20 if (false) { } // +1c\n\
12709 }\n",
12710 "foo.cpp",
12711 |metric| {
12712 assert_eq!(metric.abc.conditions_sum(), 2);
12713 insta::assert_json_snapshot!(metric.abc);
12714 },
12715 );
12716 }
12717
12718 #[cfg(feature = "cpp")]
12719 #[test]
12720 fn cpp_cast_expression_in_logical_chain_counts() {
12721 // Regression for findings.md round-2 #1 (C++):
12722 // `if ((bool)ptr && ready) {}` had the `||` walker missing
12723 // the `(bool)ptr` operand because `CastExpression` was not
12724 // in the former `cpp_bool_terminal_kinds!()`. Mirrors C#'s
12725 // former `csharp_bool_terminal_kinds!()`, which listed
12726 // `CastExpression` (lesson 19, #372).
12727 check_metrics::<CppParser>(
12728 "void f(void* ptr, bool ready) { if ((bool)ptr && ready) { } }\n",
12729 "foo.cpp",
12730 |metric| {
12731 // `&&` walker counts both operands: `(bool)ptr` (1)
12732 // and `ready` (1). Total: 2.
12733 assert_eq!(metric.abc.conditions_sum(), 2);
12734 insta::assert_json_snapshot!(metric.abc);
12735 },
12736 );
12737 }
12738
12739 #[cfg(feature = "cpp")]
12740 #[test]
12741 fn cpp_qualified_identifier_condition_counts() {
12742 // Regression for findings.md #3 (C++): tree-sitter-cpp emits
12743 // `qualified_identifier` under four kind_ids (573..576) per
12744 // the production-rule path; runtime kind for `ns::flag` is
12745 // 574 (`QualifiedIdentifier2`). Pre-fix the
12746 // former `cpp_bool_terminal_kinds!()` macro listed neither the
12747 // primary nor any alias, so `if (n::flag) {}` reported zero
12748 // conditions. The slot now pays for every variant, since none
12749 // scores itself (lesson #2).
12750 check_metrics::<CppParser>(
12751 "namespace n { extern bool flag; }\n\
12752 void f() { if (n::flag) { } }\n",
12753 "foo.cpp",
12754 |metric| {
12755 assert_eq!(metric.abc.conditions_sum(), 1);
12756 insta::assert_json_snapshot!(metric.abc);
12757 },
12758 );
12759 }
12760
12761 #[cfg(feature = "cpp")]
12762 #[test]
12763 fn cpp_if_boolean_literal_condition() {
12764 check_metrics::<CppParser>(
12765 "void f() {\n\
12766 \x20 if (true) {} // +1c\n\
12767 \x20 if (!false) {} // +1c\n\
12768 \x20 while (true) {} // +1c\n\
12769 \x20 do {} while (false); // +1c\n\
12770 }\n",
12771 "foo.cpp",
12772 |metric| {
12773 assert_eq!(metric.abc.conditions_sum(), 4);
12774 insta::assert_json_snapshot!(metric.abc);
12775 },
12776 );
12777 }
12778
12779 #[cfg(feature = "cpp")]
12780 #[test]
12781 fn cpp_methods_arguments_with_conditions() {
12782 check_metrics::<CppParser>(
12783 "void f(bool a, bool b) {\n\
12784 \x20 m(a, b); // +1b\n\
12785 \x20 m(!a, !b); // +1b +2c\n\
12786 }\n",
12787 "foo.cpp",
12788 |metric| {
12789 assert_eq!(metric.abc.branches_sum(), 2);
12790 assert_eq!(metric.abc.conditions_sum(), 2);
12791 insta::assert_json_snapshot!(metric.abc);
12792 },
12793 );
12794 }
12795
12796 #[cfg(feature = "cpp")]
12797 #[test]
12798 fn cpp_return_with_conditions() {
12799 check_metrics::<CppParser>(
12800 "bool m1(int z) { return !(z >= 0); }\n\
12801 bool m2(bool x) { return (((!x))); }\n\
12802 bool m3(bool x, bool y) { return x && y; }\n",
12803 "foo.cpp",
12804 |metric| {
12805 // m1: !(z >= 0) → `>=` (1). `!` wraps a paren'd
12806 // BinaryExpression — inspect_container reaches
12807 // the inner BinaryExpression and stops, no
12808 // walker count. +1.
12809 // m2: (((!x))) → ReturnStatement → inspect_container
12810 // unwraps three parens + one unary → reaches `x`
12811 // in has_boolean_content=true (seeded by the
12812 // unary `!`). +1.
12813 // m3: x && y → `&&` walker counts both → +2.
12814 // Sum: 1 + 1 + 2 = 4.
12815 assert_eq!(metric.abc.conditions_sum(), 4);
12816 insta::assert_json_snapshot!(metric.abc);
12817 },
12818 );
12819 }
12820
12821 // #1547: `co_return` is a coroutine's `return`, so ABC reads its value
12822 // the same way. Each row's expected value is its `return` twin's. A
12823 // `co_yield` hands a value back without returning, as the JS family's
12824 // `yield` does, and scores no value slot either.
12825 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
12826 fn assert_co_return_scores_like_return(lang: LANG) {
12827 let source = |body: &str| format!("task f(bool a) {{ {body} }}\n");
12828 let rows = [
12829 ("co_return !a;", "return !a;", 1),
12830 ("co_return /*c*/ !a;", "return /*c*/ !a;", 1),
12831 ("co_return a;", "return a;", 0),
12832 ("co_return;", "return;", 0),
12833 ];
12834 for (coroutine, twin, expected) in rows {
12835 assert_eq!(
12836 abc_conditions(lang, &source(twin)),
12837 expected,
12838 "{lang:?} `{twin}`"
12839 );
12840 assert_eq!(
12841 abc_conditions(lang, &source(coroutine)),
12842 expected,
12843 "{lang:?} `{coroutine}`"
12844 );
12845 }
12846 assert_eq!(
12847 abc_conditions(lang, &source("co_yield !a;")),
12848 0,
12849 "{lang:?} co_yield"
12850 );
12851 }
12852
12853 #[cfg(feature = "cpp")]
12854 #[test]
12855 fn cpp_co_return_value_scores_like_return_1547() {
12856 assert_co_return_scores_like_return(LANG::Cpp);
12857 }
12858
12859 // Mozcpp owns no file extension, so this is its only coverage.
12860 #[cfg(feature = "mozcpp")]
12861 #[test]
12862 fn mozcpp_co_return_value_scores_like_return_1547() {
12863 assert_co_return_scores_like_return(LANG::Mozcpp);
12864 }
12865
12866 #[cfg(feature = "cpp")]
12867 #[test]
12868 fn cpp_short_circuit_with_boolean_literal_operand() {
12869 // `a && true` reports 2 conditions: one for the identifier
12870 // operand, one for the `True` literal operand.
12871 check_metrics::<CppParser>(
12872 "bool f(bool a) { return a && true; }\n",
12873 "foo.cpp",
12874 |metric| {
12875 assert_eq!(metric.abc.conditions_sum(), 2);
12876 insta::assert_json_snapshot!(metric.abc);
12877 },
12878 );
12879 }
12880
12881 #[cfg(feature = "javascript")]
12882 #[test]
12883 fn javascript_empty_unit_zero() {
12884 // No code → A=B=C=0. Wires up the trait and exercises the
12885 // per-language compute reachability.
12886 check_metrics::<JavascriptParser>("", "empty.js", |metric| {
12887 assert_eq!(metric.abc.assignments_sum(), 0);
12888 assert_eq!(metric.abc.branches_sum(), 0);
12889 assert_eq!(metric.abc.conditions_sum(), 0);
12890 insta::assert_json_snapshot!(metric.abc);
12891 });
12892 }
12893
12894 #[cfg(feature = "javascript")]
12895 #[test]
12896 fn javascript_plain_and_compound_assignments_count() {
12897 // `let` / `var` declarations behave like TypeScript: only a
12898 // `const` initializer is suppressed. So `let x = 0` does count as
12899 // A=+1; only `const PI = 3.14` would be elided. Plain `x = 5`,
12900 // `x += 2`, `x = 7` all count → A = 4 total here.
12901 check_metrics::<JavascriptParser>(
12902 "function f() { let x = 0; x = 5; x += 2; x = 7; }",
12903 "foo.js",
12904 |metric| {
12905 assert_eq!(metric.abc.assignments_sum(), 4);
12906 assert_eq!(metric.abc.branches_sum(), 0);
12907 assert_eq!(metric.abc.conditions_sum(), 0);
12908 insta::assert_json_snapshot!(metric.abc);
12909 },
12910 );
12911 }
12912
12913 #[cfg(feature = "javascript")]
12914 #[test]
12915 fn javascript_const_initializer_not_assignment() {
12916 // `const PI = 3.14` must NOT count as an assignment — its `=`
12917 // initialises a `const` binding. `let x = 1` and `var y = 2`
12918 // still count (matches the TS impl: only `const` suppresses).
12919 check_metrics::<JavascriptParser>(
12920 "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
12921 "foo.js",
12922 |metric| {
12923 // `const PI` suppressed; `let x = 1`, `var y = 2`,
12924 // `x = 9` all count → A = 3.
12925 assert_eq!(metric.abc.assignments_sum(), 3);
12926 insta::assert_json_snapshot!(metric.abc);
12927 },
12928 );
12929 }
12930
12931 #[cfg(feature = "javascript")]
12932 #[test]
12933 fn javascript_asi_const_does_not_suppress_later_assignments() {
12934 // The issue #1277 reproducer verbatim. JavaScript half of the
12935 // cluster documented at
12936 // `typescript_asi_const_does_not_suppress_later_assignments`.
12937 check_metrics::<JavascriptParser>(
12938 "function f() {
12939 const a = 1
12940 x = 2
12941 return x
12942 }",
12943 "foo.js",
12944 |metric| {
12945 // Pre-#1277 this reported 0; the semicolon-terminated
12946 // spelling reported 1.
12947 assert_eq!(metric.abc.assignments_sum(), 1);
12948 },
12949 );
12950 }
12951
12952 #[cfg(feature = "javascript")]
12953 #[test]
12954 fn javascript_nested_arrow_const_does_not_leak() {
12955 // The ASI leak beside a nested space: the arrow body opens its
12956 // own space, so `y = 1` was always counted there, but
12957 // `const f = () => …` has no `;`, so `h`'s sentinel stayed live
12958 // and suppressed `z = 2`. The stack was per-space state that
12959 // `Stats::merge` never carried, so this is the terminator defect
12960 // and not a second route.
12961 check_metrics::<JavascriptParser>(
12962 "function h() {
12963 const f = () => { y = 1 }
12964 z = 2
12965 }",
12966 "foo.js",
12967 |metric| {
12968 // `y = 1` and `z = 2`; the `const` initializer is
12969 // suppressed. Pre-#1277: 1.
12970 assert_eq!(metric.abc.assignments_sum(), 2);
12971 },
12972 );
12973 }
12974
12975 #[cfg(feature = "javascript")]
12976 #[test]
12977 fn javascript_non_declarator_equals_still_count() {
12978 // An `=` that does not belong to a `const` declarator is always
12979 // an assignment: a class `field_definition` initializer, a
12980 // default-parameter `assignment_pattern`, and a destructured
12981 // parameter's defaults, whose climb crosses the same pattern
12982 // layers as a `const` pattern's but ends at `formal_parameters`.
12983 // A predicate that accepted any pattern ancestor as a declarator
12984 // would zero the last three.
12985 check_metrics::<JavascriptParser>(
12986 "class K { f = 1; g(p = 2, {q = 3} = {}) { let d = 4 } }",
12987 "foo.js",
12988 |metric| {
12989 // `f = 1`, `p = 2`, `q = 3`, `= {}`, `let d = 4`.
12990 assert_eq!(metric.abc.assignments_sum(), 5);
12991 },
12992 );
12993 }
12994
12995 #[cfg(feature = "javascript")]
12996 #[test]
12997 fn javascript_const_initializer_value_assignments_still_count() {
12998 // An `=` inside a `const` initializer's *value* is an
12999 // `assignment_expression`: its climb reaches no pattern layer and
13000 // no declarator, so it counts. The pre-#1277 sentinel
13001 // blanket-suppressed every `=` between `const` and `;`, which is
13002 // the shape that moved the pdf.js corpus snapshots
13003 // (`const bbox = (this.data.rect = …)`).
13004 check_metrics::<JavascriptParser>(
13005 "function m(o, a, b) { const x = (o.p = 1); const y = a || (b = 2); }",
13006 "foo.js",
13007 |metric| {
13008 // `o.p = 1` and `b = 2`; the two `const` initializers are
13009 // suppressed. Pre-#1277: 0.
13010 assert_eq!(metric.abc.assignments_sum(), 2);
13011 },
13012 );
13013 }
13014
13015 #[cfg(feature = "javascript")]
13016 #[test]
13017 fn javascript_const_declarator_shapes_stay_suppressed() {
13018 // JavaScript half of
13019 // `typescript_const_declarator_shapes_stay_suppressed`.
13020 check_metrics::<JavascriptParser>(
13021 "function f(o, xs) {
13022 const a = 1, b = 2
13023 const {c = 5, d: {e = 6} = {}} = o
13024 const [g = 7, ...[h = 8]] = xs
13025 let i = 3
13026 var j = 4
13027 for (const x of xs) { k(x) }
13028 }",
13029 "foo.js",
13030 |metric| {
13031 assert_eq!(metric.abc.assignments_sum(), 2);
13032 },
13033 );
13034 }
13035
13036 #[cfg(feature = "javascript")]
13037 #[test]
13038 fn javascript_increment_and_decrement_count_as_assignment() {
13039 // `x++` (post) and `--x` (pre) both update an lvalue and so
13040 // count as assignments. Combined with the `let x = 0`
13041 // initializer (which counts under the JS/TS rule — only `const`
13042 // suppresses), A = 3.
13043 check_metrics::<JavascriptParser>(
13044 "function f() { let x = 0; x++; --x; }",
13045 "foo.js",
13046 |metric| {
13047 assert_eq!(metric.abc.assignments_sum(), 3);
13048 insta::assert_json_snapshot!(metric.abc);
13049 },
13050 );
13051 }
13052
13053 #[cfg(feature = "javascript")]
13054 #[test]
13055 fn javascript_calls_are_branches() {
13056 // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
13057 // `new_expression` → B = 1. Total B = 2.
13058 check_metrics::<JavascriptParser>(
13059 "function f() { g(1); new Foo(2); }",
13060 "foo.js",
13061 |metric| {
13062 assert_eq!(metric.abc.branches_sum(), 2);
13063 assert_eq!(metric.abc.conditions_sum(), 0);
13064 insta::assert_json_snapshot!(metric.abc);
13065 },
13066 );
13067 }
13068
13069 #[cfg(feature = "javascript")]
13070 #[test]
13071 fn javascript_comparisons_count_conditions() {
13072 // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
13073 // once. The `&&` / `||` short-circuit operators are NOT
13074 // counted as conditions in this impl (matches the TS
13075 // precedent — short-circuit ops are folded into the
13076 // surrounding `if` / control-flow arm, not separately).
13077 // Total C = 8.
13078 check_metrics::<JavascriptParser>(
13079 "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
13080 "foo.js",
13081 |metric| {
13082 assert_eq!(metric.abc.conditions_sum(), 8);
13083 insta::assert_json_snapshot!(metric.abc);
13084 },
13085 );
13086 }
13087
13088 // #1297 in the `js_abc_compute!` expansion, where `LT` / `GT` had
13089 // no gate at all: plain JavaScript has no generics, but
13090 // tree-sitter-javascript parses JSX unconditionally, so the same
13091 // seven tag brackets that
13092 // `tsx_jsx_elements_are_not_conditions` covers scored seven
13093 // conditions in a `.js` file too. Same fixture minus the type
13094 // annotations, same discriminating numbers: 8 pre-fix, 1 after.
13095 #[cfg(feature = "javascript")]
13096 #[test]
13097 fn javascript_jsx_elements_are_not_conditions() {
13098 check_metrics::<JavascriptParser>(
13099 "function f(a, b) {
13100 if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
13101 return <br />;
13102 }",
13103 "foo.js",
13104 |metric| {
13105 assert_eq!(metric.abc.conditions_sum(), 1);
13106 // Fixture-presence anchor: the `className="x"` attribute
13107 // `=` is this fixture's only assignment, so the test
13108 // fails if a later edit drops the JSX. Without it the
13109 // surviving `a < b` alone also scores 1, and the
13110 // conditions assertion would pass while proving nothing.
13111 assert_eq!(metric.abc.assignments_sum(), 1);
13112 },
13113 );
13114 }
13115
13116 #[cfg(feature = "javascript")]
13117 #[test]
13118 fn javascript_number_truthy_condition_counts() {
13119 // Regression for #772: JS treats every non-zero number as
13120 // truthy, so `while (5)` and `x && 5` should each count their
13121 // numeric literal as a Fitzpatrick unary condition. Pre-fix
13122 // the former `javascript_bool_terminal_kinds!()` listed `True` /
13123 // `False` but omitted `Number`, so the walker dropped every numeric-
13124 // truthy operand (mirrors the Lua `Number` fix).
13125 check_metrics::<JavascriptParser>(
13126 "function f(x) { while (5) {} return x && 5; }",
13127 "foo.js",
13128 |metric| {
13129 // `while (5)` → Number literal (+1). `x && 5` → both
13130 // operands count: identifier `x` (+1), Number `5` (+1).
13131 // Total: 3.
13132 assert_eq!(metric.abc.conditions_sum(), 3);
13133 insta::assert_json_snapshot!(metric.abc);
13134 },
13135 );
13136 }
13137
13138 #[cfg(feature = "typescript")]
13139 #[test]
13140 fn typescript_number_truthy_condition_counts() {
13141 // Regression for #772: TS shares the JS truthy semantics. The
13142 // numeric *literal* `5` (kind `Number`) counts; the type-keyword
13143 // `number` (kind `Number2`, the `predefined_type`) must not —
13144 // see the former `typescript_bool_terminal_kinds!`.
13145 check_metrics::<TypescriptParser>(
13146 "function f(x: number) { while (5) {} return x && 5; }",
13147 "foo.ts",
13148 |metric| {
13149 // `while (5)` → +1; `x && 5` → `x` (+1) + `5` (+1).
13150 // Total: 3. The `: number` annotation contributes 0.
13151 assert_eq!(metric.abc.conditions_sum(), 3);
13152 insta::assert_json_snapshot!(metric.abc);
13153 },
13154 );
13155 }
13156
13157 #[cfg(feature = "javascript")]
13158 #[test]
13159 fn javascript_nullish_coalescing_counts_condition() {
13160 // `a ?? b` is one nullish-coalescing operator → C = 1.
13161 check_metrics::<JavascriptParser>(
13162 "function f(a, b) { return a ?? b; }",
13163 "foo.js",
13164 |metric| {
13165 assert_eq!(metric.abc.conditions_sum(), 1);
13166 insta::assert_json_snapshot!(metric.abc);
13167 },
13168 );
13169 }
13170
13171 #[cfg(feature = "javascript")]
13172 #[test]
13173 fn javascript_else_ternary_case_default_try_catch() {
13174 // `else`, `?` (ternary), `case`, `try`, `catch` all count.
13175 // `default` is the unconditional fallthrough → +0 (#469).
13176 // With the comparisons:
13177 // - `a > 0` → 1
13178 // - `else` opens an else_clause → 1
13179 // - `?` ternary → 1
13180 // - the ternary's bare-identifier condition `a` → 1 (#1102)
13181 // - `case 1` → 1
13182 // - `default` → 0 (fallthrough, #469)
13183 // - `try` + `catch` → 2
13184 // Total C = 7.
13185 check_metrics::<JavascriptParser>(
13186 "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
13187 "foo.js",
13188 |metric| {
13189 assert_eq!(metric.abc.conditions_sum(), 7);
13190 insta::assert_json_snapshot!(metric.abc);
13191 },
13192 );
13193 }
13194
13195 // Issue #1102, JS-family half. See
13196 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
13197 // rule; the two families were behind Java by the same three units.
13198 #[cfg(feature = "javascript")]
13199 #[test]
13200 fn javascript_ternary_operand_slots_count_as_unary_conditions() {
13201 // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4.
13202 check_metrics::<JavascriptParser>(
13203 "function f() { x = a ? !b : !c; }",
13204 "foo.js",
13205 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13206 );
13207 // No-double-count pin: `?` (1) + `>` (1) = 2, unchanged by the
13208 // fix — the parenthesised condition unwraps to a
13209 // `binary_expression` (not a boolean terminal) and neither
13210 // branch is negated.
13211 check_metrics::<JavascriptParser>(
13212 "function f() { x = (a > 0) ? b : -b; }",
13213 "foo.js",
13214 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
13215 );
13216 // Nested: two `?` tokens plus the two bare-identifier
13217 // conditions = 4.
13218 check_metrics::<JavascriptParser>(
13219 "function f() { x = a ? b ? c : d : e; }",
13220 "foo.js",
13221 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13222 );
13223 // A negated condition is the only input reaching the walker's
13224 // `else` fallback — see the C++ sibling for why. `?` (1) +
13225 // `!a` (1) = 2.
13226 check_metrics::<JavascriptParser>("function f() { x = !a ? b : c; }", "foo.js", |metric| {
13227 assert_eq!(metric.abc.conditions_sum(), 2);
13228 });
13229 }
13230
13231 // TypeScript expands the same `ts_abc_compute!` arm from a separate
13232 // macro body than JavaScript's `js_abc_compute!`, so wiring one and
13233 // not the other is a live failure mode; TSX and Mozjs are clones of
13234 // these two.
13235 #[cfg(feature = "typescript")]
13236 #[test]
13237 fn typescript_ternary_operand_slots_count_as_unary_conditions() {
13238 check_metrics::<TypescriptParser>(
13239 "function f() { x = a ? !b : !c; }",
13240 "foo.ts",
13241 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
13242 );
13243 check_metrics::<TypescriptParser>(
13244 "function f() { x = (a > 0) ? b : -b; }",
13245 "foo.ts",
13246 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
13247 );
13248 }
13249
13250 // Issue #1276, JS-family half. See
13251 // `cpp_for_condition_slot_counts_unary_conditions` for the rule.
13252 // The JS grammar marks the `condition` field on both the expression
13253 // and the `;` closing it, so `child_by_field_name` is the only
13254 // addressing that lands on the expression for every header shape.
13255 #[cfg(feature = "javascript")]
13256 #[test]
13257 fn javascript_for_condition_slot_counts_unary_conditions() {
13258 // Bare identifier: no operator token anywhere in the header.
13259 check_metrics::<JavascriptParser>("function f(a) { for (; a; ) {} }", "foo.js", |metric| {
13260 assert_eq!(metric.abc.conditions_sum(), 1);
13261 });
13262 // Negation, through the `!` unwrap.
13263 check_metrics::<JavascriptParser>(
13264 "function f(a) { for (; !a; ) {} }",
13265 "foo.js",
13266 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
13267 );
13268 // Parentheses: counts only via the `ForStatement`
13269 // boolean-context seed #1276 found dead.
13270 check_metrics::<JavascriptParser>(
13271 "function f(a) { for (; (a); ) {} }",
13272 "foo.js",
13273 |metric| assert_eq!(metric.abc.conditions_sum(), 1),
13274 );
13275 // No-double-count pin: the `<` arm already counted this shape.
13276 // `let i = 0` and `i++` are the two assignments, which also
13277 // confirms the header parsed as the three-clause form.
13278 check_metrics::<JavascriptParser>(
13279 "function f(n) { for (let i = 0; i < n; i++) {} }",
13280 "foo.js",
13281 |metric| {
13282 assert_eq!(metric.abc.conditions_sum(), 1);
13283 assert_eq!(metric.abc.assignments_sum(), 2);
13284 },
13285 );
13286 // Empty condition: the slot holds an `empty_statement`, which
13287 // is neither a terminal nor a wrapper, so it counts nothing.
13288 check_metrics::<JavascriptParser>(
13289 "function f() { for (;;) { break; } }",
13290 "foo.js",
13291 |metric| assert_eq!(metric.abc.conditions_sum(), 0),
13292 );
13293 }
13294
13295 // TypeScript expands the `ForStatement` arm from `ts_abc_compute!`,
13296 // a separate macro body from JavaScript's `js_abc_compute!`, so
13297 // wiring one and not the other is a live failure mode; TSX and
13298 // Mozjs are the clones of those two. The expected values are
13299 // derived from the JavaScript run rather than hardcoded.
13300 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13301 #[test]
13302 fn js_family_for_condition_slot_agrees_with_javascript() {
13303 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13304 const BARE: &str = "function f(a) { for (; a; ) {} }\n";
13305 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13306 const EMPTY: &str = "function f() { for (;;) { break; } }\n";
13307 let conditions = abc_conditions;
13308
13309 let bare = conditions(LANG::Javascript, BARE);
13310 // Non-degenerate: a zeroed reference makes the comparisons
13311 // below vacuous.
13312 assert_eq!(bare, 1, "JavaScript reference value for `for (; a; )`");
13313 assert_eq!(
13314 conditions(LANG::Javascript, EMPTY),
13315 0,
13316 "JavaScript `for (;;)`"
13317 );
13318
13319 for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
13320 assert_eq!(conditions(lang, BARE), bare, "{lang:?} bare for-condition");
13321 assert_eq!(conditions(lang, EMPTY), 0, "{lang:?} empty for-condition");
13322 }
13323 }
13324
13325 #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13326 #[test]
13327 fn js_family_ternary_operand_slots_agree_with_javascript() {
13328 // `a ? !b : !c` is the one shape that tells the ternary walker
13329 // from the `for`-header walker: both read the `condition` field
13330 // and share the `(&Node, &mut f64)` signature, so a transposed
13331 // pair in a `ts_abc_compute!` / `js_abc_compute!` invocation
13332 // compiles, passes every condition-slot test, and drops only the
13333 // two branch operands. TypeScript alone pinned those before; the
13334 // other three expansions now do too.
13335 #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
13336 const SRC: &str = "function f(a, b, c) { x = a ? !b : !c; }\n";
13337 let javascript = abc_conditions(LANG::Javascript, SRC);
13338 // expected: 4 — the `?`, the `a` condition slot and both negated
13339 // branch operands; non-degenerate by construction.
13340 assert_eq!(
13341 javascript, 4,
13342 "JavaScript reference value for `a ? !b : !c`"
13343 );
13344 for lang in [LANG::Mozjs, LANG::Typescript, LANG::Tsx] {
13345 assert_eq!(
13346 abc_conditions(lang, SRC),
13347 javascript,
13348 "{lang:?} ternary operand slots"
13349 );
13350 }
13351 }
13352
13353 #[cfg(feature = "javascript")]
13354 #[test]
13355 fn javascript_instanceof_counts_condition() {
13356 // `x instanceof Foo` is a binary expression whose operator is
13357 // the `instanceof` keyword token → C = 1.
13358 check_metrics::<JavascriptParser>(
13359 "function f(x) { return x instanceof Foo; }",
13360 "foo.js",
13361 |metric| {
13362 assert_eq!(metric.abc.conditions_sum(), 1);
13363 insta::assert_json_snapshot!(metric.abc);
13364 },
13365 );
13366 }
13367
13368 #[cfg(feature = "javascript")]
13369 #[test]
13370 fn javascript_complex_function_abc() {
13371 // Mixed-shape regression. Verified by hand:
13372 // - assignments: `let x = 0` (a `let` initializer counts)
13373 // + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
13374 // `let p = ...` (likewise) → A = 7.
13375 // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
13376 // - conditions: `a == b`, `a > 0` → 2 inside the if header
13377 // (`&&` is not counted directly). `else` (1) + `a > b`,
13378 // `?` → 2 in the ternary. `a < b` → 1 in the else-if.
13379 // `!x` → 1 from the Fitzpatrick Rule 9 walker on `||`
13380 // (issue #403): the wrapped Identifier counts once.
13381 // `case 1` → 1 in the switch; `default` → 0 (fallthrough,
13382 // #469). Total C = 8.
13383 check_metrics::<JavascriptParser>(
13384 "function f(a, b) {\n\
13385 let x = 0;\n\
13386 x = 5;\n\
13387 x += 2;\n\
13388 x++;\n\
13389 if (a == b && a > 0) {\n\
13390 x = (a > b) ? a : b;\n\
13391 } else if (a < b || !x) {\n\
13392 x = b;\n\
13393 }\n\
13394 switch (x) {\n\
13395 case 1: break;\n\
13396 default: break;\n\
13397 }\n\
13398 let p = new Bar();\n\
13399 return f(a, b);\n\
13400 }\n",
13401 "foo.js",
13402 |metric| {
13403 assert_eq!(metric.abc.assignments_sum(), 7);
13404 assert_eq!(metric.abc.branches_sum(), 2);
13405 assert_eq!(metric.abc.conditions_sum(), 8);
13406 insta::assert_json_snapshot!(metric.abc);
13407 },
13408 );
13409 }
13410
13411 #[cfg(feature = "mozjs")]
13412 #[test]
13413 fn mozjs_asi_const_does_not_suppress_later_assignments() {
13414 // Mozjs half of the #1277 cluster; the fork carries its own
13415 // kind-id numbering, so it needs its own fixture. See
13416 // `typescript_asi_const_does_not_suppress_later_assignments`.
13417 check_metrics::<MozjsParser>(
13418 "function f() {
13419 const a = 1
13420 x = 2
13421 y = 3
13422 }",
13423 "foo.jsm",
13424 |metric| {
13425 assert_eq!(metric.abc.assignments_sum(), 2);
13426 },
13427 );
13428 }
13429
13430 #[cfg(feature = "mozjs")]
13431 #[test]
13432 fn mozjs_const_declarator_shapes_stay_suppressed() {
13433 // Mozjs half of
13434 // `javascript_const_declarator_shapes_stay_suppressed`.
13435 check_metrics::<MozjsParser>(
13436 "function f(o, xs) {
13437 const a = 1, b = 2
13438 const {c = 5, d: {e = 6} = {}} = o
13439 const [g = 7, ...[h = 8]] = xs
13440 let i = 3
13441 var j = 4
13442 for (const x of xs) { k(x) }
13443 }",
13444 "foo.jsm",
13445 |metric| {
13446 assert_eq!(metric.abc.assignments_sum(), 2);
13447 },
13448 );
13449 }
13450
13451 #[cfg(feature = "mozjs")]
13452 #[test]
13453 fn mozjs_complex_function_abc() {
13454 // Mozjs shares JavaScript's expression / statement vocabulary;
13455 // the `js_abc_compute!` macro expands identical token-level
13456 // rules for both. This test pins parity against the JS impl.
13457 check_metrics::<MozjsParser>(
13458 "function f(a, b) {\n\
13459 let x = 0;\n\
13460 x = 5;\n\
13461 x += 2;\n\
13462 x++;\n\
13463 if (a == b && a > 0) {\n\
13464 x = (a > b) ? a : b;\n\
13465 } else if (a < b || !x) {\n\
13466 x = b;\n\
13467 }\n\
13468 switch (x) {\n\
13469 case 1: break;\n\
13470 default: break;\n\
13471 }\n\
13472 let p = new Bar();\n\
13473 return f(a, b);\n\
13474 }\n",
13475 "foo.js",
13476 |metric| {
13477 assert_eq!(metric.abc.assignments_sum(), 7);
13478 assert_eq!(metric.abc.branches_sum(), 2);
13479 assert_eq!(metric.abc.conditions_sum(), 8);
13480 insta::assert_json_snapshot!(metric.abc);
13481 },
13482 );
13483 }
13484
13485 // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots -----
13486
13487 #[cfg(feature = "javascript")]
13488 #[test]
13489 fn javascript_await_expression_condition_counts() {
13490 // Regression for findings.md round-2 #2 (JS):
13491 // `if (await ready()) {}` parses with `await_expression` as
13492 // the condition node inside the `parenthesized_expression`.
13493 // The former `javascript_inspect_container` unwrapped the paren
13494 // but the await child was not in the terminal-bool set, so the
13495 // walker broke without counting. Mirrors C# (lesson 19).
13496 check_metrics::<JavascriptParser>(
13497 "async function ready() { return true; }\n\
13498 async function f() { if (await ready()) { } }\n",
13499 "foo.js",
13500 |metric| {
13501 assert_eq!(metric.abc.branches_sum(), 1);
13502 assert_eq!(metric.abc.conditions_sum(), 1);
13503 insta::assert_json_snapshot!(metric.abc);
13504 },
13505 );
13506 }
13507
13508 #[cfg(feature = "javascript")]
13509 #[test]
13510 fn javascript_member_expression_condition_counts() {
13511 // Regression for findings.md #3 (JS-family): tree-sitter-
13512 // javascript emits `member_expression` under three kind_ids
13513 // (191 primary, 208, 228 — `MemberExpression2/3`) depending
13514 // on the production rule path. The verifier in this audit
13515 // confirmed runtime kind for `o.x` is 208. Pre-fix the
13516 // shared former `js_family_bool_terminal_kinds!()` macro listed
13517 // only the primary, so every `if (o.x) {}` / `o.x && o.y`
13518 // condition silently reported zero. The slot now pays for all
13519 // three aliases, none of which scores itself (lesson #2).
13520 check_metrics::<JavascriptParser>(
13521 "function f(o) {\n\
13522 \x20 if (o.x) {} // +1c\n\
13523 \x20 return o.x && o.y; // +2c (walker on &&)\n\
13524 }\n",
13525 "foo.js",
13526 |metric| {
13527 assert_eq!(metric.abc.conditions_sum(), 3);
13528 insta::assert_json_snapshot!(metric.abc);
13529 },
13530 );
13531 }
13532
13533 #[cfg(feature = "javascript")]
13534 #[test]
13535 fn javascript_if_boolean_literal_condition() {
13536 check_metrics::<JavascriptParser>(
13537 "function f() {\n\
13538 \x20 if (true) {} // +1c\n\
13539 \x20 if (!false) {} // +1c\n\
13540 \x20 while (true) {} // +1c\n\
13541 \x20 do {} while (false); // +1c\n\
13542 }\n",
13543 "foo.js",
13544 |metric| {
13545 assert_eq!(metric.abc.conditions_sum(), 4);
13546 insta::assert_json_snapshot!(metric.abc);
13547 },
13548 );
13549 }
13550
13551 #[cfg(feature = "javascript")]
13552 #[test]
13553 fn javascript_methods_arguments_with_conditions() {
13554 check_metrics::<JavascriptParser>(
13555 "function f(a, b) {\n\
13556 \x20 m(a, b); // +1b\n\
13557 \x20 m(!a, !b); // +1b +2c\n\
13558 }\n",
13559 "foo.js",
13560 |metric| {
13561 assert_eq!(metric.abc.branches_sum(), 2);
13562 assert_eq!(metric.abc.conditions_sum(), 2);
13563 insta::assert_json_snapshot!(metric.abc);
13564 },
13565 );
13566 }
13567
13568 #[cfg(feature = "javascript")]
13569 #[test]
13570 fn javascript_return_with_conditions() {
13571 check_metrics::<JavascriptParser>(
13572 "function m1(z) { return !(z >= 0); }\n\
13573 function m2(x) { return (((!x))); }\n\
13574 function m3(x, y) { return x && y; }\n",
13575 "foo.js",
13576 |metric| {
13577 // m1: 1 (`>=`). m2: 1 (walker unwraps to `x`).
13578 // m3: 2 (`&&` walker counts both terminals).
13579 assert_eq!(metric.abc.conditions_sum(), 4);
13580 insta::assert_json_snapshot!(metric.abc);
13581 },
13582 );
13583 }
13584
13585 #[cfg(feature = "typescript")]
13586 #[test]
13587 fn typescript_if_boolean_literal_condition() {
13588 check_metrics::<TypescriptParser>(
13589 "function f() {\n\
13590 \x20 if (true) {}\n\
13591 \x20 if (!false) {}\n\
13592 \x20 while (true) {}\n\
13593 \x20 do {} while (false);\n\
13594 }\n",
13595 "foo.ts",
13596 |metric| {
13597 assert_eq!(metric.abc.conditions_sum(), 4);
13598 insta::assert_json_snapshot!(metric.abc);
13599 },
13600 );
13601 }
13602
13603 #[cfg(feature = "typescript")]
13604 #[test]
13605 fn typescript_methods_arguments_with_conditions() {
13606 check_metrics::<TypescriptParser>(
13607 "function f(a: boolean, b: boolean) {\n\
13608 \x20 m(a, b);\n\
13609 \x20 m(!a, !b);\n\
13610 }\n",
13611 "foo.ts",
13612 |metric| {
13613 assert_eq!(metric.abc.branches_sum(), 2);
13614 assert_eq!(metric.abc.conditions_sum(), 2);
13615 insta::assert_json_snapshot!(metric.abc);
13616 },
13617 );
13618 }
13619
13620 #[cfg(feature = "typescript")]
13621 #[test]
13622 fn typescript_return_with_conditions() {
13623 check_metrics::<TypescriptParser>(
13624 "function m1(z: number): boolean { return !(z >= 0); }\n\
13625 function m2(x: boolean): boolean { return (((!x))); }\n\
13626 function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
13627 "foo.ts",
13628 |metric| {
13629 assert_eq!(metric.abc.conditions_sum(), 4);
13630 insta::assert_json_snapshot!(metric.abc);
13631 },
13632 );
13633 }
13634
13635 #[cfg(feature = "typescript")]
13636 #[test]
13637 fn tsx_if_boolean_literal_condition() {
13638 check_metrics::<TsxParser>(
13639 "function f() {\n\
13640 \x20 if (true) {}\n\
13641 \x20 if (!false) {}\n\
13642 \x20 while (true) {}\n\
13643 \x20 do {} while (false);\n\
13644 }\n",
13645 "foo.tsx",
13646 |metric| {
13647 assert_eq!(metric.abc.conditions_sum(), 4);
13648 insta::assert_json_snapshot!(metric.abc);
13649 },
13650 );
13651 }
13652
13653 #[cfg(feature = "typescript")]
13654 #[test]
13655 fn tsx_methods_arguments_with_conditions() {
13656 check_metrics::<TsxParser>(
13657 "function f(a: boolean, b: boolean) {\n\
13658 \x20 m(a, b);\n\
13659 \x20 m(!a, !b);\n\
13660 }\n",
13661 "foo.tsx",
13662 |metric| {
13663 assert_eq!(metric.abc.branches_sum(), 2);
13664 assert_eq!(metric.abc.conditions_sum(), 2);
13665 insta::assert_json_snapshot!(metric.abc);
13666 },
13667 );
13668 }
13669
13670 #[cfg(feature = "typescript")]
13671 #[test]
13672 fn tsx_return_with_conditions() {
13673 check_metrics::<TsxParser>(
13674 "function m1(z: number): boolean { return !(z >= 0); }\n\
13675 function m2(x: boolean): boolean { return (((!x))); }\n\
13676 function m3(x: boolean, y: boolean): boolean { return x && y; }\n",
13677 "foo.tsx",
13678 |metric| {
13679 assert_eq!(metric.abc.conditions_sum(), 4);
13680 insta::assert_json_snapshot!(metric.abc);
13681 },
13682 );
13683 }
13684
13685 #[cfg(feature = "mozjs")]
13686 #[test]
13687 fn mozjs_jsx_elements_are_not_conditions() {
13688 // #1297 in the second expansion of `js_abc_compute!`. The
13689 // vendored mozjs fork carries the same three JSX productions as
13690 // upstream tree-sitter-javascript with its own `kind_id`s, so
13691 // the gate is a distinct instantiation and needs its own
13692 // fixture — a passing JavaScript test says nothing about the
13693 // macro's other expansion. Same shape and same discriminating
13694 // numbers as `javascript_jsx_elements_are_not_conditions`: 8
13695 // pre-fix, 1 after.
13696 check_metrics::<MozjsParser>(
13697 "function f(a, b) {
13698 if (a < b) { return <div className=\"x\"><span>hi</span></div>; }
13699 return <br />;
13700 }",
13701 "foo.js",
13702 |metric| {
13703 assert_eq!(metric.abc.conditions_sum(), 1);
13704 // Fixture-presence anchor: the `className="x"` attribute
13705 // `=` is this fixture's only assignment, so the test
13706 // fails if a later edit drops the JSX. Without it the
13707 // surviving `a < b` alone also scores 1, and the
13708 // conditions assertion would pass while proving nothing.
13709 assert_eq!(metric.abc.assignments_sum(), 1);
13710 },
13711 );
13712 }
13713
13714 #[cfg(feature = "mozjs")]
13715 #[test]
13716 fn mozjs_if_boolean_literal_condition() {
13717 check_metrics::<MozjsParser>(
13718 "function f() {\n\
13719 \x20 if (true) {}\n\
13720 \x20 if (!false) {}\n\
13721 \x20 while (true) {}\n\
13722 \x20 do {} while (false);\n\
13723 }\n",
13724 "foo.js",
13725 |metric| {
13726 assert_eq!(metric.abc.conditions_sum(), 4);
13727 insta::assert_json_snapshot!(metric.abc);
13728 },
13729 );
13730 }
13731
13732 #[cfg(feature = "mozjs")]
13733 #[test]
13734 fn mozjs_methods_arguments_with_conditions() {
13735 check_metrics::<MozjsParser>(
13736 "function f(a, b) {\n\
13737 \x20 m(a, b);\n\
13738 \x20 m(!a, !b);\n\
13739 }\n",
13740 "foo.js",
13741 |metric| {
13742 assert_eq!(metric.abc.branches_sum(), 2);
13743 assert_eq!(metric.abc.conditions_sum(), 2);
13744 insta::assert_json_snapshot!(metric.abc);
13745 },
13746 );
13747 }
13748
13749 #[cfg(feature = "mozjs")]
13750 #[test]
13751 fn mozjs_return_with_conditions() {
13752 check_metrics::<MozjsParser>(
13753 "function m1(z) { return !(z >= 0); }\n\
13754 function m2(x) { return (((!x))); }\n\
13755 function m3(x, y) { return x && y; }\n",
13756 "foo.js",
13757 |metric| {
13758 assert_eq!(metric.abc.conditions_sum(), 4);
13759 insta::assert_json_snapshot!(metric.abc);
13760 },
13761 );
13762 }
13763
13764 // ----- JS / TS / Tsx / Mozjs unary-conditional walker -----
13765
13766 #[cfg(feature = "javascript")]
13767 #[test]
13768 fn javascript_if_multiple_conditions() {
13769 check_metrics::<JavascriptParser>(
13770 "function f(a, b, c, d) {\n\
13771 \x20 if (a || b || c || d) {} // +4c\n\
13772 \x20 if (a && b && c) {} // +3c\n\
13773 \x20 if (!a && !b) {} // +2c\n\
13774 }\n",
13775 "foo.js",
13776 |metric| {
13777 assert_eq!(metric.abc.conditions_sum(), 9);
13778 insta::assert_json_snapshot!(metric.abc);
13779 },
13780 );
13781 }
13782
13783 #[cfg(feature = "javascript")]
13784 #[test]
13785 fn javascript_while_and_do_while_conditions() {
13786 check_metrics::<JavascriptParser>(
13787 "function f(a, b) {\n\
13788 \x20 while (a || b) {} // +2c\n\
13789 \x20 do {} while (a && !b); // +2c\n\
13790 }\n",
13791 "foo.js",
13792 |metric| {
13793 assert_eq!(metric.abc.conditions_sum(), 4);
13794 insta::assert_json_snapshot!(metric.abc);
13795 },
13796 );
13797 }
13798
13799 #[cfg(feature = "javascript")]
13800 #[test]
13801 fn javascript_short_circuit_with_boolean_literal_operand() {
13802 check_metrics::<JavascriptParser>(
13803 "function f(a) { return a && true; }\n",
13804 "foo.js",
13805 |metric| {
13806 assert_eq!(metric.abc.conditions_sum(), 2);
13807 insta::assert_json_snapshot!(metric.abc);
13808 },
13809 );
13810 }
13811
13812 #[cfg(feature = "typescript")]
13813 #[test]
13814 fn typescript_if_multiple_conditions() {
13815 check_metrics::<TypescriptParser>(
13816 "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
13817 \x20 if (a || b || c || d) {} // +4c\n\
13818 \x20 if (a && b && c) {} // +3c\n\
13819 \x20 if (!a && !b) {} // +2c\n\
13820 }\n",
13821 "foo.ts",
13822 |metric| {
13823 assert_eq!(metric.abc.conditions_sum(), 9);
13824 insta::assert_json_snapshot!(metric.abc);
13825 },
13826 );
13827 }
13828
13829 #[cfg(feature = "typescript")]
13830 #[test]
13831 fn typescript_while_and_do_while_conditions() {
13832 check_metrics::<TypescriptParser>(
13833 "function f(a: boolean, b: boolean) {\n\
13834 \x20 while (a || b) {} // +2c\n\
13835 \x20 do {} while (a && !b); // +2c\n\
13836 }\n",
13837 "foo.ts",
13838 |metric| {
13839 assert_eq!(metric.abc.conditions_sum(), 4);
13840 insta::assert_json_snapshot!(metric.abc);
13841 },
13842 );
13843 }
13844
13845 #[cfg(feature = "typescript")]
13846 #[test]
13847 fn typescript_short_circuit_with_boolean_literal_operand() {
13848 check_metrics::<TypescriptParser>(
13849 "function f(a: boolean): boolean { return a && true; }\n",
13850 "foo.ts",
13851 |metric| {
13852 assert_eq!(metric.abc.conditions_sum(), 2);
13853 insta::assert_json_snapshot!(metric.abc);
13854 },
13855 );
13856 }
13857
13858 #[cfg(feature = "typescript")]
13859 #[test]
13860 fn tsx_if_multiple_conditions() {
13861 check_metrics::<TsxParser>(
13862 "function f(a: boolean, b: boolean, c: boolean, d: boolean) {\n\
13863 \x20 if (a || b || c || d) {} // +4c\n\
13864 \x20 if (a && b && c) {} // +3c\n\
13865 \x20 if (!a && !b) {} // +2c\n\
13866 }\n",
13867 "foo.tsx",
13868 |metric| {
13869 assert_eq!(metric.abc.conditions_sum(), 9);
13870 insta::assert_json_snapshot!(metric.abc);
13871 },
13872 );
13873 }
13874
13875 #[cfg(feature = "typescript")]
13876 #[test]
13877 fn tsx_while_and_do_while_conditions() {
13878 check_metrics::<TsxParser>(
13879 "function f(a: boolean, b: boolean) {\n\
13880 \x20 while (a || b) {} // +2c\n\
13881 \x20 do {} while (a && !b); // +2c\n\
13882 }\n",
13883 "foo.tsx",
13884 |metric| {
13885 assert_eq!(metric.abc.conditions_sum(), 4);
13886 insta::assert_json_snapshot!(metric.abc);
13887 },
13888 );
13889 }
13890
13891 #[cfg(feature = "typescript")]
13892 #[test]
13893 fn tsx_short_circuit_with_boolean_literal_operand() {
13894 check_metrics::<TsxParser>(
13895 "function f(a: boolean): boolean { return a && true; }\n",
13896 "foo.tsx",
13897 |metric| {
13898 assert_eq!(metric.abc.conditions_sum(), 2);
13899 insta::assert_json_snapshot!(metric.abc);
13900 },
13901 );
13902 }
13903
13904 #[cfg(feature = "mozjs")]
13905 #[test]
13906 fn mozjs_if_multiple_conditions() {
13907 check_metrics::<MozjsParser>(
13908 "function f(a, b, c, d) {\n\
13909 \x20 if (a || b || c || d) {} // +4c\n\
13910 \x20 if (a && b && c) {} // +3c\n\
13911 \x20 if (!a && !b) {} // +2c\n\
13912 }\n",
13913 "foo.js",
13914 |metric| {
13915 assert_eq!(metric.abc.conditions_sum(), 9);
13916 insta::assert_json_snapshot!(metric.abc);
13917 },
13918 );
13919 }
13920
13921 #[cfg(feature = "mozjs")]
13922 #[test]
13923 fn mozjs_while_and_do_while_conditions() {
13924 check_metrics::<MozjsParser>(
13925 "function f(a, b) {\n\
13926 \x20 while (a || b) {} // +2c\n\
13927 \x20 do {} while (a && !b); // +2c\n\
13928 }\n",
13929 "foo.js",
13930 |metric| {
13931 assert_eq!(metric.abc.conditions_sum(), 4);
13932 insta::assert_json_snapshot!(metric.abc);
13933 },
13934 );
13935 }
13936
13937 #[cfg(feature = "mozjs")]
13938 #[test]
13939 fn mozjs_short_circuit_with_boolean_literal_operand() {
13940 check_metrics::<MozjsParser>(
13941 "function f(a) { return a && true; }\n",
13942 "foo.js",
13943 |metric| {
13944 assert_eq!(metric.abc.conditions_sum(), 2);
13945 insta::assert_json_snapshot!(metric.abc);
13946 },
13947 );
13948 }
13949
13950 // ---------- Perl ABC tests ----------
13951
13952 #[cfg(feature = "perl")]
13953 #[test]
13954 fn perl_empty_unit_zero() {
13955 // Empty source produces zero ABC magnitude — pins the trait
13956 // wiring without exercising any compute branch.
13957 check_metrics::<PerlParser>("", "empty.pl", |metric| {
13958 assert_eq!(metric.abc.assignments_sum(), 0);
13959 assert_eq!(metric.abc.branches_sum(), 0);
13960 assert_eq!(metric.abc.conditions_sum(), 0);
13961 insta::assert_json_snapshot!(metric.abc);
13962 });
13963 }
13964
13965 #[cfg(feature = "perl")]
13966 #[test]
13967 fn perl_plain_and_compound_assignments_count() {
13968 // `my $x = 0` parses as a `binary_expression` with an `=`
13969 // token, so the initialiser counts (Perl has no equivalent of
13970 // the JS `const` initialiser-suppression rule). Each
13971 // assignment operator token contributes one assignment:
13972 // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
13973 // from the `my $x = 0` initialiser and the later `$x = 5`
13974 // reassignment.
13975 check_metrics::<PerlParser>(
13976 "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
13977 "foo.pl",
13978 |metric| {
13979 assert_eq!(metric.abc.assignments_sum(), 5);
13980 assert_eq!(metric.abc.branches_sum(), 0);
13981 assert_eq!(metric.abc.conditions_sum(), 0);
13982 insta::assert_json_snapshot!(metric.abc);
13983 },
13984 );
13985 }
13986
13987 #[cfg(feature = "perl")]
13988 #[test]
13989 fn perl_calls_are_branches() {
13990 // `foo()` parses as `call_expression_with_args_with_brackets`
13991 // wrapping an inner `call_expression_with_bareword(foo)`;
13992 // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
13993 // appears as a standalone bareword. The bareword-inside-
13994 // wrapper case must NOT double-count — only the outer wrapper
13995 // contributes a branch. So B = 3 (foo, bar, shift), not 5.
13996 check_metrics::<PerlParser>(
13997 "sub f { foo(); bar 1, 2; my $a = shift; }",
13998 "foo.pl",
13999 |metric| {
14000 // shift's `my $a = shift` initialiser contributes one
14001 // assignment via the `=` token.
14002 assert_eq!(metric.abc.assignments_sum(), 1);
14003 assert_eq!(metric.abc.branches_sum(), 3);
14004 assert_eq!(metric.abc.conditions_sum(), 0);
14005 insta::assert_json_snapshot!(metric.abc);
14006 },
14007 );
14008 }
14009
14010 #[cfg(feature = "perl")]
14011 #[test]
14012 fn perl_method_invocation_counts_as_branch() {
14013 // `$obj->method(...)` parses as `method_invocation`. Any
14014 // arrow-dispatch counts as one branch regardless of how the
14015 // arguments are passed.
14016 check_metrics::<PerlParser>(
14017 "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
14018 "foo.pl",
14019 |metric| {
14020 // `my $obj = shift` → A=1, B=1 (shift bareword).
14021 // `$obj->run($x)` and `$obj->ping` → 2 more branches.
14022 assert_eq!(metric.abc.assignments_sum(), 1);
14023 assert_eq!(metric.abc.branches_sum(), 3);
14024 assert_eq!(metric.abc.conditions_sum(), 0);
14025 insta::assert_json_snapshot!(metric.abc);
14026 },
14027 );
14028 }
14029
14030 // #1297's own sweep cleared Perl, and it was wrong. `<STDIN>` does
14031 // lex as a single `standard_input` token and scores 0, which is
14032 // what the issue measured — but the filehandle and lexical-handle
14033 // readlines do not: `<FH>` is `standard_input_to_identifier` and
14034 // `<$fh>` is `standard_input_to_variable`, each a plain three-token
14035 // sequence whose brackets are the same bare `<` / `>` a comparison
14036 // uses, and the arm was ungated. Each scored two phantom
14037 // conditions.
14038 //
14039 // All three readline spellings against two genuine comparisons, so
14040 // every mis-aim lands on its own number: 6 pre-fix, 2 once the arm
14041 // allows `BinaryExpression`, 0 if it allows the wrong parent or the
14042 // fixture stops parsing. The assertion is also the grammar-dispatch
14043 // §8 pin — `cyclomatic()` is 3 on this space, so decisions is 2 and
14044 // the two counts agree exactly.
14045 #[cfg(feature = "perl")]
14046 #[test]
14047 fn perl_readline_angle_brackets_are_not_conditions() {
14048 check_func_space::<PerlParser, _>(
14049 "sub g {\n\
14050 my ($a, $b, $fh) = @_;\n\
14051 my $l = <FH>;\n\
14052 my $m = <$fh>;\n\
14053 my $n = <STDIN>;\n\
14054 if ($a < $b) { return 1; }\n\
14055 if ($a > $b) { return 2; }\n\
14056 return 0;\n\
14057 }",
14058 "foo.pl",
14059 |space| {
14060 assert_deepest_conditions_match_cyclomatic(&space, 2);
14061 // Fixture-presence anchor: the three readlines are
14062 // three of this sub's four assignments (the fourth is
14063 // the `my (...) = @_` unpack), so dropping them from
14064 // the fixture fails here rather than leaving a test
14065 // that still reports 2 conditions and proves nothing.
14066 assert_eq!(space.metrics.abc.assignments_sum(), 4);
14067 },
14068 );
14069 }
14070
14071 #[cfg(feature = "perl")]
14072 #[test]
14073 fn perl_numeric_and_string_comparisons_count_conditions() {
14074 // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
14075 // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
14076 // fire once per token. The sample below uses one of each →
14077 // C = 14. No assignments, no branches.
14078 check_metrics::<PerlParser>(
14079 "sub f {\n\
14080 my $r;\n\
14081 $r = $a == $b;\n\
14082 $r = $a != $b;\n\
14083 $r = $a < $b;\n\
14084 $r = $a > $b;\n\
14085 $r = $a <= $b;\n\
14086 $r = $a >= $b;\n\
14087 $r = $a <=> $b;\n\
14088 $r = $a eq $b;\n\
14089 $r = $a ne $b;\n\
14090 $r = $a lt $b;\n\
14091 $r = $a gt $b;\n\
14092 $r = $a le $b;\n\
14093 $r = $a ge $b;\n\
14094 $r = $a cmp $b;\n\
14095 }",
14096 "foo.pl",
14097 |metric| {
14098 // 15 `=` tokens: one declaration `my $r` (no `=`),
14099 // then 14 `$r = …` plus there's no `=` in `my $r;`.
14100 // Actually: `my $r;` has no `=`; the 14 `$r = …` are
14101 // 14 `=` tokens. So A=14, C=14.
14102 assert_eq!(metric.abc.assignments_sum(), 14);
14103 assert_eq!(metric.abc.branches_sum(), 0);
14104 assert_eq!(metric.abc.conditions_sum(), 14);
14105 insta::assert_json_snapshot!(metric.abc);
14106 },
14107 );
14108 }
14109
14110 #[cfg(feature = "perl")]
14111 #[test]
14112 fn perl_short_circuit_not_counted_directly_ternary_counts() {
14113 // `&&`, `||`, `//`, low-precedence `and`, `or` are NOT
14114 // counted as conditions on their own (Fitzpatrick Rule 5;
14115 // #395) — instead each operand is counted as a unary
14116 // conditional by the walker (Rule 9; #403). `xor` evaluates
14117 // both operands, so it is a value like `^` and, outside a
14118 // slot, scores nothing (#1536). The pinned
14119 // tree-sitter-perl parses the low-precedence `and` as a
14120 // two-operand `unary_expression` rather than a
14121 // `binary_expression`; the walker reads the operands of either
14122 // since #1526, which is what moved this count from 10 to 14.
14123 // The low-precedence forms bind looser than `=`, so
14124 // `$r = $a and $b` is `($r = $a) and $b`, and its left operand
14125 // pays through the assignment's value.
14126 // Net: 5 chain lines × 2 operands + 0 for the `xor` line + 1
14127 // ternary node + 1 for the ternary's bare `$a` condition operand
14128 // (#1102) = 12.
14129 check_metrics::<PerlParser>(
14130 "sub f {\n\
14131 my $r;\n\
14132 $r = $a && $b;\n\
14133 $r = $a || $b;\n\
14134 $r = $a // $b;\n\
14135 $r = $a and $b;\n\
14136 $r = $a or $b;\n\
14137 $r = $a xor $b;\n\
14138 $r = $a ? 1 : 2;\n\
14139 }",
14140 "foo.pl",
14141 |metric| {
14142 // 7 `=` tokens (one per reassignment line).
14143 assert_eq!(metric.abc.assignments_sum(), 7);
14144 assert_eq!(metric.abc.branches_sum(), 0);
14145 // 5 chain lines × 2 operands + 1 ternary node + 1 for
14146 // its bare `$a` condition operand = 12.
14147 assert_eq!(metric.abc.conditions_sum(), 12);
14148 insta::assert_json_snapshot!(metric.abc);
14149 },
14150 );
14151 }
14152
14153 // Issue #1102, Perl half. See
14154 // `cpp_ternary_operand_slots_count_as_unary_conditions` for the
14155 // rule. Like PHP, Perl's ABC dispatcher has no `?`-token arm — the
14156 // grammar does emit the token, but the `ternary_expression` node is
14157 // what carries the tally's +1. tree-sitter-perl names the branch
14158 // fields `true` / `false` rather than the C-family `consequence` /
14159 // `alternative`, so a copied C-family gate would match nothing.
14160 #[cfg(feature = "perl")]
14161 #[test]
14162 fn perl_ternary_operand_slots_count_as_unary_conditions() {
14163 // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4.
14164 check_metrics::<PerlParser>("sub f { my $x = $a ? !$b : !$c; }", "foo.pl", |metric| {
14165 assert_eq!(metric.abc.conditions_sum(), 4);
14166 });
14167 // No-double-count pin: ternary (1) + `>` (1) = 2, unchanged by
14168 // the fix.
14169 check_metrics::<PerlParser>(
14170 "sub f { my $x = ($a > 0) ? $b : -$b; }",
14171 "foo.pl",
14172 |metric| assert_eq!(metric.abc.conditions_sum(), 2),
14173 );
14174 // A negated *condition*: the slot peels the `!`
14175 // (`perl_wrapper_operand`) and pays for `$a`, which does not
14176 // score itself. ternary (1) + `!$a` (1) = 2.
14177 check_metrics::<PerlParser>("sub f { my $x = !$a ? $b : $c; }", "foo.pl", |metric| {
14178 assert_eq!(metric.abc.conditions_sum(), 2);
14179 });
14180 // Nested: two ternary nodes plus the two bare-variable
14181 // conditions = 4.
14182 check_metrics::<PerlParser>(
14183 "sub f { my $x = $a ? ($b ? $c : $d) : $e; }",
14184 "foo.pl",
14185 |metric| assert_eq!(metric.abc.conditions_sum(), 4),
14186 );
14187 }
14188
14189 // Issue #1273. tree-sitter-perl 1.1.2 parses `$a > $b ? 1 : 2` as
14190 // `$a > ($b ? 1 : 2)`, and the ternary's condition slot then paid
14191 // for `$b`: 3 against JavaScript's 2 for `a > b ? 1 : 2`. Every
14192 // chaining comparison does it; each row must also equal its
14193 // parenthesised spelling, which parses correctly and so takes its
14194 // value from outside the fix.
14195 #[cfg(feature = "perl")]
14196 #[test]
14197 fn perl_comparison_conditioned_ternary_counts_the_comparison_once() {
14198 let operators = [
14199 "<", ">", "<=", ">=", "lt", "gt", "le", "ge", "==", "!=", "eq", "ne",
14200 ];
14201 for op in operators {
14202 let bare = format!("sub f {{ my $m = $a {op} $b ? 1 : 2; }}\n");
14203 let grouped = format!("sub f {{ my $m = ($a {op} $b) ? 1 : 2; }}\n");
14204 // The ternary (1) and the comparison token (1).
14205 assert_eq!(abc_conditions(LANG::Perl, &bare), 2, "`{op}`");
14206 assert_eq!(abc_conditions(LANG::Perl, &grouped), 2, "`({op})`");
14207 }
14208 }
14209
14210 // Issue #1273, the general shape: a chaining comparison swallows
14211 // every lower-precedence operator to its right, so a logical chain
14212 // misparses too — `$a > $b && $c` is `$a > ($b && $c)`, and the
14213 // chain walker paid for `$b`. Expected values are the JavaScript
14214 // twins (`a > b && c ? 1 : 2` = 3, …) or, where JavaScript has no
14215 // spelling, the parenthesised Perl one.
14216 #[cfg(feature = "perl")]
14217 #[test]
14218 fn perl_misparsed_comparison_operand_is_not_a_condition() {
14219 let cases = [
14220 // `<` `<` `?`; `($a < $b < $c) ? 1 : 2` is 3.
14221 ("my $m = $a < $b < $c ? 1 : 2;", 3),
14222 // `>` `?` `>` `?`, as `a > b ? (c > d ? 1 : 2) : 3`.
14223 ("my $m = $a > $b ? $c > $d ? 1 : 2 : 3;", 4),
14224 // Two ternaries and two comparisons, as the JS twin.
14225 ("if ($a > $b ? $c : $d) { } return $a == $b ? 1 : 0;", 4),
14226 // The chain is the ternary's condition: `>` `?` `$c`.
14227 ("my $m = $a > $b && $c ? 1 : 2;", 3),
14228 // The ternary hangs off the chain's right comparison.
14229 ("my $m = $x > 0 && $y > 0 ? 1 : 2;", 3),
14230 ("my $m = $x && $a > $b ? 1 : 2;", 3),
14231 ("if ($a > $b && $c) { }", 2),
14232 ("if ($a eq $b && $c eq $d) { }", 2),
14233 // `&&` is the first operand of `||`: the climb crosses it.
14234 ("if ($a > $b && $c || $d) { }", 3),
14235 ("if ($a > $b && $c && $d) { }", 3),
14236 // `and` is a two-operand `unary_expression`, which the
14237 // climb crosses too: `(($a > $b) && $c) and $d` is 3.
14238 ("if ($a > $b and $c) { }", 2),
14239 ("if ($a > $b && $c and $d) { }", 3),
14240 // A comment between the operator and the ternary is an
14241 // `extra` sibling, not the operator.
14242 ("my $m = $a > # c\n $b ? 1 : 2;", 2),
14243 ];
14244 for (body, expected) in cases {
14245 let src = format!("sub f {{ {body} }}\n");
14246 assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14247 }
14248 }
14249
14250 // Issue #1273: shapes the workaround must leave alone. The explicit
14251 // `$a > ($b ? 1 : 2)` puts an `array` between the comparison and
14252 // the ternary, so `$b` really is a condition: 3, as
14253 // `a > (b ? 1 : 2)` in JavaScript. `<=>`, `cmp` and `=~` do not
14254 // chain and parse correctly; those rows pin that grammar fact, not
14255 // the workaround's operator list — adding `<=>` to the list moves
14256 // nothing, since no tree ever puts a ternary straight after it.
14257 #[cfg(feature = "perl")]
14258 #[test]
14259 fn perl_correctly_parsed_ternaries_keep_their_condition_slot() {
14260 let cases = [
14261 ("my $m = ($a > $b) ? 1 : 2;", 2),
14262 ("my $m = $a <=> $b ? 1 : 2;", 2),
14263 ("my $m = $a cmp $b ? 1 : 2;", 2),
14264 ("my $m = $a =~ /x/ ? 1 : 2;", 2),
14265 ("my $m = $a > ($b ? 1 : 2);", 3),
14266 ("my $m = $a ? !$b : !$c;", 4),
14267 ("my $m = $a && $b ? 1 : 2;", 3),
14268 ];
14269 for (body, expected) in cases {
14270 let src = format!("sub f {{ {body} }}\n");
14271 assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14272 }
14273 }
14274
14275 #[cfg(feature = "perl")]
14276 #[test]
14277 fn perl_elsif_and_else_count_conditions() {
14278 // `if (… == …) { … } elsif (… < …) { … } else { … }` →
14279 // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
14280 // `else_clause` each + 1 → C = 4. Branches: 0 (only
14281 // assignments). Assignments: just the `=` initialisers /
14282 // reassignments — there are 4 here (`$x` init plus three
14283 // `$x = …` reassigns).
14284 check_metrics::<PerlParser>(
14285 "sub f {\n\
14286 my $x = 0;\n\
14287 if ($a == $b) {\n\
14288 $x = 1;\n\
14289 } elsif ($a < $b) {\n\
14290 $x = 2;\n\
14291 } else {\n\
14292 $x = 3;\n\
14293 }\n\
14294 }",
14295 "foo.pl",
14296 |metric| {
14297 assert_eq!(metric.abc.assignments_sum(), 4);
14298 assert_eq!(metric.abc.branches_sum(), 0);
14299 assert_eq!(metric.abc.conditions_sum(), 4);
14300 insta::assert_json_snapshot!(metric.abc);
14301 },
14302 );
14303 }
14304
14305 #[cfg(feature = "perl")]
14306 #[test]
14307 fn perl_regex_match_operators_count_conditions() {
14308 // `=~` and `!~` are pattern-match operators; we count both
14309 // as conditions because they evaluate the regex match in a
14310 // boolean context.
14311 check_metrics::<PerlParser>(
14312 "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
14313 "foo.pl",
14314 |metric| {
14315 // 3 `=` tokens, 0 branches except `shift` bareword.
14316 assert_eq!(metric.abc.assignments_sum(), 3);
14317 assert_eq!(metric.abc.branches_sum(), 1);
14318 assert_eq!(metric.abc.conditions_sum(), 2);
14319 insta::assert_json_snapshot!(metric.abc);
14320 },
14321 );
14322 }
14323
14324 #[cfg(feature = "perl")]
14325 #[test]
14326 fn perl_complex_function_abc() {
14327 // Mixed program exercising every category. Computed
14328 // expected:
14329 // Assignments: `my $i = 0` (1), `$i++` is a unary
14330 // increment — Perl's grammar emits `PLUSPLUS` not an `=`
14331 // operator, so it does NOT count under the operator-
14332 // token rule. The for-loop's `$i++` is similarly
14333 // uncounted.
14334 // Total A: 1 from `my $i = 0`, 1 from `$total += $i`
14335 // (the `+=` token) → A = 2.
14336 // Branches: `do_work($i)` → 1; `print "done\n"` is a
14337 // call_expression_with_spaced_args → 1; `return $total`
14338 // uses the `return` keyword not a call → 0. B = 2.
14339 // Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
14340 // 1; `else_clause` → 1. C = 3.
14341 check_metrics::<PerlParser>(
14342 "sub run {\n\
14343 my $total = 0;\n\
14344 for (my $i = 0; $i < 10; $i++) {\n\
14345 if ($i % 2 == 0) {\n\
14346 do_work($i);\n\
14347 } else {\n\
14348 $total += $i;\n\
14349 }\n\
14350 }\n\
14351 print \"done\\n\";\n\
14352 return $total;\n\
14353 }",
14354 "foo.pl",
14355 |metric| {
14356 // `my $total = 0` is one `=`; `my $i = 0` is another
14357 // `=`; `$total += $i` is one `+=`. Total = 3.
14358 assert_eq!(metric.abc.assignments_sum(), 3);
14359 assert_eq!(metric.abc.branches_sum(), 2);
14360 assert_eq!(metric.abc.conditions_sum(), 3);
14361 insta::assert_json_snapshot!(metric.abc);
14362 },
14363 );
14364 }
14365
14366 #[cfg(feature = "perl")]
14367 #[test]
14368 fn perl_if_multiple_conditions() {
14369 // Fitzpatrick Rule 9 walker (issue #403): each operand of a
14370 // `&&` / `||` / `//` / `and` / `or` chain is one
14371 // condition. ScalarVariable operands ($a, $b, …) qualify as
14372 // terminal-bool kinds for the walker.
14373 check_metrics::<PerlParser>(
14374 "sub f {\n\
14375 my ($a, $b, $c, $d) = @_;\n\
14376 if ($a || $b || $c || $d) { return 1; } # +4c\n\
14377 if ($a && $b && $c) { return 2; } # +3c\n\
14378 if (!$a && !$b) { return 3; } # +2c\n\
14379 return 0;\n\
14380 }",
14381 "foo.pl",
14382 |metric| {
14383 assert_eq!(metric.abc.conditions_sum(), 9);
14384 insta::assert_json_snapshot!(metric.abc);
14385 },
14386 );
14387 }
14388
14389 #[cfg(feature = "perl")]
14390 #[test]
14391 fn perl_while_and_until_conditions() {
14392 // Perl has no `do { ... } while(cond);` shape in this grammar
14393 // — `while` and `until` are the loop forms with a condition
14394 // slot. The walker fires on each `&&` / `||` token inside
14395 // those headers.
14396 check_metrics::<PerlParser>(
14397 "sub f {\n\
14398 my ($a, $b) = @_;\n\
14399 while ($a || $b) { last; } # +2c\n\
14400 until ($a && !$b) { last; } # +2c\n\
14401 }",
14402 "foo.pl",
14403 |metric| {
14404 assert_eq!(metric.abc.conditions_sum(), 4);
14405 insta::assert_json_snapshot!(metric.abc);
14406 },
14407 );
14408 }
14409
14410 #[cfg(feature = "perl")]
14411 #[test]
14412 fn perl_for_header_condition_slot_counts_unary_conditions() {
14413 // The Perl half of #1276. The C-style `for` header's condition
14414 // slot is a Rule 9 unary condition like the `if` slot: a bare
14415 // `$ok`, a negated `!$ok` and a parenthesised `($ok)` each count
14416 // one, an empty header and a `foreach` count zero, and a
14417 // comparison-shaped `$i < $n` stays at the one the `<` arm
14418 // already counts. Every C-style row is the three-clause spelling
14419 // because tree-sitter-perl does not parse an empty initializer.
14420 let cases = [
14421 ("for (my $i = 0; $ok; $i++) { }", 1),
14422 ("for (my $i = 0; !$ok; $i++) { }", 1),
14423 ("for (my $i = 0; ($ok); $i++) { }", 1),
14424 ("for (my $i = 0; $i < $n; $i++) { }", 1),
14425 ("for (my $i = 0; $ok && $j; $i++) { }", 2),
14426 ("for (;;) { last; }", 0),
14427 ("for my $x (@l) { }", 0),
14428 ];
14429 let mut ran = 0;
14430 for (body, expected) in cases {
14431 let src = format!("sub f {{ {body} }}\n");
14432 assert_eq!(abc_conditions(LANG::Perl, &src), expected, "`{body}`");
14433 ran += 1;
14434 }
14435 assert_eq!(ran, cases.len());
14436 assert!(cases.iter().any(|&(_, n)| n == 0));
14437 assert!(cases.iter().any(|&(_, n)| n == 2));
14438
14439 // The slot agrees with the `if` slot for every shape, which is
14440 // the property the fix restores; the reference is non-degenerate
14441 // by the table above.
14442 for condition in ["$ok", "!$ok", "($ok)", "$ok && $j"] {
14443 let in_for = format!("sub f {{ for (my $i = 0; {condition}; $i++) {{ }} }}\n");
14444 let in_if = format!("sub f {{ if ({condition}) {{ }} }}\n");
14445 assert_eq!(
14446 abc_conditions(LANG::Perl, &in_for),
14447 abc_conditions(LANG::Perl, &in_if),
14448 "`{condition}` must score alike in a `for` header and an `if`",
14449 );
14450 }
14451 }
14452
14453 #[cfg(feature = "perl")]
14454 #[test]
14455 fn perl_short_circuit_counts_scalar_variable_operands() {
14456 // `$a && $b` reports 2 conditions — one walker count per
14457 // `ScalarVariable` operand. Renamed from the cross-language
14458 // `_with_boolean_literal_operand` convention because Perl has
14459 // no readily-grammar-exposed boolean literal in an `&&`
14460 // operand slot at the pinned grammar version (the `Boolean`
14461 // kind only fires on the `boolean` pragma's named constants,
14462 // not bareword `1` / `0`). Two scalar variables are the
14463 // grammar-stable terminal-set witness for Perl.
14464 check_metrics::<PerlParser>(
14465 "sub f { my ($a) = @_; return $a && $b; }\n",
14466 "foo.pl",
14467 |metric| {
14468 assert_eq!(metric.abc.conditions_sum(), 2);
14469 insta::assert_json_snapshot!(metric.abc);
14470 },
14471 );
14472 }
14473
14474 #[cfg(feature = "perl")]
14475 #[test]
14476 fn perl_array_in_binary_operand_descends_to_scalar_context_value() {
14477 // Regression test for the code-review findings on the
14478 // Phase-2B Perl walker:
14479 // - Pre-fix-A: the former `perl_inspect_container` descended `Array`
14480 // via `node.child(1)` — the FIRST element — wrongly
14481 // attributing `$x` for `($x, $y)` (semantically `$y`
14482 // is the scalar-context value).
14483 // - Fix-A (the `array_is_paren` guard, 5db8078): dropped
14484 // Array-as-paren entirely in `BinaryExpression` operand
14485 // contexts to avoid the wrong attribution — but
14486 // regressed `$a || ($x)` (single paren-grouped operand)
14487 // to C=1 instead of 2.
14488 // - Fix-B (this change): keeps Array-as-paren unconditional
14489 // but descends via the LAST named child. `$a || ($x)`
14490 // reaches `$x` (count both operands → 2);
14491 // `$a || ($x, $y)` reaches `$y` (count `$a` + `$y` →
14492 // still 2, matching Fitzpatrick Rule 7 "one per
14493 // operand"); `if ($a)` still reaches `$a` (single-
14494 // element grouping → 1).
14495 check_metrics::<PerlParser>(
14496 "sub f { my ($a, $x, $y) = @_;\n\
14497 \x20 my $r = $a || ($x, $y); # +2c: $a + last-named $y\n\
14498 \x20 my $s = $a || ($x); # +2c: $a + only-named $x\n\
14499 \x20 $r + $s;\n\
14500 }\n",
14501 "foo.pl",
14502 |metric| {
14503 // 2 + 2 = 4 unary conditions from the two `||`s.
14504 assert_eq!(metric.abc.conditions_sum(), 4);
14505 insta::assert_json_snapshot!(metric.abc);
14506 },
14507 );
14508 }
14509
14510 #[cfg(feature = "perl")]
14511 #[test]
14512 fn perl_if_scalar_variable_condition() {
14513 // Renamed from the cross-language
14514 // `_if_boolean_literal_condition` convention because
14515 // Perl has no readily-grammar-exposed boolean literal in
14516 // an `if (cond)` slot at the pinned grammar version:
14517 // tree-sitter-perl's `Boolean` kind only fires for the
14518 // `boolean` pragma's named constants (not bareword `1` /
14519 // `0`, which surface as `Integer` / not in the
14520 // terminal-bool set). A scalar-variable condition is the
14521 // grammar-stable witness — `if ($a)` reaches
14522 // `scalar_variable` via the `Array` paren unwrap.
14523 check_metrics::<PerlParser>(
14524 "sub f { my ($a) = @_; if ($a) { return 1; } }\n",
14525 "foo.pl",
14526 |metric| {
14527 assert_eq!(metric.abc.conditions_sum(), 1);
14528 insta::assert_json_snapshot!(metric.abc);
14529 },
14530 );
14531 }
14532
14533 #[cfg(feature = "perl")]
14534 #[test]
14535 fn perl_methods_arguments_with_conditions() {
14536 // `call(!$a, !$b)` — argument list walker counts each
14537 // unary-conditional argument once. Cannot use `m(...)` as
14538 // the function name — tree-sitter-perl parses `m(...)` as
14539 // the regex-match operator, not a function call.
14540 check_metrics::<PerlParser>(
14541 "sub f { my ($a, $b) = @_; call($a, $b); call(!$a, !$b); }\n",
14542 "foo.pl",
14543 |metric| {
14544 // Two calls × 1 branch each = 2 branches.
14545 // `call(!$a, !$b)` contributes 2 walker conditions
14546 // (one per `!`-wrapped scalar-variable argument);
14547 // `call($a, $b)` contributes 0 (bare-args don't
14548 // count via the Arguments walker — list_kind !=
14549 // BinaryExpression).
14550 assert_eq!(metric.abc.branches_sum(), 2);
14551 assert_eq!(metric.abc.conditions_sum(), 2);
14552 insta::assert_json_snapshot!(metric.abc);
14553 },
14554 );
14555 }
14556
14557 #[cfg(feature = "perl")]
14558 #[test]
14559 fn perl_return_with_conditions() {
14560 // `return !$a` reports 1 condition via the walker (unary
14561 // unwrap to scalar-variable terminal). `return $a` reports
14562 // 0 (no paren / unary wrap, has_boolean_content stays
14563 // false from ReturnExpression parent).
14564 check_metrics::<PerlParser>(
14565 "sub m1 { my ($z) = @_; return !($z); }\n\
14566 sub m2 { my ($x) = @_; return (((!$x))); }\n\
14567 sub m3 { my ($x, $y) = @_; return $x && $y; }\n",
14568 "foo.pl",
14569 |metric| {
14570 // m1: !($z) → walker on `!` unwraps paren to $z (1).
14571 // m2: (((!$x))) → walker unwraps three parens + one
14572 // unary to $x (1).
14573 // m3: $x && $y → walker on `&&` counts both (2).
14574 // Sum: 4.
14575 assert_eq!(metric.abc.conditions_sum(), 4);
14576 insta::assert_json_snapshot!(metric.abc);
14577 },
14578 );
14579 }
14580
14581 // A bare `/re/` or `m{re}` is a match against `$_`, a relational
14582 // operator spelled with no operator token, so it scores by use
14583 // (#1467, the rule #1461 applied to five other languages). Each
14584 // `*_out` member and `ret` / `notm` / `grp` / `arg` / `smart` was 0
14585 // before the arm: only a boolean slot reached the pattern, through
14586 // the former `perl_bool_terminal_kinds!()`. `mbare_out` is the `m{}` spelling, a
14587 // sibling kind rather than an alias. `smart` levels a smartmatch
14588 // with its `=~` sibling: no arm counts `~~`, so the pattern does.
14589 //
14590 // The in-slot members are the §5 check that the slot stopped
14591 // counting the pattern in the change that gave it an arm: each
14592 // reads what it read before. `bound_out` / `nbound` pin the gate on
14593 // `=~` / `!~` — the operator token scores, the pattern under it does
14594 // not — and `ctrl_out` is the `==` control the rest level with.
14595 // `s///`, `tr///` and `qr//` are not matches and stay at 0; the
14596 // bound `s///` / `tr///` agree (#1540, the test below).
14597 #[cfg(feature = "perl")]
14598 #[test]
14599 fn perl_bare_match_is_a_condition_by_use() {
14600 check_func_space::<PerlParser, _>(
14601 "sub bare_out { my $r = /^#/; return $r; }\n\
14602 sub mbare_out { my $r = m{^#}; return $r; }\n\
14603 sub ret { return /x/; }\n\
14604 sub notm { my $r = !/x/; return $r; }\n\
14605 sub grp { my @a = @_; return grep { /x/ } @a; }\n\
14606 sub arg { foo(/x/); }\n\
14607 sub smart { my ($x) = @_; my $r = $x ~~ /re/; return $r; }\n\
14608 sub bare_in { if (/^#/) { return 1; } return 0; }\n\
14609 sub mbare_in { unless (m{^#}) { return 1; } return 0; }\n\
14610 sub modif { return 0 unless /x/; return 1; }\n\
14611 sub andy { my ($y) = @_; return /x/ && $y; }\n\
14612 sub oror { return /a/ || /b/; }\n\
14613 sub tern { return /x/ ? 1 : 0; }\n\
14614 sub forc { for (my $i = 0; /x/; $i++) { } }\n\
14615 sub bound_out { my ($x) = @_; my $r = ($x =~ /^#/); return $r; }\n\
14616 sub nbound { my ($x) = @_; my $r = ($x !~ /^#/); return $r; }\n\
14617 sub ctrl_out { my ($x) = @_; my $r = ($x == 1); return $r; }\n\
14618 sub subst { my $n = s/x/y/; return $n; }\n\
14619 sub tr1 { my $n = tr/a/b/; return $n; }\n\
14620 sub qrx { my $q = qr/x/; return $q; }\n",
14621 "foo.pl",
14622 |space| {
14623 assert_members_score(
14624 &space,
14625 &[
14626 ("bare_out", 1, 1),
14627 ("mbare_out", 1, 1),
14628 ("ret", 1, 1),
14629 // `!` adds nothing outside a slot; the match is
14630 // the one condition.
14631 ("notm", 1, 1),
14632 ("grp", 1, 1),
14633 ("arg", 1, 1),
14634 ("smart", 1, 1),
14635 ("bare_in", 1, 2),
14636 ("mbare_in", 1, 2),
14637 ("modif", 1, 2),
14638 // One per `&&` / `||` operand, the match included.
14639 ("andy", 2, 2),
14640 ("oror", 2, 2),
14641 // The ternary node plus its condition.
14642 ("tern", 2, 2),
14643 ("forc", 1, 2),
14644 ("bound_out", 1, 1),
14645 ("nbound", 1, 1),
14646 ("ctrl_out", 1, 1),
14647 ("subst", 0, 1),
14648 ("tr1", 0, 1),
14649 ("qrx", 0, 1),
14650 ],
14651 );
14652 },
14653 );
14654 }
14655
14656 // A substitution or transliteration bound with `=~` / `!~` is the
14657 // same edit as its bare form on `$_`, so it scores by use exactly as
14658 // that twin does: 0 (#1540). Each `*_s` / `*_tr` / `*_y` row below
14659 // was 1 through the operator token before the gate; `count` (an
14660 // assigned count) and `sr` (a `/r` copy, a value) are uses too. A
14661 // match keeps its 1 whatever spells it — a pattern, `qr//`, or a
14662 // variable holding one. The `if_*` rows are the slot side: each pays
14663 // once, as its bare twin `if_s` does, because
14664 // `perl_condition_scores_itself` stops claiming a rewrite binding in
14665 // the same change; `tern_s` and `neg_s` reach that predicate through
14666 // a ternary condition and a `!`-proved `return` operand.
14667 #[cfg(feature = "perl")]
14668 #[test]
14669 fn perl_bound_rewrite_scores_like_its_bare_twin() {
14670 check_func_space::<PerlParser, _>(
14671 "sub ctrl { my ($x) = @_; $x; return $x; }\n\
14672 sub m_bare { my ($x) = @_; /re/; return $x; }\n\
14673 sub m_bound { my ($x) = @_; $x =~ /re/; return $x; }\n\
14674 sub m_nbound { my ($x) = @_; $x !~ /re/; return $x; }\n\
14675 sub m_qr { my ($x) = @_; $x =~ qr/re/; return $x; }\n\
14676 sub m_var { my ($x, $q) = @_; $x =~ $q; return $x; }\n\
14677 sub s_bare { my ($x) = @_; s/a/b/; return $x; }\n\
14678 sub s_bound { my ($x) = @_; $x =~ s/a/b/; return $x; }\n\
14679 sub s_nbound { my ($x) = @_; $x !~ s/a/b/; return $x; }\n\
14680 sub tr_bare { my ($x) = @_; tr/a/b/; return $x; }\n\
14681 sub tr_bound { my ($x) = @_; $x =~ tr/a/b/; return $x; }\n\
14682 sub tr_nbound { my ($x) = @_; $x !~ tr/a/b/; return $x; }\n\
14683 sub y_bound { my ($x) = @_; $x =~ y/a/b/; return $x; }\n\
14684 sub count { my ($x) = @_; my $n = ($x =~ s/a/b/g); return $x; }\n\
14685 sub sr { my ($x) = @_; my $r = $x =~ s/a/b/r; return $r; }\n\
14686 sub if_s { my ($x) = @_; if (s/a/b/) { g(); } return $x; }\n\
14687 sub if_m { my ($x) = @_; if ($x =~ /re/) { g(); } return $x; }\n\
14688 sub if_bs { my ($x) = @_; if ($x =~ s/a/b/) { g(); } return $x; }\n\
14689 sub if_nbs { my ($x) = @_; if ($x !~ s/a/b/) { g(); } return $x; }\n\
14690 sub if_btr { my ($x) = @_; if ($x =~ tr/a/b/) { g(); } return $x; }\n\
14691 sub if_by { my ($x) = @_; if ($x =~ y/a/b/) { g(); } return $x; }\n\
14692 sub if_bsr { my ($x) = @_; if ($x =~ s/a/b/r) { g(); } return $x; }\n\
14693 sub tern_s { my ($x) = @_; return ($x =~ s/a/b/) ? 1 : 0; }\n\
14694 sub neg_s { my ($x) = @_; return !($x =~ s/a/b/); }\n",
14695 "foo.pl",
14696 |space| {
14697 assert_members_score(
14698 &space,
14699 &[
14700 ("ctrl", 0, 1),
14701 ("m_bare", 1, 1),
14702 ("m_bound", 1, 1),
14703 ("m_nbound", 1, 1),
14704 ("m_qr", 1, 1),
14705 ("m_var", 1, 1),
14706 ("s_bare", 0, 1),
14707 ("s_bound", 0, 1),
14708 ("s_nbound", 0, 1),
14709 ("tr_bare", 0, 1),
14710 ("tr_bound", 0, 1),
14711 ("tr_nbound", 0, 1),
14712 ("y_bound", 0, 1),
14713 ("count", 0, 1),
14714 ("sr", 0, 1),
14715 ("if_s", 1, 2),
14716 ("if_m", 1, 2),
14717 ("if_bs", 1, 2),
14718 ("if_nbs", 1, 2),
14719 ("if_btr", 1, 2),
14720 ("if_by", 1, 2),
14721 ("if_bsr", 1, 2),
14722 // The ternary node plus its condition slot.
14723 ("tern_s", 2, 2),
14724 ("neg_s", 1, 1),
14725 ],
14726 );
14727 },
14728 );
14729 }
14730
14731 // The pattern `split` takes first is its delimiter, not a match
14732 // against `$_`, so it is no condition (#1467) in any spelling: the
14733 // spaced and parenthesised call forms, the `m{}` pattern, and the
14734 // `CORE::` qualifier. Three discriminating members keep the exclusion
14735 // narrow: `spl2` puts the pattern in `split`'s *second* slot, where
14736 // it is an ordinary match whose result `split` receives, `grpe`
14737 // puts one first in a call that is not `split`, and `tsplit` puts
14738 // one first in a list whose grandparent holds a bare `split` without
14739 // being that call — the ternary node plus `$c` plus the match.
14740 #[cfg(feature = "perl")]
14741 #[test]
14742 fn perl_split_delimiter_is_not_a_condition() {
14743 use crate::{ParserTrait, Perl};
14744
14745 let src = "sub spl { my ($s) = @_; my @p = split /,/, $s; return @p; }\n\
14746 sub splp { my ($s) = @_; my @p = split(/,/, $s); return @p; }\n\
14747 sub splm { my ($s) = @_; my @p = split m{,}, $s; return @p; }\n\
14748 sub splc { my ($s) = @_; my @p = CORE::split(/,/, $s); return @p; }\n\
14749 sub spl2 { my @p = split ' ', /x/; return @p; }\n\
14750 sub grpe { my @a = @_; return grep /x/, @a; }\n\
14751 sub tsplit { my ($c) = @_; my @r = $c ? split : (/x/); return @r; }\n";
14752
14753 // Fixture census: each member but `tsplit` holds exactly one
14754 // pattern, as a direct argument of its call — so a zero below
14755 // is the exclusion at work and not a pattern the fixture lost.
14756 let parser = PerlParser::new(
14757 src.as_bytes().to_vec(),
14758 &std::path::PathBuf::from("foo.pl"),
14759 None,
14760 );
14761 let call_arguments = parser
14762 .root()
14763 .preorder()
14764 .filter(|n| {
14765 matches!(
14766 n.kind_id().into(),
14767 Perl::PatternMatcher | Perl::PatternMatcherM
14768 )
14769 })
14770 .filter(|n| {
14771 n.parent()
14772 .and_then(|list| list.parent())
14773 .is_some_and(|call| {
14774 matches!(
14775 call.kind_id().into(),
14776 Perl::CallExpressionWithSpacedArgs
14777 | Perl::CallExpressionWithArgsWithBrackets
14778 )
14779 })
14780 })
14781 .count();
14782 assert_eq!(call_arguments, 6, "one call-argument pattern per member");
14783
14784 check_func_space::<PerlParser, _>(src, "foo.pl", |space| {
14785 assert_members_score(
14786 &space,
14787 &[
14788 ("spl", 0, 1),
14789 ("splp", 0, 1),
14790 ("splm", 0, 1),
14791 ("splc", 0, 1),
14792 ("spl2", 1, 1),
14793 ("grpe", 1, 1),
14794 ("tsplit", 3, 2),
14795 ],
14796 );
14797 });
14798 }
14799
14800 // tree-sitter-perl names its separators, so a trailing `,` or `=>`
14801 // was read as the last element of a condition list and paid a slot
14802 // of its own: `if ($a > 1,)` scored 2. `perl -MO=Deparse` reduces
14803 // every `*_sep` row to its `*_twin`, which is where the expected
14804 // values come from; the chain rows put the list inside a `&&`
14805 // operand, which reaches the same peel through
14806 // `perl_count_chain_operands`.
14807 #[cfg(feature = "perl")]
14808 #[test]
14809 fn perl_trailing_separator_scores_like_its_bare_twin() {
14810 check_func_space::<PerlParser, _>(
14811 "sub cmp_twin { my ($a) = @_; if ($a > 1) { g(); } }\n\
14812 sub cmp_comma { my ($a) = @_; if ($a > 1,) { g(); } }\n\
14813 sub cmp_fat { my ($a) = @_; if ($a > 1 =>) { g(); } }\n\
14814 sub var_twin { my ($a) = @_; if ($a) { g(); } }\n\
14815 sub var_comma { my ($a) = @_; if ($a,) { g(); } }\n\
14816 sub chain_twin { my ($a, $b) = @_; if (($a > 1) && $b) { g(); } }\n\
14817 sub chain_comma { my ($a, $b) = @_; if (($a > 1,) && $b) { g(); } }\n",
14818 "foo.pl",
14819 |space| {
14820 assert_members_score(
14821 &space,
14822 &[
14823 ("cmp_twin", 1, 2),
14824 ("cmp_comma", 1, 2),
14825 ("cmp_fat", 1, 2),
14826 ("var_twin", 1, 2),
14827 ("var_comma", 1, 2),
14828 ("chain_twin", 2, 3),
14829 ("chain_comma", 2, 3),
14830 ],
14831 );
14832 },
14833 );
14834 }
14835
14836 // A pattern's role is read on the outermost `( … )` around it, which
14837 // is the value inside it, so each `*_paren` row scores as its bare
14838 // `*_twin` (#1467, #1540); `perl -MO=Deparse` prints each pair
14839 // identically. The reversed binding is the other direction: in
14840 // `/foo/ =~ $x` the pattern is the *left* operand, so it first
14841 // matches `$_` and that result is bound (Deparse: `/foo/ =~ /$x/`) —
14842 // the match and the `=~` both pay, as in the explicit `rev_twin`.
14843 #[cfg(feature = "perl")]
14844 #[test]
14845 fn perl_pattern_role_reads_through_parentheses() {
14846 check_func_space::<PerlParser, _>(
14847 "sub bind_twin { my ($x) = @_; return $x =~ /foo/; }\n\
14848 sub bind_paren { my ($x) = @_; return $x =~ (/foo/); }\n\
14849 sub bind_parens_m { my ($x) = @_; return $x =~ (((m{foo}))); }\n\
14850 sub bind_list { my ($x, $y) = @_; return $x =~ (/foo/, $y); }\n\
14851 sub bind_list_twin { my ($x, $y) = @_; /foo/; return $x =~ $y; }\n\
14852 sub split_twin { my ($x) = @_; my @p = split(/foo/, $x); return @p; }\n\
14853 sub split_paren { my ($x) = @_; my @p = split((/foo/), $x); return @p; }\n\
14854 sub split_sole { my @p = split(/foo/); return @p; }\n\
14855 sub subst_twin { my ($x) = @_; $x =~ s/a/b/; return $x; }\n\
14856 sub subst_paren { my ($x) = @_; $x =~ (s/a/b/); return $x; }\n\
14857 sub rev { my ($x) = @_; return /foo/ =~ $x; }\n\
14858 sub rev_twin { my ($x) = @_; my $m = /foo/; return $m =~ $x; }\n",
14859 "foo.pl",
14860 |space| {
14861 assert_members_score(
14862 &space,
14863 &[
14864 ("bind_twin", 1, 1),
14865 ("bind_paren", 1, 1),
14866 ("bind_parens_m", 1, 1),
14867 // A comma operator: `/foo/` matches `$_` and `$y`
14868 // is what binds, so the match pays as in the twin.
14869 ("bind_list", 2, 1),
14870 ("bind_list_twin", 2, 1),
14871 ("split_twin", 0, 1),
14872 ("split_paren", 0, 1),
14873 // The pattern is the whole argument list, which is
14874 // the call's own and no wrapper to climb through.
14875 ("split_sole", 0, 1),
14876 ("subst_twin", 0, 1),
14877 ("subst_paren", 0, 1),
14878 ("rev", 2, 1),
14879 ("rev_twin", 2, 1),
14880 ],
14881 );
14882 },
14883 );
14884 }
14885
14886 // Only the builtin `split` takes its first pattern as a delimiter:
14887 // `My::split` is a user sub, so the pattern it is handed is an
14888 // ordinary match, as it is for `My::other` (#1467). `CORE::split`
14889 // is the builtin by its full name.
14890 #[cfg(feature = "perl")]
14891 #[test]
14892 fn perl_split_exemption_is_the_builtin_only() {
14893 check_func_space::<PerlParser, _>(
14894 "sub mine { my ($x) = @_; my @p = My::split(/foo/, $x); return @p; }\n\
14895 sub other { my ($x) = @_; my @p = My::other(/foo/, $x); return @p; }\n\
14896 sub core { my ($x) = @_; my @p = CORE::split(/foo/, $x); return @p; }\n",
14897 "foo.pl",
14898 |space| {
14899 assert_members_score(&space, &[("mine", 1, 1), ("other", 1, 1), ("core", 0, 1)]);
14900 },
14901 );
14902 }
14903
14904 // ---------- Lua ABC tests ----------
14905
14906 #[cfg(feature = "lua")]
14907 #[test]
14908 fn lua_empty_unit_zero() {
14909 check_metrics::<LuaParser>("", "empty.lua", |metric| {
14910 assert_eq!(metric.abc.assignments_sum(), 0);
14911 assert_eq!(metric.abc.branches_sum(), 0);
14912 assert_eq!(metric.abc.conditions_sum(), 0);
14913 insta::assert_json_snapshot!(metric.abc);
14914 });
14915 }
14916
14917 #[cfg(feature = "lua")]
14918 #[test]
14919 fn lua_assignments_count_locals_and_plain() {
14920 // `local x = 0` wraps an `assignment_statement` under a
14921 // `variable_declaration`; the inner wrapper still counts.
14922 // Multi-target assignment `a, b = 1, 2` is a single
14923 // `assignment_statement` and contributes 1, NOT 2 — the
14924 // wrapper is the unit of counting (matches the Python rule:
14925 // one `Assignment` node, one assignment).
14926 check_metrics::<LuaParser>(
14927 "function f()\n\
14928 local x = 0\n\
14929 x = 1\n\
14930 local a, b = 1, 2\n\
14931 a, b = b, a\n\
14932 end",
14933 "foo.lua",
14934 |metric| {
14935 assert_eq!(metric.abc.assignments_sum(), 4);
14936 assert_eq!(metric.abc.branches_sum(), 0);
14937 assert_eq!(metric.abc.conditions_sum(), 0);
14938 insta::assert_json_snapshot!(metric.abc);
14939 },
14940 );
14941 }
14942
14943 #[cfg(feature = "lua")]
14944 #[test]
14945 fn lua_calls_are_branches() {
14946 // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
14947 // call form is a `function_call` node. The nested
14948 // `f(g(1))` counts as 2 branches (one per dispatch).
14949 check_metrics::<LuaParser>(
14950 "function r(x)\n\
14951 print(x)\n\
14952 obj.m(x)\n\
14953 obj:m(x)\n\
14954 return f(g(1))\n\
14955 end",
14956 "foo.lua",
14957 |metric| {
14958 assert_eq!(metric.abc.assignments_sum(), 0);
14959 assert_eq!(metric.abc.branches_sum(), 5);
14960 assert_eq!(metric.abc.conditions_sum(), 0);
14961 insta::assert_json_snapshot!(metric.abc);
14962 },
14963 );
14964 }
14965
14966 #[cfg(feature = "lua")]
14967 #[test]
14968 fn lua_comparisons_count_logical_ops_do_not() {
14969 // Each comparison token contributes one condition; `and` /
14970 // `or` are NOT counted on their own (Fitzpatrick Rule 5;
14971 // #395) — instead each operand is counted as a unary
14972 // conditional by the walker (Rule 9; #403). The two
14973 // `a and b` / `a or b` lines add 2 walker conditions each.
14974 check_metrics::<LuaParser>(
14975 "function f(a, b)\n\
14976 local r\n\
14977 r = a == b\n\
14978 r = a ~= b\n\
14979 r = a < b\n\
14980 r = a > b\n\
14981 r = a <= b\n\
14982 r = a >= b\n\
14983 r = a and b\n\
14984 r = a or b\n\
14985 end",
14986 "foo.lua",
14987 |metric| {
14988 // 8 `r = …` reassignments, plus `local r` (no `=`).
14989 assert_eq!(metric.abc.assignments_sum(), 8);
14990 assert_eq!(metric.abc.branches_sum(), 0);
14991 // 6 comparisons (+6) + 2 logical lines × 2 walker
14992 // operands (+4) = 10.
14993 assert_eq!(metric.abc.conditions_sum(), 10);
14994 insta::assert_json_snapshot!(metric.abc);
14995 },
14996 );
14997 }
14998
14999 // #1297: Lua 5.4 brackets a variable attribute with the same two
15000 // bare tokens a comparison uses, and the `<` / `>` arm had no gate,
15001 // so `local x <const> = 1` scored two conditions. Two attributes
15002 // against one genuine `a < b` separate every mis-aim: 5 pre-fix, 1
15003 // once the arm allows `BinaryExpression`, 0 if it allows the wrong
15004 // parent or the fixture stops parsing.
15005 //
15006 // No fixture-presence anchor is available, unlike the JSX and Perl
15007 // fixtures: once excluded, a Lua attribute contributes to no ABC
15008 // axis at all (`local x <const> = 1` and `local x = 1` score
15009 // identically on assignments, branches and conditions), and it
15010 // opens no space. Deleting the two attributes would leave this test
15011 // passing on the `a < b` alone. The coverage that the attributes
15012 // are reaching the arm is the revert test: removing the gate makes
15013 // this the only failing test in the suite.
15014 #[cfg(feature = "lua")]
15015 #[test]
15016 fn lua_variable_attributes_are_not_conditions() {
15017 check_metrics::<LuaParser>(
15018 "local function f(a, b)\n\
15019 local x <const> = 1\n\
15020 local y <close> = nil\n\
15021 if a < b then return x end\n\
15022 return y\n\
15023 end",
15024 "foo.lua",
15025 |metric| {
15026 assert_eq!(metric.abc.conditions_sum(), 1);
15027 },
15028 );
15029 }
15030
15031 #[cfg(feature = "lua")]
15032 #[test]
15033 fn lua_elseif_and_else_count_conditions() {
15034 // Each elseif / else arm of the if contributes one
15035 // condition, mirroring the Python rule.
15036 check_metrics::<LuaParser>(
15037 "function f(x)\n\
15038 if x > 0 then\n\
15039 return 1\n\
15040 elseif x < 0 then\n\
15041 return -1\n\
15042 else\n\
15043 return 0\n\
15044 end\n\
15045 end",
15046 "foo.lua",
15047 |metric| {
15048 // Comparisons: `>`, `<` → 2; elseif_statement → 1;
15049 // else_statement → 1. C = 4. No branches (no calls).
15050 assert_eq!(metric.abc.assignments_sum(), 0);
15051 assert_eq!(metric.abc.branches_sum(), 0);
15052 assert_eq!(metric.abc.conditions_sum(), 4);
15053 insta::assert_json_snapshot!(metric.abc);
15054 },
15055 );
15056 }
15057
15058 #[cfg(feature = "lua")]
15059 #[test]
15060 fn lua_complex_function_abc() {
15061 // Combines every category to pin the metric.
15062 check_metrics::<LuaParser>(
15063 "function run(n)\n\
15064 local total = 0\n\
15065 for i = 1, n do\n\
15066 if i % 2 == 0 then\n\
15067 do_work(i)\n\
15068 else\n\
15069 total = total + i\n\
15070 end\n\
15071 end\n\
15072 print(\"done\")\n\
15073 return total\n\
15074 end",
15075 "foo.lua",
15076 |metric| {
15077 // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
15078 // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
15079 // Conditions: `==` (1), `else_statement` (1) → 2.
15080 assert_eq!(metric.abc.assignments_sum(), 2);
15081 assert_eq!(metric.abc.branches_sum(), 2);
15082 assert_eq!(metric.abc.conditions_sum(), 2);
15083 insta::assert_json_snapshot!(metric.abc);
15084 },
15085 );
15086 }
15087
15088 #[cfg(feature = "lua")]
15089 #[test]
15090 fn lua_if_multiple_conditions() {
15091 // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or`
15092 // are keyword tokens inside a `binary_expression`.
15093 check_metrics::<LuaParser>(
15094 "function f(a, b, c, d)\n\
15095 if a or b or c or d then return 1 end -- +4c\n\
15096 if a and b and c then return 2 end -- +3c\n\
15097 if not a and not b then return 3 end -- +2c\n\
15098 return 0\n\
15099 end",
15100 "foo.lua",
15101 |metric| {
15102 assert_eq!(metric.abc.conditions_sum(), 9);
15103 insta::assert_json_snapshot!(metric.abc);
15104 },
15105 );
15106 }
15107
15108 #[cfg(feature = "lua")]
15109 #[test]
15110 fn lua_while_conditions() {
15111 // Lua has no `do { ... } while(cond);` — `while cond do …
15112 // end` and `repeat … until cond` are the loop forms.
15113 check_metrics::<LuaParser>(
15114 "function f(a, b)\n\
15115 while a or b do break end -- +2c\n\
15116 repeat break until a and not b -- +2c\n\
15117 end",
15118 "foo.lua",
15119 |metric| {
15120 assert_eq!(metric.abc.conditions_sum(), 4);
15121 insta::assert_json_snapshot!(metric.abc);
15122 },
15123 );
15124 }
15125
15126 #[cfg(feature = "lua")]
15127 #[test]
15128 fn lua_short_circuit_with_boolean_literal_operand() {
15129 // `a and true` reports 2 conditions: one Identifier, one
15130 // True keyword literal.
15131 check_metrics::<LuaParser>("function f(a) return a and true end", "foo.lua", |metric| {
15132 assert_eq!(metric.abc.conditions_sum(), 2);
15133 insta::assert_json_snapshot!(metric.abc);
15134 });
15135 }
15136
15137 #[cfg(feature = "lua")]
15138 #[test]
15139 fn lua_number_truthy_condition_counts() {
15140 // Regression for findings.md #2: Lua treats every non-nil,
15141 // non-false value as truthy, so `if 1 then ... end` and
15142 // `return a and 2` should each count their numeric literal
15143 // as a Fitzpatrick Rule 6 / 7 unary condition. Pre-fix,
15144 // the former `lua_bool_terminal_kinds!()` listed `True` /
15145 // `False` / `Nil` but omitted `Number`, so the walker dropped
15146 // every numeric-truthy operand. The walker comment at the top
15147 // of the former `lua_inspect_container` already promised
15148 // numbers were terminal-bool kinds; this commit closed the gap.
15149 check_metrics::<LuaParser>(
15150 "function f(a)\n\
15151 \x20 if 1 then return 1 end\n\
15152 \x20 return a and 2\n\
15153 end",
15154 "foo.lua",
15155 |metric| {
15156 // `if 1 then` → walker counts the Number literal (+1).
15157 // `a and 2` → `and` walker counts both operands:
15158 // identifier `a` (+1), Number `2` (+1).
15159 // Total: 3.
15160 assert_eq!(metric.abc.conditions_sum(), 3);
15161 insta::assert_json_snapshot!(metric.abc);
15162 },
15163 );
15164 }
15165
15166 #[cfg(feature = "lua")]
15167 #[test]
15168 fn lua_if_boolean_literal_condition() {
15169 check_metrics::<LuaParser>(
15170 "function f()\n\
15171 if true then end -- +1c\n\
15172 if not false then end -- +1c\n\
15173 while true do break end -- +1c\n\
15174 repeat break until false -- +1c\n\
15175 end",
15176 "foo.lua",
15177 |metric| {
15178 assert_eq!(metric.abc.conditions_sum(), 4);
15179 insta::assert_json_snapshot!(metric.abc);
15180 },
15181 );
15182 }
15183
15184 #[cfg(feature = "lua")]
15185 #[test]
15186 fn lua_methods_arguments_with_conditions() {
15187 // `m(not a, not b)` — argument list walker counts each
15188 // unary-conditional argument once. Bare-identifier args
15189 // (`m(a, b)`) do not count (list_kind != BinaryExpression).
15190 check_metrics::<LuaParser>(
15191 "function f(a, b) m(a, b); m(not a, not b) end",
15192 "foo.lua",
15193 |metric| {
15194 assert_eq!(metric.abc.branches_sum(), 2);
15195 assert_eq!(metric.abc.conditions_sum(), 2);
15196 insta::assert_json_snapshot!(metric.abc);
15197 },
15198 );
15199 }
15200
15201 #[cfg(feature = "lua")]
15202 #[test]
15203 fn lua_return_with_conditions() {
15204 // `return not (z >= 0)` → walker on `not` unwraps the paren
15205 // chain and reaches the inner BinaryExpression; the inner
15206 // `>=` comparison is the actual Fitzpatrick condition.
15207 check_metrics::<LuaParser>(
15208 "function m1(z) return not (z >= 0) end\n\
15209 function m2(x) return (((not x))) end\n\
15210 function m3(x, y) return x and y end",
15211 "foo.lua",
15212 |metric| {
15213 // m1: `>=` (1). `not` wraps a paren'd
15214 // BinaryExpression — `lua_count_negated` peels it
15215 // to the comparison, which scores itself, so the
15216 // return value pays nothing extra. +1.
15217 // m2: ReturnStatement → iterate expression_list →
15218 // `lua_count_negated` peels the parens and the
15219 // `not` (which proves boolean) down to `x`, which
15220 // does not score itself. +1.
15221 // m3: x and y → `and` walker counts both → +2.
15222 // Sum: 4.
15223 assert_eq!(metric.abc.conditions_sum(), 4);
15224 insta::assert_json_snapshot!(metric.abc);
15225 },
15226 );
15227 }
15228
15229 // ---------- Tcl ABC tests ----------
15230
15231 #[cfg(feature = "tcl")]
15232 #[test]
15233 fn tcl_empty_unit_zero() {
15234 check_metrics::<TclParser>("", "empty.tcl", |metric| {
15235 assert_eq!(metric.abc.assignments_sum(), 0);
15236 assert_eq!(metric.abc.branches_sum(), 0);
15237 assert_eq!(metric.abc.conditions_sum(), 0);
15238 insta::assert_json_snapshot!(metric.abc);
15239 });
15240 }
15241
15242 #[cfg(feature = "tcl")]
15243 #[test]
15244 fn tcl_set_command_counts_assignment() {
15245 // `set` has its own grammar production; each invocation is
15246 // one assignment.
15247 check_metrics::<TclParser>(
15248 "proc f {} {\n\
15249 set x 1\n\
15250 set y 2\n\
15251 set x [expr {$x + $y}]\n\
15252 }",
15253 "foo.tcl",
15254 |metric| {
15255 // 3 `set` invocations → A=3. The inner `expr` is a
15256 // sub-command (`command_substitution` + `expr_cmd`),
15257 // not a `command` node, so it doesn't add a branch.
15258 assert_eq!(metric.abc.assignments_sum(), 3);
15259 assert_eq!(metric.abc.branches_sum(), 0);
15260 assert_eq!(metric.abc.conditions_sum(), 0);
15261 insta::assert_json_snapshot!(metric.abc);
15262 },
15263 );
15264 }
15265
15266 #[cfg(feature = "tcl")]
15267 #[test]
15268 fn tcl_incr_append_lappend_count_assignment() {
15269 // Variable-mutation commands (`incr`, `append`, `lappend`)
15270 // are recognised by name and count as assignments, not
15271 // branches.
15272 check_metrics::<TclParser>(
15273 "proc f {} {\n\
15274 set x 0\n\
15275 incr x\n\
15276 append s \"hi\"\n\
15277 lappend lst 1\n\
15278 }",
15279 "foo.tcl",
15280 |metric| {
15281 // `set` (1) + `incr` (1) + `append` (1) + `lappend`
15282 // (1) → A=4. No branches, no conditions.
15283 assert_eq!(metric.abc.assignments_sum(), 4);
15284 assert_eq!(metric.abc.branches_sum(), 0);
15285 assert_eq!(metric.abc.conditions_sum(), 0);
15286 insta::assert_json_snapshot!(metric.abc);
15287 },
15288 );
15289 }
15290
15291 #[cfg(feature = "tcl")]
15292 #[test]
15293 fn tcl_qualified_mutator_commands_count_assignment() {
15294 // `::incr` is `incr` through the global namespace. Anchored on
15295 // `branches_sum() == 0` as well as the assignment total: an
15296 // unresolved name falls to the branch arm, so the two columns
15297 // move in opposite directions and no single total can mask the
15298 // regression (#1381 review).
15299 check_metrics::<TclParser>(
15300 "proc f {} {\n\
15301 ::incr x\n\
15302 ::append s \"hi\"\n\
15303 ::lappend lst 1\n\
15304 }",
15305 "foo.tcl",
15306 |metric| {
15307 assert_eq!(metric.abc.assignments_sum(), 3);
15308 assert_eq!(metric.abc.branches_sum(), 0);
15309 assert_eq!(metric.abc.conditions_sum(), 0);
15310 },
15311 );
15312 }
15313
15314 #[cfg(feature = "tcl")]
15315 #[test]
15316 fn tcl_namespaced_mutator_command_stays_a_branch() {
15317 // Control: only the *leading* qualifier names the core command,
15318 // so `ns::incr` is a user proc and keeps the branch column — the
15319 // direction `BRACED_WORD_VALUE_CASES` pins for `ns::eval`.
15320 check_metrics::<TclParser>(
15321 "proc f {} {\n\
15322 ns::incr x\n\
15323 }",
15324 "foo.tcl",
15325 |metric| {
15326 assert_eq!(metric.abc.assignments_sum(), 0);
15327 assert_eq!(metric.abc.branches_sum(), 1);
15328 },
15329 );
15330 }
15331
15332 #[cfg(feature = "tcl")]
15333 #[test]
15334 fn tcl_computed_command_name_is_not_an_assignment() {
15335 // A command whose leading word is computed (`$cmd args`) names no
15336 // builtin the parser can resolve, so it stays a branch. Pins the
15337 // field-addressed, `simple_word`-gated read the classifier shares
15338 // with the Cognitive / Cyclomatic detectors (grammar-dispatch §3):
15339 // the earlier `child(0)` byte-slice addressed the slot by position
15340 // and compared whatever literal text sat there, which agrees with
15341 // the gated read on today's grammar only because no mutator name is
15342 // spelled with a leading `$`. A grammar that moved the name out of
15343 // `child(0)`, or a mutator list that grew a computed-looking entry,
15344 // would diverge — this fixture is where that surfaces.
15345 check_metrics::<TclParser>(
15346 "proc f {cmd x} {\n\
15347 $cmd $x\n\
15348 incr x\n\
15349 }",
15350 "foo.tcl",
15351 |metric| {
15352 // `incr x` is the only assignment; `$cmd $x` is a branch.
15353 assert_eq!(metric.abc.assignments_sum(), 1);
15354 assert_eq!(metric.abc.branches_sum(), 1);
15355 assert_eq!(metric.abc.conditions_sum(), 0);
15356 },
15357 );
15358 }
15359
15360 #[cfg(feature = "tcl")]
15361 #[test]
15362 fn tcl_generic_commands_are_branches() {
15363 // Anything that isn't `set` or a known mutator command
15364 // counts as a branch — including builtins like `puts` and
15365 // `return`.
15366 check_metrics::<TclParser>(
15367 "proc f {} {\n\
15368 puts \"hello\"\n\
15369 do_work 1 2\n\
15370 return 0\n\
15371 }",
15372 "foo.tcl",
15373 |metric| {
15374 // 3 commands, all branches.
15375 assert_eq!(metric.abc.assignments_sum(), 0);
15376 assert_eq!(metric.abc.branches_sum(), 3);
15377 assert_eq!(metric.abc.conditions_sum(), 0);
15378 insta::assert_json_snapshot!(metric.abc);
15379 },
15380 );
15381 }
15382
15383 #[cfg(feature = "tcl")]
15384 #[test]
15385 fn tcl_comparisons_count_logical_ops_do_not() {
15386 // `expr` predicates expose comparison / logical tokens at
15387 // the leaf level. Each comparison token contributes one
15388 // condition; `&&` and `||` are NOT counted on their own
15389 // (Fitzpatrick Rule 5; #395) — instead each operand is
15390 // counted as a unary conditional by the walker (Rule 9;
15391 // #403). The two logical lines add 2 walker conditions
15392 // each (variable-substitution operands).
15393 check_metrics::<TclParser>(
15394 "proc f {a b} {\n\
15395 set r [expr {$a == $b}]\n\
15396 set r [expr {$a != $b}]\n\
15397 set r [expr {$a < $b}]\n\
15398 set r [expr {$a > $b}]\n\
15399 set r [expr {$a <= $b}]\n\
15400 set r [expr {$a >= $b}]\n\
15401 set r [expr {$a eq $b}]\n\
15402 set r [expr {$a ne $b}]\n\
15403 set r [expr {$a && $b}]\n\
15404 set r [expr {$a || $b}]\n\
15405 }",
15406 "foo.tcl",
15407 |metric| {
15408 // 10 `set` assignments.
15409 assert_eq!(metric.abc.assignments_sum(), 10);
15410 assert_eq!(metric.abc.branches_sum(), 0);
15411 // 8 comparisons (+8) + 2 logical lines × 2 walker
15412 // operands (+4) = 12.
15413 assert_eq!(metric.abc.conditions_sum(), 12);
15414 insta::assert_json_snapshot!(metric.abc);
15415 },
15416 );
15417 }
15418
15419 #[cfg(feature = "tcl")]
15420 #[test]
15421 fn tcl_ternary_counts_condition() {
15422 // The `ternary_expr` node is one condition and its condition
15423 // slot `$a` — a bare truthy test — is another, matching C++'s
15424 // `int r = a ? b : c;` (also 2). The two branch operands are
15425 // unnegated and so contribute nothing (#1180).
15426 check_metrics::<TclParser>(
15427 "proc f {a b c} {\n\
15428 set r [expr {$a ? $b : $c}]\n\
15429 }",
15430 "foo.tcl",
15431 |metric| {
15432 assert_eq!(metric.abc.assignments_sum(), 1);
15433 assert_eq!(metric.abc.branches_sum(), 0);
15434 assert_eq!(metric.abc.conditions_sum(), 2);
15435 insta::assert_json_snapshot!(metric.abc);
15436 },
15437 );
15438 }
15439
15440 /// The Tcl half of the ternary slot-location guard (#1180).
15441 ///
15442 /// `ternary_expr` exposes no grammar fields, so the slots are found
15443 /// relative to the `?` and `:` tokens. A *parenthesised* condition is
15444 /// the input that discriminates that from a fixed-index reading:
15445 /// `_expr` inlines `( … )` as anonymous children of `ternary_expr`,
15446 /// so `($a) ? !$b : !$c` shifts every operand right by one and
15447 /// `child(0)` / `child(2)` / `child(4)` land on `(`, `)` and `?`.
15448 /// Without this case the whole fixed-index revert passes.
15449 #[cfg(feature = "tcl")]
15450 #[test]
15451 fn tcl_parenthesised_ternary_condition_matches_the_bare_form() {
15452 let conditions = |source: &str| {
15453 crate::test_support::metrics_verbatim(
15454 crate::LANG::Tcl,
15455 source.as_bytes(),
15456 crate::MetricsOptions::default(),
15457 )
15458 .abc
15459 .conditions_sum()
15460 };
15461 let bare = conditions("proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}");
15462 assert_eq!(bare, 4, "the bare form is the documented reference value");
15463 assert_eq!(
15464 conditions("proc f {a b c} {\n set r [expr {($a) ? !$b : !$c}]\n}"),
15465 bare,
15466 "parenthesising the condition must not change the count"
15467 );
15468 }
15469
15470 /// A parenthesised operand under `!` counts the same as a bare one.
15471 ///
15472 /// `_expr` inlines `( … )` as anonymous children, so `!($a)` puts
15473 /// `(` where a positional read expects the operand. The walker's
15474 /// negation branch kept a fixed `child(1)` through the first draft of
15475 /// #1180 and scored these 0 while the unparenthesised forms scored 1
15476 /// — an inconsistency the fix itself introduced, since before it
15477 /// neither form counted. Found in review, not by the tests: the
15478 /// parenthesised fixtures added with #1180 covered the ternary
15479 /// *condition* slot only.
15480 #[cfg(feature = "tcl")]
15481 #[test]
15482 fn tcl_parenthesised_negated_operands_match_the_bare_form() {
15483 let conditions = |source: &str| {
15484 crate::test_support::metrics_verbatim(
15485 crate::LANG::Tcl,
15486 source.as_bytes(),
15487 crate::MetricsOptions::default(),
15488 )
15489 .abc
15490 .conditions_sum()
15491 };
15492 for (bare, parenthesised) in [
15493 (
15494 "proc f {a} {\n if {!$a} { puts x }\n}",
15495 "proc f {a} {\n if {!($a)} { puts x }\n}",
15496 ),
15497 (
15498 "proc f {a b} {\n if {$a && !$b} { puts x }\n}",
15499 "proc f {a b} {\n if {$a && !($b)} { puts x }\n}",
15500 ),
15501 (
15502 "proc f {a b c} {\n set r [expr {$a ? !$b : !$c}]\n}",
15503 "proc f {a b c} {\n set r [expr {$a ? !($b) : !$c}]\n}",
15504 ),
15505 ] {
15506 let want = conditions(bare);
15507 assert!(want > 0, "the bare form must count something: {bare}");
15508 assert_eq!(
15509 conditions(parenthesised),
15510 want,
15511 "parenthesising the negated operand changed the count\n bare: {bare}\n paren: {parenthesised}"
15512 );
15513 }
15514 }
15515
15516 #[cfg(feature = "irules")]
15517 #[test]
15518 fn irules_abc_parenthesised_negated_operands_match_the_bare_form() {
15519 let conditions = |source: &str| {
15520 crate::test_support::metrics_verbatim(
15521 crate::LANG::Irules,
15522 source.as_bytes(),
15523 crate::MetricsOptions::default(),
15524 )
15525 .abc
15526 .conditions_sum()
15527 };
15528 let want = conditions("when X {\n if { !$a } { log local0. hi }\n}\n");
15529 assert_eq!(want, 1);
15530 assert_eq!(
15531 conditions("when X {\n if { !($a) } { log local0. hi }\n}\n"),
15532 want
15533 );
15534 }
15535
15536 #[cfg(feature = "tcl")]
15537 #[test]
15538 fn tcl_bare_truthy_and_negated_predicates_count_one_condition() {
15539 // The headline #1180 fix, on the Tcl side: both were 0 before.
15540 let conditions = |source: &str| {
15541 crate::test_support::metrics_verbatim(
15542 crate::LANG::Tcl,
15543 source.as_bytes(),
15544 crate::MetricsOptions::default(),
15545 )
15546 .abc
15547 .conditions_sum()
15548 };
15549 assert_eq!(conditions("proc f {a} {\n if {$a} { puts x }\n}"), 1);
15550 assert_eq!(conditions("proc f {a} {\n if {!$a} { puts x }\n}"), 1);
15551 assert_eq!(conditions("proc f {a} {\n while {$a} { puts x }\n}"), 1);
15552 assert_eq!(conditions("proc f {a} {\n while {!$a} { puts x }\n}"), 1);
15553 }
15554
15555 #[cfg(feature = "tcl")]
15556 #[test]
15557 fn tcl_bare_truthy_elseif_predicate_counts_one_condition() {
15558 // The `Tcl::Elseif` arm routes its predicate through
15559 // `tcl_condition_expr` exactly as `If` / `While` do (#1180), but
15560 // a comparison predicate is counted by its leaf operator, so it
15561 // cannot tell the routing from its absence. Only a bare truthy
15562 // predicate can: `$b` scores through the routed walker or not at
15563 // all.
15564 let conditions = |source: &str| {
15565 crate::test_support::metrics_verbatim(
15566 crate::LANG::Tcl,
15567 source.as_bytes(),
15568 crate::MetricsOptions::default(),
15569 )
15570 .abc
15571 .conditions_sum()
15572 };
15573 // `$a` truthy (1) + `elseif` clause (1) + `$b` truthy (1).
15574 assert_eq!(
15575 conditions("proc f {a b} {\n if {$a} { puts x } elseif {$b} { puts y }\n}"),
15576 3
15577 );
15578 }
15579
15580 #[cfg(feature = "tcl")]
15581 #[test]
15582 fn tcl_elseif_and_else_count_conditions() {
15583 // `if` / `elseif` / `else` clause productions each
15584 // contribute one condition. The leaf comparison inside the
15585 // predicate is counted independently.
15586 check_metrics::<TclParser>(
15587 "proc f {x} {\n\
15588 if {$x > 0} {\n\
15589 return 1\n\
15590 } elseif {$x < 0} {\n\
15591 return -1\n\
15592 } else {\n\
15593 return 0\n\
15594 }\n\
15595 }",
15596 "foo.tcl",
15597 |metric| {
15598 // Branches: three `return` commands → 3.
15599 // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
15600 // (1) → 4.
15601 assert_eq!(metric.abc.assignments_sum(), 0);
15602 assert_eq!(metric.abc.branches_sum(), 3);
15603 assert_eq!(metric.abc.conditions_sum(), 4);
15604 insta::assert_json_snapshot!(metric.abc);
15605 },
15606 );
15607 }
15608
15609 #[cfg(feature = "tcl")]
15610 #[test]
15611 fn tcl_if_multiple_conditions() {
15612 // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot
15613 // exposes `&&` / `||` operands as variable substitutions
15614 // (`$a`, `$b`, …) inside a `binop_expr`.
15615 check_metrics::<TclParser>(
15616 "proc f {a b c d} {\n\
15617 if {[expr {$a || $b || $c || $d}]} { return 1 } \n\
15618 if {[expr {$a && $b && $c}]} { return 2 } \n\
15619 return 0\n\
15620 }",
15621 "foo.tcl",
15622 |metric| {
15623 // The two chains feed the walker: 4 + 3 = 7. Each `if`
15624 // predicate is additionally a bare truthy test of a
15625 // command substitution — `{[expr {…}]}` is structurally
15626 // `if {[somecmd]}`, which counts 1 exactly as `if {$a}`
15627 // does — so 7 + 2 = 9 (#1180). Written without the
15628 // redundant `[expr …]` wrapper, `if {$a || $b}` scores
15629 // 2, matching C++'s `if (a || b)`.
15630 assert_eq!(metric.abc.conditions_sum(), 9);
15631 insta::assert_json_snapshot!(metric.abc);
15632 },
15633 );
15634 }
15635
15636 #[cfg(feature = "tcl")]
15637 #[test]
15638 fn tcl_while_conditions() {
15639 // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is
15640 // the standard loop. The walker fires on `&&` / `||` tokens
15641 // inside the `expr` predicate.
15642 check_metrics::<TclParser>(
15643 "proc f {a b} {\n\
15644 while {[expr {$a || $b}]} { break } \n\
15645 while {[expr {$a && $b}]} { break } \n\
15646 }",
15647 "foo.tcl",
15648 |metric| {
15649 // 2 + 2 from the chains, plus one bare truthy test per
15650 // `while` predicate — see `tcl_if_multiple_conditions`
15651 // (#1180).
15652 assert_eq!(metric.abc.conditions_sum(), 6);
15653 insta::assert_json_snapshot!(metric.abc);
15654 },
15655 );
15656 }
15657
15658 #[cfg(feature = "tcl")]
15659 #[test]
15660 fn tcl_short_circuit_with_boolean_literal_operand() {
15661 // `$a && 1` reports 2 conditions: a VariableSubstitution
15662 // operand plus a Number-literal operand. Confirms `Number`
15663 // is in the walker terminal set. `true` / `false` Tcl
15664 // keywords are not literal tokens in tree-sitter-tcl —
15665 // they're emitted as the operator-context word, which is
15666 // captured separately by the `Tcl::Boolean` kind for
15667 // dedicated `expr {true}` predicates but not as a `&&`
15668 // operand at this iteration; using a numeric literal keeps
15669 // the assertion grammar-stable.
15670 check_metrics::<TclParser>(
15671 "proc f {a} { return [expr {$a && 1}] }\n",
15672 "foo.tcl",
15673 |metric| {
15674 assert_eq!(metric.abc.conditions_sum(), 2);
15675 insta::assert_json_snapshot!(metric.abc);
15676 },
15677 );
15678 }
15679
15680 #[cfg(feature = "tcl")]
15681 #[test]
15682 fn tcl_complex_function_abc() {
15683 // Mixed program covering every category. Tcl's grammar
15684 // re-parses braced content that looks command-shaped as a
15685 // nested `command` node, which inflates the branch count
15686 // relative to a naive read of the source — see breakdown.
15687 check_metrics::<TclParser>(
15688 "proc run {n} {\n\
15689 set total 0\n\
15690 for {set i 0} {$i < $n} {incr i} {\n\
15691 if {$i % 2 == 0} {\n\
15692 do_work $i\n\
15693 } else {\n\
15694 incr total $i\n\
15695 }\n\
15696 }\n\
15697 puts \"done\"\n\
15698 return $total\n\
15699 }",
15700 "foo.tcl",
15701 |metric| {
15702 // Assignments: `set total 0` (1), `set i 0` (1),
15703 // `incr i` (1), `incr total $i` (1) → A = 4.
15704 // Branches: the outer `for …` is one `command` node;
15705 // the `{$i < $n}` predicate ALSO re-parses as a
15706 // `command` node (tree-sitter-tcl treats braced
15707 // predicates as nested commands at the pinned
15708 // grammar version); plus `do_work $i`, `puts
15709 // "done"`, and `return $total`. The for-loop body's
15710 // `incr` and `incr total $i` are assignment commands
15711 // and don't add branches. Total B = 5.
15712 // Conditions: `==` (1) and `else` (1) → C = 2. The
15713 // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
15714 // that predicate re-parses as a `command`, the `<`
15715 // is emitted as `simple_word`. Only `<` inside a
15716 // real `expr` production becomes `Tcl::LT`.
15717 assert_eq!(metric.abc.assignments_sum(), 4);
15718 assert_eq!(metric.abc.branches_sum(), 5);
15719 assert_eq!(metric.abc.conditions_sum(), 2);
15720 insta::assert_json_snapshot!(metric.abc);
15721 },
15722 );
15723 }
15724
15725 /// The dedicated `set name value` production counts as one assignment.
15726 #[cfg(feature = "irules")]
15727 #[test]
15728 fn irules_abc_set_assignment() {
15729 check_metrics::<IrulesParser>("when X {\n set x 1\n}\n", "foo.irule", |metric| {
15730 assert_eq!(metric.abc.assignments_sum(), 1);
15731 assert_eq!(metric.abc.branches_sum(), 0);
15732 assert_eq!(metric.abc.conditions_sum(), 0);
15733 });
15734 }
15735
15736 /// Mutator commands (`incr` / `append` / `lappend`) count as
15737 /// assignments, not branches — iRules has no assignment operators, so
15738 /// mutation is always a command invocation.
15739 #[cfg(feature = "irules")]
15740 #[test]
15741 fn irules_abc_mutator_commands() {
15742 check_metrics::<IrulesParser>(
15743 "when X {\n incr x\n append s \"y\"\n lappend l 1\n}\n",
15744 "foo.irule",
15745 |metric| {
15746 assert_eq!(metric.abc.assignments_sum(), 3);
15747 assert_eq!(metric.abc.branches_sum(), 0);
15748 },
15749 );
15750 }
15751
15752 /// `::incr` is `incr` through the global namespace. iRules resolves
15753 /// the leading word in `irules_command_is_assignment` rather than
15754 /// through `tcl_command_name`, so this pins the strip on that second
15755 /// path (#1381 review).
15756 #[cfg(feature = "irules")]
15757 #[test]
15758 fn irules_abc_qualified_mutator_commands() {
15759 check_metrics::<IrulesParser>(
15760 "when X {\n ::incr x\n ::append s \"y\"\n ::lappend l 1\n}\n",
15761 "foo.irule",
15762 |metric| {
15763 assert_eq!(metric.abc.assignments_sum(), 3);
15764 assert_eq!(metric.abc.branches_sum(), 0);
15765 },
15766 );
15767 }
15768
15769 /// Control: `ns::incr` is a proc in `ns`, so it stays a branch.
15770 #[cfg(feature = "irules")]
15771 #[test]
15772 fn irules_abc_namespaced_mutator_command_stays_a_branch() {
15773 check_metrics::<IrulesParser>("when X {\n ns::incr x\n}\n", "foo.irule", |metric| {
15774 assert_eq!(metric.abc.assignments_sum(), 0);
15775 assert_eq!(metric.abc.branches_sum(), 1);
15776 });
15777 }
15778
15779 /// Generic (non-mutator) commands count as branches.
15780 #[cfg(feature = "irules")]
15781 #[test]
15782 fn irules_abc_branch_commands() {
15783 check_metrics::<IrulesParser>(
15784 "when X {\n log local0. hi\n pool p1\n}\n",
15785 "foo.irule",
15786 |metric| {
15787 assert_eq!(metric.abc.assignments_sum(), 0);
15788 assert_eq!(metric.abc.branches_sum(), 2);
15789 assert_eq!(metric.abc.conditions_sum(), 0);
15790 },
15791 );
15792 }
15793
15794 /// A numeric comparison (`==`) is one condition; the `log` inside the
15795 /// `if` body is one branch.
15796 #[cfg(feature = "irules")]
15797 #[test]
15798 fn irules_abc_comparison_condition() {
15799 check_metrics::<IrulesParser>(
15800 "when X {\n if { $a == 1 } { log local0. hi }\n}\n",
15801 "foo.irule",
15802 |metric| {
15803 assert_eq!(metric.abc.branches_sum(), 1);
15804 assert_eq!(metric.abc.conditions_sum(), 1);
15805 },
15806 );
15807 }
15808
15809 /// A word-form string comparator (`contains`) is a condition just like
15810 /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If
15811 /// `contains` were dropped from the condition set this would report 0.
15812 #[cfg(feature = "irules")]
15813 #[test]
15814 fn irules_abc_string_op_condition() {
15815 check_metrics::<IrulesParser>(
15816 "when X {\n if { $a contains \"x\" } { log local0. hi }\n}\n",
15817 "foo.irule",
15818 |metric| {
15819 assert_eq!(metric.abc.branches_sum(), 1);
15820 assert_eq!(metric.abc.conditions_sum(), 1);
15821 },
15822 );
15823 }
15824
15825 /// Each `elseif` / `else` clause is one condition; the three `set`s are
15826 /// assignments. The leading `if` is not itself a condition.
15827 #[cfg(feature = "irules")]
15828 #[test]
15829 fn irules_abc_elseif_else_conditions() {
15830 check_metrics::<IrulesParser>(
15831 "when X {\n if { $a } { set r 1 } elseif { $b } { set r 2 } else { set r 3 }\n}\n",
15832 "foo.irule",
15833 |metric| {
15834 assert_eq!(metric.abc.assignments_sum(), 3);
15835 // The `elseif` and `else` clauses are one condition each,
15836 // as before; #1180 adds the two bare truthy predicates
15837 // (`{ $a }`, `{ $b }`). C++'s
15838 // `if(a){} else if(b){} else {}` also scores 4.
15839 assert_eq!(metric.abc.conditions_sum(), 4);
15840 },
15841 );
15842 }
15843
15844 /// A ternary contributes its own condition plus the `>` comparison in
15845 /// its test: conditions 2; the `set` is one assignment.
15846 #[cfg(feature = "irules")]
15847 #[test]
15848 fn irules_abc_ternary_condition() {
15849 check_metrics::<IrulesParser>(
15850 "when X {\n set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
15851 "foo.irule",
15852 |metric| {
15853 assert_eq!(metric.abc.assignments_sum(), 1);
15854 assert_eq!(metric.abc.conditions_sum(), 2);
15855 },
15856 );
15857 }
15858
15859 /// Fitzpatrick Rule 9: the short-circuit `&&` is not itself a condition,
15860 /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards
15861 /// the `irules_count_condition` chain-operand walk — conditions 2.
15862 #[cfg(feature = "irules")]
15863 #[test]
15864 fn irules_abc_negated_operands_in_chain() {
15865 check_metrics::<IrulesParser>(
15866 "when X {\n if { !$a && !$b } { log local0. hi }\n}\n",
15867 "foo.irule",
15868 |metric| {
15869 assert_eq!(metric.abc.branches_sum(), 1);
15870 assert_eq!(metric.abc.conditions_sum(), 2);
15871 },
15872 );
15873 }
15874
15875 /// A bare-truthy `if {$a}` predicate is one condition (#1180).
15876 ///
15877 /// It carries no comparison, ternary or short-circuit operator, so
15878 /// before the Phase 2B slot routing landed nothing invoked the
15879 /// unary-conditional walker and the whole predicate scored 0 — this
15880 /// test previously pinned that absence, and the metrics book's ABC
15881 /// deviation table said so. Now the `if` node routes its `expr`
15882 /// predicate and the count matches C++'s `if (a)`, which is also 1.
15883 /// The `log` command remains the single branch.
15884 #[cfg(feature = "irules")]
15885 #[test]
15886 fn irules_abc_bare_truthy_counts_one_condition() {
15887 check_metrics::<IrulesParser>(
15888 "when X {\n if { $a } { log local0. hi }\n}\n",
15889 "foo.irule",
15890 |metric| {
15891 assert_eq!(metric.abc.branches_sum(), 1);
15892 assert_eq!(metric.abc.conditions_sum(), 1);
15893 },
15894 );
15895 }
15896
15897 /// The negated form of the same predicate, which was *also* 0 before
15898 /// #1180: `!$a` reached the walker but no parent seeded boolean
15899 /// context, so the terminal operand was never counted. Distinct from
15900 /// `irules_abc_negated_operands_in_chain`, whose `&&` supplied the
15901 /// seed the bare form lacked.
15902 #[cfg(feature = "irules")]
15903 #[test]
15904 fn irules_abc_negated_bare_truthy_counts_one_condition() {
15905 check_metrics::<IrulesParser>(
15906 "when X {\n if { !$a } { log local0. hi }\n}\n",
15907 "foo.irule",
15908 |metric| {
15909 assert_eq!(metric.abc.branches_sum(), 1);
15910 assert_eq!(metric.abc.conditions_sum(), 1);
15911 },
15912 );
15913 }
15914
15915 /// The ternary's three operand slots, located relative to the `?`
15916 /// and `:` tokens because the grammar exposes no fields (#1180).
15917 ///
15918 /// `$a ? !$b : !$c` is four: the `ternary_expr` node, the bare
15919 /// truthy condition, and one per negated branch — the same value
15920 /// Java, C#, Groovy, the C family, the JS family, PHP, Perl, Ruby
15921 /// and Python report for the identical expression.
15922 #[cfg(feature = "irules")]
15923 #[test]
15924 fn irules_abc_ternary_routes_its_operand_slots() {
15925 check_metrics::<IrulesParser>(
15926 "when X {\n set y [expr { $a ? !$b : !$c }]\n}\n",
15927 "foo.irule",
15928 |metric| {
15929 assert_eq!(metric.abc.conditions_sum(), 4);
15930 },
15931 );
15932 }
15933
15934 /// The #1161 control: a ternary whose condition is a *comparison*
15935 /// must not move. The `>` already supplied its condition and the
15936 /// branches are unnegated, so routing the slots adds nothing.
15937 #[cfg(feature = "irules")]
15938 #[test]
15939 fn irules_abc_comparison_ternary_is_unchanged_by_slot_routing() {
15940 check_metrics::<IrulesParser>(
15941 "when X {\n set y [expr { $a > 0 ? 1 : 0 }]\n}\n",
15942 "foo.irule",
15943 |metric| {
15944 assert_eq!(metric.abc.conditions_sum(), 2);
15945 },
15946 );
15947 }
15948
15949 /// A parenthesised ternary condition scores the same as a bare one.
15950 ///
15951 /// The grammar inlines `( … )` as anonymous children of
15952 /// `ternary_expr` rather than wrapping them in a node, so a
15953 /// fixed-index reading of the slots would shift right by one and
15954 /// mis-assign every operand. This is the input that discriminates
15955 /// the token-relative location the fix uses.
15956 #[cfg(feature = "irules")]
15957 #[test]
15958 fn irules_abc_parenthesised_ternary_condition_matches_the_bare_form() {
15959 // `check_metrics` takes a bare `fn`, so it cannot carry the
15960 // first measurement into the second comparison; `metrics_verbatim`
15961 // returns a value instead.
15962 let conditions = |source: &str| {
15963 crate::test_support::metrics_verbatim(
15964 crate::LANG::Irules,
15965 source.as_bytes(),
15966 crate::MetricsOptions::default(),
15967 )
15968 .abc
15969 .conditions_sum()
15970 };
15971 let bare = conditions("when X {\n set y [expr { $a ? !$b : !$c }]\n}\n");
15972 assert_eq!(bare, 4, "the bare form is the documented reference value");
15973 assert_eq!(
15974 conditions("when X {\n set y [expr { ($a) ? !$b : !$c }]\n}\n"),
15975 bare,
15976 "parenthesising the condition must not change the count"
15977 );
15978 }
15979
15980 // #1454's five-language sweep of #1422's C# rule: a pattern-match
15981 // guard is a condition *slot*, so every spelling of one contributes
15982 // exactly one condition, and the guard is a cyclomatic decision the
15983 // arm it guards does not already pay for.
15984 //
15985 // Each fixture is the §11 triple — an operator guard (`> 5`, whose
15986 // comparison token already counted before the fix), a call guard,
15987 // and a bare-identifier guard (the two that counted nothing) —
15988 // plus a parenthesised guard, which is the only member that
15989 // exercises the boolean-context seed, plus an unguarded control
15990 // that must stay where it was. A single-spelling fixture cannot
15991 // show the slot works: before the fix the operator member already
15992 // read the right number.
15993 //
15994 // The controls are load-bearing twice over. `none` pins that the
15995 // slot fires on the guard and not on the arm — a `+1` on every arm
15996 // would satisfy every guarded row and break this one — and the
15997 // helper (`is_even` / `isEven`) pins that the call-guard row is
15998 // scoring its guard rather than its call, since a call is an ABC
15999 // *branch* everywhere and a condition nowhere.
16000 //
16001 // `assert_members_score` rather than `assert_every_member_scores`:
16002 // the guarded members and their control sit at different values,
16003 // which is the comparison these tests exist to make.
16004
16005 #[cfg(feature = "rust")]
16006 #[test]
16007 fn rust_match_guard_scores_one_condition_however_spelled() {
16008 let src = "fn is_even(n: i32) -> bool {
16009 n == 0
16010 }
16011 fn tok(x: i32) -> i32 {
16012 match x { n if n > 5 => 1, _ => 0 }
16013 }
16014 fn call(x: i32) -> i32 {
16015 match x { n if is_even(n) => 1, _ => 0 }
16016 }
16017 fn bare(x: i32, b: bool) -> i32 {
16018 match x { _ if b => 1, _ => 0 }
16019 }
16020 fn paren(x: i32, b: bool) -> i32 {
16021 match x { n if (b) => n, _ => 0 }
16022 }
16023 fn none(x: i32) -> i32 {
16024 match x { 1 => 1, _ => 0 }
16025 }";
16026 assert_fixture_spells::<RustParser>(
16027 src,
16028 "foo.rs",
16029 &[
16030 // The guard's own `if` keyword, one per guarded member.
16031 // It is also how Rust cyclomatic already saw the guard,
16032 // which is why only the ABC half moved here.
16033 (Rust::If as u16, 4, "match guards"),
16034 (Rust::GT as u16, 1, "`tok`'s `>`"),
16035 (Rust::CallExpression as u16, 1, "the `is_even` call"),
16036 (
16037 Rust::ParenthesizedExpression as u16,
16038 1,
16039 "`paren`'s parenthesised guard",
16040 ),
16041 // Two arms per member: the guarded one and its bare `_`.
16042 (Rust::MatchPattern as u16, 10, "match arms"),
16043 ],
16044 );
16045 check_func_space::<RustParser, _>(src, "foo.rs", |space| {
16046 assert_members_score(
16047 &space,
16048 &[
16049 ("is_even", 1, 1),
16050 ("tok", 2, 3),
16051 // Was 1 — the guard spelling nothing counted.
16052 ("call", 2, 3),
16053 ("bare", 2, 3),
16054 ("paren", 2, 3),
16055 ("none", 1, 2),
16056 ],
16057 );
16058 });
16059 }
16060
16061 #[cfg(feature = "java")]
16062 #[test]
16063 fn java_pattern_switch_guard_scores_one_condition_however_spelled() {
16064 let src = "class T {
16065 static boolean isEven(int i) { return i == 0; }
16066 int tok(Object o) { return switch (o) { case Integer i when i > 5 -> 1; default -> 0; }; }
16067 int call(Object o) { return switch (o) { case Integer i when isEven(i) -> 1; default -> 0; }; }
16068 int bare(Object o, boolean b) { return switch (o) { case Integer i when b -> 1; default -> 0; }; }
16069 int paren(Object o, boolean b) { return switch (o) { case Integer i when (b) -> 1; default -> 0; }; }
16070 int none(Object o) { return switch (o) { case Integer i -> 1; default -> 0; }; }
16071 }";
16072 assert_fixture_spells::<JavaParser>(
16073 src,
16074 "foo.java",
16075 &[
16076 (Java::Guard as u16, 4, "`when` guards"),
16077 (Java::GT as u16, 1, "`tok`'s `>`"),
16078 (Java::MethodInvocation as u16, 1, "the `isEven` call"),
16079 // Five `switch (o)` subjects plus `paren`'s `when (b)`.
16080 // Java wraps a switch subject in the same kind, so this
16081 // count moves if either is edited out.
16082 (
16083 Java::ParenthesizedExpression as u16,
16084 6,
16085 "the switch subjects and `paren`'s guard",
16086 ),
16087 ],
16088 );
16089 check_func_space::<JavaParser, _>(src, "foo.java", |space| {
16090 assert_members_score(
16091 &space.spaces[0],
16092 &[
16093 ("isEven", 1, 1),
16094 // Was 2 conditions against a *flat* cyclomatic of 2:
16095 // Java had neither half of the rule, so the guard
16096 // was invisible to cyclomatic and visible to ABC
16097 // only through whatever operator it spelled.
16098 ("tok", 2, 3),
16099 ("call", 2, 3),
16100 ("bare", 2, 3),
16101 ("paren", 2, 3),
16102 ("none", 1, 2),
16103 ],
16104 );
16105 });
16106 }
16107
16108 #[cfg(feature = "python")]
16109 #[test]
16110 fn python_case_guard_scores_one_condition_however_spelled() {
16111 let src = "def is_even(n):
16112 return n == 0
16113def tok(x):
16114 match x:
16115 case n if n > 5:
16116 return 1
16117 case _:
16118 return 0
16119def call(x):
16120 match x:
16121 case n if is_even(x):
16122 return 1
16123 case _:
16124 return 0
16125def bare(x, b):
16126 match x:
16127 case _ if b:
16128 return 1
16129 case _:
16130 return 0
16131def paren(x, b):
16132 match x:
16133 case n if (b):
16134 return n
16135 case _:
16136 return 0
16137def none(x):
16138 match x:
16139 case 1:
16140 return 1
16141 case _:
16142 return 0
16143";
16144 assert_fixture_spells::<PythonParser>(
16145 src,
16146 "foo.py",
16147 &[
16148 // The `case` guard clause. A comprehension filter is the
16149 // same kind in a different role, and the `guard` field
16150 // is what keeps it out of the slot — there is none in
16151 // this fixture, and `python_comprehension_if_clause_is_not_a_case_guard`
16152 // is where that separation is pinned.
16153 (Python::IfClause as u16, 4, "`case` guards"),
16154 (
16155 Python::ComparisonOperator as u16,
16156 2,
16157 "`tok`'s `>` and `is_even`'s `==`",
16158 ),
16159 (Python::Call as u16, 1, "the `is_even` call"),
16160 (
16161 Python::ParenthesizedExpression as u16,
16162 1,
16163 "`paren`'s parenthesised guard",
16164 ),
16165 ],
16166 );
16167 check_func_space::<PythonParser, _>(src, "foo.py", |space| {
16168 assert_members_score(
16169 &space,
16170 &[
16171 ("is_even", 1, 1),
16172 ("tok", 2, 3),
16173 ("call", 2, 3),
16174 ("bare", 2, 3),
16175 ("paren", 2, 3),
16176 ("none", 1, 2),
16177 ],
16178 );
16179 });
16180 }
16181
16182 // A comprehension's `if` filter is an `if_clause` too, and it is not
16183 // a `case` guard: the slot reads `case_clause`'s `guard` field, so
16184 // the two cannot be confused by construction. Python cyclomatic does
16185 // count the filter (through the same `If` keyword token it counts a
16186 // guard by), and ABC does not — a pre-existing divergence this
16187 // change deliberately leaves where it found it. The test is here so
16188 // that a later `IfClause` arm added without the field read fails
16189 // loudly rather than moving comprehensions silently.
16190 #[cfg(feature = "python")]
16191 #[test]
16192 fn python_comprehension_if_clause_is_not_a_case_guard() {
16193 let src = "def m(xs):
16194 return [x for x in xs if x]
16195";
16196 assert_fixture_spells::<PythonParser>(
16197 src,
16198 "foo.py",
16199 &[(Python::IfClause as u16, 1, "the comprehension filter")],
16200 );
16201 check_func_space::<PythonParser, _>(src, "foo.py", |space| {
16202 assert_members_score(&space, &[("m", 0, 3)]);
16203 });
16204 }
16205
16206 #[cfg(feature = "ruby")]
16207 #[test]
16208 fn ruby_in_clause_guard_scores_one_condition_however_spelled() {
16209 let src = "def is_even(x)
16210 x == 0
16211end
16212def tok(x)
16213 case x
16214 in [n] if n > 5 then 1
16215 in _ then 0
16216 end
16217end
16218def call(x)
16219 case x
16220 in [n] if n.even? then 1
16221 in _ then 0
16222 end
16223end
16224def bare(x, b)
16225 case x
16226 in [n] if b then 1
16227 in _ then 0
16228 end
16229end
16230def unguard(x, b)
16231 case x
16232 in [n] unless b then 1
16233 in _ then 0
16234 end
16235end
16236def paren(x, b)
16237 case x
16238 in [n] if (b) then 1
16239 in _ then 0
16240 end
16241end
16242def none(x)
16243 case x
16244 in [n] then 1
16245 in _ then 0
16246 end
16247end
16248";
16249 assert_fixture_spells::<RubyParser>(
16250 src,
16251 "foo.rb",
16252 &[
16253 (Ruby::IfGuard as u16, 4, "`if` guards"),
16254 (Ruby::UnlessGuard as u16, 1, "the `unless` guard"),
16255 (Ruby::GT as u16, 1, "`tok`'s `>`"),
16256 (
16257 Ruby::ParenthesizedStatements as u16,
16258 1,
16259 "`paren`'s parenthesised guard",
16260 ),
16261 ],
16262 );
16263 // `Ruby::Guard` (210) is the hidden `_guard` supertype: the two
16264 // dispatchers list it beside the concrete kinds so a grammar
16265 // that starts emitting it keeps working, and this pins that it
16266 // does not emit it today — otherwise the defensive arm is
16267 // indistinguishable from a dead one
16268 // (`.claude/rules/grammar-dispatch.md` §2).
16269 let parser = RubyParser::new(
16270 src.as_bytes().to_vec(),
16271 &std::path::PathBuf::from("foo.rb"),
16272 None,
16273 );
16274 assert!(
16275 !ast_has_kind_id(&parser, Ruby::Guard as u16),
16276 "`_guard` stopped being hidden — the defensive arms now fire"
16277 );
16278 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
16279 assert_members_score(
16280 &space,
16281 &[
16282 ("is_even", 1, 1),
16283 // Was 2 / 2: Ruby had neither half either, and the
16284 // arm's own condition supplied the 2 that made the
16285 // operator spelling look correct.
16286 ("tok", 2, 3),
16287 ("call", 2, 3),
16288 ("bare", 2, 3),
16289 ("unguard", 2, 3),
16290 ("paren", 2, 3),
16291 ("none", 1, 2),
16292 ],
16293 );
16294 });
16295 }
16296
16297 // Elixir is the inverse of the four above: ABC counted the `when`
16298 // token from the start and cyclomatic had no arm at all, so a guard
16299 // read as a condition with no decision behind it. #1454 adds the
16300 // decision, which is why `tok` / `call` / `bare` move on the
16301 // cyclomatic axis here and on the ABC axis everywhere else.
16302 //
16303 // Elixir was also the one language of the six that #1454 left
16304 // *without* the slot. Its `when` arm added a flat one on top of
16305 // whatever the guard's sub-structure already scored, so `tok` came
16306 // back 3 against `call` and `bare`'s 2 — the spelling-dependence the
16307 // batch exists to remove, and a `.claude/rules/grammar-dispatch.md`
16308 // §5 double count with the `>` token arm. A whole-branch review of
16309 // the batch found it; all three rows now read 2, level with the
16310 // sibling fixtures above and with their own decision counts.
16311 #[cfg(feature = "elixir")]
16312 #[test]
16313 fn elixir_guard_is_a_decision_however_spelled() {
16314 let src = "defmodule T do
16315 def is_even(x) do
16316 x == 0
16317 end
16318 def tok(x) do
16319 case x do
16320 n when n > 5 -> 1
16321 _ -> 0
16322 end
16323 end
16324 def call(x) do
16325 case x do
16326 n when is_integer(n) -> 1
16327 _ -> 0
16328 end
16329 end
16330 def bare(x, b) do
16331 case x do
16332 _n when b -> 1
16333 _ -> 0
16334 end
16335 end
16336 def none(x) do
16337 case x do
16338 1 -> 1
16339 _ -> 0
16340 end
16341 end
16342end
16343";
16344 assert_fixture_spells::<ElixirParser>(
16345 src,
16346 "foo.ex",
16347 &[
16348 (Elixir::When as u16, 3, "`when` guards"),
16349 (Elixir::GT as u16, 1, "`tok`'s `>`"),
16350 ],
16351 );
16352 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16353 assert_members_score(
16354 &space.spaces[0],
16355 &[
16356 ("is_even", 1, 1),
16357 // All three guarded members were cyclomatic 2 —
16358 // level with `none` — before the decision arm, and
16359 // `tok` was abc 3 before the slot.
16360 ("tok", 2, 3),
16361 ("call", 2, 3),
16362 ("bare", 2, 3),
16363 ("none", 1, 2),
16364 ],
16365 );
16366 });
16367 }
16368
16369 // A statement modifier's `condition` is the same boolean slot as the
16370 // block form's, so `g if (b)` must score what `if (b) then g end`
16371 // does. The modifier kinds were missing from the peel's seed, so a
16372 // parenthesised operand unwrapped into a context only a negation
16373 // could mark boolean and scored 0 (#1521). Every member here is one
16374 // decision, so each reads `conditions == cyclomatic - 1`; `bare` and
16375 // `block` are the analogues the modifier rows must match.
16376 #[cfg(feature = "ruby")]
16377 #[test]
16378 fn ruby_parenthesised_modifier_condition_scores_one() {
16379 let src = "def bare(b)
16380 g if b
16381end
16382def block(b)
16383 if (b) then g end
16384end
16385def if1(b)
16386 g if (b)
16387end
16388def if2(b)
16389 g if ((b))
16390end
16391def unless1(b)
16392 g unless (b)
16393end
16394def unless2(b)
16395 g unless ((b))
16396end
16397def while1(b)
16398 g while (b)
16399end
16400def while2(b)
16401 g while ((b))
16402end
16403def until1(b)
16404 g until (b)
16405end
16406def until2(b)
16407 g until ((b))
16408end
16409";
16410 assert_fixture_spells::<RubyParser>(
16411 src,
16412 "foo.rb",
16413 &[
16414 (Ruby::IfModifier as u16, 3, "`if` modifiers"),
16415 (Ruby::UnlessModifier as u16, 2, "`unless` modifiers"),
16416 (Ruby::WhileModifier as u16, 2, "`while` modifiers"),
16417 (Ruby::UntilModifier as u16, 2, "`until` modifiers"),
16418 (
16419 Ruby::ParenthesizedStatements as u16,
16420 13,
16421 "one layer per `(b)`, two per `((b))`",
16422 ),
16423 ],
16424 );
16425 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
16426 assert_members_score(
16427 &space,
16428 &[
16429 ("bare", 1, 2),
16430 ("block", 1, 2),
16431 ("if1", 1, 2),
16432 ("if2", 1, 2),
16433 ("unless1", 1, 2),
16434 ("unless2", 1, 2),
16435 ("while1", 1, 2),
16436 ("while2", 1, 2),
16437 ("until1", 1, 2),
16438 ("until2", 1, 2),
16439 ],
16440 );
16441 });
16442 }
16443
16444 // The slot's whole claim, on every spelling the Elixir grammar gives
16445 // a guard — and on `assert_every_member_scores`, which asserts §8
16446 // parity where the fixture above cannot (its unguarded control and
16447 // its bare-comparison member legitimately sit off their decision
16448 // counts, which is why that one takes a per-member table).
16449 //
16450 // Nine members, nine routes to the same 2:
16451 //
16452 // * `tok` / `eq` — the guard is an operator application, so the
16453 // slot adds nothing and the `>` / `==` token arm supplies the
16454 // one. These were 3 before the slot.
16455 // * `call` / `bare` — no operator counts a `Call` or an
16456 // `Identifier`, so the slot supplies the one directly.
16457 // * `paren` / `negated` — `block` and `unary_operator` are
16458 // wrappers, peeled by `elixir_wrapper_operand` before the slot
16459 // asks whether an operator already paid. `negated` covers the
16460 // keyword `not`, which the peel once did not recognise as a
16461 // negation.
16462 // * `membership` / `nonmembership` — `in` / `not in` are
16463 // relational operators, given the by-use arm #1461 gave the
16464 // other five languages. Without it these two would score zero,
16465 // since the slot correctly declines an operator application.
16466 //
16467 // A repeated guard (`when a when b`) is deliberately absent: it is
16468 // an or-chain and scores one per alternative, so it belongs with
16469 // `elixir_repeated_guard_matches_the_or_chain` below rather than in
16470 // a table whose every row reads 2.
16471 //
16472 // §11: each bullet is an independent path, and no other member can
16473 // stand in for it — deleting the `in` arm leaves `membership` and
16474 // `nonmembership` alone failing, deleting the `not` recognition
16475 // leaves `negated` alone failing.
16476 #[cfg(feature = "elixir")]
16477 #[test]
16478 fn elixir_guard_scores_one_however_spelled() {
16479 let src = "defmodule T do
16480 def tok(x) do
16481 case x do
16482 n when n > 5 -> 1
16483 _ -> 0
16484 end
16485 end
16486 def eq(x) do
16487 case x do
16488 n when n == 5 -> 1
16489 _ -> 0
16490 end
16491 end
16492 def call(x) do
16493 case x do
16494 n when is_integer(n) -> 1
16495 _ -> 0
16496 end
16497 end
16498 def bare(x, b) do
16499 case x do
16500 _n when b -> 1
16501 _ -> 0
16502 end
16503 end
16504 def paren(x, b) do
16505 case x do
16506 _n when (b) -> 1
16507 _ -> 0
16508 end
16509 end
16510 def negated(x, b) do
16511 case x do
16512 _n when not b -> 1
16513 _ -> 0
16514 end
16515 end
16516 def membership(x) do
16517 case x do
16518 n when n in [1, 2] -> 1
16519 _ -> 0
16520 end
16521 end
16522 def nonmembership(x) do
16523 case x do
16524 n when n not in [1, 2] -> 1
16525 _ -> 0
16526 end
16527 end
16528end
16529";
16530 assert_fixture_spells::<ElixirParser>(
16531 src,
16532 "foo.ex",
16533 &[
16534 (Elixir::When as u16, 8, "one `when` guard per member"),
16535 (Elixir::GT as u16, 1, "`tok`'s `>`"),
16536 (Elixir::EQEQ as u16, 1, "`eq`'s `==`"),
16537 (Elixir::Block as u16, 1, "`paren`'s parenthesised guard"),
16538 (Elixir::Not as u16, 1, "`negated`'s `not`"),
16539 (Elixir::In as u16, 1, "`membership`'s `in`"),
16540 (Elixir::Notin as u16, 1, "`nonmembership`'s `not in`"),
16541 ],
16542 );
16543 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16544 assert_every_member_scores(
16545 &space.spaces[0],
16546 8,
16547 2,
16548 "one for the `case` arm and one for the guard, however the guard is spelled",
16549 );
16550 });
16551 }
16552
16553 // Elixir's repeated guard is an or-chain, not a longer spelling of
16554 // one guard: each `when` expression is tried in turn and evaluation
16555 // moves to the next when the previous one is false *or raises*. So
16556 // `when a when b` carries the alternatives `when a or b` does, and
16557 // the two spellings have to score alike.
16558 //
16559 // They did not. The nesting is right-associative
16560 // (`head when (a when (b when c))`), only the outermost `when` sat
16561 // on the anchor `elixir_when_is_guard` tests, and the `Abc` slot
16562 // peeled to the last alternative and scored that one — so a
16563 // repeated guard read *identically to a single guard* on both axes
16564 // while the `or` form read one higher per alternative.
16565 //
16566 // Asserted as an equality against the `or` spelling rather than
16567 // against separate literals, so neither row can drift alone; the
16568 // literals are here too, because a pair of equal wrong numbers
16569 // would satisfy the equality on its own.
16570 //
16571 // Both anchors, because the climb has to reach each: a definition
16572 // `Call`'s `arguments` (`repeated` / `ored`) and a `stab_clause`'s
16573 // `left` (`clause` / `clause_or`). `single` is the control the
16574 // repeated rows must sit *above*, which is the comparison that
16575 // failed before.
16576 #[cfg(feature = "elixir")]
16577 #[test]
16578 fn elixir_repeated_guard_matches_the_or_chain() {
16579 let src = "defmodule T do
16580 def repeated(n) when is_integer(n) when is_float(n) when is_atom(n) do
16581 n
16582 end
16583 def ored(n) when is_integer(n) or is_float(n) or is_atom(n) do
16584 n
16585 end
16586 def single(n) when is_integer(n) do
16587 n
16588 end
16589 def clause(x) do
16590 case x do
16591 n when is_integer(n) when is_float(n) -> 1
16592 _ -> 0
16593 end
16594 end
16595 def clause_or(x) do
16596 case x do
16597 n when is_integer(n) or is_float(n) -> 1
16598 _ -> 0
16599 end
16600 end
16601end
16602";
16603 assert_fixture_spells::<ElixirParser>(
16604 src,
16605 "foo.ex",
16606 &[
16607 (
16608 Elixir::When as u16,
16609 8,
16610 "three repeated, one `or`-spelled, one single, two \
16611 repeated and one `or`-spelled in a clause",
16612 ),
16613 (Elixir::Or as u16, 3, "the two `or` spellings' operators"),
16614 ],
16615 );
16616 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16617 // Positional: a `def` whose head carries a guard parses its
16618 // name out of a `binary_operator` rather than a plain
16619 // `Call`, so `repeated`, `ored` and `single` all come back
16620 // `<anonymous>` (the same naming gap
16621 // `elixir_typespec_when_is_not_a_guard` reads around).
16622 let members: Vec<(u64, u64)> = space.spaces[0]
16623 .spaces
16624 .iter()
16625 .map(|m| {
16626 (
16627 m.metrics.abc.conditions(),
16628 m.metrics.cyclomatic.cyclomatic(),
16629 )
16630 })
16631 .collect();
16632 assert_eq!(
16633 members,
16634 vec![(3, 4), (3, 4), (1, 2), (3, 4), (3, 4)],
16635 "a repeated guard scores one alternative per `when`; the \
16636 single guard is the control one alternative lower"
16637 );
16638 assert_eq!(
16639 members[0], members[1],
16640 "`when a when b when c` is the or-chain `when a or b or c`"
16641 );
16642 assert_eq!(
16643 members[3], members[4],
16644 "and the same holds on a `stab_clause` anchor"
16645 );
16646 });
16647 }
16648
16649 // The gate that makes the Elixir arm safe. Elixir has no dedicated
16650 // guard production, and a typespec's binding clause spells the same
16651 // `when` token — so an ungated arm would have made type syntax a
16652 // decision. It was already an ABC condition against no decision
16653 // anywhere, which this removes.
16654 //
16655 // Both members carry the same `@spec`; only `guarded` carries a real
16656 // head guard, so the difference between the two rows is the guard
16657 // and nothing else.
16658 #[cfg(feature = "elixir")]
16659 #[test]
16660 fn elixir_typespec_when_is_not_a_guard() {
16661 let src = "defmodule T do
16662 @spec plain(a) :: a when a: integer
16663 def plain(x) do
16664 x
16665 end
16666 @spec guarded(a) :: a when a: integer
16667 def guarded(x) when is_integer(x) do
16668 x
16669 end
16670end
16671";
16672 assert_fixture_spells::<ElixirParser>(
16673 src,
16674 "foo.ex",
16675 &[(
16676 Elixir::When as u16,
16677 3,
16678 "two typespec `when`s and one head guard",
16679 )],
16680 );
16681 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16682 let module = &space.spaces[0];
16683 // Positional rather than by name: a `def` whose head carries
16684 // a guard parses its name out of a `binary_operator` instead
16685 // of a plain `Call` target, and the space comes back
16686 // `<anonymous>`. That naming gap predates this change and is
16687 // why `assert_members_score` cannot serve here.
16688 let members: Vec<(u64, u64)> = module
16689 .spaces
16690 .iter()
16691 .map(|m| {
16692 (
16693 m.metrics.abc.conditions(),
16694 m.metrics.cyclomatic.cyclomatic(),
16695 )
16696 })
16697 .collect();
16698 assert_eq!(
16699 members,
16700 vec![(0, 1), (1, 2)],
16701 "`plain` scores nothing; `guarded` scores its head guard \
16702 in both metrics"
16703 );
16704 // The typespecs sit in the module body, outside either
16705 // member, so their (non-)contribution has to be read off
16706 // the container. Was 2 before the gate: one per `@spec`.
16707 assert_eq!(
16708 module.metrics.abc.conditions(),
16709 0,
16710 "a typespec `when` is type syntax, not a guard"
16711 );
16712 });
16713 }
16714
16715 // The other half of that gate's allowlist. `elixir_when_is_guard`
16716 // accepts a definition `Call`'s `arguments` anchor when the call's
16717 // keyword is a method macro *or* one of `defguard` / `defguardp`,
16718 // and the second disjunct is its own path: `elixir_is_method_macro`
16719 // spells only `def` / `defp` / `defmacro` / `defmacrop`, so a
16720 // `defguard` head reaches the anchor test solely through the
16721 // `matches!`. Nothing in the suite spelled `defguard` before this,
16722 // which left the disjunct scored by no fixture at all.
16723 //
16724 // `defguard` is where a guard is most obviously a decision — the
16725 // macro exists to name one — so an ungated head would have scored
16726 // the construct zero on both axes while the `def f(x) when g`
16727 // spelling it expands into scores one.
16728 //
16729 // Read off the *module*, not off a member: `defguard` is not a
16730 // method macro, so it opens no function space and its guard lands
16731 // in the container's body. `plain` is the control that pins the
16732 // module's rows to the two guards and nothing else, and the guard
16733 // bodies are calls (`is_integer/1`, `is_atom/1`) rather than
16734 // comparisons so neither can supply a condition of its own.
16735 #[cfg(feature = "elixir")]
16736 #[test]
16737 fn elixir_defguard_head_is_a_guard() {
16738 let src = "defmodule T do
16739 defguard is_int(x) when is_integer(x)
16740 defguardp is_at(x) when is_atom(x)
16741 def plain(x) do
16742 x
16743 end
16744end
16745";
16746 assert_fixture_spells::<ElixirParser>(
16747 src,
16748 "foo.ex",
16749 &[(
16750 Elixir::When as u16,
16751 2,
16752 "one per `defguard` / `defguardp` head",
16753 )],
16754 );
16755 check_func_space::<ElixirParser, _>(src, "foo.ex", |space| {
16756 let module = &space.spaces[0];
16757 assert_eq!(
16758 (
16759 module.metrics.abc.conditions(),
16760 module.metrics.cyclomatic.cyclomatic()
16761 ),
16762 (2, 3),
16763 "each of `defguard` and `defguardp` carries one guard, on \
16764 both axes (cyclomatic counts from its base of 1)"
16765 );
16766 let members: Vec<(u64, u64)> = module
16767 .spaces
16768 .iter()
16769 .map(|m| {
16770 (
16771 m.metrics.abc.conditions(),
16772 m.metrics.cyclomatic.cyclomatic(),
16773 )
16774 })
16775 .collect();
16776 assert_eq!(
16777 members,
16778 vec![(0, 1)],
16779 "`defguard` opens no function space, so `plain` is the \
16780 module's only member and scores nothing"
16781 );
16782 });
16783 }
16784
16785 // #1461's structural half: a relational operator scores by *use*,
16786 // a value-bearing operand scores in a boolean *slot*.
16787 //
16788 // Until this, the relational constructs a grammar spells as their
16789 // own production — C#'s two `is` tests, Java's and Groovy's
16790 // `instanceof`, Groovy's `in`, Kotlin's `is` / `in`, Ruby's
16791 // one-line `in` — reached `stats.conditions` only through
16792 // `<lang>_bool_terminal_kinds!()`, which every walker consults
16793 // inside a boolean slot and nowhere else. The comparison token
16794 // beside each of them carried no such gate, so one language scored
16795 // two spellings of the same relational test differently by
16796 // position:
16797 //
16798 // val b = a == c // 1 condition
16799 // val b = a is String // 0 conditions
16800 //
16801 // Fitzpatrick Rule 5 scores a relational operator wherever it is
16802 // written, so the asymmetry was in the mechanism. Each construct
16803 // now has an unconditional arm in its language's `compute` and has
16804 // left the terminal set; listing it in both would score it twice
16805 // (`.claude/rules/grammar-dispatch.md` §5).
16806 //
16807 // Every fixture below carries the same four shapes, because they
16808 // are structurally independent paths and covering one says nothing
16809 // about the others (§11):
16810 //
16811 // - `out*` — the construct with no boolean slot above it. This is
16812 // the case the change exists for, and the one no prior fixture
16813 // covered: measured against the pre-change binary, every `out*`
16814 // member here scored **0**.
16815 // - `outCtl` — the same member spelling `==`. It read 1 before and
16816 // after, so it is the anchor that makes `out*`'s 1 a claim about
16817 // the construct rather than about the member shape
16818 // (`.claude/rules/testing.md`, "Perturb the fixture as well as
16819 // the production line").
16820 // - `in*` / `inAnd` — the predicate slot and the `&&`-chain walker,
16821 // the two paths that already owned the count. They must be
16822 // *unchanged*; a §5 double count surfaces here as a 2 against the
16823 // control's 1, which is how #1459 caught the Kotlin `as?`
16824 // collision.
16825 // - `forIn` (Kotlin / Groovy / Ruby) — the `for … in …` header,
16826 // which spells the same keyword from a different production and
16827 // must stay at 0. It is the reason those three count the node and
16828 // not the token.
16829
16830 #[cfg(feature = "csharp")]
16831 #[test]
16832 fn csharp_is_tests_score_outside_a_boolean_slot() {
16833 let src = "class A {
16834 bool outIs(object x) => x is int;
16835 bool outPat(object x) => x is null;
16836 bool outCtl(int x) => x == 1;
16837 int inIs(object x) { if (x is int) { return 1; } return 0; }
16838 int inAnd(object x, bool b) { if (x is int && b) { return 1; } return 0; }
16839 }";
16840 assert_csharp_fixture_spells(
16841 src,
16842 &[
16843 (Csharp::IsExpression as u16, 3, "the bare `is` type tests"),
16844 (
16845 Csharp::IsPatternExpression as u16,
16846 1,
16847 "`outPat`'s null pattern",
16848 ),
16849 (Csharp::EQEQ as u16, 1, "`outCtl`'s comparison"),
16850 ],
16851 );
16852 check_func_space::<CsharpParser, _>(src, "foo.cs", |space| {
16853 assert_members_score(
16854 &space.spaces[0],
16855 &[
16856 // Both were 0, against `outCtl`'s 1 on the same
16857 // expression-bodied shape.
16858 ("outIs", 1, 1),
16859 ("outPat", 1, 1),
16860 ("outCtl", 1, 1),
16861 // Unchanged: the slot no longer counts the test, the
16862 // new arm does, and the total stays put.
16863 ("inIs", 1, 2),
16864 ("inAnd", 2, 3),
16865 ],
16866 );
16867 });
16868 }
16869
16870 #[cfg(feature = "java")]
16871 #[test]
16872 fn java_instanceof_scores_outside_a_boolean_slot() {
16873 let src = "class A {
16874 boolean outOf(Object x) { return x instanceof String; }
16875 boolean outCtl(int x) { return x == 1; }
16876 int inOf(Object x) { if (x instanceof String) { return 1; } return 0; }
16877 int inAnd(Object x, boolean b) { if (x instanceof String && b) { return 1; } return 0; }
16878 }";
16879 assert_fixture_spells::<JavaParser>(
16880 src,
16881 "foo.java",
16882 &[
16883 (
16884 Java::InstanceofExpression as u16,
16885 3,
16886 "the `instanceof` tests",
16887 ),
16888 (Java::EQEQ as u16, 1, "`outCtl`'s comparison"),
16889 ],
16890 );
16891 check_func_space::<JavaParser, _>(src, "foo.java", |space| {
16892 assert_members_score(
16893 &space.spaces[0],
16894 &[
16895 // Was 0, against `outCtl`'s 1 on the same
16896 // `return`-a-predicate shape.
16897 ("outOf", 1, 1),
16898 ("outCtl", 1, 1),
16899 ("inOf", 1, 2),
16900 ("inAnd", 2, 3),
16901 ],
16902 );
16903 });
16904 }
16905
16906 // Groovy carries item 4 as well as the structural pass. `<=>`
16907 // yields -1 / 0 / 1 rather than a boolean, which is why it never
16908 // belonged in the terminal set — that set holds operands — but it
16909 // is a relational operator, and `LTEQGT` is already a condition
16910 // token in Ruby, PHP, C++ and Mozcpp. Groovy was the outlier at 0.
16911 #[cfg(feature = "groovy")]
16912 #[test]
16913 fn groovy_relational_productions_score_outside_a_boolean_slot() {
16914 let src = "class A {
16915 def outIs(x) { def b = x instanceof String; return b }
16916 def outIn(x, l) { def b = x in l; return b }
16917 def outShip(a, c) { def r = a <=> c; return r }
16918 def outCtl(x) { def b = x == 1; return b }
16919 def inIs(x) { if (x instanceof String) { return 1 }; return 0 }
16920 def inIn(x, l) { if (x in l) { return 1 }; return 0 }
16921 def forIn(l) { for (q in l) { }; return 0 }
16922 }";
16923 assert_fixture_spells::<GroovyParser>(
16924 src,
16925 "foo.groovy",
16926 &[
16927 (
16928 Groovy::InstanceofExpression as u16,
16929 2,
16930 "the `instanceof` tests",
16931 ),
16932 (Groovy::MembershipExpression as u16, 2, "the `in` tests"),
16933 (Groovy::SpaceshipExpression as u16, 1, "`outShip`'s `<=>`"),
16934 (
16935 Groovy::ForInStatement as u16,
16936 1,
16937 "`forIn`'s loop header, which spells the same `in`",
16938 ),
16939 (Groovy::EQEQ as u16, 1, "`outCtl`'s comparison"),
16940 ],
16941 );
16942 check_func_space::<GroovyParser, _>(src, "foo.groovy", |space| {
16943 assert_members_score(
16944 &space.spaces[0],
16945 &[
16946 // All three were 0, against `outCtl`'s 1 on the
16947 // identical declare-and-return shape.
16948 ("outIs", 1, 1),
16949 ("outIn", 1, 1),
16950 ("outShip", 1, 1),
16951 ("outCtl", 1, 1),
16952 ("inIs", 1, 2),
16953 ("inIn", 1, 2),
16954 // The loop header is not a membership test and
16955 // stays at 0 — counting the `in` *token* would have
16956 // scored it.
16957 ("forIn", 0, 2),
16958 ],
16959 );
16960 });
16961 }
16962
16963 #[cfg(feature = "kotlin")]
16964 #[test]
16965 fn kotlin_is_and_in_score_outside_a_boolean_slot() {
16966 let src = "class K {
16967 fun outIs(a: Any): Boolean { val b = a is String; return b }
16968 fun outIn(a: Int): Boolean { val b = a in 1..2; return b }
16969 fun outCtl(a: Int): Boolean { val b = a == 1; return b }
16970 fun inIs(a: Any): Int { if (a is String) { return 1 }; return 0 }
16971 fun forIn(l: List<Int>): Int { for (q in l) { }; return 0 }
16972 }";
16973 assert_kotlin_class_members(
16974 src,
16975 &[
16976 (Kotlin::IsExpression as u16, 2, "the `is` tests"),
16977 (Kotlin::InExpression as u16, 1, "`outIn`'s `in` test"),
16978 (
16979 Kotlin::ForStatement as u16,
16980 1,
16981 "`forIn`'s loop header, which spells the same `in`",
16982 ),
16983 (Kotlin::EQEQ as u16, 1, "`outCtl`'s comparison"),
16984 ],
16985 &[
16986 // Both were 0, against `outCtl`'s 1 on the identical
16987 // `val`-and-return shape.
16988 ("outIs", 1, 1),
16989 ("outIn", 1, 1),
16990 ("outCtl", 1, 1),
16991 ("inIs", 1, 2),
16992 ("forIn", 0, 2),
16993 ],
16994 );
16995 }
16996
16997 #[cfg(feature = "ruby")]
16998 #[test]
16999 fn ruby_test_pattern_scores_outside_a_boolean_slot() {
17000 let src = "def out_in(a)
17001 b = a in Integer
17002 b
17003end
17004def out_ctl(a)
17005 b = a == 1
17006 b
17007end
17008def in_in(a)
17009 if a in Integer
17010 return 1
17011 end
17012 0
17013end
17014def for_in(l)
17015 for q in l do end
17016 0
17017end
17018";
17019 assert_fixture_spells::<RubyParser>(
17020 src,
17021 "foo.rb",
17022 &[
17023 (Ruby::TestPattern as u16, 2, "the one-line `in` tests"),
17024 (
17025 Ruby::For as u16,
17026 1,
17027 "`for_in`'s loop header, which spells the same `in`",
17028 ),
17029 (Ruby::EQEQ as u16, 1, "`out_ctl`'s comparison"),
17030 ],
17031 );
17032 check_func_space::<RubyParser, _>(src, "foo.rb", |space| {
17033 assert_members_score(
17034 &space,
17035 &[
17036 // Was 0, against `out_ctl`'s 1 on the identical
17037 // assign-and-return shape.
17038 ("out_in", 1, 1),
17039 ("out_ctl", 1, 1),
17040 ("in_in", 1, 2),
17041 ("for_in", 0, 2),
17042 ],
17043 );
17044 });
17045 }
17046
17047 // #1461 item 1. The walrus is an *operand*, not an operator, so it
17048 // joined the former `python_bool_terminal_kinds!()` and stays slot-scoped —
17049 // `b = (n := g())` outside a predicate is a binding, not a
17050 // decision. Inside one the slot tests `g()`'s truth, and
17051 // `if (n := g()):` scored 0 where the `if g():` it is a
17052 // refactoring of scored 1.
17053 //
17054 // `assignments` is asserted beside `conditions` because the walrus
17055 // is the one construct in the survey that pays on two axes, and
17056 // that is deliberate rather than a §5 double count: ABC's axes are
17057 // independent measurements of the same source, and `if (n := g()):`
17058 // genuinely both binds a name and decides a branch. Without this
17059 // row a later reader has no way to tell the intent from an
17060 // oversight.
17061 #[cfg(feature = "python")]
17062 #[test]
17063 fn python_walrus_is_a_unary_condition() {
17064 let src = "def in_slot(g):
17065 if (n := g()):
17066 return 1
17067 return 0
17068
17069def chain(g, b):
17070 return b and (n := g())
17071
17072def ctrl(g):
17073 if g():
17074 return 1
17075 return 0
17076";
17077 assert_fixture_spells::<PythonParser>(
17078 src,
17079 "foo.py",
17080 &[(Python::NamedExpression as u16, 2, "the walrus bindings")],
17081 );
17082 check_func_space::<PythonParser, _>(src, "foo.py", |space| {
17083 assert_members_score(
17084 &space,
17085 &[
17086 // Was 0, against `ctrl`'s 1 for the same predicate
17087 // without the binding.
17088 ("in_slot", 1, 2),
17089 // The `and`-chain walker is the second, independent
17090 // path (§11): was 1, the bare `b` operand alone.
17091 ("chain", 2, 2),
17092 ("ctrl", 1, 2),
17093 ],
17094 );
17095 for (name, assignments) in [("in_slot", 1u64), ("chain", 1), ("ctrl", 0)] {
17096 assert_eq!(
17097 child_space(&space, name).metrics.abc.assignments(),
17098 assignments,
17099 "{name}: the walrus also pays on the A axis, on purpose"
17100 );
17101 }
17102 });
17103 }
17104
17105 // #1461 item 2. A macro in a boolean slot expands to a boolean
17106 // expression, and `if matches!(x, Some(_))` scored 0 against a
17107 // cyclomatic decision of 1.
17108 //
17109 // `cfg_slot` is in the fixture because the arm's *breadth* is the
17110 // decision, not a side effect: these sets discriminate on slot and
17111 // never on return type, so `cfg!` counts exactly as `matches!`
17112 // does. It is also the construct the corpus actually moves —
17113 // serde's one changed snapshot is `if cfg!(no_underscore_consts)`,
17114 // not a `matches!` — so asserting only `matches!` would leave the
17115 // measured case uncovered.
17116 #[cfg(feature = "rust")]
17117 #[test]
17118 fn rust_macro_invocation_is_a_unary_condition() {
17119 let src = "fn in_slot(x: Option<u8>) -> u8 { if matches!(x, Some(_)) { 1 } else { 0 } }
17120fn cfg_slot() -> u8 { if cfg!(unix) { 1 } else { 0 } }
17121fn chain(x: Option<u8>, b: bool) -> bool { b && matches!(x, Some(_)) }
17122fn ctrl(x: Option<u8>) -> u8 { if x.is_some() { 1 } else { 0 } }
17123";
17124 assert_fixture_spells::<RustParser>(
17125 src,
17126 "foo.rs",
17127 &[(
17128 Rust::MacroInvocation as u16,
17129 3,
17130 "the `matches!` / `cfg!` predicates",
17131 )],
17132 );
17133 check_func_space::<RustParser, _>(src, "foo.rs", |space| {
17134 assert_members_score(
17135 &space,
17136 &[
17137 // Each was 1 — the `else` alone — against `ctrl`'s
17138 // 2 for the same shape with a method call in the
17139 // predicate.
17140 ("in_slot", 2, 2),
17141 ("cfg_slot", 2, 2),
17142 // The `&&`-chain walker, the second independent
17143 // path (§11): was 1, the bare `b` operand alone.
17144 ("chain", 2, 2),
17145 ("ctrl", 2, 2),
17146 ],
17147 );
17148 });
17149 }
17150}
17151
17152/// A comment inside a ternary must not change its ABC conditions
17153/// (#1181).
17154///
17155/// Two opposite defects, one cause: tree-sitter counts a comment among a
17156/// node's children, so it is the operand's previous sibling *and* it
17157/// shifts every positional index.
17158///
17159/// * Languages whose seed asked "is my previous sibling `?` or `:`"
17160/// (C family, PHP, Perl, JS family) read the comment as "not a
17161/// ternary token", flipped the boolean-context seed on for a *branch*
17162/// slot, and **over**-counted: `a ? /*n*/ (b) : c` scored 3 where
17163/// `a ? (b) : c` scores 2.
17164/// * Languages whose branch walk read `child(2)` / `child(4)` (Java,
17165/// C#, Groovy) landed on the comment instead of the operand, never
17166/// inspected it, and **under**-counted: `a ? /*n*/ !b : c` scored 2
17167/// where `a ? !b : c` scores 3.
17168///
17169/// Both slots are now addressed by grammar field. The parenthesised
17170/// operand is the only input that discriminates the first defect and
17171/// the negated operand the only one that discriminates the second —
17172/// existing ternary fixtures use neither.
17173// Gated on the union of the features [`ternary_comment_invariance::cases`]
17174// has a row for, so a build enabling none of them — `--no-default-features
17175// --features go` — drops both tests rather than tripping their
17176// `checked > 0` guards (`.claude/rules/testing.md`, #1286). Both tests
17177// read the same table, so one gate on the module is exact for each.
17178#[cfg(test)]
17179#[cfg(any(
17180 feature = "c",
17181 feature = "cpp",
17182 feature = "csharp",
17183 feature = "groovy",
17184 feature = "java",
17185 feature = "javascript",
17186 feature = "mozcpp",
17187 feature = "mozjs",
17188 feature = "objc",
17189 feature = "perl",
17190 feature = "php",
17191 feature = "typescript"
17192))]
17193mod ternary_comment_invariance {
17194 use crate::test_support::metrics_verbatim;
17195 use crate::{LANG, MetricsOptions};
17196
17197 fn conditions(lang: LANG, source: &str) -> u64 {
17198 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17199 .abc
17200 .conditions_sum()
17201 }
17202
17203 /// `(base, with_comment)` for a parenthesised and a negated branch
17204 /// operand, per language.
17205 fn cases(lang: LANG) -> Option<[(String, String); 2]> {
17206 // `{}` marks the consequence slot; `/*n*/` the inserted comment.
17207 let (template, paren, negated, comment) = match lang {
17208 LANG::Cpp | LANG::C | LANG::Objc | LANG::Mozcpp => {
17209 ("int f(){ int x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
17210 }
17211 LANG::Java => (
17212 "class K{ void f(){ int x = a ? {} : c; } }",
17213 "(b)",
17214 "!b",
17215 "/*n*/ ",
17216 ),
17217 LANG::Csharp => (
17218 "class K{ void f(){ var x = a ? {} : c; } }",
17219 "(b)",
17220 "!b",
17221 "/*n*/ ",
17222 ),
17223 LANG::Groovy => ("def f(){ def x = a ? {} : c }", "(b)", "!b", "/*n*/ "),
17224 LANG::Javascript | LANG::Typescript | LANG::Tsx | LANG::Mozjs => {
17225 ("function f(){ var x = a ? {} : c; }", "(b)", "!b", "/*n*/ ")
17226 }
17227 LANG::Php => (
17228 "<?php function f(){ $x = $a ? {} : $c; }",
17229 "($b)",
17230 "!$b",
17231 "/*n*/ ",
17232 ),
17233 // Perl has no block comment: `#` runs to end of line, so the
17234 // comment must carry its own newline.
17235 LANG::Perl => ("sub f { my $x = $a ? {} : $c; }", "($b)", "!$b", "# n\n "),
17236 _ => return None,
17237 };
17238 let build = |operand: &str, with_comment: bool| {
17239 let slot = if with_comment {
17240 format!("{comment}{operand}")
17241 } else {
17242 operand.to_owned()
17243 };
17244 template.replace("{}", &slot)
17245 };
17246 Some([
17247 (build(paren, false), build(paren, true)),
17248 (build(negated, false), build(negated, true)),
17249 ])
17250 }
17251
17252 #[test]
17253 fn a_comment_before_a_branch_operand_changes_nothing() {
17254 let mut checked = 0;
17255 for lang in LANG::into_enum_iter() {
17256 if !lang.is_enabled() {
17257 continue;
17258 }
17259 let Some(pairs) = cases(lang) else { continue };
17260 checked += 1;
17261 for (base, commented) in pairs {
17262 assert_eq!(
17263 conditions(lang, &commented),
17264 conditions(lang, &base),
17265 "{lang:?}: a comment changed the ABC conditions of a ternary\n \
17266 without: {base}\n with: {commented}"
17267 );
17268 }
17269 }
17270 assert!(
17271 checked > 0,
17272 "no ternary language enabled; this test asserted nothing"
17273 );
17274 }
17275
17276 /// The absolute values the invariance test compares against, so a
17277 /// regression that moved *both* sides equally still fails.
17278 ///
17279 /// expected: `a ? (b) : c` counts the `?` marker plus the condition
17280 /// `a` in boolean context = 2. Negating the consequence adds one
17281 /// more, since `!b` establishes boolean content for that slot = 3.
17282 #[test]
17283 fn the_baseline_values_are_two_and_three() {
17284 let mut checked = 0;
17285 for lang in LANG::into_enum_iter() {
17286 if !lang.is_enabled() {
17287 continue;
17288 }
17289 let Some([(paren, _), (negated, _)]) = cases(lang) else {
17290 continue;
17291 };
17292 assert_eq!(
17293 conditions(lang, &paren),
17294 2,
17295 "{lang:?}: parenthesised branch"
17296 );
17297 assert_eq!(conditions(lang, &negated), 3, "{lang:?}: negated branch");
17298 checked += 1;
17299 }
17300 assert!(
17301 checked > 0,
17302 "no ternary language enabled; this test asserted nothing"
17303 );
17304 }
17305}
17306
17307/// A keyword negation must score like its symbolic twin (#1182).
17308///
17309/// `not b` and `!b` are the same negation — they differ in precedence,
17310/// not in meaning, and ABC counts the negation rather than the parse.
17311/// Ruby and Perl tested only the `!` token, so `if not b` scored 0
17312/// against `if !b`'s 1, and a `not` ternary scored 2 against the `!`
17313/// form's 4.
17314///
17315/// Lua and Elixir were checked in the same sweep and were already
17316/// correct: Lua's only negation keyword *is* `not` and it was the token
17317/// being tested, and Elixir reaches the same count by another path.
17318/// They are exercised here so a future edit cannot regress them
17319/// silently. Python counts `not` through its own dispatcher arm and has
17320/// no `!` spelling to compare against.
17321// Gated on the union of every row below — the three `pairs` languages
17322// plus Lua, whose test is separate — so a build enabling none of them
17323// drops the module instead of failing its guards
17324// (`.claude/rules/testing.md`, #1286 / #1411). The three tests do *not*
17325// share a row set, so each carries its own narrower gate: a Lua-only
17326// build must not be asked to run the Ruby/Perl baselines.
17327#[cfg(test)]
17328#[cfg(any(
17329 feature = "elixir",
17330 feature = "lua",
17331 feature = "perl",
17332 feature = "ruby"
17333))]
17334mod keyword_negation_parity {
17335 use crate::test_support::metrics_verbatim;
17336 use crate::{LANG, MetricsOptions};
17337
17338 fn conditions(lang: LANG, source: &str) -> u64 {
17339 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17340 .abc
17341 .conditions_sum()
17342 }
17343
17344 /// `(bang_form, keyword_form)` pairs that must score identically.
17345 #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
17346 fn pairs(lang: LANG) -> Option<Vec<(String, String)>> {
17347 let build =
17348 |t: &str| -> (String, String) { (t.replace("{NOT}", "!"), t.replace("{NOT}", "not ")) };
17349 let templates: &[&str] = match lang {
17350 LANG::Ruby => &[
17351 "def f(b)\n if {NOT}b\n 1\n end\nend\n",
17352 "def f(a, b, c)\n x = a ? ({NOT}b) : ({NOT}c)\nend\n",
17353 "def f(a, b, c)\n x = a ? b : ({NOT}c)\nend\n",
17354 ],
17355 LANG::Perl => &[
17356 "sub f { if ({NOT}$b) { 1; } }",
17357 "sub f { my $x = $a ? ({NOT}$b) : ({NOT}$c); }",
17358 "sub f { my $x = $a ? $b : ({NOT}$c); }",
17359 ],
17360 // Already correct before #1182; pinned so they stay that way.
17361 LANG::Elixir => &["def f(b) do\n if {NOT}b do\n 1\n end\nend\n"],
17362 _ => return None,
17363 };
17364 Some(templates.iter().map(|t| build(t)).collect())
17365 }
17366
17367 #[test]
17368 #[cfg(any(feature = "elixir", feature = "perl", feature = "ruby"))]
17369 fn the_not_keyword_scores_like_bang() {
17370 let mut checked = 0;
17371 for lang in LANG::into_enum_iter() {
17372 if !lang.is_enabled() {
17373 continue;
17374 }
17375 let Some(pairs) = pairs(lang) else { continue };
17376 checked += 1;
17377 for (bang, keyword) in pairs {
17378 assert_eq!(
17379 conditions(lang, &keyword),
17380 conditions(lang, &bang),
17381 "{lang:?}: `not` and `!` scored differently\n bang: {bang}\n keyword: {keyword}"
17382 );
17383 }
17384 }
17385 assert!(
17386 checked > 0,
17387 "no language enabled; this test asserted nothing"
17388 );
17389 }
17390
17391 /// The absolute values, so a regression that moved both spellings
17392 /// equally still fails.
17393 ///
17394 /// expected: `if !b` is one condition — the negated bare operand.
17395 /// `a ? (!b) : (!c)` is four: the `?` marker, the condition `a` in
17396 /// boolean context, and one per negated branch operand.
17397 #[test]
17398 #[cfg(any(feature = "perl", feature = "ruby"))]
17399 fn the_baseline_values_are_one_and_four() {
17400 let mut checked = 0;
17401 for (lang, guard, ternary) in [
17402 (
17403 LANG::Ruby,
17404 "def f(b)\n if not b\n 1\n end\nend\n",
17405 "def f(a, b, c)\n x = a ? (not b) : (not c)\nend\n",
17406 ),
17407 (
17408 LANG::Perl,
17409 "sub f { if (not $b) { 1; } }",
17410 "sub f { my $x = $a ? (not $b) : (not $c); }",
17411 ),
17412 ] {
17413 if !lang.is_enabled() {
17414 continue;
17415 }
17416 assert_eq!(conditions(lang, guard), 1, "{lang:?}: `if not b`");
17417 assert_eq!(conditions(lang, ternary), 4, "{lang:?}: `not` ternary");
17418 checked += 1;
17419 }
17420 assert!(
17421 checked > 0,
17422 "no language enabled; this test asserted nothing"
17423 );
17424 }
17425
17426 /// Lua's only negation keyword is `not`, so it has no `!` twin to
17427 /// compare against — its guard is that the keyword counts at all.
17428 #[test]
17429 #[cfg(feature = "lua")]
17430 fn lua_counts_its_only_negation_keyword() {
17431 // Was a silent `if !is_enabled() { return; }`, which made the
17432 // whole test a no-op rather than absent whenever the runtime
17433 // check and the feature disagreed. The `cfg` above is the
17434 // absence half; this is the loud residual half (#1286).
17435 assert!(
17436 LANG::Lua.is_enabled(),
17437 "compiled under `feature = \"lua\"` but `LANG::Lua` reports disabled; \
17438 this test asserted nothing"
17439 );
17440 assert_eq!(
17441 conditions(
17442 LANG::Lua,
17443 "function f(b)\n if not b then return 1 end\nend\n"
17444 ),
17445 1
17446 );
17447 }
17448}
17449
17450/// A numeric literal used as a bare `&&` / `||` operand is a Fitzpatrick
17451/// Rule 9 unary condition in every truthy-valued language (#1379).
17452///
17453/// Ruby's terminal-bool set named `integer` and none of `float` /
17454/// `rational` / `complex`, Elixir's named `integer` and not `float`, and
17455/// Perl's named no numeric kind at all — so `a && 1.0` scored one
17456/// condition where `a && 1` scored two, and in Perl even `$a && 1`
17457/// scored one. Python had both kinds since #772 and is the control the
17458/// other three were brought level with.
17459///
17460/// #1410 closed the six sets #1379 deferred: PHP, Groovy, and the
17461/// name-keyed C / C++ / Mozcpp / Objective-C set, where the omission
17462/// was visible *within* one language — `if (true)` scored one condition
17463/// and `if (1)` none, because `"true"` was in the set and
17464/// `number_literal` was not.
17465///
17466/// The headline claim of each case is a *comparison*: a numeric operand
17467/// must score exactly what an identifier operand scores in the same slot.
17468/// That is what discriminates the defect — with the kind missing from the
17469/// set the numeric form drops while the identifier form does not — and it
17470/// gives a failure message naming both spellings.
17471/// `every_operand_scores_its_recorded_values` then pins the absolute
17472/// numbers, so a regression moving *both* sides equally still fails, and
17473/// carries the cross-metric anchor `.claude/rules/grammar-dispatch.md` §8
17474/// asks for.
17475///
17476/// Two slots per language, because the sets feed two independent walker
17477/// paths (grammar-dispatch §11) and Perl's defect showed in both: the
17478/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
17479/// only the first leaves `perl_count_condition` and `ruby_count_condition`
17480/// untested. Since #1520 Ruby's predicate pays for any operand no other
17481/// arm counts, terminal or not, so for Ruby only the chain half can tell
17482/// a kind missing from the set; the predicate half pins the slot itself.
17483///
17484/// The recorded cyclomatic figure is the same 3 for both slots — one file
17485/// space, one function space, one decision — while conditions differ (2
17486/// for the chain, 1 for the predicate, since ABC scores an `if` through
17487/// its predicate rather than the keyword). So §8's `conditions ==
17488/// cyclomatic - 1` identity is chain-specific arithmetic, not a law; what
17489/// generalises is that cyclomatic must not move when only the operand
17490/// spelling does, which is what makes a `conditions` move unambiguously
17491/// ABC's.
17492///
17493/// `for_each_case` guards the two ways this table could decay into
17494/// asserting nothing: an emptied `numerics` slice, and a template that
17495/// loses its `{}` slot (which would make every comparison `x == x`).
17496/// Both were reachable in the first draft and neither failed a test.
17497#[cfg(test)]
17498#[cfg(any(
17499 feature = "ruby",
17500 feature = "elixir",
17501 feature = "perl",
17502 feature = "python",
17503 feature = "lua",
17504 feature = "javascript",
17505 feature = "php",
17506 feature = "groovy",
17507 feature = "c",
17508 feature = "cpp",
17509 feature = "mozcpp",
17510 feature = "objc"
17511))]
17512mod numeric_bool_operands {
17513 use crate::test_support::metrics_verbatim;
17514 use crate::{LANG, MetricsOptions};
17515
17516 /// One fixture shape: a source template with a `{}` operand slot, and
17517 /// the `abc.conditions_sum` / `cyclomatic_sum` every spelling of that
17518 /// operand must produce.
17519 type Slot = (&'static str, u64, u64);
17520
17521 /// A language's two slots, its identifier baseline operand, the
17522 /// truthy literal operands that must score the same, and how many of
17523 /// those there should be. Every row but the C family's lists numeric
17524 /// literals only; that one adds `char_literal`, which is a second
17525 /// integral-literal kind rather than a second numeric spelling.
17526 ///
17527 /// The count is not bookkeeping. `for_each_case` counts *languages*,
17528 /// so trimming a row's operand list back to `&["1"]` — which is
17529 /// exactly the pre-#1379 fixture — left the whole module green when
17530 /// measured. Pinning the length makes that a deliberate two-line
17531 /// edit instead of a silent one.
17532 type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
17533
17534 fn conditions(lang: LANG, source: &str) -> u64 {
17535 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17536 .abc
17537 .conditions_sum()
17538 }
17539
17540 fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
17541 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17542 .cyclomatic
17543 .cyclomatic_sum()
17544 }
17545
17546 /// `([chain_slot, condition_slot], identifier, numerics)` per language.
17547 ///
17548 /// `{}` is the right-hand operand of a two-operand short-circuit chain
17549 /// in the first slot and the whole `if` predicate in the second, so
17550 /// every fixture differs from its own baseline in exactly one token.
17551 ///
17552 /// Each row lists **one spelling per numeric kind the grammar emits**,
17553 /// which is the unit that matters: #1379's misses were sibling rules
17554 /// under a shared supertype, not aliases of one rule.
17555 ///
17556 /// - Ruby: `integer` / `float` / `rational` / `complex`. `1r` parses
17557 /// to `rational(integer)`, `2i` to `complex(integer)` and `1ri` to
17558 /// `complex(rational(integer))` — verified by `bca dump` — and the
17559 /// walker cannot descend into any wrapper, so listing the wrapper
17560 /// is the only way each scores at all. Radix prefixes and `_`
17561 /// separators fold into `integer`.
17562 /// - Perl: all five members of the hidden `_numeric_literals` choice.
17563 /// `0xff` and `1.5e10` are here because the first cut of #1379
17564 /// named only `integer` and `floating_point` and left them scoring
17565 /// 1 against an identifier's 2. `017` is *not* a sixth row — it
17566 /// lexes as `integer`, and the `octal` kind is unreachable.
17567 /// - Elixir: `integer` / `float` / `char`. `?a` is the codepoint 97,
17568 /// a numeric literal wearing a sigil.
17569 /// - PHP: `integer` / `float`. Every radix prefix, `_` separator and
17570 /// exponent folds into those two, and the `float` *type* keyword
17571 /// `Float2` is a third id rendering to the same name that must
17572 /// stay out of the set (#1410).
17573 /// - Groovy: one consolidated `number_literal`, type suffixes
17574 /// (`1L`, `1.5f`, `1G`) included.
17575 /// - C, C++, Mozcpp and Objective-C: one consolidated
17576 /// `number_literal`, plus `char_literal` — not a numeric spelling
17577 /// but a second integral-literal *kind*, since `'c'` has type
17578 /// `int` in C and `char` in C++ and is truthy for the same reason.
17579 /// All four share one name-keyed set and so run the same fixture,
17580 /// which is the only coverage `Mozcpp` can have: it owns no file
17581 /// extension, and `metrics_verbatim` is the one entry point that
17582 /// reaches a `LANG` without going through one.
17583 ///
17584 /// Python, Lua and JavaScript were already correct and ride along as
17585 /// controls — they are what the first three were measured against,
17586 /// and a future edit that breaks them fails here too. Each folds
17587 /// every radix into one `Number` / `integer` kind, checked by
17588 /// measurement.
17589 fn cases(lang: LANG) -> Option<Case> {
17590 Some(match lang {
17591 LANG::Ruby => (
17592 [
17593 ("def f(a)\n a && {}\nend\n", 2, 3),
17594 ("def f\n if {}\n 1\n end\nend\n", 1, 3),
17595 ],
17596 "b",
17597 &["1", "1.0", "1r", "2i", "1ri"],
17598 5,
17599 ),
17600 LANG::Elixir => (
17601 [
17602 ("def f(a) do\n a && {}\nend\n", 2, 3),
17603 ("def f() do\n if {} do\n 1\n end\nend\n", 1, 3),
17604 ],
17605 "b",
17606 &["1", "1.0", "?a"],
17607 3,
17608 ),
17609 LANG::Perl => (
17610 [
17611 ("sub f {\n my $x = $a && {};\n}\n", 2, 3),
17612 ("sub f {\n if ({}) { 1; }\n}\n", 1, 3),
17613 ],
17614 "$b",
17615 &["1", "1.0", "1.5e10", "0xff"],
17616 4,
17617 ),
17618 LANG::Python => (
17619 [
17620 ("def f(a):\n return a and {}\n", 2, 3),
17621 ("def f():\n if {}:\n return 1\n", 1, 3),
17622 ],
17623 "b",
17624 &["1", "1.0", "0xff", "1j"],
17625 4,
17626 ),
17627 LANG::Lua => (
17628 [
17629 ("function f(a)\n return a and {}\nend\n", 2, 3),
17630 ("function f()\n if {} then return 1 end\nend\n", 1, 3),
17631 ],
17632 "b",
17633 &["1", "1.0", "0xff"],
17634 3,
17635 ),
17636 LANG::Javascript => (
17637 [
17638 ("function f(a) {\n return a && {};\n}\n", 2, 3),
17639 ("function f() {\n if ({}) { return 1; }\n}\n", 1, 3),
17640 ],
17641 "b",
17642 &["1", "1.0", "0xff"],
17643 3,
17644 ),
17645 LANG::Php => (
17646 [
17647 ("<?php\nfunction f($a) {\n return $a && {};\n}\n", 2, 3),
17648 ("<?php\nfunction f() {\n if ({}) { return 1; }\n}\n", 1, 3),
17649 ],
17650 "$b",
17651 &["1", "1.0"],
17652 2,
17653 ),
17654 LANG::Groovy => (
17655 [
17656 ("def f(a) {\n return a && {}\n}\n", 2, 3),
17657 ("def f() {\n if ({}) { return 1 }\n}\n", 1, 3),
17658 ],
17659 "b",
17660 &["1"],
17661 1,
17662 ),
17663 LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc => (
17664 [
17665 ("int f(int a) {\n return a && {};\n}\n", 2, 3),
17666 ("int f() {\n if ({}) { return 1; }\n}\n", 1, 3),
17667 ],
17668 "b",
17669 &["1", "'c'"],
17670 2,
17671 ),
17672 _ => return None,
17673 })
17674 }
17675
17676 /// Runs `check` once per enabled language that has a case, having
17677 /// first established that the case can still assert something.
17678 ///
17679 /// Three guards, all of which had to be added after the first draft
17680 /// shipped without them and a measured perturbation of each left the
17681 /// whole module green:
17682 ///
17683 /// - **`checked > 0`** is the non-vacuity half of the rule in
17684 /// `.claude/rules/testing.md`. The `#[cfg(any(feature = …))]` on
17685 /// this module makes the tests *absent* when no truthy-valued
17686 /// language is compiled in; this catches the residual case where
17687 /// the runtime `is_enabled()` check stops agreeing with the feature
17688 /// it compiled under.
17689 /// - **the `numerics` list is non-empty and keeps its recorded
17690 /// length** — `checked` counts *languages*, so trimming a row's
17691 /// numeric list back to `&["1"]` (the pre-#1379 fixture) or
17692 /// emptying it left every test passing when measured, with that
17693 /// language's coverage deleted. Two assertions, because the
17694 /// length check passes an emptied row whose count was zeroed with
17695 /// it.
17696 /// - **every template keeps its `{}`** — without the slot,
17697 /// `str::replace` is a no-op, baseline and candidate are computed
17698 /// from the same string, and the comparison degenerates to
17699 /// `x == x`.
17700 ///
17701 /// Sharing one driver means no test can lose any of the three.
17702 fn for_each_case(check: impl Fn(LANG, Case)) {
17703 let mut checked = 0;
17704 for lang in LANG::into_enum_iter() {
17705 if !lang.is_enabled() {
17706 continue;
17707 }
17708 let Some(case @ (slots, _, numerics, expected_kinds)) = cases(lang) else {
17709 continue;
17710 };
17711 // Emptying a row is the half the length check below cannot
17712 // see: `&[]` against an `expected_kinds` of 0 agrees with
17713 // itself, the inner loops run zero times, and `checked` —
17714 // which counts languages, not spellings — still reads
17715 // satisfied.
17716 assert!(
17717 !numerics.is_empty(),
17718 "{lang:?}: the numeric-operand list is empty; this language asserted nothing"
17719 );
17720 assert_eq!(
17721 numerics.len(),
17722 expected_kinds,
17723 "{lang:?}: the numeric-operand list no longer covers one spelling \
17724 per grammar numeric kind"
17725 );
17726 for (template, _, _) in slots {
17727 assert!(
17728 template.contains("{}"),
17729 "{lang:?}: template lost its `{{}}` operand slot: {template}"
17730 );
17731 }
17732 check(lang, case);
17733 checked += 1;
17734 }
17735 assert!(
17736 checked > 0,
17737 "no truthy-valued language enabled; this test asserted nothing"
17738 );
17739 }
17740
17741 #[test]
17742 fn a_numeric_operand_scores_like_an_identifier_operand() {
17743 for_each_case(|lang, (slots, identifier, numerics, _)| {
17744 for (template, _, _) in slots {
17745 let baseline = conditions(lang, &template.replace("{}", identifier));
17746 for numeric in numerics {
17747 let source = template.replace("{}", numeric);
17748 let scored = conditions(lang, &source);
17749 assert_eq!(
17750 scored, baseline,
17751 "{lang:?}: `{numeric}` scored {scored} unary conditions against \
17752 `{identifier}`'s {baseline}\n source: {source}"
17753 );
17754 }
17755 }
17756 });
17757 }
17758
17759 /// The absolute anchor under the comparison above: every operand
17760 /// spelling must produce the slot's recorded `conditions`, and must
17761 /// leave `cyclomatic` alone. The second half is what rules out a
17762 /// regression that moved both metrics together.
17763 #[test]
17764 fn every_operand_scores_its_recorded_values() {
17765 for_each_case(|lang, (slots, identifier, numerics, _)| {
17766 for (template, expected_conditions, expected_cyclomatic) in slots {
17767 for operand in std::iter::once(identifier).chain(numerics.iter().copied()) {
17768 let source = template.replace("{}", operand);
17769 assert_eq!(
17770 conditions(lang, &source),
17771 expected_conditions,
17772 "{lang:?}: `{operand}` conditions\n source: {source}"
17773 );
17774 assert_eq!(
17775 cyclomatic_sum(lang, &source),
17776 expected_cyclomatic,
17777 "{lang:?}: `{operand}` cyclomatic_sum\n source: {source}"
17778 );
17779 }
17780 }
17781 });
17782 }
17783}
17784
17785/// A relational construct with its own grammar production must score
17786/// like an identifier in the same boolean slot (#1449, #1461).
17787///
17788/// Every language here spells at least one boolean test as a dedicated
17789/// node rather than as a `binary_expression`, so no comparison-token
17790/// arm ever sees it. A kind missing from the former per-language
17791/// terminal-kind sets scored **zero**, silently, and zero is
17792/// indistinguishable from a legitimate zero — which is why the headline
17793/// claim of each case is a *comparison* against an identifier control
17794/// in the identical slot rather than an absolute number.
17795///
17796/// The constructs, and the route each fix took:
17797///
17798/// - **Groovy** `a in l` / `a !in l` (`membership_expression`), counted
17799/// as a node by `groovy_count_token_condition`. The other four —
17800/// `a === b` / `a !== b` (`identity_expression`) and `s =~ /p/` /
17801/// `s ==~ /p/` (`regex_find_expression`, `regex_match_expression`) —
17802/// are counted there as operator tokens, so all five score outside a
17803/// boolean slot as `==` does, and `groovy_condition_scores_itself`
17804/// lists each so the slot holding one does not pay again; a construct
17805/// takes exactly one of the two routes, never both
17806/// (`.claude/rules/grammar-dispatch.md` §5).
17807/// - **Perl** `/^#/` (`pattern_matcher`) and `m{^#}`
17808/// (`pattern_matcher_m`), the two spellings of a match against the
17809/// implicit `$_`. They are sibling rules, not aliases, so both are
17810/// covered — a fixture carrying only the first would have left the
17811/// `m{}` half at zero and read as covered. Since #1467 they take the
17812/// other route: a gated arm in `PerlCode::compute` counts them by use,
17813/// and they have left the former `perl_bool_terminal_kinds!()`.
17814/// - **Ruby** `a in Integer` (`test_pattern`), the one-line pattern
17815/// test. Decided against `match_pattern` (`expr => pat`), which
17816/// raises rather than yielding a boolean.
17817/// - **Objective-C** `@available(iOS 13.0, *)` and its
17818/// `__builtin_available` synonym (`available_expression`), the runtime
17819/// OS-version check. Not relational, but boolean by definition and
17820/// invisible to every comparison-token arm, so it measured short in
17821/// exactly the same way (#1457). Its entry landed in the *name*-keyed
17822/// former `cpp_bool_terminal_kinds!()` that C, C++ and Mozcpp shared, which is
17823/// why `the_c_family_grammars_do_not_emit_available_expression` below
17824/// pins the inertness the other three rely on.
17825///
17826/// Rust's `matches!(…)` measures short in the same way, and is
17827/// deliberately **not** here: its fix is `macro_invocation`, which also
17828/// catches `cfg!` / `dbg!` / `todo!`, and the one corpus snapshot it
17829/// moves is a `cfg!` rather than a `matches!`. That breadth is what
17830/// #1461 item 2 exists to decide, so it is left to that issue rather
17831/// than settled by inclusion here.
17832///
17833/// Two slots per language, because they are two independent walker
17834/// paths (§11) and every construct above measured short in **both**: the
17835/// operands of a `&&` chain, and the predicate of an `if`. A fixture of
17836/// only one leaves the other untested.
17837///
17838/// Each row also carries the construct's negated spelling, which reaches
17839/// the construct only through the language's value peel: the `!…` /
17840/// `(…)` wrapper is peeled before `count_boolean_slot` asks
17841/// `<lang>_condition_scores_itself` whether the operand already paid.
17842/// Without a negated row the peel is the path no fixture here exercises.
17843///
17844/// Since #1526 no language here has a terminal-kind set: the chain
17845/// operands and the slots both score through the shared
17846/// `count_boolean_slot` rule, and Perl's rows score through the by-use
17847/// arm (#1467).
17848///
17849/// The `cyclomatic` half of `every_construct_scores_its_recorded_values`
17850/// is what rules out a regression that moved both metrics together:
17851/// cyclomatic must not move when only the operand spelling does, which
17852/// is what makes a `conditions` move unambiguously ABC's. The recorded
17853/// figures differ per slot (Rust's chain slot carries the extra `&&`
17854/// decision), so each slot pins its own pair rather than sharing one.
17855// Gated on the union of the three languages with rows, so a build
17856// enabling none of them drops the module instead of failing its
17857// guards (`.claude/rules/testing.md`, #1286 / #1411).
17858#[cfg(test)]
17859#[cfg(any(
17860 feature = "groovy",
17861 feature = "perl",
17862 feature = "ruby",
17863 feature = "objc"
17864))]
17865mod own_production_bool_constructs {
17866 use crate::test_support::metrics_verbatim;
17867 use crate::{LANG, MetricsOptions};
17868
17869 /// One fixture shape: a source template with a `{}` slot for the
17870 /// whole boolean expression, and the `abc.conditions_sum` /
17871 /// `cyclomatic_sum` every spelling in that slot must produce.
17872 type Slot = (&'static str, u64, u64);
17873
17874 /// A language's two slots, its identifier control, the constructs
17875 /// that must score the same, and how many of those there should be.
17876 ///
17877 /// The count is not bookkeeping. `for_each_case` counts *languages*,
17878 /// so trimming Groovy's list back to a single construct — or
17879 /// dropping Perl's `m{}` half, which is a separate grammar rule from
17880 /// its `//` half — leaves the module green with that coverage
17881 /// deleted. Pinning the length makes such a trim a deliberate
17882 /// two-line edit instead of a silent one.
17883 type Case = ([Slot; 2], &'static str, &'static [&'static str], usize);
17884
17885 fn conditions(lang: LANG, source: &str) -> u64 {
17886 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17887 .abc
17888 .conditions_sum()
17889 }
17890
17891 fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
17892 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
17893 .cyclomatic
17894 .cyclomatic_sum()
17895 }
17896
17897 /// `([chain_slot, predicate_slot], identifier, constructs)` per
17898 /// language.
17899 ///
17900 /// `{}` is the left-hand operand of a two-operand short-circuit
17901 /// chain in the first slot and the whole `if` predicate in the
17902 /// second, so every fixture differs from its own control in exactly
17903 /// the construct under test.
17904 ///
17905 /// Ruby's chain slot parenthesises the slot because `in` binds
17906 /// looser than `&&` there; the control is parenthesised identically,
17907 /// so the parens cancel out of the comparison and the peel unwraps
17908 /// them for both sides alike.
17909 fn cases(lang: LANG) -> Option<Case> {
17910 Some(match lang {
17911 LANG::Groovy => (
17912 [
17913 ("def f(a, b, l, s) {\n return {} && b\n}\n", 2, 3),
17914 ("def f(a, b, l, s) {\n if ({}) { return 1 }\n}\n", 1, 3),
17915 ],
17916 "b",
17917 &[
17918 "a in l",
17919 "a !in l",
17920 "a === b",
17921 "a !== b",
17922 "s =~ /p/",
17923 "s ==~ /p/",
17924 "!(a in l)",
17925 // The indexing / navigation kinds added by #1466.
17926 // These two are the cyclomatic-neutral ones, so
17927 // they belong in this module's same-as-the-control
17928 // shape. The three *null-safe* spellings (`a?.b`,
17929 // `a??.b`, `l?[0]`) each short-circuit on a null
17930 // receiver and so add a cyclomatic decision, which
17931 // a row whose contract is "the control's
17932 // cyclomatic" cannot express; they get their own
17933 // test,
17934 // `groovy_safe_navigation_closes_the_two_below_gap`,
17935 // which anchors them on that axis explicitly.
17936 "l[0]",
17937 "a.@b",
17938 ],
17939 9,
17940 ),
17941 LANG::Perl => (
17942 [
17943 ("sub f {\n my $x = {} && $b;\n}\n", 2, 3),
17944 ("sub f {\n if ({}) { 1; }\n}\n", 1, 3),
17945 ],
17946 "$a",
17947 &["/^#/", "m{^#}", "!/^#/"],
17948 3,
17949 ),
17950 LANG::Ruby => (
17951 [
17952 ("def f(a)\n ({}) && b\nend\n", 2, 3),
17953 ("def f(a)\n if {}\n 1\n end\nend\n", 1, 3),
17954 ],
17955 "a",
17956 &["a in Integer", "!(a in Integer)"],
17957 2,
17958 ),
17959 // Objective-C's `@available` is not relational like the
17960 // constructs above, but it reaches the terminal set by the
17961 // route Groovy's `in` takes: a dedicated `available_expression`
17962 // production whose value is a boolean, seen by no
17963 // comparison-token arm (#1457). Both spellings are listed
17964 // because they are alternatives of one rule's leading token
17965 // rather than two rules — so unlike Perl's `m{}` above they
17966 // cannot drift apart, and the second row is here to keep that
17967 // claim measured rather than assumed.
17968 LANG::Objc => (
17969 [
17970 ("int f(int a) {\n return {} && b;\n}\n", 2, 3),
17971 ("int f() {\n if ({}) { return 1; }\n}\n", 1, 3),
17972 ],
17973 "a",
17974 &[
17975 "@available(iOS 13.0, *)",
17976 "__builtin_available(macOS 10.15, *)",
17977 "!@available(iOS 13.0, *)",
17978 ],
17979 3,
17980 ),
17981 _ => return None,
17982 })
17983 }
17984
17985 /// Runs `check` once per enabled language that has a case, having
17986 /// first established that the case can still assert something.
17987 ///
17988 /// The four guards mirror `numeric_bool_operands::for_each_case`,
17989 /// where each was added only after a measured perturbation of it
17990 /// left that module green: `checked > 0` for the runtime half of
17991 /// the feature gate, a non-empty construct list and its recorded
17992 /// count so a trimmed row cannot vanish silently, and the `{}` slot
17993 /// so `str::replace` does not degenerate into comparing a string
17994 /// with itself.
17995 fn for_each_case(check: impl Fn(LANG, Case)) {
17996 let mut checked = 0;
17997 for lang in LANG::into_enum_iter() {
17998 if !lang.is_enabled() {
17999 continue;
18000 }
18001 let Some(case @ (slots, _, constructs, expected_constructs)) = cases(lang) else {
18002 continue;
18003 };
18004 // Both halves are needed, and the second does not imply the
18005 // first. The length check alone only asserts that the list
18006 // and its recorded count *agree*, so emptying a row and
18007 // setting its count to 0 — the shape a careless "the test
18008 // failed, fix the number" edit takes — deleted that
18009 // language's coverage with both tests still green when
18010 // measured.
18011 assert!(
18012 !constructs.is_empty(),
18013 "{lang:?}: the construct list is empty; this language asserted nothing"
18014 );
18015 assert_eq!(
18016 constructs.len(),
18017 expected_constructs,
18018 "{lang:?}: the construct list no longer covers every own-production \
18019 boolean spelling this language was fixed for"
18020 );
18021 for (template, _, _) in slots {
18022 assert!(
18023 template.contains("{}"),
18024 "{lang:?}: template lost its `{{}}` slot: {template}"
18025 );
18026 }
18027 check(lang, case);
18028 checked += 1;
18029 }
18030 assert!(
18031 checked > 0,
18032 "no language with an own-production boolean construct enabled; \
18033 this test asserted nothing"
18034 );
18035 }
18036
18037 #[test]
18038 fn an_own_production_construct_scores_like_an_identifier() {
18039 for_each_case(|lang, (slots, identifier, constructs, _)| {
18040 for (template, _, _) in slots {
18041 let baseline = conditions(lang, &template.replace("{}", identifier));
18042 for construct in constructs {
18043 let source = template.replace("{}", construct);
18044 let scored = conditions(lang, &source);
18045 assert_eq!(
18046 scored, baseline,
18047 "{lang:?}: `{construct}` scored {scored} conditions against \
18048 `{identifier}`'s {baseline}\n source: {source}"
18049 );
18050 }
18051 }
18052 });
18053 }
18054
18055 /// Groovy's three null-safe spellings, on the cyclomatic axis.
18056 ///
18057 /// They cannot ride the rows above, whose contract is "scores the
18058 /// control's `conditions` *and* the control's `cyclomatic`":
18059 /// the boolean-slot rule (`groovy_count_condition`) does not move cyclomatic, but
18060 /// `?.` (`QMARKDOT`), `??.` (`QMARKQMARKDOT`) and `?[`
18061 /// (`QMARKLBRACK`) are already cyclomatic decisions in their own
18062 /// right (`src/metrics/cyclomatic/groovy.rs`), so each spelling
18063 /// scores the control's conditions against the control's cyclomatic
18064 /// **plus one**. That is the whole reason this family was the worst
18065 /// case in #1466: before the fix ABC sat *two* below its own
18066 /// decision count on `if (a?.b)`, against one below for every other
18067 /// spelling.
18068 ///
18069 /// `l?[0]` joined the family in #1471. It sat in the neutral rows
18070 /// above until then, passing only because Groovy cyclomatic had no
18071 /// `?[` arm — a fixture that would have rejected the fix for the
18072 /// very gap it recorded.
18073 ///
18074 /// Asserting the offset rather than the bare conditions is what
18075 /// makes this a §5 double-count guard as well. If a later change
18076 /// added `QMARKDOT` to `groovy_count_token_condition`'s arm while
18077 /// the wrapper stayed in the terminal set, `conditions` would go to
18078 /// `control + 1` and this test — not a snapshot — would say so.
18079 #[test]
18080 #[cfg(feature = "groovy")]
18081 fn groovy_safe_navigation_closes_the_two_below_gap() {
18082 for (template, control_conditions, control_cyclomatic) in [
18083 ("def f(a, b, l) {\n return {} && b\n}\n", 2, 3),
18084 ("def f(a, b, l) {\n if ({}) { return 1 }\n}\n", 1, 3),
18085 ] {
18086 let control = template.replace("{}", "b");
18087 assert_eq!(conditions(LANG::Groovy, &control), control_conditions);
18088 assert_eq!(cyclomatic_sum(LANG::Groovy, &control), control_cyclomatic);
18089
18090 for spelling in ["a?.b", "a??.b", "l?[0]"] {
18091 let source = template.replace("{}", spelling);
18092 assert_eq!(
18093 conditions(LANG::Groovy, &source),
18094 control_conditions,
18095 "`{spelling}` conditions\n source: {source}"
18096 );
18097 assert_eq!(
18098 cyclomatic_sum(LANG::Groovy, &source),
18099 control_cyclomatic + 1,
18100 "`{spelling}` cyclomatic_sum: the navigation operator's own \
18101 decision has moved\n source: {source}"
18102 );
18103 }
18104 }
18105 }
18106
18107 /// The `!` operand is read by field, so an `extra` cannot hide it.
18108 ///
18109 /// `groovy_wrapper_operand` took `child(1)` until #1466, which is
18110 /// the `!` operand only when nothing sits between the two. A comment
18111 /// is an `extra` and occupies that slot, so `if (! /*c*/ a)` scored
18112 /// zero conditions where `if (!a)` scored one — the positional-read
18113 /// class grammar-dispatch §3 is about, and the observable half of
18114 /// the peel rewrite.
18115 ///
18116 /// `parenthesized_expression` names nothing in node-types.json, so
18117 /// it kept the positional read, and the bug, until #1455 read it as
18118 /// the first operand that is not an extra.
18119 #[test]
18120 #[cfg(feature = "groovy")]
18121 fn groovy_negation_operand_survives_an_interposed_comment() {
18122 let template = "def f(a) {\n if ({}) { return 1 }\n}\n";
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 `!` operand is being read positionally again; a comment displaces it"
18129 );
18130
18131 assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "(a)")), 1);
18132 assert_eq!(
18133 conditions(LANG::Groovy, &template.replace("{}", "( /*c*/ a)")),
18134 1,
18135 "the parenthesised operand is being read positionally again; a comment \
18136 displaces it"
18137 );
18138 }
18139
18140 /// The arithmetic unary operators are values, not wrappers.
18141 ///
18142 /// `groovy_count_condition` routed every `unary_expression` to the
18143 /// peel while the peel handled only the `!` spelling, so the arm
18144 /// claimed `~a` / `-a` / `+a` and dropped them
18145 /// (grammar-dispatch §7). Since #1526 the slot pays for any
18146 /// predicate no other arm counts — Groovy truth makes `if (-a)` a
18147 /// decision — so the three spellings score what `a` does. Outside a
18148 /// slot they are no negation, so a call argument scores nothing for
18149 /// them, while `!-a` scores its negation once.
18150 #[test]
18151 #[cfg(feature = "groovy")]
18152 fn groovy_arithmetic_unary_is_a_value_not_a_wrapper() {
18153 let template = "def f(a) {\n if ({}) { return 1 }\n}\n";
18154 let call = "def f(a) {\n g({})\n}\n";
18155
18156 assert_eq!(conditions(LANG::Groovy, &template.replace("{}", "a")), 1);
18157 for spelling in ["~a", "-a", "+a"] {
18158 assert_eq!(
18159 conditions(LANG::Groovy, &template.replace("{}", spelling)),
18160 1,
18161 "`{spelling}` in a slot pays as `a` does"
18162 );
18163 assert_eq!(
18164 conditions(LANG::Groovy, &call.replace("{}", spelling)),
18165 0,
18166 "`{spelling}` outside a slot is not a negation"
18167 );
18168 let negated = format!("!{spelling}");
18169 assert_eq!(
18170 conditions(LANG::Groovy, &call.replace("{}", &negated)),
18171 1,
18172 "`{negated}` outside a slot is one negation"
18173 );
18174 }
18175 }
18176
18177 /// The two Perl spellings must remain two distinct grammar kinds.
18178 ///
18179 /// The bare-match arm of `PerlCode::compute` lists `PatternMatcher`
18180 /// **and** `PatternMatcherM` on the stated grounds that `/^#/` and `m{^#}`
18181 /// are sibling rules rather than aliases of one. Nothing above pins
18182 /// that: if a grammar bump collapsed `m{}` onto `pattern_matcher`,
18183 /// both fixtures would keep scoring through the surviving entry,
18184 /// the `PatternMatcherM` arm would become dead code, and every test
18185 /// in this module would stay green — the silent-drift shape lesson
18186 /// 34 and grammar-dispatch §2 exist to catch.
18187 ///
18188 /// Asserting the ids differ is not enough on its own, since two
18189 /// distinct enum variants prove nothing about what the parser
18190 /// emits. So each spelling is parsed and required to produce its
18191 /// own kind and not the other's.
18192 #[test]
18193 #[cfg(feature = "perl")]
18194 fn the_two_perl_match_spellings_are_distinct_kinds() {
18195 use crate::{ParserTrait, Perl, PerlParser};
18196 use big_code_analysis_ast::test_support::ast_has_kind_id;
18197
18198 assert_ne!(
18199 Perl::PatternMatcher as u16,
18200 Perl::PatternMatcherM as u16,
18201 "the two spellings share one kind id; one bare-match arm entry is dead"
18202 );
18203
18204 for (src, present, absent) in [
18205 (
18206 "sub f { if (/^#/) { 1; } }",
18207 ("pattern_matcher", Perl::PatternMatcher as u16),
18208 ("pattern_matcher_m", Perl::PatternMatcherM as u16),
18209 ),
18210 (
18211 "sub f { if (m{^#}) { 1; } }",
18212 ("pattern_matcher_m", Perl::PatternMatcherM as u16),
18213 ("pattern_matcher", Perl::PatternMatcher as u16),
18214 ),
18215 ] {
18216 let parser = PerlParser::new(
18217 src.as_bytes().to_vec(),
18218 &std::path::PathBuf::from("f.pl"),
18219 None,
18220 );
18221 let (present_name, present_id) = present;
18222 let (absent_name, absent_id) = absent;
18223 assert!(
18224 ast_has_kind_id(&parser, present_id),
18225 "`{src}` no longer emits `{present_name}`"
18226 );
18227 assert!(
18228 !ast_has_kind_id(&parser, absent_id),
18229 "`{src}` now also emits `{absent_name}`; the two spellings have \
18230 collapsed and one bare-match arm entry is dead"
18231 );
18232 }
18233 }
18234
18235 /// The absolute anchor under the comparison above: every spelling
18236 /// must produce the slot's recorded `conditions`, and must leave
18237 /// `cyclomatic` alone.
18238 #[test]
18239 fn every_construct_scores_its_recorded_values() {
18240 for_each_case(|lang, (slots, identifier, constructs, _)| {
18241 for (template, expected_conditions, expected_cyclomatic) in slots {
18242 for spelling in std::iter::once(identifier).chain(constructs.iter().copied()) {
18243 let source = template.replace("{}", spelling);
18244 assert_eq!(
18245 conditions(lang, &source),
18246 expected_conditions,
18247 "{lang:?}: `{spelling}` conditions\n source: {source}"
18248 );
18249 assert_eq!(
18250 cyclomatic_sum(lang, &source),
18251 expected_cyclomatic,
18252 "{lang:?}: `{spelling}` cyclomatic_sum\n source: {source}"
18253 );
18254 }
18255 }
18256 });
18257 }
18258}
18259
18260/// A Perl statement modifier must score like the block form it is
18261/// shorthand for (#1464).
18262///
18263/// `return 1 if $x;` and `if ($x) { return 1; }` are the same decision
18264/// written two ways, and the modifier is the idiomatic Perl spelling —
18265/// `next unless $ok;`, `warn "..." if $debug;`. Perl's *cyclomatic*
18266/// dispatcher already counted all six modifier kinds, so this was a
18267/// straight ABC undercount against Perl's own decision count rather
18268/// than a policy disagreement between the two metrics.
18269///
18270/// Each pair is asserted equal **and** equal to a literal, so a
18271/// regression that zeroed both spellings still fails. The fixtures are
18272/// spell-anchored because the modifier is the only thing in them that
18273/// scores a condition: deleting `if $x` from the modifier fixture would
18274/// otherwise turn it into a silent copy of nothing at all
18275/// (`.claude/rules/testing.md`, "Perturb the fixture as well as the
18276/// production line").
18277#[cfg(test)]
18278#[cfg(feature = "perl")]
18279mod perl_statement_modifier_parity {
18280 use crate::test_support::{assert_perl_fixture_spells, metrics_verbatim};
18281 use crate::{LANG, MetricsOptions, Perl};
18282
18283 fn conditions(source: &str) -> u64 {
18284 metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default())
18285 .abc
18286 .conditions_sum()
18287 }
18288
18289 /// `(keyword, kind id, kind name)` — the four modifier keywords
18290 /// whose grammar slot is a `condition`. `for` / `foreach` is the
18291 /// fifth spelling and is covered by its own test below; `when` is
18292 /// the sixth and has no fixture at all, being unreachable from
18293 /// valid Perl (see `perl_walk_statement_modifier`).
18294 const CONDITION_MODIFIERS: [(&str, u16, &str); 4] = [
18295 ("if", Perl::IfSimpleStatement as u16, "if_simple_statement"),
18296 (
18297 "unless",
18298 Perl::UnlessSimpleStatement as u16,
18299 "unless_simple_statement",
18300 ),
18301 (
18302 "while",
18303 Perl::WhileSimpleStatement as u16,
18304 "while_simple_statement",
18305 ),
18306 (
18307 "until",
18308 Perl::UntilSimpleStatement as u16,
18309 "until_simple_statement",
18310 ),
18311 ];
18312
18313 #[test]
18314 fn a_modifier_scores_like_its_block_form() {
18315 assert!(
18316 LANG::Perl.is_enabled(),
18317 "compiled under `feature = \"perl\"` but `LANG::Perl` reports disabled; \
18318 this test asserted nothing"
18319 );
18320 for (keyword, kind, name) in CONDITION_MODIFIERS {
18321 let modifier = format!("sub f {{ my $x = shift; return 1 {keyword} $x; }}");
18322 let block = format!("sub f {{ my $x = shift; {keyword} ($x) {{ return 1; }} }}");
18323 assert_perl_fixture_spells(&modifier, &[(kind, 1, name)]);
18324 assert_eq!(
18325 conditions(&modifier),
18326 conditions(&block),
18327 "`{keyword}` modifier and block form disagree\n modifier: {modifier}\n block: {block}"
18328 );
18329 assert_eq!(
18330 conditions(&modifier),
18331 1,
18332 "`{keyword}` modifier: the bare scalar predicate is one condition"
18333 );
18334 }
18335 }
18336
18337 /// The modifier's `condition` field goes through the shared
18338 /// condition classifier, not a flat `+1`, so a compound predicate
18339 /// keeps its sub-structure — three shapes the block form already
18340 /// scores above and below one.
18341 #[test]
18342 fn a_modifier_condition_keeps_its_substructure() {
18343 for (predicate, expected) in [("!$x", 1), ("($x)", 1), ("$x && $x", 2), ("$x > 2", 1)] {
18344 let modifier = format!("sub f {{ my $x = shift; return 1 if {predicate}; }}");
18345 let block = format!("sub f {{ my $x = shift; if ({predicate}) {{ return 1; }} }}");
18346 assert_perl_fixture_spells(
18347 &modifier,
18348 &[(Perl::IfSimpleStatement as u16, 1, "if_simple_statement")],
18349 );
18350 assert_eq!(
18351 conditions(&modifier),
18352 expected,
18353 "`if {predicate}` modifier\n source: {modifier}"
18354 );
18355 assert_eq!(
18356 conditions(&block),
18357 expected,
18358 "`if ({predicate})` block form\n source: {block}"
18359 );
18360 }
18361 }
18362
18363 /// The `for` / `foreach` modifier iterates a list and is not a
18364 /// boolean test — the grammar names its slot `list`, not
18365 /// `condition`. Pinned against the `if` modifier so the zero reads
18366 /// as a decision rather than as an arm nothing reaches.
18367 ///
18368 /// Measured: simply *adding* `ForSimpleStatement` to the dispatch
18369 /// arm does not fail this test, because the walker reads the slot by
18370 /// field name and `for_simple_statement` exposes no `condition` —
18371 /// the exclusion is structural, not a listed omission. What it does
18372 /// catch is a rework that reaches the slot some other way (a child
18373 /// index, the `list` field) or a grammar that renames the field.
18374 #[test]
18375 fn the_for_modifier_is_not_a_condition() {
18376 for keyword in ["for", "foreach"] {
18377 let source = format!("sub f {{ my $x = shift; print 5 {keyword} @$x; }}");
18378 assert_perl_fixture_spells(
18379 &source,
18380 &[(Perl::ForSimpleStatement as u16, 1, "for_simple_statement")],
18381 );
18382 let abc =
18383 metrics_verbatim(LANG::Perl, source.as_bytes(), MetricsOptions::default()).abc;
18384 assert_eq!(abc.conditions_sum(), 0, "`{keyword}` modifier: {source}");
18385 // The expected value *is* the default, so the zero above
18386 // proves nothing on its own — a fixture that stopped
18387 // parsing would satisfy it too. `my $x = shift` is the
18388 // file's one assignment, and `shift` and `print 5` its two
18389 // branches; all three must survive.
18390 assert_eq!(
18391 abc.assignments_sum(),
18392 1,
18393 "`{keyword}`: fixture stopped scoring"
18394 );
18395 assert_eq!(
18396 abc.branches_sum(),
18397 2,
18398 "`{keyword}`: fixture stopped scoring"
18399 );
18400 }
18401 assert_eq!(
18402 conditions("sub f { my $x = shift; return 1 if $x; }"),
18403 1,
18404 "control: the `if` modifier still counts, so the zero above is a \
18405 policy decision and not a dead arm"
18406 );
18407 }
18408}
18409
18410/// A non-numeric literal in a boolean operand slot must score like an
18411/// identifier in the same slot (#1462).
18412///
18413/// The sibling module above pins the *numeric* half, closed by #1410.
18414/// The non-numeric literals were never swept and were missing from
18415/// every set the numerics were added to — same mechanism, same silent
18416/// zero, same file. Measured before the fix, every row below scored
18417/// exactly one condition short of its identifier control, in **both**
18418/// slots: `x || "default"` — the language's commonest truthy-default
18419/// idiom — reported 1 against `x || y`'s 2.
18420///
18421/// Scope is the eleven truthy-valued sets. #1462 shipped eight of them
18422/// and called that complete; a whole-branch review of the batch found
18423/// Perl, Ruby and Elixir in neither the swept list nor the
18424/// compile-error exemption, each already carrying `False` / `Nil`, and
18425/// each reproducing the gap exactly. They were swept on the same terms
18426/// and are rows here. Tcl and iRules are truthy too and needed nothing:
18427/// their `quoted_word` / `braced_word_simple` / `number` kinds already
18428/// cover every literal an `expr {…}` operand can hold, so they have no
18429/// row rather than a vacuous one.
18430///
18431/// C#, Java, Kotlin, Rust and Go name no literal kind at all and stay
18432/// that way: a bare literal in a boolean slot is a compile error there,
18433/// so there is nothing to count. The C family is integer-truthy, and
18434/// #1462 deferred it for its corpus exposure. #1526 then closed its gap
18435/// without naming a kind, by making every slot pay for an occupant no
18436/// other arm counts; its rows here (#1469) pin that answer, because a
18437/// kind `cpp_condition_scores_itself` wrongly claimed would drop a slot
18438/// exactly as the missing terminal kind once did. Its `true`, `false`
18439/// and `null` rows are the exception to "measured short": the former
18440/// set already named them, and they stay so the pin covers every kind.
18441///
18442/// Three things each row pins that a conditions comparison alone
18443/// cannot:
18444///
18445/// - **The `kind_id` each spelling parses to.** This is the whole bug
18446/// class (`.claude/rules/grammar-dispatch.md` §1): the grammars here
18447/// emit `string` under two ids in JavaScript, Mozjs and Tsx and under
18448/// three in Tsx counting the type keyword, and listing the wrong one
18449/// compiles, runs, and returns the unfixed number. Asserting the id
18450/// turns that measurement into a standing claim rather than a note in
18451/// an issue. It is also the fixture anchor `.claude/rules/testing.md`
18452/// asks for — editing `"s"` to `1` in a row now fails by name instead
18453/// of quietly turning that row into a copy of the numeric module.
18454/// - **`cyclomatic` does not move.** Only the operand spelling changes
18455/// between a row and its control, so a `conditions` difference is
18456/// unambiguously ABC's.
18457/// - **Both walker paths.** The sets feed two structurally independent
18458/// consumers (§11) — the operands of a `&&` / `and` chain, and the
18459/// predicate of an `if` — and every row measured short in both. A
18460/// fixture of only one leaves the other path invisible.
18461///
18462/// The JS-family rows go through `js_literals!` rather than four hand-
18463/// written arrays, because the four languages differ in exactly one
18464/// place — which `string` alias an operand slot carries — and making
18465/// that the macro's only argument states the asymmetry instead of
18466/// leaving a reader to diff four near-identical lists for it.
18467#[cfg(test)]
18468#[cfg(any(
18469 feature = "javascript",
18470 feature = "mozjs",
18471 feature = "typescript",
18472 feature = "python",
18473 feature = "lua",
18474 feature = "php",
18475 feature = "groovy",
18476 feature = "perl",
18477 feature = "ruby",
18478 feature = "elixir",
18479 feature = "c",
18480 feature = "cpp",
18481 feature = "mozcpp",
18482 feature = "objc"
18483))]
18484mod literal_bool_operands {
18485 use crate::test_support::{assert_fixture_spells, metrics_verbatim};
18486 use crate::{LANG, MetricsOptions};
18487
18488 /// One fixture shape: a source template with a `{}` operand slot,
18489 /// and the `abc.conditions_sum` / `cyclomatic_sum` every spelling of
18490 /// that operand must produce.
18491 type Slot = (&'static str, u64, u64);
18492
18493 /// One literal operand: its spelling, and the `kind_id` the grammar
18494 /// must emit for it.
18495 type Literal = (&'static str, u16);
18496
18497 /// A language's two slots, its identifier baseline operand, the
18498 /// literal operands that must score the same, and how many of those
18499 /// there should be.
18500 ///
18501 /// The count is not bookkeeping. `for_each_case` counts *languages*,
18502 /// so trimming a row's literal list — the pre-#1462 state is the
18503 /// empty list — would leave the whole module green.
18504 type Case = ([Slot; 2], &'static str, &'static [Literal], usize);
18505
18506 /// The seven non-numeric literal kinds of a JS-family grammar, with
18507 /// the `string` alias an operand slot carries passed in: `String2`
18508 /// for JavaScript, Mozjs and Tsx, `String` for TypeScript. That one
18509 /// argument is the entire difference between the four rows, and it
18510 /// is the same split `Checker::is_string` makes (§7).
18511 macro_rules! js_literals {
18512 ($Lang:ident, $string:ident) => {
18513 &[
18514 ("\"s\"", crate::$Lang::$string as u16),
18515 ("`t`", crate::$Lang::TemplateString as u16),
18516 ("/re/", crate::$Lang::Regex as u16),
18517 ("null", crate::$Lang::Null as u16),
18518 ("undefined", crate::$Lang::Undefined as u16),
18519 ("{}", crate::$Lang::Object as u16),
18520 ("[]", crate::$Lang::Array as u16),
18521 ]
18522 };
18523 }
18524
18525 const JS_SLOTS: [Slot; 2] = [
18526 ("function f(a) {\n return a && {};\n}\n", 2, 3),
18527 ("function f() {\n if ({}) { return 1; }\n}\n", 1, 3),
18528 ];
18529
18530 /// The five non-numeric literal kinds every C-family grammar shares,
18531 /// then the grammar's own. The string and null spellings are passed
18532 /// in because Objective-C writes its strings `@"s"` and C++ its null
18533 /// `nullptr`; each still parses to the shared `string_literal` /
18534 /// `null` kind, so the row stays one spelling per kind.
18535 macro_rules! c_family_literals {
18536 ($Lang:ident, $string:literal, $null:literal $(, $extra:expr)* $(,)?) => {
18537 &[
18538 ($string, crate::$Lang::StringLiteral as u16),
18539 ("\"a\" \"b\"", crate::$Lang::ConcatenatedString as u16),
18540 ("true", crate::$Lang::True as u16),
18541 ("false", crate::$Lang::False as u16),
18542 ($null, crate::$Lang::Null as u16),
18543 $($extra,)*
18544 ]
18545 };
18546 }
18547
18548 const C_FAMILY_SLOTS: [Slot; 2] = [
18549 ("int f(int a) {\n return a && {};\n}\n", 2, 3),
18550 ("int f() {\n if ({}) { return 1; }\n}\n", 1, 3),
18551 ];
18552
18553 fn conditions(lang: LANG, source: &str) -> u64 {
18554 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
18555 .abc
18556 .conditions_sum()
18557 }
18558
18559 fn cyclomatic_sum(lang: LANG, source: &str) -> u64 {
18560 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
18561 .cyclomatic
18562 .cyclomatic_sum()
18563 }
18564
18565 /// Asserts the fixture still spells the construct under test, by
18566 /// parsing it with the language's own parser. Split out of `cases`
18567 /// because only the parser types are feature-gated; the `kind_id`
18568 /// enums are compiled unconditionally.
18569 fn assert_spells(lang: LANG, source: &str, kinds: &[(u16, usize, &str)]) {
18570 match lang {
18571 #[cfg(feature = "javascript")]
18572 LANG::Javascript => {
18573 assert_fixture_spells::<crate::JavascriptParser>(source, "f.js", kinds);
18574 }
18575 #[cfg(feature = "mozjs")]
18576 LANG::Mozjs => assert_fixture_spells::<crate::MozjsParser>(source, "f.jsm", kinds),
18577 #[cfg(feature = "typescript")]
18578 LANG::Typescript => {
18579 assert_fixture_spells::<crate::TypescriptParser>(source, "f.ts", kinds);
18580 }
18581 #[cfg(feature = "typescript")]
18582 LANG::Tsx => assert_fixture_spells::<crate::TsxParser>(source, "f.tsx", kinds),
18583 #[cfg(feature = "python")]
18584 LANG::Python => assert_fixture_spells::<crate::PythonParser>(source, "f.py", kinds),
18585 #[cfg(feature = "lua")]
18586 LANG::Lua => assert_fixture_spells::<crate::LuaParser>(source, "f.lua", kinds),
18587 #[cfg(feature = "php")]
18588 LANG::Php => assert_fixture_spells::<crate::PhpParser>(source, "f.php", kinds),
18589 #[cfg(feature = "groovy")]
18590 LANG::Groovy => {
18591 assert_fixture_spells::<crate::GroovyParser>(source, "f.groovy", kinds);
18592 }
18593 #[cfg(feature = "perl")]
18594 LANG::Perl => assert_fixture_spells::<crate::PerlParser>(source, "f.pl", kinds),
18595 #[cfg(feature = "ruby")]
18596 LANG::Ruby => assert_fixture_spells::<crate::RubyParser>(source, "f.rb", kinds),
18597 #[cfg(feature = "elixir")]
18598 LANG::Elixir => assert_fixture_spells::<crate::ElixirParser>(source, "f.ex", kinds),
18599 #[cfg(feature = "c")]
18600 LANG::C => assert_fixture_spells::<crate::CParser>(source, "f.c", kinds),
18601 #[cfg(feature = "cpp")]
18602 LANG::Cpp => assert_fixture_spells::<crate::CppParser>(source, "f.cpp", kinds),
18603 #[cfg(feature = "mozcpp")]
18604 LANG::Mozcpp => assert_fixture_spells::<crate::MozcppParser>(source, "f.cpp", kinds),
18605 #[cfg(feature = "objc")]
18606 LANG::Objc => assert_fixture_spells::<crate::ObjcParser>(source, "f.m", kinds),
18607 other => panic!("{other:?} has a case row but no parser arm"),
18608 }
18609 }
18610
18611 /// `([chain_slot, condition_slot], identifier, literals)` per
18612 /// language, one spelling per literal *kind* the grammar emits.
18613 ///
18614 /// - JS family: `string`, `template_string`, `regex`, `null`,
18615 /// `undefined`, `object`, `array`. TypeScript's `string` / `object`
18616 /// type keywords (`a: string`) are different ids rendering to the
18617 /// same names and are deliberately not in the sets, so a row that
18618 /// accidentally named one would fail the id anchor here.
18619 /// - Python: `string` covers `'s'`, `"""s"""`, `f'x'` and `b'x'`,
18620 /// while the implicit-join `'a' 'b'` is the separate
18621 /// `concatenated_string` rule — the sibling-rule-under-a-supertype
18622 /// shape #1379's Perl numerals were, which an alias sweep cannot
18623 /// see. Then `none`, the four collection displays, and `ellipsis`.
18624 /// - Lua: one `string` kind for `"s"`, `'s'` and `[[s]]`, plus
18625 /// `table_constructor`. Lua's is the strongest case of the eight:
18626 /// everything but `false` and `nil` is truthy, which is why
18627 /// `cond and "a" or "b"` is the language's ternary.
18628 /// - PHP: `string` (single-quoted) and `encapsed_string`
18629 /// (interpolating) are separate rules, as are `heredoc` and
18630 /// `nowdoc`; then `array_creation_expression`, `null`, the
18631 /// backtick `shell_command_expression`, and `cast_expression` —
18632 /// the second finding of #1462, PHP being the only set in the
18633 /// Java / C# / Groovy / PHP group that named no cast kind.
18634 /// - Groovy: `string_literal` (which also covers the slashy `/re/`),
18635 /// `null_literal`, `list_literal`, `map_literal`.
18636 /// - Perl: the four string productions the grammar keeps separate
18637 /// (`'s'`, `q()`, `"s"`, `qq()`), the two command substitutions
18638 /// (`qx()`, backticks), the `qw()` / `[…]` / `{…}` collection
18639 /// literals, `qr//`, and `special_literal` (`__FILE__`).
18640 /// `heredoc_initializer` is the twelfth kind and cannot live in
18641 /// this table — a Perl heredoc body follows the *statement*, not
18642 /// the operand — so it has its own test below.
18643 /// - Ruby: `string` (covering `"s"`, `'s'`, `%q()`, `%Q()`),
18644 /// `chained_string` (the adjacent-literal join `"a" "b"`, a
18645 /// sibling rule an alias sweep cannot see), `subshell`, the four
18646 /// collection literals `array` / `hash` / `%w[]` / `%i[]`,
18647 /// `regex`, the one-character `?a`, and both symbol productions.
18648 /// `heredoc_beginning` is the twelfth and shares the Perl heredoc
18649 /// test for the same reason.
18650 /// - Elixir: `string` (one kind for `"s"` and the `"""` heredoc),
18651 /// `charlist`, `sigil` (one kind for `~r//`, `~s()`, `~w()`),
18652 /// `quoted_atom` — a separate production from `atom`, so
18653 /// `:"q a"` scored zero while `:atom` scored one — and the four
18654 /// collection literals `list` / `tuple` / `map` / `bitstring`.
18655 /// - C family: `string_literal` (one kind for `"s"`, `L"s"`, `u8"s"`
18656 /// and Objective-C's `@"s"`), `concatenated_string`, `true`,
18657 /// `false` and `null` (`NULL`, and C++'s `nullptr`, which the
18658 /// grammar wraps in a `null` node). C++ adds `raw_string_literal`
18659 /// and `user_defined_literal` (`"s"_x`, and `1.0_km` around a
18660 /// number); Objective-C adds its boxed forms — `array_literal`,
18661 /// `dictionary_literal`, `at_expression` (`@1`, `@YES`, `@(b)`,
18662 /// `@protocol(P)`), `selector_expression` and `encode_expression`.
18663 /// `char_literal` is numeric and pinned in the sibling module.
18664 /// Objective-C's `nil`, `YES` and `NO` parse as plain identifiers,
18665 /// so they need no row of their own.
18666 ///
18667 /// Shapes that measured short and are not rows here, none of them a
18668 /// literal: JavaScript's `this` and Groovy's
18669 /// `object_creation_expression` (recorded in #1462); Perl's
18670 /// `s///` and `tr///` (operations on `$_` evaluating to a count),
18671 /// `anonymous_function` and `array_dereference`; Ruby's `lambda`;
18672 /// and Elixir's `anonymous_function` and `&f/1` capture. Every one
18673 /// pays its slot since #1526 (Elixir's since #1527), and
18674 /// `slot_pays_tests` pins them against an identifier control (#1475).
18675 fn cases(lang: LANG) -> Option<Case> {
18676 Some(match lang {
18677 LANG::Javascript => (JS_SLOTS, "b", js_literals!(Javascript, String2), 7),
18678 LANG::Mozjs => (JS_SLOTS, "b", js_literals!(Mozjs, String2), 7),
18679 LANG::Typescript => (JS_SLOTS, "b", js_literals!(Typescript, String), 7),
18680 LANG::Tsx => (JS_SLOTS, "b", js_literals!(Tsx, String2), 7),
18681 LANG::Python => (
18682 [
18683 ("def f(a):\n return a and {}\n", 2, 3),
18684 ("def f():\n if {}:\n return 1\n", 1, 3),
18685 ],
18686 "b",
18687 &[
18688 ("'s'", crate::Python::String as u16),
18689 ("'a' 'b'", crate::Python::ConcatenatedString as u16),
18690 ("None", crate::Python::None as u16),
18691 ("[]", crate::Python::List as u16),
18692 ("{1}", crate::Python::Set as u16),
18693 ("()", crate::Python::Tuple as u16),
18694 ("{}", crate::Python::Dictionary as u16),
18695 ("...", crate::Python::Ellipsis as u16),
18696 ],
18697 8,
18698 ),
18699 LANG::Lua => (
18700 [
18701 ("function f(a)\n return a and {}\nend\n", 2, 3),
18702 ("function f()\n if {} then return 1 end\nend\n", 1, 3),
18703 ],
18704 "b",
18705 &[
18706 ("\"s\"", crate::Lua::String as u16),
18707 ("{}", crate::Lua::TableConstructor as u16),
18708 ],
18709 2,
18710 ),
18711 LANG::Php => (
18712 [
18713 ("<?php\nfunction f($a) {\n return $a && {};\n}\n", 2, 3),
18714 ("<?php\nfunction f() {\n if ({}) { return 1; }\n}\n", 1, 3),
18715 ],
18716 "$b",
18717 &[
18718 ("'s'", crate::Php::String as u16),
18719 ("\"x$b\"", crate::Php::EncapsedString as u16),
18720 ("<<<EOT\ns\nEOT", crate::Php::Heredoc as u16),
18721 ("<<<'EOT'\ns\nEOT", crate::Php::Nowdoc as u16),
18722 ("[]", crate::Php::ArrayCreationExpression as u16),
18723 ("null", crate::Php::Null as u16),
18724 ("`ls`", crate::Php::ShellCommandExpression as u16),
18725 ("(bool)$b", crate::Php::CastExpression as u16),
18726 ],
18727 8,
18728 ),
18729 LANG::Groovy => (
18730 [
18731 ("def f(a) {\n return a && {}\n}\n", 2, 3),
18732 ("def f() {\n if ({}) { return 1 }\n}\n", 1, 3),
18733 ],
18734 "b",
18735 &[
18736 ("\"s\"", crate::Groovy::StringLiteral as u16),
18737 ("null", crate::Groovy::NullLiteral as u16),
18738 ("[]", crate::Groovy::ListLiteral as u16),
18739 ("[:]", crate::Groovy::MapLiteral as u16),
18740 ],
18741 4,
18742 ),
18743 LANG::Perl => (
18744 [
18745 (
18746 "sub f {\n my ($a, $b) = @_;\n if ($a && {}) { print 1; }\n}\n",
18747 2,
18748 4,
18749 ),
18750 (
18751 "sub f {\n my ($a, $b) = @_;\n if ({}) { print 1; }\n}\n",
18752 1,
18753 3,
18754 ),
18755 ],
18756 "$b",
18757 &[
18758 ("'s'", crate::Perl::StringSingleQuoted as u16),
18759 ("q(s)", crate::Perl::StringQQuoted as u16),
18760 ("\"s\"", crate::Perl::StringDoubleQuoted as u16),
18761 ("qq(s)", crate::Perl::StringQqQuoted as u16),
18762 ("qx(ls)", crate::Perl::CommandQxQuoted as u16),
18763 ("`ls`", crate::Perl::BacktickQuoted as u16),
18764 ("qw(a b)", crate::Perl::WordListQw as u16),
18765 ("[1, 2]", crate::Perl::ArrayRef as u16),
18766 ("{ a => 1 }", crate::Perl::HashRef as u16),
18767 ("qr/re/", crate::Perl::RegexPatternQr as u16),
18768 ("__FILE__", crate::Perl::SpecialLiteral as u16),
18769 ],
18770 11,
18771 ),
18772 LANG::Ruby => (
18773 [
18774 ("def f(a, b)\n if a && {} then 1 else 0 end\nend\n", 3, 4),
18775 ("def f(a, b)\n if {} then 1 else 0 end\nend\n", 2, 3),
18776 ],
18777 "b",
18778 &[
18779 ("\"s\"", crate::Ruby::String as u16),
18780 ("\"a\" \"b\"", crate::Ruby::ChainedString as u16),
18781 ("`ls`", crate::Ruby::Subshell as u16),
18782 ("[1, 2]", crate::Ruby::Array as u16),
18783 ("{ a: 1 }", crate::Ruby::Hash as u16),
18784 ("%w[a b]", crate::Ruby::StringArray as u16),
18785 ("%i[a b]", crate::Ruby::SymbolArray as u16),
18786 ("/re/", crate::Ruby::Regex as u16),
18787 ("?a", crate::Ruby::Character as u16),
18788 (":sym", crate::Ruby::SimpleSymbol as u16),
18789 (":\"sym\"", crate::Ruby::DelimitedSymbol as u16),
18790 ],
18791 11,
18792 ),
18793 LANG::Elixir => (
18794 [
18795 (
18796 "defmodule M do\n def f(a, b) do\n if a && {} do\n a\n end\n end\nend\n",
18797 2,
18798 5,
18799 ),
18800 (
18801 "defmodule M do\n def f(a, b) do\n if {} do\n a\n end\n end\nend\n",
18802 1,
18803 4,
18804 ),
18805 ],
18806 "b",
18807 &[
18808 ("\"s\"", crate::Elixir::String as u16),
18809 ("'c'", crate::Elixir::Charlist as u16),
18810 ("~r/re/", crate::Elixir::Sigil as u16),
18811 (":\"q a\"", crate::Elixir::QuotedAtom as u16),
18812 ("[1, 2]", crate::Elixir::List as u16),
18813 ("{1, 2}", crate::Elixir::Tuple as u16),
18814 ("%{a: 1}", crate::Elixir::Map as u16),
18815 ("<<1>>", crate::Elixir::Bitstring as u16),
18816 ],
18817 8,
18818 ),
18819 LANG::C => (
18820 C_FAMILY_SLOTS,
18821 "b",
18822 c_family_literals!(C, "\"s\"", "NULL"),
18823 5,
18824 ),
18825 LANG::Cpp => (
18826 C_FAMILY_SLOTS,
18827 "b",
18828 c_family_literals!(
18829 Cpp,
18830 "\"s\"",
18831 "nullptr",
18832 ("R\"(x)\"", crate::Cpp::RawStringLiteral as u16),
18833 ("\"s\"_x", crate::Cpp::UserDefinedLiteral as u16),
18834 ),
18835 7,
18836 ),
18837 LANG::Mozcpp => (
18838 C_FAMILY_SLOTS,
18839 "b",
18840 c_family_literals!(
18841 Mozcpp,
18842 "\"s\"",
18843 "nullptr",
18844 ("R\"(x)\"", crate::Mozcpp::RawStringLiteral as u16),
18845 ("\"s\"_x", crate::Mozcpp::UserDefinedLiteral as u16),
18846 ),
18847 7,
18848 ),
18849 LANG::Objc => (
18850 C_FAMILY_SLOTS,
18851 "b",
18852 c_family_literals!(
18853 Objc,
18854 "@\"s\"",
18855 "NULL",
18856 ("@[]", crate::Objc::ArrayLiteral as u16),
18857 ("@{}", crate::Objc::DictionaryLiteral as u16),
18858 ("@1", crate::Objc::AtExpression as u16),
18859 ("@selector(x)", crate::Objc::SelectorExpression as u16),
18860 ("@encode(int)", crate::Objc::EncodeExpression as u16),
18861 ),
18862 10,
18863 ),
18864 _ => return None,
18865 })
18866 }
18867
18868 /// Runs `check` once per enabled language that has a case, having
18869 /// first established that the case can still assert something. The
18870 /// four guards are the sibling module's, for the same four ways
18871 /// this table could decay into asserting nothing: no language
18872 /// enabled, an emptied literal list, a list that no longer covers
18873 /// every kind, and a template that lost its `{}` slot (which makes
18874 /// every comparison `x == x`).
18875 fn for_each_case(check: impl Fn(LANG, Case)) {
18876 let mut checked = 0;
18877 for lang in LANG::into_enum_iter() {
18878 if !lang.is_enabled() {
18879 continue;
18880 }
18881 let Some(case @ (slots, _, literals, expected_kinds)) = cases(lang) else {
18882 continue;
18883 };
18884 // The length check does not imply this one: it asserts only
18885 // that the list and its recorded count *agree*, which an
18886 // emptied row with `expected_kinds` set to 0 satisfies —
18887 // the shape a "the test failed, fix the number" edit takes.
18888 // `checked` counts languages, so that row still increments
18889 // it and the non-vacuity guard below reads satisfied.
18890 assert!(
18891 !literals.is_empty(),
18892 "{lang:?}: the literal-operand list is empty; this language asserted nothing"
18893 );
18894 assert_eq!(
18895 literals.len(),
18896 expected_kinds,
18897 "{lang:?}: the literal-operand list no longer covers one spelling \
18898 per grammar literal kind"
18899 );
18900 for (template, _, _) in slots {
18901 assert!(
18902 template.contains("{}"),
18903 "{lang:?}: template lost its `{{}}` operand slot: {template}"
18904 );
18905 }
18906 check(lang, case);
18907 checked += 1;
18908 }
18909 assert!(
18910 checked > 0,
18911 "no truthy-valued language enabled; this test asserted nothing"
18912 );
18913 }
18914
18915 #[test]
18916 fn a_literal_operand_scores_like_an_identifier_operand() {
18917 for_each_case(|lang, (slots, identifier, literals, _)| {
18918 for (template, _, _) in slots {
18919 let baseline = conditions(lang, &template.replace("{}", identifier));
18920 for (literal, _) in literals {
18921 let source = template.replace("{}", literal);
18922 let scored = conditions(lang, &source);
18923 assert_eq!(
18924 scored, baseline,
18925 "{lang:?}: `{literal}` scored {scored} unary conditions against \
18926 `{identifier}`'s {baseline}\n source: {source}"
18927 );
18928 }
18929 }
18930 });
18931 }
18932
18933 /// The absolute anchor under the comparison above: every operand
18934 /// spelling must produce the slot's recorded `conditions`, and must
18935 /// leave `cyclomatic` alone.
18936 #[test]
18937 fn every_literal_operand_scores_its_recorded_values() {
18938 for_each_case(|lang, (slots, identifier, literals, _)| {
18939 for (template, expected_conditions, expected_cyclomatic) in slots {
18940 let operands = std::iter::once(identifier)
18941 .chain(literals.iter().map(|(spelling, _)| *spelling));
18942 for operand in operands {
18943 let source = template.replace("{}", operand);
18944 assert_eq!(
18945 conditions(lang, &source),
18946 expected_conditions,
18947 "{lang:?}: `{operand}` conditions\n source: {source}"
18948 );
18949 assert_eq!(
18950 cyclomatic_sum(lang, &source),
18951 expected_cyclomatic,
18952 "{lang:?}: `{operand}` cyclomatic_sum\n source: {source}"
18953 );
18954 }
18955 }
18956 });
18957 }
18958
18959 /// The twelfth Perl and Ruby literal kind, which the table above
18960 /// cannot hold: in both languages a heredoc body follows the
18961 /// *statement*, so the operand slot carries only the introducer and
18962 /// the rest of the literal lands on later lines. Substituting one
18963 /// into a single-line template produces an unterminated heredoc and
18964 /// an `ERROR` parse, not a measurement.
18965 ///
18966 /// That shape is also why exactly one kind is listed per language
18967 /// (§5): `heredoc_initializer` / `heredoc_beginning` is the node in
18968 /// the operand slot, while `heredoc_body_statement` /
18969 /// `heredoc_body` is a sibling the walker never reaches. Listing
18970 /// both would score one literal twice.
18971 #[cfg(any(feature = "perl", feature = "ruby"))]
18972 #[test]
18973 fn a_heredoc_operand_scores_like_an_identifier_operand() {
18974 #[cfg(feature = "perl")]
18975 {
18976 let heredoc = "sub f {\n my ($a, $b) = @_;\n if ($a && <<\"EOT\") { print 1; }\nhello\nEOT\n}\n";
18977 assert_spells(
18978 LANG::Perl,
18979 heredoc,
18980 &[(crate::Perl::HeredocInitializer as u16, 1, "<<\"EOT\"")],
18981 );
18982 assert_eq!(
18983 conditions(LANG::Perl, heredoc),
18984 conditions(
18985 LANG::Perl,
18986 "sub f {\n my ($a, $b) = @_;\n if ($a && $b) { print 1; }\n}\n"
18987 ),
18988 "Perl heredoc operand"
18989 );
18990 }
18991 #[cfg(feature = "ruby")]
18992 {
18993 let heredoc =
18994 "def f(a, b)\n if a && <<~TXT then 1 else 0 end\n hello\n TXT\nend\n";
18995 assert_spells(
18996 LANG::Ruby,
18997 heredoc,
18998 &[(crate::Ruby::HeredocBeginning as u16, 1, "<<~TXT")],
18999 );
19000 assert_eq!(
19001 conditions(LANG::Ruby, heredoc),
19002 conditions(
19003 LANG::Ruby,
19004 "def f(a, b)\n if a && b then 1 else 0 end\nend\n"
19005 ),
19006 "Ruby heredoc operand"
19007 );
19008 }
19009 }
19010
19011 /// Every spelling must parse to the `kind_id` its row names — the
19012 /// §1 alias claim, which is the one thing a conditions comparison
19013 /// cannot check: a row naming the wrong alias of a multi-id kind
19014 /// would simply keep reporting the unfixed number.
19015 #[test]
19016 fn every_literal_spelling_parses_to_the_kind_its_row_names() {
19017 for_each_case(|lang, (slots, _, literals, _)| {
19018 for (template, _, _) in slots {
19019 for (literal, kind) in literals {
19020 let source = template.replace("{}", literal);
19021 assert_spells(lang, &source, &[(*kind, 1, literal)]);
19022 }
19023 }
19024 });
19025 }
19026}
19027
19028/// The condition slot and the wrapper peel agree on which kinds are
19029/// wrappers, in each language whose slot used to restate the list
19030/// (#1470).
19031///
19032/// `<lang>_count_condition` decided which slot expressions to hand the
19033/// peel, and the former `<lang>_inspect_container` which it could descend.
19034/// Spelled as two lists they drifted three times — Kotlin (#1459),
19035/// Groovy (#1466) and C# (#1463) — each time scoring a valid predicate
19036/// zero. The six languages here now ask `<lang>_wrapper_operand`
19037/// instead, which removed the duplication without moving a number: the
19038/// wrapper matrix measured identical before and after, so a revert
19039/// cannot fail these tests. What they pin is the *answer* both halves
19040/// must keep giving:
19041///
19042/// - **Routed.** Every wrapper the peel accepts scores its slot once.
19043/// A slot that stops asking the peel scores these zero.
19044/// - **Declined.** A unary the peel does not accept (`-b`, or a valid
19045/// non-wrapper such as `b & b`) stops the peel: a slot pays for it
19046/// once (#1526; Go's terminal-kind slot still scores `-b` nothing),
19047/// and outside a slot it scores nothing, so a peel that starts
19048/// accepting arithmetic operators by accident is caught here rather
19049/// than in a corpus snapshot.
19050/// - **Proves boolean.** A negation counts its operand outside any
19051/// boolean slot and a parenthesis does not — the second half of the
19052/// peel's answer, which the slot never sees.
19053///
19054/// Each template's `@` is the operand under test.
19055#[cfg(test)]
19056#[cfg(any(
19057 feature = "rust",
19058 feature = "go",
19059 feature = "java",
19060 feature = "python",
19061 feature = "ruby",
19062 feature = "lua"
19063))]
19064mod wrapper_peel_routing {
19065 use crate::test_support::metrics_verbatim;
19066 use crate::{LANG, MetricsOptions};
19067
19068 fn conditions(lang: LANG, source: &str) -> u64 {
19069 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
19070 .abc
19071 .conditions_sum()
19072 }
19073
19074 /// Asserts `conditions` for every `(operand, expected)` row of one
19075 /// template.
19076 fn assert_operands(lang: LANG, template: &str, rows: &[(&str, u64)]) {
19077 assert!(template.contains('@'), "template has no operand slot");
19078 for &(operand, expected) in rows {
19079 let source = template.replace('@', operand);
19080 assert_eq!(
19081 conditions(lang, &source),
19082 expected,
19083 "`{operand}` in {lang:?}\n source: {source}"
19084 );
19085 }
19086 }
19087
19088 /// Rust's slot pays for any predicate no other arm counted (#1526),
19089 /// so the declined half reads the valid non-wrapper `b & b`: the
19090 /// peel stops on it, the slot pays for it, and the call argument
19091 /// below does not.
19092 #[test]
19093 #[cfg(feature = "rust")]
19094 fn rust_condition_slot_routes_what_the_peel_accepts() {
19095 assert_operands(
19096 LANG::Rust,
19097 "fn f(b: bool) { if @ {} }\n",
19098 &[
19099 ("b", 1),
19100 ("(b)", 1),
19101 ("!b", 1),
19102 ("!(b)", 1),
19103 ("b & b", 1),
19104 ("(b & b)", 1),
19105 ("!(b & b)", 1),
19106 ],
19107 );
19108 assert_operands(
19109 LANG::Rust,
19110 "fn f(b: bool) { g(@); }\n",
19111 &[("!b", 1), ("(b)", 0), ("b & b", 0), ("!(b & b)", 1)],
19112 );
19113 }
19114
19115 #[test]
19116 #[cfg(feature = "go")]
19117 fn go_condition_slot_routes_what_the_peel_accepts() {
19118 assert_operands(
19119 LANG::Go,
19120 "package p\nfunc f(b bool) { if @ {} }\n",
19121 &[
19122 ("b", 1),
19123 ("(b)", 1),
19124 ("!b", 1),
19125 ("!(b)", 1),
19126 ("-b", 0),
19127 ("(-b)", 0),
19128 ],
19129 );
19130 assert_operands(
19131 LANG::Go,
19132 "package p\nfunc f(b bool) { g(@) }\n",
19133 &[("!b", 1), ("(b)", 0), ("-b", 0)],
19134 );
19135 }
19136
19137 /// The ternary condition is a slot like the `if` one; the `?` itself
19138 /// is the base 1. Java's slot pays for any predicate no other arm
19139 /// counted (#1526), so the declined half reads the valid non-wrapper
19140 /// `b & b`: the peel stops on it, the slot pays for it, and the call
19141 /// argument below does not.
19142 #[test]
19143 #[cfg(feature = "java")]
19144 fn java_condition_slot_routes_what_the_peel_accepts() {
19145 assert_operands(
19146 LANG::Java,
19147 "class A { int f(boolean b) { return @ ? 1 : 2; } }\n",
19148 &[
19149 ("b", 2),
19150 ("(b)", 2),
19151 ("!b", 2),
19152 ("!(b)", 2),
19153 ("b & b", 2),
19154 ("(b & b)", 2),
19155 ],
19156 );
19157 assert_operands(
19158 LANG::Java,
19159 "class A { void f(boolean b) { g(@); } }\n",
19160 &[("!b", 1), ("(b)", 0), ("b & b", 0), ("!(b & b)", 1)],
19161 );
19162 }
19163
19164 /// Python peels parentheses and the walrus only: `not` is counted by
19165 /// its own `NotOperator` arm, so `python_wrapper_operand` carries no
19166 /// proves-boolean flag and there is no outside-a-slot half to pin.
19167 /// Python is truthy-valued, so the slot pays for any predicate no
19168 /// other arm counted, `-b` included (#1526), as Ruby's does.
19169 #[test]
19170 #[cfg(feature = "python")]
19171 fn python_condition_slot_routes_what_the_peel_accepts() {
19172 assert_operands(
19173 LANG::Python,
19174 "def f(b):\n if @:\n pass\n",
19175 &[("b", 1), ("(b)", 1), ("((b))", 1), ("-b", 1), ("(-b)", 1)],
19176 );
19177 }
19178
19179 /// Ruby has no counted call-argument slot, so the outside-a-slot
19180 /// half reads a ternary *branch*, which is type-free: only a
19181 /// negation there proves its operand boolean. That branch is also
19182 /// where the declined half lives for Ruby: its condition slot pays
19183 /// for any predicate no other arm counted, `-b` included (#1520), so
19184 /// `if -b` scores 1 rather than 0.
19185 #[test]
19186 #[cfg(feature = "ruby")]
19187 fn ruby_condition_slot_routes_what_the_peel_accepts() {
19188 assert_operands(
19189 LANG::Ruby,
19190 "def f(b)\n if @ then 1 end\nend\n",
19191 &[
19192 ("b", 1),
19193 ("(b)", 1),
19194 ("!b", 1),
19195 ("not b", 1),
19196 ("!(b)", 1),
19197 ("-b", 1),
19198 ("(-b)", 1),
19199 ],
19200 );
19201 assert_operands(
19202 LANG::Ruby,
19203 "def f(b, c)\n x = c ? 1 : @\nend\n",
19204 &[
19205 ("!b", 3),
19206 ("not b", 3),
19207 ("(b)", 2),
19208 ("-b", 2),
19209 // A negated branch is a slot whatever it negates (#1529).
19210 ("!self", 3),
19211 ("not -b", 3),
19212 ("!(b > 1)", 3),
19213 ],
19214 );
19215 // A statement sequence evaluates to its last statement, so that is
19216 // the operand the slot reads: `(b; b > 1)` is the comparison,
19217 // already counted by its own `>`, and `(b > 1; b)` is the bare `b`,
19218 // which the slot counts beside that `>`. Reading the first
19219 // statement inverted both. The subject-less `when` goes through
19220 // the same peel.
19221 let seqs: &[(&str, u64)] = &[("(b; b > 1)", 1), ("(b > 1; b)", 2)];
19222 assert_operands(LANG::Ruby, "def f(b)\n if @ then 1 end\nend\n", seqs);
19223 assert_operands(
19224 LANG::Ruby,
19225 "def f(b)\n case\n when @ then 1\n end\nend\n",
19226 seqs,
19227 );
19228 }
19229
19230 /// A subject-less `when` whose pattern is a chain adds nothing of its
19231 /// own: the Rule 9 walker scores every operand as a slot, so each
19232 /// chain already pays at least one (#1529). Before that the walker
19233 /// skipped an operand outside its terminal list, the clause had to
19234 /// pay for `-a && -b` (review of #1453), and a chain scored as many
19235 /// conditions as it had *listed* operands plus one at most. Each row
19236 /// now scores what its plain-identifier twin (`a && b`, `a || b ||
19237 /// c`) does. An assignment operand is its value (#1520), so
19238 /// `(a = 1) && (b = 2)` is `1 && 2`.
19239 #[test]
19240 #[cfg(feature = "ruby")]
19241 fn ruby_subjectless_when_chain_pays_per_operand() {
19242 assert_operands(
19243 LANG::Ruby,
19244 "def f(a, b, c)\n case\n when @ then 1\n end\nend\n",
19245 &[
19246 ("a", 1),
19247 ("-a", 1),
19248 ("a && b", 2),
19249 ("-a && -b", 2),
19250 ("(a = 1) && (b = 2)", 2),
19251 ("a || b || c", 3),
19252 ("-a || -b || -c", 3),
19253 ("-a && b", 2),
19254 ("a > 1 && b", 2),
19255 ("a > 1 && -b", 2),
19256 ("a && (b || c)", 3),
19257 ("-a && (-b || c)", 3),
19258 ],
19259 );
19260 }
19261
19262 /// #1529: an operand of a Ruby `&&` / `||` / `and` / `or` chain is
19263 /// scored by the rule #1520 gave every condition slot — one, unless
19264 /// another arm already counts it — wherever the chain is written.
19265 /// The walker used to pay only for an operand that peeled down to a
19266 /// fixed list of terminal kinds, so every spelling in `UNLISTED`
19267 /// scored one below its plain-identifier twin `b` in every position
19268 /// below. `CONTROLS` must not move: `!b` and `(b)` already scored
19269 /// their twin, and a comparison, a pattern test, or a negation of one
19270 /// is counted by its own arm, so paying the operand as well would
19271 /// count that decision twice.
19272 #[test]
19273 #[cfg(feature = "ruby")]
19274 fn ruby_chain_operand_pays_what_no_other_arm_counted() {
19275 const UNLISTED: &[&str] = &[
19276 "self",
19277 "Foo::Bar",
19278 "-b",
19279 "(-b)",
19280 "defined?(b)",
19281 "b + 1",
19282 "!self",
19283 "not self",
19284 "(y ||= b)",
19285 "(y = self)",
19286 "begin self end",
19287 ];
19288 const CONTROLS: &[&str] = &["!b", "(b)", "c > 1", "!(c > 1)", "(c in Integer)"];
19289 // (template, conditions with `b` in the `@` operand)
19290 let positions: &[(&str, u64)] = &[
19291 ("def f(a, b, c)\n g if a && @\nend\n", 2),
19292 ("def f(a, b, c)\n g if (a && @)\nend\n", 2),
19293 ("def f(a, b, c)\n x = a && @\nend\n", 2),
19294 ("def f(a, b, c)\n return a || @\nend\n", 2),
19295 ("def f(a, b, c)\n h(@ && a)\nend\n", 2),
19296 ("def f(a, b, c)\n x = (a and @)\nend\n", 2),
19297 ("def f(a, b, c)\n x = (a or @)\nend\n", 2),
19298 ("def f(a, b, c)\n y ||= a && @\nend\n", 2),
19299 ("def f(a, b, c)\n y &&= a || @\nend\n", 2),
19300 ("def f(a, b, c)\n x = a && (c || @)\nend\n", 3),
19301 // A comment is a named child of the `binary` too, and must not
19302 // pay as a third operand.
19303 ("def f(a, b, c)\n x = a && # c\n @\nend\n", 2),
19304 ("def f(a, b, c)\n while a && @ do g end\nend\n", 2),
19305 (
19306 "def f(a, b, c)\n if a then 1 elsif a && @ then 2 end\nend\n",
19307 4,
19308 ),
19309 (
19310 "def f(a, b, c)\n case\n when a && @ then 1\n end\nend\n",
19311 2,
19312 ),
19313 ];
19314 for &(template, twin) in positions {
19315 // The twin is measured, not only assumed, so a template that
19316 // stopped parsing as a chain fails here rather than agreeing
19317 // with every row at some other value.
19318 assert_operands(LANG::Ruby, template, &[("b", twin)]);
19319 let rows: Vec<_> = UNLISTED
19320 .iter()
19321 .chain(CONTROLS)
19322 .map(|&operand| (operand, twin))
19323 .collect();
19324 assert_operands(LANG::Ruby, template, &rows);
19325 }
19326 }
19327
19328 #[test]
19329 #[cfg(feature = "lua")]
19330 fn lua_condition_slot_routes_what_the_peel_accepts() {
19331 // Lua is truthy-valued, so the slot pays for any predicate no
19332 // other arm counted, `-b` and `#b` included (#1526).
19333 assert_operands(
19334 LANG::Lua,
19335 "function f(b)\n if @ then end\nend\n",
19336 &[
19337 ("b", 1),
19338 ("(b)", 1),
19339 ("not b", 1),
19340 ("not (b)", 1),
19341 ("-b", 1),
19342 ("#b", 1),
19343 ("(-b)", 1),
19344 ],
19345 );
19346 assert_operands(
19347 LANG::Lua,
19348 "function f(b)\n g(@)\nend\n",
19349 &[("not b", 1), ("(b)", 0), ("-b", 0)],
19350 );
19351 }
19352}
19353
19354/// A comment inside a condition or value slot changes nothing (#1455).
19355///
19356/// Every walker read its slots by fixed child index, and every peel
19357/// stepped into a `(…)` / negation with `child(1)` or the first named
19358/// child. tree-sitter counts a comment among a node's children and
19359/// marks it *named*, so `if (/*c*/ b)` handed the comment to the slot,
19360/// which declined it, and the condition scored zero. Each pair below is
19361/// one spelling with and without a comment; the two are asserted equal
19362/// to each other *and* to the expected literal, so a regression that
19363/// moved both sides alike — to zero, say — still fails.
19364///
19365/// The forms, per slot and per comment spelling of the language: a
19366/// comment before the operand, inside a nested parenthesis, before a
19367/// negation, between a negation and its operand, and — where the
19368/// language's syntax puts parentheses around the slot — before those
19369/// parentheses. Value slots (`return`, declarator, assignment, lambda
19370/// body) take the negated forms only, since a bare operand there is no
19371/// condition, and only the first comment spelling: a line comment after
19372/// `return` ends the statement in Go.
19373#[cfg(test)]
19374#[cfg(any(
19375 feature = "c",
19376 feature = "cpp",
19377 feature = "csharp",
19378 feature = "elixir",
19379 feature = "go",
19380 feature = "groovy",
19381 feature = "java",
19382 feature = "javascript",
19383 feature = "kotlin",
19384 feature = "lua",
19385 feature = "mozcpp",
19386 feature = "mozjs",
19387 feature = "objc",
19388 feature = "perl",
19389 feature = "php",
19390 feature = "python",
19391 feature = "ruby",
19392 feature = "rust",
19393 feature = "typescript"
19394))]
19395mod slot_comment_invariance {
19396 use crate::test_support::metrics_verbatim;
19397 use crate::{LANG, MetricsOptions};
19398
19399 fn conditions(lang: LANG, source: &str) -> u64 {
19400 metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default())
19401 .abc
19402 .conditions_sum()
19403 }
19404
19405 /// How one language spells the slots under test.
19406 struct Spelling {
19407 /// Comment spellings, each carrying the whitespace — a newline
19408 /// for a line comment — the operand after it needs.
19409 comments: &'static [&'static str],
19410 operand: &'static str,
19411 /// `None` where the peel does not descend the negation: Python
19412 /// counts `not` through its own arm, and tree-sitter-python
19413 /// fails to parse the valid `(not # c⏎ b)` at the pinned grammar.
19414 negation: Option<&'static str>,
19415 /// The slot is written inside parentheses: either the syntax
19416 /// requires them (`if (…)`), or a line comment needs them to
19417 /// stay inside the expression (Python, Ruby, Elixir).
19418 parenthesised: bool,
19419 /// The parentheses belong to the slot's syntax, so a comment
19420 /// may also sit before them (`if /*c*/ (b)`).
19421 syntactic_parens: bool,
19422 /// `(template, expected)`; `@` marks a condition slot.
19423 conditions: &'static [(&'static str, u64)],
19424 /// Templates whose `@` is a value slot; a negation there scores 1.
19425 values: &'static [&'static str],
19426 }
19427
19428 const C_COMMENTS: &[&str] = &["/*c*/ ", "//c\n "];
19429
19430 const C_FAMILY: Spelling = Spelling {
19431 comments: C_COMMENTS,
19432 operand: "b",
19433 negation: Some("!"),
19434 parenthesised: true,
19435 syntactic_parens: true,
19436 conditions: &[
19437 ("int f(){ if @ { x(); } }", 1),
19438 ("int f(){ while @ { x(); } }", 1),
19439 ("int f(){ do { x(); } while @; }", 1),
19440 ],
19441 values: &["int f(){ return @; }"],
19442 };
19443
19444 const JS_FAMILY: Spelling = Spelling {
19445 comments: C_COMMENTS,
19446 operand: "b",
19447 negation: Some("!"),
19448 parenthesised: true,
19449 syntactic_parens: true,
19450 conditions: &[
19451 ("function f(){ if @ { x(); } }", 1),
19452 ("function f(){ while @ { x(); } }", 1),
19453 ("function f(){ do { x(); } while @; }", 1),
19454 ],
19455 values: &["function f(){ return @; }"],
19456 };
19457
19458 fn spelling(lang: LANG) -> Option<Spelling> {
19459 Some(match lang {
19460 LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc => C_FAMILY,
19461 LANG::Javascript | LANG::Mozjs | LANG::Typescript | LANG::Tsx => JS_FAMILY,
19462 LANG::Java => Spelling {
19463 conditions: &[
19464 ("class K{ void f(){ if @ { x(); } } }", 1),
19465 ("class K{ void f(){ while @ { x(); } } }", 1),
19466 ("class K{ void f(){ do { x(); } while @; } }", 1),
19467 ],
19468 values: &[
19469 "class K{ boolean f(){ return @; } }",
19470 "class K{ void f(){ boolean y = @; } }",
19471 "class K{ void f(){ y = @; } }",
19472 "class K{ void f(){ Supplier<Boolean> s = () -> @; } }",
19473 ],
19474 ..C_FAMILY
19475 },
19476 LANG::Csharp => Spelling {
19477 conditions: &[
19478 ("class K{ void f(){ if @ { x(); } } }", 1),
19479 ("class K{ void f(){ while @ { x(); } } }", 1),
19480 ("class K{ void f(){ do { x(); } while @; } }", 1),
19481 ],
19482 values: &[
19483 "class K{ bool f(){ return @; } }",
19484 "class K{ void f(){ var y = @; } }",
19485 "class K{ void f(){ y = @; } }",
19486 "class K{ void f(){ Func<bool> s = () => @; } }",
19487 ],
19488 ..C_FAMILY
19489 },
19490 LANG::Groovy => Spelling {
19491 conditions: &[
19492 ("def f(){ if @ { x() } }", 1),
19493 ("def f(){ while @ { x() } }", 1),
19494 ("def f(){ do { x() } while @ }", 1),
19495 ],
19496 values: &[
19497 "def f(){ return @ }",
19498 "def f(){ def y = @ }",
19499 "def f(){ y = @ }",
19500 ],
19501 ..C_FAMILY
19502 },
19503 LANG::Php => Spelling {
19504 comments: &["/*c*/ ", "//c\n ", "#c\n "],
19505 operand: "$b",
19506 conditions: &[
19507 ("<?php function f(){ if @ { x(); } }", 1),
19508 ("<?php function f(){ while @ { x(); } }", 1),
19509 ("<?php function f(){ do { x(); } while @; }", 1),
19510 ],
19511 values: &["<?php function f(){ return @; }"],
19512 ..C_FAMILY
19513 },
19514 LANG::Kotlin => Spelling {
19515 conditions: &[
19516 ("fun f(){ if @ { x() } }", 1),
19517 ("fun f(){ while @ { x() } }", 1),
19518 ("fun f(){ do { x() } while @ }", 1),
19519 ],
19520 values: &[],
19521 ..C_FAMILY
19522 },
19523 LANG::Perl => Spelling {
19524 comments: &["# c\n "],
19525 operand: "$b",
19526 conditions: &[
19527 ("sub f { if @ { x(); } }", 1),
19528 ("sub f { unless @ { x(); } }", 1),
19529 ("sub f { while @ { x(); } }", 1),
19530 ("sub f { until @ { x(); } }", 1),
19531 ],
19532 values: &["sub f { return @; }"],
19533 ..C_FAMILY
19534 },
19535 LANG::Rust => Spelling {
19536 parenthesised: false,
19537 syntactic_parens: false,
19538 conditions: &[
19539 ("fn f(){ if @ { x(); } }", 1),
19540 ("fn f(){ while @ { x(); } }", 1),
19541 ],
19542 values: &["fn f() -> bool { return @; }"],
19543 ..C_FAMILY
19544 },
19545 LANG::Go => Spelling {
19546 parenthesised: false,
19547 syntactic_parens: false,
19548 conditions: &[
19549 ("package p\nfunc f(){ if @ { x() } }", 1),
19550 ("package p\nfunc f(){ for @ { x() } }", 1),
19551 ],
19552 values: &["package p\nfunc f() bool { return @ }"],
19553 ..C_FAMILY
19554 },
19555 LANG::Lua => Spelling {
19556 comments: &["--[[c]] ", "--c\n "],
19557 negation: Some("not "),
19558 parenthesised: false,
19559 syntactic_parens: false,
19560 conditions: &[
19561 ("function f() if @ then x() end end", 1),
19562 ("function f() while @ do x() end end", 1),
19563 ("function f() repeat x() until @ end", 1),
19564 ],
19565 values: &["function f() return @ end"],
19566 ..C_FAMILY
19567 },
19568 LANG::Python => Spelling {
19569 comments: &["# c\n "],
19570 negation: None,
19571 syntactic_parens: false,
19572 conditions: &[
19573 ("def f():\n if @:\n x()\n", 1),
19574 ("def f():\n while @:\n x()\n", 1),
19575 ],
19576 values: &[],
19577 ..C_FAMILY
19578 },
19579 LANG::Ruby => Spelling {
19580 comments: &["# c\n "],
19581 syntactic_parens: false,
19582 conditions: &[
19583 ("def f\n if @\n x\n end\nend\n", 1),
19584 ("def f\n while @\n x\n end\nend\n", 1),
19585 ("def f\n case\n when @\n x\n end\nend\n", 1),
19586 ],
19587 values: &[],
19588 ..C_FAMILY
19589 },
19590 // Elixir's `if` slot scored its operand whatever the peel
19591 // returned, so the defect showed only where the peel decides:
19592 // an operand of a `&&` chain, which counts each operand.
19593 LANG::Elixir => Spelling {
19594 comments: &["# c\n "],
19595 syntactic_parens: false,
19596 conditions: &[
19597 (
19598 "defmodule M do\n def f do\n if @ do\n x()\n end\n end\nend\n",
19599 1,
19600 ),
19601 ("defmodule M do\n def f do\n y = a && @\n end\nend\n", 2),
19602 ],
19603 values: &[],
19604 ..C_FAMILY
19605 },
19606 _ => return None,
19607 })
19608 }
19609
19610 /// `(without, with, expected)` for every form of every slot.
19611 fn cases(s: &Spelling) -> Vec<(String, String, u64)> {
19612 let slot = |x: String| {
19613 if s.parenthesised { format!("({x})") } else { x }
19614 };
19615 let b = s.operand;
19616 let mut out = Vec::new();
19617 for &(template, expected) in s.conditions {
19618 let fill = |x: String| template.replace('@', &x);
19619 for c in s.comments {
19620 let mut push = |plain: String, commented: String| {
19621 out.push((fill(plain), fill(commented), expected));
19622 };
19623 push(slot(b.into()), slot(format!("{c}{b}")));
19624 push(slot(format!("({b})")), slot(format!("({c}{b})")));
19625 if let Some(n) = s.negation {
19626 push(slot(format!("{n}{b}")), slot(format!("{c}{n}{b}")));
19627 push(slot(format!("{n}{b}")), slot(format!("{n}{c}{b}")));
19628 }
19629 if s.syntactic_parens {
19630 push(slot(b.into()), format!("{c}{}", slot(b.into())));
19631 }
19632 }
19633 }
19634 if let (Some(n), Some(c)) = (s.negation, s.comments.first()) {
19635 for template in s.values {
19636 let fill = |x: String| template.replace('@', &x);
19637 out.push((fill(format!("{n}{b}")), fill(format!("{c}{n}{b}")), 1));
19638 out.push((fill(format!("{n}{b}")), fill(format!("{n}{c}{b}")), 1));
19639 }
19640 }
19641 out
19642 }
19643
19644 #[test]
19645 fn a_comment_in_a_slot_changes_nothing() {
19646 let mut checked = 0;
19647 for lang in LANG::into_enum_iter().filter(LANG::is_enabled) {
19648 let Some(spelling) = spelling(lang) else {
19649 continue;
19650 };
19651 for (plain, commented, expected) in cases(&spelling) {
19652 let without = conditions(lang, &plain);
19653 assert_eq!(
19654 conditions(lang, &commented),
19655 without,
19656 "{lang:?}: a comment changed the ABC conditions\n \
19657 without: {plain}\n with: {commented}"
19658 );
19659 assert_eq!(without, expected, "{lang:?}: {plain}");
19660 checked += 1;
19661 }
19662 }
19663 assert!(
19664 checked > 0,
19665 "no slot language enabled; this test asserted nothing"
19666 );
19667 }
19668}
19669
19670/// Slot occupants that no fixed index or first-named-child read finds
19671/// (#1455): a preamble before the slot, a comment *after* the operand
19672/// of a last-operand read, and the non-condition reads that were
19673/// positional too. Each row pairs a spelling with its plain twin.
19674#[cfg(test)]
19675#[cfg(any(
19676 feature = "cpp",
19677 feature = "elixir",
19678 feature = "mozcpp",
19679 feature = "perl",
19680 feature = "ruby",
19681 feature = "rust"
19682))]
19683mod slot_role_reads {
19684 use crate::test_support::metrics_verbatim;
19685 use crate::{LANG, MetricsOptions};
19686
19687 /// `(conditions, assignments)`.
19688 fn abc(lang: LANG, source: &str) -> (u64, u64) {
19689 let abc = metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default()).abc;
19690 (abc.conditions_sum(), abc.assignments_sum())
19691 }
19692
19693 /// `(language, plain, variant, (conditions, assignments))`.
19694 const ROWS: &[(LANG, &str, &str, (u64, u64))] = &[
19695 // A C++ `condition_clause` holds its condition in the `value`
19696 // field; an init-statement sits at child(1) before it.
19697 (
19698 LANG::Cpp,
19699 "int f(){ int y = g(); if (b) { x(); } }",
19700 "int f(){ if (int y = g(); b) { x(); } }",
19701 (1, 1),
19702 ),
19703 (
19704 LANG::Mozcpp,
19705 "int f(){ int y = g(); if (b) { x(); } }",
19706 "int f(){ if (int y = g(); b) { x(); } }",
19707 (1, 1),
19708 ),
19709 (
19710 LANG::Cpp,
19711 "int f(){ if constexpr (b) { x(); } }",
19712 "int f(){ if constexpr (/*c*/ b) { x(); } }",
19713 (1, 0),
19714 ),
19715 // A loop label is the `while` expression's first child.
19716 (
19717 LANG::Rust,
19718 "fn f(){ while b { x(); } }",
19719 "fn f(){ 'a: while b { x(); } }",
19720 (1, 0),
19721 ),
19722 // Perl's `(…)` evaluates to its last element, so the peel takes
19723 // the last operand — never a trailing comment.
19724 (
19725 LANG::Perl,
19726 "sub f { if ($b) { x(); } }",
19727 "sub f { if ($b # c\n) { x(); } }",
19728 (1, 0),
19729 ),
19730 (
19731 LANG::Perl,
19732 "sub f { x() if $b; }",
19733 "sub f { x() if $b # c\n; }",
19734 (1, 0),
19735 ),
19736 // `split`'s delimiter is its first *operand*; a comment before it
19737 // made the pattern read as an ordinary match.
19738 (
19739 LANG::Perl,
19740 "sub f { my @a = split(/,/, $s); }",
19741 "sub f { my @a = split( # sep\n /,/, $s); }",
19742 (0, 1),
19743 ),
19744 // A subject-less `when` scores its operand once; the comment
19745 // made the peel return it, so `x > 1` counted twice.
19746 (
19747 LANG::Ruby,
19748 "def f\n case\n when (x > 1)\n y\n end\nend\n",
19749 "def f\n case\n when ( # c\n x > 1)\n y\n end\nend\n",
19750 (1, 0),
19751 ),
19752 // Elixir's match operator, read at child(1), lost to a comment
19753 // before the `=` — an assignment rather than a condition.
19754 (
19755 LANG::Elixir,
19756 "defmodule M do\n def f do\n a = 1\n end\nend\n",
19757 "defmodule M do\n def f do\n a # c\n = 1\n end\nend\n",
19758 (0, 1),
19759 ),
19760 ];
19761
19762 #[test]
19763 fn a_slot_is_read_by_role() {
19764 let mut checked = 0;
19765 for &(lang, plain, variant, expected) in ROWS.iter().filter(|row| row.0.is_enabled()) {
19766 let without = abc(lang, plain);
19767 assert_eq!(
19768 abc(lang, variant),
19769 without,
19770 "{lang:?}: the variant scored differently\n plain: {plain}\n variant: {variant}"
19771 );
19772 assert_eq!(without, expected, "{lang:?}: {plain}");
19773 checked += 1;
19774 }
19775 assert!(
19776 checked > 0,
19777 "no row language enabled; this test asserted nothing"
19778 );
19779 }
19780}