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 serde::Serialize;
25use serde::ser::{SerializeStruct, Serializer};
26use std::fmt;
27
28use crate::checker::Checker;
29use crate::macros::{
30 csharp_invocation_expr_kinds, csharp_paren_expr_kinds, csharp_prefix_unary_expr_kinds,
31 implement_metric_trait,
32};
33use crate::node::Node;
34use crate::*;
35
36/// The `ABC` metric.
37///
38/// The `ABC` metric measures the size of a source code by counting
39/// the number of Assignments (`A`), Branches (`B`) and Conditions (`C`).
40/// The metric defines an ABC score as a vector of three elements (`<A,B,C>`).
41/// The ABC score can be represented by its individual components (`A`, `B` and `C`)
42/// or by the magnitude of the vector (`|<A,B,C>| = sqrt(A^2 + B^2 + C^2)`).
43///
44/// Official paper and definition:
45///
46/// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
47///
48/// <https://www.softwarerenovation.com/Articles.aspx>
49#[derive(Debug, Clone)]
50pub struct Stats {
51 assignments: f64,
52 assignments_sum: f64,
53 assignments_min: f64,
54 assignments_max: f64,
55 branches: f64,
56 branches_sum: f64,
57 branches_min: f64,
58 branches_max: f64,
59 conditions: f64,
60 conditions_sum: f64,
61 conditions_min: f64,
62 conditions_max: f64,
63 space_count: usize,
64 declaration: Vec<DeclKind>,
65}
66
67#[derive(Debug, Clone)]
68enum DeclKind {
69 Var,
70 Const,
71}
72
73impl Default for Stats {
74 fn default() -> Self {
75 Self {
76 assignments: 0.,
77 assignments_sum: 0.,
78 assignments_min: f64::MAX,
79 assignments_max: 0.,
80 branches: 0.,
81 branches_sum: 0.,
82 branches_min: f64::MAX,
83 branches_max: 0.,
84 conditions: 0.,
85 conditions_sum: 0.,
86 conditions_min: f64::MAX,
87 conditions_max: 0.,
88 space_count: 1,
89 declaration: Vec::new(),
90 }
91 }
92}
93
94impl Serialize for Stats {
95 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96 where
97 S: Serializer,
98 {
99 let mut st = serializer.serialize_struct("abc", 13)?;
100 st.serialize_field("assignments", &self.assignments_sum())?;
101 st.serialize_field("branches", &self.branches_sum())?;
102 st.serialize_field("conditions", &self.conditions_sum())?;
103 st.serialize_field("magnitude", &self.magnitude_sum())?;
104 st.serialize_field("assignments_average", &self.assignments_average())?;
105 st.serialize_field("branches_average", &self.branches_average())?;
106 st.serialize_field("conditions_average", &self.conditions_average())?;
107 st.serialize_field("assignments_min", &self.assignments_min())?;
108 st.serialize_field("assignments_max", &self.assignments_max())?;
109 st.serialize_field("branches_min", &self.branches_min())?;
110 st.serialize_field("branches_max", &self.branches_max())?;
111 st.serialize_field("conditions_min", &self.conditions_min())?;
112 st.serialize_field("conditions_max", &self.conditions_max())?;
113 st.end()
114 }
115}
116
117impl fmt::Display for Stats {
118 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
119 write!(
120 f,
121 "assignments: {}, branches: {}, conditions: {}, magnitude: {}, \
122 assignments_average: {}, branches_average: {}, conditions_average: {}, \
123 assignments_min: {}, assignments_max: {}, \
124 branches_min: {}, branches_max: {}, \
125 conditions_min: {}, conditions_max: {}",
126 self.assignments_sum(),
127 self.branches_sum(),
128 self.conditions_sum(),
129 self.magnitude_sum(),
130 self.assignments_average(),
131 self.branches_average(),
132 self.conditions_average(),
133 self.assignments_min(),
134 self.assignments_max(),
135 self.branches_min(),
136 self.branches_max(),
137 self.conditions_min(),
138 self.conditions_max()
139 )
140 }
141}
142
143impl Stats {
144 /// Merges a second `Abc` metric into the first one.
145 pub fn merge(&mut self, other: &Stats) {
146 // Calculates minimum and maximum values
147 self.assignments_min = self.assignments_min.min(other.assignments_min);
148 self.assignments_max = self.assignments_max.max(other.assignments_max);
149 self.branches_min = self.branches_min.min(other.branches_min);
150 self.branches_max = self.branches_max.max(other.branches_max);
151 self.conditions_min = self.conditions_min.min(other.conditions_min);
152 self.conditions_max = self.conditions_max.max(other.conditions_max);
153
154 self.assignments_sum += other.assignments_sum;
155 self.branches_sum += other.branches_sum;
156 self.conditions_sum += other.conditions_sum;
157
158 self.space_count += other.space_count;
159 }
160
161 /// Returns the `Abc` assignments metric value.
162 #[must_use]
163 pub fn assignments(&self) -> f64 {
164 self.assignments
165 }
166
167 /// Returns the `Abc` assignments sum metric value.
168 #[must_use]
169 pub fn assignments_sum(&self) -> f64 {
170 self.assignments_sum
171 }
172
173 /// Returns the `Abc` assignments average value.
174 ///
175 /// This value is computed dividing the `Abc`
176 /// assignments value for the number of spaces.
177 #[must_use]
178 pub fn assignments_average(&self) -> f64 {
179 self.assignments_sum() / self.space_count as f64
180 }
181
182 /// Returns the `Abc` assignments minimum value.
183 ///
184 /// Collapses the `f64::MAX` sentinel that `Stats::default()` plants
185 /// into `assignments_min` to `0.0`, so a never-observed space
186 /// serializes to a meaningful number rather than `1.7976931e308`.
187 #[allow(clippy::float_cmp)]
188 #[must_use]
189 pub fn assignments_min(&self) -> f64 {
190 if self.assignments_min == f64::MAX {
191 0.0
192 } else {
193 self.assignments_min
194 }
195 }
196
197 /// Returns the `Abc` assignments maximum value.
198 #[must_use]
199 pub fn assignments_max(&self) -> f64 {
200 self.assignments_max
201 }
202
203 /// Returns the `Abc` branches metric value.
204 #[must_use]
205 pub fn branches(&self) -> f64 {
206 self.branches
207 }
208
209 /// Returns the `Abc` branches sum metric value.
210 #[must_use]
211 pub fn branches_sum(&self) -> f64 {
212 self.branches_sum
213 }
214
215 /// Returns the `Abc` branches average value.
216 ///
217 /// This value is computed dividing the `Abc`
218 /// branches value for the number of spaces.
219 #[must_use]
220 pub fn branches_average(&self) -> f64 {
221 self.branches_sum() / self.space_count as f64
222 }
223
224 /// Returns the `Abc` branches minimum value.
225 ///
226 /// Same `f64::MAX` sentinel collapse as `assignments_min`.
227 #[allow(clippy::float_cmp)]
228 #[must_use]
229 pub fn branches_min(&self) -> f64 {
230 if self.branches_min == f64::MAX {
231 0.0
232 } else {
233 self.branches_min
234 }
235 }
236
237 /// Returns the `Abc` branches maximum value.
238 #[must_use]
239 pub fn branches_max(&self) -> f64 {
240 self.branches_max
241 }
242
243 /// Returns the `Abc` conditions metric value.
244 #[must_use]
245 pub fn conditions(&self) -> f64 {
246 self.conditions
247 }
248
249 /// Returns the `Abc` conditions sum metric value.
250 #[must_use]
251 pub fn conditions_sum(&self) -> f64 {
252 self.conditions_sum
253 }
254
255 /// Returns the `Abc` conditions average value.
256 ///
257 /// This value is computed dividing the `Abc`
258 /// conditions value for the number of spaces.
259 #[must_use]
260 pub fn conditions_average(&self) -> f64 {
261 self.conditions_sum() / self.space_count as f64
262 }
263
264 /// Returns the `Abc` conditions minimum value.
265 ///
266 /// Same `f64::MAX` sentinel collapse as `assignments_min`.
267 #[allow(clippy::float_cmp)]
268 #[must_use]
269 pub fn conditions_min(&self) -> f64 {
270 if self.conditions_min == f64::MAX {
271 0.0
272 } else {
273 self.conditions_min
274 }
275 }
276
277 /// Returns the `Abc` conditions maximum value.
278 #[must_use]
279 pub fn conditions_max(&self) -> f64 {
280 self.conditions_max
281 }
282
283 /// Returns the `Abc` magnitude metric value.
284 #[must_use]
285 pub fn magnitude(&self) -> f64 {
286 (self.assignments.powi(2) + self.branches.powi(2) + self.conditions.powi(2)).sqrt()
287 }
288
289 /// Returns the `Abc` magnitude sum metric value.
290 #[must_use]
291 pub fn magnitude_sum(&self) -> f64 {
292 (self.assignments_sum.powi(2) + self.branches_sum.powi(2) + self.conditions_sum.powi(2))
293 .sqrt()
294 }
295
296 #[inline]
297 pub(crate) fn compute_sum(&mut self) {
298 self.assignments_sum += self.assignments;
299 self.branches_sum += self.branches;
300 self.conditions_sum += self.conditions;
301 }
302
303 #[inline]
304 pub(crate) fn compute_minmax(&mut self) {
305 self.assignments_min = self.assignments_min.min(self.assignments);
306 self.assignments_max = self.assignments_max.max(self.assignments);
307 self.branches_min = self.branches_min.min(self.branches);
308 self.branches_max = self.branches_max.max(self.branches);
309 self.conditions_min = self.conditions_min.min(self.conditions);
310 self.conditions_max = self.conditions_max.max(self.conditions);
311 self.compute_sum();
312 }
313}
314
315#[doc(hidden)]
316/// Per-language computation of the ABC metric.
317pub trait Abc
318where
319 Self: Checker,
320{
321 /// Walk `node` and update `stats` with this metric for the language
322 /// implementing the trait.
323 ///
324 /// `code` is the source bytes underlying the parsed tree. Most
325 /// languages ignore it: assignments, branches, and conditions all
326 /// surface as distinct grammar productions and a `kind_id()` match
327 /// is enough. Elixir is the exception — `case` / `cond` / `if` /
328 /// `with` / guard `when` arms surface as `Call` nodes whose keyword
329 /// target lives only in the source text. Matching the `Cyclomatic`
330 /// / `Halstead` / `Exit` / `Cognitive` pattern keeps the signature
331 /// uniform.
332 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats);
333}
334
335// Inspects the content of Java parenthesized expressions
336// and `Not` operators to find unary conditional expressions
337fn java_inspect_container(container_node: &Node, conditions: &mut f64) {
338 use Java::*;
339
340 let mut node = *container_node;
341 let mut node_kind = node.kind_id().into();
342
343 // Initializes the flag to true if the container is known to contain a boolean value
344 let Some(parent) = node.parent() else { return };
345 let mut has_boolean_content = match parent.kind_id().into() {
346 BinaryExpression | IfStatement | WhileStatement | DoStatement | ForStatement => true,
347 TernaryExpression => node
348 .previous_sibling()
349 .is_none_or(|prev_node| !matches!(prev_node.kind_id().into(), QMARK | COLON)),
350 _ => false,
351 };
352
353 // Looks inside parenthesized expressions and `Not` operators to find what they contain
354 loop {
355 // Checks if the node is a parenthesized expression or a `Not` operator
356 // The child node of index 0 contains the unary expression operator (we look for the `!` operator)
357 let is_parenthesised_exp = matches!(node_kind, ParenthesizedExpression);
358 let is_not_operator = matches!(node_kind, UnaryExpression)
359 && node
360 .child(0)
361 .is_some_and(|c| matches!(c.kind_id().into(), BANG));
362
363 // Stops the exploration if the node is neither
364 // a parenthesized expression nor a `Not` operator
365 if !is_parenthesised_exp && !is_not_operator {
366 break;
367 }
368
369 // Sets the flag to true if a `Not` operator is found
370 // This is used to prove if a variable or a value returned by a method is actually boolean
371 // e.g. `return (!x);`
372 if !has_boolean_content && is_not_operator {
373 has_boolean_content = true;
374 }
375
376 // Parenthesized expressions and `Not` operators nodes
377 // always store their expressions in the children nodes of index one
378 // https://github.com/tree-sitter/tree-sitter-java/blob/master/src/grammar.json#L2472
379 // https://github.com/tree-sitter/tree-sitter-java/blob/master/src/grammar.json#L2150
380 let Some(child) = node.child(1) else { break };
381 node = child;
382 node_kind = node.kind_id().into();
383
384 // Stops the exploration when the content is found
385 if matches!(node_kind, MethodInvocation | Identifier | True | False) {
386 if has_boolean_content {
387 *conditions += 1.;
388 }
389 break;
390 }
391 }
392}
393
394// C# analogue of `java_inspect_container`: walks parenthesised expressions
395// and `!` (PrefixUnaryExpression) wrappers to surface a unary boolean
396// condition contained within.
397fn csharp_inspect_container(container_node: &Node, conditions: &mut f64) {
398 use Csharp::*;
399
400 let mut node = *container_node;
401 let mut node_kind = node.kind_id().into();
402
403 // Seed the boolean-context flag from the parent: known-boolean
404 // contexts (loop / if / binary expression) imply the contained
405 // expression evaluates as a condition.
406 let Some(parent) = node.parent() else { return };
407 let mut has_boolean_content = match parent.kind_id().into() {
408 BinaryExpression | IfStatement | WhileStatement | DoStatement | ForStatement => true,
409 ConditionalExpression => node
410 .previous_sibling()
411 .is_none_or(|prev| !matches!(prev.kind_id().into(), QMARK | COLON)),
412 _ => false,
413 };
414
415 // Walk down through `(...)` and `!...` wrappers until we either hit
416 // the underlying operand or run out of nesting. The C# grammar
417 // aliases each of these kinds across multiple `kind_id`s
418 // (lesson #2): match every numbered variant.
419 loop {
420 let is_parens = matches!(node_kind, csharp_paren_expr_kinds!());
421 let is_not = matches!(node_kind, csharp_prefix_unary_expr_kinds!())
422 && node
423 .child(0)
424 .is_some_and(|c| matches!(c.kind_id().into(), BANG));
425
426 if !is_parens && !is_not {
427 break;
428 }
429
430 // A `!` wrapper proves the contained value is boolean even
431 // when the parent context didn't (e.g. `return !x;`).
432 if !has_boolean_content && is_not {
433 has_boolean_content = true;
434 }
435
436 // Both `parenthesized_expression` and `prefix_unary_expression`
437 // store their inner expression at child index 1.
438 let Some(child) = node.child(1) else { break };
439 node = child;
440 node_kind = node.kind_id().into();
441
442 // Found the innermost operand; count it if a boolean context
443 // was established up the chain.
444 if matches!(
445 node_kind,
446 crate::Csharp::InvocationExpression
447 | crate::Csharp::InvocationExpression2
448 | crate::Csharp::InvocationExpression3
449 | Identifier
450 | True
451 | False
452 ) {
453 if has_boolean_content {
454 *conditions += 1.;
455 }
456 break;
457 }
458 }
459}
460
461fn csharp_count_unary_conditions(list_node: &Node, conditions: &mut f64) {
462 use Csharp::*;
463
464 let list_kind = list_node.kind_id().into();
465 let mut cursor = list_node.cursor();
466
467 if cursor.goto_first_child() {
468 loop {
469 let node = cursor.node();
470 let node_kind = node.kind_id().into();
471
472 if matches!(
473 node_kind,
474 crate::Csharp::InvocationExpression
475 | crate::Csharp::InvocationExpression2
476 | crate::Csharp::InvocationExpression3
477 | Identifier
478 | True
479 | False
480 ) && matches!(list_kind, BinaryExpression)
481 {
482 *conditions += 1.;
483 } else {
484 csharp_inspect_container(&node, conditions);
485 }
486
487 if !cursor.goto_next_sibling() {
488 break;
489 }
490 }
491 }
492}
493
494// Inspects a list of elements and counts any unary conditional expression found
495fn java_count_unary_conditions(list_node: &Node, conditions: &mut f64) {
496 use Java::*;
497
498 let list_kind = list_node.kind_id().into();
499 let mut cursor = list_node.cursor();
500
501 // Scans the immediate children nodes of the argument node
502 if cursor.goto_first_child() {
503 loop {
504 // Gets the current child node and its kind
505 let node = cursor.node();
506 let node_kind = node.kind_id().into();
507
508 // Checks if the node is a unary condition
509 if matches!(node_kind, MethodInvocation | Identifier | True | False)
510 && matches!(list_kind, BinaryExpression)
511 {
512 *conditions += 1.;
513 } else {
514 // Checks if the node is a unary condition container
515 java_inspect_container(&node, conditions);
516 }
517
518 // Moves the cursor to the next sibling node of the current node
519 // Exits the scan if there is no next sibling node
520 if !cursor.goto_next_sibling() {
521 break;
522 }
523 }
524 }
525}
526
527// Groovy mirror of `java_inspect_container`. The dekobon Groovy grammar
528// inherits `parenthesized_expression`, `unary_expression`, and the
529// standard boolean-context kinds from tree-sitter-java verbatim, so the
530// body is structurally identical to Java's helper. The terminal set
531// includes `CommandChain` (the new grammar's distinct node for Groovy's
532// parens-less call form `println foo`), keeping it in sync with the
533// `impl Abc for GroovyCode` branches dispatch.
534fn groovy_inspect_container(container_node: &Node, conditions: &mut f64) {
535 use Groovy::*;
536
537 let mut node = *container_node;
538 let mut node_kind = node.kind_id().into();
539
540 let Some(parent) = node.parent() else { return };
541 let mut has_boolean_content = match parent.kind_id().into() {
542 BinaryExpression | IfStatement | WhileStatement | DoWhileStatement | ForStatement => true,
543 TernaryExpression => node
544 .previous_sibling()
545 .is_none_or(|prev_node| !matches!(prev_node.kind_id().into(), QMARK | COLON)),
546 _ => false,
547 };
548
549 loop {
550 let is_parenthesised_exp = matches!(node_kind, ParenthesizedExpression);
551 let is_not_operator = matches!(node_kind, UnaryExpression)
552 && node
553 .child(0)
554 .is_some_and(|c| matches!(c.kind_id().into(), BANG));
555
556 if !is_parenthesised_exp && !is_not_operator {
557 break;
558 }
559
560 if !has_boolean_content && is_not_operator {
561 has_boolean_content = true;
562 }
563
564 let Some(child) = node.child(1) else { break };
565 node = child;
566 node_kind = node.kind_id().into();
567
568 if matches!(
569 node_kind,
570 MethodInvocation | CommandChain | Identifier | True | False
571 ) {
572 if has_boolean_content {
573 *conditions += 1.;
574 }
575 break;
576 }
577 }
578}
579
580fn groovy_count_unary_conditions(list_node: &Node, conditions: &mut f64) {
581 use Groovy::*;
582
583 let list_kind = list_node.kind_id().into();
584 let mut cursor = list_node.cursor();
585
586 if cursor.goto_first_child() {
587 loop {
588 let node = cursor.node();
589 let node_kind = node.kind_id().into();
590
591 if matches!(
592 node_kind,
593 MethodInvocation | CommandChain | Identifier | True | False
594 ) && matches!(list_kind, BinaryExpression)
595 {
596 *conditions += 1.;
597 } else {
598 groovy_inspect_container(&node, conditions);
599 }
600
601 if !cursor.goto_next_sibling() {
602 break;
603 }
604 }
605 }
606}
607
608// Default no-op `Abc` impls. Audited in #188; the matrix below
609// records the rationale for every entry so the no-op default is a
610// deliberate choice, not scaffolding leftover.
611//
612// Real defaults (the language has no construct ABC measures, so the
613// metric is genuinely 0):
614// - PreprocCode, CcommentCode: no executable code (comments /
615// preprocessor lines only).
616implement_metric_trait!(Abc, PreprocCode, CcommentCode);
617
618// TypeScript / TSX share the same expression / statement vocabulary; the
619// `ts_abc_compute!` macro expands the same token-level Fitzpatrick rules
620// for both. Compared with the Java / C# impls we stay at the leaf-token
621// level rather than walking parenthesised / unary containers — TS source
622// rarely uses C-style `if (x)` conditions, so the
623// "unary-boolean-in-a-container" heuristic adds noise without catching
624// many real conditions. Conditions still capture every comparison and
625// control-flow arm.
626//
627// Declaration sentinel: `lexical_declaration` and `variable_declaration`
628// push a `Var` sentinel that suppresses counting the initializer `=` as
629// an assignment. The `Const` token promotes to `Const` (compile-time
630// constant — initializer is not a mutable assignment). `let` and `var`
631// keep the `Var` slot. Augmented assignments (`+=`) and update
632// expressions (`++`, `--`) always count.
633macro_rules! ts_abc_compute {
634 ($lang:ident) => {
635 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
636 use $lang::*;
637
638 match node.kind_id().into() {
639 // Augmented assignments and pre/post increment/decrement
640 // always count.
641 PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ | STARSTAREQ | AMPEQ | PIPEEQ
642 | CARETEQ | LTLTEQ | GTGTEQ | GTGTGTEQ | AMPAMPEQ | PIPEPIPEEQ | QMARKQMARKEQ
643 | PLUSPLUS | DASHDASH => {
644 stats.assignments += 1.;
645 }
646 // Variable declarations push a `Var` sentinel; the `Const`
647 // keyword promotes the top to `Const` so the initializer
648 // `=` is treated as a constant binding.
649 LexicalDeclaration | VariableDeclaration => {
650 stats.declaration.push(DeclKind::Var);
651 }
652 Const => {
653 if let Some(DeclKind::Var) = stats.declaration.last() {
654 stats.declaration.push(DeclKind::Const);
655 }
656 }
657 SEMI => {
658 if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
659 stats.declaration.clear();
660 }
661 }
662 // Plain `=` outside `const` declarations is an assignment.
663 EQ if !matches!(stats.declaration.last(), Some(DeclKind::Const)) => {
664 stats.assignments += 1.;
665 }
666 // Function invocation and object construction count as
667 // branches. Member calls and chained calls all surface
668 // as `CallExpression`.
669 CallExpression | NewExpression => {
670 stats.branches += 1.;
671 }
672 // Comparison and equality operators, ternary `?`, `??`,
673 // `instanceof`, `else`, `case`, `default`, `catch`,
674 // `try`.
675 EQEQ | EQEQEQ | BANGEQ | BANGEQEQ | LTEQ | GTEQ | QMARK | QMARKQMARK
676 | Instanceof | Else | Case | Default | Try | Catch => {
677 stats.conditions += 1.;
678 }
679 // `<` and `>` may also delimit type arguments / type
680 // parameters (`Array<number>`, `class Foo<T> {}`); skip
681 // those, count only comparison usage.
682 GT | LT
683 if node.parent().is_some_and(|p| {
684 !matches!(p.kind_id().into(), TypeArguments | TypeParameters)
685 }) =>
686 {
687 stats.conditions += 1.;
688 }
689 _ => {}
690 }
691 }
692 };
693}
694
695impl Abc for TypescriptCode {
696 ts_abc_compute!(Typescript);
697}
698
699impl Abc for TsxCode {
700 ts_abc_compute!(Tsx);
701}
702
703// JavaScript / Mozjs share TypeScript's expression / statement
704// vocabulary. The `js_abc_compute!` macro expands the same
705// token-level Fitzpatrick rules as `ts_abc_compute!`, with two
706// adjustments:
707//
708// 1. `LT` / `GT` are always comparison operators in plain JS — there
709// are no `TypeArguments` / `TypeParameters` nodes to gate against.
710// 2. JS retains the same `LexicalDeclaration` / `VariableDeclaration`
711// sentinel handling so `const x = 5` does not double-count the
712// initializer `=` as an assignment. `let x = 5` and `var x = 5`
713// DO count their initializer `=` as an assignment — only `const`
714// suppresses, matching the TS impl above. This deliberately
715// deviates from a strict reading of Fitzpatrick's "declaration
716// initialiser is not an assignment" rule because `let`/`var`
717// bindings can be reassigned and the initial value is the first
718// assignment of the binding's lifetime.
719macro_rules! js_abc_compute {
720 ($lang:ident) => {
721 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
722 use $lang::*;
723
724 match node.kind_id().into() {
725 PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ | STARSTAREQ | AMPEQ | PIPEEQ
726 | CARETEQ | LTLTEQ | GTGTEQ | GTGTGTEQ | AMPAMPEQ | PIPEPIPEEQ | QMARKQMARKEQ
727 | PLUSPLUS | DASHDASH => {
728 stats.assignments += 1.;
729 }
730 LexicalDeclaration | VariableDeclaration => {
731 stats.declaration.push(DeclKind::Var);
732 }
733 Const => {
734 if let Some(DeclKind::Var) = stats.declaration.last() {
735 stats.declaration.push(DeclKind::Const);
736 }
737 }
738 SEMI => {
739 if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
740 stats.declaration.clear();
741 }
742 }
743 EQ if !matches!(stats.declaration.last(), Some(DeclKind::Const)) => {
744 stats.assignments += 1.;
745 }
746 CallExpression | NewExpression => {
747 stats.branches += 1.;
748 }
749 EQEQ | EQEQEQ | BANGEQ | BANGEQEQ | LTEQ | GTEQ | LT | GT | QMARK | QMARKQMARK
750 | Instanceof | Else | Case | Default | Try | Catch => {
751 stats.conditions += 1.;
752 }
753 _ => {}
754 }
755 }
756 };
757}
758
759impl Abc for JavascriptCode {
760 js_abc_compute!(Javascript);
761}
762
763impl Abc for MozjsCode {
764 js_abc_compute!(Mozjs);
765}
766
767// Fitzpatrick's ABC rules adapted for Kotlin syntax. Kotlin shares the
768// JVM and Java's spec roots: assignments count once per `=` / augmented
769// assignment / ++ / --, branches count once per function invocation or
770// object construction, conditions count comparison operators plus the
771// `else` / `when`-entry / `catch` arms. Compared with the Java impl we
772// stay token-level (matching the leaf kind_ids) rather than walking
773// `Modifiers` children; the Kotlin grammar exposes the relevant
774// operators directly as token nodes inside `binary_expression`,
775// `assignment`, `prefix_expression`, and `postfix_expression`.
776impl Abc for KotlinCode {
777 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
778 use Kotlin::*;
779
780 match node.kind_id().into() {
781 // Property / local-variable declaration and primary-constructor
782 // parameter property (`class C(val a: Int = 5)`) both push a
783 // sentinel so the `=` operator initialising the binding is NOT
784 // counted as a standalone assignment (Fitzpatrick:
785 // "initialisation is part of the declaration", mirroring Java).
786 // The `Val` keyword arm below promotes the sentinel to `Const`
787 // for immutable bindings.
788 PropertyDeclaration | ClassParameter => {
789 stats.declaration.push(DeclKind::Var);
790 }
791 // `val` introduces an immutable binding; promote the pending
792 // declaration to `Const` so the upcoming `=` is suppressed
793 // (constants are not assignments in ABC).
794 Val => {
795 if let Some(DeclKind::Var) = stats.declaration.last() {
796 stats.declaration.push(DeclKind::Const);
797 }
798 }
799 // Statement terminator: the grammar emits an explicit `SEMI`
800 // only for explicit semicolons. Property declarations also
801 // terminate without one when the next token starts a new
802 // statement. We clear the sentinel on the explicit `SEMI`
803 // here; the implicit-terminator case is benign because the
804 // EQ arm reads only `last()`, which is the most recently
805 // pushed sentinel — any older entries left on the stack
806 // from preceding implicit terminators do not affect the
807 // assignment count.
808 SEMI => {
809 if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
810 stats.declaration.clear();
811 }
812 }
813 // Augmented assignments and pre/post increment-decrement
814 // always count, regardless of declaration context.
815 PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ | PLUSPLUS | DASHDASH => {
816 stats.assignments += 1.;
817 }
818 // Plain `=` token. Skip when inside a `val` declaration; count
819 // when inside a `var` declaration (initialiser of mutable
820 // binding) or a standalone `Assignment`. The DeclKind stack is
821 // cleared at the property statement boundary above.
822 EQ if stats
823 .declaration
824 .last()
825 .is_none_or(|decl| matches!(decl, DeclKind::Var)) =>
826 {
827 stats.assignments += 1.;
828 }
829 // Branches: every call expression plus object construction.
830 // Kotlin's `new` is implicit — `Foo()` parses as
831 // `CallExpression` with a type-named receiver. The
832 // Halstead-side classification treats it uniformly. Indexed
833 // access (`arr[i]`) is NOT a branch (it's an operator on a
834 // sequence), matching the Java rule of "method invocation
835 // only".
836 CallExpression => {
837 stats.branches += 1.;
838 }
839 // Conditions: comparison operators, identity equality,
840 // ternary-elvis (`?:`), `as?` safe-cast, and the arms of
841 // control-flow constructs (`else`, `catch`, `when` entries).
842 // Kotlin's `if`-expression does not need an extra count for
843 // the `if` keyword itself — Fitzpatrick counts the
844 // *conditions*, and the unary condition is already implicit
845 // in the boolean operand. We add the `if` arm via the `Else`
846 // keyword for else-branches and via `WhenEntry` for `when`.
847 LTEQ | GTEQ | EQEQ | EQEQEQ | BANGEQ | BANGEQEQ | WhenEntry | CatchBlock
848 | QMARKCOLON | AsQMARK => {
849 stats.conditions += 1.;
850 }
851 // `else` is a keyword token used in both `if_expression`'s
852 // else-clause and `when`'s `else ->` entry. Only count it
853 // when it belongs to an `if_expression`; the `WhenEntry`
854 // wrapper above already covers the `when` case.
855 Else if node.parent().is_some_and(|p| p.kind_id() == IfExpression) => {
856 stats.conditions += 1.;
857 }
858 // `<` and `>` may appear as type-argument brackets
859 // (`List<Int>`); exclude those by checking the parent kind.
860 LT | GT
861 if node.parent().is_some_and(|p| {
862 !matches!(p.kind_id().into(), TypeArguments | TypeParameters)
863 }) =>
864 {
865 stats.conditions += 1.;
866 }
867 _ => {}
868 }
869 }
870}
871
872impl Abc for PhpCode {
873 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
874 use Php::*;
875
876 match node.kind_id().into() {
877 // Assignments: explicit assignment expressions and augmented forms,
878 // plus pre/post increment and decrement. `const_declaration` and
879 // `enum_case` use their own `const_element` / value-assignment
880 // shapes, so they do not produce `AssignmentExpression` nodes —
881 // matching the assignment-expression kinds naturally excludes
882 // them.
883 AssignmentExpression
884 | AugmentedAssignmentExpression
885 | ReferenceAssignmentExpression
886 | PLUSPLUS
887 | DASHDASH => {
888 stats.assignments += 1.;
889 }
890 // Branches: every PHP call kind plus object construction.
891 FunctionCallExpression
892 | MemberCallExpression
893 | ScopedCallExpression
894 | NullsafeMemberCallExpression
895 | ObjectCreationExpression => {
896 stats.branches += 1.;
897 }
898 // Conditions: comparison and identity operators (anonymous tokens
899 // inside `binary_expression`), `instanceof`, ternary `?`, and
900 // control-flow arms.
901 EQEQ
902 | EQEQEQ
903 | BANGEQ
904 | BANGEQEQ
905 | LT
906 | GT
907 | LTEQ
908 | GTEQ
909 | LTEQGT
910 | LTGT
911 | Instanceof
912 | ConditionalExpression
913 | ElseClause
914 | ElseClause2
915 | ElseIfClause
916 | ElseIfClause2
917 | CaseStatement
918 | DefaultStatement
919 | MatchConditionalExpression
920 | MatchDefaultExpression
921 | CatchClause => {
922 stats.conditions += 1.;
923 }
924 _ => {}
925 }
926 }
927}
928
929// Ruby ABC rules follow the Fitzpatrick paper's spirit, adapted to
930// tree-sitter-ruby:
931// - Assignments: `assignment` (plain `=`) and `operator_assignment`
932// (`+=`, `-=`, `||=`, `&&=`, …). Tree-sitter wraps both forms in a
933// dedicated node, so we count one assignment per node and avoid
934// double-counting the inner `=` / augmented token.
935// - Branches: every Ruby method invocation kind (`Call` / `Call2` /
936// `Call3` / `Call4`) plus `super` and `yield`. `yield` is grammar-
937// level a "block invocation" but ABC's branch bucket is "message
938// pass / function call", so it belongs here. `attr_*` macros are
939// `Call3` nodes and are counted as branches like any other call.
940// - Conditions: comparison and equality operator tokens emitted inside
941// `binary` (`==`, `!=`, `===`, `<`, `>`, `<=`, `>=`, `<=>`,
942// `=~`, `!~`), plus the control-flow arms that the Fitzpatrick rules
943// list — the named clause nodes `Else` / `Elsif` / `When` and the
944// `?` ternary marker, plus `Rescue` (the rescue clause) and rescue
945// modifiers. `if` / `unless` themselves are not counted (the head
946// condition appears as the inner comparison); the `Then` clause is
947// an implicit grammar wrapper around every `if` / `elsif` body and
948// is NOT counted as a separate arm.
949impl Abc for RubyCode {
950 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
951 use Ruby::*;
952
953 match node.kind_id().into() {
954 Assignment | Assignment2 | OperatorAssignment | OperatorAssignment2 => {
955 stats.assignments += 1.;
956 }
957 Call | Call2 | Call3 | Call4 | Super | Yield | Yield2 => {
958 stats.branches += 1.;
959 }
960 EQEQ | BANGEQ | EQEQEQ | LT | GT | LTEQ | GTEQ | LTEQGT | EQTILDE | BANGTILDE
961 | Else | Elsif | When | QMARK | Rescue | RescueModifier | RescueModifier2
962 | RescueModifier3 => {
963 stats.conditions += 1.;
964 }
965 _ => {}
966 }
967 }
968}
969
970// Fitzpatrick's ABC rules adapted for Python.
971//
972// - Assignments: every `Assignment` node that contains an explicit `=`
973// token (plain assignment, walrus `:=` lives in `NamedExpression`,
974// handled separately), plus every `AugmentedAssignment` (`+=`,
975// `-=`, …) and every `NamedExpression` (walrus). Bare type-only
976// annotations like `x: int` also parse as `Assignment` but have no
977// `=` child — these are excluded so a class-level type annotation
978// does not inflate the assignment count.
979// - Branches: every `Call` node. Python's "object construction" is
980// syntactically a `Call` (`Foo()` parses as `call`), so the same
981// arm covers it without a separate `New`-style case.
982// - Conditions: comparison operators (`ComparisonOperator` wraps
983// `<`, `>`, `==`, `!=`, `is`, `is not`, `in`, `not in`, etc. as a
984// single node), `BooleanOperator` (`and`/`or`), `ConditionalExpression`
985// (ternary `a if c else b`), and the explicit arms of control flow:
986// `ElifClause`, `ElseClause`, `ExceptClause`, `FinallyClause`,
987// `CaseClause`. We do not separately count the `if` / `while`
988// keyword: the condition expression itself is already covered by
989// `ComparisonOperator` or `BooleanOperator`. This matches the
990// token-level approach used for PHP / Bash.
991impl Abc for PythonCode {
992 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
993 use Python::*;
994
995 match node.kind_id().into() {
996 // Plain `=` assignment. tree-sitter-python emits an
997 // `Assignment` node for both `x = 1` (LHS, `=`, RHS) and
998 // bare annotations `x: int` (LHS, `:`, type, *no* `=`).
999 // Filtering on the presence of an `EQ` child keeps the
1000 // annotation-only case out of the count.
1001 Assignment if node.first_child(|id| id == EQ).is_some() => {
1002 stats.assignments += 1.;
1003 }
1004 // Augmented assignment (`+=`, `-=`, `*=`, …) always counts;
1005 // walrus `name := expr` is a PEP-572 `NamedExpression` and
1006 // also binds a value, so it counts as one assignment under
1007 // Fitzpatrick's rule.
1008 AugmentedAssignment | NamedExpression => {
1009 stats.assignments += 1.;
1010 }
1011 // Every call — function call, method call, type
1012 // construction — is one branch. Python parses `Foo()` as
1013 // `Call`, so object construction folds into this arm.
1014 Call => {
1015 stats.branches += 1.;
1016 }
1017 // `x < y`, `a == b`, `c is None`, `n in xs`, `m not in xs`
1018 // all parse as a single `ComparisonOperator` node — one
1019 // node, one condition, regardless of how many comparison
1020 // operators are chained.
1021 ComparisonOperator
1022 | BooleanOperator
1023 | ConditionalExpression
1024 | ElifClause
1025 | ElseClause
1026 | ExceptClause
1027 | FinallyClause
1028 | NotOperator => {
1029 // `NotOperator` is Python's unary `not`. Counting it
1030 // mirrors Java's `!x` / C#'s `!x` Abc condition rule
1031 // and closes the parity gap noted in #214 — without
1032 // it, `if not flag:` reports 0 conditions while
1033 // `if !flag` in Java reports 1. Nested combos like
1034 // `not (x > 0)` count both the unary and the
1035 // comparison once each (one logical "is-negation",
1036 // one logical "comparison"), matching Java's
1037 // `!(x > 0)`.
1038 stats.conditions += 1.;
1039 }
1040 // A non-wildcard `case` arm contributes one condition,
1041 // matching Rust's bare-`_` MatchArm filter and Java/C#'s
1042 // `default:` rule. The bare wildcard is detected by: (a)
1043 // `case_pattern` is `_`, AND (b) no `if_clause` sibling
1044 // on the `case_clause` — `case _ if g:` carries a guard
1045 // and still counts. The shared classifier lives in
1046 // `super::npa` next to `pattern_is_bare_underscore`.
1047 CaseClause if super::npa::python_case_clause_counts(node, UNDERSCORE as u16) => {
1048 stats.conditions += 1.;
1049 }
1050 _ => {}
1051 }
1052 }
1053}
1054
1055impl Abc for RustCode {
1056 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1057 use Rust::*;
1058
1059 match node.kind_id().into() {
1060 // Plain `x = expr` (assignment_expression) and augmented
1061 // forms `+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`, `^=`,
1062 // `<<=`, `>>=` (compound_assignment_expr) both bind a
1063 // value; each counts as one assignment. Rust grammar
1064 // isolates both in distinct named nodes, so there is no
1065 // risk of double-counting the contained `EQ` token here.
1066 AssignmentExpression | CompoundAssignmentExpr => {
1067 stats.assignments += 1.;
1068 }
1069 // Every call expression — including method calls
1070 // (`a.b.c()` parses as `call_expression` whose callee is a
1071 // `field_expression`) — plus every `try_expression` (the
1072 // `?` operator, a short-circuit return on Result / Option)
1073 // contributes one branch. Macro invocations parse as
1074 // `macro_invocation`, NOT `call_expression`, so they are
1075 // intentionally NOT counted as branches.
1076 CallExpression | TryExpression => {
1077 stats.branches += 1.;
1078 }
1079 // Comparison operators emitted as token children of a
1080 // `binary_expression`, `if let` / `while let` conditions,
1081 // and the `else` keyword each count as one condition.
1082 // `let_condition` covers both `if let` and `while let`
1083 // (Rust's grammar uses the same node for both); inside a
1084 // `let_chain` each `let_condition` counts separately.
1085 // Java counts the `Else` token directly; Rust's grammar
1086 // exposes the same token and we follow that lead.
1087 LTEQ | GTEQ | EQEQ | BANGEQ | LetCondition | Else => {
1088 stats.conditions += 1.;
1089 }
1090 // `<` / `>` doubles as type-argument delimiter; the
1091 // `BinaryExpression` parent check disambiguates without
1092 // needing to inspect siblings.
1093 LT | GT
1094 if node
1095 .parent()
1096 .is_some_and(|p| matches!(p.kind_id().into(), BinaryExpression)) =>
1097 {
1098 stats.conditions += 1.;
1099 }
1100 // Every non-wildcard `match_arm` is one condition. A bare
1101 // `_ => ...` arm is the C / Java `default:` equivalent and
1102 // is excluded — mirrors the cyclomatic treatment and
1103 // Kotlin's `when` / Java's `case` rules. Patterns like
1104 // `Some(_)`, `(_, x)`, or `_ if guard` are not bare
1105 // wildcards and still count. The check scans only NAMED
1106 // children of `match_pattern` so anonymous tokens like a
1107 // leading `|` (allowed in or-patterns: `| _ => ...`) do
1108 // not throw off the detection. A guard (`_ if g`) adds a
1109 // second named child to `match_pattern` and so escapes
1110 // the bare-wildcard filter.
1111 MatchArm | MatchArm2 => {
1112 let is_bare_wildcard = node.child_by_field_name("pattern").is_some_and(|pat| {
1113 super::npa::pattern_is_bare_underscore(&pat, UNDERSCORE as u16)
1114 });
1115 if !is_bare_wildcard {
1116 stats.conditions += 1.;
1117 }
1118 }
1119 _ => {}
1120 }
1121 }
1122}
1123
1124impl Abc for GoCode {
1125 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1126 // Aliased because `Go::Go` (the `go` keyword variant) collides
1127 // with the bare enum name in pattern position under
1128 // `use Go::*;` (same workaround as in cyclomatic / cognitive).
1129 use Go as G;
1130
1131 match node.kind_id().into() {
1132 // Plain `=`, augmented `+=`, `-=`, … all parse as
1133 // `assignment_statement`. `:=` is a short variable
1134 // declaration. `x++` / `x--` rebind too.
1135 G::AssignmentStatement | G::ShortVarDeclaration | G::IncStatement | G::DecStatement => {
1136 stats.assignments += 1.;
1137 }
1138 // Every call expression — including method calls
1139 // (`r.Method()` parses as `call_expression` whose callee is
1140 // a `selector_expression`) — contributes one branch.
1141 // Composite literals (`Point{X: 1}`) are NOT calls.
1142 G::CallExpression => {
1143 stats.branches += 1.;
1144 }
1145 // Comparison operators emitted as token children of a
1146 // `binary_expression`, `else`, and each non-default switch
1147 // / type-switch / select arm all contribute one condition.
1148 // `<` / `>` double as type-argument delimiters in generic
1149 // instantiations (`f[T any]`, `List[int]`); the
1150 // `BinaryExpression` parent guard filters those out
1151 // without inspecting siblings. `default_case` is
1152 // intentionally excluded — like Java / C# `default:`, it
1153 // does not introduce a new decision point.
1154 G::EQEQ
1155 | G::BANGEQ
1156 | G::LTEQ
1157 | G::GTEQ
1158 | G::Else
1159 | G::ExpressionCase
1160 | G::TypeCase
1161 | G::CommunicationCase => {
1162 stats.conditions += 1.;
1163 }
1164 G::LT | G::GT
1165 if node
1166 .parent()
1167 .is_some_and(|p| matches!(p.kind_id().into(), G::BinaryExpression)) =>
1168 {
1169 stats.conditions += 1.;
1170 }
1171 _ => {}
1172 }
1173 }
1174}
1175
1176impl Abc for CppCode {
1177 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1178 use Cpp::*;
1179
1180 match node.kind_id().into() {
1181 // `assignment_expression` covers both plain `=` and every
1182 // compound form (`+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`,
1183 // `^=`, `<<=`, `>>=`); the grammar lifts them all into a
1184 // single named node so we count once per
1185 // `assignment_expression`. `update_expression` covers both
1186 // prefix and postfix `++` / `--`. Variable initialisers
1187 // (`int x = 0`) parse as `init_declarator` inside
1188 // `declaration` and never become `assignment_expression` —
1189 // they correctly stay out.
1190 AssignmentExpression | AssignmentExpression2 | UpdateExpression => {
1191 stats.assignments += 1.;
1192 }
1193 // Every call counts (method calls fold in as
1194 // `call_expression` with a `field_expression` callee). The
1195 // C++ grammar exposes two aliased `call_expression` ids.
1196 // `new T(...)` allocations count as a branch — they invoke
1197 // a constructor, mirroring Java's `New` and C#'s
1198 // `ObjectCreationExpression` rule.
1199 CallExpression | CallExpression2 | NewExpression => {
1200 stats.branches += 1.;
1201 }
1202 // Comparison operators emitted as token children of a
1203 // `binary_expression`. The C++20 spaceship `<=>` (`LTEQGT`)
1204 // is a comparison operator and counts once per use.
1205 // `&&` / `||` add one each per Fitzpatrick. `else` opens
1206 // an alternative branch path; `case` (non-default) adds
1207 // one per switch arm; `?` opens a ternary; `try` / `catch`
1208 // count per Fitzpatrick (and Java's rule). `Try2` is the
1209 // second token-id alias the C++ grammar emits for `try`
1210 // (it appears under structured-exception forms).
1211 LTEQ | GTEQ | EQEQ | BANGEQ | LTEQGT | AMPAMP | PIPEPIPE | Else | Case | QMARK
1212 | Try | Try2 | Catch => {
1213 stats.conditions += 1.;
1214 }
1215 // Plain `<` / `>` doubles as template-argument and
1216 // template-parameter delimiter (`std::vector<int>`,
1217 // `template <typename T>`). The `binary_expression` parent
1218 // check disambiguates without inspecting siblings — only
1219 // comparison uses of `<` / `>` count. Both kind-id aliases
1220 // (`BinaryExpression`, `BinaryExpression2`) are accepted
1221 // because the C++ grammar emits the same node under two
1222 // production-rule paths.
1223 LT | GT
1224 if node.parent().is_some_and(|p| {
1225 matches!(p.kind_id().into(), BinaryExpression | BinaryExpression2)
1226 }) =>
1227 {
1228 stats.conditions += 1.;
1229 }
1230 _ => {}
1231 }
1232 }
1233}
1234
1235impl Abc for BashCode {
1236 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1237 match node.kind_id().into() {
1238 // Each `variable_assignment` is one assignment regardless of
1239 // operator (`=`, `+=`, `-=`, …) — counting the parent node
1240 // avoids double-counting `Bash::EQ`, which is also produced
1241 // for the `=` inside `[ a = b ]` test expressions.
1242 Bash::VariableAssignment | Bash::VariableAssignment2 => {
1243 stats.assignments += 1.;
1244 }
1245 // Every command invocation is a branch in the ABC sense
1246 // (function-call / message-pass). `return` and `exit` builtins
1247 // are also `Bash::Command` nodes and count here too.
1248 Bash::Command => {
1249 stats.branches += 1.;
1250 }
1251 // Comparison operators inside `[[ … ]]` and `(( … ))`, plus
1252 // the prefix test operators `-z`, `-n`, `-eq`, `-lt`, … which
1253 // the grammar emits as `Bash::TestOperator`.
1254 Bash::EQEQ
1255 | Bash::BANGEQ
1256 | Bash::LT
1257 | Bash::GT
1258 | Bash::LTEQ
1259 | Bash::GTEQ
1260 | Bash::EQTILDE
1261 | Bash::TestOperator => {
1262 stats.conditions += 1.;
1263 }
1264 _ => {}
1265 }
1266 }
1267}
1268
1269// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
1270// Source: https://www.softwarerenovation.com/Articles.aspx
1271// ABC Java rules: (page 8, figure 4)
1272// ABC Java example: (page 15, listing 4)
1273impl Abc for JavaCode {
1274 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1275 use Java::*;
1276
1277 match node.kind_id().into() {
1278 STAREQ | SLASHEQ | PERCENTEQ | DASHEQ | PLUSEQ | LTLTEQ | GTGTEQ | AMPEQ | PIPEEQ
1279 | CARETEQ | GTGTGTEQ | PLUSPLUS | DASHDASH => {
1280 stats.assignments += 1.;
1281 }
1282 FieldDeclaration | LocalVariableDeclaration => {
1283 stats.declaration.push(DeclKind::Var);
1284 }
1285 Final => {
1286 if let Some(DeclKind::Var) = stats.declaration.last() {
1287 stats.declaration.push(DeclKind::Const);
1288 }
1289 }
1290 SEMI => {
1291 if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
1292 stats.declaration.clear();
1293 }
1294 }
1295 // Excludes constant declarations
1296 EQ if stats
1297 .declaration
1298 .last()
1299 .is_none_or(|decl| matches!(decl, DeclKind::Var)) =>
1300 {
1301 stats.assignments += 1.;
1302 }
1303 MethodInvocation | New => {
1304 stats.branches += 1.;
1305 }
1306 GTEQ | LTEQ | EQEQ | BANGEQ | Else | Case | Default | QMARK | Try | Catch => {
1307 stats.conditions += 1.;
1308 }
1309 GT | LT => {
1310 // Excludes `<` and `>` used for generic types
1311 if let Some(parent) = node.parent()
1312 && !matches!(parent.kind_id().into(), TypeArguments)
1313 {
1314 stats.conditions += 1.;
1315 }
1316 }
1317 // Counts unary conditions in elements separated by `&&` or `||` boolean operators
1318 AMPAMP | PIPEPIPE => {
1319 if let Some(parent) = node.parent() {
1320 java_count_unary_conditions(&parent, &mut stats.conditions);
1321 }
1322 }
1323 // Counts unary conditions among method arguments
1324 ArgumentList => {
1325 java_count_unary_conditions(node, &mut stats.conditions);
1326 }
1327 // Counts unary conditions inside assignments
1328 VariableDeclarator | AssignmentExpression => {
1329 // The child node of index 2 contains the right operand of an assignment operation
1330 if let Some(right_operand) = node.child(2)
1331 && matches!(
1332 right_operand.kind_id().into(),
1333 ParenthesizedExpression | UnaryExpression
1334 )
1335 {
1336 java_inspect_container(&right_operand, &mut stats.conditions);
1337 }
1338 }
1339 // Counts unary conditions inside if and while statements
1340 IfStatement | WhileStatement => {
1341 // The child node of index 1 contains the condition
1342 if let Some(condition) = node.child(1)
1343 && matches!(condition.kind_id().into(), ParenthesizedExpression)
1344 {
1345 java_inspect_container(&condition, &mut stats.conditions);
1346 }
1347 }
1348 // Counts unary conditions do-while statements
1349 DoStatement => {
1350 // The child node of index 3 contains the condition
1351 if let Some(condition) = node.child(3)
1352 && matches!(condition.kind_id().into(), ParenthesizedExpression)
1353 {
1354 java_inspect_container(&condition, &mut stats.conditions);
1355 }
1356 }
1357 // Counts unary conditions inside for statements
1358 ForStatement => {
1359 // The child node of index 3 contains the `condition` when
1360 // the initialization expression is a variable declaration
1361 // e.g. `for ( int i=0; `condition`; ... ) {}`
1362 if let Some(condition) = node.child(3) {
1363 match condition.kind_id().into() {
1364 SEMI => {
1365 // The child node of index 4 contains the `condition` when
1366 // the initialization expression is not a variable declaration
1367 // e.g. `for ( i=0; `condition`; ... ) {}`
1368 if let Some(cond) = node.child(4) {
1369 match cond.kind_id().into() {
1370 MethodInvocation | Identifier | True | False | SEMI
1371 | RPAREN => {
1372 stats.conditions += 1.;
1373 }
1374 ParenthesizedExpression | UnaryExpression => {
1375 java_inspect_container(&cond, &mut stats.conditions);
1376 }
1377 _ => {}
1378 }
1379 }
1380 }
1381 MethodInvocation | Identifier | True | False => {
1382 stats.conditions += 1.;
1383 }
1384 ParenthesizedExpression | UnaryExpression => {
1385 java_inspect_container(&condition, &mut stats.conditions);
1386 }
1387 _ => {}
1388 }
1389 }
1390 }
1391 // Counts unary conditions inside return statements
1392 ReturnStatement => {
1393 // The child node of index 1 contains the return value
1394 if let Some(value) = node.child(1)
1395 && matches!(
1396 value.kind_id().into(),
1397 ParenthesizedExpression | UnaryExpression
1398 )
1399 {
1400 java_inspect_container(&value, &mut stats.conditions);
1401 }
1402 }
1403 // Counts unary conditions inside implicit return statements in lambda expressions
1404 LambdaExpression => {
1405 // The child node of index 2 contains the return value
1406 if let Some(value) = node.child(2)
1407 && matches!(
1408 value.kind_id().into(),
1409 ParenthesizedExpression | UnaryExpression
1410 )
1411 {
1412 java_inspect_container(&value, &mut stats.conditions);
1413 }
1414 }
1415 // Counts unary conditions inside ternary expressions
1416 TernaryExpression => {
1417 // The child node of index 0 contains the condition
1418 if let Some(condition) = node.child(0) {
1419 match condition.kind_id().into() {
1420 MethodInvocation | Identifier | True | False => {
1421 stats.conditions += 1.;
1422 }
1423 ParenthesizedExpression | UnaryExpression => {
1424 java_inspect_container(&condition, &mut stats.conditions);
1425 }
1426 _ => {}
1427 }
1428 }
1429 // The child node of index 2 contains the first expression
1430 if let Some(expression) = node.child(2)
1431 && matches!(
1432 expression.kind_id().into(),
1433 ParenthesizedExpression | UnaryExpression
1434 )
1435 {
1436 java_inspect_container(&expression, &mut stats.conditions);
1437 }
1438 // The child node of index 4 contains the second expression
1439 if let Some(expression) = node.child(4)
1440 && matches!(
1441 expression.kind_id().into(),
1442 ParenthesizedExpression | UnaryExpression
1443 )
1444 {
1445 java_inspect_container(&expression, &mut stats.conditions);
1446 }
1447 }
1448 _ => {}
1449 }
1450 }
1451}
1452
1453// Groovy's ABC mirrors Java's directly because the dekobon Groovy
1454// grammar shares Java's expression / statement vocabulary for the
1455// shapes ABC cares about (assignments, branches, conditions).
1456// Groovy-specific touches over Java:
1457// - `CommandChain` (parens-less calls like `println foo`) counts as
1458// a branch alongside `MethodInvocation` (#247).
1459// - `DoWhileStatement` (the new grammar's name for `do { } while`)
1460// replaces the prior `DoStatement` (which the amaanq grammar used).
1461// - Closures (`{ x -> ... }`) have block bodies — no implicit-return
1462// "single-expression arm" like a Java lambda, so the prior
1463// `LambdaExpression` arm is intentionally absent.
1464// - The dekobon grammar inlines the parens of `if (…)` / `while (…)`
1465// / `do { … } while (…)` as `(` / `)` token children rather than
1466// wrapping the condition in a `parenthesized_expression`, so the
1467// condition is at a different child index than under Java's
1468// grammar and must be inspected differently.
1469impl Abc for GroovyCode {
1470 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1471 use Groovy::*;
1472
1473 match node.kind_id().into() {
1474 STAREQ | SLASHEQ | PERCENTEQ | DASHEQ | PLUSEQ | LTLTEQ | GTGTEQ | AMPEQ | PIPEEQ
1475 | CARETEQ | GTGTGTEQ | PLUSPLUS | DASHDASH => {
1476 stats.assignments += 1.;
1477 }
1478 FieldDeclaration | LocalVariableDeclaration => {
1479 stats.declaration.push(DeclKind::Var);
1480 }
1481 Final => {
1482 if let Some(DeclKind::Var) = stats.declaration.last() {
1483 stats.declaration.push(DeclKind::Const);
1484 }
1485 }
1486 SEMI => {
1487 if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
1488 stats.declaration.clear();
1489 }
1490 }
1491 EQ if stats
1492 .declaration
1493 .last()
1494 .is_none_or(|decl| matches!(decl, DeclKind::Var)) =>
1495 {
1496 stats.assignments += 1.;
1497 }
1498 MethodInvocation | CommandChain | New => {
1499 stats.branches += 1.;
1500 }
1501 GTEQ | LTEQ | EQEQ | BANGEQ | Else | Case | Default | QMARK | Try | Catch => {
1502 stats.conditions += 1.;
1503 }
1504 GT | LT => {
1505 // Excludes `<` / `>` used for generic types (e.g.
1506 // `List<String>`).
1507 if let Some(parent) = node.parent()
1508 && !matches!(parent.kind_id().into(), TypeArguments)
1509 {
1510 stats.conditions += 1.;
1511 }
1512 }
1513 AMPAMP | PIPEPIPE => {
1514 if let Some(parent) = node.parent() {
1515 groovy_count_unary_conditions(&parent, &mut stats.conditions);
1516 }
1517 }
1518 ArgumentList => {
1519 groovy_count_unary_conditions(node, &mut stats.conditions);
1520 }
1521 VariableDeclarator | AssignmentExpression => {
1522 if let Some(right_operand) = node.child(2)
1523 && matches!(
1524 right_operand.kind_id().into(),
1525 ParenthesizedExpression | UnaryExpression
1526 )
1527 {
1528 groovy_inspect_container(&right_operand, &mut stats.conditions);
1529 }
1530 }
1531 IfStatement | WhileStatement => {
1532 // dekobon `if_statement` / `while_statement` shape:
1533 // [keyword, `(`, condition, `)`, body, …]. Condition
1534 // lives at child index 2 (not 1 as under tree-sitter-
1535 // java, where the parens come wrapped in a
1536 // `parenthesized_expression`).
1537 if let Some(condition) = node.child(2) {
1538 groovy_count_condition(&condition, &mut stats.conditions);
1539 }
1540 }
1541 DoWhileStatement => {
1542 // dekobon shape: [`do`, body, `while`, `(`, condition,
1543 // `)`]. Condition is at child index 4.
1544 if let Some(condition) = node.child(4) {
1545 groovy_count_condition(&condition, &mut stats.conditions);
1546 }
1547 }
1548 ForStatement => {
1549 // Two shapes: a present condition lives at child(3);
1550 // an empty condition shows up as a bare `SEMI` token at
1551 // child(3) with the next slot (child(4)) holding either
1552 // the update expression or `;`/`)` for `for(;;)`-style
1553 // empty-condition loops, which still count as a single
1554 // condition slot.
1555 if let Some(condition) = node.child(3) {
1556 if matches!(condition.kind_id().into(), SEMI) {
1557 if let Some(cond) = node.child(4) {
1558 if matches!(cond.kind_id().into(), SEMI | RPAREN) {
1559 stats.conditions += 1.;
1560 } else {
1561 groovy_count_condition(&cond, &mut stats.conditions);
1562 }
1563 }
1564 } else {
1565 groovy_count_condition(&condition, &mut stats.conditions);
1566 }
1567 }
1568 }
1569 ReturnStatement => {
1570 if let Some(value) = node.child(1)
1571 && matches!(
1572 value.kind_id().into(),
1573 ParenthesizedExpression | UnaryExpression
1574 )
1575 {
1576 groovy_inspect_container(&value, &mut stats.conditions);
1577 }
1578 }
1579 TernaryExpression => {
1580 if let Some(condition) = node.child(0) {
1581 groovy_count_condition(&condition, &mut stats.conditions);
1582 }
1583 for branch_idx in [2, 4] {
1584 if let Some(expression) = node.child(branch_idx)
1585 && matches!(
1586 expression.kind_id().into(),
1587 ParenthesizedExpression | UnaryExpression
1588 )
1589 {
1590 groovy_inspect_container(&expression, &mut stats.conditions);
1591 }
1592 }
1593 }
1594 _ => {}
1595 }
1596 }
1597}
1598
1599// Counts a single boolean-context condition expression for the dekobon
1600// Groovy grammar. The grammar inlines `(` / `)` as token children of
1601// `if_statement` / `while_statement` / `do_while_statement` rather than
1602// wrapping the condition in a `parenthesized_expression`, so the
1603// condition shows up bare. A bare identifier / boolean / call / command
1604// chain contributes one condition directly; parenthesised and unary
1605// containers delegate to `groovy_inspect_container`; binary / ternary
1606// expressions are picked up by their own arms.
1607fn groovy_count_condition(condition: &Node, conditions: &mut f64) {
1608 use Groovy::*;
1609 match condition.kind_id().into() {
1610 MethodInvocation | CommandChain | Identifier | True | False => {
1611 *conditions += 1.;
1612 }
1613 ParenthesizedExpression | UnaryExpression => {
1614 groovy_inspect_container(condition, conditions);
1615 }
1616 _ => {}
1617 }
1618}
1619
1620impl Abc for CsharpCode {
1621 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1622 use Csharp::*;
1623
1624 match node.kind_id().into() {
1625 STAREQ | SLASHEQ | PERCENTEQ | DASHEQ | PLUSEQ | LTLTEQ | GTGTEQ | GTGTGTEQ | AMPEQ
1626 | PIPEEQ | CARETEQ | QMARKQMARKEQ | PLUSPLUS | DASHDASH => {
1627 stats.assignments += 1.;
1628 }
1629 FieldDeclaration | LocalDeclarationStatement => {
1630 stats.declaration.push(DeclKind::Var);
1631 }
1632 // C# `const` modifier marks a compile-time constant — exclude
1633 // its initializer from the assignment count (matches Java's
1634 // treatment of `final`).
1635 Const => {
1636 if let Some(DeclKind::Var) = stats.declaration.last() {
1637 stats.declaration.push(DeclKind::Const);
1638 }
1639 }
1640 SEMI => {
1641 if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
1642 stats.declaration.clear();
1643 }
1644 }
1645 // Count `=` as an assignment unless it's the initializer of a
1646 // `const` declaration (those are constant bindings, not mutable
1647 // assignments). `None` means we're outside any declaration —
1648 // still count.
1649 EQ if !matches!(stats.declaration.last(), Some(DeclKind::Const)) => {
1650 stats.assignments += 1.;
1651 }
1652 crate::Csharp::InvocationExpression
1653 | crate::Csharp::InvocationExpression2
1654 | crate::Csharp::InvocationExpression3
1655 | ObjectCreationExpression => {
1656 stats.branches += 1.;
1657 }
1658 GTEQ | LTEQ | EQEQ | BANGEQ | Else | Case | Default | QMARK | Try | Catch => {
1659 stats.conditions += 1.;
1660 }
1661 GT | LT => {
1662 // Excludes `<` and `>` used as type-syntax delimiters:
1663 // generic type arguments (`Dictionary<K, V>`), type
1664 // parameter declarations (`class Foo<T> { }`), and the
1665 // parameter-list delimiters of unsafe function-pointer
1666 // types (`delegate*<int, int>`).
1667 if let Some(parent) = node.parent()
1668 && !matches!(
1669 parent.kind_id().into(),
1670 TypeArgumentList | TypeParameterList | FunctionPointerType
1671 )
1672 {
1673 stats.conditions += 1.;
1674 }
1675 }
1676 AMPAMP | PIPEPIPE => {
1677 if let Some(parent) = node.parent() {
1678 csharp_count_unary_conditions(&parent, &mut stats.conditions);
1679 }
1680 }
1681 ArgumentList => {
1682 csharp_count_unary_conditions(node, &mut stats.conditions);
1683 }
1684 crate::Csharp::VariableDeclarator
1685 | crate::Csharp::VariableDeclarator2
1686 | AssignmentExpression => {
1687 // Child 2 is the RHS of `lhs = rhs`.
1688 inspect_csharp_child(node, 2, &mut stats.conditions);
1689 }
1690 IfStatement | WhileStatement => {
1691 // Child 1 is the parenthesised condition: `if (cond) ...`.
1692 if let Some(condition) = node.child(1)
1693 && matches!(condition.kind_id().into(), csharp_paren_expr_kinds!())
1694 {
1695 csharp_inspect_container(&condition, &mut stats.conditions);
1696 }
1697 }
1698 DoStatement => {
1699 // `do { ... } while (cond);` — condition sits at child 3
1700 // (children: `do`, body, `while`, `(cond)`, `;`).
1701 if let Some(condition) = node.child(3)
1702 && matches!(condition.kind_id().into(), csharp_paren_expr_kinds!())
1703 {
1704 csharp_inspect_container(&condition, &mut stats.conditions);
1705 }
1706 }
1707 ReturnStatement => {
1708 // Child 1 is the returned expression (child 0 is `return`).
1709 inspect_csharp_child(node, 1, &mut stats.conditions);
1710 }
1711 LambdaExpression => {
1712 // Child 2 is the lambda body for `params => body`.
1713 inspect_csharp_child(node, 2, &mut stats.conditions);
1714 }
1715 ConditionalExpression => {
1716 // `cond ? a : b` — children are [cond, ?, a, :, b].
1717 if let Some(condition) = node.child(0) {
1718 match condition.kind_id().into() {
1719 crate::Csharp::InvocationExpression
1720 | crate::Csharp::InvocationExpression2
1721 | crate::Csharp::InvocationExpression3
1722 | Identifier
1723 | True
1724 | False => {
1725 stats.conditions += 1.;
1726 }
1727 crate::Csharp::ParenthesizedExpression
1728 | crate::Csharp::ParenthesizedExpression2
1729 | crate::Csharp::ParenthesizedExpression3
1730 | crate::Csharp::PrefixUnaryExpression
1731 | crate::Csharp::PrefixUnaryExpression2 => {
1732 csharp_inspect_container(&condition, &mut stats.conditions);
1733 }
1734 _ => {}
1735 }
1736 }
1737 inspect_csharp_child(node, 2, &mut stats.conditions);
1738 inspect_csharp_child(node, 4, &mut stats.conditions);
1739 }
1740 // Counts unary / single-token conditions inside `for`
1741 // statements. The C# grammar exposes the loop condition via
1742 // the named `condition` field on `for_statement`, so we look
1743 // it up by name rather than positional index (Java's arm
1744 // relies on positional indices because its grammar does not
1745 // name the field). Comparison-operator conditions like
1746 // `i < n` are still counted by the standard `GT | LT | ...`
1747 // arms — this arm only fires when the condition is a bare
1748 // identifier, invocation, boolean literal, parenthesised
1749 // expression, or `!`-prefixed unary expression.
1750 ForStatement => {
1751 if let Some(condition) = node.child_by_field_name("condition") {
1752 let kind = condition.kind_id().into();
1753 if matches!(kind, csharp_invocation_expr_kinds!())
1754 || matches!(kind, Identifier | BooleanLiteral)
1755 {
1756 stats.conditions += 1.;
1757 } else if matches!(kind, csharp_paren_expr_kinds!())
1758 || matches!(kind, csharp_prefix_unary_expr_kinds!())
1759 {
1760 csharp_inspect_container(&condition, &mut stats.conditions);
1761 }
1762 }
1763 }
1764 _ => {}
1765 }
1766 }
1767}
1768
1769impl Abc for ElixirCode {
1770 // Elixir's pattern-match `=` is a `BinaryOperator` whose middle
1771 // child is an `EQ` token. The same wrapper node also hosts `+=`-
1772 // style augmented assignments, but Elixir is purely functional —
1773 // augmented assignment does not exist in the grammar; `EQ` is the
1774 // only assignment-shaped operator. `|>` (`PIPEGT`) is a
1775 // BinaryOperator too but its operator token differs, so the EQ
1776 // child check is what filters assignments from pipelines and from
1777 // comparison operators that share the wrapper.
1778 //
1779 // Branches cover `|>` (the pipe operator dispatches one call per
1780 // step) and every `Call` node (function / method / macro
1781 // invocation). `RemoteCallWithParentheses` and `LocalCallWith*`
1782 // variants are subordinate nodes to `Call`, so the single `Call`
1783 // match captures every dispatch site.
1784 //
1785 // Conditions cover `when` (guard token `Elixir::When`), the six
1786 // comparison operator tokens (`==`, `===`, `!=`, `!==`, `<`, `>`,
1787 // `<=`, `>=`), and the keyword-shaped `Call`s that introduce a
1788 // decision point (`if`, `unless`, `case`, `cond`, `with`).
1789 // `for` / `while` are looping forms — not condition-shaped per
1790 // the issue body's literal list — so we omit them.
1791 //
1792 // Limitations:
1793 // - `case` is counted once on the container, not once per arm
1794 // (`stab_clause`). The issue body says "conditions = case,
1795 // cond, if, with, guard when" — i.e. one condition per
1796 // construct, not per arm. Matches the Rust impl's "MatchExpression
1797 // once" rule.
1798 // - Higher-order calls like `Enum.reduce` are `RemoteCallWithParentheses`
1799 // nodes; they are still `Call` nodes and so contribute one branch
1800 // each, matching the issue's "branches = `|>`, function calls"
1801 // instruction.
1802 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
1803 use Elixir as E;
1804
1805 match node.kind_id().into() {
1806 // A `BinaryOperator` whose operator token is `EQ` is a
1807 // pattern-match assignment. The grammar shape is
1808 // `(left, operator, right)`, so the operator token is
1809 // always at child index 1 — looking it up directly is
1810 // O(1) vs. an `any()` scan of all children. This arm
1811 // fires on every Elixir binary op (comparisons, pipes,
1812 // boolean ops, arithmetic) so the constant-time check
1813 // matters.
1814 E::BinaryOperator | E::BinaryOperator2 | E::BinaryOperator3
1815 if node
1816 .child(1)
1817 .is_some_and(|c| c.kind_id() == E::EQ as u16) =>
1818 {
1819 stats.assignments += 1.;
1820 }
1821 // `|>` pipeline operator: every step in `foo |> bar |> baz`
1822 // is one branch (the pipe dispatches one call per step).
1823 E::PIPEGT => {
1824 stats.branches += 1.;
1825 }
1826 // Every Call (function, method, macro, sigil-call) is one
1827 // branch — `RemoteCallWith*`, `LocalCallWith*`,
1828 // `AnonymousCall`, and `DoubleCall` are all subordinate
1829 // node kinds underneath the top-level `Call` wrapper, so
1830 // matching `Call` alone captures every dispatch site.
1831 //
1832 // Method-defining macros (`def`/`defp`/`defmacro`/`defmacrop`)
1833 // and module/struct/protocol declarations (`defmodule`/
1834 // `defstruct`/`defprotocol`/`defimpl`) are *not* runtime
1835 // dispatch and must not inflate `branches` — they parse as
1836 // `Call` nodes because Elixir's grammar uses the same
1837 // shape for all keyword-introduced forms. Aliasing/import
1838 // directives (`alias`, `import`, `require`, `use`) are
1839 // similarly declarative and excluded.
1840 //
1841 // Cognitive's `elixir_call_keyword` lookup is reused to
1842 // identify the target keyword. Note: Cognitive only acts
1843 // on a subset of these keywords (the four method-definers
1844 // for nesting reset, plus the 7 control-flow keywords for
1845 // +nesting); Abc's broader filter additionally drops the
1846 // module/struct/protocol declarators and aliasing
1847 // directives that Cognitive ignores entirely. Filter sets
1848 // are intentionally different — both impls use the same
1849 // helper to look up the keyword, but apply different
1850 // policies on top.
1851 E::Call => {
1852 let keyword = super::cognitive::elixir_call_keyword(node, code);
1853 let is_definition_or_directive = matches!(
1854 keyword,
1855 Some(
1856 "def" | "defp" | "defmacro" | "defmacrop"
1857 | "defmodule" | "defstruct" | "defprotocol" | "defimpl"
1858 | "alias" | "import" | "require" | "use"
1859 )
1860 );
1861 if !is_definition_or_directive {
1862 stats.branches += 1.;
1863 }
1864 // Keyword-shaped control-flow Calls also contribute
1865 // one condition.
1866 if matches!(keyword, Some("if" | "unless" | "case" | "cond" | "with")) {
1867 stats.conditions += 1.;
1868 }
1869 }
1870 // Comparison operator tokens. `Elixir::LT` / `Elixir::GT`
1871 // are unambiguously comparison ops here — unlike Go's
1872 // generic-instantiation `<` / `>`, Elixir has no type
1873 // parameter brackets that share the token.
1874 E::EQEQ | E::EQEQEQ | E::BANGEQ | E::BANGEQEQ
1875 | E::LT | E::GT | E::LTEQ | E::GTEQ
1876 // Guard `when` token: introduces the guard clause of a
1877 // function head or `case` arm.
1878 | E::When => {
1879 stats.conditions += 1.;
1880 }
1881 _ => {}
1882 }
1883 }
1884}
1885
1886// Fitzpatrick's ABC rules adapted for Perl.
1887//
1888// - Assignments: every assignment operator token — plain `=` plus the
1889// compound forms `+=`, `-=`, `*=`, `/=`, `%=`, `**=`, `.=`, `x=`,
1890// `&=`, `|=`, `^=`, `<<=`, `>>=`, `&&=`, `||=`, `//=`, and the
1891// bitstring forms `&.=`, `|.=`, `^.=`. Each token fires exactly
1892// once per textual occurrence inside a `binary_expression`.
1893// - Branches: every call expression dispatch — `call_expression_with_*`
1894// (bareword / spaced args / args-with-brackets / sub / variable /
1895// recursive) plus `method_invocation`. The grammar nests an inner
1896// `call_expression_with_bareword` (just the function name)
1897// underneath the wrapper kinds carrying argument lists, so we only
1898// count `CallExpressionWithBareword` when it stands on its own;
1899// when its parent is another call form, the outer wrapper has
1900// already contributed the branch.
1901// - Conditions: numeric and string comparison operators (`==`, `!=`,
1902// `<`, `>`, `<=`, `>=`, `<=>`, `eq`, `ne`, `lt`, `gt`, `le`, `ge`,
1903// `cmp`, `=~`, `!~`), short-circuit / logical operators (`&&`,
1904// `||`, `//`, `and`, `or`, `xor`), the ternary operator
1905// (`TernaryExpression`), and each `elsif` / `else` clause of an
1906// `if` / `unless` statement. Bare predicates that have no
1907// comparison (e.g. `if ($x)`) are not separately counted; we let
1908// the comparison tokens carry the metric, mirroring the Bash /
1909// Python token-level approach.
1910impl Abc for PerlCode {
1911 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1912 use Perl as P;
1913
1914 match node.kind_id().into() {
1915 // Plain `=` and every compound assignment operator. The
1916 // grammar tokenises each operator separately, so one
1917 // textual `+=` produces exactly one token and there is no
1918 // double-counting via a wrapper.
1919 P::EQ
1920 | P::PLUSEQ
1921 | P::DASHEQ
1922 | P::STAREQ
1923 | P::SLASHEQ
1924 | P::PERCENTEQ
1925 | P::STARSTAREQ
1926 | P::DOTEQ
1927 | P::XEQ
1928 | P::AMPEQ
1929 | P::PIPEEQ
1930 | P::CARETEQ
1931 | P::LTLTEQ
1932 | P::GTGTEQ
1933 | P::AMPAMPEQ
1934 | P::PIPEPIPEEQ
1935 | P::SLASHSLASHEQ
1936 | P::AMPDOTEQ
1937 | P::PIPEDOTEQ
1938 | P::CARETDOTEQ => {
1939 stats.assignments += 1.;
1940 }
1941 // Argument-bearing call wrappers always count.
1942 P::CallExpressionWithSpacedArgs
1943 | P::CallExpressionWithSub
1944 | P::CallExpressionWithArgsWithBrackets
1945 | P::CallExpressionWithVariable
1946 | P::CallExpressionRecursive
1947 | P::MethodInvocation => {
1948 stats.branches += 1.;
1949 }
1950 // Bareword-only call (`shift`, `time`, …) — count only
1951 // when this node is the outermost dispatch site. When the
1952 // bareword sits inside one of the wrappers above, the
1953 // outer node has already been counted and this child
1954 // would double the branch tally.
1955 P::CallExpressionWithBareword
1956 if !node.parent().is_some_and(|p| {
1957 matches!(
1958 p.kind_id().into(),
1959 P::CallExpressionWithSpacedArgs
1960 | P::CallExpressionWithSub
1961 | P::CallExpressionWithArgsWithBrackets
1962 | P::CallExpressionWithVariable
1963 | P::CallExpressionRecursive
1964 )
1965 }) =>
1966 {
1967 stats.branches += 1.;
1968 }
1969 // Numeric, string, and pattern-match comparison operators
1970 // plus the spaceship / `cmp` three-way comparisons.
1971 P::EQEQ | P::BANGEQ | P::LT | P::GT | P::LTEQ | P::GTEQ | P::LTEQGT
1972 | P::Eq | P::Ne | P::Lt | P::Gt | P::Le | P::Ge | P::Cmp
1973 | P::EQTILDE | P::BANGTILDE
1974 // Short-circuit / logical operators (high- and low-
1975 // precedence forms).
1976 | P::AMPAMP | P::PIPEPIPE | P::SLASHSLASH
1977 | P::And | P::Or | P::Xor
1978 // Ternary `a ? b : c` and each `elsif` / `else` clause of
1979 // an `if` / `unless` chain.
1980 | P::TernaryExpression
1981 | P::ElsifClause
1982 | P::ElseClause => {
1983 stats.conditions += 1.;
1984 }
1985 _ => {}
1986 }
1987 }
1988}
1989
1990// Fitzpatrick's ABC rules adapted for Lua.
1991//
1992// - Assignments: every `assignment_statement` node. Lua has no
1993// compound assignment operators (`+=` and friends do not exist in
1994// the grammar), so the wrapper kind is the sole assignment node
1995// and there is no per-operator alternative to track. `local x = 1`
1996// wraps an `assignment_statement` under a `variable_declaration`,
1997// so initialisers count the same as later mutations.
1998// - Branches: every `function_call`. The Lua grammar collapses
1999// `obj.method(args)`, `obj:method(args)`, and `f(args)` into the
2000// same `function_call` node, so one arm covers all dispatch forms.
2001// - Conditions: comparison operators (`==`, `~=`, `<`, `>`, `<=`,
2002// `>=`), short-circuit operators (`and`, `or`), each elseif / else
2003// arm of an `if`. Lua has no ternary operator (`cond and a or b`
2004// is the idiom, captured by the `and` / `or` tokens above).
2005impl Abc for LuaCode {
2006 fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
2007 match node.kind_id().into() {
2008 Lua::AssignmentStatement | Lua::AssignmentStatement2 => {
2009 stats.assignments += 1.;
2010 }
2011 Lua::FunctionCall => {
2012 stats.branches += 1.;
2013 }
2014 Lua::EQEQ
2015 | Lua::TILDEEQ
2016 | Lua::LT
2017 | Lua::GT
2018 | Lua::LTEQ
2019 | Lua::GTEQ
2020 | Lua::And
2021 | Lua::Or
2022 | Lua::ElseifStatement
2023 | Lua::ElseStatement => {
2024 stats.conditions += 1.;
2025 }
2026 _ => {}
2027 }
2028 }
2029}
2030
2031// Names of Tcl commands that mutate a variable. Each invocation of
2032// one of these commands counts as an assignment, not a branch — the
2033// command is acting as an assignment operator, not as a generic
2034// dispatch. The list is intentionally narrow: only commands that
2035// every Tcl programmer recognises as primary mutators. Less-common
2036// mutators (`dict set`, `array set`, `lset`, `regsub … name`) are
2037// left as branches; treating them as assignments would require
2038// inspecting the command's second word, and the additional
2039// fidelity is not worth the complexity for the ABC magnitude.
2040const TCL_ASSIGNMENT_COMMANDS: &[&[u8]] = &[b"incr", b"append", b"lappend"];
2041
2042// Fitzpatrick's ABC rules adapted for Tcl.
2043//
2044// - Assignments: every `set` production (`set name value`) plus
2045// every `command` whose first word is one of the recognised
2046// mutator commands in `TCL_ASSIGNMENT_COMMANDS`. Tcl has no
2047// assignment operators — variable mutation is always a command
2048// invocation, so we filter on the command name. The `set` form
2049// has its own grammar production (`Tcl::Set`) and counts directly
2050// without any source-text inspection.
2051// - Branches: every other `command` node. Like Bash, `return` and
2052// `error` builtins parse as plain `command` nodes and count here
2053// too — Tcl treats every dispatch the same regardless of whether
2054// the command is a procedure call, a control-flow primitive, or a
2055// builtin. The grammar productions for `if`, `while`, `foreach`,
2056// etc. live separately from `command` and do not double-count.
2057// - Conditions: numeric (`==`, `!=`, `<`, `>`, `<=`, `>=`) and
2058// string (`eq`, `ne`, `in`, `ni`) comparison tokens, short-circuit
2059// operators (`&&`, `||`), the ternary expression production, and
2060// each `elseif` / `else` clause of an `if`.
2061impl Abc for TclCode {
2062 fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
2063 match node.kind_id().into() {
2064 // The `set` production wraps `set name value` as a
2065 // first-class node distinct from generic commands.
2066 Tcl::Set => {
2067 stats.assignments += 1.;
2068 }
2069 // Generic command: branch by default, assignment when
2070 // the first word names a known mutator. The first word
2071 // can be either a `simple_word` or a wrapped form; both
2072 // surface their literal text via `utf8_text`.
2073 Tcl::Command => {
2074 if tcl_command_is_assignment(node, code) {
2075 stats.assignments += 1.;
2076 } else {
2077 stats.branches += 1.;
2078 }
2079 }
2080 Tcl::EQEQ
2081 | Tcl::BANGEQ
2082 | Tcl::LT
2083 | Tcl::GT
2084 | Tcl::LTEQ
2085 | Tcl::GTEQ
2086 | Tcl::Eq
2087 | Tcl::Ne
2088 | Tcl::In
2089 | Tcl::Ni
2090 | Tcl::AMPAMP
2091 | Tcl::PIPEPIPE
2092 | Tcl::TernaryExpr
2093 | Tcl::Elseif
2094 | Tcl::Else => {
2095 stats.conditions += 1.;
2096 }
2097 _ => {}
2098 }
2099 }
2100}
2101
2102// Returns true when the `command` node's first word is one of the
2103// recognised Tcl assignment commands. The first word is the leftmost
2104// non-comment child; we slice the source bytes directly using the
2105// child node's byte range, which is robust to `simple_word` wrappers
2106// and avoids depending on a particular grammar shape.
2107fn tcl_command_is_assignment(node: &Node, code: &[u8]) -> bool {
2108 let Some(first) = node.child(0) else {
2109 return false;
2110 };
2111 let start = first.start_byte();
2112 let end = first.end_byte();
2113 if end > code.len() || start >= end {
2114 return false;
2115 }
2116 let word = &code[start..end];
2117 TCL_ASSIGNMENT_COMMANDS.contains(&word)
2118}
2119
2120// Shared helper: if `node.child(idx)` is a parenthesised or `!`-prefixed
2121// expression, descend into it to count any unary boolean condition.
2122// Used by every C# Abc match arm whose condition sits at a known child
2123// index (assignments, returns, lambdas, ternaries).
2124fn inspect_csharp_child(node: &Node, idx: usize, conditions: &mut f64) {
2125 if let Some(child) = node.child(idx)
2126 && matches!(
2127 child.kind_id().into(),
2128 crate::Csharp::ParenthesizedExpression
2129 | crate::Csharp::ParenthesizedExpression2
2130 | crate::Csharp::ParenthesizedExpression3
2131 | crate::Csharp::PrefixUnaryExpression
2132 | crate::Csharp::PrefixUnaryExpression2
2133 )
2134 {
2135 csharp_inspect_container(&child, conditions);
2136 }
2137}
2138
2139#[cfg(test)]
2140#[allow(
2141 clippy::float_cmp,
2142 clippy::cast_precision_loss,
2143 clippy::cast_possible_truncation,
2144 clippy::cast_sign_loss,
2145 clippy::similar_names,
2146 clippy::doc_markdown,
2147 clippy::needless_raw_string_hashes,
2148 clippy::too_many_lines
2149)]
2150mod tests {
2151 use crate::tools::check_metrics;
2152
2153 use super::*;
2154
2155 /// Regression for #227: a `Stats::default()` that never sees an
2156 /// observation must not leak the `f64::MAX` sentinel for
2157 /// `assignments_min`, `branches_min`, or `conditions_min`. All
2158 /// three getters collapse the sentinel to `0.0` so JSON never
2159 /// emits `1.7976931e308`.
2160 #[test]
2161 fn abc_empty_file_min_is_zero() {
2162 let stats = Stats::default();
2163 assert_eq!(stats.assignments_min(), 0.0);
2164 assert_eq!(stats.branches_min(), 0.0);
2165 assert_eq!(stats.conditions_min(), 0.0);
2166 }
2167
2168 // Regression test for the `EQ` arm guard in `JavaCode::compute`:
2169 // the rewrite from `.map().unwrap_or_else()` to
2170 // `is_none_or(|decl| matches!(decl, DeclKind::Var))` must preserve
2171 // the three-way truth table — None → ++, Some(Var) → ++,
2172 // Some(Const) → no-op.
2173 #[test]
2174 fn java_eq_arm_increments_when_declaration_stack_is_empty() {
2175 // No surrounding `int x = ...` / `Final` token → declaration
2176 // stack is empty when the `EQ` token is visited, so the None
2177 // branch must increment `assignments`.
2178 check_metrics::<JavaParser>(
2179 "class A { void m() { int x = 0; x = 1; x = 2; x = 3; } }",
2180 "foo.java",
2181 |metric| {
2182 // `int x = 0;` adds 1 (Some(Var) branch),
2183 // each subsequent `x = N;` adds 1 (None branch).
2184 assert_eq!(metric.abc.assignments_sum(), 4.0);
2185 },
2186 );
2187 }
2188
2189 #[test]
2190 fn java_eq_arm_skips_when_declaration_stack_top_is_const() {
2191 // `final` pushes `DeclKind::Const` on top of the active `Var`
2192 // entry, so the Some(non-Var) branch must skip the increment.
2193 check_metrics::<JavaParser>(
2194 "class A {
2195 final int X = 1;
2196 final int Y = 2;
2197 void m() { final int Z = 3; }
2198 }",
2199 "foo.java",
2200 |metric| {
2201 // All three `=` tokens land under a `Const` top, so
2202 // assignments should be 0 across all spaces.
2203 assert_eq!(metric.abc.assignments_sum(), 0.0);
2204 },
2205 );
2206 }
2207
2208 // Constant declarations are not counted as assignments
2209 #[test]
2210 fn java_constant_declarations() {
2211 check_metrics::<JavaParser>(
2212 "class A {
2213 private final int X1 = 0, Y1 = 0;
2214 public final float PI = 3.14f;
2215 final static String HELLO = \"Hello,\";
2216 protected String world = \" world!\"; // +1a
2217 public float e = 2.718f; // +1a
2218 private int x2 = 1, y2 = 2; // +2a
2219
2220 void m() {
2221 final int Z1 = 0, Z2 = 0, Z3 = 0;
2222 final float T = 0.0f;
2223 int z1 = 1, z2 = 2, z3 = 3; // +3a
2224 float t = 60.0f; // +1a
2225 }
2226 }",
2227 "foo.java",
2228 |metric| {
2229 // magnitude: sqrt(64 + 0 + 0) = sqrt(64)
2230 // space count: 3 (1 unit, 1 class and 1 method)
2231 insta::assert_json_snapshot!(
2232 metric.abc,
2233 @r###"
2234 {
2235 "assignments": 8.0,
2236 "branches": 0.0,
2237 "conditions": 0.0,
2238 "magnitude": 8.0,
2239 "assignments_average": 2.6666666666666665,
2240 "branches_average": 0.0,
2241 "conditions_average": 0.0,
2242 "assignments_min": 0.0,
2243 "assignments_max": 4.0,
2244 "branches_min": 0.0,
2245 "branches_max": 0.0,
2246 "conditions_min": 0.0,
2247 "conditions_max": 0.0
2248 }"###
2249 );
2250 },
2251 );
2252 }
2253
2254 // "In computer science, conditionals (that is, conditional statements, conditional expressions
2255 // and conditional constructs,) are programming language commands for handling decisions."
2256 // Source: https://en.wikipedia.org/wiki/Conditional_(computer_programming)
2257 // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions
2258 // Variables, method invocations and true or false values used inside
2259 // variable declarations and assignment expressions are not counted as conditions
2260 #[test]
2261 fn java_declarations_with_conditions() {
2262 check_metrics::<JavaParser>(
2263 "
2264 boolean a = (1 > 2); // +1a +1c
2265 boolean b = 3 > 4; // +1a +1c
2266 boolean c = (1 > 2) && 3 > 4; // +1a +2c
2267 boolean d = b && (x > 5) || c; // +1a +3c
2268 boolean e = !d; // +1a +1c
2269 boolean f = ((!false)); // +1a +1c
2270 boolean g = !(!(true)); // +1a +1c
2271 boolean h = true; // +1a
2272 boolean i = (false); // +1a
2273 boolean j = (((((true))))); // +1a
2274 boolean k = (((((m()))))); // +1a +1b
2275 boolean l = (((((!m()))))); // +1a +1b +1c
2276 boolean m = (!(!((m())))); // +1a +1b +1c
2277 List<String> n = null; // +1a (< and > used for generic types are not counted as conditions)
2278 ",
2279 "foo.java",
2280 |metric| {
2281 // magnitude: sqrt(196 + 9 + 144) = sqrt(349)
2282 // space count: 1 (1 unit)
2283 insta::assert_json_snapshot!(
2284 metric.abc,
2285 @r###"
2286 {
2287 "assignments": 14.0,
2288 "branches": 3.0,
2289 "conditions": 12.0,
2290 "magnitude": 18.681541692269406,
2291 "assignments_average": 14.0,
2292 "branches_average": 3.0,
2293 "conditions_average": 12.0,
2294 "assignments_min": 14.0,
2295 "assignments_max": 14.0,
2296 "branches_min": 3.0,
2297 "branches_max": 3.0,
2298 "conditions_min": 12.0,
2299 "conditions_max": 12.0
2300 }"###
2301 );
2302 },
2303 );
2304 }
2305
2306 // Conditions can be found in assignment expressions
2307 #[test]
2308 fn java_assignments_with_conditions() {
2309 check_metrics::<JavaParser>(
2310 "
2311 a = 2 < 1; // +1a +1c
2312 b = (4 >= 3) && 2 <= 1; // +1a +2c
2313 c = a || (x != 10) && b; // +1a +3c
2314 d = !false; // +1a +1c
2315 e = (!false); // +1a +1c
2316 f = !(false); // +1a +1c
2317 g = (!(((true)))); // +1a +1c
2318 h = ((true)); // +1a
2319 i = !m(); // +1a +1b +1c
2320 j = !((m())); // +1a +1b +1c
2321 k = (!(m())); // +1a +1b +1c
2322 l = ((!(m()))); // +1a +1b +1c
2323 m = !B.<Integer>m(2); // +1a +1b +1c
2324 n = !((B.<Integer>m(4))); // +1a +1b +1c
2325 ",
2326 "foo.java",
2327 |metric| {
2328 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
2329 // space count: 1 (1 unit)
2330 insta::assert_json_snapshot!(
2331 metric.abc,
2332 @r###"
2333 {
2334 "assignments": 14.0,
2335 "branches": 6.0,
2336 "conditions": 16.0,
2337 "magnitude": 22.090722034374522,
2338 "assignments_average": 14.0,
2339 "branches_average": 6.0,
2340 "conditions_average": 16.0,
2341 "assignments_min": 14.0,
2342 "assignments_max": 14.0,
2343 "branches_min": 6.0,
2344 "branches_max": 6.0,
2345 "conditions_min": 16.0,
2346 "conditions_max": 16.0
2347 }"###
2348 );
2349 },
2350 );
2351 }
2352
2353 // Conditions can be found in method arguments
2354 #[test]
2355 fn java_methods_arguments_with_conditions() {
2356 check_metrics::<JavaParser>(
2357 "
2358 m1(a); // +1b
2359 m2(a, b); // +1b
2360 m3(true, (false), (((true)))); // +1b
2361 m3(m1(false), m1(true), m1(false)); // +4b
2362 m1(!a); // +1b +1c
2363 m2((((a))), (!b)); // +1b +1c
2364 m3(!(a), b, !!!c); // +1b +2c
2365 m3(a, !b, m2(!a, !m2(!b, !m1(!c)))); // +4b +6c
2366 ",
2367 "foo.java",
2368 |metric| {
2369 // magnitude: sqrt(196 + 36 + 256) = sqrt(488)
2370 // space count: 1 (1 unit)
2371 insta::assert_json_snapshot!(
2372 metric.abc,
2373 @r###"
2374 {
2375 "assignments": 0.0,
2376 "branches": 14.0,
2377 "conditions": 10.0,
2378 "magnitude": 17.204650534085253,
2379 "assignments_average": 0.0,
2380 "branches_average": 14.0,
2381 "conditions_average": 10.0,
2382 "assignments_min": 0.0,
2383 "assignments_max": 0.0,
2384 "branches_min": 14.0,
2385 "branches_max": 14.0,
2386 "conditions_min": 10.0,
2387 "conditions_max": 10.0
2388 }"###
2389 );
2390 },
2391 );
2392 }
2393
2394 // "A unary conditional expression is an implicit condition that uses no relational operators."
2395 // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
2396 // https://www.softwarerenovation.com/Articles.aspx (page 5)
2397 #[test]
2398 fn java_if_single_conditions() {
2399 check_metrics::<JavaParser>(
2400 "
2401 if ( a < 0 ) {} // +1c
2402 if ( ((a != 0)) ) {} // +1c
2403 if ( !(a > 0) ) {} // +1c
2404 if ( !(((a == 0))) ) {} // +1c
2405 if ( b.m1() ) {} // +1b +1c
2406 if ( !b.m1() ) {} // +1b +1c
2407 if ( !!b.m2() ) {} // +1b +1c
2408 if ( (!(b.m1())) ) {} // +1b +1c
2409 if ( (!(!b.m1())) ) {} // +1b +1c
2410 if ( ((b.m2())) ) {} // +1b +1c
2411 if ( ((b.m().m1())) ) {} // +2b +1c
2412 if ( c ) {} // +1c
2413 if ( !c ) {} // +1c
2414 if ( !!!!!!!!!!c ) {} // +1c
2415 if ( (((c))) ) {} // +1c
2416 if ( (((!c))) ) {} // +1c
2417 if ( ((!(c))) ) {} // +1c
2418 if ( true ) {} // +1c
2419 if ( !true ) {} // +1c
2420 if ( ((false)) ) {} // +1c
2421 if ( !(!(false)) ) {} // +1c
2422 if ( !!!false ) {} // +1c
2423 ",
2424 "foo.java",
2425 |metric| {
2426 // magnitude: sqrt(0 + 64 + 484) = sqrt(548)
2427 // space count: 1 (1 unit)
2428 insta::assert_json_snapshot!(
2429 metric.abc,
2430 @r###"
2431 {
2432 "assignments": 0.0,
2433 "branches": 8.0,
2434 "conditions": 22.0,
2435 "magnitude": 23.40939982143925,
2436 "assignments_average": 0.0,
2437 "branches_average": 8.0,
2438 "conditions_average": 22.0,
2439 "assignments_min": 0.0,
2440 "assignments_max": 0.0,
2441 "branches_min": 8.0,
2442 "branches_max": 8.0,
2443 "conditions_min": 22.0,
2444 "conditions_max": 22.0
2445 }"###
2446 );
2447 },
2448 );
2449 }
2450
2451 #[test]
2452 fn java_if_multiple_conditions() {
2453 check_metrics::<JavaParser>(
2454 "
2455 if ( a || b || c || d ) {} // +4c
2456 if ( a || b && c && d ) {} // +4c
2457 if ( x < y && a == b ) {} // +2c
2458 if ( ((z < (x + y))) ) {} // +1c
2459 if ( a || ((((b))) && c) ) {} // +3c
2460 if ( a && ((((a == b))) && c) ) {} // +3c
2461 if ( a || ((((a == b))) || ((c))) ) {} // +3c
2462 if ( x < y && B.m() ) {} // +1b +2c
2463 if ( x < y && !(((B.m()))) ) {} // +1b +2c
2464 if ( !(x < y) && !B.m() ) {} // +1b +2c
2465 if ( !!!(!!!(a)) && B.m() || // +1b +2c
2466 !B.m() && (((x > 4))) ) {} // +1b +2c
2467 ",
2468 "foo.java",
2469 |metric| {
2470 // magnitude: sqrt(0 + 25 + 900) = sqrt(925)
2471 // space count: 1 (1 unit)
2472 insta::assert_json_snapshot!(
2473 metric.abc,
2474 @r###"
2475 {
2476 "assignments": 0.0,
2477 "branches": 5.0,
2478 "conditions": 30.0,
2479 "magnitude": 30.4138126514911,
2480 "assignments_average": 0.0,
2481 "branches_average": 5.0,
2482 "conditions_average": 30.0,
2483 "assignments_min": 0.0,
2484 "assignments_max": 0.0,
2485 "branches_min": 5.0,
2486 "branches_max": 5.0,
2487 "conditions_min": 30.0,
2488 "conditions_max": 30.0
2489 }"###
2490 );
2491 },
2492 );
2493 }
2494
2495 #[test]
2496 fn java_while_and_do_while_conditions() {
2497 check_metrics::<JavaParser>(
2498 "
2499 while ( (!(!(!(a)))) ) {} // +1c
2500 while ( b || 1 > 2 ) {} // +2c
2501 while ( x.m() && (((c))) ) {} // +1b +2c
2502 do {} while ( !!!(((!!!a))) ); // +1c
2503 do {} while ( a || (b && c) ); // +3c
2504 do {} while ( !x.m() && 1 > 2 || !true ); // +1b +3c
2505 ",
2506 "foo.java",
2507 |metric| {
2508 // magnitude: sqrt(0 + 4 + 144) = sqrt(148)
2509 // space count: 1 (1 unit)
2510 insta::assert_json_snapshot!(
2511 metric.abc,
2512 @r###"
2513 {
2514 "assignments": 0.0,
2515 "branches": 2.0,
2516 "conditions": 12.0,
2517 "magnitude": 12.165525060596439,
2518 "assignments_average": 0.0,
2519 "branches_average": 2.0,
2520 "conditions_average": 12.0,
2521 "assignments_min": 0.0,
2522 "assignments_max": 0.0,
2523 "branches_min": 2.0,
2524 "branches_max": 2.0,
2525 "conditions_min": 12.0,
2526 "conditions_max": 12.0
2527 }"###
2528 );
2529 },
2530 );
2531 }
2532
2533 // GMetrics, a Groovy source code analyzer, provides the following definition of unary conditional expression:
2534 // "These are cases where a single variable/field/value is treated as a boolean value.
2535 // Examples include `if (x)` and `return !ready`."
2536 // According to this definition, unary conditional expressions are counted also in function return values.
2537 // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html
2538 // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy
2539 #[test]
2540 fn java_return_with_conditions() {
2541 check_metrics::<JavaParser>(
2542 "class A {
2543 boolean m1() {
2544 return !(z >= 0); // +1c
2545 }
2546 boolean m2() {
2547 return (((!x))); // +1c
2548 }
2549 boolean m3() {
2550 return x && y; // +2c
2551 }
2552 boolean m4() {
2553 return y || (z < 0); // +2c
2554 }
2555 boolean m5() {
2556 return x || y ? // +3c (two unary conditions and one ?)
2557 true : false;
2558 }
2559 }",
2560 "foo.java",
2561 |metric| {
2562 // magnitude: sqrt(0 + 0 + 81) = sqrt(81)
2563 // space count: 7 (1 unit, 1 class and 5 methods)
2564 insta::assert_json_snapshot!(
2565 metric.abc,
2566 @r###"
2567 {
2568 "assignments": 0.0,
2569 "branches": 0.0,
2570 "conditions": 9.0,
2571 "magnitude": 9.0,
2572 "assignments_average": 0.0,
2573 "branches_average": 0.0,
2574 "conditions_average": 1.2857142857142858,
2575 "assignments_min": 0.0,
2576 "assignments_max": 0.0,
2577 "branches_min": 0.0,
2578 "branches_max": 0.0,
2579 "conditions_min": 0.0,
2580 "conditions_max": 3.0
2581 }"###
2582 );
2583 },
2584 );
2585 }
2586
2587 // Variables, method invocations, and true or false values
2588 // inside return statements are not counted as conditions
2589 #[test]
2590 fn java_return_without_conditions() {
2591 check_metrics::<JavaParser>(
2592 "class A {
2593 boolean m1() {
2594 return x;
2595 }
2596 boolean m2() {
2597 return (x);
2598 }
2599 boolean m3() {
2600 return y.m(); // +1b
2601 }
2602 boolean m4() {
2603 return false;
2604 }
2605 void m5() {
2606 return;
2607 }
2608 }",
2609 "foo.java",
2610 |metric| {
2611 // magnitude: sqrt(0 + 1 + 0) = sqrt(1)
2612 // space count: 7 (1 unit, 1 class and 5 methods)
2613 insta::assert_json_snapshot!(
2614 metric.abc,
2615 @r###"
2616 {
2617 "assignments": 0.0,
2618 "branches": 1.0,
2619 "conditions": 0.0,
2620 "magnitude": 1.0,
2621 "assignments_average": 0.0,
2622 "branches_average": 0.14285714285714285,
2623 "conditions_average": 0.0,
2624 "assignments_min": 0.0,
2625 "assignments_max": 0.0,
2626 "branches_min": 0.0,
2627 "branches_max": 1.0,
2628 "conditions_min": 0.0,
2629 "conditions_max": 0.0
2630 }"###
2631 );
2632 },
2633 );
2634 }
2635
2636 // Variables, method invocations, and true or false values
2637 // in lambda expression return values are not counted as conditions
2638 #[test]
2639 fn java_lambda_expressions_return_with_conditions() {
2640 check_metrics::<JavaParser>(
2641 "
2642 Predicate<Boolean> p1 = a -> a; // +1a
2643 Predicate<Boolean> p2 = b -> true; // +1a
2644 Predicate<Boolean> p3 = c -> m(); // +1a
2645 Predicate<Integer> p4 = d -> d > 10; // +1a +1c
2646 Predicate<Boolean> p5 = (e) -> !e; // +1a +1c
2647 Predicate<Boolean> p6 = (f) -> !((!f)); // +1a +1c
2648 Predicate<Boolean> p7 = (g) -> !g && true; // +1a +2c
2649 BiPredicate<Boolean, Boolean> bp1 = (h, i) -> !h && !i; // +1a +2c
2650 BiPredicate<Boolean, Boolean> bp2 = (j, k) -> {
2651 return j || k; // +1a +2c
2652 };
2653 ",
2654 "foo.java",
2655 |metric| {
2656 // magnitude: sqrt(81 + 1 + 81) = sqrt(163)
2657 // space count: 1 (1 unit)
2658 insta::assert_json_snapshot!(
2659 metric.abc,
2660 @r###"
2661 {
2662 "assignments": 9.0,
2663 "branches": 1.0,
2664 "conditions": 9.0,
2665 "magnitude": 12.767145334803704,
2666 "assignments_average": 9.0,
2667 "branches_average": 1.0,
2668 "conditions_average": 9.0,
2669 "assignments_min": 9.0,
2670 "assignments_max": 9.0,
2671 "branches_min": 1.0,
2672 "branches_max": 1.0,
2673 "conditions_min": 9.0,
2674 "conditions_max": 9.0
2675 }"###
2676 );
2677 },
2678 );
2679 }
2680
2681 #[test]
2682 fn java_for_with_variable_declaration() {
2683 check_metrics::<JavaParser>(
2684 "
2685 for ( int i1 = 0; !(!(!(!a))); i1++ ) {} // +2a +1c
2686 for ( int i2 = 0; !B.m(); i2++ ) {} // +2a +1b +1c
2687 for ( int i3 = 0; a || false; i3++ ) {} // +2a +2c
2688 for ( int i4 = 0; a && B.m() ? true : false; i4++ ) {} // +2a +1b +3c
2689 for ( int i5 = 0; true; i5++ ) {} // +2a +1c
2690 ",
2691 "foo.java",
2692 |metric| {
2693 // magnitude: sqrt(100 + 4 + 64) = sqrt(168)
2694 // space count: 1 (1 unit)
2695 insta::assert_json_snapshot!(
2696 metric.abc,
2697 @r###"
2698 {
2699 "assignments": 10.0,
2700 "branches": 2.0,
2701 "conditions": 8.0,
2702 "magnitude": 12.96148139681572,
2703 "assignments_average": 10.0,
2704 "branches_average": 2.0,
2705 "conditions_average": 8.0,
2706 "assignments_min": 10.0,
2707 "assignments_max": 10.0,
2708 "branches_min": 2.0,
2709 "branches_max": 2.0,
2710 "conditions_min": 8.0,
2711 "conditions_max": 8.0
2712 }"###
2713 );
2714 },
2715 );
2716 }
2717
2718 #[test]
2719 fn java_for_without_variable_declaration() {
2720 check_metrics::<JavaParser>(
2721 "class A{
2722 void m1() {
2723 for (i = 0; x < y; i++) {} // +2a +1c
2724 for (i = 0; ((x < y)); i++) {} // +2a +1c
2725 for (i = 0; !(!(x < y)); i++) {} // +2a +1c
2726 for (i = 0; true; i++) {} // +2a +1c
2727 }
2728 void m2() {
2729 for ( ; true; ) {} // +1c
2730 }
2731 void m3() {
2732 for ( ; ; ) {} // +1c (one implicit unary condition set to true)
2733 }
2734 }",
2735 "foo.java",
2736 |metric| {
2737 // magnitude: sqrt(64 + 0 + 36) = sqrt(100)
2738 // space count: 5 (1 unit, 1 class and 3 methods)
2739 insta::assert_json_snapshot!(
2740 metric.abc,
2741 @r###"
2742 {
2743 "assignments": 8.0,
2744 "branches": 0.0,
2745 "conditions": 6.0,
2746 "magnitude": 10.0,
2747 "assignments_average": 1.6,
2748 "branches_average": 0.0,
2749 "conditions_average": 1.2,
2750 "assignments_min": 0.0,
2751 "assignments_max": 8.0,
2752 "branches_min": 0.0,
2753 "branches_max": 0.0,
2754 "conditions_min": 0.0,
2755 "conditions_max": 4.0
2756 }"###
2757 );
2758 },
2759 );
2760 }
2761
2762 // Variables, method invocations, and true or false values
2763 // in ternary expression return values are not counted as conditions
2764 #[test]
2765 fn java_ternary_conditions() {
2766 check_metrics::<JavaParser>(
2767 "
2768 a = true; // +1a
2769 b = a ? true : false; // +1a +2c
2770 c = ((((a)))) ? !false : !b; // +1a +4c
2771 d = !this.m() ? !!a : (false); // +1a +1b +3c
2772 e = !(a) && b ? ((c)) : !d; // +1a +4c
2773 if ( this.m() ? a : !this.m() ) {} // +2b +3c
2774 if ( x > 0 ? !(false) : this.m() ) {} // +1b +3c
2775 if ( x > 0 && x != 3 ? !(a) : (!(b)) ) {} // +5c
2776 ",
2777 "foo.java",
2778 |metric| {
2779 // magnitude: sqrt(25 + 16 + 576) = sqrt(617)
2780 // space count: 1 (1 unit)
2781 insta::assert_json_snapshot!(
2782 metric.abc,
2783 @r###"
2784 {
2785 "assignments": 5.0,
2786 "branches": 4.0,
2787 "conditions": 24.0,
2788 "magnitude": 24.839484696748443,
2789 "assignments_average": 5.0,
2790 "branches_average": 4.0,
2791 "conditions_average": 24.0,
2792 "assignments_min": 5.0,
2793 "assignments_max": 5.0,
2794 "branches_min": 4.0,
2795 "branches_max": 4.0,
2796 "conditions_min": 24.0,
2797 "conditions_max": 24.0
2798 }"###
2799 );
2800 },
2801 );
2802 }
2803
2804 #[test]
2805 fn bash_assignments_only() {
2806 check_metrics::<BashParser>(
2807 "f() {
2808 a=1
2809 b=2
2810 c+=3
2811 }",
2812 "foo.sh",
2813 |metric| {
2814 insta::assert_json_snapshot!(
2815 metric.abc,
2816 @r###"
2817 {
2818 "assignments": 3.0,
2819 "branches": 0.0,
2820 "conditions": 0.0,
2821 "magnitude": 3.0,
2822 "assignments_average": 1.5,
2823 "branches_average": 0.0,
2824 "conditions_average": 0.0,
2825 "assignments_min": 0.0,
2826 "assignments_max": 3.0,
2827 "branches_min": 0.0,
2828 "branches_max": 0.0,
2829 "conditions_min": 0.0,
2830 "conditions_max": 0.0
2831 }"###
2832 );
2833 },
2834 );
2835 }
2836
2837 #[test]
2838 fn bash_commands_only() {
2839 check_metrics::<BashParser>(
2840 "f() {
2841 echo a
2842 ls
2843 }",
2844 "foo.sh",
2845 |metric| {
2846 insta::assert_json_snapshot!(
2847 metric.abc,
2848 @r###"
2849 {
2850 "assignments": 0.0,
2851 "branches": 2.0,
2852 "conditions": 0.0,
2853 "magnitude": 2.0,
2854 "assignments_average": 0.0,
2855 "branches_average": 1.0,
2856 "conditions_average": 0.0,
2857 "assignments_min": 0.0,
2858 "assignments_max": 0.0,
2859 "branches_min": 0.0,
2860 "branches_max": 2.0,
2861 "conditions_min": 0.0,
2862 "conditions_max": 0.0
2863 }"###
2864 );
2865 },
2866 );
2867 }
2868
2869 #[test]
2870 fn bash_conditions_mix() {
2871 // Exercises every condition path: `==` and `!=` inside `[[ ]]`,
2872 // arithmetic `<` inside `(( ))`, and the prefix `-z` test operator
2873 // inside `[ ]`. Each `if` body's `echo` contributes a branch.
2874 check_metrics::<BashParser>(
2875 "f() {
2876 if [[ \"$a\" == \"$b\" ]]; then
2877 echo eq
2878 fi
2879 if [[ \"$x\" != \"$y\" ]]; then
2880 echo ne
2881 fi
2882 if (( $a < $b )); then
2883 echo lt
2884 fi
2885 if [ -z \"$x\" ]; then
2886 echo empty
2887 fi
2888 }",
2889 "foo.sh",
2890 |metric| {
2891 insta::assert_json_snapshot!(
2892 metric.abc,
2893 @r###"
2894 {
2895 "assignments": 0.0,
2896 "branches": 4.0,
2897 "conditions": 4.0,
2898 "magnitude": 5.656854249492381,
2899 "assignments_average": 0.0,
2900 "branches_average": 2.0,
2901 "conditions_average": 2.0,
2902 "assignments_min": 0.0,
2903 "assignments_max": 0.0,
2904 "branches_min": 0.0,
2905 "branches_max": 4.0,
2906 "conditions_min": 0.0,
2907 "conditions_max": 4.0
2908 }"###
2909 );
2910 },
2911 );
2912 }
2913
2914 #[test]
2915 fn bash_magnitude() {
2916 // Combined assignments + branches + conditions: magnitude must
2917 // equal sqrt(2² + 1² + 1²) = sqrt(6).
2918 check_metrics::<BashParser>(
2919 "f() {
2920 a=1
2921 b=2
2922 if [[ \"$a\" == \"$b\" ]]; then
2923 echo eq
2924 fi
2925 }",
2926 "foo.sh",
2927 |metric| {
2928 insta::assert_json_snapshot!(
2929 metric.abc,
2930 @r###"
2931 {
2932 "assignments": 2.0,
2933 "branches": 1.0,
2934 "conditions": 1.0,
2935 "magnitude": 2.449489742783178,
2936 "assignments_average": 1.0,
2937 "branches_average": 0.5,
2938 "conditions_average": 0.5,
2939 "assignments_min": 0.0,
2940 "assignments_max": 2.0,
2941 "branches_min": 0.0,
2942 "branches_max": 1.0,
2943 "conditions_min": 0.0,
2944 "conditions_max": 1.0
2945 }"###
2946 );
2947 },
2948 );
2949 }
2950
2951 #[test]
2952 fn java_malformed_parenthesized_no_panic() {
2953 check_metrics::<JavaParser>("class A { void m() { if (( }) }", "foo.java", |metric| {
2954 // tree-sitter emits ERROR nodes for this malformed source, so no
2955 // IfStatement, branch, or condition is recognised — all counts are 0.
2956 // Primary goal: the unwrap-free path does not panic.
2957 assert_eq!(metric.abc.assignments(), 0.0);
2958 assert_eq!(metric.abc.branches(), 0.0);
2959 assert_eq!(metric.abc.conditions(), 0.0);
2960 assert_eq!(metric.abc.magnitude(), 0.0);
2961 });
2962 }
2963
2964 #[test]
2965 fn groovy_no_abc() {
2966 // Comment-only file has no executable code → all-zero ABC.
2967 check_metrics::<GroovyParser>(
2968 "// just a comment, no executable code",
2969 "foo.groovy",
2970 |metric| {
2971 assert_eq!(metric.abc.assignments_sum(), 0.0);
2972 assert_eq!(metric.abc.branches_sum(), 0.0);
2973 assert_eq!(metric.abc.conditions_sum(), 0.0);
2974 },
2975 );
2976 }
2977
2978 #[test]
2979 fn groovy_single_assignment() {
2980 // `int x = 1` is a local-variable declaration whose `=` counts
2981 // as one assignment (matches Java's semantics).
2982 check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
2983 assert_eq!(metric.abc.assignments_sum(), 1.0);
2984 assert_eq!(metric.abc.branches_sum(), 0.0);
2985 assert_eq!(metric.abc.conditions_sum(), 0.0);
2986 });
2987 }
2988
2989 #[test]
2990 fn groovy_assignments() {
2991 check_metrics::<GroovyParser>(
2992 "void f() {
2993 int a = 1
2994 int b = 2
2995 a = 3
2996 b = 4
2997 a += 1
2998 b -= 1
2999 }",
3000 "foo.groovy",
3001 |metric| {
3002 // Six `=` tokens total. The two `Final`-less local
3003 // var-decls (`int a = 1`, `int b = 2`) and the two
3004 // bare assignments (`a = 3`, `b = 4`) each contribute
3005 // one assignment via the `EQ` arm; the `+=` / `-=`
3006 // each contribute one via the compound-assign arm.
3007 assert_eq!(metric.abc.assignments_sum(), 6.0);
3008 },
3009 );
3010 }
3011
3012 #[test]
3013 fn groovy_branches() {
3014 check_metrics::<GroovyParser>(
3015 "void f() {
3016 doStuff()
3017 helper.invoke()
3018 new Worker()
3019 }",
3020 "foo.groovy",
3021 |metric| {
3022 // 2 method invocations + 1 object creation = 3 branches
3023 assert_eq!(metric.abc.branches_sum(), 3.0);
3024 },
3025 );
3026 }
3027
3028 #[test]
3029 fn groovy_conditions_in_if() {
3030 check_metrics::<GroovyParser>(
3031 "void f(int a) {
3032 if (a == 0) { println(a) }
3033 if (a >= 1) { println(a) }
3034 if (a != 2) { println(a) }
3035 }",
3036 "foo.groovy",
3037 |metric| {
3038 // Three relational ops = 3 conditions
3039 assert_eq!(metric.abc.conditions_sum(), 3.0);
3040 },
3041 );
3042 }
3043
3044 #[test]
3045 fn groovy_branches_with_juxt_call() {
3046 // Groovy's parens-less call form `println foo` must be counted
3047 // as a branch (`JuxtFunctionCall`).
3048 check_metrics::<GroovyParser>(
3049 "void f() {
3050 println 'hi'
3051 println 'bye'
3052 }",
3053 "foo.groovy",
3054 |metric| {
3055 // 2 juxt calls = 2 branches.
3056 assert_eq!(metric.abc.branches_sum(), 2.0);
3057 },
3058 );
3059 }
3060
3061 #[test]
3062 fn groovy_try_catch_conditions() {
3063 // Each `try` and `catch` keyword token contributes +1 to
3064 // conditions (mirrors Java).
3065 check_metrics::<GroovyParser>(
3066 "void f() {
3067 try {
3068 risky()
3069 } catch (Exception e) {
3070 handle(e)
3071 }
3072 }",
3073 "foo.groovy",
3074 |metric| {
3075 // try + catch = 2 conditions
3076 assert_eq!(metric.abc.conditions_sum(), 2.0);
3077 },
3078 );
3079 }
3080
3081 #[test]
3082 fn groovy_ternary_conditions() {
3083 check_metrics::<GroovyParser>(
3084 "void f(int x) {
3085 def y = x > 0 ? 1 : 2
3086 }",
3087 "foo.groovy",
3088 |metric| {
3089 // QMARK alone is +1 condition, plus the `>` condition = 2.
3090 assert_eq!(metric.abc.conditions_sum(), 2.0);
3091 },
3092 );
3093 }
3094
3095 #[test]
3096 fn groovy_constant_excluded_from_assignments() {
3097 // `final` declarations are not counted as assignments
3098 // (mirrors Java's `Final` handling).
3099 check_metrics::<GroovyParser>(
3100 "class A {
3101 final int CONST = 42
3102 int field = 0
3103 }",
3104 "foo.groovy",
3105 |metric| {
3106 // The `=` on `final int CONST = 42` is a constant
3107 // initialiser (skipped). Only `field = 0` counts.
3108 assert_eq!(metric.abc.assignments_sum(), 1.0);
3109 },
3110 );
3111 }
3112
3113 #[test]
3114 fn groovy_malformed_parenthesized_no_panic() {
3115 // Regression: malformed Groovy input must not panic the ABC
3116 // walker; the `spaces.rs` Unit fallback (lesson 9) covers
3117 // structural recovery. amaanq's grammar treats `def x = (((`
3118 // as a `local_variable_declaration` whose initialiser is the
3119 // first opening paren — the `=` still fires the assignment
3120 // arm.
3121 check_metrics::<GroovyParser>("def x = (((", "foo.groovy", |metric| {
3122 assert_eq!(metric.abc.assignments_sum(), 1.0);
3123 });
3124 }
3125
3126 #[test]
3127 fn groovy_if_multiple_conditions() {
3128 // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains
3129 // and parenthesised unary forms each contribute one
3130 // condition per primitive comparison; the inspect-container
3131 // pass picks up the unary `!a` / `!b` arguments inside the
3132 // `BinaryExpression` and counts them too.
3133 check_metrics::<GroovyParser>(
3134 "void f(boolean a, boolean b, boolean c) {
3135 if (a || b || c) { println(a) }
3136 if (a && b && c) { println(a) }
3137 if (!a && !b) { println(a) }
3138 }",
3139 "foo.groovy",
3140 |metric| {
3141 // Conditions counted via the AMPAMP/PIPEPIPE arms
3142 // (one count per identifier in the chain — three
3143 // for `||`, three for `&&`, two for the unary chain)
3144 // = 8.
3145 assert_eq!(metric.abc.conditions_sum(), 8.0);
3146 // Three `println a` juxt calls — each is a branch.
3147 assert_eq!(metric.abc.branches_sum(), 3.0);
3148 },
3149 );
3150 }
3151
3152 #[test]
3153 fn groovy_while_and_do_while_conditions() {
3154 // Covers the WhileStatement and DoStatement arms in
3155 // `impl Abc for GroovyCode`. Each `while` / `do-while` has
3156 // its condition inspected through `groovy_inspect_container`.
3157 check_metrics::<GroovyParser>(
3158 "void f(boolean a, boolean b) {
3159 while (a) {
3160 a = false
3161 }
3162 do {
3163 b = !b
3164 } while (b)
3165 }",
3166 "foo.groovy",
3167 |metric| {
3168 // `while(a)` + `while(b)` each contribute one condition;
3169 // the unary `!b` on the do body's right-hand side adds
3170 // one more via the assignment-arm inspection = 3.
3171 assert_eq!(metric.abc.conditions_sum(), 3.0);
3172 // Two assignments to existing variables (`a = false`,
3173 // `b = !b`).
3174 assert_eq!(metric.abc.assignments_sum(), 2.0);
3175 },
3176 );
3177 }
3178
3179 #[test]
3180 fn groovy_methods_arguments_with_conditions() {
3181 // Mirror of `java_methods_arguments_with_conditions`: a
3182 // unary `!x` inside an argument list must count both the
3183 // method invocation as a branch AND the unary as a
3184 // condition. The `ArgumentList | ArgumentList2` arm in
3185 // `impl Abc for GroovyCode` is what exercises this.
3186 check_metrics::<GroovyParser>(
3187 "void f(boolean a, boolean b, boolean c) {
3188 m1(a)
3189 m1(!a)
3190 m2(!a, !b)
3191 }",
3192 "foo.groovy",
3193 |metric| {
3194 // 3 method invocations (m1, m1, m2) — each fires the
3195 // branches arm.
3196 assert_eq!(metric.abc.branches_sum(), 3.0);
3197 // Three `!` unaries — `m1(!a)` and the two args of
3198 // `m2(!a, !b)` — each contribute one condition via
3199 // the ArgumentList inspection.
3200 assert_eq!(metric.abc.conditions_sum(), 3.0);
3201 },
3202 );
3203 }
3204
3205 #[test]
3206 fn groovy_return_with_conditions() {
3207 // Mirror of `java_return_with_conditions`: a parenthesised
3208 // or unary expression inside `return` flows through the
3209 // `ReturnStatement` arm to `groovy_inspect_container`.
3210 check_metrics::<GroovyParser>(
3211 "boolean f(boolean a) {
3212 return (a)
3213 }
3214 boolean g(boolean a) {
3215 return !a
3216 }",
3217 "foo.groovy",
3218 |metric| {
3219 // Only one of the two return forms surfaces a
3220 // condition: `return !a` hits the UnaryExpression
3221 // path and adds one; `return (a)` reaches
3222 // `groovy_inspect_container` but the inner
3223 // identifier `a` is not in a boolean-context-firing
3224 // parent, so no condition is added.
3225 assert_eq!(metric.abc.conditions_sum(), 1.0);
3226 },
3227 );
3228 }
3229
3230 #[test]
3231 fn groovy_for_with_variable_declaration() {
3232 // Classical `for (int i = 0; cond; i++)` form. The init
3233 // slot's `int i = 0` is suppressed from assignments by the
3234 // `LocalVariableDeclaration` push/pop dance; the `i++` in
3235 // the update slot contributes one assignment via the
3236 // `PLUSPLUS` arm. The condition `i < 10` flows through the
3237 // `ForStatement` arm.
3238 check_metrics::<GroovyParser>(
3239 "void f() {
3240 for (int i = 0; i < 10; i++) {
3241 println(i)
3242 }
3243 }",
3244 "foo.groovy",
3245 |metric| {
3246 // `int i = 0` fires the EQ arm + `i++` fires the
3247 // PLUSPLUS arm = 2 assignments.
3248 assert_eq!(metric.abc.assignments_sum(), 2.0);
3249 // `i < 10` is one condition (the LT arm).
3250 assert_eq!(metric.abc.conditions_sum(), 1.0);
3251 },
3252 );
3253 }
3254
3255 #[test]
3256 fn groovy_eq_arm_increments_when_no_declaration() {
3257 // Bare reassignment of an already-declared variable: the
3258 // `EQ` arm fires when the declaration stack is empty
3259 // (`stats.declaration.last().is_none()`), so the `=` counts
3260 // as one assignment. Mirrors `java_eq_arm_increments_when_
3261 // declaration_stack_is_empty`.
3262 check_metrics::<GroovyParser>(
3263 "void f(int x) {
3264 x = 42
3265 }",
3266 "foo.groovy",
3267 |metric| {
3268 assert_eq!(metric.abc.assignments_sum(), 1.0);
3269 assert_eq!(metric.abc.branches_sum(), 0.0);
3270 assert_eq!(metric.abc.conditions_sum(), 0.0);
3271 },
3272 );
3273 }
3274
3275 #[test]
3276 fn csharp_constant_declarations() {
3277 check_metrics::<CsharpParser>(
3278 "class A {
3279 private const int X1 = 0, Y1 = 0;
3280 public const float PI = 3.14f;
3281 const string HELLO = \"Hello,\";
3282 protected string world = \" world!\";
3283 public float e = 2.718f;
3284 private int x2 = 1, y2 = 2;
3285 void M() {
3286 const int Z1 = 0, Z2 = 0, Z3 = 0;
3287 const float T = 0.0f;
3288 int z1 = 1, z2 = 2, z3 = 3;
3289 }
3290 }",
3291 "foo.cs",
3292 |metric| insta::assert_json_snapshot!(metric.abc),
3293 );
3294 }
3295
3296 #[test]
3297 fn csharp_declarations_with_conditions() {
3298 check_metrics::<CsharpParser>(
3299 "class A {
3300 bool a = (1 == 2);
3301 bool b = (1 < 2);
3302 bool c = !true;
3303 bool d = !false;
3304 }",
3305 "foo.cs",
3306 |metric| insta::assert_json_snapshot!(metric.abc),
3307 );
3308 }
3309
3310 #[test]
3311 fn csharp_assignments_with_conditions() {
3312 check_metrics::<CsharpParser>(
3313 "class A {
3314 void M() {
3315 int a = 0;
3316 a += 1;
3317 a -= 2;
3318 a *= 3;
3319 a /= 4;
3320 a %= 5;
3321 a++;
3322 a--;
3323 }
3324 }",
3325 "foo.cs",
3326 |metric| insta::assert_json_snapshot!(metric.abc),
3327 );
3328 }
3329
3330 #[test]
3331 fn csharp_methods_arguments_with_conditions() {
3332 check_metrics::<CsharpParser>(
3333 "class A {
3334 void M(int x, int y) {
3335 F(x == y, x < y, !x.Equals(y));
3336 }
3337 void F(bool a, bool b, bool c) {}
3338 }",
3339 "foo.cs",
3340 |metric| insta::assert_json_snapshot!(metric.abc),
3341 );
3342 }
3343
3344 #[test]
3345 fn csharp_if_single_conditions() {
3346 check_metrics::<CsharpParser>(
3347 "class A {
3348 void M(int x) {
3349 if (x > 0) { System.Console.WriteLine(\"a\"); }
3350 if (x < 0) { System.Console.WriteLine(\"b\"); }
3351 if (x == 0) { System.Console.WriteLine(\"c\"); }
3352 }
3353 }",
3354 "foo.cs",
3355 |metric| insta::assert_json_snapshot!(metric.abc),
3356 );
3357 }
3358
3359 #[test]
3360 fn csharp_if_multiple_conditions() {
3361 check_metrics::<CsharpParser>(
3362 "class A {
3363 void M(int x, int y) {
3364 if (x > 0 && y > 0) { System.Console.WriteLine(\"a\"); }
3365 if (x < 0 || y < 0) { System.Console.WriteLine(\"b\"); }
3366 }
3367 }",
3368 "foo.cs",
3369 |metric| insta::assert_json_snapshot!(metric.abc),
3370 );
3371 }
3372
3373 #[test]
3374 fn csharp_while_and_do_while_conditions() {
3375 check_metrics::<CsharpParser>(
3376 "class A {
3377 void M(int x) {
3378 while (x > 0) { x--; }
3379 do { x++; } while (x < 10);
3380 }
3381 }",
3382 "foo.cs",
3383 |metric| insta::assert_json_snapshot!(metric.abc),
3384 );
3385 }
3386
3387 #[test]
3388 fn csharp_return_with_conditions() {
3389 check_metrics::<CsharpParser>(
3390 "class A {
3391 bool M(int x) {
3392 return (x > 0);
3393 }
3394 bool N(int x) {
3395 return !(x < 0);
3396 }
3397 }",
3398 "foo.cs",
3399 |metric| insta::assert_json_snapshot!(metric.abc),
3400 );
3401 }
3402
3403 #[test]
3404 fn csharp_return_without_conditions() {
3405 check_metrics::<CsharpParser>(
3406 "class A {
3407 int M() { return 42; }
3408 string N() { return \"hi\"; }
3409 }",
3410 "foo.cs",
3411 |metric| insta::assert_json_snapshot!(metric.abc),
3412 );
3413 }
3414
3415 #[test]
3416 fn csharp_lambda_expressions_return_with_conditions() {
3417 check_metrics::<CsharpParser>(
3418 "class A {
3419 public void M() {
3420 System.Func<int, bool> f = x => (x > 0);
3421 System.Func<int, bool> g = x => !(x < 0);
3422 }
3423 }",
3424 "foo.cs",
3425 |metric| insta::assert_json_snapshot!(metric.abc),
3426 );
3427 }
3428
3429 #[test]
3430 fn csharp_for_with_variable_declaration() {
3431 check_metrics::<CsharpParser>(
3432 "class A {
3433 void M() {
3434 for (int i = 0; i < 10; i++) {
3435 System.Console.WriteLine(i);
3436 }
3437 }
3438 }",
3439 "foo.cs",
3440 |metric| insta::assert_json_snapshot!(metric.abc),
3441 );
3442 }
3443
3444 #[test]
3445 fn csharp_for_without_variable_declaration() {
3446 check_metrics::<CsharpParser>(
3447 "class A {
3448 void M() {
3449 int i;
3450 for (i = 0; i < 10; i++) {
3451 System.Console.WriteLine(i);
3452 }
3453 }
3454 }",
3455 "foo.cs",
3456 |metric| insta::assert_json_snapshot!(metric.abc),
3457 );
3458 }
3459
3460 #[test]
3461 fn csharp_for_identifier_condition() {
3462 check_metrics::<CsharpParser>(
3463 "class A {
3464 void M(bool ready) {
3465 for (; ready ;) { }
3466 }
3467 }",
3468 "foo.cs",
3469 |metric| {
3470 // expected: assignments=0 (no `=` / `++` / `--`),
3471 // branches=0 (no invocation / object creation),
3472 // conditions=1 (bare-identifier for-loop condition).
3473 // Averages divide by 3 spaces (top-level + class + method).
3474 insta::assert_json_snapshot!(
3475 metric.abc,
3476 @r###"
3477 {
3478 "assignments": 0.0,
3479 "branches": 0.0,
3480 "conditions": 1.0,
3481 "magnitude": 1.0,
3482 "assignments_average": 0.0,
3483 "branches_average": 0.0,
3484 "conditions_average": 0.3333333333333333,
3485 "assignments_min": 0.0,
3486 "assignments_max": 0.0,
3487 "branches_min": 0.0,
3488 "branches_max": 0.0,
3489 "conditions_min": 0.0,
3490 "conditions_max": 1.0
3491 }
3492 "###
3493 );
3494 },
3495 );
3496 }
3497
3498 #[test]
3499 fn csharp_for_invocation_condition() {
3500 check_metrics::<CsharpParser>(
3501 "class A {
3502 bool Ok() { return true; }
3503 void M() {
3504 for (; Ok() ;) { }
3505 }
3506 }",
3507 "foo.cs",
3508 |metric| {
3509 // expected: assignments=0, branches=1 (the `Ok()` call),
3510 // conditions=1 (invocation as for-loop condition).
3511 // Averages divide by 4 spaces (top-level + class + two
3512 // methods).
3513 insta::assert_json_snapshot!(
3514 metric.abc,
3515 @r###"
3516 {
3517 "assignments": 0.0,
3518 "branches": 1.0,
3519 "conditions": 1.0,
3520 "magnitude": 1.4142135623730951,
3521 "assignments_average": 0.0,
3522 "branches_average": 0.25,
3523 "conditions_average": 0.25,
3524 "assignments_min": 0.0,
3525 "assignments_max": 0.0,
3526 "branches_min": 0.0,
3527 "branches_max": 1.0,
3528 "conditions_min": 0.0,
3529 "conditions_max": 1.0
3530 }
3531 "###
3532 );
3533 },
3534 );
3535 }
3536
3537 // Regression coverage for #279: the C# grammar wraps a literal
3538 // `true` / `false` for-loop condition in a `boolean_literal` node.
3539 // The `BooleanLiteral` arm in the `ForStatement` dispatch must
3540 // attribute one condition; without it, `for (; true ;)` would
3541 // contribute 0 (the bug fixed by this commit also affected this
3542 // shape).
3543 #[test]
3544 fn csharp_for_boolean_literal_condition() {
3545 check_metrics::<CsharpParser>(
3546 "class A {
3547 void M() {
3548 for (; true ;) { }
3549 }
3550 }",
3551 "foo.cs",
3552 |metric| {
3553 // expected: assignments=0, branches=0,
3554 // conditions=1 (the `true` literal as condition).
3555 assert_eq!(metric.abc.conditions_sum(), 1.0);
3556 assert_eq!(metric.abc.assignments_sum(), 0.0);
3557 assert_eq!(metric.abc.branches_sum(), 0.0);
3558 },
3559 );
3560 }
3561
3562 // Regression coverage for #279: an empty for-loop condition such as
3563 // `for (; ;) {}` must contribute 0 to conditions — there is no
3564 // condition node to count.
3565 #[test]
3566 fn csharp_for_empty_condition() {
3567 check_metrics::<CsharpParser>(
3568 "class A {
3569 void M() {
3570 for (; ;) { }
3571 }
3572 }",
3573 "foo.cs",
3574 |metric| {
3575 // expected: assignments=0, branches=0, conditions=0
3576 // (no condition expression in `for (; ;)`).
3577 insta::assert_json_snapshot!(
3578 metric.abc,
3579 @r###"
3580 {
3581 "assignments": 0.0,
3582 "branches": 0.0,
3583 "conditions": 0.0,
3584 "magnitude": 0.0,
3585 "assignments_average": 0.0,
3586 "branches_average": 0.0,
3587 "conditions_average": 0.0,
3588 "assignments_min": 0.0,
3589 "assignments_max": 0.0,
3590 "branches_min": 0.0,
3591 "branches_max": 0.0,
3592 "conditions_min": 0.0,
3593 "conditions_max": 0.0
3594 }
3595 "###
3596 );
3597 },
3598 );
3599 }
3600
3601 #[test]
3602 fn csharp_ternary_conditions() {
3603 check_metrics::<CsharpParser>(
3604 "class A {
3605 int Sign(int x) {
3606 return (x > 0) ? 1 : (x < 0 ? -1 : 0);
3607 }
3608 }",
3609 "foo.cs",
3610 |metric| insta::assert_json_snapshot!(metric.abc),
3611 );
3612 }
3613
3614 #[test]
3615 fn csharp_malformed_parenthesized_no_panic() {
3616 check_metrics::<CsharpParser>("class A { void M() { if (( }) }", "foo.cs", |metric| {
3617 // Don't panic on malformed source.
3618 assert_eq!(metric.abc.assignments(), 0.0);
3619 assert_eq!(metric.abc.branches(), 0.0);
3620 });
3621 }
3622
3623 #[test]
3624 fn csharp_function_pointer_type_no_double_count() {
3625 // EC1 extension — `<` and `>` are also parameter-list delimiters
3626 // for unsafe function-pointer types. `FunctionPointerType` must
3627 // be in the LT/GT exclusion list, otherwise these brackets
3628 // accumulate spurious `conditions` counts.
3629 check_metrics::<CsharpParser>(
3630 "unsafe class A {
3631 public delegate*<int, int, int> Adder;
3632 public delegate*<string, void> Logger;
3633 }",
3634 "foo.cs",
3635 |metric| {
3636 assert_eq!(
3637 metric.abc.conditions(),
3638 0.0,
3639 "function-pointer-type angle brackets must not count"
3640 );
3641 },
3642 );
3643 }
3644
3645 #[test]
3646 fn csharp_generic_type_args_no_double_count() {
3647 // EC1 — `<` and `>` inside TypeArgumentList must not count as
3648 // boolean conditions.
3649 check_metrics::<CsharpParser>(
3650 "class A {
3651 void M(System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>> d) {
3652 System.Console.WriteLine(d);
3653 }
3654 }",
3655 "foo.cs",
3656 |metric| insta::assert_json_snapshot!(metric.abc),
3657 );
3658 }
3659
3660 #[test]
3661 fn csharp_aliased_invocation_expression_branches() {
3662 // Regression for issue #94 (lesson #2): the C# grammar emits three
3663 // aliased `kind_id`s for `invocation_expression`. Code that matches
3664 // only the unsuffixed `Csharp::InvocationExpression` undercounts ABC
3665 // branches whenever the AST emits an aliased variant. The three
3666 // method calls live in `M`, so the per-method maximum (visible at
3667 // the unit-space aggregate as `branches_max`) must be 3.
3668 check_metrics::<CsharpParser>(
3669 "class A {
3670 void M() {
3671 System.Console.WriteLine(1);
3672 System.Console.WriteLine(2);
3673 System.Console.WriteLine(3);
3674 }
3675 }",
3676 "foo.cs",
3677 |metric| {
3678 assert_eq!(metric.abc.branches_max(), 3.0);
3679 assert_eq!(metric.abc.conditions_max(), 0.0);
3680 },
3681 );
3682 }
3683
3684 #[test]
3685 fn php_zero_abc() {
3686 check_metrics::<PhpParser>("<?php\n", "foo.php", |metric| {
3687 assert_eq!(metric.abc.assignments_sum(), 0.0);
3688 assert_eq!(metric.abc.branches_sum(), 0.0);
3689 assert_eq!(metric.abc.conditions_sum(), 0.0);
3690 insta::assert_json_snapshot!(metric.abc);
3691 });
3692 }
3693
3694 #[test]
3695 fn php_simple_assignment() {
3696 check_metrics::<PhpParser>(
3697 "<?php
3698function f(): void {
3699 $a = 1;
3700 $b = 2;
3701}",
3702 "foo.php",
3703 |metric| insta::assert_json_snapshot!(metric.abc),
3704 );
3705 }
3706
3707 #[test]
3708 fn php_augmented_assignment() {
3709 check_metrics::<PhpParser>(
3710 "<?php
3711function f(int $x): int {
3712 $a = 0;
3713 $a += $x;
3714 $a -= 1;
3715 $a *= 2;
3716 return $a;
3717}",
3718 "foo.php",
3719 |metric| insta::assert_json_snapshot!(metric.abc),
3720 );
3721 }
3722
3723 #[test]
3724 fn php_const_excluded() {
3725 // Constant declarations and enum cases are NOT counted as
3726 // assignments — they declare immutable values.
3727 check_metrics::<PhpParser>(
3728 "<?php
3729class A {
3730 const PI = 3.14;
3731 const E = 2.71;
3732}
3733enum Color {
3734 case Red;
3735 case Green;
3736}",
3737 "foo.php",
3738 |metric| insta::assert_json_snapshot!(metric.abc),
3739 );
3740 }
3741
3742 #[test]
3743 fn php_function_call() {
3744 check_metrics::<PhpParser>(
3745 "<?php
3746function f(): void {
3747 foo();
3748 bar(1, 2);
3749}",
3750 "foo.php",
3751 |metric| insta::assert_json_snapshot!(metric.abc),
3752 );
3753 }
3754
3755 #[test]
3756 fn php_method_call() {
3757 check_metrics::<PhpParser>(
3758 "<?php
3759function f($obj): void {
3760 $obj->m1();
3761 $obj->m2(1);
3762}",
3763 "foo.php",
3764 |metric| insta::assert_json_snapshot!(metric.abc),
3765 );
3766 }
3767
3768 #[test]
3769 fn php_static_call() {
3770 check_metrics::<PhpParser>(
3771 "<?php
3772function f(): void {
3773 Foo::bar();
3774 Foo::baz(1);
3775}",
3776 "foo.php",
3777 |metric| insta::assert_json_snapshot!(metric.abc),
3778 );
3779 }
3780
3781 #[test]
3782 fn php_nullsafe_call() {
3783 check_metrics::<PhpParser>(
3784 "<?php
3785function f($obj): void {
3786 $obj?->m1();
3787 $obj?->m2(1);
3788}",
3789 "foo.php",
3790 |metric| insta::assert_json_snapshot!(metric.abc),
3791 );
3792 }
3793
3794 #[test]
3795 fn php_object_creation() {
3796 check_metrics::<PhpParser>(
3797 "<?php
3798function f(): void {
3799 new Foo();
3800 new Bar(1);
3801}",
3802 "foo.php",
3803 |metric| insta::assert_json_snapshot!(metric.abc),
3804 );
3805 }
3806
3807 #[test]
3808 fn php_comparison_eq() {
3809 check_metrics::<PhpParser>(
3810 "<?php
3811function f(int $a, int $b): bool {
3812 return $a == $b || $a != $b;
3813}",
3814 "foo.php",
3815 |metric| insta::assert_json_snapshot!(metric.abc),
3816 );
3817 }
3818
3819 #[test]
3820 fn php_comparison_strict() {
3821 check_metrics::<PhpParser>(
3822 "<?php
3823function f(int $a, int $b): bool {
3824 return $a === $b || $a !== $b;
3825}",
3826 "foo.php",
3827 |metric| insta::assert_json_snapshot!(metric.abc),
3828 );
3829 }
3830
3831 #[test]
3832 fn php_spaceship() {
3833 check_metrics::<PhpParser>(
3834 "<?php
3835function f(int $a, int $b): int {
3836 return $a <=> $b;
3837}",
3838 "foo.php",
3839 |metric| insta::assert_json_snapshot!(metric.abc),
3840 );
3841 }
3842
3843 #[test]
3844 fn php_instanceof() {
3845 check_metrics::<PhpParser>(
3846 "<?php
3847function f($x): bool {
3848 return $x instanceof Foo;
3849}",
3850 "foo.php",
3851 |metric| insta::assert_json_snapshot!(metric.abc),
3852 );
3853 }
3854
3855 #[test]
3856 fn php_complex_function() {
3857 // One snippet exercising A, B, C buckets together.
3858 check_metrics::<PhpParser>(
3859 "<?php
3860function f(int $a, int $b): int {
3861 $sum = $a + $b;
3862 $prod = $a * $b;
3863 if ($sum > 0 && $prod === 0) {
3864 return foo($sum);
3865 }
3866 return bar()->double();
3867}",
3868 "foo.php",
3869 |metric| insta::assert_json_snapshot!(metric.abc),
3870 );
3871 }
3872
3873 // --- Kotlin ABC tests -------------------------------------------------
3874
3875 #[test]
3876 fn kotlin_empty_class() {
3877 check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
3878 assert_eq!(metric.abc.assignments_sum(), 0.0);
3879 assert_eq!(metric.abc.branches_sum(), 0.0);
3880 assert_eq!(metric.abc.conditions_sum(), 0.0);
3881 insta::assert_json_snapshot!(metric.abc);
3882 });
3883 }
3884
3885 #[test]
3886 fn kotlin_val_declarations_are_not_assignments() {
3887 // `val` introduces an immutable binding — the `=` initialising it
3888 // is not an assignment in the ABC sense.
3889 check_metrics::<KotlinParser>(
3890 "class C {
3891 val a: Int = 1
3892 val b: Int = 2
3893 val c: Int = 3
3894 }",
3895 "foo.kt",
3896 |metric| {
3897 assert_eq!(metric.abc.assignments_sum(), 0.0);
3898 assert_eq!(metric.abc.branches_sum(), 0.0);
3899 insta::assert_json_snapshot!(metric.abc);
3900 },
3901 );
3902 }
3903
3904 #[test]
3905 fn kotlin_var_declarations_count_assignment() {
3906 // `var` initialisers count as assignments (mutable binding).
3907 check_metrics::<KotlinParser>(
3908 "class C {
3909 var a: Int = 1
3910 var b: Int = 2
3911 }",
3912 "foo.kt",
3913 |metric| {
3914 assert_eq!(metric.abc.assignments_sum(), 2.0);
3915 insta::assert_json_snapshot!(metric.abc);
3916 },
3917 );
3918 }
3919
3920 #[test]
3921 fn kotlin_augmented_assignments_count() {
3922 // Augmented operators (+=, -=, etc.) and ++/-- always count.
3923 check_metrics::<KotlinParser>(
3924 "fun m() {
3925 var x = 0
3926 x += 1
3927 x -= 2
3928 x *= 3
3929 x++
3930 --x
3931 }",
3932 "foo.kt",
3933 |metric| {
3934 // var declaration (var x = 0): +1
3935 // x += 1, x -= 2, x *= 3, x++, --x: +5
3936 assert_eq!(metric.abc.assignments_sum(), 6.0);
3937 insta::assert_json_snapshot!(metric.abc);
3938 },
3939 );
3940 }
3941
3942 #[test]
3943 fn kotlin_branches_call_expression() {
3944 check_metrics::<KotlinParser>(
3945 "fun m() {
3946 println(\"a\")
3947 println(\"b\")
3948 println(\"c\")
3949 }",
3950 "foo.kt",
3951 |metric| {
3952 assert_eq!(metric.abc.branches_sum(), 3.0);
3953 insta::assert_json_snapshot!(metric.abc);
3954 },
3955 );
3956 }
3957
3958 #[test]
3959 fn kotlin_object_construction_branch() {
3960 // Kotlin's object construction is just `Foo()` — a `CallExpression`.
3961 check_metrics::<KotlinParser>(
3962 "class P(val x: Int)
3963 fun m(): P = P(1)",
3964 "foo.kt",
3965 |metric| {
3966 assert_eq!(metric.abc.branches_sum(), 1.0);
3967 insta::assert_json_snapshot!(metric.abc);
3968 },
3969 );
3970 }
3971
3972 #[test]
3973 fn kotlin_comparisons_count_conditions() {
3974 check_metrics::<KotlinParser>(
3975 "fun m(a: Int, b: Int): Boolean {
3976 val r1 = a < b
3977 val r2 = a > b
3978 val r3 = a <= b
3979 val r4 = a >= b
3980 val r5 = a == b
3981 val r6 = a != b
3982 return r1 || r2 || r3 || r4 || r5 || r6
3983 }",
3984 "foo.kt",
3985 |metric| {
3986 // Six binary operators: <, >, <=, >=, ==, != → 6 conditions.
3987 assert_eq!(metric.abc.conditions_sum(), 6.0);
3988 insta::assert_json_snapshot!(metric.abc);
3989 },
3990 );
3991 }
3992
3993 #[test]
3994 fn kotlin_identity_equality_conditions() {
3995 // `===` / `!==` are referential equality in Kotlin; they count too.
3996 check_metrics::<KotlinParser>(
3997 "fun m(a: Any, b: Any): Boolean {
3998 return a === b || a !== b
3999 }",
4000 "foo.kt",
4001 |metric| {
4002 assert_eq!(metric.abc.conditions_sum(), 2.0);
4003 insta::assert_json_snapshot!(metric.abc);
4004 },
4005 );
4006 }
4007
4008 #[test]
4009 fn kotlin_else_branch_counts() {
4010 check_metrics::<KotlinParser>(
4011 "fun m(x: Int): Int {
4012 return if (x > 0) 1 else -1
4013 }",
4014 "foo.kt",
4015 |metric| {
4016 // condition: > (1) + else (1) = 2
4017 assert_eq!(metric.abc.conditions_sum(), 2.0);
4018 insta::assert_json_snapshot!(metric.abc);
4019 },
4020 );
4021 }
4022
4023 #[test]
4024 fn kotlin_when_entries_count() {
4025 check_metrics::<KotlinParser>(
4026 "fun m(x: Int): Int {
4027 return when (x) {
4028 1 -> 10
4029 2 -> 20
4030 else -> 0
4031 }
4032 }",
4033 "foo.kt",
4034 |metric| {
4035 // Each WhenEntry counts once (including `else`).
4036 assert_eq!(metric.abc.conditions_sum(), 3.0);
4037 insta::assert_json_snapshot!(metric.abc);
4038 },
4039 );
4040 }
4041
4042 #[test]
4043 fn kotlin_catch_block_counts() {
4044 check_metrics::<KotlinParser>(
4045 "fun m() {
4046 try {
4047 println(\"ok\")
4048 } catch (e: Exception) {
4049 println(\"err\")
4050 }
4051 }",
4052 "foo.kt",
4053 |metric| {
4054 // CatchBlock contributes 1 condition.
4055 assert_eq!(metric.abc.conditions_sum(), 1.0);
4056 insta::assert_json_snapshot!(metric.abc);
4057 },
4058 );
4059 }
4060
4061 #[test]
4062 fn kotlin_elvis_and_safe_cast() {
4063 // `?:` (elvis) and `as?` (safe cast) are condition-like.
4064 check_metrics::<KotlinParser>(
4065 "fun m(s: String?): Int {
4066 val n = (s as? Int) ?: 0
4067 return n
4068 }",
4069 "foo.kt",
4070 |metric| {
4071 // as? (+1) + ?: (+1) = 2 conditions.
4072 assert_eq!(metric.abc.conditions_sum(), 2.0);
4073 insta::assert_json_snapshot!(metric.abc);
4074 },
4075 );
4076 }
4077
4078 #[test]
4079 fn kotlin_generic_brackets_not_conditions() {
4080 // `<` / `>` used as type-parameter brackets must not be counted.
4081 check_metrics::<KotlinParser>(
4082 "class Box<T>(val v: T)
4083 fun <T> wrap(x: T): Box<T> = Box(x)",
4084 "foo.kt",
4085 |metric| {
4086 // No comparisons — only generic brackets.
4087 assert_eq!(metric.abc.conditions_sum(), 0.0);
4088 insta::assert_json_snapshot!(metric.abc);
4089 },
4090 );
4091 }
4092
4093 #[test]
4094 fn kotlin_class_with_methods_and_branches() {
4095 check_metrics::<KotlinParser>(
4096 "class C {
4097 var counter: Int = 0
4098 fun bump() {
4099 counter += 1
4100 println(counter)
4101 }
4102 }",
4103 "foo.kt",
4104 |metric| {
4105 // assignments: var counter = 0 (+1), counter += 1 (+1) = 2
4106 // branches: println(counter) = 1
4107 assert_eq!(metric.abc.assignments_sum(), 2.0);
4108 assert_eq!(metric.abc.branches_sum(), 1.0);
4109 assert_eq!(metric.abc.conditions_sum(), 0.0);
4110 insta::assert_json_snapshot!(metric.abc);
4111 },
4112 );
4113 }
4114
4115 #[test]
4116 fn kotlin_object_singleton_abc() {
4117 check_metrics::<KotlinParser>(
4118 "object Util {
4119 fun work(x: Int): Int {
4120 var y = x
4121 y += 1
4122 if (y > 0) {
4123 return y
4124 }
4125 return -1
4126 }
4127 }",
4128 "foo.kt",
4129 |metric| {
4130 // assignments: var y = x (+1), y += 1 (+1) = 2
4131 // branches: 0 (return is not a call)
4132 // conditions: y > 0 (+1) = 1
4133 assert_eq!(metric.abc.assignments_sum(), 2.0);
4134 assert_eq!(metric.abc.branches_sum(), 0.0);
4135 assert_eq!(metric.abc.conditions_sum(), 1.0);
4136 insta::assert_json_snapshot!(metric.abc);
4137 },
4138 );
4139 }
4140
4141 #[test]
4142 fn kotlin_interface_abc() {
4143 // Pure-abstract interface with no bodies — all-zero.
4144 check_metrics::<KotlinParser>(
4145 "interface I {
4146 fun work(): Int
4147 fun describe(): String
4148 }",
4149 "foo.kt",
4150 |metric| {
4151 assert_eq!(metric.abc.assignments_sum(), 0.0);
4152 assert_eq!(metric.abc.branches_sum(), 0.0);
4153 assert_eq!(metric.abc.conditions_sum(), 0.0);
4154 insta::assert_json_snapshot!(metric.abc);
4155 },
4156 );
4157 }
4158
4159 #[test]
4160 fn kotlin_nested_class_abc() {
4161 check_metrics::<KotlinParser>(
4162 "class Outer {
4163 var o: Int = 0
4164 class Nested {
4165 var n: Int = 0
4166 fun bump() { n += 1 }
4167 }
4168 }",
4169 "foo.kt",
4170 |metric| {
4171 // Outer: var o = 0 (+1)
4172 // Nested: var n = 0 (+1), n += 1 (+1) = 2
4173 // total assignments = 3
4174 assert_eq!(metric.abc.assignments_sum(), 3.0);
4175 insta::assert_json_snapshot!(metric.abc);
4176 },
4177 );
4178 }
4179
4180 #[test]
4181 fn kotlin_data_class_abc() {
4182 // `data class` with primary-constructor `val`s — no assignments
4183 // (vals don't count) and no body conditions.
4184 check_metrics::<KotlinParser>(
4185 "data class Point(val x: Int, val y: Int)",
4186 "foo.kt",
4187 |metric| {
4188 assert_eq!(metric.abc.assignments_sum(), 0.0);
4189 assert_eq!(metric.abc.branches_sum(), 0.0);
4190 assert_eq!(metric.abc.conditions_sum(), 0.0);
4191 insta::assert_json_snapshot!(metric.abc);
4192 },
4193 );
4194 }
4195
4196 #[test]
4197 fn kotlin_primary_constructor_default_value_not_assignment() {
4198 // Regression: default values on primary-constructor `val`
4199 // parameters are initialisers, not assignments. Without
4200 // `ClassParameter` pushing a declaration sentinel, the `=` token
4201 // here would be counted unconditionally as a standalone
4202 // assignment.
4203 check_metrics::<KotlinParser>("class C(val a: Int = 5)", "foo.kt", |metric| {
4204 // `val a = 5` → suppressed (Const sentinel).
4205 assert_eq!(metric.abc.assignments_sum(), 0.0);
4206 insta::assert_json_snapshot!(metric.abc);
4207 });
4208 }
4209
4210 // --- TypeScript / TSX ABC tests --------------------------------------
4211 //
4212 // Assignment, branch, condition counting per Fitzpatrick:
4213 // - Augmented assignment / `++` / `--` always count.
4214 // - Plain `=` counts unless inside `const` declaration.
4215 // - `call_expression` / `new_expression` count as branches.
4216 // - Comparison / equality operators, ternary `?`, `??`, control-flow
4217 // arms (`else`, `case`, `default`, `catch`, `try`, `instanceof`),
4218 // and `<`/`>` (outside `type_arguments` / `type_parameters`) count
4219 // as conditions.
4220
4221 #[test]
4222 fn typescript_assignments_basic() {
4223 check_metrics::<TypescriptParser>(
4224 "class C {
4225 m(): void {
4226 let x = 0; // const-sentinel suppressed since `let`, but x is Var → +1
4227 x = 1; // +1
4228 x += 2; // +1
4229 x++; // +1
4230 }
4231 }",
4232 "foo.ts",
4233 |metric| {
4234 assert_eq!(metric.abc.assignments_sum(), 4.0);
4235 insta::assert_json_snapshot!(metric.abc);
4236 },
4237 );
4238 }
4239
4240 #[test]
4241 fn typescript_const_excluded_from_assignments() {
4242 check_metrics::<TypescriptParser>(
4243 "class C {
4244 m(): void {
4245 const a = 1; // suppressed (Const sentinel)
4246 const b = 2; // suppressed
4247 let c = 3; // +1 (Var sentinel)
4248 }
4249 }",
4250 "foo.ts",
4251 |metric| {
4252 assert_eq!(metric.abc.assignments_sum(), 1.0);
4253 insta::assert_json_snapshot!(metric.abc);
4254 },
4255 );
4256 }
4257
4258 #[test]
4259 fn typescript_branches_function_calls() {
4260 check_metrics::<TypescriptParser>(
4261 "class C {
4262 m(): void {
4263 foo(); // +1
4264 bar(1, 2); // +1
4265 new Date(); // +1
4266 }
4267 }",
4268 "foo.ts",
4269 |metric| {
4270 assert_eq!(metric.abc.branches_sum(), 3.0);
4271 insta::assert_json_snapshot!(metric.abc);
4272 },
4273 );
4274 }
4275
4276 #[test]
4277 fn typescript_conditions_comparison_operators() {
4278 check_metrics::<TypescriptParser>(
4279 "class C {
4280 m(x: number, y: number): boolean {
4281 return x == y // +1
4282 || x === y // +1
4283 || x != y // +1
4284 || x !== y // +1
4285 || x < y // +1
4286 || x <= y // +1
4287 || x > y // +1
4288 || x >= y; // +1
4289 }
4290 }",
4291 "foo.ts",
4292 |metric| {
4293 assert_eq!(metric.abc.conditions_sum(), 8.0);
4294 insta::assert_json_snapshot!(metric.abc);
4295 },
4296 );
4297 }
4298
4299 #[test]
4300 fn typescript_conditions_control_flow_arms() {
4301 check_metrics::<TypescriptParser>(
4302 "class C {
4303 m(x: number): number {
4304 try { // +1 (try)
4305 if (x > 0) { // +1 (>)
4306 return 1;
4307 } else { // +1 (else)
4308 return -1;
4309 }
4310 } catch (e) { // +1 (catch)
4311 return 0;
4312 }
4313 }
4314 }",
4315 "foo.ts",
4316 |metric| {
4317 assert_eq!(metric.abc.conditions_sum(), 4.0);
4318 insta::assert_json_snapshot!(metric.abc);
4319 },
4320 );
4321 }
4322
4323 #[test]
4324 fn typescript_conditions_switch_case() {
4325 check_metrics::<TypescriptParser>(
4326 "class C {
4327 m(x: number): number {
4328 switch (x) {
4329 case 1: // +1
4330 return 1;
4331 case 2: // +1
4332 return 2;
4333 default: // +1
4334 return 0;
4335 }
4336 }
4337 }",
4338 "foo.ts",
4339 |metric| {
4340 assert_eq!(metric.abc.conditions_sum(), 3.0);
4341 insta::assert_json_snapshot!(metric.abc);
4342 },
4343 );
4344 }
4345
4346 #[test]
4347 fn typescript_ternary_and_nullish() {
4348 check_metrics::<TypescriptParser>(
4349 "class C {
4350 m(x: number | null): number {
4351 return x !== null // +1 (!==)
4352 ? x // +1 (ternary ?)
4353 : 0;
4354 }
4355 n(x: number | null): number {
4356 return x ?? 0; // +1 (??)
4357 }
4358 }",
4359 "foo.ts",
4360 |metric| {
4361 assert_eq!(metric.abc.conditions_sum(), 3.0);
4362 insta::assert_json_snapshot!(metric.abc);
4363 },
4364 );
4365 }
4366
4367 #[test]
4368 fn typescript_instanceof_counts_as_condition() {
4369 check_metrics::<TypescriptParser>(
4370 "class C {
4371 m(o: unknown): boolean {
4372 return o instanceof C; // +1
4373 }
4374 }",
4375 "foo.ts",
4376 |metric| {
4377 assert_eq!(metric.abc.conditions_sum(), 1.0);
4378 insta::assert_json_snapshot!(metric.abc);
4379 },
4380 );
4381 }
4382
4383 #[test]
4384 fn typescript_generic_lt_gt_not_a_condition() {
4385 // `<T>` in `class C<T>` and `Array<number>` should not contribute
4386 // to conditions even though the tokens are `<` and `>`.
4387 check_metrics::<TypescriptParser>(
4388 "class C<T> {
4389 xs: Array<number> = [];
4390 m(): void {
4391 const arr: Array<string> = []; // suppressed const
4392 void arr;
4393 }
4394 }",
4395 "foo.ts",
4396 |metric| {
4397 assert_eq!(metric.abc.conditions_sum(), 0.0);
4398 insta::assert_json_snapshot!(metric.abc);
4399 },
4400 );
4401 }
4402
4403 #[test]
4404 fn typescript_abstract_class_abc() {
4405 // Abstract methods have no body — they contribute nothing.
4406 check_metrics::<TypescriptParser>(
4407 "abstract class C {
4408 abstract a(): void;
4409 m(x: number): number {
4410 if (x > 0) return 1; // +1 condition
4411 return 0;
4412 }
4413 }",
4414 "foo.ts",
4415 |metric| {
4416 assert_eq!(metric.abc.conditions_sum(), 1.0);
4417 assert_eq!(metric.abc.branches_sum(), 0.0);
4418 insta::assert_json_snapshot!(metric.abc);
4419 },
4420 );
4421 }
4422
4423 #[test]
4424 fn typescript_interface_abc_zero() {
4425 check_metrics::<TypescriptParser>(
4426 "interface I {
4427 a(): void;
4428 b(): number;
4429 p: string;
4430 }",
4431 "foo.ts",
4432 |metric| {
4433 assert_eq!(metric.abc.assignments_sum(), 0.0);
4434 assert_eq!(metric.abc.branches_sum(), 0.0);
4435 assert_eq!(metric.abc.conditions_sum(), 0.0);
4436 insta::assert_json_snapshot!(metric.abc);
4437 },
4438 );
4439 }
4440
4441 #[test]
4442 fn typescript_arrow_field_contributes_abc() {
4443 // Arrow function class members are function spaces; their
4444 // assignments/branches/conditions are counted.
4445 check_metrics::<TypescriptParser>(
4446 "class C {
4447 arrow = (x: number) => {
4448 if (x > 0) { // +1 condition
4449 return foo(); // +1 branch
4450 }
4451 return 0;
4452 };
4453 }",
4454 "foo.ts",
4455 |metric| {
4456 assert_eq!(metric.abc.conditions_sum(), 1.0);
4457 assert_eq!(metric.abc.branches_sum(), 1.0);
4458 insta::assert_json_snapshot!(metric.abc);
4459 },
4460 );
4461 }
4462
4463 #[test]
4464 fn typescript_parameter_property_init_not_assignment() {
4465 // Parameter properties don't introduce a `=` token themselves;
4466 // only the explicit `let z = 0` body assignment is counted.
4467 // The class field initializer `f: number = 0` likewise has a `=`
4468 // that DOES count (matches `typescript_assignments_basic`).
4469 check_metrics::<TypescriptParser>(
4470 "class C {
4471 f: number = 0;
4472 constructor(public x: number, private y: string) {
4473 let z = 0;
4474 }
4475 }",
4476 "foo.ts",
4477 |metric| {
4478 // f's initializer + `let z = 0` = 2 assignments; the
4479 // parameter properties contribute zero.
4480 assert_eq!(metric.abc.assignments_sum(), 2.0);
4481 insta::assert_json_snapshot!(metric.abc);
4482 },
4483 );
4484 }
4485
4486 // TSX parity
4487
4488 #[test]
4489 fn tsx_assignments_basic() {
4490 check_metrics::<TsxParser>(
4491 "class C {
4492 m(): void {
4493 let x = 0;
4494 x = 1;
4495 x += 2;
4496 x++;
4497 }
4498 }",
4499 "foo.tsx",
4500 |metric| {
4501 assert_eq!(metric.abc.assignments_sum(), 4.0);
4502 insta::assert_json_snapshot!(metric.abc);
4503 },
4504 );
4505 }
4506
4507 #[test]
4508 fn tsx_const_excluded_from_assignments() {
4509 check_metrics::<TsxParser>(
4510 "class C {
4511 m(): void {
4512 const a = 1;
4513 let b = 2;
4514 }
4515 }",
4516 "foo.tsx",
4517 |metric| {
4518 assert_eq!(metric.abc.assignments_sum(), 1.0);
4519 insta::assert_json_snapshot!(metric.abc);
4520 },
4521 );
4522 }
4523
4524 #[test]
4525 fn tsx_branches_function_calls() {
4526 check_metrics::<TsxParser>(
4527 "class C {
4528 m(): void {
4529 foo();
4530 new Date();
4531 }
4532 }",
4533 "foo.tsx",
4534 |metric| {
4535 assert_eq!(metric.abc.branches_sum(), 2.0);
4536 insta::assert_json_snapshot!(metric.abc);
4537 },
4538 );
4539 }
4540
4541 #[test]
4542 fn tsx_conditions_comparison_operators() {
4543 check_metrics::<TsxParser>(
4544 "class C {
4545 m(x: number, y: number): boolean {
4546 return x == y || x < y || x >= y;
4547 }
4548 }",
4549 "foo.tsx",
4550 |metric| {
4551 assert_eq!(metric.abc.conditions_sum(), 3.0);
4552 insta::assert_json_snapshot!(metric.abc);
4553 },
4554 );
4555 }
4556
4557 #[test]
4558 fn tsx_conditions_control_flow_arms() {
4559 check_metrics::<TsxParser>(
4560 "class C {
4561 m(x: number): number {
4562 try {
4563 if (x > 0) return 1;
4564 else return -1;
4565 } catch (e) {
4566 return 0;
4567 }
4568 }
4569 }",
4570 "foo.tsx",
4571 |metric| {
4572 assert_eq!(metric.abc.conditions_sum(), 4.0);
4573 insta::assert_json_snapshot!(metric.abc);
4574 },
4575 );
4576 }
4577
4578 #[test]
4579 fn tsx_conditions_switch_case() {
4580 check_metrics::<TsxParser>(
4581 "class C {
4582 m(x: number): number {
4583 switch (x) {
4584 case 1: return 1;
4585 case 2: return 2;
4586 default: return 0;
4587 }
4588 }
4589 }",
4590 "foo.tsx",
4591 |metric| {
4592 assert_eq!(metric.abc.conditions_sum(), 3.0);
4593 insta::assert_json_snapshot!(metric.abc);
4594 },
4595 );
4596 }
4597
4598 #[test]
4599 fn tsx_ternary_and_nullish() {
4600 check_metrics::<TsxParser>(
4601 "class C {
4602 m(x: number | null): number {
4603 return x !== null ? x : 0;
4604 }
4605 n(x: number | null): number { return x ?? 0; }
4606 }",
4607 "foo.tsx",
4608 |metric| {
4609 assert_eq!(metric.abc.conditions_sum(), 3.0);
4610 insta::assert_json_snapshot!(metric.abc);
4611 },
4612 );
4613 }
4614
4615 #[test]
4616 fn tsx_instanceof_counts_as_condition() {
4617 check_metrics::<TsxParser>(
4618 "class C { m(o: unknown): boolean { return o instanceof C; } }",
4619 "foo.tsx",
4620 |metric| {
4621 assert_eq!(metric.abc.conditions_sum(), 1.0);
4622 insta::assert_json_snapshot!(metric.abc);
4623 },
4624 );
4625 }
4626
4627 #[test]
4628 fn tsx_generic_lt_gt_not_a_condition() {
4629 check_metrics::<TsxParser>(
4630 "class C<T> { xs: Array<number> = []; }",
4631 "foo.tsx",
4632 |metric| {
4633 assert_eq!(metric.abc.conditions_sum(), 0.0);
4634 insta::assert_json_snapshot!(metric.abc);
4635 },
4636 );
4637 }
4638
4639 #[test]
4640 fn tsx_abstract_class_abc() {
4641 check_metrics::<TsxParser>(
4642 "abstract class C {
4643 abstract a(): void;
4644 m(x: number): number {
4645 if (x > 0) return 1;
4646 return 0;
4647 }
4648 }",
4649 "foo.tsx",
4650 |metric| {
4651 assert_eq!(metric.abc.conditions_sum(), 1.0);
4652 assert_eq!(metric.abc.branches_sum(), 0.0);
4653 insta::assert_json_snapshot!(metric.abc);
4654 },
4655 );
4656 }
4657
4658 #[test]
4659 fn tsx_interface_abc_zero() {
4660 check_metrics::<TsxParser>(
4661 "interface I { a(): void; p: string; }",
4662 "foo.tsx",
4663 |metric| {
4664 assert_eq!(metric.abc.assignments_sum(), 0.0);
4665 assert_eq!(metric.abc.branches_sum(), 0.0);
4666 assert_eq!(metric.abc.conditions_sum(), 0.0);
4667 insta::assert_json_snapshot!(metric.abc);
4668 },
4669 );
4670 }
4671
4672 #[test]
4673 fn tsx_arrow_field_contributes_abc() {
4674 check_metrics::<TsxParser>(
4675 "class C {
4676 arrow = (x: number) => {
4677 if (x > 0) return foo();
4678 return 0;
4679 };
4680 }",
4681 "foo.tsx",
4682 |metric| {
4683 assert_eq!(metric.abc.conditions_sum(), 1.0);
4684 assert_eq!(metric.abc.branches_sum(), 1.0);
4685 insta::assert_json_snapshot!(metric.abc);
4686 },
4687 );
4688 }
4689
4690 #[test]
4691 fn tsx_parameter_property_init_not_assignment() {
4692 // Parameter properties contribute no `=`; the body's `let z = 0`
4693 // and the field initializer do.
4694 check_metrics::<TsxParser>(
4695 "class C {
4696 f: number = 0;
4697 constructor(public x: number) { let z = 0; }
4698 }",
4699 "foo.tsx",
4700 |metric| {
4701 assert_eq!(metric.abc.assignments_sum(), 2.0);
4702 insta::assert_json_snapshot!(metric.abc);
4703 },
4704 );
4705 }
4706
4707 // --- Ruby ABC tests ---------------------------------------------------
4708 //
4709 // Each Ruby `assignment` / `operator_assignment` is one assignment
4710 // regardless of whether the LHS is a local, instance, or class
4711 // variable. Every `call` / `super` / `yield` is one branch. Every
4712 // comparison-operator token inside a `binary` node plus each
4713 // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is
4714 // one condition.
4715
4716 #[test]
4717 fn ruby_zero_abc() {
4718 check_metrics::<RubyParser>("\n", "foo.rb", |metric| {
4719 assert_eq!(metric.abc.assignments_sum(), 0.0);
4720 assert_eq!(metric.abc.branches_sum(), 0.0);
4721 assert_eq!(metric.abc.conditions_sum(), 0.0);
4722 insta::assert_json_snapshot!(metric.abc);
4723 });
4724 }
4725
4726 #[test]
4727 fn ruby_simple_assignment() {
4728 check_metrics::<RubyParser>("def f\n a = 1\n b = 2\nend\n", "foo.rb", |metric| {
4729 assert_eq!(metric.abc.assignments_sum(), 2.0);
4730 assert_eq!(metric.abc.branches_sum(), 0.0);
4731 assert_eq!(metric.abc.conditions_sum(), 0.0);
4732 insta::assert_json_snapshot!(metric.abc);
4733 });
4734 }
4735
4736 #[test]
4737 fn ruby_augmented_assignment() {
4738 // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is
4739 // one assignment. Plain `=` to set the initial value adds one
4740 // more.
4741 check_metrics::<RubyParser>(
4742 "def f(x)\n a = 0\n a += x\n a -= 1\n a *= 2\nend\n",
4743 "foo.rb",
4744 |metric| {
4745 assert_eq!(metric.abc.assignments_sum(), 4.0);
4746 insta::assert_json_snapshot!(metric.abc);
4747 },
4748 );
4749 }
4750
4751 #[test]
4752 fn ruby_logical_augmented_assignment() {
4753 // `||=` and `&&=` are also `operator_assignment` nodes.
4754 check_metrics::<RubyParser>("def f\n @x ||= 0\n @x &&= 1\nend\n", "foo.rb", |metric| {
4755 assert_eq!(metric.abc.assignments_sum(), 2.0);
4756 insta::assert_json_snapshot!(metric.abc);
4757 });
4758 }
4759
4760 #[test]
4761 fn ruby_method_call_branch() {
4762 // Each method invocation is one branch.
4763 check_metrics::<RubyParser>(
4764 "def f(obj)\n foo()\n obj.bar(1)\nend\n",
4765 "foo.rb",
4766 |metric| {
4767 assert_eq!(metric.abc.branches_sum(), 2.0);
4768 insta::assert_json_snapshot!(metric.abc);
4769 },
4770 );
4771 }
4772
4773 #[test]
4774 fn ruby_super_and_yield_branches() {
4775 // `super` and `yield` both count as branches (control-pass).
4776 check_metrics::<RubyParser>("def f\n super\n yield\nend\n", "foo.rb", |metric| {
4777 assert_eq!(metric.abc.branches_sum(), 2.0);
4778 assert_eq!(metric.abc.assignments_sum(), 0.0);
4779 insta::assert_json_snapshot!(metric.abc);
4780 });
4781 }
4782
4783 #[test]
4784 fn ruby_attr_macro_is_branch() {
4785 // `attr_accessor` is a `Call3` node and registers as a branch
4786 // like any method invocation.
4787 check_metrics::<RubyParser>("class A\n attr_accessor :x\nend\n", "foo.rb", |metric| {
4788 assert_eq!(metric.abc.branches_sum(), 1.0);
4789 insta::assert_json_snapshot!(metric.abc);
4790 });
4791 }
4792
4793 #[test]
4794 fn ruby_comparison_conditions() {
4795 // Each comparison operator is one condition.
4796 check_metrics::<RubyParser>(
4797 "def f(a, b)\n a == b\n a != b\n a < b\n a > b\n a <= b\n a >= b\nend\n",
4798 "foo.rb",
4799 |metric| {
4800 assert_eq!(metric.abc.conditions_sum(), 6.0);
4801 insta::assert_json_snapshot!(metric.abc);
4802 },
4803 );
4804 }
4805
4806 #[test]
4807 fn ruby_spaceship_and_case_equality() {
4808 // `<=>` and `===` are comparison operators (conditions).
4809 check_metrics::<RubyParser>(
4810 "def f(a, b)\n a <=> b\n a === b\nend\n",
4811 "foo.rb",
4812 |metric| {
4813 assert_eq!(metric.abc.conditions_sum(), 2.0);
4814 insta::assert_json_snapshot!(metric.abc);
4815 },
4816 );
4817 }
4818
4819 #[test]
4820 fn ruby_ternary_condition() {
4821 // The `?` ternary marker is one condition; the inner `==` is
4822 // another.
4823 check_metrics::<RubyParser>("def f(x)\n x == 0 ? :z : :nz\nend\n", "foo.rb", |metric| {
4824 assert_eq!(metric.abc.conditions_sum(), 2.0);
4825 insta::assert_json_snapshot!(metric.abc);
4826 });
4827 }
4828
4829 #[test]
4830 fn ruby_case_when_arms() {
4831 // Each `when` named clause and the `else` clause count as one
4832 // condition each; the `case` head and the implicit `then`
4833 // wrappers do not.
4834 check_metrics::<RubyParser>(
4835 "def f(x)\n case x\n when 1 then 'one'\n when 2 then 'two'\n else 'other'\n end\nend\n",
4836 "foo.rb",
4837 |metric| {
4838 // 2 `when` + 1 `else` = 3 conditions.
4839 assert_eq!(metric.abc.conditions_sum(), 3.0);
4840 insta::assert_json_snapshot!(metric.abc);
4841 },
4842 );
4843 }
4844
4845 #[test]
4846 fn ruby_elsif_and_else() {
4847 // `elsif` and `else` named clauses are conditions; their inner
4848 // `then` wrappers are not.
4849 check_metrics::<RubyParser>(
4850 "def f(x)\n if x > 0\n 1\n elsif x < 0\n -1\n else\n 0\n end\nend\n",
4851 "foo.rb",
4852 |metric| {
4853 // `>`(1) + `elsif`(1) + `<`(1) + `else`(1) = 4.
4854 assert_eq!(metric.abc.conditions_sum(), 4.0);
4855 insta::assert_json_snapshot!(metric.abc);
4856 },
4857 );
4858 }
4859
4860 #[test]
4861 fn ruby_rescue_clause_condition() {
4862 // The `rescue` named clause is one condition; the `rescue`
4863 // keyword token (`Rescue2`) is not counted on its own.
4864 // `do_it` without parens is an `identifier`, not a `call`, so
4865 // it contributes no branch. `handle(e)` is a `call` (1 branch).
4866 check_metrics::<RubyParser>(
4867 "def f\n begin\n do_it\n rescue StandardError => e\n handle(e)\n end\nend\n",
4868 "foo.rb",
4869 |metric| {
4870 assert_eq!(metric.abc.conditions_sum(), 1.0);
4871 assert_eq!(metric.abc.branches_sum(), 1.0);
4872 insta::assert_json_snapshot!(metric.abc);
4873 },
4874 );
4875 }
4876
4877 #[test]
4878 fn ruby_class_complex_function() {
4879 // Mixed: assignment(=), branch(call), conditions(`>` and `==`).
4880 check_metrics::<RubyParser>(
4881 "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",
4882 "foo.rb",
4883 |metric| {
4884 assert_eq!(metric.abc.assignments_sum(), 1.0);
4885 assert_eq!(metric.abc.branches_sum(), 1.0);
4886 // `>`(1) + `==`(1) = 2 conditions. `if` is not a token;
4887 // `&&` is `AMPAMP` which is NOT a Fitzpatrick condition
4888 // in our Ruby impl (it's a logical operator, not a
4889 // comparison). The Fitzpatrick paper allows either
4890 // choice; we follow the comparison-only rule like
4891 // Java/PHP.
4892 assert_eq!(metric.abc.conditions_sum(), 2.0);
4893 insta::assert_json_snapshot!(metric.abc);
4894 },
4895 );
4896 }
4897
4898 // ---------------------------------------------------------------
4899 // Default-impl placeholder smoke tests (audited in #188).
4900 //
4901 // These tests assert that the *current* default-impl languages
4902 // return ABC = 0/0/0 for source that DOES contain branches,
4903 // conditions, and assignments. When the real impl lands for any
4904 // of these languages, the corresponding assertion below will fire
4905 // — the implementer must update the expected values, which is the
4906 // gate. Tag the follow-up issue in each test.
4907 // ---------------------------------------------------------------
4908
4909 // --- Python ABC ---------------------------------------------------
4910
4911 #[test]
4912 fn python_empty_module_zero() {
4913 check_metrics::<PythonParser>("", "empty.py", |metric| {
4914 assert_eq!(metric.abc.assignments_sum(), 0.0);
4915 assert_eq!(metric.abc.branches_sum(), 0.0);
4916 assert_eq!(metric.abc.conditions_sum(), 0.0);
4917 insta::assert_json_snapshot!(metric.abc);
4918 });
4919 }
4920
4921 #[test]
4922 fn python_plain_assignments_count() {
4923 // Three plain `=` assignments → A=3. No branches, no conditions.
4924 check_metrics::<PythonParser>("x = 1\ny = 2\nz = x\n", "foo.py", |metric| {
4925 assert_eq!(metric.abc.assignments_sum(), 3.0);
4926 assert_eq!(metric.abc.branches_sum(), 0.0);
4927 assert_eq!(metric.abc.conditions_sum(), 0.0);
4928 insta::assert_json_snapshot!(metric.abc);
4929 });
4930 }
4931
4932 #[test]
4933 fn python_typed_assignment_counts_bare_annotation_does_not() {
4934 // `x: int = 1` carries an `=`, so it counts.
4935 // `y: int` is a bare annotation (no `=`) — declares a type but
4936 // binds nothing; it must NOT inflate the assignment count.
4937 check_metrics::<PythonParser>("x: int = 1\ny: int\n", "foo.py", |metric| {
4938 assert_eq!(metric.abc.assignments_sum(), 1.0);
4939 insta::assert_json_snapshot!(metric.abc);
4940 });
4941 }
4942
4943 #[test]
4944 fn python_augmented_assignments_count() {
4945 // Each augmented op counts once.
4946 check_metrics::<PythonParser>("x = 0\nx += 1\nx -= 1\nx *= 2\n", "foo.py", |metric| {
4947 // 1 plain `=` + 3 augmented = 4 assignments.
4948 assert_eq!(metric.abc.assignments_sum(), 4.0);
4949 insta::assert_json_snapshot!(metric.abc);
4950 });
4951 }
4952
4953 #[test]
4954 fn python_walrus_counts_as_assignment() {
4955 // `x := 10` is a `NamedExpression` (PEP 572). It binds a value
4956 // → one assignment under Fitzpatrick's rule.
4957 check_metrics::<PythonParser>("if (n := 10) > 5:\n pass\n", "foo.py", |metric| {
4958 // 1 assignment (walrus) + 1 condition (`> 5` is a
4959 // ComparisonOperator).
4960 assert_eq!(metric.abc.assignments_sum(), 1.0);
4961 assert_eq!(metric.abc.conditions_sum(), 1.0);
4962 insta::assert_json_snapshot!(metric.abc);
4963 });
4964 }
4965
4966 #[test]
4967 fn python_calls_are_branches() {
4968 // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call`
4969 // → three branches.
4970 check_metrics::<PythonParser>(
4971 "def foo():\n pass\ndef bar():\n pass\nclass Baz:\n pass\nfoo()\nbar()\nBaz()\n",
4972 "foo.py",
4973 |metric| {
4974 assert_eq!(metric.abc.branches_sum(), 3.0);
4975 assert_eq!(metric.abc.assignments_sum(), 0.0);
4976 insta::assert_json_snapshot!(metric.abc);
4977 },
4978 );
4979 }
4980
4981 #[test]
4982 fn python_comparisons_count_conditions() {
4983 // `x > 0`, `x == y`, `x is None` are each a single
4984 // `ComparisonOperator` node — three conditions.
4985 check_metrics::<PythonParser>(
4986 "def f(x, y):\n a = x > 0\n b = x == y\n c = x is None\n",
4987 "foo.py",
4988 |metric| {
4989 assert_eq!(metric.abc.conditions_sum(), 3.0);
4990 // 3 plain assignments; the comparisons are operands.
4991 assert_eq!(metric.abc.assignments_sum(), 3.0);
4992 insta::assert_json_snapshot!(metric.abc);
4993 },
4994 );
4995 }
4996
4997 #[test]
4998 fn python_chained_comparison_counts_once() {
4999 // tree-sitter-python collapses `0 < x < 10` into a single
5000 // `ComparisonOperator` — one condition, not two.
5001 check_metrics::<PythonParser>("def f(x):\n return 0 < x < 10\n", "foo.py", |metric| {
5002 assert_eq!(metric.abc.conditions_sum(), 1.0);
5003 insta::assert_json_snapshot!(metric.abc);
5004 });
5005 }
5006
5007 #[test]
5008 fn python_boolean_operators_count_conditions() {
5009 // `and` / `or` are each a `BooleanOperator` node → one condition
5010 // per logical-binop instance.
5011 check_metrics::<PythonParser>(
5012 "def f(a, b, c):\n if a and b or c:\n pass\n",
5013 "foo.py",
5014 |metric| {
5015 // `a and b or c` parses as `BooleanOperator(or,
5016 // BooleanOperator(and, a, b), c)` → 2 BooleanOperator
5017 // nodes → 2 conditions.
5018 assert_eq!(metric.abc.conditions_sum(), 2.0);
5019 insta::assert_json_snapshot!(metric.abc);
5020 },
5021 );
5022 }
5023
5024 /// Python's unary `not` operator parses as `NotOperator` and now
5025 /// counts as one condition, matching Java's `!x` rule. Closes
5026 /// the parity gap noted in #214: without this, `if not flag:`
5027 /// reported 0 conditions while the Java equivalent reports 1.
5028 #[test]
5029 fn python_unary_not_counts_as_condition() {
5030 check_metrics::<PythonParser>(
5031 "def f(flag):\n if not flag:\n return 1\n return 0\n",
5032 "foo.py",
5033 |metric| {
5034 // One `NotOperator` -> 1 condition. The `if` itself
5035 // is structural and doesn't add an Abc condition.
5036 assert_eq!(metric.abc.conditions_sum(), 1.0);
5037 insta::assert_json_snapshot!(metric.abc);
5038 },
5039 );
5040 }
5041
5042 /// `return not flag` — the unary `not` is the entire return
5043 /// expression. Without `NotOperator` counted, this reports zero
5044 /// conditions; with it, one. Java's `return !flag;` is one.
5045 #[test]
5046 fn python_return_unary_not_counts() {
5047 check_metrics::<PythonParser>("def f(flag):\n return not flag\n", "foo.py", |metric| {
5048 assert_eq!(metric.abc.conditions_sum(), 1.0);
5049 insta::assert_json_snapshot!(metric.abc);
5050 });
5051 }
5052
5053 /// `foo(not ready, value)` — the unary `not` inside an argument
5054 /// list still contributes. Mirrors Java's
5055 /// `java_count_unary_conditions` walk over argument lists.
5056 #[test]
5057 fn python_unary_not_in_argument_list_counts() {
5058 check_metrics::<PythonParser>(
5059 "def f(ready, value):\n log(not ready, value)\n",
5060 "foo.py",
5061 |metric| {
5062 // 1 Call (log) -> 1 branch.
5063 // 1 NotOperator (not ready) -> 1 condition.
5064 assert_eq!(metric.abc.branches_sum(), 1.0);
5065 assert_eq!(metric.abc.conditions_sum(), 1.0);
5066 insta::assert_json_snapshot!(metric.abc);
5067 },
5068 );
5069 }
5070
5071 /// Nested `not` + comparison counts each unique node once.
5072 /// `not (x > 0)` parses as `NotOperator(ParenthesizedExpression(
5073 /// ComparisonOperator))`; both the unary and the comparison
5074 /// contribute one condition (mirrors Java's `!(x > 0)` = 2
5075 /// conditions).
5076 #[test]
5077 fn python_unary_not_with_comparison_counts_each_once() {
5078 check_metrics::<PythonParser>(
5079 "def f(x):\n if not (x > 0):\n return 1\n return 0\n",
5080 "foo.py",
5081 |metric| {
5082 // NotOperator (1) + ComparisonOperator (1) = 2.
5083 assert_eq!(metric.abc.conditions_sum(), 2.0);
5084 insta::assert_json_snapshot!(metric.abc);
5085 },
5086 );
5087 }
5088
5089 /// `not x and y` parses as `BooleanOperator(NotOperator(x), and,
5090 /// y)`. The BooleanOperator counts (and/or = 1 condition); the
5091 /// inner NotOperator also counts. Total: 2.
5092 #[test]
5093 fn python_unary_not_with_boolean_combinator_counts_each() {
5094 check_metrics::<PythonParser>(
5095 "def f(x, y):\n if not x and y:\n return 1\n return 0\n",
5096 "foo.py",
5097 |metric| {
5098 // BooleanOperator (1) + NotOperator (1) = 2.
5099 assert_eq!(metric.abc.conditions_sum(), 2.0);
5100 insta::assert_json_snapshot!(metric.abc);
5101 },
5102 );
5103 }
5104
5105 #[test]
5106 fn python_control_flow_arms_count_conditions() {
5107 // `elif`, `else`, `except`, `finally`, `case` each contribute
5108 // one condition. The comparisons in the `if`/`elif`/`while`
5109 // headers contribute their own ComparisonOperator counts.
5110 check_metrics::<PythonParser>(
5111 "def f(x):\n if x > 0:\n a = 1\n elif x > -1:\n a = 2\n else:\n a = 3\n",
5112 "foo.py",
5113 |metric| {
5114 // 2 ComparisonOperator (`x > 0`, `x > -1`) + 1
5115 // ElifClause + 1 ElseClause = 4 conditions.
5116 assert_eq!(metric.abc.conditions_sum(), 4.0);
5117 insta::assert_json_snapshot!(metric.abc);
5118 },
5119 );
5120 }
5121
5122 #[test]
5123 fn python_ternary_counts_as_condition() {
5124 // `a if c else b` is `ConditionalExpression` → 1 condition.
5125 // `c > 0` adds 1 more (ComparisonOperator).
5126 check_metrics::<PythonParser>(
5127 "def f(c):\n return 1 if c > 0 else 0\n",
5128 "foo.py",
5129 |metric| {
5130 assert_eq!(metric.abc.conditions_sum(), 2.0);
5131 insta::assert_json_snapshot!(metric.abc);
5132 },
5133 );
5134 }
5135
5136 #[test]
5137 fn python_try_except_finally_count_conditions() {
5138 // ExceptClause + FinallyClause → 2 conditions.
5139 check_metrics::<PythonParser>(
5140 "def f():\n try:\n pass\n except ValueError:\n pass\n finally:\n pass\n",
5141 "foo.py",
5142 |metric| {
5143 assert_eq!(metric.abc.conditions_sum(), 2.0);
5144 insta::assert_json_snapshot!(metric.abc);
5145 },
5146 );
5147 }
5148
5149 #[test]
5150 fn python_match_case_counts_conditions() {
5151 // Each non-wildcard `CaseClause` → 1 condition. The bare
5152 // `case _:` arm is the language-neutral `default:` equivalent
5153 // and is excluded (matches Rust's bare-`_` MatchArm filter and
5154 // Java/C#'s `default:` rule). Source has `case 1:` (counts) +
5155 // `case _:` (excluded) → C = 1.
5156 check_metrics::<PythonParser>(
5157 "def f(x):\n match x:\n case 1:\n pass\n case _:\n pass\n",
5158 "foo.py",
5159 |metric| {
5160 assert_eq!(metric.abc.conditions_sum(), 1.0);
5161 insta::assert_json_snapshot!(metric.abc);
5162 },
5163 );
5164 }
5165
5166 #[test]
5167 fn python_match_case_guarded_wildcard_counts() {
5168 // `case _ if g:` is NOT a bare wildcard — the guard
5169 // contributes real branching, so the arm counts as a
5170 // condition. Mirrors Rust's `_ if g => ...` behavior.
5171 // Source: `case 1:` (counts) + `case _ if x > 0:` (guarded
5172 // wildcard, counts) + `case _:` (bare wildcard, excluded) →
5173 // C from CaseClause = 2; the guard's `x > 0` adds one
5174 // ComparisonOperator → total C = 3.
5175 check_metrics::<PythonParser>(
5176 "def f(x):\n match x:\n case 1:\n pass\n case _ if x > 0:\n pass\n case _:\n pass\n",
5177 "foo.py",
5178 |metric| {
5179 assert_eq!(metric.abc.conditions_sum(), 3.0);
5180 insta::assert_json_snapshot!(metric.abc);
5181 },
5182 );
5183 }
5184
5185 #[test]
5186 fn python_complex_function_abc() {
5187 // Mixed-shape regression: assignments, calls, conditions all in
5188 // a single function.
5189 check_metrics::<PythonParser>(
5190 "def f(items, threshold):\n\
5191 \x20 result = []\n\
5192 \x20 for item in items:\n\
5193 \x20 if item > threshold:\n\
5194 \x20 result.append(item)\n\
5195 \x20 return result\n",
5196 "foo.py",
5197 |metric| {
5198 // assignments: `result = []` → 1
5199 // branches: `result.append(item)` is one call → 1
5200 // conditions: `item > threshold` is one
5201 // ComparisonOperator → 1
5202 assert_eq!(metric.abc.assignments_sum(), 1.0);
5203 assert_eq!(metric.abc.branches_sum(), 1.0);
5204 assert_eq!(metric.abc.conditions_sum(), 1.0);
5205 insta::assert_json_snapshot!(metric.abc);
5206 },
5207 );
5208 }
5209
5210 #[test]
5211 fn rust_empty_unit_zero() {
5212 // No code at all → A=B=C=0. Establishes the trait is wired up
5213 // and the per-language compute is reachable.
5214 check_metrics::<RustParser>("", "empty.rs", |metric| {
5215 assert_eq!(metric.abc.assignments_sum(), 0.0);
5216 assert_eq!(metric.abc.branches_sum(), 0.0);
5217 assert_eq!(metric.abc.conditions_sum(), 0.0);
5218 insta::assert_json_snapshot!(metric.abc);
5219 });
5220 }
5221
5222 #[test]
5223 fn rust_assignments_count_outside_let() {
5224 // `let x = 0` is a declaration — its `=` is NOT a Fitzpatrick
5225 // assignment (mirrors Java's local-variable-declaration rule).
5226 // `x = 5` and `x = 7` are plain `=` assignments → 2. `x += 2`
5227 // is a compound assignment → 1. Total A = 3.
5228 check_metrics::<RustParser>(
5229 "fn f() { let mut x = 0; x = 5; x += 2; x = 7; }",
5230 "foo.rs",
5231 |metric| {
5232 assert_eq!(metric.abc.assignments_sum(), 3.0);
5233 assert_eq!(metric.abc.branches_sum(), 0.0);
5234 assert_eq!(metric.abc.conditions_sum(), 0.0);
5235 insta::assert_json_snapshot!(metric.abc);
5236 },
5237 );
5238 }
5239
5240 #[test]
5241 fn rust_calls_are_branches() {
5242 // Free function call + method call (parses as call_expression
5243 // with a field_expression callee) + associated-fn call. All
5244 // three are `call_expression` → B = 3. Macro invocations like
5245 // `println!` parse as `macro_invocation`, NOT `call_expression`,
5246 // so they are not branches.
5247 check_metrics::<RustParser>(
5248 "fn f() { g(); 1.to_string(); String::new(); }\nfn g() {}\n",
5249 "foo.rs",
5250 |metric| {
5251 assert_eq!(metric.abc.branches_sum(), 3.0);
5252 assert_eq!(metric.abc.assignments_sum(), 0.0);
5253 assert_eq!(metric.abc.conditions_sum(), 0.0);
5254 insta::assert_json_snapshot!(metric.abc);
5255 },
5256 );
5257 }
5258
5259 #[test]
5260 fn rust_try_operator_is_branch() {
5261 // `?` parses as `try_expression` and counts as one branch
5262 // (short-circuit return on Err / None). The `Err(())` call
5263 // contributes one branch in addition (call_expression).
5264 check_metrics::<RustParser>(
5265 "fn f() -> Result<i32, ()> { let r: Result<i32, ()> = Err(()); Ok(r?) }",
5266 "foo.rs",
5267 |metric| {
5268 // Err(()) + Ok(...) + r? → 2 calls + 1 try = 3 branches.
5269 assert_eq!(metric.abc.branches_sum(), 3.0);
5270 insta::assert_json_snapshot!(metric.abc);
5271 },
5272 );
5273 }
5274
5275 #[test]
5276 fn rust_comparisons_count_conditions() {
5277 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
5278 // comparisons → C = 6.
5279 check_metrics::<RustParser>(
5280 "fn f(a: i32, b: i32) -> bool { a < b || a > b || a <= b || a >= b || a == b || a != b }",
5281 "foo.rs",
5282 |metric| {
5283 assert_eq!(metric.abc.conditions_sum(), 6.0);
5284 insta::assert_json_snapshot!(metric.abc);
5285 },
5286 );
5287 }
5288
5289 #[test]
5290 fn rust_generic_brackets_not_conditions() {
5291 // `<` / `>` in `Vec<i32>` are TypeArguments delimiters, not
5292 // comparison operators. The parent-check in the LT/GT arms
5293 // must filter them out. Expected C = 0.
5294 check_metrics::<RustParser>(
5295 "fn f() -> Vec<i32> { Vec::<i32>::new() }",
5296 "foo.rs",
5297 |metric| {
5298 assert_eq!(metric.abc.conditions_sum(), 0.0);
5299 insta::assert_json_snapshot!(metric.abc);
5300 },
5301 );
5302 }
5303
5304 #[test]
5305 fn rust_if_let_counts_as_condition() {
5306 // `if let Some(v) = opt { ... }` introduces a `let_condition`
5307 // → 1 condition. The `if` keyword itself does not add another
5308 // count — Fitzpatrick counts conditions, not branch keywords.
5309 check_metrics::<RustParser>(
5310 "fn f(opt: Option<i32>) { if let Some(_v) = opt { } }",
5311 "foo.rs",
5312 |metric| {
5313 assert_eq!(metric.abc.conditions_sum(), 1.0);
5314 insta::assert_json_snapshot!(metric.abc);
5315 },
5316 );
5317 }
5318
5319 #[test]
5320 fn rust_while_let_counts_as_condition() {
5321 // `while let Some(y) = it.next() { ... }` is also a
5322 // `let_condition` (the `while` form). One condition; the
5323 // `it.next()` call adds one branch.
5324 check_metrics::<RustParser>(
5325 "fn f(mut it: std::vec::IntoIter<i32>) { while let Some(_y) = it.next() { } }",
5326 "foo.rs",
5327 |metric| {
5328 assert_eq!(metric.abc.conditions_sum(), 1.0);
5329 assert_eq!(metric.abc.branches_sum(), 1.0);
5330 insta::assert_json_snapshot!(metric.abc);
5331 },
5332 );
5333 }
5334
5335 #[test]
5336 fn rust_match_arms_count_conditions_wildcard_excluded() {
5337 // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The
5338 // bare wildcard is the `default:` equivalent and is skipped.
5339 // The guarded arm has a `n if n > 0` pattern (more than one
5340 // child in the match_pattern) and still counts. Two non-wildcard
5341 // arms → C = 2 from MatchArm. Plus the comparison `n > 0`
5342 // adds one more → C = 3.
5343 check_metrics::<RustParser>(
5344 "fn f(x: i32) -> i32 { match x { 0 => 1, n if n > 0 => n, _ => -1, } }",
5345 "foo.rs",
5346 |metric| {
5347 assert_eq!(metric.abc.conditions_sum(), 3.0);
5348 insta::assert_json_snapshot!(metric.abc);
5349 },
5350 );
5351 }
5352
5353 #[test]
5354 fn rust_else_counts_as_condition() {
5355 // `if a > b { ... } else { ... }` → `a > b` is one condition,
5356 // `else` is one condition → C = 2.
5357 check_metrics::<RustParser>(
5358 "fn f(a: i32, b: i32) -> i32 { if a > b { a } else { b } }",
5359 "foo.rs",
5360 |metric| {
5361 assert_eq!(metric.abc.conditions_sum(), 2.0);
5362 insta::assert_json_snapshot!(metric.abc);
5363 },
5364 );
5365 }
5366
5367 #[test]
5368 fn rust_complex_function_abc() {
5369 // Mixed-shape regression: assignments, calls, conditions, `?`,
5370 // `if let`, `match` in one body. Verified by hand:
5371 // - assignments: `x = 5`, `x += 2` → A = 2 (the `let` initialisers
5372 // are not assignments).
5373 // - branches: `xs.iter()`, `.next()`, `Err(())`, `r?` → B = 4
5374 // (3 calls + 1 try).
5375 // - conditions: `if let Some(v) = opt` → 1, `match x` arms
5376 // `0`, `n if n>0` (wildcard excluded) → 2, `n > 0` → 1.
5377 // Total C = 4.
5378 check_metrics::<RustParser>(
5379 "fn f(opt: Option<i32>, xs: Vec<i32>) -> Result<i32, ()> {\n\
5380 \x20 let mut x = 0;\n\
5381 \x20 x = 5;\n\
5382 \x20 x += 2;\n\
5383 \x20 if let Some(_v) = opt { }\n\
5384 \x20 let _ = xs.iter().next();\n\
5385 \x20 let r: Result<i32, ()> = Err(());\n\
5386 \x20 let _v = r?;\n\
5387 \x20 Ok(match x {\n\
5388 \x20 0 => 1,\n\
5389 \x20 n if n > 0 => n,\n\
5390 \x20 _ => -1,\n\
5391 \x20 })\n\
5392 }\n",
5393 "foo.rs",
5394 |metric| {
5395 assert_eq!(metric.abc.assignments_sum(), 2.0);
5396 // calls: xs.iter(), .next(), Err(()), Ok(...) → 4 calls
5397 // plus 1 try (`r?`) → 5 branches.
5398 assert_eq!(metric.abc.branches_sum(), 5.0);
5399 // 1 let_condition + 2 non-wildcard match_arms + 1
5400 // comparison (`n > 0`) → 4.
5401 assert_eq!(metric.abc.conditions_sum(), 4.0);
5402 insta::assert_json_snapshot!(metric.abc);
5403 },
5404 );
5405 }
5406
5407 // ----- Go -----
5408
5409 #[test]
5410 fn go_empty_unit_zero() {
5411 // Package declaration only — no Fitzpatrick events. Confirms the
5412 // GoCode Abc trait is wired up and emits zero counts.
5413 check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
5414 assert_eq!(metric.abc.assignments_sum(), 0.0);
5415 assert_eq!(metric.abc.branches_sum(), 0.0);
5416 assert_eq!(metric.abc.conditions_sum(), 0.0);
5417 insta::assert_json_snapshot!(metric.abc);
5418 });
5419 }
5420
5421 #[test]
5422 fn go_assignments_count_plain_compound_short_var_and_incdec() {
5423 // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`),
5424 // `x += 2` (compound), `x++` (inc) → A = 5. `var y = 1` is a
5425 // declaration — its `=` is not counted (matches the Rust/Java
5426 // rule for `let` / `int y = 1`).
5427 check_metrics::<GoParser>(
5428 "package main\nfunc f() { var y = 1; _ = y; x := 0; x = 5; x += 2; x = 7; x++ }\n",
5429 "foo.go",
5430 |metric| {
5431 // `_ = y` is itself an assignment_statement → +1.
5432 // x:= + x=5 + x+=2 + x=7 + x++ + _=y → 6
5433 assert_eq!(metric.abc.assignments_sum(), 6.0);
5434 assert_eq!(metric.abc.branches_sum(), 0.0);
5435 assert_eq!(metric.abc.conditions_sum(), 0.0);
5436 insta::assert_json_snapshot!(metric.abc);
5437 },
5438 );
5439 }
5440
5441 #[test]
5442 fn go_calls_are_branches() {
5443 // Three calls: free function `g()`, method call `r.Inc()`, and
5444 // builtin call `len(s)`. All parse as `call_expression` → B = 3.
5445 // Composite literal `Foo{}` is NOT a call.
5446 check_metrics::<GoParser>(
5447 "package main\n\
5448 type R struct{}\n\
5449 func (r R) Inc() {}\n\
5450 func g() {}\n\
5451 func f(s string) { g(); var r R = R{}; r.Inc(); _ = len(s) }\n",
5452 "foo.go",
5453 |metric| {
5454 assert_eq!(metric.abc.branches_sum(), 3.0);
5455 insta::assert_json_snapshot!(metric.abc);
5456 },
5457 );
5458 }
5459
5460 #[test]
5461 fn go_comparisons_count_conditions() {
5462 // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six
5463 // comparisons → C = 6.
5464 check_metrics::<GoParser>(
5465 "package main\nfunc f(a, b int) bool { return a < b || a > b || a <= b || a >= b || a == b || a != b }\n",
5466 "foo.go",
5467 |metric| {
5468 assert_eq!(metric.abc.conditions_sum(), 6.0);
5469 insta::assert_json_snapshot!(metric.abc);
5470 },
5471 );
5472 }
5473
5474 #[test]
5475 fn go_generic_brackets_not_conditions() {
5476 // Generic instantiation `Min[int](a, b)` puts `int` inside
5477 // `TypeArguments`, not `BinaryExpression`. The parent guard on
5478 // `<` / `>` must not count these. Expected C = 0; B = 1 (one call).
5479 check_metrics::<GoParser>(
5480 "package main\nfunc Min[T int | float64](a, b T) T { return a }\nfunc f() { _ = Min[int](1, 2) }\n",
5481 "foo.go",
5482 |metric| {
5483 assert_eq!(metric.abc.conditions_sum(), 0.0);
5484 assert_eq!(metric.abc.branches_sum(), 1.0);
5485 insta::assert_json_snapshot!(metric.abc);
5486 },
5487 );
5488 }
5489
5490 #[test]
5491 fn go_switch_arms_count_conditions_default_excluded() {
5492 // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The
5493 // bare `default` is the C/Java `default:` equivalent and is
5494 // excluded — 3 conditions from ExpressionCase. The switch
5495 // expression `x` is bare (no comparison), so no extra
5496 // condition from `==`-style operators.
5497 check_metrics::<GoParser>(
5498 "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",
5499 "foo.go",
5500 |metric| {
5501 assert_eq!(metric.abc.conditions_sum(), 3.0);
5502 insta::assert_json_snapshot!(metric.abc);
5503 },
5504 );
5505 }
5506
5507 #[test]
5508 fn go_type_switch_arms_count_conditions() {
5509 // Type switch: `case int:`, `case string:`, `default:`. Two
5510 // non-default type-case arms → C = 2.
5511 check_metrics::<GoParser>(
5512 "package main\nfunc f(v interface{}) { switch v.(type) { case int: return; case string: return; default: return } }\n",
5513 "foo.go",
5514 |metric| {
5515 assert_eq!(metric.abc.conditions_sum(), 2.0);
5516 insta::assert_json_snapshot!(metric.abc);
5517 },
5518 );
5519 }
5520
5521 #[test]
5522 fn go_select_arms_count_conditions() {
5523 // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`.
5524 // Two non-default communication cases → C = 2.
5525 check_metrics::<GoParser>(
5526 "package main\nfunc f(ch chan int) { select { case <-ch: return; case ch <- 1: return; default: return } }\n",
5527 "foo.go",
5528 |metric| {
5529 assert_eq!(metric.abc.conditions_sum(), 2.0);
5530 insta::assert_json_snapshot!(metric.abc);
5531 },
5532 );
5533 }
5534
5535 #[test]
5536 fn go_else_counts_as_condition() {
5537 // `if a > b { ... } else { ... }` → `a > b` is one condition,
5538 // `else` is one condition → C = 2.
5539 check_metrics::<GoParser>(
5540 "package main\nfunc f(a, b int) int { if a > b { return a } else { return b } }\n",
5541 "foo.go",
5542 |metric| {
5543 assert_eq!(metric.abc.conditions_sum(), 2.0);
5544 insta::assert_json_snapshot!(metric.abc);
5545 },
5546 );
5547 }
5548
5549 #[test]
5550 fn go_complex_function_abc() {
5551 // Mixed shape, verified by hand:
5552 // - Assignments: `_ = x` (after `var`), `n := 0`,
5553 // `n = n + 1`, `n += 2`, `n++`, `_ = len(s)` → A = 6.
5554 // `var x = 10` is a declaration, not counted. Every `_ = ...`
5555 // IS counted as an assignment_statement.
5556 // - Branches: `len(s)` → B = 1.
5557 // - Conditions: `n < 10` → 1, `else` → 1, switch arms `case 0:`
5558 // and `case 1:` (default excluded) → 2 → total C = 4.
5559 check_metrics::<GoParser>(
5560 "package main\nfunc f(s string) int {\n\
5561 \x20 var x = 10\n\
5562 \x20 _ = x\n\
5563 \x20 n := 0\n\
5564 \x20 if n < 10 { n = n + 1 } else { n += 2 }\n\
5565 \x20 n++\n\
5566 \x20 _ = len(s)\n\
5567 \x20 switch n {\n\
5568 \x20 case 0: return 0\n\
5569 \x20 case 1: return 1\n\
5570 \x20 default: return n\n\
5571 \x20 }\n\
5572 }\n",
5573 "foo.go",
5574 |metric| {
5575 assert_eq!(metric.abc.assignments_sum(), 6.0);
5576 assert_eq!(metric.abc.branches_sum(), 1.0);
5577 assert_eq!(metric.abc.conditions_sum(), 4.0);
5578 insta::assert_json_snapshot!(metric.abc);
5579 },
5580 );
5581 }
5582
5583 // ----- Elixir -----
5584
5585 // No top-level Calls and no operators → all three vectors are
5586 // zero. Uses a bare expression rather than a `defmodule` wrapper
5587 // (which would itself be a Call → 1 branch). Confirms the
5588 // ElixirCode Abc trait is wired up and the metric emits.
5589 #[test]
5590 fn elixir_empty_unit_zero() {
5591 check_metrics::<ElixirParser>(":ok\n", "foo.ex", |metric| {
5592 assert_eq!(metric.abc.assignments_sum(), 0.0);
5593 assert_eq!(metric.abc.branches_sum(), 0.0);
5594 assert_eq!(metric.abc.conditions_sum(), 0.0);
5595 insta::assert_json_snapshot!(metric.abc);
5596 });
5597 }
5598
5599 // An empty `defmodule Foo do ... end` is itself ONE `Call` →
5600 // Documents that module-/function-defining macros (`defmodule`,
5601 // `def`, `defp`, `defmacro`, `defmacrop`) and declarative
5602 // directives (`alias`, `import`, `require`, `use`) are NOT
5603 // runtime dispatch and therefore do NOT inflate `branches`,
5604 // matching Cognitive's treatment.
5605 #[test]
5606 fn elixir_defmodule_is_zero_branches() {
5607 check_metrics::<ElixirParser>("defmodule Foo do\nend\n", "foo.ex", |metric| {
5608 assert_eq!(metric.abc.branches_sum(), 0.0);
5609 assert_eq!(metric.abc.assignments_sum(), 0.0);
5610 assert_eq!(metric.abc.conditions_sum(), 0.0);
5611 insta::assert_json_snapshot!(metric.abc);
5612 });
5613 }
5614
5615 // Pattern-match `=` counts as an assignment. Two bindings → A = 2.
5616 // `defmodule` and `def` are declarative-Call wrappers and are
5617 // filtered out of branches; the assertion focuses on assignments
5618 // so we only pin that vector.
5619 #[test]
5620 fn elixir_pattern_match_is_assignment() {
5621 check_metrics::<ElixirParser>(
5622 "defmodule Foo do\n def f do\n x = 1\n y = x + 1\n y\n end\nend\n",
5623 "foo.ex",
5624 |metric| {
5625 assert_eq!(metric.abc.assignments_sum(), 2.0);
5626 insta::assert_json_snapshot!(metric.abc);
5627 },
5628 );
5629 }
5630
5631 // `|>` pipeline operator: each `|>` token contributes one branch.
5632 // Two `|>` ops → +2 from the pipe operator itself. Each pipeline
5633 // step also dispatches a Call (`String.upcase(...)`,
5634 // `String.trim(...)`) — these are wrapped inside the outer
5635 // pipeline Call tree, contributing additional Call branches.
5636 // The headline assertion confirms (a) `|>` is detected and (b)
5637 // pipeline steps are not silently dropped.
5638 #[test]
5639 fn elixir_pipeline_each_step_is_branch() {
5640 check_metrics::<ElixirParser>(
5641 "defmodule Foo do\n def normalize(s) do\n s |> String.trim() |> String.upcase()\n end\nend\n",
5642 "foo.ex",
5643 |metric| {
5644 // Pipeline yields 2 `|>` branches plus Calls for
5645 // String.trim, String.upcase, and the outer pipeline
5646 // (which surfaces as a Call wrapping the binary
5647 // operator). `def` and `defmodule` are declarative
5648 // and excluded. Empirical total: B = 5.
5649 assert_eq!(metric.abc.branches_sum(), 5.0);
5650 assert_eq!(metric.abc.assignments_sum(), 0.0);
5651 insta::assert_json_snapshot!(metric.abc);
5652 },
5653 );
5654 }
5655
5656 // Comparison operators all count as conditions. Six comparisons
5657 // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6.
5658 #[test]
5659 fn elixir_comparisons_are_conditions() {
5660 check_metrics::<ElixirParser>(
5661 "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",
5662 "foo.ex",
5663 |metric| {
5664 assert_eq!(metric.abc.conditions_sum(), 6.0);
5665 insta::assert_json_snapshot!(metric.abc);
5666 },
5667 );
5668 }
5669
5670 // Strict-equality operators `===` / `!==` count as conditions too.
5671 #[test]
5672 fn elixir_strict_equality_is_condition() {
5673 check_metrics::<ElixirParser>(
5674 "defmodule Foo do\n def f(a, b) do\n a === b or a !== b\n end\nend\n",
5675 "foo.ex",
5676 |metric| {
5677 assert_eq!(metric.abc.conditions_sum(), 2.0);
5678 insta::assert_json_snapshot!(metric.abc);
5679 },
5680 );
5681 }
5682
5683 // Guard `when` clause counts as a condition. One `when` → +1.
5684 // `def f(x) when x > 0` also has `>` → +1, totalling 2.
5685 #[test]
5686 fn elixir_guard_when_is_condition() {
5687 check_metrics::<ElixirParser>(
5688 "defmodule Foo do\n def f(x) when x > 0 do\n :pos\n end\nend\n",
5689 "foo.ex",
5690 |metric| {
5691 // when (+1) + > (+1) = 2
5692 assert_eq!(metric.abc.conditions_sum(), 2.0);
5693 insta::assert_json_snapshot!(metric.abc);
5694 },
5695 );
5696 }
5697
5698 // Keyword-shaped Calls (`case`, `cond`, `if`, `with`) each count
5699 // as one condition AND one branch. `case` here adds 1 condition
5700 // (the keyword Call) + 1 branch (the Call itself).
5701 #[test]
5702 fn elixir_case_is_condition_and_branch() {
5703 check_metrics::<ElixirParser>(
5704 "defmodule Foo do\n def f(x) do\n case x do\n 1 -> :one\n _ -> :other\n end\n end\nend\n",
5705 "foo.ex",
5706 |metric| {
5707 // conditions: case → 1
5708 assert_eq!(metric.abc.conditions_sum(), 1.0);
5709 insta::assert_json_snapshot!(metric.abc);
5710 },
5711 );
5712 }
5713
5714 // `cond` is structurally identical to `case` for Abc.
5715 #[test]
5716 fn elixir_cond_is_condition() {
5717 check_metrics::<ElixirParser>(
5718 "defmodule Foo do\n def f(x) do\n cond do\n x > 0 -> :pos\n true -> :other\n end\n end\nend\n",
5719 "foo.ex",
5720 |metric| {
5721 // conditions: cond (+1) + > (+1) = 2
5722 assert_eq!(metric.abc.conditions_sum(), 2.0);
5723 insta::assert_json_snapshot!(metric.abc);
5724 },
5725 );
5726 }
5727
5728 // `for` is a comprehension/loop, NOT in the issue's condition
5729 // list. It is still a Call so it contributes one branch, but no
5730 // condition.
5731 #[test]
5732 fn elixir_for_is_branch_not_condition() {
5733 check_metrics::<ElixirParser>(
5734 "defmodule Foo do\n def f(xs) do\n for x <- xs, do: x * 2\n end\nend\n",
5735 "foo.ex",
5736 |metric| {
5737 assert_eq!(metric.abc.conditions_sum(), 0.0);
5738 insta::assert_json_snapshot!(metric.abc);
5739 },
5740 );
5741 }
5742
5743 // Mixed shape, verified by hand: defmodule Call + def Call + if Call
5744 // + Call to side_effect/0 + assignment `x = 1` + comparison `x > 0`.
5745 // - Assignments: `x = 1` → A = 1.
5746 // - Branches: `defmodule` and `def` are declarative and excluded;
5747 // `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2.
5748 // - Conditions: `if` keyword → 1, `x > 0` → 1 → C = 2.
5749 #[test]
5750 fn elixir_mixed_abc() {
5751 check_metrics::<ElixirParser>(
5752 "defmodule Foo do\n def f do\n x = 1\n if x > 0 do\n side_effect()\n end\n end\nend\n",
5753 "foo.ex",
5754 |metric| {
5755 assert_eq!(metric.abc.assignments_sum(), 1.0);
5756 assert_eq!(metric.abc.branches_sum(), 2.0);
5757 assert_eq!(metric.abc.conditions_sum(), 2.0);
5758 insta::assert_json_snapshot!(metric.abc);
5759 },
5760 );
5761 }
5762
5763 // ----- C++ -----
5764
5765 #[test]
5766 fn cpp_empty_unit_zero() {
5767 // No code → A=B=C=0. Wires up the trait and exercises the
5768 // per-language compute reachability.
5769 check_metrics::<CppParser>("", "empty.cpp", |metric| {
5770 assert_eq!(metric.abc.assignments_sum(), 0.0);
5771 assert_eq!(metric.abc.branches_sum(), 0.0);
5772 assert_eq!(metric.abc.conditions_sum(), 0.0);
5773 insta::assert_json_snapshot!(metric.abc);
5774 });
5775 }
5776
5777 #[test]
5778 fn cpp_plain_and_compound_assignments_count() {
5779 // `int x = 0` is an `init_declarator` (declaration initialiser)
5780 // and NOT a Fitzpatrick assignment. `x = 5`, `x += 2`, `x = 7`
5781 // all parse as `assignment_expression` → A = 3.
5782 check_metrics::<CppParser>(
5783 "void f() { int x = 0; x = 5; x += 2; x = 7; }",
5784 "foo.cpp",
5785 |metric| {
5786 assert_eq!(metric.abc.assignments_sum(), 3.0);
5787 assert_eq!(metric.abc.branches_sum(), 0.0);
5788 assert_eq!(metric.abc.conditions_sum(), 0.0);
5789 insta::assert_json_snapshot!(metric.abc);
5790 },
5791 );
5792 }
5793
5794 #[test]
5795 fn cpp_increment_and_decrement_count_as_assignment() {
5796 // `x++` / `--x` / prefix and postfix forms each parse as
5797 // `update_expression` and count as 1 assignment per Fitzpatrick.
5798 check_metrics::<CppParser>(
5799 "void f() { int x = 0; x++; --x; ++x; x--; }",
5800 "foo.cpp",
5801 |metric| {
5802 assert_eq!(metric.abc.assignments_sum(), 4.0);
5803 insta::assert_json_snapshot!(metric.abc);
5804 },
5805 );
5806 }
5807
5808 #[test]
5809 fn cpp_calls_are_branches() {
5810 // Free call + member-fn call (parses as `call_expression` with
5811 // a `field_expression` callee) + `new` allocation. All three
5812 // are branches → B = 3.
5813 check_metrics::<CppParser>(
5814 "struct S { void m(); }; void g(); void f() { g(); S s; s.m(); auto* p = new int(5); }",
5815 "foo.cpp",
5816 |metric| {
5817 assert_eq!(metric.abc.branches_sum(), 3.0);
5818 insta::assert_json_snapshot!(metric.abc);
5819 },
5820 );
5821 }
5822
5823 #[test]
5824 fn cpp_comparisons_count_conditions() {
5825 // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship
5826 // `<=>` each contribute one condition. Seven comparisons → C = 7.
5827 check_metrics::<CppParser>(
5828 "#include <compare>\n\
5829 bool f(int a, int b) {\n\
5830 return a < b || a > b || a <= b || a >= b || a == b || a != b || (a <=> b) == 0;\n\
5831 }\n",
5832 "foo.cpp",
5833 |metric| {
5834 // 6 plain comparisons + 1 spaceship + 1 `||` adds = 7? Let's
5835 // pin the exact count by hand:
5836 // `<`, `>`, `<=`, `>=`, `==`, `!=` → 6 from the
5837 // chained `||` expression. `(a <=> b) == 0` adds the
5838 // spaceship → 7, plus its `== 0` adds one more → 8.
5839 // Six `||` short-circuits add → 8 + 6 = 14.
5840 assert_eq!(metric.abc.conditions_sum(), 14.0);
5841 insta::assert_json_snapshot!(metric.abc);
5842 },
5843 );
5844 }
5845
5846 #[test]
5847 fn cpp_short_circuit_ops_count_conditions() {
5848 // `&&` and `||` each count once per occurrence (Fitzpatrick
5849 // rule). Two short-circuits → C = 2 (plus two comparisons → 4).
5850 check_metrics::<CppParser>(
5851 "bool f(int a, int b) { return a == b && a > 0 || b < 0; }",
5852 "foo.cpp",
5853 |metric| {
5854 // == 1, > 1, < 1, && 1, || 1 → 5.
5855 assert_eq!(metric.abc.conditions_sum(), 5.0);
5856 insta::assert_json_snapshot!(metric.abc);
5857 },
5858 );
5859 }
5860
5861 #[test]
5862 fn cpp_generic_brackets_not_conditions() {
5863 // `<` / `>` in `std::vector<int>` are `template_argument_list`
5864 // delimiters, NOT comparison operators. The `binary_expression`
5865 // parent check must filter them out → C = 0.
5866 check_metrics::<CppParser>(
5867 "#include <vector>\nstd::vector<int> f() { return std::vector<int>{}; }",
5868 "foo.cpp",
5869 |metric| {
5870 assert_eq!(metric.abc.conditions_sum(), 0.0);
5871 insta::assert_json_snapshot!(metric.abc);
5872 },
5873 );
5874 }
5875
5876 #[test]
5877 fn cpp_else_and_ternary_count_conditions() {
5878 // `if (cond) ... else ...` + ternary `cond ? a : b`. The
5879 // `if`-keyword is NOT a condition (its condition is the
5880 // comparison inside, which counts separately). `else` adds 1,
5881 // `?` adds 1. Two comparisons (`a > b`, `b < 0`) → 2. Total = 4.
5882 check_metrics::<CppParser>(
5883 "int f(int a, int b) {\n\
5884 if (a > b) { return a; } else { return b; }\n\
5885 return (b < 0) ? -b : b;\n\
5886 }\n",
5887 "foo.cpp",
5888 |metric| {
5889 assert_eq!(metric.abc.conditions_sum(), 4.0);
5890 insta::assert_json_snapshot!(metric.abc);
5891 },
5892 );
5893 }
5894
5895 #[test]
5896 fn cpp_switch_cases_count_default_excluded() {
5897 // `case 1`, `case 2` → 2 conditions. `default` is intentionally
5898 // excluded (matches the C-family precedent in Rust / Go / Python
5899 // and Java's omission of `Default` from this rule? — actually
5900 // Java DOES count `Default`. We follow Rust / Go and exclude
5901 // it). C = 2.
5902 check_metrics::<CppParser>(
5903 "void f(int x) {\n\
5904 switch (x) {\n\
5905 case 1: break;\n\
5906 case 2: break;\n\
5907 default: break;\n\
5908 }\n\
5909 }\n",
5910 "foo.cpp",
5911 |metric| {
5912 assert_eq!(metric.abc.conditions_sum(), 2.0);
5913 insta::assert_json_snapshot!(metric.abc);
5914 },
5915 );
5916 }
5917
5918 #[test]
5919 fn cpp_try_catch_count_conditions() {
5920 // `try` and `catch` each add one condition (Fitzpatrick's rule;
5921 // Java's impl above counts them too).
5922 check_metrics::<CppParser>(
5923 "void f() { try { } catch (int) { } catch (...) { } }",
5924 "foo.cpp",
5925 |metric| {
5926 // 1 `try` + 2 `catch` arms = 3.
5927 assert_eq!(metric.abc.conditions_sum(), 3.0);
5928 insta::assert_json_snapshot!(metric.abc);
5929 },
5930 );
5931 }
5932
5933 #[test]
5934 fn cpp_complex_function_abc() {
5935 // Mixed-shape regression: assignments, calls, conditions,
5936 // ternary, switch, new. Verified by hand:
5937 // - assignments: `x = 5`, `x += 2`, `x++`, `x = (a > b) ? a : b`,
5938 // `x = b` → A = 5. (`int x = 0`, `auto y = ...`, `auto* p = ...`
5939 // are declaration initialisers and don't count.)
5940 // - branches: `f(a, b)` self-call + `new int(5)` → B = 2.
5941 // - conditions: `a == b`, `&&`, `a > 0` → 3 inside the if.
5942 // `else` (1) + `a > b`, `?` → 2 in the ternary. `a < b`,
5943 // `||` → 2 in the else-if. `case 1`, `case 2` → 2.
5944 // default excluded. Total C = 10.
5945 check_metrics::<CppParser>(
5946 "int f(int a, int b) {\n\
5947 int x = 0;\n\
5948 x = 5;\n\
5949 x += 2;\n\
5950 x++;\n\
5951 if (a == b && a > 0) {\n\
5952 x = (a > b) ? a : b;\n\
5953 } else if (a < b || !x) {\n\
5954 x = b;\n\
5955 }\n\
5956 switch (x) {\n\
5957 case 1: break;\n\
5958 case 2: break;\n\
5959 default: break;\n\
5960 }\n\
5961 auto* p = new int(5);\n\
5962 return f(a, b);\n\
5963 }\n",
5964 "foo.cpp",
5965 |metric| {
5966 assert_eq!(metric.abc.assignments_sum(), 5.0);
5967 assert_eq!(metric.abc.branches_sum(), 2.0);
5968 assert_eq!(metric.abc.conditions_sum(), 10.0);
5969 insta::assert_json_snapshot!(metric.abc);
5970 },
5971 );
5972 }
5973
5974 #[test]
5975 fn javascript_empty_unit_zero() {
5976 // No code → A=B=C=0. Wires up the trait and exercises the
5977 // per-language compute reachability.
5978 check_metrics::<JavascriptParser>("", "empty.js", |metric| {
5979 assert_eq!(metric.abc.assignments_sum(), 0.0);
5980 assert_eq!(metric.abc.branches_sum(), 0.0);
5981 assert_eq!(metric.abc.conditions_sum(), 0.0);
5982 insta::assert_json_snapshot!(metric.abc);
5983 });
5984 }
5985
5986 #[test]
5987 fn javascript_plain_and_compound_assignments_count() {
5988 // `let` / `var` declarations behave like TypeScript: the `Var`
5989 // sentinel is pushed but only `const` suppresses the
5990 // initializer `=`. So `let x = 0` does count as A=+1; only
5991 // `const PI = 3.14` would be elided. Plain `x = 5`, `x += 2`,
5992 // `x = 7` all count → A = 4 total here.
5993 check_metrics::<JavascriptParser>(
5994 "function f() { let x = 0; x = 5; x += 2; x = 7; }",
5995 "foo.js",
5996 |metric| {
5997 assert_eq!(metric.abc.assignments_sum(), 4.0);
5998 assert_eq!(metric.abc.branches_sum(), 0.0);
5999 assert_eq!(metric.abc.conditions_sum(), 0.0);
6000 insta::assert_json_snapshot!(metric.abc);
6001 },
6002 );
6003 }
6004
6005 #[test]
6006 fn javascript_const_initializer_not_assignment() {
6007 // `const PI = 3.14` must NOT count as an assignment — the
6008 // `Const` sentinel suppresses the initializer `=`. `let x = 1`
6009 // and `var y = 2` still count (matches the TS impl: only
6010 // `const` suppresses).
6011 check_metrics::<JavascriptParser>(
6012 "function f() { const PI = 3.14; let x = 1; var y = 2; x = 9; }",
6013 "foo.js",
6014 |metric| {
6015 // `const PI` suppressed; `let x = 1`, `var y = 2`,
6016 // `x = 9` all count → A = 3.
6017 assert_eq!(metric.abc.assignments_sum(), 3.0);
6018 insta::assert_json_snapshot!(metric.abc);
6019 },
6020 );
6021 }
6022
6023 #[test]
6024 fn javascript_increment_and_decrement_count_as_assignment() {
6025 // `x++` (post) and `--x` (pre) both update an lvalue and so
6026 // count as assignments. Combined with the `let x = 0`
6027 // initializer (which counts under the JS/TS sentinel rule —
6028 // only `const` suppresses), A = 3.
6029 check_metrics::<JavascriptParser>(
6030 "function f() { let x = 0; x++; --x; }",
6031 "foo.js",
6032 |metric| {
6033 assert_eq!(metric.abc.assignments_sum(), 3.0);
6034 insta::assert_json_snapshot!(metric.abc);
6035 },
6036 );
6037 }
6038
6039 #[test]
6040 fn javascript_calls_are_branches() {
6041 // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a
6042 // `new_expression` → B = 1. Total B = 2.
6043 check_metrics::<JavascriptParser>(
6044 "function f() { g(1); new Foo(2); }",
6045 "foo.js",
6046 |metric| {
6047 assert_eq!(metric.abc.branches_sum(), 2.0);
6048 assert_eq!(metric.abc.conditions_sum(), 0.0);
6049 insta::assert_json_snapshot!(metric.abc);
6050 },
6051 );
6052 }
6053
6054 #[test]
6055 fn javascript_comparisons_count_conditions() {
6056 // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count
6057 // once. The `&&` / `||` short-circuit operators are NOT
6058 // counted as conditions in this impl (matches the TS
6059 // precedent — short-circuit ops are folded into the
6060 // surrounding `if` / control-flow arm, not separately).
6061 // Total C = 8.
6062 check_metrics::<JavascriptParser>(
6063 "function f(a, b) { return a == b && a === b && a != b && a !== b && a < b && a > b && a <= b && a >= b; }",
6064 "foo.js",
6065 |metric| {
6066 assert_eq!(metric.abc.conditions_sum(), 8.0);
6067 insta::assert_json_snapshot!(metric.abc);
6068 },
6069 );
6070 }
6071
6072 #[test]
6073 fn javascript_nullish_coalescing_counts_condition() {
6074 // `a ?? b` is one nullish-coalescing operator → C = 1.
6075 check_metrics::<JavascriptParser>(
6076 "function f(a, b) { return a ?? b; }",
6077 "foo.js",
6078 |metric| {
6079 assert_eq!(metric.abc.conditions_sum(), 1.0);
6080 insta::assert_json_snapshot!(metric.abc);
6081 },
6082 );
6083 }
6084
6085 #[test]
6086 fn javascript_else_ternary_case_default_try_catch() {
6087 // `else`, `?` (ternary), `case`, `default`, `try`, `catch`
6088 // all count. With the comparisons:
6089 // - `a > 0` → 1
6090 // - `else` opens an else_clause → 1
6091 // - `?` ternary → 1
6092 // - `case 1` → 1
6093 // - `default` → 1
6094 // - `try` + `catch` → 2
6095 // Total C = 7.
6096 check_metrics::<JavascriptParser>(
6097 "function f(a) { if (a > 0) {} else {} let x = a ? 1 : 2; switch (x) { case 1: break; default: break; } try { } catch (e) { } }",
6098 "foo.js",
6099 |metric| {
6100 assert_eq!(metric.abc.conditions_sum(), 7.0);
6101 insta::assert_json_snapshot!(metric.abc);
6102 },
6103 );
6104 }
6105
6106 #[test]
6107 fn javascript_instanceof_counts_condition() {
6108 // `x instanceof Foo` is a binary expression whose operator is
6109 // the `instanceof` keyword token → C = 1.
6110 check_metrics::<JavascriptParser>(
6111 "function f(x) { return x instanceof Foo; }",
6112 "foo.js",
6113 |metric| {
6114 assert_eq!(metric.abc.conditions_sum(), 1.0);
6115 insta::assert_json_snapshot!(metric.abc);
6116 },
6117 );
6118 }
6119
6120 #[test]
6121 fn javascript_complex_function_abc() {
6122 // Mixed-shape regression. Verified by hand:
6123 // - assignments: `let x = 0` (Var sentinel does not suppress)
6124 // + `x = 5`, `x += 2`, `x++`, `x = (a>b)?a:b`, `x = b`,
6125 // `let p = ...` (Var sentinel) → A = 7.
6126 // - branches: `f(a, b)` self-call + `new Bar()` → B = 2.
6127 // - conditions: `a == b`, `a > 0` → 2 inside the if header
6128 // (`&&` is not counted). `else` (1) + `a > b`, `?` → 2 in
6129 // the ternary. `a < b` → 1 in the else-if (`||` not
6130 // counted). `case 1`, `default` → 2 in the switch. Total
6131 // C = 8.
6132 check_metrics::<JavascriptParser>(
6133 "function f(a, b) {\n\
6134 let x = 0;\n\
6135 x = 5;\n\
6136 x += 2;\n\
6137 x++;\n\
6138 if (a == b && a > 0) {\n\
6139 x = (a > b) ? a : b;\n\
6140 } else if (a < b || !x) {\n\
6141 x = b;\n\
6142 }\n\
6143 switch (x) {\n\
6144 case 1: break;\n\
6145 default: break;\n\
6146 }\n\
6147 let p = new Bar();\n\
6148 return f(a, b);\n\
6149 }\n",
6150 "foo.js",
6151 |metric| {
6152 assert_eq!(metric.abc.assignments_sum(), 7.0);
6153 assert_eq!(metric.abc.branches_sum(), 2.0);
6154 assert_eq!(metric.abc.conditions_sum(), 8.0);
6155 insta::assert_json_snapshot!(metric.abc);
6156 },
6157 );
6158 }
6159
6160 #[test]
6161 fn mozjs_complex_function_abc() {
6162 // Mozjs shares JavaScript's expression / statement vocabulary;
6163 // the `js_abc_compute!` macro expands identical token-level
6164 // rules for both. This test pins parity against the JS impl.
6165 check_metrics::<MozjsParser>(
6166 "function f(a, b) {\n\
6167 let x = 0;\n\
6168 x = 5;\n\
6169 x += 2;\n\
6170 x++;\n\
6171 if (a == b && a > 0) {\n\
6172 x = (a > b) ? a : b;\n\
6173 } else if (a < b || !x) {\n\
6174 x = b;\n\
6175 }\n\
6176 switch (x) {\n\
6177 case 1: break;\n\
6178 default: break;\n\
6179 }\n\
6180 let p = new Bar();\n\
6181 return f(a, b);\n\
6182 }\n",
6183 "foo.js",
6184 |metric| {
6185 assert_eq!(metric.abc.assignments_sum(), 7.0);
6186 assert_eq!(metric.abc.branches_sum(), 2.0);
6187 assert_eq!(metric.abc.conditions_sum(), 8.0);
6188 insta::assert_json_snapshot!(metric.abc);
6189 },
6190 );
6191 }
6192
6193 // ---------- Perl ABC tests ----------
6194
6195 #[test]
6196 fn perl_empty_unit_zero() {
6197 // Empty source produces zero ABC magnitude — pins the trait
6198 // wiring without exercising any compute branch.
6199 check_metrics::<PerlParser>("", "empty.pl", |metric| {
6200 assert_eq!(metric.abc.assignments_sum(), 0.0);
6201 assert_eq!(metric.abc.branches_sum(), 0.0);
6202 assert_eq!(metric.abc.conditions_sum(), 0.0);
6203 insta::assert_json_snapshot!(metric.abc);
6204 });
6205 }
6206
6207 #[test]
6208 fn perl_plain_and_compound_assignments_count() {
6209 // `my $x = 0` parses as a `binary_expression` with an `=`
6210 // token, so the initialiser counts (Perl has no equivalent of
6211 // the JS `const` initialiser-suppression rule). Each
6212 // assignment operator token contributes one assignment:
6213 // `=`, `=`, `+=`, `.=`, `**=` → A = 5. Two of those `=` come
6214 // from the `my $x = 0` initialiser and the later `$x = 5`
6215 // reassignment.
6216 check_metrics::<PerlParser>(
6217 "sub f { my $x = 0; $x = 5; $x += 2; $x .= \"a\"; $x **= 3; }",
6218 "foo.pl",
6219 |metric| {
6220 assert_eq!(metric.abc.assignments_sum(), 5.0);
6221 assert_eq!(metric.abc.branches_sum(), 0.0);
6222 assert_eq!(metric.abc.conditions_sum(), 0.0);
6223 insta::assert_json_snapshot!(metric.abc);
6224 },
6225 );
6226 }
6227
6228 #[test]
6229 fn perl_calls_are_branches() {
6230 // `foo()` parses as `call_expression_with_args_with_brackets`
6231 // wrapping an inner `call_expression_with_bareword(foo)`;
6232 // `bar 1, 2` wraps `bar` likewise under spaced-args; `shift`
6233 // appears as a standalone bareword. The bareword-inside-
6234 // wrapper case must NOT double-count — only the outer wrapper
6235 // contributes a branch. So B = 3 (foo, bar, shift), not 5.
6236 check_metrics::<PerlParser>(
6237 "sub f { foo(); bar 1, 2; my $a = shift; }",
6238 "foo.pl",
6239 |metric| {
6240 // shift's `my $a = shift` initialiser contributes one
6241 // assignment via the `=` token.
6242 assert_eq!(metric.abc.assignments_sum(), 1.0);
6243 assert_eq!(metric.abc.branches_sum(), 3.0);
6244 assert_eq!(metric.abc.conditions_sum(), 0.0);
6245 insta::assert_json_snapshot!(metric.abc);
6246 },
6247 );
6248 }
6249
6250 #[test]
6251 fn perl_method_invocation_counts_as_branch() {
6252 // `$obj->method(...)` parses as `method_invocation`. Any
6253 // arrow-dispatch counts as one branch regardless of how the
6254 // arguments are passed.
6255 check_metrics::<PerlParser>(
6256 "sub f { my $obj = shift; $obj->run($x); $obj->ping; }",
6257 "foo.pl",
6258 |metric| {
6259 // `my $obj = shift` → A=1, B=1 (shift bareword).
6260 // `$obj->run($x)` and `$obj->ping` → 2 more branches.
6261 assert_eq!(metric.abc.assignments_sum(), 1.0);
6262 assert_eq!(metric.abc.branches_sum(), 3.0);
6263 assert_eq!(metric.abc.conditions_sum(), 0.0);
6264 insta::assert_json_snapshot!(metric.abc);
6265 },
6266 );
6267 }
6268
6269 #[test]
6270 fn perl_numeric_and_string_comparisons_count_conditions() {
6271 // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and
6272 // string ops `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp` each
6273 // fire once per token. The sample below uses one of each →
6274 // C = 14. No assignments, no branches.
6275 check_metrics::<PerlParser>(
6276 "sub f {\n\
6277 my $r;\n\
6278 $r = $a == $b;\n\
6279 $r = $a != $b;\n\
6280 $r = $a < $b;\n\
6281 $r = $a > $b;\n\
6282 $r = $a <= $b;\n\
6283 $r = $a >= $b;\n\
6284 $r = $a <=> $b;\n\
6285 $r = $a eq $b;\n\
6286 $r = $a ne $b;\n\
6287 $r = $a lt $b;\n\
6288 $r = $a gt $b;\n\
6289 $r = $a le $b;\n\
6290 $r = $a ge $b;\n\
6291 $r = $a cmp $b;\n\
6292 }",
6293 "foo.pl",
6294 |metric| {
6295 // 15 `=` tokens: one declaration `my $r` (no `=`),
6296 // then 14 `$r = …` plus there's no `=` in `my $r;`.
6297 // Actually: `my $r;` has no `=`; the 14 `$r = …` are
6298 // 14 `=` tokens. So A=14, C=14.
6299 assert_eq!(metric.abc.assignments_sum(), 14.0);
6300 assert_eq!(metric.abc.branches_sum(), 0.0);
6301 assert_eq!(metric.abc.conditions_sum(), 14.0);
6302 insta::assert_json_snapshot!(metric.abc);
6303 },
6304 );
6305 }
6306
6307 #[test]
6308 fn perl_short_circuit_and_ternary_count_conditions() {
6309 // `&&`, `||`, `//`, low-precedence `and`, `or`, `xor`, plus
6310 // ternary `? :` each contribute one condition.
6311 check_metrics::<PerlParser>(
6312 "sub f {\n\
6313 my $r;\n\
6314 $r = $a && $b;\n\
6315 $r = $a || $b;\n\
6316 $r = $a // $b;\n\
6317 $r = $a and $b;\n\
6318 $r = $a or $b;\n\
6319 $r = $a xor $b;\n\
6320 $r = $a ? 1 : 2;\n\
6321 }",
6322 "foo.pl",
6323 |metric| {
6324 // 7 `=` tokens (one per reassignment line).
6325 assert_eq!(metric.abc.assignments_sum(), 7.0);
6326 assert_eq!(metric.abc.branches_sum(), 0.0);
6327 assert_eq!(metric.abc.conditions_sum(), 7.0);
6328 insta::assert_json_snapshot!(metric.abc);
6329 },
6330 );
6331 }
6332
6333 #[test]
6334 fn perl_elsif_and_else_count_conditions() {
6335 // `if (… == …) { … } elsif (… < …) { … } else { … }` →
6336 // 2 comparison tokens (`==`, `<`), plus `elsif_clause` and
6337 // `else_clause` each + 1 → C = 4. Branches: 0 (only
6338 // assignments). Assignments: just the `=` initialisers /
6339 // reassignments — there are 4 here (`$x` init plus three
6340 // `$x = …` reassigns).
6341 check_metrics::<PerlParser>(
6342 "sub f {\n\
6343 my $x = 0;\n\
6344 if ($a == $b) {\n\
6345 $x = 1;\n\
6346 } elsif ($a < $b) {\n\
6347 $x = 2;\n\
6348 } else {\n\
6349 $x = 3;\n\
6350 }\n\
6351 }",
6352 "foo.pl",
6353 |metric| {
6354 assert_eq!(metric.abc.assignments_sum(), 4.0);
6355 assert_eq!(metric.abc.branches_sum(), 0.0);
6356 assert_eq!(metric.abc.conditions_sum(), 4.0);
6357 insta::assert_json_snapshot!(metric.abc);
6358 },
6359 );
6360 }
6361
6362 #[test]
6363 fn perl_regex_match_operators_count_conditions() {
6364 // `=~` and `!~` are pattern-match operators; we count both
6365 // as conditions because they evaluate the regex match in a
6366 // boolean context.
6367 check_metrics::<PerlParser>(
6368 "sub f { my $s = shift; my $m = $s =~ /foo/; my $n = $s !~ /bar/; }",
6369 "foo.pl",
6370 |metric| {
6371 // 3 `=` tokens, 0 branches except `shift` bareword.
6372 assert_eq!(metric.abc.assignments_sum(), 3.0);
6373 assert_eq!(metric.abc.branches_sum(), 1.0);
6374 assert_eq!(metric.abc.conditions_sum(), 2.0);
6375 insta::assert_json_snapshot!(metric.abc);
6376 },
6377 );
6378 }
6379
6380 #[test]
6381 fn perl_complex_function_abc() {
6382 // Mixed program exercising every category. Computed
6383 // expected:
6384 // Assignments: `my $i = 0` (1), `$i++` is a unary
6385 // increment — Perl's grammar emits `PLUSPLUS` not an `=`
6386 // operator, so it does NOT count under the operator-
6387 // token rule. The for-loop's `$i++` is similarly
6388 // uncounted.
6389 // Total A: 1 from `my $i = 0`, 1 from `$total += $i`
6390 // (the `+=` token) → A = 2.
6391 // Branches: `do_work($i)` → 1; `print "done\n"` is a
6392 // call_expression_with_spaced_args → 1; `return $total`
6393 // uses the `return` keyword not a call → 0. B = 2.
6394 // Conditions: `$i < 10` (`<`) → 1; `$i % 2 == 0` (`==`) →
6395 // 1; `else_clause` → 1. C = 3.
6396 check_metrics::<PerlParser>(
6397 "sub run {\n\
6398 my $total = 0;\n\
6399 for (my $i = 0; $i < 10; $i++) {\n\
6400 if ($i % 2 == 0) {\n\
6401 do_work($i);\n\
6402 } else {\n\
6403 $total += $i;\n\
6404 }\n\
6405 }\n\
6406 print \"done\\n\";\n\
6407 return $total;\n\
6408 }",
6409 "foo.pl",
6410 |metric| {
6411 // `my $total = 0` is one `=`; `my $i = 0` is another
6412 // `=`; `$total += $i` is one `+=`. Total = 3.
6413 assert_eq!(metric.abc.assignments_sum(), 3.0);
6414 assert_eq!(metric.abc.branches_sum(), 2.0);
6415 assert_eq!(metric.abc.conditions_sum(), 3.0);
6416 insta::assert_json_snapshot!(metric.abc);
6417 },
6418 );
6419 }
6420
6421 // ---------- Lua ABC tests ----------
6422
6423 #[test]
6424 fn lua_empty_unit_zero() {
6425 check_metrics::<LuaParser>("", "empty.lua", |metric| {
6426 assert_eq!(metric.abc.assignments_sum(), 0.0);
6427 assert_eq!(metric.abc.branches_sum(), 0.0);
6428 assert_eq!(metric.abc.conditions_sum(), 0.0);
6429 insta::assert_json_snapshot!(metric.abc);
6430 });
6431 }
6432
6433 #[test]
6434 fn lua_assignments_count_locals_and_plain() {
6435 // `local x = 0` wraps an `assignment_statement` under a
6436 // `variable_declaration`; the inner wrapper still counts.
6437 // Multi-target assignment `a, b = 1, 2` is a single
6438 // `assignment_statement` and contributes 1, NOT 2 — the
6439 // wrapper is the unit of counting (matches the Python rule:
6440 // one `Assignment` node, one assignment).
6441 check_metrics::<LuaParser>(
6442 "function f()\n\
6443 local x = 0\n\
6444 x = 1\n\
6445 local a, b = 1, 2\n\
6446 a, b = b, a\n\
6447 end",
6448 "foo.lua",
6449 |metric| {
6450 assert_eq!(metric.abc.assignments_sum(), 4.0);
6451 assert_eq!(metric.abc.branches_sum(), 0.0);
6452 assert_eq!(metric.abc.conditions_sum(), 0.0);
6453 insta::assert_json_snapshot!(metric.abc);
6454 },
6455 );
6456 }
6457
6458 #[test]
6459 fn lua_calls_are_branches() {
6460 // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every
6461 // call form is a `function_call` node. The nested
6462 // `f(g(1))` counts as 2 branches (one per dispatch).
6463 check_metrics::<LuaParser>(
6464 "function r(x)\n\
6465 print(x)\n\
6466 obj.m(x)\n\
6467 obj:m(x)\n\
6468 return f(g(1))\n\
6469 end",
6470 "foo.lua",
6471 |metric| {
6472 assert_eq!(metric.abc.assignments_sum(), 0.0);
6473 assert_eq!(metric.abc.branches_sum(), 5.0);
6474 assert_eq!(metric.abc.conditions_sum(), 0.0);
6475 insta::assert_json_snapshot!(metric.abc);
6476 },
6477 );
6478 }
6479
6480 #[test]
6481 fn lua_comparisons_and_boolean_ops_count_conditions() {
6482 // Each comparison / logical operator token contributes one
6483 // condition.
6484 check_metrics::<LuaParser>(
6485 "function f(a, b)\n\
6486 local r\n\
6487 r = a == b\n\
6488 r = a ~= b\n\
6489 r = a < b\n\
6490 r = a > b\n\
6491 r = a <= b\n\
6492 r = a >= b\n\
6493 r = a and b\n\
6494 r = a or b\n\
6495 end",
6496 "foo.lua",
6497 |metric| {
6498 // 8 `r = …` reassignments, plus `local r` (no `=`).
6499 assert_eq!(metric.abc.assignments_sum(), 8.0);
6500 assert_eq!(metric.abc.branches_sum(), 0.0);
6501 assert_eq!(metric.abc.conditions_sum(), 8.0);
6502 insta::assert_json_snapshot!(metric.abc);
6503 },
6504 );
6505 }
6506
6507 #[test]
6508 fn lua_elseif_and_else_count_conditions() {
6509 // Each elseif / else arm of the if contributes one
6510 // condition, mirroring the Python rule.
6511 check_metrics::<LuaParser>(
6512 "function f(x)\n\
6513 if x > 0 then\n\
6514 return 1\n\
6515 elseif x < 0 then\n\
6516 return -1\n\
6517 else\n\
6518 return 0\n\
6519 end\n\
6520 end",
6521 "foo.lua",
6522 |metric| {
6523 // Comparisons: `>`, `<` → 2; elseif_statement → 1;
6524 // else_statement → 1. C = 4. No branches (no calls).
6525 assert_eq!(metric.abc.assignments_sum(), 0.0);
6526 assert_eq!(metric.abc.branches_sum(), 0.0);
6527 assert_eq!(metric.abc.conditions_sum(), 4.0);
6528 insta::assert_json_snapshot!(metric.abc);
6529 },
6530 );
6531 }
6532
6533 #[test]
6534 fn lua_complex_function_abc() {
6535 // Combines every category to pin the metric.
6536 check_metrics::<LuaParser>(
6537 "function run(n)\n\
6538 local total = 0\n\
6539 for i = 1, n do\n\
6540 if i % 2 == 0 then\n\
6541 do_work(i)\n\
6542 else\n\
6543 total = total + i\n\
6544 end\n\
6545 end\n\
6546 print(\"done\")\n\
6547 return total\n\
6548 end",
6549 "foo.lua",
6550 |metric| {
6551 // Assignments: `local total = 0` (1), `total = total + i` (1) → 2.
6552 // Branches: `do_work(i)` (1), `print(\"done\")` (1) → 2.
6553 // Conditions: `==` (1), `else_statement` (1) → 2.
6554 assert_eq!(metric.abc.assignments_sum(), 2.0);
6555 assert_eq!(metric.abc.branches_sum(), 2.0);
6556 assert_eq!(metric.abc.conditions_sum(), 2.0);
6557 insta::assert_json_snapshot!(metric.abc);
6558 },
6559 );
6560 }
6561
6562 // ---------- Tcl ABC tests ----------
6563
6564 #[test]
6565 fn tcl_empty_unit_zero() {
6566 check_metrics::<TclParser>("", "empty.tcl", |metric| {
6567 assert_eq!(metric.abc.assignments_sum(), 0.0);
6568 assert_eq!(metric.abc.branches_sum(), 0.0);
6569 assert_eq!(metric.abc.conditions_sum(), 0.0);
6570 insta::assert_json_snapshot!(metric.abc);
6571 });
6572 }
6573
6574 #[test]
6575 fn tcl_set_command_counts_assignment() {
6576 // `set` has its own grammar production; each invocation is
6577 // one assignment.
6578 check_metrics::<TclParser>(
6579 "proc f {} {\n\
6580 set x 1\n\
6581 set y 2\n\
6582 set x [expr {$x + $y}]\n\
6583 }",
6584 "foo.tcl",
6585 |metric| {
6586 // 3 `set` invocations → A=3. The inner `expr` is a
6587 // sub-command (`command_substitution` + `expr_cmd`),
6588 // not a `command` node, so it doesn't add a branch.
6589 assert_eq!(metric.abc.assignments_sum(), 3.0);
6590 assert_eq!(metric.abc.branches_sum(), 0.0);
6591 assert_eq!(metric.abc.conditions_sum(), 0.0);
6592 insta::assert_json_snapshot!(metric.abc);
6593 },
6594 );
6595 }
6596
6597 #[test]
6598 fn tcl_incr_append_lappend_count_assignment() {
6599 // Variable-mutation commands (`incr`, `append`, `lappend`)
6600 // are recognised by name and count as assignments, not
6601 // branches.
6602 check_metrics::<TclParser>(
6603 "proc f {} {\n\
6604 set x 0\n\
6605 incr x\n\
6606 append s \"hi\"\n\
6607 lappend lst 1\n\
6608 }",
6609 "foo.tcl",
6610 |metric| {
6611 // `set` (1) + `incr` (1) + `append` (1) + `lappend`
6612 // (1) → A=4. No branches, no conditions.
6613 assert_eq!(metric.abc.assignments_sum(), 4.0);
6614 assert_eq!(metric.abc.branches_sum(), 0.0);
6615 assert_eq!(metric.abc.conditions_sum(), 0.0);
6616 insta::assert_json_snapshot!(metric.abc);
6617 },
6618 );
6619 }
6620
6621 #[test]
6622 fn tcl_generic_commands_are_branches() {
6623 // Anything that isn't `set` or a known mutator command
6624 // counts as a branch — including builtins like `puts` and
6625 // `return`.
6626 check_metrics::<TclParser>(
6627 "proc f {} {\n\
6628 puts \"hello\"\n\
6629 do_work 1 2\n\
6630 return 0\n\
6631 }",
6632 "foo.tcl",
6633 |metric| {
6634 // 3 commands, all branches.
6635 assert_eq!(metric.abc.assignments_sum(), 0.0);
6636 assert_eq!(metric.abc.branches_sum(), 3.0);
6637 assert_eq!(metric.abc.conditions_sum(), 0.0);
6638 insta::assert_json_snapshot!(metric.abc);
6639 },
6640 );
6641 }
6642
6643 #[test]
6644 fn tcl_comparisons_and_boolean_ops_count_conditions() {
6645 // `expr` predicates expose comparison / logical tokens at
6646 // the leaf level; each token contributes one condition.
6647 check_metrics::<TclParser>(
6648 "proc f {a b} {\n\
6649 set r [expr {$a == $b}]\n\
6650 set r [expr {$a != $b}]\n\
6651 set r [expr {$a < $b}]\n\
6652 set r [expr {$a > $b}]\n\
6653 set r [expr {$a <= $b}]\n\
6654 set r [expr {$a >= $b}]\n\
6655 set r [expr {$a eq $b}]\n\
6656 set r [expr {$a ne $b}]\n\
6657 set r [expr {$a && $b}]\n\
6658 set r [expr {$a || $b}]\n\
6659 }",
6660 "foo.tcl",
6661 |metric| {
6662 // 10 `set` assignments. Each `expr` predicate
6663 // produces exactly one comparison/logical token.
6664 assert_eq!(metric.abc.assignments_sum(), 10.0);
6665 assert_eq!(metric.abc.branches_sum(), 0.0);
6666 assert_eq!(metric.abc.conditions_sum(), 10.0);
6667 insta::assert_json_snapshot!(metric.abc);
6668 },
6669 );
6670 }
6671
6672 #[test]
6673 fn tcl_ternary_counts_condition() {
6674 // `$a ? $b : $c` inside an `expr` is one `ternary_expr`
6675 // node → 1 condition.
6676 check_metrics::<TclParser>(
6677 "proc f {a b c} {\n\
6678 set r [expr {$a ? $b : $c}]\n\
6679 }",
6680 "foo.tcl",
6681 |metric| {
6682 assert_eq!(metric.abc.assignments_sum(), 1.0);
6683 assert_eq!(metric.abc.branches_sum(), 0.0);
6684 assert_eq!(metric.abc.conditions_sum(), 1.0);
6685 insta::assert_json_snapshot!(metric.abc);
6686 },
6687 );
6688 }
6689
6690 #[test]
6691 fn tcl_elseif_and_else_count_conditions() {
6692 // `if` / `elseif` / `else` clause productions each
6693 // contribute one condition. The leaf comparison inside the
6694 // predicate is counted independently.
6695 check_metrics::<TclParser>(
6696 "proc f {x} {\n\
6697 if {$x > 0} {\n\
6698 return 1\n\
6699 } elseif {$x < 0} {\n\
6700 return -1\n\
6701 } else {\n\
6702 return 0\n\
6703 }\n\
6704 }",
6705 "foo.tcl",
6706 |metric| {
6707 // Branches: three `return` commands → 3.
6708 // Conditions: `>` (1), `<` (1), `elseif` (1), `else`
6709 // (1) → 4.
6710 assert_eq!(metric.abc.assignments_sum(), 0.0);
6711 assert_eq!(metric.abc.branches_sum(), 3.0);
6712 assert_eq!(metric.abc.conditions_sum(), 4.0);
6713 insta::assert_json_snapshot!(metric.abc);
6714 },
6715 );
6716 }
6717
6718 #[test]
6719 fn tcl_complex_function_abc() {
6720 // Mixed program covering every category. Tcl's grammar
6721 // re-parses braced content that looks command-shaped as a
6722 // nested `command` node, which inflates the branch count
6723 // relative to a naive read of the source — see breakdown.
6724 check_metrics::<TclParser>(
6725 "proc run {n} {\n\
6726 set total 0\n\
6727 for {set i 0} {$i < $n} {incr i} {\n\
6728 if {$i % 2 == 0} {\n\
6729 do_work $i\n\
6730 } else {\n\
6731 incr total $i\n\
6732 }\n\
6733 }\n\
6734 puts \"done\"\n\
6735 return $total\n\
6736 }",
6737 "foo.tcl",
6738 |metric| {
6739 // Assignments: `set total 0` (1), `set i 0` (1),
6740 // `incr i` (1), `incr total $i` (1) → A = 4.
6741 // Branches: the outer `for …` is one `command` node;
6742 // the `{$i < $n}` predicate ALSO re-parses as a
6743 // `command` node (tree-sitter-tcl treats braced
6744 // predicates as nested commands at the pinned
6745 // grammar version); plus `do_work $i`, `puts
6746 // "done"`, and `return $total`. The for-loop body's
6747 // `incr` and `incr total $i` are assignment commands
6748 // and don't add branches. Total B = 5.
6749 // Conditions: `==` (1) and `else` (1) → C = 2. The
6750 // `<` inside `{$i < $n}` is NOT `Tcl::LT`: because
6751 // that predicate re-parses as a `command`, the `<`
6752 // is emitted as `simple_word`. Only `<` inside a
6753 // real `expr` production becomes `Tcl::LT`.
6754 assert_eq!(metric.abc.assignments_sum(), 4.0);
6755 assert_eq!(metric.abc.branches_sum(), 5.0);
6756 assert_eq!(metric.abc.conditions_sum(), 2.0);
6757 insta::assert_json_snapshot!(metric.abc);
6758 },
6759 );
6760 }
6761}