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