Skip to main content

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