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