big_code_analysis/metrics/wmc.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(clippy::wildcard_imports, clippy::enum_glob_use)]
8// WMC stores cumulative cyclomatic as `f64` internally but exposes
9// integral `u64` accessors (#530); the `f64 as u64` / `u64 as f64` casts
10// are bounded by the counts they came from.
11#![allow(
12 clippy::cast_precision_loss,
13 clippy::cast_possible_truncation,
14 clippy::cast_sign_loss
15)]
16
17use std::fmt;
18
19use crate::checker::Checker;
20use crate::macros::implement_metric_trait;
21use crate::*;
22
23/// The `Wmc` metric.
24///
25/// This metric sums the cyclomatic complexities of all the methods defined in a class.
26/// The `Wmc` (Weighted Methods per Class) is an object-oriented metric for classes.
27///
28/// Original paper and definition:
29/// <https://www.researchgate.net/publication/3187649_Kemerer_CF_A_metric_suite_for_object_oriented_design_IEEE_Trans_Softw_Eng_206_476-493>
30#[derive(Debug, Clone, Default, PartialEq)]
31#[non_exhaustive]
32pub struct Stats {
33 cyclomatic: f64,
34 // Cumulative cyclomatic carried by descendant Class / Interface
35 // spaces (anonymous classes, nested object literals, …). A method
36 // that *contains* a nested class must not fold that class's
37 // complexity into its enclosing class's WMC — the nested class is
38 // its own WMC scope and already counts those methods. Tracking the
39 // nested-class cyclomatic lets `merge` subtract it from a Function's
40 // contribution, preventing double-attribution (#463).
41 nested_class_cyclomatic: f64,
42 // This space is a function written inside a container it is not a
43 // member of: a C++ `friend` with an inline body (#1301), or a plain
44 // C function inside an Objective-C `@implementation` / `@interface`
45 // / `@protocol` (#1356). WMC sums over a class's *methods*, so the
46 // enclosing class must not weight it. Phrased negatively so
47 // `Default` (false) means the ordinary case and only the exception
48 // has to be marked.
49 non_member: bool,
50 class_wmc: f64,
51 interface_wmc: f64,
52 class_wmc_sum: f64,
53 interface_wmc_sum: f64,
54 space_kind: SpaceKind,
55}
56
57impl fmt::Display for Stats {
58 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59 write!(
60 f,
61 "classes: {}, interfaces: {}, total: {}",
62 self.class_wmc_sum(),
63 self.interface_wmc_sum(),
64 self.total_wmc()
65 )
66 }
67}
68
69impl Stats {
70 /// Merges a second `Wmc` metric into the first one
71 pub fn merge(&mut self, other: &Stats) {
72 use SpaceKind::*;
73
74 // Rolls a child space's cyclomatic into the enclosing class /
75 // interface WMC, subtracting any nested-class complexity so it is
76 // not double-counted (#463). See the per-arm rationale below.
77 match other.space_kind {
78 // A method contributes its own cyclomatic minus the
79 // complexity already claimed by nested class / interface
80 // spaces it contains. Those nested classes form their own WMC
81 // scope (their members roll up via `class_wmc_sum`), so the
82 // method must not re-add them. Its nested-class total also
83 // bubbles up so an *ancestor* method can exclude this whole
84 // subtree in turn.
85 Function => {
86 let own_cyclomatic = other.cyclomatic - other.nested_class_cyclomatic;
87 // A non-member function nested in this container is not
88 // one of its methods, so it contributes nothing to the
89 // container's WMC (#1301). Its own space still reports
90 // the full cyclomatic, and the `nested_class_cyclomatic`
91 // bookkeeping below still bubbles, so a class defined
92 // inside it stays accounted for exactly once.
93 if !other.non_member {
94 match self.space_kind {
95 Class => self.class_wmc += own_cyclomatic,
96 Interface => self.interface_wmc += own_cyclomatic,
97 _ => {}
98 }
99 }
100 self.nested_class_cyclomatic += other.nested_class_cyclomatic;
101 }
102 // A nested Class / Interface space (e.g. an anonymous class)
103 // contributes its *cumulative* cyclomatic — which already
104 // subsumes any classes nested inside it — so we record only
105 // `other.cyclomatic` here, never also its
106 // `nested_class_cyclomatic` (that would double-count the
107 // inner classes, see #463 nested-anonymous case).
108 Class | Interface => self.nested_class_cyclomatic += other.cyclomatic,
109 _ => {}
110 }
111
112 self.class_wmc_sum += other.class_wmc_sum;
113 self.interface_wmc_sum += other.interface_wmc_sum;
114 }
115
116 /// Returns the `Wmc` metric value of the classes in a space.
117 #[inline]
118 #[must_use]
119 pub fn class_wmc(&self) -> u64 {
120 self.class_wmc as u64
121 }
122
123 /// Returns the `Wmc` metric value of the interfaces in a space.
124 #[inline]
125 #[must_use]
126 pub fn interface_wmc(&self) -> u64 {
127 self.interface_wmc as u64
128 }
129
130 /// Returns the sum of the `Wmc` metric values of the classes in a space.
131 #[inline]
132 #[must_use]
133 pub fn class_wmc_sum(&self) -> u64 {
134 self.class_wmc_sum as u64
135 }
136
137 /// Returns the sum of the `Wmc` metric values of the interfaces in a space.
138 #[inline]
139 #[must_use]
140 pub fn interface_wmc_sum(&self) -> u64 {
141 self.interface_wmc_sum as u64
142 }
143
144 /// Returns the total `Wmc` metric value in a space.
145 #[inline]
146 #[must_use]
147 pub fn total_wmc(&self) -> u64 {
148 self.class_wmc_sum() + self.interface_wmc_sum()
149 }
150
151 // Accumulates the `Wmc` metric values
152 // of classes and interfaces into the sums
153 #[inline]
154 pub(crate) fn compute_sum(&mut self) {
155 self.class_wmc_sum += self.class_wmc;
156 self.interface_wmc_sum += self.interface_wmc;
157 }
158
159 // Marks this space as a function that is not a member of the
160 // container enclosing it, so `merge` keeps its cyclomatic out of
161 // that container's WMC (#1301). Set once when the space is opened,
162 // from `Checker::is_non_member_function`.
163 #[inline]
164 pub(crate) fn mark_non_member(&mut self) {
165 self.non_member = true;
166 }
167
168 // Checks if the `Wmc` metric is disabled
169 #[inline]
170 pub(crate) fn is_disabled(&self) -> bool {
171 !self.space_kind.is_member_scope()
172 }
173}
174
175#[doc(hidden)]
176/// Per-language computation of weighted methods per class.
177pub(crate) trait Wmc
178where
179 Self: Checker,
180{
181 /// Walk `node` and update `stats` with this metric for the language
182 /// implementing the trait.
183 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats);
184}
185
186// Shared WMC compute for languages with class / interface / function /
187// unit space kinds (Java, C#, Kotlin). Records the space kind once and,
188// for function spaces, captures the cyclomatic sum so the aggregator can
189// roll it into the enclosing class / interface.
190fn class_interface_compute(
191 space_kind: SpaceKind,
192 cyclomatic: &cyclomatic::Stats,
193 stats: &mut Stats,
194) {
195 use SpaceKind::*;
196
197 if let Unit | Class | Interface | Function = space_kind {
198 if stats.space_kind == Unknown {
199 stats.space_kind = space_kind;
200 }
201 // Record the cumulative cyclomatic for Function spaces (the
202 // method's WMC contribution) and for Class / Interface spaces
203 // (so an ancestor method can subtract a nested class's
204 // complexity from its own contribution — see `merge`, #463).
205 if let Function | Class | Interface = space_kind {
206 stats.cyclomatic = cyclomatic.cyclomatic_sum() as f64;
207 }
208 }
209}
210
211impl Wmc for JavaCode {
212 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
213 class_interface_compute(space_kind, cyclomatic, stats);
214 }
215}
216
217impl Wmc for GroovyCode {
218 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
219 class_interface_compute(space_kind, cyclomatic, stats);
220 }
221}
222
223impl Wmc for CsharpCode {
224 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
225 class_interface_compute(space_kind, cyclomatic, stats);
226 }
227}
228
229// Kotlin's `class_declaration` becomes either `Class` or `Interface` via
230// `Getter::get_space_kind` (the keyword child disambiguates). `object`
231// singletons map to `Class`. Function spaces (top-level `fun`, member
232// `fun`, secondary constructors, lambdas, anonymous functions) contribute
233// their cyclomatic to the enclosing class / interface. `companion_object`
234// IS a `func_space` and opens its own `Class` scope (#431): `Checker::
235// is_func_space` matches `Kotlin::CompanionObject` and `Getter::
236// get_space_kind` maps it to `SpaceKind::Class`, so its members accrue to
237// the companion's own class-WMC rather than folding into the surrounding
238// class. See `kotlin_companion_object_opens_class_space` below.
239impl Wmc for KotlinCode {
240 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
241 class_interface_compute(space_kind, cyclomatic, stats);
242 }
243}
244
245impl Wmc for PhpCode {
246 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
247 use SpaceKind::*;
248
249 // Anonymous classes, enums, and traits all map to `Class` via
250 // `Getter::get_space_kind`, so a single `Class` arm covers them.
251 if let Unit | Class | Interface | Function = space_kind {
252 if stats.space_kind == Unknown {
253 stats.space_kind = space_kind;
254 }
255 // Record cyclomatic for Function spaces (the method's WMC
256 // contribution) and for Class / Interface spaces (so an
257 // ancestor method can exclude a nested class's complexity —
258 // see `merge`, #463; matters for PHP `AnonymousClass` nested
259 // inside a method).
260 if let Function | Class | Interface = space_kind {
261 stats.cyclomatic = cyclomatic.cyclomatic_sum() as f64;
262 }
263 }
264 }
265}
266
267// Python WMC. The shared `class_interface_compute` already does the
268// right thing for the four space kinds Python produces:
269// - `Unit` (module-level — receives WMC totals from top-level
270// classes, mirroring the Java unit-space aggregation).
271// - `Class` (every `ClassDefinition`).
272// - `Function` (every `FunctionDefinition`; captures the
273// per-function cyclomatic sum that the aggregator rolls up into
274// the enclosing class).
275// - `Unknown` (anything else — skipped).
276//
277// Lambdas (`Lambda`) are not `is_func` and therefore do not open a
278// `Function` space, so they correctly do *not* contribute to WMC
279// — they are anonymous expressions, not methods.
280impl Wmc for PythonCode {
281 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
282 class_interface_compute(space_kind, cyclomatic, stats);
283 }
284}
285
286// Rust WMC. Rust's `Impl` / `Trait` space kinds map onto the OO
287// "class" / "interface" concept for WMC purposes: every `impl` block
288// is a class, every `trait` is an interface, and each `function_item`
289// inside contributes its cyclomatic complexity to the surrounding
290// space.
291//
292// `class_interface_compute` is reused after mapping the space kind:
293// the Wmc `Stats.space_kind` field is the recipient that
294// `Stats::merge` keys off when rolling per-function cyclomatics into
295// the parent. Mapping to `Class` / `Interface` means the existing
296// merge logic produces the right numbers without touching the shared
297// helpers.
298//
299// Multiple `impl Foo` blocks each open their own Impl space and
300// accumulate independently; their `class_wmc_sum` values are merged
301// into the parent space (Unit) during finalisation, so the
302// file-level `class_wmc_sum` is the sum of cyclomatic complexity
303// across every impl block in the file.
304impl Wmc for RustCode {
305 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
306 let mapped = match space_kind {
307 SpaceKind::Impl => SpaceKind::Class,
308 SpaceKind::Trait => SpaceKind::Interface,
309 other => other,
310 };
311 class_interface_compute(mapped, cyclomatic, stats);
312 }
313}
314
315// C++ WMC. C++'s `class_specifier` and `struct_specifier` both map to
316// classes from the OO-metric perspective — `struct` and `class` differ
317// only in default visibility, not in their ability to hold methods.
318// The `Getter::get_space_kind` impl emits `SpaceKind::Struct` for
319// `struct_specifier`, so we collapse it onto `Class` before delegating
320// to `class_interface_compute`.
321//
322// `SpaceKind::Namespace` is intentionally dropped — namespaces are not
323// classes; their member functions are free functions and do not
324// contribute to a per-class WMC. The Unit space still accumulates the
325// per-class sums for file-level reporting.
326impl Wmc for CppCode {
327 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
328 let mapped = match space_kind {
329 SpaceKind::Struct => SpaceKind::Class,
330 other => other,
331 };
332 class_interface_compute(mapped, cyclomatic, stats);
333 }
334}
335
336impl Wmc for MozcppCode {
337 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
338 let mapped = match space_kind {
339 SpaceKind::Struct => SpaceKind::Class,
340 other => other,
341 };
342 class_interface_compute(mapped, cyclomatic, stats);
343 }
344}
345
346// Objective-C: `@implementation` is a `SpaceKind::Class` and `@interface`
347// / `@protocol` are `SpaceKind::Interface` (see `getter.rs`); each
348// `method_definition` opens a `SpaceKind::Function` whose cyclomatic sum
349// rolls into its enclosing class via the shared aggregator — the same
350// shape as Java / TS. ObjC has no `struct`-as-class, so no `Struct`
351// remap is needed.
352//
353// A plain C `function_definition` written inside any of those three is a
354// file-static helper rather than a method, and `ObjcCode::is_non_member_function`
355// keeps it out of the container's WMC (#1356) — the C++ `friend`
356// divergence of #1301 in a sibling language.
357impl Wmc for ObjcCode {
358 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
359 class_interface_compute(space_kind, cyclomatic, stats);
360 }
361}
362
363// TypeScript / TSX both expose `class_declaration`,
364// `abstract_class_declaration` (mapped to `SpaceKind::Class` in
365// `getter.rs`) and `interface_declaration` (`SpaceKind::Interface`).
366// Method bodies live in `method_definition` and `arrow_function`
367// function spaces; their cyclomatic sums roll into the enclosing
368// class / interface via `class_interface_compute`. Abstract method
369// signatures (`abstract_method_signature`) have no body and so
370// contribute zero to WMC, matching Java's `abstract` method rule.
371impl Wmc for TypescriptCode {
372 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
373 class_interface_compute(space_kind, cyclomatic, stats);
374 }
375}
376
377impl Wmc for TsxCode {
378 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
379 class_interface_compute(space_kind, cyclomatic, stats);
380 }
381}
382
383// Ruby's `Class` and `SingletonClass` map to `SpaceKind::Class` via
384// `Getter::get_space_kind`; `Module` maps to `SpaceKind::Namespace`
385// and so does not contribute a `Wmc` bucket of its own. Every Ruby
386// `Method` / `SingletonMethod` is a `SpaceKind::Function` whose
387// cyclomatic sum rolls into the enclosing class via
388// `class_interface_compute`. Ruby has no interface construct, so the
389// `Interface` arm is unreachable but harmless.
390impl Wmc for RubyCode {
391 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
392 class_interface_compute(space_kind, cyclomatic, stats);
393 }
394}
395
396// JavaScript / Mozjs WMC. JS classes (`class_declaration`,
397// `class_expression`) both map to `SpaceKind::Class` in `getter.rs`,
398// and method bodies are `method_definition` / `arrow_function`
399// function spaces — the same shape as TS/Java. `class_interface_compute`
400// rolls per-function cyclomatic sums into the enclosing class.
401impl Wmc for JavascriptCode {
402 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
403 class_interface_compute(space_kind, cyclomatic, stats);
404 }
405}
406
407impl Wmc for MozjsCode {
408 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
409 class_interface_compute(space_kind, cyclomatic, stats);
410 }
411}
412
413// Go WMC is intentionally a no-op. Go has no `class` syntactic
414// construct; methods are declared as `MethodDeclaration` nodes
415// attached to a receiver type that lives elsewhere as a `StructType`.
416// The Wmc trait signature receives only the per-space `SpaceKind` and
417// cyclomatic stats — it cannot tell which `Function` space corresponds
418// to a `MethodDeclaration` (receiver method) versus a free-standing
419// `FunctionDeclaration`, and the FuncSpace tree exposes no
420// per-receiver "class" space to attribute methods to. Implementing
421// Wmc correctly per the issue's "methods grouped by receiver = class"
422// rule would require either a new `SpaceKind::Struct` variant for Go
423// receiver methods or a richer trait signature, both of which are
424// out of scope. See the issue body's explicit option (a): "keep
425// scoring zero with a documented reason".
426
427// Elixir WMC. Classes (`defmodule`) and Functions (`def` / `defp` /
428// `defmacro` / `defmacrop`) are detected by source-aware Checker /
429// Getter dispatch (#275), so the FuncSpace tree already carries the
430// right `SpaceKind`s. Each method-defining macro opens a Function
431// space whose cyclomatic complexity rolls into its enclosing
432// `defmodule` Class via the shared aggregator. `defp` (private) is
433// included — it is still a *method* of the class even though it is
434// not part of the public API (the npm-style "public" filter belongs
435// in `Npm`, not `Wmc`).
436impl Wmc for ElixirCode {
437 fn compute(space_kind: SpaceKind, cyclomatic: &cyclomatic::Stats, stats: &mut Stats) {
438 class_interface_compute(space_kind, cyclomatic, stats);
439 }
440}
441
442// Default no-op `Wmc` impls. Audited in #188. See the rationale block
443// on `implement_metric_trait!(Npa, …)` in `src/metrics/npa.rs`. Wmc needs
444// the same per-language class / method detection plumbing (Wmc =
445// sum-of-cyclomatic-per-method), and mirrors Npa's set EXCEPT Go: Npa/Npm
446// implement Go but Wmc no-ops it because Go's flat space model cannot
447// attribute methods to a receiver class (see the Go rationale block above).
448implement_metric_trait!(
449 Wmc,
450 CCode,
451 PreprocCode,
452 CcommentCode,
453 GoCode,
454 PerlCode,
455 BashCode,
456 LuaCode,
457 TclCode,
458 IrulesCode
459);
460
461#[cfg(test)]
462#[allow(
463 clippy::float_cmp,
464 clippy::cast_precision_loss,
465 clippy::cast_possible_truncation,
466 clippy::cast_sign_loss,
467 clippy::similar_names,
468 clippy::doc_markdown,
469 clippy::needless_raw_string_hashes,
470 clippy::too_many_lines
471)]
472mod tests {
473 use crate::test_support::{
474 assert_child_space_kind, check_func_space_only_shim, check_metrics_only_shim, child_space,
475 };
476
477 use super::*;
478
479 // Wmc's dependency closure adds Cyclomatic + Nom — the per-method
480 // complexities it weights and the method count it needs to find them.
481 check_metrics_only_shim!(check_metrics, Wmc);
482 check_func_space_only_shim!(check_func_space, Wmc);
483 // The C# indexer / property tests (#464, #472) cross-check that the
484 // WMC accessor folding agrees with npm's method count for the same
485 // member, so they need Npm too.
486 check_metrics_only_shim!(check_wmc_and_npm, Wmc, Npm);
487
488 #[test]
489 fn java_single_class() {
490 check_metrics::<JavaParser>(
491 "public class Example { // wmc = 13
492
493 public boolean m1(boolean a, boolean b) { // +1
494 boolean r = false;
495 if (a && b == a || b) { // +3
496 r = true;
497 }
498 return r;
499 }
500
501 public boolean m2(int n) { // +1
502 for (int i = 0; i < n; i++) { // +1
503 int j = n;
504 while (j > i) { // +1
505 j--;
506 }
507 }
508 return (n % 2 == 0) ? true : false; // +1
509 }
510
511 public int m3(int x, int y, int z) { // +1
512 int ret;
513 try {
514 z = x/y + y/x;
515 } catch (ArithmeticException e) { // +1
516 z = (x == 0) ? -1 : -2; // +1
517 }
518 switch (z) {
519 case -1: // +1
520 ret = y * y;
521 break;
522 case -2: // +1
523 ret = x * x;
524 break;
525 default:
526 ret = x + y;
527 }
528 return ret;
529 }
530 }",
531 "foo.java",
532 |metric| {
533 // 1 class
534 insta::assert_json_snapshot!(
535 metric.wmc,
536 @r#"
537 {
538 "class_wmc_sum": 13,
539 "interface_wmc_sum": 0,
540 "total": 13
541 }
542 "#
543 );
544 },
545 );
546 }
547
548 #[test]
549 fn groovy_single_class() {
550 // WMC = sum of method cyclomatic complexities for the class.
551 check_metrics::<GroovyParser>(
552 "class Example {
553 boolean m1(boolean a, boolean b) {
554 boolean r = false
555 if (a && b == a || b) {
556 r = true
557 }
558 return r
559 }
560 boolean m2(int n) {
561 for (int i = 0; i < n; i++) {
562 int j = n
563 while (j > i) {
564 j--
565 }
566 }
567 return (n % 2 == 0) ? true : false
568 }
569 }",
570 "foo.groovy",
571 |metric| {
572 // m1: entry(1) + if(1) + &&(1) + ||(1) = 4
573 // m2: entry(1) + for(1) + while(1) + ternary(1) = 4
574 // WMC = 4 + 4 = 8
575 assert_eq!(metric.wmc.class_wmc_sum(), 8);
576 },
577 );
578 }
579
580 #[test]
581 fn groovy_empty_class() {
582 check_metrics::<GroovyParser>("class Empty {}", "foo.groovy", |metric| {
583 assert_eq!(metric.wmc.class_wmc_sum(), 0);
584 });
585 }
586
587 #[test]
588 fn groovy_class_with_single_method() {
589 check_metrics::<GroovyParser>(
590 "class A {
591 void foo() {
592 println 'hi'
593 }
594 }",
595 "foo.groovy",
596 |metric| {
597 // single method has entry +1 = 1
598 assert_eq!(metric.wmc.class_wmc_sum(), 1);
599 },
600 );
601 }
602
603 #[test]
604 fn groovy_multiple_classes() {
605 check_metrics::<GroovyParser>(
606 "class A {
607 void f() { if (true) {} }
608 }
609 class B {
610 void g() {}
611 }",
612 "foo.groovy",
613 |metric| {
614 // A.f: 1 + 1 (if) = 2, B.g: 1 → total = 3
615 assert_eq!(metric.wmc.class_wmc_sum(), 3);
616 },
617 );
618 }
619
620 #[test]
621 fn groovy_class_with_branching_methods() {
622 check_metrics::<GroovyParser>(
623 "class Calc {
624 int abs(int x) {
625 if (x < 0) {
626 return -x
627 }
628 return x
629 }
630 int sign(int x) {
631 if (x > 0) return 1
632 if (x < 0) return -1
633 return 0
634 }
635 }",
636 "foo.groovy",
637 |metric| {
638 // abs: 1 + 1 (if) = 2; sign: 1 + 2 (two ifs) = 3 → 5
639 assert_eq!(metric.wmc.class_wmc_sum(), 5);
640 },
641 );
642 }
643
644 #[test]
645 fn groovy_interface_wmc_is_zero() {
646 // Interfaces declare method signatures with no body — wmc = 0.
647 check_metrics::<GroovyParser>(
648 "interface I {
649 void a()
650 void b()
651 }",
652 "foo.groovy",
653 |metric| {
654 assert_eq!(metric.wmc.class_wmc_sum(), 0);
655 },
656 );
657 }
658
659 #[test]
660 fn groovy_static_nested_class() {
661 // Mirrors `java_static_nested_class`: nested classes get
662 // their own WMC space tied to their parent class's scope.
663 check_metrics::<GroovyParser>(
664 "class TopLevelClass {
665 static class StaticNestedClass {
666 private void m() {
667 println 'Test'
668 }
669 }
670 }",
671 "foo.groovy",
672 |metric| {
673 // TopLevelClass(0) + StaticNestedClass(1 = entry only).
674 assert_eq!(metric.wmc.class_wmc_sum(), 1);
675 },
676 );
677 }
678
679 #[test]
680 #[ignore = "dekobon Groovy grammar v1 does not yet support inner classes inside class bodies"]
681 fn groovy_nested_inner_classes_wmc() {
682 // Three nested classes each with one trivial method.
683 // Mirrors `java_nested_inner_classes` (wmc.rs flavor).
684 check_metrics::<GroovyParser>(
685 "class X {
686 void a() {}
687 class Y {
688 void b() {}
689 class Z {
690 void c() {}
691 }
692 }
693 }",
694 "foo.groovy",
695 |metric| {
696 // 3 classes, each with one method => 1 + 1 + 1 = 3.
697 assert_eq!(metric.wmc.class_wmc_sum(), 3);
698 },
699 );
700 }
701
702 #[test]
703 fn groovy_local_inner_class() {
704 // A class declared inside a method body. WMC counts its method
705 // like any other class, and the local class is its own WMC scope:
706 // `Local.l` (entry 1 + if 1 = 2) must not also fold into
707 // `Outer.m`'s contribution to `Outer`. `Outer.m` itself = 1, so
708 // total = 1 + 2 = 3 with no double-attribution (#463; the prior
709 // value of 6 double-counted the local class's complexity into both
710 // scopes, compounded by the Groovy grammar wrapping `m`'s body in a
711 // `closure`).
712 check_metrics::<GroovyParser>(
713 "class Outer {
714 void m() {
715 class Local {
716 void l() {
717 if (true) {}
718 }
719 }
720 }
721 }",
722 "foo.groovy",
723 |metric| {
724 assert_eq!(metric.wmc.class_wmc_sum(), 3);
725 },
726 );
727 }
728
729 #[test]
730 #[ignore = "dekobon Groovy grammar v1 does not yet support anonymous inner classes (`new T() { … }`)"]
731 fn groovy_anonymous_inner_class_wmc() {
732 // `new Runnable() { ... }` anonymous inner class. WMC
733 // includes the inner's method bodies.
734 check_metrics::<GroovyParser>(
735 "abstract class Base {
736 abstract void m1()
737 }
738 class Top {
739 void m() {
740 def b = new Base() {
741 void m1() {
742 for (int i = 0; i < 5; i++) {
743 println(i)
744 }
745 }
746 }
747 }
748 }",
749 "foo.groovy",
750 |metric| {
751 // Base.m1(1) + Top.m(1) + anonymous.m1(1+for(1)) = 4
752 assert_eq!(metric.wmc.class_wmc_sum(), 4);
753 },
754 );
755 }
756
757 #[test]
758 fn groovy_lambda_expression_wmc() {
759 // Lambdas inside a method body don't form their own class
760 // space, but the surrounding methods still count toward WMC.
761 check_metrics::<GroovyParser>(
762 "class Top {
763 void m1() {
764 def list = [1, 2, 3]
765 list.each { n -> println(n) }
766 }
767 void m2() {
768 if (true) {}
769 }
770 }",
771 "foo.groovy",
772 |metric| {
773 // m1(1) + m2(1 + if(1)) = 3
774 assert_eq!(metric.wmc.class_wmc_sum(), 3);
775 },
776 );
777 }
778
779 #[test]
780 fn groovy_single_interface_wmc() {
781 // Default methods inside an interface contribute to WMC.
782 // Mirrors `java_single_interface`.
783 check_metrics::<GroovyParser>(
784 "interface Example {
785 default boolean m1(boolean a, boolean b) {
786 return (a && b == a || b)
787 }
788 default int m2(int n) {
789 return (n != 0) ? 1/n : n
790 }
791 void m3()
792 }",
793 "foo.groovy",
794 |metric| {
795 // m1(1 + && + ||) + m2(1 + ternary) + m3(1) = 6
796 assert_eq!(metric.wmc.interface_wmc_sum(), 6);
797 assert_eq!(metric.wmc.class_wmc_sum(), 0);
798 },
799 );
800 }
801
802 #[test]
803 #[ignore = "dekobon Groovy grammar v1 does not yet support inner classes inside interface bodies"]
804 fn groovy_class_in_interface() {
805 // Inner class inside an interface — its methods count
806 // toward `class_wmc`, not `interface_wmc`.
807 check_metrics::<GroovyParser>(
808 "interface Outer {
809 void api()
810 class Inner {
811 void f() {
812 if (true) {}
813 }
814 }
815 }",
816 "foo.groovy",
817 |metric| {
818 // Outer interface: api(1) = 1; Inner class: f(1+if) = 2.
819 assert_eq!(metric.wmc.interface_wmc_sum(), 1);
820 assert_eq!(metric.wmc.class_wmc_sum(), 2);
821 },
822 );
823 }
824
825 // Regression for issue #280: Groovy enum bodies fold method-level
826 // cyclomatic into `class_wmc_sum` just like Java.
827 #[test]
828 fn groovy_enum_wmc_aggregates_method_complexity() {
829 check_metrics::<GroovyParser>(
830 "enum Status {
831 ACTIVE, INACTIVE;
832 public int code(int n) {
833 if (n > 0) { return n }
834 return 0
835 }
836 }",
837 "foo.groovy",
838 |metric| {
839 assert_eq!(metric.wmc.class_wmc_sum(), 2);
840 },
841 );
842 }
843
844 // Mirror of `java_annotation_type_opens_interface_space_with_zero_wmc`
845 // — verifies #280 wired `Groovy::AnnotationTypeDeclaration` into
846 // `is_func_space` (the structural check) while keeping
847 // `interface_wmc_sum` at 0 because elements are not method
848 // declarations. The structural assertion is what distinguishes a
849 // working fix from a vacuous one (see the Java sibling for the
850 // rationale).
851 #[test]
852 #[ignore = "dekobon Groovy grammar v1 does not support annotation type elements with `default` values"]
853 fn groovy_annotation_type_opens_interface_space_with_zero_wmc() {
854 check_func_space::<GroovyParser, _>(
855 "public @interface Marker {
856 String value() default \"\";
857 int priority() default 0;
858 }",
859 "foo.groovy",
860 |func_space| {
861 assert_eq!(func_space.metrics.wmc.interface_wmc_sum(), 0);
862 assert_child_space_kind(&func_space, "Marker", SpaceKind::Interface);
863 },
864 );
865 }
866
867 // Constructors are considered as methods
868 // Reference: https://pdepend.org/documentation/software-metrics/weighted-method-count.html
869 #[test]
870 fn java_multiple_classes() {
871 check_metrics::<JavaParser>(
872 "public class MainClass { // wmc = 3
873 private int a;
874 public MainClass() { // +1
875 a = 0;
876 }
877 public void setA(int n) { // +1
878 a = n;
879 }
880 public int getA() { // +1
881 return a;
882 }
883 }
884
885 class TopLevelClass { // wmc = 2
886 private int b;
887 public TopLevelClass() { // +1
888 b = 0;
889 }
890 public int getB() { // +1
891 return b;
892 }
893 }",
894 "foo.java",
895 |metric| {
896 // 2 classes (3 + 2)
897 insta::assert_json_snapshot!(
898 metric.wmc,
899 @r#"
900 {
901 "class_wmc_sum": 5,
902 "interface_wmc_sum": 0,
903 "total": 5
904 }
905 "#
906 );
907 },
908 );
909 }
910
911 #[test]
912 fn java_static_nested_class() {
913 check_metrics::<JavaParser>(
914 "public class TopLevelClass { // wmc = 0
915 public static class StaticNestedClass { // wmc = 1
916 private void m() { // +1
917 System.out.println(\"Test\");
918 }
919 }
920 }",
921 "foo.java",
922 |metric| {
923 // 2 classes (0 + 2)
924 insta::assert_json_snapshot!(
925 metric.wmc,
926 @r#"
927 {
928 "class_wmc_sum": 1,
929 "interface_wmc_sum": 0,
930 "total": 1
931 }
932 "#
933 );
934 },
935 );
936 }
937
938 #[test]
939 fn java_nested_inner_classes() {
940 check_metrics::<JavaParser>(
941 "public class TopLevelClass { // wmc = 2
942 private int a;
943
944 class InnerClassBefore { // wmc = 1
945 private boolean b = (a % 2 == 0) ? true : false;
946 public boolean getB() { // +1
947 return b;
948 }
949 }
950
951 public TopLevelClass(int n) { // +1
952 if (a != n) { // +1
953 a = n;
954 }
955 }
956
957 class InnerClassAfter { // wmc = 2
958 private int c = a;
959
960 public int getC() { // +1
961 return c;
962 }
963 public void setC(int n) { // +1
964 c = n;
965 }
966
967 class InnerClass1 { // wmc = 1
968 private int p1;
969 class InnerClass2 { // wmc = 1
970 private int p2;
971 public int getP2() { // +1
972 return p2;
973 }
974 class InnerClass3 { // wmc = 2
975 private int p3;
976 public int getP3() { // +1
977 return p3;
978 }
979 public void setP3(int n) { // +1
980 p3 = n;
981 }
982 }
983 }
984 public void setP1(int n) { // +1
985 p1 = n;
986 }
987 }
988 }
989 }",
990 "foo.java",
991 |metric| {
992 // 6 classes (2 + 1 + 2 + 1 + 1 + 2)
993 insta::assert_json_snapshot!(
994 metric.wmc,
995 @r#"
996 {
997 "class_wmc_sum": 9,
998 "interface_wmc_sum": 0,
999 "total": 9
1000 }
1001 "#
1002 );
1003 },
1004 );
1005 }
1006
1007 #[test]
1008 fn java_local_inner_class() {
1009 check_metrics::<JavaParser>(
1010 "import java.util.LinkedList;
1011 import java.util.List;
1012
1013 public final class FinalClass { // wmc = 1 (test only)
1014 private int a = 1;
1015 public void test() { // +1
1016 final List<String> localList = new LinkedList<String>();
1017
1018 class LocalInnerClass { // wmc = 2 (print only)
1019 private int b = (a == 1) ? 1 : 0; // field init, not a method
1020 public void print() { // +1
1021 for ( String s : localList ) { // +1
1022 System.out.println(s);
1023 }
1024 }
1025 }
1026 }
1027 }",
1028 "foo.java",
1029 |metric| {
1030 // Two classes: `FinalClass` (its only method `test` = 1)
1031 // and the local `LocalInnerClass` (its only method `print`
1032 // = base 1 + for 1 = 2). The ternary in `b`'s initializer
1033 // is class-body cyclomatic, not a method, so it does not
1034 // count toward WMC. `LocalInnerClass` is its own WMC scope,
1035 // so its complexity must NOT also fold into `test`'s
1036 // contribution to `FinalClass` — total = 1 + 2 = 3, with no
1037 // double-attribution (#463; previously the local class's
1038 // complexity was double-counted into both scopes, giving an
1039 // inflated 7).
1040 assert_eq!(metric.wmc.class_wmc_sum(), 3);
1041 insta::assert_json_snapshot!(
1042 metric.wmc,
1043 @r#"
1044 {
1045 "class_wmc_sum": 3,
1046 "interface_wmc_sum": 0,
1047 "total": 3
1048 }
1049 "#
1050 );
1051 },
1052 );
1053 }
1054
1055 #[test]
1056 fn java_anonymous_inner_class() {
1057 check_metrics::<JavaParser>(
1058 "abstract class AbstractClass { // wmc = 1
1059 abstract void m1(); // +1
1060 }
1061 public class TopLevelClass{ // wmc = 3
1062 public void m(){ // +1
1063 AbstractClass ac1 = new AbstractClass() {
1064 void m1() { // +1
1065 for (int i = 0; i < 5; i++) { // +1
1066 System.out.println(\"Test 1: \" + i);
1067 }
1068 }
1069 };
1070 ac1.m1();
1071 }
1072 }",
1073 "foo.java",
1074 |metric| {
1075 // 2 classes (1 + 3)
1076 insta::assert_json_snapshot!(
1077 metric.wmc,
1078 @r#"
1079 {
1080 "class_wmc_sum": 4,
1081 "interface_wmc_sum": 0,
1082 "total": 4
1083 }
1084 "#
1085 );
1086 },
1087 );
1088 }
1089
1090 #[test]
1091 fn java_nested_anonymous_inner_classes() {
1092 check_metrics::<JavaParser>(
1093 "abstract class AbstractClass{ // wmc = 2
1094 abstract void m1(); // +1
1095 abstract void m2(); // +1
1096 }
1097 public class TopLevelClass{ // wmc = 6
1098 public void m(){ // +1
1099
1100 AbstractClass ac1 = new AbstractClass() {
1101 void m1() { // +1
1102 for (int i = 0; i < 5; i++) { // +1
1103 System.out.println(\"Test 1: \" + i);
1104 }
1105 }
1106 void m2() { // +1
1107 AbstractClass ac2 = new AbstractClass() {
1108 void m1() { // +1
1109 System.out.println(\"Test A\");
1110 }
1111 void m2() { // +1
1112 System.out.println(\"Test B\");
1113 }
1114 };
1115 ac2.m2();
1116 System.out.println(\"Test 2\");
1117 }
1118 };
1119 ac1.m1();
1120 }
1121 }",
1122 "foo.java",
1123 |metric| {
1124 // 2 classes (2 + 6)
1125 insta::assert_json_snapshot!(
1126 metric.wmc,
1127 @r#"
1128 {
1129 "class_wmc_sum": 8,
1130 "interface_wmc_sum": 0,
1131 "total": 8
1132 }
1133 "#
1134 );
1135 },
1136 );
1137 }
1138
1139 #[test]
1140 fn java_lambda_expression() {
1141 check_metrics::<JavaParser>(
1142 "import java.util.ArrayList;
1143
1144 public class TopLevelClass { // wmc = 2
1145 private ArrayList<Integer> numbers;
1146
1147 public void m1() { // +1
1148 numbers = new ArrayList<Integer>();
1149 numbers.add(1);
1150 numbers.add(2);
1151 numbers.add(3);
1152 }
1153
1154 public void m2() { // +1
1155 numbers.forEach( (n) -> { System.out.println(n); } );
1156 }
1157 }",
1158 "foo.java",
1159 |metric| {
1160 // 1 class
1161 insta::assert_json_snapshot!(
1162 metric.wmc,
1163 @r#"
1164 {
1165 "class_wmc_sum": 2,
1166 "interface_wmc_sum": 0,
1167 "total": 2
1168 }
1169 "#
1170 );
1171 },
1172 );
1173 }
1174
1175 #[test]
1176 fn java_single_interface() {
1177 check_metrics::<JavaParser>(
1178 "interface Example { // wmc = 6
1179 default boolean m1(boolean a, boolean b) { // +1
1180 return (a && b == a || b); // +2
1181 }
1182 default int m2(int n) { // +1
1183 return (n != 0) ? 1/n : n; // +1
1184 };
1185 void m3(); // +1
1186 }",
1187 "foo.java",
1188 |metric| {
1189 // 1 interface
1190 insta::assert_json_snapshot!(
1191 metric.wmc,
1192 @r#"
1193 {
1194 "class_wmc_sum": 0,
1195 "interface_wmc_sum": 6,
1196 "total": 6
1197 }
1198 "#
1199 );
1200 },
1201 );
1202 }
1203
1204 #[test]
1205 fn java_multiple_interfaces() {
1206 check_metrics::<JavaParser>(
1207 "interface FirstInterface { // wmc = 1
1208 int a = 0;
1209 default int getA() { // +1
1210 return a;
1211 }
1212 }
1213
1214 interface SecondInterface { // wmc = 2
1215 void setB(int n); // +1
1216 int getB(); // +1
1217 }",
1218 "foo.java",
1219 |metric| {
1220 // 2 interfaces (1 + 2)
1221 insta::assert_json_snapshot!(
1222 metric.wmc,
1223 @r#"
1224 {
1225 "class_wmc_sum": 0,
1226 "interface_wmc_sum": 3,
1227 "total": 3
1228 }
1229 "#
1230 );
1231 },
1232 );
1233 }
1234
1235 #[test]
1236 fn java_nested_inner_interfaces() {
1237 check_metrics::<JavaParser>(
1238 "interface TopLevelInterface { // wmc = 1
1239 interface InnerInterfaceBefore { // wmc = 1
1240 void m1(); // +1
1241 }
1242
1243 void m2(); // +1
1244
1245 interface InnerInterfaceAfter { // wmc = 2
1246 void m3(); // +1
1247 interface InnerInterface { // wmc = 1
1248 void m4(); // +1
1249 }
1250 void m5(); // +1
1251 }
1252 }",
1253 "foo.java",
1254 |metric| {
1255 // 4 interfaces (1 + 1 + 2 + 1)
1256 insta::assert_json_snapshot!(
1257 metric.wmc,
1258 @r#"
1259 {
1260 "class_wmc_sum": 0,
1261 "interface_wmc_sum": 5,
1262 "total": 5
1263 }
1264 "#
1265 );
1266 },
1267 );
1268 }
1269
1270 #[test]
1271 fn java_class_in_interface() {
1272 check_metrics::<JavaParser>(
1273 "interface TopLevelInterface { // wmc = 2
1274 int getA(); // +1
1275 boolean getB(); // +1
1276
1277 class InnerClass { // wmc = 2
1278 float c;
1279 double d;
1280 float getC() { // +1
1281 return c;
1282 }
1283 double getD() { // +1
1284 return d;
1285 }
1286 }
1287 }",
1288 "foo.java",
1289 |metric| {
1290 // 1 class 1 interface
1291 insta::assert_json_snapshot!(
1292 metric.wmc,
1293 @r#"
1294 {
1295 "class_wmc_sum": 2,
1296 "interface_wmc_sum": 2,
1297 "total": 4
1298 }
1299 "#
1300 );
1301 },
1302 );
1303 }
1304
1305 #[test]
1306 fn java_interface_in_class() {
1307 check_metrics::<JavaParser>(
1308 "class TopLevelClass { // wmc = 2
1309 int a;
1310 boolean b;
1311 int getA() { // +1
1312 return a;
1313 }
1314 boolean getB() { // +1
1315 return b;
1316 }
1317
1318 interface InnerInterface { // wmc = 2
1319 float getC(); // +1
1320 double getD(); // +1
1321 }
1322 }",
1323 "foo.java",
1324 |metric| {
1325 // 1 class 1 interface
1326 insta::assert_json_snapshot!(
1327 metric.wmc,
1328 @r#"
1329 {
1330 "class_wmc_sum": 2,
1331 "interface_wmc_sum": 2,
1332 "total": 4
1333 }
1334 "#
1335 );
1336 },
1337 );
1338 }
1339
1340 // Regression for issue #280: Java `EnumDeclaration` opens a class
1341 // space, so method-level cyclomatic complexity inside the enum
1342 // body folds into `class_wmc_sum`.
1343 #[test]
1344 fn java_enum_wmc_aggregates_method_complexity() {
1345 check_metrics::<JavaParser>(
1346 "enum Status {
1347 ACTIVE, INACTIVE;
1348 public int code(int n) { // entry +1
1349 if (n > 0) { // if +1
1350 return n;
1351 }
1352 return 0;
1353 }
1354 }",
1355 "foo.java",
1356 |metric| {
1357 // 1 enum (class), 1 method with cyclomatic = 2.
1358 assert_eq!(metric.wmc.class_wmc_sum(), 2);
1359 },
1360 );
1361 }
1362
1363 // Regression for issue #280: Java `RecordDeclaration` is treated as
1364 // a class space; methods inside its explicit body contribute to
1365 // WMC.
1366 #[test]
1367 fn java_record_wmc_aggregates_method_complexity() {
1368 check_metrics::<JavaParser>(
1369 "record Point(int x, int y) {
1370 public int describe() { // entry +1
1371 return (x == 0) ? 0 : 1; // ternary +1
1372 }
1373 }",
1374 "foo.java",
1375 |metric| {
1376 assert_eq!(metric.wmc.class_wmc_sum(), 2);
1377 },
1378 );
1379 }
1380
1381 /// Regression for #1160: a record's compact constructor parses as
1382 /// `compact_constructor_declaration`, not `constructor_declaration`,
1383 /// so it opened no `SpaceKind::Function` space and its cyclomatic
1384 /// never reached the record's WMC.
1385 ///
1386 /// The two constructor spellings coexist here deliberately. JLS
1387 /// 8.10.4 forbids declaring the *canonical* constructor both
1388 /// compactly and normally, but an alternative constructor delegating
1389 /// with `this(…)` is legal alongside a compact one — and each must
1390 /// open its own space even though both are named `R`.
1391 #[test]
1392 fn java_record_compact_and_alternative_constructors_open_two_spaces() {
1393 check_func_space::<JavaParser, _>(
1394 "record R(int a, int b) {
1395 R { // entry +1, if +1
1396 if (a < 0) { throw new IllegalArgumentException(); }
1397 }
1398 R(int a) { this(a, 0); } // entry +1
1399 }",
1400 "R.java",
1401 |space| {
1402 assert_child_space_kind(&space, "R", SpaceKind::Class);
1403 let record = child_space(&space, "R");
1404 let constructors = record
1405 .spaces
1406 .iter()
1407 .filter(|child| child.kind == SpaceKind::Function)
1408 .count();
1409 assert_eq!(constructors, 2, "compact + delegating constructor spaces");
1410 // 2 (compact: entry + `if`) + 1 (delegating: entry).
1411 // Pre-fix the compact constructor contributed nothing and
1412 // this read 1.
1413 assert_eq!(record.metrics.wmc.class_wmc(), 3);
1414 },
1415 );
1416 }
1417
1418 /// Control for #1160: a record with no constructor is unchanged.
1419 /// `Java::RecordDeclaration` sits in `Checker::is_func_space` (#280)
1420 /// and is what opens the class space the compact constructor now
1421 /// nests inside — the record still opens one class space holding
1422 /// exactly its one declared method.
1423 ///
1424 /// It holds in both directions, deliberately: #1160 touched `is_func`
1425 /// and `get_space_kind`, not `is_func_space`, so no perturbation of
1426 /// that change can make this fail. It is here to state the boundary
1427 /// of the change, not to cover a line — the `RecordDeclaration` arm
1428 /// itself is covered by #280's tests.
1429 #[test]
1430 fn java_record_without_constructor_opens_only_its_methods() {
1431 check_func_space::<JavaParser, _>(
1432 "record R(int a, int b) {
1433 int sum() { return a + b; } // entry +1
1434 }",
1435 "R.java",
1436 |space| {
1437 assert_child_space_kind(&space, "R", SpaceKind::Class);
1438 let record = child_space(&space, "R");
1439 let functions: Vec<_> = record
1440 .spaces
1441 .iter()
1442 .filter(|child| child.kind == SpaceKind::Function)
1443 .map(|child| child.name.as_deref())
1444 .collect();
1445 assert_eq!(functions, vec![Some("sum")]);
1446 assert_eq!(record.metrics.wmc.class_wmc(), 1);
1447 },
1448 );
1449 }
1450
1451 // Regression for issue #280: Java `AnnotationTypeDeclaration` must
1452 // open a `SpaceKind::Interface` FuncSpace (the `is_func_space`
1453 // change) AND must not aggregate WMC because annotation type
1454 // elements parse as `AnnotationTypeElementDeclaration`, not
1455 // `MethodDeclaration`, so no `Function` space is opened for them
1456 // and their entry cyclomatic is not folded into
1457 // `interface_wmc_sum`. Asserting only `interface_wmc_sum == 0`
1458 // would pass vacuously even if `AnnotationTypeDeclaration` were
1459 // dropped from `is_func_space` (the FuncSpace tree would simply
1460 // omit the annotation type space, and `0 == 0` would still hold);
1461 // the structural check on `space.kind` is what catches that
1462 // regression.
1463 #[test]
1464 fn java_annotation_type_opens_interface_space_with_zero_wmc() {
1465 check_func_space::<JavaParser, _>(
1466 "@interface Marker {
1467 String value() default \"\";
1468 int priority() default 0;
1469 }",
1470 "foo.java",
1471 |func_space| {
1472 assert_eq!(func_space.metrics.wmc.interface_wmc_sum(), 0);
1473 // Without `AnnotationTypeDeclaration` in `is_func_space`,
1474 // the file-level Unit would have zero child spaces here.
1475 assert_child_space_kind(&func_space, "Marker", SpaceKind::Interface);
1476 },
1477 );
1478 }
1479
1480 #[test]
1481 fn csharp_single_class() {
1482 check_metrics::<CsharpParser>(
1483 "public class Example {
1484 public bool M1(bool a, bool b) {
1485 bool r = false;
1486 if (a && b == a || b) {
1487 r = true;
1488 }
1489 return r;
1490 }
1491 public int M2(int n) {
1492 for (int i = 0; i < n; i++) {
1493 int j = n;
1494 while (j > i) {
1495 j--;
1496 }
1497 }
1498 return (n % 2 == 0) ? 1 : 0;
1499 }
1500 }",
1501 "foo.cs",
1502 |metric| {
1503 assert_eq!(metric.wmc.class_wmc_sum(), 8);
1504 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1505 insta::assert_json_snapshot!(metric.wmc);
1506 },
1507 );
1508 }
1509
1510 #[test]
1511 fn csharp_multiple_classes() {
1512 check_metrics::<CsharpParser>(
1513 "public class A {
1514 private int a;
1515 public A() { a = 0; }
1516 public void SetA(int n) { a = n; }
1517 public int GetA() { return a; }
1518 }
1519 class B {
1520 private int b;
1521 public B() { b = 0; }
1522 public int GetB() { return b; }
1523 }",
1524 "foo.cs",
1525 |metric| {
1526 assert_eq!(metric.wmc.class_wmc_sum(), 5);
1527 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1528 insta::assert_json_snapshot!(metric.wmc);
1529 },
1530 );
1531 }
1532
1533 #[test]
1534 fn csharp_static_nested_class() {
1535 check_metrics::<CsharpParser>(
1536 "public class Outer {
1537 public static class Nested {
1538 private void M() {
1539 System.Console.WriteLine(\"Test\");
1540 }
1541 }
1542 }",
1543 "foo.cs",
1544 |metric| {
1545 assert_eq!(metric.wmc.class_wmc_sum(), 1);
1546 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1547 insta::assert_json_snapshot!(metric.wmc);
1548 },
1549 );
1550 }
1551
1552 #[test]
1553 fn csharp_nested_inner_classes() {
1554 check_metrics::<CsharpParser>(
1555 "public class Outer {
1556 private int a;
1557 public class Inner {
1558 public int GetX() { return 0; }
1559 public class Innermost {
1560 public int GetY() { return 1; }
1561 }
1562 }
1563 public int GetA() { return a; }
1564 }",
1565 "foo.cs",
1566 |metric| {
1567 assert_eq!(metric.wmc.class_wmc_sum(), 3);
1568 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1569 insta::assert_json_snapshot!(metric.wmc);
1570 },
1571 );
1572 }
1573
1574 #[test]
1575 fn csharp_local_inner_class() {
1576 // C# uses local functions instead of Java's local classes.
1577 check_metrics::<CsharpParser>(
1578 "public class A {
1579 public int M(int x) {
1580 int Local(int y) {
1581 if (y > 0) return y;
1582 return -y;
1583 }
1584 return Local(x);
1585 }
1586 }",
1587 "foo.cs",
1588 |metric| {
1589 assert_eq!(metric.wmc.class_wmc_sum(), 3);
1590 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1591 insta::assert_json_snapshot!(metric.wmc);
1592 },
1593 );
1594 }
1595
1596 #[test]
1597 fn csharp_anonymous_inner_class() {
1598 check_metrics::<CsharpParser>(
1599 "public class A {
1600 public void Run() {
1601 System.Action f = delegate(int x) {
1602 if (x > 0) System.Console.WriteLine(x);
1603 };
1604 }
1605 }",
1606 "foo.cs",
1607 |metric| {
1608 assert_eq!(metric.wmc.class_wmc_sum(), 3);
1609 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1610 insta::assert_json_snapshot!(metric.wmc);
1611 },
1612 );
1613 }
1614
1615 #[test]
1616 fn csharp_nested_anonymous_inner_classes() {
1617 check_metrics::<CsharpParser>(
1618 "public class A {
1619 public void Run() {
1620 System.Action f = delegate(int x) {
1621 System.Action g = delegate(int y) {
1622 if (y > 0) System.Console.WriteLine(y);
1623 };
1624 };
1625 }
1626 }",
1627 "foo.cs",
1628 |metric| {
1629 assert_eq!(metric.wmc.class_wmc_sum(), 4);
1630 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1631 insta::assert_json_snapshot!(metric.wmc);
1632 },
1633 );
1634 }
1635
1636 #[test]
1637 fn csharp_lambda_expression() {
1638 check_metrics::<CsharpParser>(
1639 "public class A {
1640 public void Run() {
1641 System.Func<int, int> f = x => x > 0 ? x : -x;
1642 }
1643 }",
1644 "foo.cs",
1645 |metric| {
1646 assert_eq!(metric.wmc.class_wmc_sum(), 3);
1647 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1648 insta::assert_json_snapshot!(metric.wmc);
1649 },
1650 );
1651 }
1652
1653 #[test]
1654 fn csharp_indexer_wmc() {
1655 // A bodied indexer folds its accessor complexities into the
1656 // enclosing class. Before #464 the `indexer_declaration` node
1657 // itself ALSO opened a method space, folding an extra entry on
1658 // top of get=1 + set=1 (`class_wmc_sum == 3`). The correct sum is
1659 // 2 — one unit of complexity per accessor — matching the npm path.
1660 check_wmc_and_npm::<CsharpParser>(
1661 "class A {
1662 private int[] _d;
1663 public int this[int i] { get => _d[i]; set => _d[i] = value; }
1664 }",
1665 "foo.cs",
1666 |metric| {
1667 // expected: get (cyclomatic 1) + set (cyclomatic 1) = 2;
1668 // no extra entry from the IndexerDeclaration node.
1669 assert_eq!(metric.wmc.class_wmc_sum(), 2);
1670 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1671 assert_eq!(metric.npm.class_nm_sum(), 2);
1672 insta::assert_json_snapshot!(metric.wmc);
1673 },
1674 );
1675 }
1676
1677 #[test]
1678 fn csharp_expression_bodied_indexer_wmc() {
1679 // The accessor-less expression-bodied form (`this[int i] => _d[i];`)
1680 // has no `accessor_declaration` child, so the #464 gate keeps the
1681 // IndexerDeclaration node itself opening a single method space —
1682 // it must stay at 1, not regress to 0 (mirrors npm `.max(1)`).
1683 check_wmc_and_npm::<CsharpParser>(
1684 "class A {
1685 private int[] _d;
1686 public int this[int i] => _d[i];
1687 }",
1688 "foo.cs",
1689 |metric| {
1690 // expected: one implicit getter (cyclomatic 1) = 1.
1691 assert_eq!(metric.wmc.class_wmc_sum(), 1);
1692 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1693 assert_eq!(metric.npm.class_nm_sum(), 1);
1694 insta::assert_json_snapshot!(metric.wmc);
1695 },
1696 );
1697 }
1698
1699 #[test]
1700 fn csharp_property_wmc() {
1701 // A bodied property folds its accessor complexities into the
1702 // enclosing class. The `property_declaration` node must NOT open an
1703 // extra method space on top of get=1 + set=1 (the property analogue
1704 // of the #464 double-count). The correct sum is 2.
1705 check_wmc_and_npm::<CsharpParser>(
1706 "class A {
1707 private int _w;
1708 public int W { get => _w; set => _w = value; }
1709 }",
1710 "foo.cs",
1711 |metric| {
1712 // expected: get (cyclomatic 1) + set (cyclomatic 1) = 2;
1713 // no extra entry from the PropertyDeclaration node (#472).
1714 assert_eq!(metric.wmc.class_wmc_sum(), 2);
1715 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1716 assert_eq!(metric.npm.class_nm_sum(), 2);
1717 },
1718 );
1719 }
1720
1721 #[test]
1722 fn csharp_expression_bodied_property_wmc() {
1723 // The accessor-less expression-bodied form (`int W => _w;`) has no
1724 // `accessor_declaration` child, so the #472 gate lets the
1725 // PropertyDeclaration node itself open a single method space — it
1726 // must be 1, not 0 as before the fix (mirrors npm `.max(1)`).
1727 check_wmc_and_npm::<CsharpParser>(
1728 "class A {
1729 private int _w;
1730 public int W => _w;
1731 }",
1732 "foo.cs",
1733 |metric| {
1734 // expected: one implicit getter (cyclomatic 1) = 1.
1735 assert_eq!(metric.wmc.class_wmc_sum(), 1);
1736 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
1737 assert_eq!(metric.npm.class_nm_sum(), 1);
1738 },
1739 );
1740 }
1741
1742 #[test]
1743 fn csharp_single_interface() {
1744 check_metrics::<CsharpParser>(
1745 "public interface I {
1746 int GetA();
1747 int GetB();
1748 }",
1749 "foo.cs",
1750 |metric| {
1751 assert_eq!(metric.wmc.class_wmc_sum(), 0);
1752 assert_eq!(metric.wmc.interface_wmc_sum(), 2);
1753 insta::assert_json_snapshot!(metric.wmc);
1754 },
1755 );
1756 }
1757
1758 #[test]
1759 fn csharp_multiple_interfaces() {
1760 check_metrics::<CsharpParser>(
1761 "public interface I1 { int GetA(); }
1762 public interface I2 { bool GetB(); float GetC(); }",
1763 "foo.cs",
1764 |metric| {
1765 assert_eq!(metric.wmc.class_wmc_sum(), 0);
1766 assert_eq!(metric.wmc.interface_wmc_sum(), 3);
1767 insta::assert_json_snapshot!(metric.wmc);
1768 },
1769 );
1770 }
1771
1772 #[test]
1773 fn csharp_nested_inner_interfaces() {
1774 check_metrics::<CsharpParser>(
1775 "public interface I1 {
1776 int GetA();
1777 public interface I2 {
1778 bool GetB();
1779 }
1780 }",
1781 "foo.cs",
1782 |metric| {
1783 assert_eq!(metric.wmc.class_wmc_sum(), 0);
1784 assert_eq!(metric.wmc.interface_wmc_sum(), 2);
1785 insta::assert_json_snapshot!(metric.wmc);
1786 },
1787 );
1788 }
1789
1790 #[test]
1791 fn csharp_class_in_interface() {
1792 check_metrics::<CsharpParser>(
1793 "public interface I {
1794 int GetA();
1795 public class Helper {
1796 public int M() { return 0; }
1797 }
1798 }",
1799 "foo.cs",
1800 |metric| {
1801 assert_eq!(metric.wmc.class_wmc_sum(), 1);
1802 assert_eq!(metric.wmc.interface_wmc_sum(), 1);
1803 insta::assert_json_snapshot!(metric.wmc);
1804 },
1805 );
1806 }
1807
1808 #[test]
1809 fn csharp_interface_in_class() {
1810 check_metrics::<CsharpParser>(
1811 "class Outer {
1812 int a;
1813 bool b;
1814 public int GetA() { return a; }
1815 public bool GetB() { return b; }
1816 public interface InnerI {
1817 float GetC();
1818 double GetD();
1819 }
1820 }",
1821 "foo.cs",
1822 |metric| {
1823 assert_eq!(metric.wmc.class_wmc_sum(), 2);
1824 assert_eq!(metric.wmc.interface_wmc_sum(), 2);
1825 insta::assert_json_snapshot!(metric.wmc);
1826 },
1827 );
1828 }
1829
1830 #[test]
1831 fn php_no_classes() {
1832 check_metrics::<PhpParser>(
1833 "<?php function f(): int { return 1; }",
1834 "foo.php",
1835 |metric| insta::assert_json_snapshot!(metric.wmc),
1836 );
1837 }
1838
1839 #[test]
1840 fn php_one_class_simple() {
1841 check_metrics::<PhpParser>(
1842 "<?php
1843 class A {
1844 public function a(): int { return 1; }
1845 public function b(): int { return 2; }
1846 }",
1847 "foo.php",
1848 |metric| insta::assert_json_snapshot!(metric.wmc),
1849 );
1850 }
1851
1852 #[test]
1853 fn php_one_class_with_loops() {
1854 check_metrics::<PhpParser>(
1855 "<?php
1856 class A {
1857 public function f(int $n): int {
1858 $sum = 0;
1859 for ($i = 0; $i < $n; $i++) {
1860 $sum += $i;
1861 }
1862 return $sum;
1863 }
1864 }",
1865 "foo.php",
1866 |metric| insta::assert_json_snapshot!(metric.wmc),
1867 );
1868 }
1869
1870 #[test]
1871 fn php_one_class_with_branches() {
1872 check_metrics::<PhpParser>(
1873 "<?php
1874 class A {
1875 public function f(int $x): int {
1876 if ($x > 0) {
1877 return 1;
1878 }
1879 if ($x < 0) {
1880 return -1;
1881 }
1882 return 0;
1883 }
1884 }",
1885 "foo.php",
1886 |metric| insta::assert_json_snapshot!(metric.wmc),
1887 );
1888 }
1889
1890 #[test]
1891 fn php_class_with_methods_only() {
1892 check_metrics::<PhpParser>(
1893 "<?php
1894 class A {
1895 public function a(): void {}
1896 public function b(): void {}
1897 public function c(): void {}
1898 }",
1899 "foo.php",
1900 |metric| insta::assert_json_snapshot!(metric.wmc),
1901 );
1902 }
1903
1904 #[test]
1905 fn php_multiple_classes() {
1906 check_metrics::<PhpParser>(
1907 "<?php
1908 class A {
1909 public function f(int $x): int {
1910 if ($x > 0) { return 1; }
1911 return 0;
1912 }
1913 }
1914 class B {
1915 public function g(int $x): int {
1916 return $x;
1917 }
1918 }",
1919 "foo.php",
1920 |metric| insta::assert_json_snapshot!(metric.wmc),
1921 );
1922 }
1923
1924 #[test]
1925 fn php_anonymous_class() {
1926 check_metrics::<PhpParser>(
1927 "<?php
1928 $obj = new class {
1929 public function f(int $x): int {
1930 if ($x > 0) { return 1; }
1931 return 0;
1932 }
1933 };",
1934 "foo.php",
1935 |metric| insta::assert_json_snapshot!(metric.wmc),
1936 );
1937 }
1938
1939 #[test]
1940 fn php_class_with_static_methods() {
1941 check_metrics::<PhpParser>(
1942 "<?php
1943 class A {
1944 public static function f(int $x): int {
1945 if ($x > 0) { return 1; }
1946 return 0;
1947 }
1948 public static function g(): int { return 1; }
1949 }",
1950 "foo.php",
1951 |metric| insta::assert_json_snapshot!(metric.wmc),
1952 );
1953 }
1954
1955 #[test]
1956 fn php_interface_wmc() {
1957 check_metrics::<PhpParser>(
1958 "<?php
1959 interface I {
1960 public function a(): void;
1961 public function b(): int;
1962 }",
1963 "foo.php",
1964 |metric| insta::assert_json_snapshot!(metric.wmc),
1965 );
1966 }
1967
1968 #[test]
1969 fn php_trait_wmc() {
1970 check_metrics::<PhpParser>(
1971 "<?php
1972 trait T {
1973 public function f(int $x): int {
1974 if ($x > 0) { return 1; }
1975 return 0;
1976 }
1977 }",
1978 "foo.php",
1979 |metric| insta::assert_json_snapshot!(metric.wmc),
1980 );
1981 }
1982
1983 #[test]
1984 fn php_enum_with_methods() {
1985 check_metrics::<PhpParser>(
1986 "<?php
1987 enum Color {
1988 case Red;
1989 case Green;
1990 public function label(): string {
1991 return match ($this) {
1992 Color::Red => 'r',
1993 Color::Green => 'g',
1994 };
1995 }
1996 }",
1997 "foo.php",
1998 |metric| insta::assert_json_snapshot!(metric.wmc),
1999 );
2000 }
2001
2002 #[test]
2003 fn php_class_inside_namespace() {
2004 check_metrics::<PhpParser>(
2005 "<?php
2006 namespace App;
2007 class A {
2008 public function f(int $x): int {
2009 if ($x > 0) { return 1; }
2010 return 0;
2011 }
2012 }",
2013 "foo.php",
2014 |metric| insta::assert_json_snapshot!(metric.wmc),
2015 );
2016 }
2017
2018 #[test]
2019 fn php_class_complex() {
2020 check_metrics::<PhpParser>(
2021 "<?php
2022 class Calc {
2023 public function add(int $a, int $b): int {
2024 if ($a > 0 && $b > 0) {
2025 return $a + $b;
2026 }
2027 return 0;
2028 }
2029 public function loop(int $n): int {
2030 $s = 0;
2031 for ($i = 0; $i < $n; $i++) {
2032 if ($i % 2 === 0) { $s += $i; }
2033 }
2034 return $s;
2035 }
2036 }",
2037 "foo.php",
2038 |metric| insta::assert_json_snapshot!(metric.wmc),
2039 );
2040 }
2041
2042 // --- Kotlin WMC tests -------------------------------------------------
2043 //
2044 // Reference: Kotlin `class_declaration` carries either a `class` or
2045 // `interface` keyword child; the getter routes the former to
2046 // `SpaceKind::Class` and the latter to `SpaceKind::Interface`. Member
2047 // function cyclomatic complexity accumulates into the enclosing
2048 // class/interface bucket, mirroring the Java impl.
2049
2050 #[test]
2051 fn kotlin_empty_class() {
2052 // Empty class — no methods, WMC = 0.
2053 check_metrics::<KotlinParser>("class Empty {}", "foo.kt", |metric| {
2054 assert_eq!(metric.wmc.class_wmc_sum(), 0);
2055 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2056 insta::assert_json_snapshot!(metric.wmc);
2057 });
2058 }
2059
2060 #[test]
2061 fn kotlin_single_class() {
2062 // wmc = 1 (method base) + 1 (if) + 1 (explicit when arm; `else`
2063 // skipped per #282) = 3
2064 check_metrics::<KotlinParser>(
2065 "class C {
2066 fun m(x: Int): Int { // +1
2067 if (x > 0) { // +1
2068 return x
2069 }
2070 return when (x) {
2071 0 -> 0 // +1 (WhenEntry)
2072 else -> -x // skipped (else is default)
2073 }
2074 }
2075 }",
2076 "foo.kt",
2077 |metric| {
2078 assert_eq!(metric.wmc.class_wmc_sum(), 3);
2079 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2080 insta::assert_json_snapshot!(metric.wmc);
2081 },
2082 );
2083 }
2084
2085 #[test]
2086 fn kotlin_multiple_classes() {
2087 // A: constructor 1 + setA 1 + getA 1 = 3
2088 // B: constructor 1 + getB 1 = 2
2089 check_metrics::<KotlinParser>(
2090 "class A {
2091 private var a: Int = 0
2092 constructor(n: Int) { a = n } // +1
2093 fun setA(n: Int) { a = n } // +1
2094 fun getA(): Int = a // +1
2095 }
2096 class B {
2097 private var b: Int = 0
2098 constructor(n: Int) { b = n } // +1
2099 fun getB(): Int = b // +1
2100 }",
2101 "foo.kt",
2102 |metric| {
2103 assert_eq!(metric.wmc.class_wmc_sum(), 5);
2104 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2105 insta::assert_json_snapshot!(metric.wmc);
2106 },
2107 );
2108 }
2109
2110 #[test]
2111 fn kotlin_nested_class() {
2112 // Outer: 0 methods. Nested: m(): +1
2113 check_metrics::<KotlinParser>(
2114 "class Outer {
2115 class Nested {
2116 fun m() { println(\"hi\") } // +1
2117 }
2118 }",
2119 "foo.kt",
2120 |metric| {
2121 assert_eq!(metric.wmc.class_wmc_sum(), 1);
2122 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2123 insta::assert_json_snapshot!(metric.wmc);
2124 },
2125 );
2126 }
2127
2128 #[test]
2129 fn kotlin_inner_class() {
2130 // `inner class` differs semantically (captures outer reference) but
2131 // structurally still opens a new class space.
2132 check_metrics::<KotlinParser>(
2133 "class Outer {
2134 fun outerM() {} // +1
2135 inner class Inner {
2136 fun innerM() {} // +1
2137 }
2138 }",
2139 "foo.kt",
2140 |metric| {
2141 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2142 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2143 insta::assert_json_snapshot!(metric.wmc);
2144 },
2145 );
2146 }
2147
2148 #[test]
2149 fn kotlin_data_class() {
2150 // `data class` synthesizes copy/equals/hashCode/toString at
2151 // compile time, but only user-written methods are counted —
2152 // compiler-generated members are not user code.
2153 check_metrics::<KotlinParser>(
2154 "data class Point(val x: Int, val y: Int) {
2155 fun manhattan(): Int = kotlin.math.abs(x) + kotlin.math.abs(y) // +1
2156 }",
2157 "foo.kt",
2158 |metric| {
2159 assert_eq!(metric.wmc.class_wmc_sum(), 1);
2160 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2161 insta::assert_json_snapshot!(metric.wmc);
2162 },
2163 );
2164 }
2165
2166 #[test]
2167 fn kotlin_object_singleton() {
2168 // `object` declarations are singletons; the getter routes them to
2169 // `SpaceKind::Class` so their methods count as class methods.
2170 check_metrics::<KotlinParser>(
2171 "object Util {
2172 fun add(a: Int, b: Int): Int = a + b // +1
2173 fun gtZero(n: Int): Boolean { // +1
2174 return n > 0
2175 }
2176 }",
2177 "foo.kt",
2178 |metric| {
2179 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2180 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2181 insta::assert_json_snapshot!(metric.wmc);
2182 },
2183 );
2184 }
2185
2186 #[test]
2187 fn kotlin_companion_object() {
2188 // A `companion object` opens its own Class space, exactly like a
2189 // named `object` declaration (#431). The companion's `mk` and the
2190 // enclosing class's `get` are each attributed to their own Class
2191 // space; the file-level `class_wmc_sum` aggregates both (1 + 1 = 2)
2192 // with no member lost or double-counted.
2193 check_metrics::<KotlinParser>(
2194 "class Holder {
2195 val instance: Int = 1
2196 fun get(): Int = instance // +1
2197 companion object {
2198 val SCALE: Int = 10
2199 fun mk(): Holder = Holder() // +1
2200 }
2201 }",
2202 "foo.kt",
2203 |metric| {
2204 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2205 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2206 insta::assert_json_snapshot!(metric.wmc);
2207 },
2208 );
2209 }
2210
2211 #[test]
2212 fn kotlin_companion_object_opens_class_space() {
2213 // Structural guard for #431: a named `companion object` must open
2214 // its own Class space, mirroring the named-object handling, rather
2215 // than folding its members into the enclosing class. Reverting the
2216 // `CompanionObject` arm in `get_space_kind` / `is_func_space` drops
2217 // the `Companion` child space, failing this assertion.
2218 check_func_space::<KotlinParser, _>(
2219 "class Holder {
2220 fun get(): Int = 1
2221 companion object Companion {
2222 fun mk(): Holder = Holder()
2223 }
2224 }",
2225 "foo.kt",
2226 |func_space| {
2227 let holder = func_space
2228 .spaces
2229 .iter()
2230 .find(|s| s.name.as_deref() == Some("Holder"))
2231 .expect("expected a child FuncSpace named \"Holder\"");
2232 // The companion is a Class space nested inside Holder, not a
2233 // sibling at the file level and not absorbed into Holder.
2234 assert_child_space_kind(holder, "Companion", crate::SpaceKind::Class);
2235 let companion = holder
2236 .spaces
2237 .iter()
2238 .find(|s| s.name.as_deref() == Some("Companion"))
2239 .expect("expected a child FuncSpace named \"Companion\"");
2240 // `mk` is attributed to the companion space (wmc = 1), and
2241 // Holder's roll-up totals get + mk = 2 with no double-count
2242 // (the file-level aggregate stays 2, not 3).
2243 assert_eq!(companion.metrics.wmc.class_wmc_sum(), 1);
2244 assert_eq!(holder.metrics.wmc.class_wmc_sum(), 2);
2245 },
2246 );
2247 }
2248
2249 #[test]
2250 fn kotlin_object_literal_opens_class_space() {
2251 // Structural guard for #463: an anonymous `object : T { ... }`
2252 // (`object_literal`) must open its own Class space, exactly like a
2253 // named `object` or `companion object`, rather than folding its
2254 // members into the enclosing function. Reverting the
2255 // `ObjectLiteral` arm in `get_space_kind` / `is_func_space`
2256 // attributes `run` and `helper` to `Holder.get`, failing the
2257 // structural assertions below (verified by revert).
2258 check_func_space::<KotlinParser, _>(
2259 "class Holder {
2260 fun get(): Int {
2261 val r = object : Runnable {
2262 override fun run() {}
2263 fun helper(): Int = 42
2264 }
2265 return 1
2266 }
2267 }",
2268 "foo.kt",
2269 |func_space| {
2270 let holder = func_space
2271 .spaces
2272 .iter()
2273 .find(|s| s.name.as_deref() == Some("Holder"))
2274 .expect("expected a child FuncSpace named \"Holder\"");
2275 let get = holder
2276 .spaces
2277 .iter()
2278 .find(|s| s.name.as_deref() == Some("get"))
2279 .expect("expected a child FuncSpace named \"get\"");
2280 // The anonymous object opens a Class space nested inside
2281 // `get`, named `<anonymous>` (default `get_func_space_name`).
2282 assert_child_space_kind(get, "<anonymous>", crate::SpaceKind::Class);
2283 let anon = get
2284 .spaces
2285 .iter()
2286 .find(|s| s.name.as_deref() == Some("<anonymous>"))
2287 .expect("expected a child FuncSpace named \"<anonymous>\"");
2288 // `run` and `helper` are attributed to the anonymous space
2289 // (its two child Function spaces), NOT to `get`. `get`'s
2290 // only direct child is the anonymous space — there are no
2291 // stray `run` / `helper` siblings folded into `get`.
2292 assert_eq!(
2293 get.spaces.len(),
2294 1,
2295 "get's only child is the anonymous class"
2296 );
2297 let anon_methods = anon
2298 .spaces
2299 .iter()
2300 .filter(|s| s.kind == crate::SpaceKind::Function)
2301 .count();
2302 assert_eq!(
2303 anon_methods, 2,
2304 "run + helper attributed to the anonymous class"
2305 );
2306 // Issue's core requirement: members are *removed* from the
2307 // enclosing method, not merely added to the anonymous space.
2308 // `get`'s own function count is just itself; reverting the
2309 // space-opening arm folds run/helper back in, lifting it to 3.
2310 assert_eq!(
2311 get.metrics.nom.functions(),
2312 1,
2313 "get owns only itself; run/helper are not folded in"
2314 );
2315 // The anonymous class rolls up both methods' WMC (run +
2316 // helper = 2). These two methods are accounted for in the
2317 // anonymous space, not double-counted into `get`.
2318 assert_eq!(anon.metrics.wmc.class_wmc_sum(), 2);
2319 },
2320 );
2321 }
2322
2323 #[test]
2324 fn java_anonymous_class_opens_space() {
2325 // #463: a Java anonymous class (`new Runnable() { ... }`) opens its
2326 // own Class space so its members are attributed to it, not the
2327 // enclosing method. A plain `new Object()` (no `class_body` child)
2328 // and a lambda (`() -> {}`, a distinct `lambda_expression`) must
2329 // NOT open a Class space — the gate is on the `class_body` child.
2330 // Reverting the `ObjectCreationExpression` gate drops the anonymous
2331 // Class space and re-attributes `run` to `m`, failing this test.
2332 check_func_space::<JavaParser, _>(
2333 "class C {
2334 void m() {
2335 Runnable r = new Runnable() {
2336 public void run() {}
2337 };
2338 Object o = new Object();
2339 Runnable l = () -> {};
2340 }
2341 }",
2342 "C.java",
2343 |func_space| {
2344 let c = func_space
2345 .spaces
2346 .iter()
2347 .find(|s| s.name.as_deref() == Some("C"))
2348 .expect("expected a child FuncSpace named \"C\"");
2349 let m = c
2350 .spaces
2351 .iter()
2352 .find(|s| s.name.as_deref() == Some("m"))
2353 .expect("expected a child FuncSpace named \"m\"");
2354 // Exactly one Class child under `m`: the anonymous class.
2355 // Plain `new Object()` and the lambda must not over-open;
2356 // the lambda is a Function space (Java tags
2357 // `LambdaExpression` as Function), so count Class children.
2358 let anon_classes: Vec<_> = m
2359 .spaces
2360 .iter()
2361 .filter(|s| s.kind == crate::SpaceKind::Class)
2362 .collect();
2363 assert_eq!(anon_classes.len(), 1, "exactly one anonymous Class space");
2364 assert_eq!(anon_classes[0].name.as_deref(), Some("<anonymous>"));
2365 // `run` is attributed to the anonymous class (its single
2366 // child Function space), not to `m`.
2367 let anon_methods = anon_classes[0]
2368 .spaces
2369 .iter()
2370 .filter(|s| s.kind == crate::SpaceKind::Function)
2371 .count();
2372 assert_eq!(anon_methods, 1, "run attributed to the anonymous class");
2373 assert_eq!(anon_classes[0].metrics.wmc.class_wmc_sum(), 1);
2374 },
2375 );
2376 }
2377
2378 #[test]
2379 fn java_lambda_opens_no_class_space() {
2380 // Guard against mis-detection (#463): a Java lambda is a
2381 // `lambda_expression`, NOT an `object_creation_expression`, so it
2382 // must never trip the anonymous-class gate. It opens a Function
2383 // space (existing behaviour), never a Class space.
2384 check_func_space::<JavaParser, _>(
2385 "class C {
2386 void m() {
2387 Runnable l = () -> { int x = 1; };
2388 }
2389 }",
2390 "C.java",
2391 |func_space| {
2392 let c = func_space
2393 .spaces
2394 .iter()
2395 .find(|s| s.name.as_deref() == Some("C"))
2396 .expect("expected a child FuncSpace named \"C\"");
2397 let m = c
2398 .spaces
2399 .iter()
2400 .find(|s| s.name.as_deref() == Some("m"))
2401 .expect("expected a child FuncSpace named \"m\"");
2402 let class_children = m
2403 .spaces
2404 .iter()
2405 .filter(|s| s.kind == crate::SpaceKind::Class)
2406 .count();
2407 assert_eq!(class_children, 0, "a lambda must not open a Class space");
2408 },
2409 );
2410 }
2411
2412 #[test]
2413 fn groovy_anonymous_class_models_body_as_closure() {
2414 // #463 upstream-grammar note: the pinned dekobon Groovy grammar
2415 // does NOT attach an anonymous-class body to its
2416 // `object_creation_expression`. It parses `new Runnable()` as a
2417 // bare constructor call and the trailing `{ ... }` as a separate
2418 // `closure`, which already opens a Function space. So Groovy gets
2419 // no Class space for an anonymous class (unlike Java), but its
2420 // members are still NOT mis-attributed to the enclosing method —
2421 // they land in the closure's Function space. This pins that
2422 // behaviour so a future grammar bump that starts modelling
2423 // `class_body` here is caught and the Groovy `get_space_kind` arm
2424 // can be revisited.
2425 check_func_space::<GroovyParser, _>(
2426 "class C {
2427 void m() {
2428 def r = new Runnable() {
2429 void run() {}
2430 }
2431 }
2432 }",
2433 "C.groovy",
2434 |func_space| {
2435 let c = func_space
2436 .spaces
2437 .iter()
2438 .find(|s| s.name.as_deref() == Some("C"))
2439 .expect("expected a child FuncSpace named \"C\"");
2440 let m = c
2441 .spaces
2442 .iter()
2443 .find(|s| s.name.as_deref() == Some("m"))
2444 .expect("expected a child FuncSpace named \"m\"");
2445 // No Class space (grammar limitation), but `run` lands in a
2446 // nested Function space (the closure), not in `m` itself.
2447 let class_children = m
2448 .spaces
2449 .iter()
2450 .filter(|s| s.kind == crate::SpaceKind::Class)
2451 .count();
2452 assert_eq!(
2453 class_children, 0,
2454 "Groovy grammar models the body as a closure, not a class"
2455 );
2456 assert_eq!(m.metrics.nom.functions(), 1, "`m` itself, not `run`");
2457 let nested_funcs: u64 = m.spaces.iter().map(|s| s.metrics.nom.functions()).sum();
2458 assert_eq!(
2459 nested_funcs, 1,
2460 "`run` is attributed to the nested closure space"
2461 );
2462 },
2463 );
2464 }
2465
2466 #[test]
2467 fn kotlin_interface_simple() {
2468 // Interface methods all contribute to the interface bucket.
2469 check_metrics::<KotlinParser>(
2470 "interface I {
2471 fun work(): Int // +1
2472 fun describe(): String // +1
2473 }",
2474 "foo.kt",
2475 |metric| {
2476 assert_eq!(metric.wmc.class_wmc_sum(), 0);
2477 assert_eq!(metric.wmc.interface_wmc_sum(), 2);
2478 insta::assert_json_snapshot!(metric.wmc);
2479 },
2480 );
2481 }
2482
2483 #[test]
2484 fn kotlin_interface_with_default_method() {
2485 // Default method with control flow counts its full cyclomatic.
2486 check_metrics::<KotlinParser>(
2487 "interface I {
2488 fun abs(n: Int): Int { // +1
2489 return if (n < 0) -n else n // +1 if
2490 }
2491 fun pure(): Int // +1
2492 }",
2493 "foo.kt",
2494 |metric| {
2495 assert_eq!(metric.wmc.class_wmc_sum(), 0);
2496 assert_eq!(metric.wmc.interface_wmc_sum(), 3);
2497 insta::assert_json_snapshot!(metric.wmc);
2498 },
2499 );
2500 }
2501
2502 #[test]
2503 fn kotlin_override_function() {
2504 // `override fun` is structurally just a `function_declaration` with
2505 // an `override` modifier — counts like any other method.
2506 check_metrics::<KotlinParser>(
2507 "open class Base {
2508 open fun greet(): String = \"hi\" // +1
2509 }
2510 class Sub : Base() {
2511 override fun greet(): String = \"yo\" // +1
2512 }",
2513 "foo.kt",
2514 |metric| {
2515 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2516 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2517 insta::assert_json_snapshot!(metric.wmc);
2518 },
2519 );
2520 }
2521
2522 #[test]
2523 fn kotlin_secondary_constructor() {
2524 // Secondary constructors are explicit `secondary_constructor`
2525 // nodes; they count as methods.
2526 check_metrics::<KotlinParser>(
2527 "class C {
2528 private var a: Int = 0
2529 constructor(n: Int) { // +1
2530 a = n
2531 }
2532 constructor(n: Int, m: Int) { // +1
2533 a = n + m
2534 }
2535 fun get(): Int = a // +1
2536 }",
2537 "foo.kt",
2538 |metric| {
2539 assert_eq!(metric.wmc.class_wmc_sum(), 3);
2540 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2541 insta::assert_json_snapshot!(metric.wmc);
2542 },
2543 );
2544 }
2545
2546 #[test]
2547 fn kotlin_init_block() {
2548 // An `init` block opens a function space since #1184, so its
2549 // complexity rolls into the class's WMC — the metric is the sum
2550 // of a class's method-body complexities, and an initializer body
2551 // is real class complexity a reader has to account for. Before
2552 // #1184 the block opened no space at all: its control flow was
2553 // charged to the enclosing class space without WMC ever seeing
2554 // it, and `bca check` could not flag one however complex it got.
2555 //
2556 // WMC keys on `SpaceKind::Function` rather than on `is_func`,
2557 // which is why this moves even though the block is deliberately
2558 // *not* in `is_func` (it is not a callable anyone names).
2559 check_metrics::<KotlinParser>(
2560 "class C(val n: Int) {
2561 init { // +1 since #1184
2562 require(n >= 0) { \"n must be non-negative\" }
2563 }
2564 fun get(): Int = n // +1
2565 }",
2566 "foo.kt",
2567 |metric| {
2568 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2569 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2570 insta::assert_json_snapshot!(metric.wmc);
2571 },
2572 );
2573 }
2574
2575 #[test]
2576 fn kotlin_top_level_function_excluded() {
2577 // Top-level `fun` and `val` belong to the `Unit` space, not a class
2578 // space — they must not contribute to any class metric.
2579 check_metrics::<KotlinParser>(
2580 "fun freeFunction(): Int = 42
2581 val freeVal: Int = 0
2582 class C { fun m(): Int = 1 } // +1
2583 ",
2584 "foo.kt",
2585 |metric| {
2586 assert_eq!(metric.wmc.class_wmc_sum(), 1);
2587 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2588 insta::assert_json_snapshot!(metric.wmc);
2589 },
2590 );
2591 }
2592
2593 #[test]
2594 fn kotlin_extension_function_excluded() {
2595 // Extension functions look syntactically like methods but the
2596 // grammar parses them as top-level `function_declaration` with a
2597 // receiver-type prefix; they belong to the `Unit` space, not a
2598 // class. Class still gets +1 for its declared method.
2599 check_metrics::<KotlinParser>(
2600 "fun List<Int>.sum2(): Int = this.size // top-level
2601 class C { fun m(): Int = 1 } // +1
2602 ",
2603 "foo.kt",
2604 |metric| {
2605 assert_eq!(metric.wmc.class_wmc_sum(), 1);
2606 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2607 insta::assert_json_snapshot!(metric.wmc);
2608 },
2609 );
2610 }
2611
2612 #[test]
2613 fn kotlin_generic_class() {
2614 // Generic class with two methods.
2615 check_metrics::<KotlinParser>(
2616 "class Box<T>(val value: T) {
2617 fun get(): T = value // +1
2618 fun mapTo(f: (T) -> T): T = f(value) // +1
2619 }",
2620 "foo.kt",
2621 |metric| {
2622 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2623 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2624 insta::assert_json_snapshot!(metric.wmc);
2625 },
2626 );
2627 }
2628
2629 #[test]
2630 fn kotlin_class_in_interface() {
2631 // Nested class inside an interface: the inner class is a class
2632 // space (its method counts toward classes_wmc), and the interface
2633 // is the outer.
2634 check_metrics::<KotlinParser>(
2635 "interface Outer {
2636 fun work(): Int // +1 (interface)
2637 class Helper {
2638 fun help(): Int = 0 // +1 (class)
2639 }
2640 }",
2641 "foo.kt",
2642 |metric| {
2643 assert_eq!(metric.wmc.class_wmc_sum(), 1);
2644 assert_eq!(metric.wmc.interface_wmc_sum(), 1);
2645 insta::assert_json_snapshot!(metric.wmc);
2646 },
2647 );
2648 }
2649
2650 #[test]
2651 fn kotlin_interface_in_class() {
2652 // Inverse of the prior test.
2653 check_metrics::<KotlinParser>(
2654 "class Outer {
2655 fun work(): Int = 1 // +1 (class)
2656 interface Sub {
2657 fun help(): Int // +1 (interface)
2658 }
2659 }",
2660 "foo.kt",
2661 |metric| {
2662 assert_eq!(metric.wmc.class_wmc_sum(), 1);
2663 assert_eq!(metric.wmc.interface_wmc_sum(), 1);
2664 insta::assert_json_snapshot!(metric.wmc);
2665 },
2666 );
2667 }
2668
2669 // --- TypeScript / TSX WMC tests --------------------------------------
2670 //
2671 // Each class method contributes its cyclomatic complexity to the
2672 // enclosing class's WMC. Arrow function class members behave as
2673 // methods. Interface method signatures have no bodies and add zero
2674 // (matching Java's abstract-method rule).
2675
2676 #[test]
2677 fn typescript_class_wmc_single_method() {
2678 check_metrics::<TypescriptParser>(
2679 "class C {
2680 m(): number { return 1; } // cyclomatic 1
2681 }",
2682 "foo.ts",
2683 |metric| {
2684 assert_eq!(metric.wmc.class_wmc_sum(), 1);
2685 insta::assert_json_snapshot!(metric.wmc);
2686 },
2687 );
2688 }
2689
2690 #[test]
2691 fn typescript_class_wmc_two_methods() {
2692 check_metrics::<TypescriptParser>(
2693 "class C {
2694 a(): number { return 1; } // +1
2695 b(x: number): number { // +2 (if branch)
2696 if (x > 0) return x;
2697 return 0;
2698 }
2699 }",
2700 "foo.ts",
2701 |metric| {
2702 assert_eq!(metric.wmc.class_wmc_sum(), 3);
2703 insta::assert_json_snapshot!(metric.wmc);
2704 },
2705 );
2706 }
2707
2708 #[test]
2709 fn typescript_class_wmc_with_branches() {
2710 check_metrics::<TypescriptParser>(
2711 "class C {
2712 m(x: number): number {
2713 if (x > 0) { // +1
2714 return 1;
2715 } else if (x < 0) { // +1
2716 return -1;
2717 }
2718 return 0;
2719 } // base 1
2720 }",
2721 "foo.ts",
2722 |metric| {
2723 assert_eq!(metric.wmc.class_wmc_sum(), 3);
2724 insta::assert_json_snapshot!(metric.wmc);
2725 },
2726 );
2727 }
2728
2729 #[test]
2730 fn typescript_class_wmc_arrow_field() {
2731 // Arrow-function class fields contribute their cyclomatic to
2732 // the enclosing class — they are function spaces.
2733 check_metrics::<TypescriptParser>(
2734 "class C {
2735 arrow = (x: number) => {
2736 if (x > 0) return x; // +1
2737 return 0;
2738 }; // base 1
2739 }",
2740 "foo.ts",
2741 |metric| {
2742 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2743 insta::assert_json_snapshot!(metric.wmc);
2744 },
2745 );
2746 }
2747
2748 #[test]
2749 fn typescript_class_wmc_with_loops() {
2750 check_metrics::<TypescriptParser>(
2751 "class C {
2752 m(xs: number[]): number {
2753 let total = 0;
2754 for (const x of xs) { // +1
2755 total += x;
2756 }
2757 return total;
2758 } // base 1
2759 }",
2760 "foo.ts",
2761 |metric| {
2762 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2763 insta::assert_json_snapshot!(metric.wmc);
2764 },
2765 );
2766 }
2767
2768 #[test]
2769 fn typescript_abstract_class_wmc() {
2770 // Abstract method signatures have no body — contribute 0.
2771 check_metrics::<TypescriptParser>(
2772 "abstract class C {
2773 abstract a(): void; // signature only, 0
2774 m(): number { return 1; } // +1
2775 }",
2776 "foo.ts",
2777 |metric| {
2778 assert_eq!(metric.wmc.class_wmc_sum(), 1);
2779 insta::assert_json_snapshot!(metric.wmc);
2780 },
2781 );
2782 }
2783
2784 #[test]
2785 fn typescript_interface_wmc_zero() {
2786 // Interface method signatures have no bodies → 0 WMC.
2787 check_metrics::<TypescriptParser>(
2788 "interface I {
2789 a(): void;
2790 b(): number;
2791 }",
2792 "foo.ts",
2793 |metric| {
2794 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
2795 assert_eq!(metric.wmc.class_wmc_sum(), 0);
2796 insta::assert_json_snapshot!(metric.wmc);
2797 },
2798 );
2799 }
2800
2801 #[test]
2802 fn typescript_constructor_wmc() {
2803 // Constructor counts as a method; its cyclomatic adds to the
2804 // class WMC.
2805 check_metrics::<TypescriptParser>(
2806 "class C {
2807 x: number;
2808 constructor(n: number) {
2809 if (n > 0) { // +1
2810 this.x = n;
2811 } else {
2812 this.x = 0;
2813 }
2814 } // base 1
2815 }",
2816 "foo.ts",
2817 |metric| {
2818 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2819 insta::assert_json_snapshot!(metric.wmc);
2820 },
2821 );
2822 }
2823
2824 #[test]
2825 fn typescript_getter_setter_wmc() {
2826 // Getter and setter each contribute 1 (base).
2827 check_metrics::<TypescriptParser>(
2828 "class C {
2829 _x: number = 0;
2830 get x(): number { return this._x; }
2831 set x(v: number) { this._x = v; }
2832 }",
2833 "foo.ts",
2834 |metric| {
2835 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2836 insta::assert_json_snapshot!(metric.wmc);
2837 },
2838 );
2839 }
2840
2841 #[test]
2842 fn typescript_multiple_classes_wmc_independent() {
2843 check_metrics::<TypescriptParser>(
2844 "class A { m(): number { return 1; } }
2845 class B {
2846 m(x: number): number {
2847 if (x > 0) return x; // +1
2848 return 0;
2849 } // base 1
2850 }",
2851 "foo.ts",
2852 |metric| {
2853 // A: 1 + B: 2 = 3 total.
2854 assert_eq!(metric.wmc.class_wmc_sum(), 3);
2855 insta::assert_json_snapshot!(metric.wmc);
2856 },
2857 );
2858 }
2859
2860 #[test]
2861 fn typescript_class_wmc_with_ternary_and_logical() {
2862 check_metrics::<TypescriptParser>(
2863 "class C {
2864 m(x: number, y: number): number {
2865 return x > 0 && y > 0 // +1 (ternary) +1 (&&)
2866 ? x + y
2867 : 0;
2868 } // base 1
2869 }",
2870 "foo.ts",
2871 |metric| {
2872 assert_eq!(metric.wmc.class_wmc_sum(), 3);
2873 insta::assert_json_snapshot!(metric.wmc);
2874 },
2875 );
2876 }
2877
2878 #[test]
2879 fn typescript_generic_class_wmc() {
2880 check_metrics::<TypescriptParser>(
2881 "class Box<T> {
2882 value: T;
2883 set(v: T): void { this.value = v; }
2884 get(): T { return this.value; }
2885 }",
2886 "foo.ts",
2887 |metric| {
2888 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2889 insta::assert_json_snapshot!(metric.wmc);
2890 },
2891 );
2892 }
2893
2894 // TSX parity
2895
2896 #[test]
2897 fn tsx_class_wmc_single_method() {
2898 check_metrics::<TsxParser>(
2899 "class C { m(): number { return 1; } }",
2900 "foo.tsx",
2901 |metric| {
2902 assert_eq!(metric.wmc.class_wmc_sum(), 1);
2903 insta::assert_json_snapshot!(metric.wmc);
2904 },
2905 );
2906 }
2907
2908 #[test]
2909 fn tsx_class_wmc_two_methods() {
2910 check_metrics::<TsxParser>(
2911 "class C {
2912 a(): number { return 1; }
2913 b(x: number): number {
2914 if (x > 0) return x;
2915 return 0;
2916 }
2917 }",
2918 "foo.tsx",
2919 |metric| {
2920 assert_eq!(metric.wmc.class_wmc_sum(), 3);
2921 insta::assert_json_snapshot!(metric.wmc);
2922 },
2923 );
2924 }
2925
2926 #[test]
2927 fn tsx_class_wmc_with_branches() {
2928 check_metrics::<TsxParser>(
2929 "class C {
2930 m(x: number): number {
2931 if (x > 0) return 1;
2932 else if (x < 0) return -1;
2933 return 0;
2934 }
2935 }",
2936 "foo.tsx",
2937 |metric| {
2938 assert_eq!(metric.wmc.class_wmc_sum(), 3);
2939 insta::assert_json_snapshot!(metric.wmc);
2940 },
2941 );
2942 }
2943
2944 #[test]
2945 fn tsx_class_wmc_arrow_field() {
2946 check_metrics::<TsxParser>(
2947 "class C {
2948 arrow = (x: number) => {
2949 if (x > 0) return x;
2950 return 0;
2951 };
2952 }",
2953 "foo.tsx",
2954 |metric| {
2955 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2956 insta::assert_json_snapshot!(metric.wmc);
2957 },
2958 );
2959 }
2960
2961 #[test]
2962 fn tsx_class_wmc_with_loops() {
2963 check_metrics::<TsxParser>(
2964 "class C {
2965 m(xs: number[]): number {
2966 let total = 0;
2967 for (const x of xs) { total += x; }
2968 return total;
2969 }
2970 }",
2971 "foo.tsx",
2972 |metric| {
2973 assert_eq!(metric.wmc.class_wmc_sum(), 2);
2974 insta::assert_json_snapshot!(metric.wmc);
2975 },
2976 );
2977 }
2978
2979 #[test]
2980 fn tsx_abstract_class_wmc() {
2981 check_metrics::<TsxParser>(
2982 "abstract class C {
2983 abstract a(): void;
2984 m(): number { return 1; }
2985 }",
2986 "foo.tsx",
2987 |metric| {
2988 assert_eq!(metric.wmc.class_wmc_sum(), 1);
2989 insta::assert_json_snapshot!(metric.wmc);
2990 },
2991 );
2992 }
2993
2994 #[test]
2995 fn tsx_interface_wmc_zero() {
2996 check_metrics::<TsxParser>(
2997 "interface I { a(): void; b(): number; }",
2998 "foo.tsx",
2999 |metric| {
3000 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3001 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3002 insta::assert_json_snapshot!(metric.wmc);
3003 },
3004 );
3005 }
3006
3007 #[test]
3008 fn tsx_constructor_wmc() {
3009 check_metrics::<TsxParser>(
3010 "class C {
3011 x: number;
3012 constructor(n: number) {
3013 if (n > 0) this.x = n;
3014 else this.x = 0;
3015 }
3016 }",
3017 "foo.tsx",
3018 |metric| {
3019 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3020 insta::assert_json_snapshot!(metric.wmc);
3021 },
3022 );
3023 }
3024
3025 #[test]
3026 fn tsx_getter_setter_wmc() {
3027 check_metrics::<TsxParser>(
3028 "class C {
3029 _x: number = 0;
3030 get x(): number { return this._x; }
3031 set x(v: number) { this._x = v; }
3032 }",
3033 "foo.tsx",
3034 |metric| {
3035 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3036 insta::assert_json_snapshot!(metric.wmc);
3037 },
3038 );
3039 }
3040
3041 #[test]
3042 fn tsx_multiple_classes_wmc_independent() {
3043 check_metrics::<TsxParser>(
3044 "class A { m(): number { return 1; } }
3045 class B {
3046 m(x: number): number {
3047 if (x > 0) return x;
3048 return 0;
3049 }
3050 }",
3051 "foo.tsx",
3052 |metric| {
3053 assert_eq!(metric.wmc.class_wmc_sum(), 3);
3054 insta::assert_json_snapshot!(metric.wmc);
3055 },
3056 );
3057 }
3058
3059 #[test]
3060 fn tsx_class_wmc_with_ternary_and_logical() {
3061 check_metrics::<TsxParser>(
3062 "class C {
3063 m(x: number, y: number): number {
3064 return x > 0 && y > 0 ? x + y : 0;
3065 }
3066 }",
3067 "foo.tsx",
3068 |metric| {
3069 assert_eq!(metric.wmc.class_wmc_sum(), 3);
3070 insta::assert_json_snapshot!(metric.wmc);
3071 },
3072 );
3073 }
3074
3075 #[test]
3076 fn tsx_generic_class_wmc() {
3077 check_metrics::<TsxParser>(
3078 "class Box<T> {
3079 value: T;
3080 set(v: T): void { this.value = v; }
3081 get(): T { return this.value; }
3082 }",
3083 "foo.tsx",
3084 |metric| {
3085 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3086 insta::assert_json_snapshot!(metric.wmc);
3087 },
3088 );
3089 }
3090
3091 // --- Ruby WMC tests ---------------------------------------------------
3092 //
3093 // Reference: Ruby `Class` and `SingletonClass` map to `SpaceKind::Class`
3094 // via `Getter::get_space_kind`; `Module` is a `SpaceKind::Namespace`
3095 // and does not contribute to WMC. Method cyclomatic complexities
3096 // accumulate into the enclosing class via `class_interface_compute`.
3097
3098 #[test]
3099 fn ruby_no_classes() {
3100 // File with only a top-level method — no class space, WMC = 0.
3101 check_metrics::<RubyParser>("def foo\n 1\nend\n", "foo.rb", |metric| {
3102 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3103 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3104 insta::assert_json_snapshot!(metric.wmc);
3105 });
3106 }
3107
3108 #[test]
3109 fn ruby_empty_class() {
3110 // Class with no methods → wmc = 0.
3111 check_metrics::<RubyParser>("class Foo\nend\n", "foo.rb", |metric| {
3112 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3113 insta::assert_json_snapshot!(metric.wmc);
3114 });
3115 }
3116
3117 #[test]
3118 fn ruby_one_class_simple() {
3119 // Two methods, each with cyclomatic = 1 (the method base) → wmc = 2.
3120 check_metrics::<RubyParser>(
3121 "class A\n def a\n 1\n end\n def b\n 2\n end\nend\n",
3122 "foo.rb",
3123 |metric| {
3124 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3125 insta::assert_json_snapshot!(metric.wmc);
3126 },
3127 );
3128 }
3129
3130 #[test]
3131 fn ruby_one_class_with_branch() {
3132 // One method with cyclomatic 1 (base) + 1 (if) = 2.
3133 check_metrics::<RubyParser>(
3134 "class A\n def f(x)\n if x > 0\n 1\n else\n 0\n end\n end\nend\n",
3135 "foo.rb",
3136 |metric| {
3137 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3138 insta::assert_json_snapshot!(metric.wmc);
3139 },
3140 );
3141 }
3142
3143 #[test]
3144 fn ruby_one_class_with_loop() {
3145 // One method with cyclomatic 1 (base) + 1 (while) = 2.
3146 check_metrics::<RubyParser>(
3147 "class A\n def f(n)\n while n > 0\n n -= 1\n end\n end\nend\n",
3148 "foo.rb",
3149 |metric| {
3150 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3151 insta::assert_json_snapshot!(metric.wmc);
3152 },
3153 );
3154 }
3155
3156 #[test]
3157 fn ruby_singleton_method_included() {
3158 // Mix of regular and singleton (`def self.x`) methods, both
3159 // contribute to the class WMC.
3160 check_metrics::<RubyParser>(
3161 "class A\n def f\n 1\n end\n def self.g\n 2\n end\nend\n",
3162 "foo.rb",
3163 |metric| {
3164 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3165 insta::assert_json_snapshot!(metric.wmc);
3166 },
3167 );
3168 }
3169
3170 #[test]
3171 fn ruby_singleton_class_methods_included() {
3172 // Methods inside `class << self` belong to the enclosing class
3173 // (singleton class is a `SpaceKind::Class` of its own).
3174 check_metrics::<RubyParser>(
3175 "class A\n class << self\n def s\n 1\n end\n end\nend\n",
3176 "foo.rb",
3177 |metric| {
3178 // Two class spaces: outer A (wmc 0, no methods) and the
3179 // singleton class with its single method (wmc 1).
3180 assert_eq!(metric.wmc.class_wmc_sum(), 1);
3181 insta::assert_json_snapshot!(metric.wmc);
3182 },
3183 );
3184 }
3185
3186 #[test]
3187 fn ruby_multiple_classes() {
3188 // Each class contributes its method-cyclomatic sum to the rollup.
3189 check_metrics::<RubyParser>(
3190 "class A\n def f(x)\n if x > 0\n 1\n end\n end\nend\nclass B\n def g\n 1\n end\nend\n",
3191 "foo.rb",
3192 |metric| {
3193 // A: 2 (base + if). B: 1 (base only). Sum = 3.
3194 assert_eq!(metric.wmc.class_wmc_sum(), 3);
3195 insta::assert_json_snapshot!(metric.wmc);
3196 },
3197 );
3198 }
3199
3200 #[test]
3201 fn ruby_module_only() {
3202 // Module is a `Namespace` space — does NOT contribute to WMC even
3203 // though the body has methods.
3204 check_metrics::<RubyParser>(
3205 "module M\n def f\n 1\n end\nend\n",
3206 "foo.rb",
3207 |metric| {
3208 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3209 insta::assert_json_snapshot!(metric.wmc);
3210 },
3211 );
3212 }
3213
3214 #[test]
3215 fn ruby_class_with_inheritance() {
3216 // `class A < B` inherits — irrelevant to WMC, which depends only on
3217 // the method bodies inside this class.
3218 check_metrics::<RubyParser>(
3219 "class A < B\n def f\n 1\n end\n def g\n 2\n end\nend\n",
3220 "foo.rb",
3221 |metric| {
3222 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3223 insta::assert_json_snapshot!(metric.wmc);
3224 },
3225 );
3226 }
3227
3228 #[test]
3229 fn ruby_class_with_visibility_keywords() {
3230 // Visibility keywords do NOT affect WMC — every method body
3231 // contributes regardless of `private` / `protected`.
3232 check_metrics::<RubyParser>(
3233 "class A\n def a\n 1\n end\n private\n def b\n 1\n end\n protected\n def c\n 1\n end\nend\n",
3234 "foo.rb",
3235 |metric| {
3236 assert_eq!(metric.wmc.class_wmc_sum(), 3);
3237 insta::assert_json_snapshot!(metric.wmc);
3238 },
3239 );
3240 }
3241
3242 #[test]
3243 fn ruby_class_complex() {
3244 // Class with two methods whose cyclomatic sums combine.
3245 // `add`: base(1) + `if`(1) + `&&`(1) = 3.
3246 // `loop`: base(1) + `while`(1) + `if`(1) = 3.
3247 // Class WMC = 6.
3248 check_metrics::<RubyParser>(
3249 "class Calc\n def add(a, b)\n if a > 0 && b > 0\n a + b\n end\n end\n def loop(n)\n s = 0\n while n > 0\n if n.even?\n s += n\n end\n n -= 1\n end\n s\n end\nend\n",
3250 "foo.rb",
3251 |metric| {
3252 assert_eq!(metric.wmc.class_wmc_sum(), 6);
3253 insta::assert_json_snapshot!(metric.wmc);
3254 },
3255 );
3256 }
3257
3258 // ---------------------------------------------------------------
3259 // Default-impl placeholder smoke tests (audited in #188).
3260 //
3261 // Each test feeds a class / struct with multiple branchy methods
3262 // to a language whose `Wmc` is currently the default no-op. The
3263 // assertion pins the current 0 value; when the real impl lands
3264 // the assertion will fire and force a test update.
3265 // ---------------------------------------------------------------
3266
3267 // --- Python WMC ---------------------------------------------------
3268
3269 #[test]
3270 fn python_empty_class_zero_wmc() {
3271 check_metrics::<PythonParser>("class C:\n pass\n", "foo.py", |metric| {
3272 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3273 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3274 insta::assert_json_snapshot!(metric.wmc);
3275 });
3276 }
3277
3278 #[test]
3279 fn python_single_method_wmc_one() {
3280 // Single straight-line method → cyclomatic 1 → WMC 1.
3281 check_metrics::<PythonParser>(
3282 "class C:\n def m(self):\n return 1\n",
3283 "foo.py",
3284 |metric| {
3285 assert_eq!(metric.wmc.class_wmc_sum(), 1);
3286 insta::assert_json_snapshot!(metric.wmc);
3287 },
3288 );
3289 }
3290
3291 #[test]
3292 fn python_method_with_if_adds_to_wmc() {
3293 // Cyclomatic: 1 (base) + 1 (if) = 2. WMC = 2.
3294 check_metrics::<PythonParser>(
3295 "class C:\n def m(self, x):\n if x > 0:\n return 1\n return 0\n",
3296 "foo.py",
3297 |metric| {
3298 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3299 insta::assert_json_snapshot!(metric.wmc);
3300 },
3301 );
3302 }
3303
3304 #[test]
3305 fn python_multiple_methods_wmc_sums() {
3306 // method1 cyclomatic 1, method2 cyclomatic 2 (if), method3
3307 // cyclomatic 3 (if + for). WMC sum = 1 + 2 + 3 = 6.
3308 check_metrics::<PythonParser>(
3309 "class C:\n\
3310 \x20 def m1(self):\n\
3311 \x20 return 1\n\
3312 \x20 def m2(self, x):\n\
3313 \x20 if x:\n\
3314 \x20 return 1\n\
3315 \x20 return 0\n\
3316 \x20 def m3(self, xs):\n\
3317 \x20 for x in xs:\n\
3318 \x20 if x:\n\
3319 \x20 return x\n\
3320 \x20 return None\n",
3321 "foo.py",
3322 |metric| {
3323 assert_eq!(metric.wmc.class_wmc_sum(), 6);
3324 insta::assert_json_snapshot!(metric.wmc);
3325 },
3326 );
3327 }
3328
3329 #[test]
3330 fn python_top_level_function_does_not_contribute_to_class_wmc() {
3331 // Top-level function lives in the module/unit space, not in a
3332 // class space — class_wmc stays at 0.
3333 check_metrics::<PythonParser>(
3334 "def f(x):\n if x:\n return 1\n return 0\n",
3335 "foo.py",
3336 |metric| {
3337 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3338 insta::assert_json_snapshot!(metric.wmc);
3339 },
3340 );
3341 }
3342
3343 #[test]
3344 fn python_multiple_classes_wmc_independent() {
3345 // Each class accumulates its own methods' cyclomatic. The
3346 // file-level class_wmc_sum is the sum of every class's WMC.
3347 // A.m1 (1) + B.m2 (2 — has an if) = 3.
3348 check_metrics::<PythonParser>(
3349 "class A:\n\
3350 \x20 def m1(self):\n\
3351 \x20 return 1\n\
3352 class B:\n\
3353 \x20 def m2(self, x):\n\
3354 \x20 if x:\n\
3355 \x20 return 1\n\
3356 \x20 return 0\n",
3357 "foo.py",
3358 |metric| {
3359 assert_eq!(metric.wmc.class_wmc_sum(), 3);
3360 insta::assert_json_snapshot!(metric.wmc);
3361 },
3362 );
3363 }
3364
3365 #[test]
3366 fn rust_empty_unit_zero_wmc() {
3367 check_metrics::<RustParser>("", "empty.rs", |metric| {
3368 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3369 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3370 insta::assert_json_snapshot!(metric.wmc);
3371 });
3372 }
3373
3374 #[test]
3375 fn rust_single_impl_method_wmc_one() {
3376 // Single straight-line method → cyclomatic 1 → WMC 1.
3377 check_metrics::<RustParser>(
3378 "struct Foo;\nimpl Foo { fn m(&self) -> i32 { 1 } }\n",
3379 "foo.rs",
3380 |metric| {
3381 assert_eq!(metric.wmc.class_wmc_sum(), 1);
3382 insta::assert_json_snapshot!(metric.wmc);
3383 },
3384 );
3385 }
3386
3387 #[test]
3388 fn rust_method_with_if_adds_to_wmc() {
3389 // Cyclomatic: 1 (base) + 1 (if) = 2. WMC = 2.
3390 check_metrics::<RustParser>(
3391 "struct Foo;\n\
3392 impl Foo {\n\
3393 \x20 fn m(&self, x: i32) -> i32 {\n\
3394 \x20 if x > 0 { 1 } else { 0 }\n\
3395 \x20 }\n\
3396 }\n",
3397 "foo.rs",
3398 |metric| {
3399 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3400 insta::assert_json_snapshot!(metric.wmc);
3401 },
3402 );
3403 }
3404
3405 #[test]
3406 fn rust_multiple_methods_wmc_sums() {
3407 // m1 cyclomatic 1, m2 cyclomatic 2 (if), m3 cyclomatic 3 (if
3408 // inside for). WMC = 1 + 2 + 3 = 6.
3409 check_metrics::<RustParser>(
3410 "struct Foo;\n\
3411 impl Foo {\n\
3412 \x20 fn m1(&self) -> i32 { 1 }\n\
3413 \x20 fn m2(&self, x: i32) -> i32 { if x > 0 { 1 } else { 0 } }\n\
3414 \x20 fn m3(&self, xs: &[i32]) -> i32 {\n\
3415 \x20 for x in xs { if *x > 0 { return *x; } }\n\
3416 \x20 0\n\
3417 \x20 }\n\
3418 }\n",
3419 "foo.rs",
3420 |metric| {
3421 assert_eq!(metric.wmc.class_wmc_sum(), 6);
3422 insta::assert_json_snapshot!(metric.wmc);
3423 },
3424 );
3425 }
3426
3427 #[test]
3428 fn rust_multiple_impls_wmc_aggregate() {
3429 // Two `impl` blocks for Foo, each contributing 1 method with
3430 // cyclomatic 1. Unit-level class_wmc_sum = 2.
3431 check_metrics::<RustParser>(
3432 "struct Foo;\n\
3433 impl Foo { fn m1(&self) {} }\n\
3434 impl Foo { fn m2(&self) {} }\n",
3435 "foo.rs",
3436 |metric| {
3437 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3438 insta::assert_json_snapshot!(metric.wmc);
3439 },
3440 );
3441 }
3442
3443 #[test]
3444 fn rust_trait_default_method_contributes_to_interface_wmc() {
3445 // A trait method with a default body — `area` is a function
3446 // space inside the trait. Cyclomatic = 1 → interface_wmc = 1.
3447 // The signature-only `draw` has no body and contributes
3448 // nothing.
3449 check_metrics::<RustParser>(
3450 "trait T { fn draw(&self); fn area(&self) -> f64 { 0.0 } }",
3451 "foo.rs",
3452 |metric| {
3453 assert_eq!(metric.wmc.interface_wmc_sum(), 1);
3454 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3455 insta::assert_json_snapshot!(metric.wmc);
3456 },
3457 );
3458 }
3459
3460 #[test]
3461 fn rust_top_level_function_does_not_contribute_to_class_wmc() {
3462 // Free `fn f()` opens a Function space but no class/trait
3463 // surrounds it. The Unit space is not a class space, so
3464 // class_wmc_sum stays at 0.
3465 check_metrics::<RustParser>(
3466 "fn f(x: i32) -> i32 { if x > 0 { 1 } else { 0 } }",
3467 "foo.rs",
3468 |metric| {
3469 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3470 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3471 insta::assert_json_snapshot!(metric.wmc);
3472 },
3473 );
3474 }
3475
3476 // ----- Go -----
3477
3478 #[test]
3479 fn go_wmc_is_zero_documented_limitation() {
3480 // Go's flat space model does not expose per-receiver class
3481 // spaces, and the Wmc trait signature receives only a
3482 // `SpaceKind` (Function for both `MethodDeclaration` and
3483 // free `FunctionDeclaration`). Implementing receiver-grouped
3484 // WMC would require space-model changes that are out of
3485 // scope for this fix; per the issue's option (a), the metric
3486 // stays at zero with a documented reason. This test pins
3487 // that behaviour so any future Wmc work for Go has to update
3488 // it deliberately.
3489 check_metrics::<GoParser>(
3490 "package main\n\
3491 type Foo struct{}\n\
3492 func (f Foo) M(x int) int { if x > 0 { return 1 } else { return 0 } }\n\
3493 func (f Foo) N() {}\n",
3494 "foo.go",
3495 |metric| {
3496 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3497 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3498 insta::assert_json_snapshot!(metric.wmc);
3499 },
3500 );
3501 }
3502
3503 // ----- Elixir -----
3504
3505 // Issue #275: Elixir's `def` / `defp` declarations parse as
3506 // `Call` nodes whose `target` Identifier text spells the
3507 // keyword. The source-aware Checker / Getter dispatch promotes
3508 // them to Function spaces inside the surrounding `defmodule`
3509 // Class. WMC then aggregates cyclomatic per method into the
3510 // class via the shared `class_interface_compute` aggregator.
3511 #[test]
3512 fn elixir_wmc_aggregates_def_methods() {
3513 check_metrics::<ElixirParser>(
3514 "defmodule Foo do\n def m(x) do\n if x > 0 do\n 1\n else\n 0\n end\n end\n def n, do: :ok\nend\n",
3515 "foo.ex",
3516 |metric| {
3517 // m: entry(1) + if(1) = 2; n: entry(1) = 1 → wmc = 3.
3518 assert_eq!(metric.wmc.class_wmc_sum(), 3);
3519 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3520 insta::assert_json_snapshot!(
3521 metric.wmc,
3522 @r#"
3523 {
3524 "class_wmc_sum": 3,
3525 "interface_wmc_sum": 0,
3526 "total": 3
3527 }
3528 "#
3529 );
3530 },
3531 );
3532 }
3533
3534 #[test]
3535 fn elixir_wmc_def_plus_defp_counts_both() {
3536 check_metrics::<ElixirParser>(
3537 "defmodule Foo do\n def pub_one, do: 1\n defp priv_one, do: 1\nend\n",
3538 "foo.ex",
3539 |metric| {
3540 // Both `def` and `defp` are methods of the class — npm
3541 // distinguishes public vs private, wmc does not.
3542 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3543 },
3544 );
3545 }
3546
3547 #[test]
3548 fn elixir_wmc_defmacro_counts() {
3549 check_metrics::<ElixirParser>(
3550 "defmodule Foo do\n defmacro stuff(x) do\n if x > 0, do: :pos, else: :neg\n end\nend\n",
3551 "foo.ex",
3552 |metric| {
3553 // defmacro is a method; body has entry(1) + if(1) = 2.
3554 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3555 },
3556 );
3557 }
3558
3559 #[test]
3560 fn elixir_wmc_multiple_clauses_each_a_method() {
3561 // Each `def f(...)` head is a Call with its own Function
3562 // space, so multiple clauses for the same name each count.
3563 check_metrics::<ElixirParser>(
3564 "defmodule Foo do\n def f(0), do: :zero\n def f(_), do: :other\nend\n",
3565 "foo.ex",
3566 |metric| {
3567 // Two clauses, entry(1) each → wmc = 2.
3568 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3569 },
3570 );
3571 }
3572
3573 #[test]
3574 fn elixir_wmc_nested_defmodule_isolates() {
3575 check_metrics::<ElixirParser>(
3576 "defmodule Outer do\n def o, do: 1\n defmodule Inner do\n def i, do: 1\n end\nend\n",
3577 "foo.ex",
3578 |metric| {
3579 // Outer.o(1) + Inner.i(1) → file-level sum is 2.
3580 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3581 },
3582 );
3583 }
3584
3585 #[test]
3586 fn elixir_wmc_user_macro_not_classified_as_method() {
3587 // A user-defined `defmacro custom_def`, then invoking
3588 // `custom_def foo, do: ...` must NOT be classified as a
3589 // method — the literal-text comparison in
3590 // `elixir_call_keyword` only matches the four built-in
3591 // method-defining macros. The `def unquote(name)` inside the
3592 // `quote do … end` block is also rejected (it is a code
3593 // template emitted on macro expansion, not a real definition
3594 // of any method of `Foo`); `elixir_is_inside_quote_block`
3595 // filters it out, keeping `Wmc` aligned with `Npm` (#310).
3596 check_metrics::<ElixirParser>(
3597 "defmodule Foo do\n defmacro custom_def(name, body) do\n quote do\n def unquote(name), do: unquote(body)\n end\n end\n custom_def foo, do: 1\nend\n",
3598 "foo.ex",
3599 |metric| {
3600 // Only the `defmacro custom_def` itself is a method
3601 // of `Foo`. Body cyclomatic: entry(1) → wmc = 1.
3602 assert_eq!(metric.wmc.class_wmc_sum(), 1);
3603 },
3604 );
3605 }
3606
3607 #[test]
3608 fn elixir_wmc_quoted_defs_do_not_inflate_method_count() {
3609 // Regression test for #310: previously, every `def` lexically
3610 // present in the source was promoted to a Function space and
3611 // counted toward `Wmc`, even when nested inside `quote do …
3612 // end` (a metaprogramming template that does not declare
3613 // methods of the enclosing module). That made `Wmc` disagree
3614 // with `Npm`'s direct-children classification.
3615 //
3616 // Here `Foo` has exactly one real method (the `defmacro
3617 // multi`); the three quoted `def`s inside its body are not
3618 // methods of `Foo`. `Wmc` should now agree.
3619 check_metrics::<ElixirParser>(
3620 "defmodule Foo do\n defmacro multi do\n quote do\n def a, do: 1\n def b, do: 2\n defp c, do: 3\n end\n end\nend\n",
3621 "foo.ex",
3622 |metric| {
3623 // Only `defmacro multi` is a method of Foo: entry(1)
3624 // → wmc = 1.
3625 assert_eq!(metric.wmc.class_wmc_sum(), 1);
3626 },
3627 );
3628 }
3629
3630 // ----- Objective-C -----
3631
3632 #[test]
3633 fn objc_wmc() {
3634 // `@implementation` is a Class space; each `method_definition`
3635 // opens a Function space whose cyclomatic rolls up. `a` has one
3636 // `if` (cyclomatic 2), `b` is empty (cyclomatic 1) → class WMC = 3.
3637 // The `@interface` is an Interface space whose method *declaration*
3638 // has no body, so it contributes 0 — `interface_wmc_sum` stays 0
3639 // even though a declared method exists.
3640 check_metrics::<ObjcParser>(
3641 "@interface Foo : NSObject\n\
3642 - (int)a:(int)x;\n\
3643 @end\n\
3644 @implementation Foo\n\
3645 - (int)a:(int)x { if (x > 0) { return 1; } return 0; }\n\
3646 - (void)b { }\n\
3647 @end\n",
3648 "foo.m",
3649 |metric| {
3650 // Also the §6 over-suppression guard for #1356: this
3651 // fixture has no C function at all, so a predicate that
3652 // answered on the `implementation_definition` parent —
3653 // which wraps `method_definition` too — would read 0.
3654 assert_eq!(metric.wmc.class_wmc_sum(), 3);
3655 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3656 },
3657 );
3658 }
3659
3660 // ----- Objective-C: a C function is never a member (#1356) -----
3661 //
3662 // A plain C function written inside `@implementation` is a
3663 // file-static helper — no receiver, not in the method table, not
3664 // sendable. `npm` has always declined to count it; `wmc` weighted
3665 // it, so the two disagreed about the same container. This is the
3666 // C++ `friend` divergence of #1301 in a sibling language. No `.m`
3667 // file is snapshotted at all — DeepSpeech's fifteen sit under
3668 // `tensorflow/`, outside the corpus test's `native_client/`
3669 // selection — so these tests are its only coverage.
3670
3671 // The issue's own fixture, verbatim. Three outcomes are reachable,
3672 // so the expectation cannot hold for the wrong reason: 3 is the
3673 // pre-fix value (`helper`'s cyclomatic 2 plus `m`'s 1), 1 is
3674 // correct, and 0 would mean the roll-up dropped `m` as well.
3675 const OBJC_HELPER_IN_IMPLEMENTATION: &str = "@implementation Foo\n\
3676 static int helper(int x) { if (x) { return 1; } return 0; }\n\
3677 - (void)m { }\n\
3678 @end\n";
3679
3680 // `m` branches, so the expectation (2) differs from the pre-fix
3681 // value (4), from the method count (1), and from zero.
3682 const OBJC_HELPER_AND_BRANCHING_METHOD: &str = "@implementation Foo\n\
3683 static int helper(int x) { if (x) { return 1; } return 0; }\n\
3684 - (int)m:(int)x { if (x) { return 1; } return 0; }\n\
3685 @end\n";
3686
3687 // A category `@implementation Foo (Cat)` parses as the same
3688 // `class_implementation` node with a `category` field, so its
3689 // members nest identically.
3690 const OBJC_HELPER_IN_CATEGORY: &str = "@implementation Foo (Cat)\n\
3691 static int helper(int x) { if (x) { return 1; } return 0; }\n\
3692 - (int)m:(int)x { if (x) { return 1; } return 0; }\n\
3693 @end\n";
3694
3695 // `@interface` and `@protocol` admit a `function_definition` as a
3696 // *direct* child — no `implementation_definition` wrapper, unlike
3697 // `@implementation`. Their own members are bodyless
3698 // `method_declaration`s, so a correct `interface_wmc_sum` is 0; the
3699 // `@implementation` in the same fixture keeps a non-zero
3700 // `class_wmc_sum` so the pair cannot pass by everything being zero.
3701 const OBJC_HELPER_IN_INTERFACE: &str = "@interface Foo : NSObject\n\
3702 static int helper(int x) { if (x) { return 1; } return 0; }\n\
3703 - (int)m:(int)x;\n\
3704 @end\n\
3705 @implementation Foo\n\
3706 - (int)m:(int)x { if (x) { return 1; } return 0; }\n\
3707 @end\n";
3708
3709 const OBJC_HELPER_IN_PROTOCOL: &str = "@protocol Proto\n\
3710 static int helper(int x) { if (x) { return 1; } return 0; }\n\
3711 - (int)m:(int)x;\n\
3712 @end\n\
3713 @implementation Foo\n\
3714 - (int)m:(int)x { if (x) { return 1; } return 0; }\n\
3715 @end\n";
3716
3717 // The shape no parent-kind list can reach. Inside `@implementation`
3718 // the grammar re-wraps, so the helper's parent is still
3719 // `implementation_definition`; inside `@interface` it is the
3720 // `preproc_if` — a kind shared with a file-scope `#if`. Keying on
3721 // the node's own kind covers both.
3722 const OBJC_HELPER_BEHIND_PREPROC: &str = "@interface Foo : NSObject\n\
3723 #if FOO\n\
3724 static int declared(int x) { if (x) { return 1; } return 0; }\n\
3725 #endif\n\
3726 - (int)m:(int)x;\n\
3727 @end\n\
3728 @implementation Foo\n\
3729 #if FOO\n\
3730 static int helper(int x) { if (x) { return 1; } return 0; }\n\
3731 #endif\n\
3732 - (int)m:(int)x { if (x) { return 1; } return 0; }\n\
3733 @end\n";
3734
3735 // The reverse direction: a C function at file scope, which no
3736 // container ever weighted, alongside a class that has one branching
3737 // method.
3738 const OBJC_FILE_SCOPE_FUNCTION: &str = "\
3739 static int loose(int x) { if (x) { return 1; } return 0; }\n\
3740 @implementation Foo\n\
3741 - (int)m:(int)x { if (x) { return 1; } return 0; }\n\
3742 @end\n";
3743
3744 #[test]
3745 fn objc_static_helper_in_implementation_is_not_weighted_into_the_class() {
3746 check_wmc_and_npm::<ObjcParser>(OBJC_HELPER_IN_IMPLEMENTATION, "foo.m", |metric| {
3747 // The triple the issue tabulated. `wmc` read 3 before the
3748 // fix while `npm` read 1.
3749 assert_eq!(metric.wmc.class_wmc_sum(), 1);
3750 assert_eq!(metric.npm.class_nm_sum(), 1);
3751 assert_eq!(metric.npm.class_npm_sum(), 1);
3752 // `nom` counts free functions wherever they appear, so it
3753 // still sees two. That is its own contract, unchanged.
3754 assert_eq!(metric.nom.functions_sum(), 2);
3755 // The helper's complexity is excluded from the class, not
3756 // discarded: unit 1 + class 1 + `helper` 2 + `m` 1.
3757 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
3758 });
3759 }
3760
3761 #[test]
3762 fn objc_static_helper_keeps_its_own_function_space() {
3763 // Where the excluded complexity lands. The space tree is built
3764 // by a single-pass stack walk and
3765 // `tests/parity/space_span_containment.rs` requires a child's
3766 // span to sit inside its parent's, so the helper's space cannot
3767 // be reparented out of the `@implementation` it is written in —
3768 // it stays a `Function` space there, reporting its full
3769 // cyclomatic. Only the class's WMC roll-up declines it.
3770 check_func_space::<ObjcParser, _>(OBJC_HELPER_IN_IMPLEMENTATION, "foo.m", |space| {
3771 let class = child_space(&space, "Foo");
3772 assert_eq!(class.kind, SpaceKind::Class);
3773 assert_eq!(class.metrics.wmc.class_wmc(), 1);
3774 let helper = child_space(class, "helper");
3775 assert_eq!(helper.kind, SpaceKind::Function);
3776 assert_eq!(helper.metrics.cyclomatic.cyclomatic(), 2);
3777 });
3778 }
3779
3780 #[test]
3781 fn objc_static_helper_leaves_a_branching_method_its_weight() {
3782 check_wmc_and_npm::<ObjcParser>(OBJC_HELPER_AND_BRANCHING_METHOD, "foo.m", |metric| {
3783 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3784 assert_eq!(metric.npm.class_nm_sum(), 1);
3785 assert_eq!(metric.nom.functions_sum(), 2);
3786 });
3787 }
3788
3789 #[test]
3790 fn objc_static_helper_in_a_category_is_not_weighted_into_the_class() {
3791 check_wmc_and_npm::<ObjcParser>(OBJC_HELPER_IN_CATEGORY, "foo.m", |metric| {
3792 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3793 assert_eq!(metric.npm.class_nm_sum(), 1);
3794 assert_eq!(metric.nom.functions_sum(), 2);
3795 });
3796 }
3797
3798 #[test]
3799 fn objc_static_helper_in_an_interface_is_not_weighted_into_the_interface() {
3800 check_wmc_and_npm::<ObjcParser>(OBJC_HELPER_IN_INTERFACE, "foo.m", |metric| {
3801 // 2 before the fix, from the helper alone — an `@interface`
3802 // declares no bodies, so nothing else can reach it.
3803 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3804 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3805 assert_eq!(metric.npm.interface_nm_sum(), 1);
3806 assert_eq!(metric.nom.functions_sum(), 2);
3807 });
3808 }
3809
3810 #[test]
3811 fn objc_static_helper_in_a_protocol_is_not_weighted_into_the_protocol() {
3812 check_wmc_and_npm::<ObjcParser>(OBJC_HELPER_IN_PROTOCOL, "foo.m", |metric| {
3813 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3814 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3815 assert_eq!(metric.npm.interface_nm_sum(), 1);
3816 assert_eq!(metric.nom.functions_sum(), 2);
3817 });
3818 }
3819
3820 #[test]
3821 fn objc_static_helper_behind_a_preprocessor_conditional_is_not_weighted() {
3822 check_wmc_and_npm::<ObjcParser>(OBJC_HELPER_BEHIND_PREPROC, "foo.m", |metric| {
3823 // 2 each before the fix. The `@interface` half is the one a
3824 // parent-kind predicate cannot reach: `preproc_if` is the
3825 // helper's parent there, and a file-scope `#if` spells it
3826 // the same way.
3827 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3828 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3829 assert_eq!(metric.nom.functions_sum(), 3);
3830 });
3831 }
3832
3833 #[test]
3834 fn objc_file_scope_function_is_unaffected() {
3835 check_wmc_and_npm::<ObjcParser>(OBJC_FILE_SCOPE_FUNCTION, "foo.m", |metric| {
3836 // This one pins the *inertness* of marking every ObjC
3837 // `function_definition`, not the fix itself: `loose` never
3838 // reached a container's WMC either way, so every assertion
3839 // here also holds against the pre-fix tree. It is kept
3840 // because it still discriminates against two plausible wrong
3841 // implementations — a predicate widened to `MethodDefinition`
3842 // moves `class_wmc_sum`, and a reparent-based fix moves
3843 // `cyclomatic_sum` — not because it guards the change.
3844 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3845 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3846 assert_eq!(metric.nom.functions_sum(), 2);
3847 // unit 1 + `loose` 2 + class 1 + `m` 2. This also pins that
3848 // `loose` still opens a space of its own: folding it into
3849 // the unit instead would read 5.
3850 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 6);
3851 });
3852 }
3853
3854 // ----- C++ -----
3855
3856 #[test]
3857 fn cpp_empty_unit_zero_wmc() {
3858 // No code → no class spaces → wmc = 0. Wires up the trait.
3859 check_metrics::<CppParser>("", "empty.cpp", |metric| {
3860 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3861 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3862 insta::assert_json_snapshot!(metric.wmc);
3863 });
3864 }
3865
3866 #[test]
3867 fn cpp_single_method_wmc_one() {
3868 // One method with no control flow → cyclomatic = 1 → wmc = 1.
3869 check_metrics::<CppParser>("class Foo { public: void m() {} };", "foo.cpp", |metric| {
3870 assert_eq!(metric.wmc.class_wmc_sum(), 1);
3871 insta::assert_json_snapshot!(metric.wmc);
3872 });
3873 }
3874
3875 #[test]
3876 fn cpp_method_with_if_adds_to_wmc() {
3877 // One method with one `if` → cyclomatic = 2 → wmc = 2.
3878 check_metrics::<CppParser>(
3879 "class Foo {\n\
3880 public:\n\
3881 int m(int x) {\n\
3882 if (x > 0) { return 1; }\n\
3883 return 0;\n\
3884 }\n\
3885 };",
3886 "foo.cpp",
3887 |metric| {
3888 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3889 insta::assert_json_snapshot!(metric.wmc);
3890 },
3891 );
3892 }
3893
3894 #[test]
3895 fn cpp_struct_wmc_maps_to_class() {
3896 // `struct` opens a `SpaceKind::Struct` space — the C++ Wmc
3897 // impl maps it to `Class` so the same `class_wmc_sum`
3898 // accumulator receives the cyclomatic of struct methods.
3899 check_metrics::<CppParser>(
3900 "struct Foo {\n\
3901 int m(int x) {\n\
3902 if (x > 0) { return 1; }\n\
3903 return 0;\n\
3904 }\n\
3905 };",
3906 "foo.cpp",
3907 |metric| {
3908 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3909 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3910 insta::assert_json_snapshot!(metric.wmc);
3911 },
3912 );
3913 }
3914
3915 #[test]
3916 fn cpp_free_function_does_not_contribute_to_class_wmc() {
3917 // A top-level function is not inside any class — its
3918 // cyclomatic complexity must NOT contribute to class_wmc_sum.
3919 // The `Unit` space is mapped through `class_interface_compute`
3920 // unchanged; only `Function` spaces inside a `Class` /
3921 // `Struct` propagate up.
3922 check_metrics::<CppParser>(
3923 "int free_fn(int x) { if (x > 0) { return 1; } return 0; }",
3924 "foo.cpp",
3925 |metric| {
3926 assert_eq!(metric.wmc.class_wmc_sum(), 0);
3927 assert_eq!(metric.wmc.interface_wmc_sum(), 0);
3928 insta::assert_json_snapshot!(metric.wmc);
3929 },
3930 );
3931 }
3932
3933 #[test]
3934 fn cpp_multiple_methods_wmc_sums() {
3935 // Two methods, one with `if` (cyclomatic 2), one without
3936 // (cyclomatic 1). class_wmc_sum = 3.
3937 check_metrics::<CppParser>(
3938 "class Foo {\n\
3939 public:\n\
3940 int a(int x) { if (x > 0) { return 1; } return 0; }\n\
3941 int b() { return 42; }\n\
3942 };",
3943 "foo.cpp",
3944 |metric| {
3945 assert_eq!(metric.wmc.class_wmc_sum(), 3);
3946 insta::assert_json_snapshot!(metric.wmc);
3947 },
3948 );
3949 }
3950
3951 #[test]
3952 fn cpp_multiple_classes_wmc_aggregate() {
3953 // File-level rollup: Foo has wmc 1, Bar has wmc 1. Unit
3954 // class_wmc_sum = 2.
3955 check_metrics::<CppParser>(
3956 "class Foo { public: void a() {} };\nstruct Bar { void b() {} };",
3957 "foo.cpp",
3958 |metric| {
3959 assert_eq!(metric.wmc.class_wmc_sum(), 2);
3960 insta::assert_json_snapshot!(metric.wmc);
3961 },
3962 );
3963 }
3964
3965 // ----- C++ `friend` (#1301) -----
3966 //
3967 // A `friend` with an inline body is written inside the class braces
3968 // but is a free function the class merely grants access to. `npm`
3969 // has always declined to count it as a method; `wmc` weighted it,
3970 // so the two metrics disagreed about the same class. The fixtures
3971 // below are shared with the Mozcpp mirrors further down — mozcpp
3972 // owns no file extension, so nothing else exercises its clone of
3973 // these impls.
3974
3975 // The issue's own fixture, verbatim. Three outcomes are reachable,
3976 // so the expectation cannot hold for the wrong reason: 3 is the
3977 // pre-fix value (`amigo`'s cyclomatic 2 plus `mine`'s 1), 1 is
3978 // correct, and 0 would mean the roll-up dropped `mine` as well.
3979 const INLINE_FRIEND: &str = "class R {\n\
3980 public:\n\
3981 friend void amigo() { if (1) { } }\n\
3982 void mine() { }\n\
3983 };";
3984
3985 // `friend std::ostream& operator<<(…)` is the idiomatic inline
3986 // friend, and parses through the same `friend_declaration >
3987 // function_definition` shape as a named one. `dump` is deliberately
3988 // a *branching* method so the expectation (2) differs from both the
3989 // pre-fix value (4) and the method count (1).
3990 const INLINE_FRIEND_OPERATOR: &str = "class R {\n\
3991 public:\n\
3992 friend std::ostream& operator<<(std::ostream& o, const R& r) {\n\
3993 if (r.n) { return o; }\n\
3994 return o;\n\
3995 }\n\
3996 int dump(int x) { if (x) { return 1; } return 0; }\n\
3997 int n;\n\
3998 };";
3999
4000 // A templated friend nests one level deeper —
4001 // `template_declaration > friend_declaration > function_definition`
4002 // — so the function's parent is the `friend_declaration` either
4003 // way and one parent check covers both shapes. This is the friend
4004 // from #1258's `NON_METHOD_TEMPLATE_PAYLOADS`, whose `npm` side
4005 // that issue fixed and whose `wmc` side this one does.
4006 const TEMPLATED_INLINE_FRIEND: &str = "class R {\n\
4007 public:\n\
4008 template<typename T> friend void amigo(T t) { if (t) { } }\n\
4009 int mine(int x) { if (x) { return 1; } return 0; }\n\
4010 };";
4011
4012 // Every bodyless `friend` form: a plain declaration, an operator
4013 // declaration, and a befriended class. All parse as
4014 // `friend_declaration > declaration` (or bare tokens), open no
4015 // function space, and so never reach the predicate at all.
4016 const FRIEND_WITHOUT_BODY: &str = "class R {\n\
4017 public:\n\
4018 friend void amigo();\n\
4019 friend R operator+(const R& l, const R& r);\n\
4020 friend class Other;\n\
4021 int mine(int x) { if (x) { return 1; } return 0; }\n\
4022 };";
4023
4024 // A friend of a *nested* class. `Inner` keeps its own WMC scope
4025 // (2, from `hidden`) and `Outer` keeps its own (1, from
4026 // `outer_m`), so the file sum is 3 — never 5, which is what
4027 // folding `amigo` into `Inner` produced.
4028 const NESTED_CLASS_FRIEND: &str = "class Outer {\n\
4029 public:\n\
4030 class Inner {\n\
4031 public:\n\
4032 friend void amigo() { if (1) { } }\n\
4033 int hidden(int x) { if (x) { return 1; } return 0; }\n\
4034 };\n\
4035 void outer_m() { }\n\
4036 };";
4037
4038 #[test]
4039 fn cpp_inline_friend_is_not_weighted_into_the_class() {
4040 check_wmc_and_npm::<CppParser>(INLINE_FRIEND, "foo.cpp", |metric| {
4041 // The triple the issue tabulated. `wmc` read 3 before the
4042 // fix while `npm` read 1 — the disagreement #1258 removed
4043 // for templated member bodies and this removes for friends.
4044 assert_eq!(metric.wmc.class_wmc_sum(), 1);
4045 assert_eq!(metric.npm.class_nm_sum(), 1);
4046 assert_eq!(metric.npm.class_npm_sum(), 1);
4047 // `nom` counts free functions wherever they appear, so it
4048 // still sees two. That is its own contract and #1301 left
4049 // it alone.
4050 assert_eq!(metric.nom.functions_sum(), 2);
4051 // The friend's complexity is excluded from the class, not
4052 // discarded: unit 1 + class 1 + `amigo` 2 + `mine` 1.
4053 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
4054 });
4055 }
4056
4057 #[test]
4058 fn cpp_inline_friend_keeps_its_own_function_space() {
4059 // Where the excluded complexity lands. The space tree is built
4060 // by a single-pass stack walk and
4061 // `tests/parity/space_span_containment.rs` requires a child's
4062 // span to sit inside its parent's, so the friend's space cannot
4063 // be reparented out of the class it is written in — it stays a
4064 // `Function` space there, reporting its full cyclomatic. Only
4065 // the class's WMC roll-up declines it.
4066 check_func_space::<CppParser, _>(INLINE_FRIEND, "foo.cpp", |space| {
4067 let class = child_space(&space, "R");
4068 assert_eq!(class.kind, SpaceKind::Class);
4069 assert_eq!(class.metrics.wmc.class_wmc(), 1);
4070 let amigo = child_space(class, "amigo");
4071 assert_eq!(amigo.kind, SpaceKind::Function);
4072 assert_eq!(amigo.metrics.cyclomatic.cyclomatic(), 2);
4073 });
4074 }
4075
4076 #[test]
4077 fn cpp_inline_friend_operator_is_not_weighted_into_the_class() {
4078 check_wmc_and_npm::<CppParser>(INLINE_FRIEND_OPERATOR, "foo.cpp", |metric| {
4079 assert_eq!(metric.wmc.class_wmc_sum(), 2);
4080 assert_eq!(metric.npm.class_nm_sum(), 1);
4081 assert_eq!(metric.nom.functions_sum(), 2);
4082 });
4083 }
4084
4085 #[test]
4086 fn cpp_templated_inline_friend_is_not_weighted_into_the_class() {
4087 check_wmc_and_npm::<CppParser>(TEMPLATED_INLINE_FRIEND, "foo.cpp", |metric| {
4088 assert_eq!(metric.wmc.class_wmc_sum(), 2);
4089 assert_eq!(metric.npm.class_nm_sum(), 1);
4090 assert_eq!(metric.nom.functions_sum(), 2);
4091 });
4092 }
4093
4094 #[test]
4095 fn cpp_friend_declared_without_a_body_leaves_wmc_alone() {
4096 check_wmc_and_npm::<CppParser>(FRIEND_WITHOUT_BODY, "foo.cpp", |metric| {
4097 // `mine` alone, at cyclomatic 2 — a predicate that had
4098 // over-matched the whole `friend_declaration` subtree and
4099 // swallowed the sibling method would read 0 here.
4100 assert_eq!(metric.wmc.class_wmc_sum(), 2);
4101 assert_eq!(metric.npm.class_nm_sum(), 1);
4102 assert_eq!(metric.nom.functions_sum(), 1);
4103 });
4104 }
4105
4106 #[test]
4107 fn cpp_friend_of_a_nested_class_is_weighted_into_neither() {
4108 check_wmc_and_npm::<CppParser>(NESTED_CLASS_FRIEND, "foo.cpp", |metric| {
4109 assert_eq!(metric.wmc.class_wmc_sum(), 3);
4110 assert_eq!(metric.npm.class_nm_sum(), 2);
4111 assert_eq!(metric.nom.functions_sum(), 3);
4112 });
4113 }
4114
4115 // ----- Mozilla C++ (`friend`, #1301) -----
4116 //
4117 // `Mozcpp` owns no file extension, so no integration corpus reaches
4118 // it and its `Checker` clone would drift silently. These mirror the
4119 // C++ cases above over the same fixtures.
4120
4121 #[test]
4122 fn mozcpp_inline_friend_is_not_weighted_into_the_class() {
4123 check_wmc_and_npm::<MozcppParser>(INLINE_FRIEND, "foo.cpp", |metric| {
4124 assert_eq!(metric.wmc.class_wmc_sum(), 1);
4125 assert_eq!(metric.npm.class_nm_sum(), 1);
4126 assert_eq!(metric.npm.class_npm_sum(), 1);
4127 assert_eq!(metric.nom.functions_sum(), 2);
4128 assert_eq!(metric.cyclomatic.cyclomatic_sum(), 5);
4129 });
4130 }
4131
4132 #[test]
4133 fn mozcpp_inline_friend_operator_is_not_weighted_into_the_class() {
4134 check_wmc_and_npm::<MozcppParser>(INLINE_FRIEND_OPERATOR, "foo.cpp", |metric| {
4135 assert_eq!(metric.wmc.class_wmc_sum(), 2);
4136 assert_eq!(metric.npm.class_nm_sum(), 1);
4137 assert_eq!(metric.nom.functions_sum(), 2);
4138 });
4139 }
4140
4141 #[test]
4142 fn mozcpp_templated_inline_friend_is_not_weighted_into_the_class() {
4143 check_wmc_and_npm::<MozcppParser>(TEMPLATED_INLINE_FRIEND, "foo.cpp", |metric| {
4144 assert_eq!(metric.wmc.class_wmc_sum(), 2);
4145 assert_eq!(metric.npm.class_nm_sum(), 1);
4146 assert_eq!(metric.nom.functions_sum(), 2);
4147 });
4148 }
4149
4150 #[test]
4151 fn mozcpp_friend_declared_without_a_body_leaves_wmc_alone() {
4152 check_wmc_and_npm::<MozcppParser>(FRIEND_WITHOUT_BODY, "foo.cpp", |metric| {
4153 assert_eq!(metric.wmc.class_wmc_sum(), 2);
4154 assert_eq!(metric.npm.class_nm_sum(), 1);
4155 assert_eq!(metric.nom.functions_sum(), 1);
4156 });
4157 }
4158
4159 #[test]
4160 fn mozcpp_friend_of_a_nested_class_is_weighted_into_neither() {
4161 check_wmc_and_npm::<MozcppParser>(NESTED_CLASS_FRIEND, "foo.cpp", |metric| {
4162 assert_eq!(metric.wmc.class_wmc_sum(), 3);
4163 assert_eq!(metric.npm.class_nm_sum(), 2);
4164 assert_eq!(metric.nom.functions_sum(), 3);
4165 });
4166 }
4167
4168 #[test]
4169 fn javascript_empty_unit_zero_wmc() {
4170 check_metrics::<JavascriptParser>("", "empty.js", |metric| {
4171 assert_eq!(metric.wmc.class_wmc_sum(), 0);
4172 insta::assert_json_snapshot!(metric.wmc);
4173 });
4174 }
4175
4176 #[test]
4177 fn javascript_single_method_wmc_one() {
4178 // Class with a single straight-line method has wmc = 1 (the
4179 // method's cyclomatic) rolling into the class space.
4180 check_metrics::<JavascriptParser>("class Foo { a() { return 1; } }", "foo.js", |metric| {
4181 assert_eq!(metric.wmc.class_wmc_sum(), 1);
4182 insta::assert_json_snapshot!(metric.wmc);
4183 });
4184 }
4185
4186 #[test]
4187 fn javascript_method_with_if_adds_to_wmc() {
4188 // Method body with an `if` has cyclomatic = 2 → class_wmc = 2.
4189 check_metrics::<JavascriptParser>(
4190 "class Foo { a(x) { if (x > 0) return 1; return 0; } }",
4191 "foo.js",
4192 |metric| {
4193 assert_eq!(metric.wmc.class_wmc_sum(), 2);
4194 insta::assert_json_snapshot!(metric.wmc);
4195 },
4196 );
4197 }
4198
4199 #[test]
4200 fn javascript_free_function_does_not_contribute_to_class_wmc() {
4201 // Top-level functions are not class methods; their
4202 // cyclomatic does not roll into a class.
4203 check_metrics::<JavascriptParser>(
4204 "function f(x) { if (x > 0) return 1; return 0; }\nclass Foo { a() { return 1; } }",
4205 "foo.js",
4206 |metric| {
4207 // Only the class method contributes.
4208 assert_eq!(metric.wmc.class_wmc_sum(), 1);
4209 insta::assert_json_snapshot!(metric.wmc);
4210 },
4211 );
4212 }
4213
4214 #[test]
4215 fn javascript_multiple_classes_wmc_aggregate() {
4216 // File-level rollup: Foo has wmc 1, Bar has wmc 1. Unit
4217 // class_wmc_sum = 2.
4218 check_metrics::<JavascriptParser>(
4219 "class Foo { a() { return 1; } }\nclass Bar { b() { return 1; } }",
4220 "foo.js",
4221 |metric| {
4222 assert_eq!(metric.wmc.class_wmc_sum(), 2);
4223 insta::assert_json_snapshot!(metric.wmc);
4224 },
4225 );
4226 }
4227
4228 #[test]
4229 fn mozjs_single_method_wmc_one() {
4230 check_metrics::<MozjsParser>("class Foo { a() { return 1; } }", "foo.js", |metric| {
4231 assert_eq!(metric.wmc.class_wmc_sum(), 1);
4232 insta::assert_json_snapshot!(metric.wmc);
4233 });
4234 }
4235
4236 // #530: a method that *contains* a nested class must yield a
4237 // non-negative integer `class_wmc_sum`. The nested class is its own
4238 // WMC scope (#463), so `Outer.m` (base 1) plus the nested `Inner.n`
4239 // (base 1) sum to exactly 2 with no double-attribution and no
4240 // negative intermediate. Mirrors `java_local_inner_class`, kept
4241 // minimal to pin the `u64` accessor's non-negativity.
4242 #[test]
4243 fn java_method_with_nested_class_wmc_is_non_negative_integer() {
4244 check_metrics::<JavaParser>(
4245 "public class Outer {
4246 public void m() {
4247 class Inner {
4248 public void n() {}
4249 }
4250 }
4251 }",
4252 "Outer.java",
4253 |metric| {
4254 let total = metric.wmc.class_wmc_sum();
4255 assert_eq!(total, 2, "Outer.m (1) + Inner.n (1) with no double-count");
4256 },
4257 );
4258 }
4259
4260 // Rounds out `wmc`'s public surface — the `Display` impl and the
4261 // per-space `class_wmc` / `interface_wmc` accessors — mirroring the
4262 // `Display` tests the sibling metrics (nom, nargs, halstead) carry.
4263 #[test]
4264 fn stats_display_and_per_space_accessors() {
4265 check_func_space::<JavaParser, _>(
4266 "public interface I {\n void p();\n}\n\
4267 public class C {\n public void m() {}\n private void n() {}\n}\n",
4268 "X.java",
4269 |unit| {
4270 // Root rollup: class C contributes WMC 2, interface I contributes 1.
4271 assert_eq!(unit.metrics.wmc.class_wmc_sum(), 2);
4272 assert_eq!(unit.metrics.wmc.interface_wmc_sum(), 1);
4273 assert_eq!(
4274 unit.metrics.wmc.to_string(),
4275 "classes: 2, interfaces: 1, total: 3"
4276 );
4277 // The singular accessors are per-space and populate only on the
4278 // owning class / interface space (0 on the file-unit root), so
4279 // assert them where they are nonzero — an accessor that always
4280 // returned 0 or read the wrong field would fail here.
4281 let class = child_space(&unit, "C");
4282 assert_eq!(class.kind, SpaceKind::Class);
4283 assert_eq!(class.metrics.wmc.class_wmc(), 2);
4284 let iface = child_space(&unit, "I");
4285 assert_eq!(iface.kind, SpaceKind::Interface);
4286 assert_eq!(iface.metrics.wmc.interface_wmc(), 1);
4287 },
4288 );
4289 }
4290}