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