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