Skip to main content

big_code_analysis/metrics/
npm.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// Metric counts (token, function, branch, argument, etc.) are stored as
9// `usize` and crossed with `f64` averages, ratios, and Halstead scores
10// across the cyclomatic / MI / Halstead computations. The `usize as f64`
11// and `f64 as usize` casts are intentional and snapshot-anchored — every
12// site is bounded by the count it came from. Allowing the lints at the
13// module level keeps the metric arithmetic legible.
14#![allow(
15    clippy::cast_precision_loss,
16    clippy::cast_possible_truncation,
17    clippy::cast_sign_loss
18)]
19
20use std::fmt;
21
22use crate::checker::{Checker, csharp_accessor_count};
23use crate::langs::*;
24use crate::macros::implement_metric_trait;
25use crate::metrics::npa::{accessibility_ratio, python_is_block, ts_member_is_public};
26use crate::node::Node;
27use crate::*;
28
29/// The `Npm` metric.
30///
31/// This metric counts the number of public methods
32/// of classes/interfaces.
33#[derive(Clone, Debug, Default, PartialEq)]
34#[non_exhaustive]
35pub struct Stats {
36    class_npm: usize,
37    interface_npm: usize,
38    class_nm: usize,
39    interface_nm: usize,
40    class_npm_sum: usize,
41    interface_npm_sum: usize,
42    class_nm_sum: usize,
43    interface_nm_sum: usize,
44    is_class_space: bool,
45}
46
47impl fmt::Display for Stats {
48    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
49        write!(
50            f,
51            "classes: {}, interfaces: {}, class_methods: {}, interface_methods: {}, class_coa: {}, interface_coa: {}, total: {}, total_methods: {}, coa: {}",
52            self.class_npm_sum(),
53            self.interface_npm_sum(),
54            self.class_nm_sum(),
55            self.interface_nm_sum(),
56            self.class_coa(),
57            self.interface_coa(),
58            self.total_npm(),
59            self.total_nm(),
60            self.total_coa()
61        )
62    }
63}
64
65impl Stats {
66    /// Merges a second `Npm` metric into the first one
67    pub fn merge(&mut self, other: &Stats) {
68        self.class_npm_sum += other.class_npm_sum;
69        self.interface_npm_sum += other.interface_npm_sum;
70        self.class_nm_sum += other.class_nm_sum;
71        self.interface_nm_sum += other.interface_nm_sum;
72    }
73
74    /// Returns the number of class public methods in a space.
75    #[inline]
76    #[must_use]
77    pub fn class_npm(&self) -> u64 {
78        self.class_npm as u64
79    }
80
81    /// Returns the number of interface public methods in a space.
82    #[inline]
83    #[must_use]
84    pub fn interface_npm(&self) -> u64 {
85        self.interface_npm as u64
86    }
87
88    /// Returns the number of class methods in a space.
89    #[inline]
90    #[must_use]
91    pub fn class_nm(&self) -> u64 {
92        self.class_nm as u64
93    }
94
95    /// Returns the number of interface methods in a space.
96    #[inline]
97    #[must_use]
98    pub fn interface_nm(&self) -> u64 {
99        self.interface_nm as u64
100    }
101
102    /// Returns the number of class public methods sum in a space.
103    #[inline]
104    #[must_use]
105    pub fn class_npm_sum(&self) -> u64 {
106        self.class_npm_sum as u64
107    }
108
109    /// Returns the number of interface public methods sum in a space.
110    #[inline]
111    #[must_use]
112    pub fn interface_npm_sum(&self) -> u64 {
113        self.interface_npm_sum as u64
114    }
115
116    /// Returns the number of class methods sum in a space.
117    #[inline]
118    #[must_use]
119    pub fn class_nm_sum(&self) -> u64 {
120        self.class_nm_sum as u64
121    }
122
123    /// Returns the number of interface methods sum in a space.
124    #[inline]
125    #[must_use]
126    pub fn interface_nm_sum(&self) -> u64 {
127        self.interface_nm_sum as u64
128    }
129
130    /// Returns the class `Coa` metric value
131    ///
132    /// The `Class Operation Accessibility` metric value for a class
133    /// is computed by dividing the `Npm` value of the class
134    /// by the total number of methods defined in the class.
135    ///
136    /// This metric is an adaptation of the `Classified Operation Accessibility` (`COA`)
137    /// security metric for not classified methods.
138    /// Paper: <https://ieeexplore.ieee.org/abstract/document/5381538>
139    #[inline]
140    #[must_use]
141    pub fn class_coa(&self) -> f64 {
142        accessibility_ratio(self.class_npm_sum() as f64, self.class_nm_sum() as f64)
143    }
144
145    /// Returns the interface `Coa` metric value
146    ///
147    /// The `Class Operation Accessibility` metric value for an interface
148    /// is computed by dividing the `Npm` value of the interface
149    /// by the total number of methods defined in the interface.
150    ///
151    /// This metric is an adaptation of the `Classified Operation Accessibility` (`COA`)
152    /// security metric for not classified methods.
153    /// Paper: <https://ieeexplore.ieee.org/abstract/document/5381538>
154    #[inline]
155    #[must_use]
156    pub fn interface_coa(&self) -> f64 {
157        // Java interface methods are implicitly public, so when every counted
158        // method is public (`npm == nm != 0`) the ratio is exactly 1.0 and the
159        // division is skipped. The empty case falls through to
160        // `accessibility_ratio`, which is guarded to return a finite 0.0 (not
161        // `NaN`) for a zero denominator (#438).
162        if self.interface_npm_sum == self.interface_nm_sum && self.interface_npm_sum != 0 {
163            1.0
164        } else {
165            accessibility_ratio(
166                self.interface_npm_sum() as f64,
167                self.interface_nm_sum() as f64,
168            )
169        }
170    }
171
172    /// Returns the total `Coa` metric value
173    ///
174    /// The total `Class Operation Accessibility` metric value
175    /// is computed by dividing the total `Npm` value
176    /// by the total number of methods.
177    ///
178    /// This metric is an adaptation of the `Classified Operation Accessibility` (`COA`)
179    /// security metric for not classified methods.
180    /// Paper: <https://ieeexplore.ieee.org/abstract/document/5381538>
181    #[inline]
182    #[must_use]
183    pub fn total_coa(&self) -> f64 {
184        accessibility_ratio(self.total_npm() as f64, self.total_nm() as f64)
185    }
186
187    /// Returns the total number of public methods in a space.
188    #[inline]
189    #[must_use]
190    pub fn total_npm(&self) -> u64 {
191        self.class_npm_sum() + self.interface_npm_sum()
192    }
193
194    /// Returns the total number of methods in a space.
195    #[inline]
196    #[must_use]
197    pub fn total_nm(&self) -> u64 {
198        self.class_nm_sum() + self.interface_nm_sum()
199    }
200
201    // Accumulates the number of class and interface
202    // public and not public methods into the sums
203    #[inline]
204    pub(crate) fn compute_sum(&mut self) {
205        self.class_npm_sum += self.class_npm;
206        self.interface_npm_sum += self.interface_npm;
207        self.class_nm_sum += self.class_nm;
208        self.interface_nm_sum += self.interface_nm;
209    }
210
211    // Checks if the `Npm` metric is disabled
212    #[inline]
213    pub(crate) fn is_disabled(&self) -> bool {
214        !self.is_class_space
215    }
216}
217
218#[doc(hidden)]
219/// Per-language counting of public methods.
220pub(crate) trait Npm
221where
222    Self: Checker,
223{
224    /// Walk `node` and update `stats` with this metric for the language
225    /// implementing the trait.
226    ///
227    /// `code` is the raw source-bytes buffer; languages whose visibility
228    /// rules are encoded in identifier text (Ruby's keyword-style
229    /// `private` / `public` / `protected`) read identifier text from
230    /// it. Languages whose visibility rules are encoded purely in
231    /// distinct token kinds (Java's `Public` / `Private`, PHP's
232    /// `VisibilityModifier`) ignore the parameter.
233    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats);
234}
235
236// Java and Groovy share their grammar tokens for class / interface
237// bodies, so `Npm::compute` differs only by the language enum.
238// `impl_npm_java_like!` emits the same body against each enum
239// (mirrors `impl_npa_java_like!` in `npa.rs`; issue #280).
240//
241// `ClassBody` covers class and record explicit bodies;
242// `EnumBodyDeclarations` is the optional declarations block inside
243// `EnumBody` (after the enum constants) and may contain method
244// declarations. Both share the same Java public-method detection rule.
245//
246// `InterfaceBody`: all methods in an interface are implicitly public
247// (https://docs.oracle.com/javase/tutorial/java/IandI/interfaceDef.html).
248// `AnnotationTypeBody`: annotation type elements are abstract public
249// methods at the bytecode level and obey the same rule.
250macro_rules! impl_npm_java_like {
251    ($code:ty, $lang:ident) => {
252        impl Npm for $code {
253            fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
254                use $lang::*;
255
256                if Self::is_func_space(node) && stats.is_disabled() {
257                    stats.is_class_space = true;
258                }
259
260                match node.kind_id().into() {
261                    ClassBody | EnumBodyDeclarations => {
262                        for method in node.children().filter(|n| Self::is_func(n)) {
263                            stats.class_nm += 1;
264                            // The first child node contains the list of method modifiers.
265                            // Source: https://docs.oracle.com/javase/tutorial/reflect/member/methodModifiers.html
266                            if let Some(modifiers) = method.child(0)
267                                && matches!(modifiers.kind_id().into(), Modifiers)
268                                && modifiers.first_child(|id| id == Public).is_some()
269                            {
270                                stats.class_npm += 1;
271                            }
272                        }
273                    }
274                    InterfaceBody => {
275                        stats.interface_nm += node.children().filter(|n| Self::is_func(n)).count();
276                        stats.interface_npm = stats.interface_nm;
277                    }
278                    AnnotationTypeBody => {
279                        stats.interface_nm += node
280                            .children()
281                            .filter(|n| {
282                                matches!(n.kind_id().into(), AnnotationTypeElementDeclaration)
283                            })
284                            .count();
285                        stats.interface_npm = stats.interface_nm;
286                    }
287                    _ => {}
288                }
289            }
290        }
291    };
292}
293
294// TypeScript / TSX share the same OOP node shape, so we expand the
295// same compute logic into both impls via `ts_npm_compute!`.
296//
297// What counts as a class method:
298// - `method_definition` direct children of `class_body` (regular
299//   instance methods, static methods, abstract method
300//   implementations, getters/setters/constructors). Each counts as
301//   one method — getter and setter each count separately, matching
302//   their distinct accessor semantics. Method overloads in TS share
303//   a single `method_definition` body (signature-only overloads are
304//   `method_signature` nodes inside a class body — those are
305//   declaration-only and we do not count them).
306// - `public_field_definition` whose initializer is an
307//   `arrow_function` (or `function_expression`). These are class
308//   members written as `foo = () => {}` and behave as methods.
309// - `abstract_method_signature` direct children of `class_body`
310//   (abstract method declarations on abstract classes).
311//
312// Interface decision: `method_signature`, `abstract_method_signature`,
313// and `construct_signature` direct children of `interface_body` count
314// toward `interface_npm` / `interface_nm`. Interface members are
315// implicitly public.
316//
317// Method overload signatures inside a class (`method_signature` as a
318// direct child of `class_body`) are NOT counted — they are
319// type-system declarations whose implementation is the `method_definition`
320// they precede. Counting them would double-count overloaded methods.
321macro_rules! ts_npm_compute {
322    ($lang:ident) => {
323        fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
324            use $lang::*;
325
326            if Self::is_func_space(node) && stats.is_disabled() {
327                stats.is_class_space = true;
328            }
329
330            match node.kind_id().into() {
331                ClassBody => {
332                    for member in node.children() {
333                        match member.kind_id().into() {
334                            MethodDefinition | AbstractMethodSignature => {
335                                stats.class_nm += 1;
336                                if ts_member_is_public!($lang, member) {
337                                    stats.class_npm += 1;
338                                }
339                            }
340                            // Field-as-arrow-function (`foo = () => …`) is a
341                            // class method written as a field initializer.
342                            PublicFieldDefinition
343                                if member
344                                    .first_child(|id| {
345                                        id == $lang::ArrowFunction
346                                            || id == $lang::FunctionExpression
347                                    })
348                                    .is_some() =>
349                            {
350                                stats.class_nm += 1;
351                                if ts_member_is_public!($lang, member) {
352                                    stats.class_npm += 1;
353                                }
354                            }
355                            _ => {}
356                        }
357                    }
358                }
359                InterfaceBody => {
360                    let count = node
361                        .children()
362                        .filter(|c| {
363                            matches!(
364                                c.kind_id().into(),
365                                MethodSignature | AbstractMethodSignature | ConstructSignature
366                            )
367                        })
368                        .count();
369                    stats.interface_nm += count;
370                    stats.interface_npm = stats.interface_nm;
371                }
372                _ => {}
373            }
374        }
375    };
376}
377
378// JavaScript / Mozjs class methods. JS has no `accessibility_modifier`
379// — every class member is public, so each method maps 1:1 to both
380// `nm` and `npm`. Two shapes count:
381//
382//   1. `method_definition` direct children of `class_body`
383//      (regular methods, getters/setters, the constructor — all share
384//      the same kind id in the JS grammar).
385//   2. `field_definition` whose initializer is an `arrow_function` or
386//      `function_expression` (method written as a field initializer:
387//      `foo = () => {}`).
388//
389// Prototype methods (`Foo.prototype.bar = function() {}`) would also
390// qualify, but detecting them requires matching the `prototype`
391// property text. The `Npm::compute` trait does not carry source
392// bytes, so prototype-shaped methods are intentionally not counted.
393// Modern ES2015+ class syntax is unaffected.
394macro_rules! js_npm_compute {
395    ($lang:ident) => {
396        fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
397            use $lang::*;
398
399            if Self::is_func_space(node) && stats.is_disabled() {
400                stats.is_class_space = true;
401            }
402
403            if !matches!(node.kind_id().into(), ClassBody) {
404                return;
405            }
406
407            for member in node.children() {
408                match member.kind_id().into() {
409                    MethodDefinition => {
410                        stats.class_nm += 1;
411                        stats.class_npm += 1;
412                    }
413                    FieldDefinition
414                        if member
415                            .first_child(|id| {
416                                id == $lang::ArrowFunction || id == $lang::FunctionExpression
417                            })
418                            .is_some() =>
419                    {
420                        stats.class_nm += 1;
421                        stats.class_npm += 1;
422                    }
423                    _ => {}
424                }
425            }
426        }
427    };
428}
429
430// Per-language `Npm` impls live in sibling modules. The `mod`
431// declarations sit after the local `macro_rules!` so textual macro
432// scoping reaches the child files (mirrors `getter.rs` and
433// `metrics::abc`).
434mod cpp;
435mod csharp;
436mod elixir;
437mod go;
438mod groovy;
439mod java;
440mod javascript;
441mod kotlin;
442mod mozcpp;
443mod mozjs;
444mod objc;
445mod php;
446mod python;
447mod ruby;
448mod rust;
449mod tsx;
450mod typescript;
451
452// Default no-op `Npm` impls. Audited in #188. See the rationale block
453// on `implement_metric_trait!(Npa, …)` in `src/metrics/npa.rs` — Npm
454// classification mirrors Npa one-for-one (same set of "has classes?"
455// questions, same follow-up issues).
456
457implement_metric_trait!(
458    Npm,
459    CCode,
460    PreprocCode,
461    CcommentCode,
462    PerlCode,
463    BashCode,
464    LuaCode,
465    TclCode,
466    IrulesCode
467);
468
469#[cfg(test)]
470#[allow(
471    clippy::float_cmp,
472    clippy::cast_precision_loss,
473    clippy::cast_possible_truncation,
474    clippy::cast_sign_loss,
475    clippy::similar_names,
476    clippy::doc_markdown,
477    clippy::needless_raw_string_hashes,
478    clippy::too_many_lines
479)]
480mod tests {
481    use crate::tools::{assert_child_space_kind, check_func_space, check_metrics};
482
483    use super::*;
484
485    #[test]
486    fn java_constructors() {
487        check_metrics::<JavaParser>(
488            "class X {
489                X() {}
490                private X(int a) {}
491                protected X(int a, int b) {}
492                public X(int a, int b, int c) {}    // +1
493            }",
494            "foo.java",
495            |metric| {
496                insta::assert_json_snapshot!(
497                    metric.npm,
498                    @r#"
499                {
500                  "class_npm_sum": 1,
501                  "interface_npm_sum": 0,
502                  "class_methods": 4,
503                  "interface_methods": 0,
504                  "class_coa": 0.25,
505                  "interface_coa": 0.0,
506                  "total": 1,
507                  "total_methods": 4,
508                  "coa": 0.25
509                }
510                "#
511                );
512            },
513        );
514    }
515
516    #[test]
517    fn groovy_no_methods() {
518        check_metrics::<GroovyParser>("class A { int x = 1 }", "foo.groovy", |metric| {
519            assert_eq!(metric.npm.total_nm(), 0);
520        });
521    }
522
523    #[test]
524    fn groovy_public_methods() {
525        check_metrics::<GroovyParser>(
526            "class A {
527                public void m1() {}
528                public int m2() { return 0 }
529                private void m3() {}
530            }",
531            "foo.groovy",
532            |metric| {
533                assert_eq!(metric.npm.class_nm_sum(), 3);
534                assert_eq!(metric.npm.class_npm_sum(), 2);
535            },
536        );
537    }
538
539    #[test]
540    fn groovy_interface_methods_implicitly_public() {
541        // Asserting only the body-walker `interface_*_sum` totals
542        // would pass vacuously if `InterfaceDeclaration` were dropped
543        // from `GroovyCode::is_func_space`. The structural
544        // `assert_child_space_kind` call catches that revert by
545        // requiring the interface to actually open an `Interface`
546        // FuncSpace.
547        check_func_space::<GroovyParser, _>(
548            "interface I {
549                void a()
550                int b()
551            }",
552            "foo.groovy",
553            |func_space| {
554                let metric = &func_space.metrics;
555                // Interface methods are implicitly public.
556                assert_eq!(metric.npm.interface_nm_sum(), 2);
557                assert_eq!(metric.npm.interface_npm_sum(), 2);
558                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
559            },
560        );
561    }
562
563    // Regression for issue #280: Groovy mirrors Java's enum / record /
564    // annotation method counting.
565    #[test]
566    fn groovy_enum_counts_methods() {
567        check_metrics::<GroovyParser>(
568            "enum Status {
569                ACTIVE, INACTIVE;
570                public int code() { return 0 }
571                private void reset() {}
572            }",
573            "foo.groovy",
574            |metric| {
575                assert_eq!(metric.npm.class_nm_sum(), 2);
576                assert_eq!(metric.npm.class_npm_sum(), 1);
577            },
578        );
579    }
580
581    #[test]
582    #[ignore = "dekobon Groovy grammar v1 does not support annotation type elements with `default` values; the trailing `default \"\"`/`default 0` make the body fail to parse"]
583    fn groovy_annotation_type_counts_elements() {
584        // The Groovy tree-sitter grammar parses `@interface` only when
585        // preceded by a modifier and when each element ends in `;` (it
586        // inherits the Java parser's strictness). This source shape
587        // produces a clean `annotation_type_declaration` →
588        // `annotation_type_body` → `annotation_type_element_declaration`
589        // tree. Mirror of `java_annotation_type_counts_elements` — the
590        // body-walker count is identical whether or not Groovy's
591        // `AnnotationTypeDeclaration` is wired into `is_func_space`,
592        // so the structural `check_func_space` assertion is what
593        // catches a revert.
594        check_func_space::<GroovyParser, _>(
595            "public @interface Marker {
596                String value() default \"\";
597                int priority() default 0;
598            }",
599            "foo.groovy",
600            |func_space| {
601                assert_eq!(func_space.metrics.npm.interface_nm_sum(), 2);
602                assert_eq!(func_space.metrics.npm.interface_npm_sum(), 2);
603                assert_child_space_kind(&func_space, "Marker", SpaceKind::Interface);
604            },
605        );
606    }
607
608    #[test]
609    fn groovy_constructors() {
610        check_metrics::<GroovyParser>(
611            "class X {
612                X() {}
613                private X(int a) {}
614                protected X(int a, int b) {}
615                public X(int a, int b, int c) {}
616            }",
617            "foo.groovy",
618            |metric| {
619                // 4 constructors total, 1 public
620                assert_eq!(metric.npm.class_nm_sum(), 4);
621                assert_eq!(metric.npm.class_npm_sum(), 1);
622            },
623        );
624    }
625
626    #[test]
627    fn groovy_no_methods_in_unit_scope() {
628        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
629            assert_eq!(metric.npm.total_nm(), 0);
630        });
631    }
632
633    #[test]
634    fn groovy_multiple_classes_methods() {
635        check_metrics::<GroovyParser>(
636            "class A { public void a() {} }
637            class B { public void b() {} }",
638            "foo.groovy",
639            |metric| {
640                assert_eq!(metric.npm.class_nm_sum(), 2);
641                assert_eq!(metric.npm.class_npm_sum(), 2);
642            },
643        );
644    }
645
646    #[test]
647    fn groovy_methods_returning_primitive_types() {
648        // Mirror of `java_methods_returning_primitive_types`. Each
649        // method declaration is counted regardless of return type;
650        // `public` modifier promotes to NPM.
651        check_metrics::<GroovyParser>(
652            "class X {
653                public byte a() {}
654                public int b() {}
655                public double c() {}
656                public boolean d() {}
657                byte e() {}
658                int f() {}
659            }",
660            "foo.groovy",
661            |metric| {
662                // 6 methods, 4 public.
663                assert_eq!(metric.npm.class_nm_sum(), 6);
664                assert_eq!(metric.npm.class_npm_sum(), 4);
665            },
666        );
667    }
668
669    #[test]
670    fn groovy_methods_with_generic_types() {
671        // Methods with generic parameter/return types.
672        check_metrics::<GroovyParser>(
673            "class X {
674                public List<String> a() {}
675                public Map<String, Integer> b() {}
676                List<Integer> c() {}
677            }",
678            "foo.groovy",
679            |metric| {
680                assert_eq!(metric.npm.class_nm_sum(), 3);
681                assert_eq!(metric.npm.class_npm_sum(), 2);
682            },
683        );
684    }
685
686    #[test]
687    fn groovy_method_modifiers() {
688        // Modifier ordering doesn't matter — what matters is
689        // whether the `Modifiers` block contains `Public`. Mirrors
690        // `java_method_modifiers`.
691        check_metrics::<GroovyParser>(
692            "abstract class X {
693                public static void a() {}
694                static public void b() {}
695                public final void c() {}
696                final public void d() {}
697                protected static void e() {}
698                static protected void f() {}
699                abstract public void g()
700                abstract void h()
701            }",
702            "foo.groovy",
703            |metric| {
704                // 8 methods, 5 public.
705                assert_eq!(metric.npm.class_nm_sum(), 8);
706                assert_eq!(metric.npm.class_npm_sum(), 5);
707            },
708        );
709    }
710
711    #[test]
712    #[ignore = "dekobon Groovy grammar v1 does not yet support inner classes inside class bodies"]
713    fn groovy_nested_inner_classes() {
714        // Each nested `class` declaration is its own class space.
715        // Mirrors `java_nested_inner_classes`.
716        check_metrics::<GroovyParser>(
717            "class X {
718                public void a() {}
719                class Y {
720                    public void b() {}
721                    class Z {
722                        public void c() {}
723                    }
724                }
725            }",
726            "foo.groovy",
727            |metric| {
728                // 3 classes, 3 public methods (one per class).
729                assert_eq!(metric.npm.class_nm_sum(), 3);
730                assert_eq!(metric.npm.class_npm_sum(), 3);
731            },
732        );
733    }
734
735    #[test]
736    #[ignore = "dekobon Groovy grammar v1 does not yet support anonymous inner classes (`new T() { … }`)"]
737    fn groovy_anonymous_inner_class() {
738        // Anonymous inner class via `new T() { ... }`. Its methods
739        // are counted in a separate class space.
740        check_metrics::<GroovyParser>(
741            "class X {
742                public Runnable r = new Runnable() {
743                    public void run() {}
744                    void helper() {}
745                }
746            }",
747            "foo.groovy",
748            |metric| {
749                // Inner anonymous: 2 methods (run + helper), 1 public
750                // (run). Outer X has no methods.
751                assert_eq!(metric.npm.class_nm_sum(), 2);
752                assert_eq!(metric.npm.class_npm_sum(), 1);
753            },
754        );
755    }
756
757    #[test]
758    fn groovy_interfaces_and_class() {
759        // Mixed interfaces + class. Interface methods are
760        // implicitly public; class methods need explicit `public`.
761        // Mirrors `java_interfaces_and_class`. Structural
762        // `assert_child_space_kind` guards against an
763        // `InterfaceDeclaration` revert (see #311).
764        check_func_space::<GroovyParser, _>(
765            "interface X {
766                void a()
767            }
768            interface Y extends X {
769                void b()
770                void c()
771            }
772            class Z implements Y {
773                public void a() {}
774                public void b() {}
775                public void c() {}
776                void d() {}
777                void e() {}
778            }",
779            "foo.groovy",
780            |func_space| {
781                let metric = &func_space.metrics;
782                // Interfaces: 3 total methods (a, b, c), all 3 public.
783                assert_eq!(metric.npm.interface_nm_sum(), 3);
784                assert_eq!(metric.npm.interface_npm_sum(), 3);
785                // Class Z: 5 methods, 3 public (a, b, c — d, e are
786                // package-private).
787                assert_eq!(metric.npm.class_nm_sum(), 5);
788                assert_eq!(metric.npm.class_npm_sum(), 3);
789                assert_child_space_kind(&func_space, "X", SpaceKind::Interface);
790                assert_child_space_kind(&func_space, "Y", SpaceKind::Interface);
791                assert_child_space_kind(&func_space, "Z", SpaceKind::Class);
792            },
793        );
794    }
795
796    #[test]
797    fn java_methods_returning_primitive_types() {
798        check_metrics::<JavaParser>(
799            "class X {
800                public byte a() {}      // +1
801                public short b() {}     // +1
802                public int c() {}       // +1
803                public long d() {}      // +1
804                public float e() {}     // +1
805                public double f() {}    // +1
806                public boolean g() {}   // +1
807                public char h() {}      // +1
808                byte i() {}
809                short j() {}
810                int k() {}
811                long l() {}
812                float m() {}
813                double n() {}
814                boolean o() {}
815                char p() {}
816            }",
817            "foo.java",
818            |metric| {
819                insta::assert_json_snapshot!(
820                    metric.npm,
821                    @r#"
822                {
823                  "class_npm_sum": 8,
824                  "interface_npm_sum": 0,
825                  "class_methods": 16,
826                  "interface_methods": 0,
827                  "class_coa": 0.5,
828                  "interface_coa": 0.0,
829                  "total": 8,
830                  "total_methods": 16,
831                  "coa": 0.5
832                }
833                "#
834                );
835            },
836        );
837    }
838
839    #[test]
840    fn java_methods_returning_arrays() {
841        check_metrics::<JavaParser>(
842            "class X {
843                public byte[] a() {}    // +1
844                public short[] b() {}   // +1
845                public int[] c() {}     // +1
846                public long[] d() {}    // +1
847                public float[] e() {}   // +1
848                public double[] f() {}  // +1
849                public boolean[] g() {} // +1
850                public char[] h() {}    // +1
851                byte[] i() {}
852                short[] j() {}
853                int[] k() {}
854                long[] l() {}
855                float[] m() {}
856                double[] n() {}
857                boolean[] o() {}
858                char[] p() {}
859            }",
860            "foo.java",
861            |metric| {
862                insta::assert_json_snapshot!(
863                    metric.npm,
864                    @r#"
865                {
866                  "class_npm_sum": 8,
867                  "interface_npm_sum": 0,
868                  "class_methods": 16,
869                  "interface_methods": 0,
870                  "class_coa": 0.5,
871                  "interface_coa": 0.0,
872                  "total": 8,
873                  "total_methods": 16,
874                  "coa": 0.5
875                }
876                "#
877                );
878            },
879        );
880    }
881
882    #[test]
883    fn java_methods_returning_objects() {
884        check_metrics::<JavaParser>(
885            "class X {
886                public Integer[] a() {} // +1
887                public Integer b() {}   // +1
888                public String[] c() {}  // +1
889                public String d() {}    // +1
890                public Y[] e() {}       // +1
891                public Y f() {}         // +1
892                Integer[] g() {}
893                Integer h() {}
894                String[] i() {}
895                String j() {}
896                Y[] k() {}
897                Y l() {}
898            }",
899            "foo.java",
900            |metric| {
901                insta::assert_json_snapshot!(
902                    metric.npm,
903                    @r#"
904                {
905                  "class_npm_sum": 6,
906                  "interface_npm_sum": 0,
907                  "class_methods": 12,
908                  "interface_methods": 0,
909                  "class_coa": 0.5,
910                  "interface_coa": 0.0,
911                  "total": 6,
912                  "total_methods": 12,
913                  "coa": 0.5
914                }
915                "#
916                );
917            },
918        );
919    }
920
921    #[test]
922    fn java_methods_with_generic_types() {
923        check_metrics::<JavaParser>(
924            "class X {
925                public <T, S extends T> void a(T x, S y) {} // +1
926                public <T, S> int b(T x, S y) {}            // +1
927                public <T> boolean c(T x) {}                // +1
928                public <T> ArrayList<T> d() {}              // +1
929                public Y<String> e() {}                     // +1
930                <T, S extends T> void f(T x, S y) {}
931                <T, S> int g(T x, S y) {}
932                <T> boolean h(T x) {}
933                <T> ArrayList<T> i() {}
934                Y<String> j() {}
935            }",
936            "foo.java",
937            |metric| {
938                insta::assert_json_snapshot!(
939                    metric.npm,
940                    @r#"
941                {
942                  "class_npm_sum": 5,
943                  "interface_npm_sum": 0,
944                  "class_methods": 10,
945                  "interface_methods": 0,
946                  "class_coa": 0.5,
947                  "interface_coa": 0.0,
948                  "total": 5,
949                  "total_methods": 10,
950                  "coa": 0.5
951                }
952                "#
953                );
954            },
955        );
956    }
957
958    #[test]
959    fn java_method_modifiers() {
960        check_metrics::<JavaParser>(
961            "abstract class X {
962                public static final synchronized strictfp void a() {}   // +1
963                static public final synchronized strictfp void b() {}   // +1
964                static final public synchronized strictfp void c() {}   // +1
965                static final synchronized public strictfp void d() {}   // +1
966                static final synchronized strictfp public void e() {}   // +1
967                protected static final synchronized native void f();
968                static protected final synchronized native void g();
969                static final protected synchronized native void h();
970                static final synchronized protected native void i();
971                static final synchronized native protected void j();
972                abstract public void k();                               // +1
973                abstract void l();
974            }",
975            "foo.java",
976            |metric| {
977                insta::assert_json_snapshot!(
978                    metric.npm,
979                    @r#"
980                {
981                  "class_npm_sum": 6,
982                  "interface_npm_sum": 0,
983                  "class_methods": 12,
984                  "interface_methods": 0,
985                  "class_coa": 0.5,
986                  "interface_coa": 0.0,
987                  "total": 6,
988                  "total_methods": 12,
989                  "coa": 0.5
990                }
991                "#
992                );
993            },
994        );
995    }
996
997    #[test]
998    fn java_classes() {
999        check_metrics::<JavaParser>(
1000            "class X {
1001                public void a() {}  // +1
1002                public void b() {}  // +1
1003                private void c() {}
1004            }
1005            class Y {
1006                private void d() {}
1007                private void e() {}
1008                public void f() {}  // +1
1009            }",
1010            "foo.java",
1011            |metric| {
1012                insta::assert_json_snapshot!(
1013                    metric.npm,
1014                    @r#"
1015                {
1016                  "class_npm_sum": 3,
1017                  "interface_npm_sum": 0,
1018                  "class_methods": 6,
1019                  "interface_methods": 0,
1020                  "class_coa": 0.5,
1021                  "interface_coa": 0.0,
1022                  "total": 3,
1023                  "total_methods": 6,
1024                  "coa": 0.5
1025                }
1026                "#
1027                );
1028            },
1029        );
1030    }
1031
1032    #[test]
1033    fn java_nested_inner_classes() {
1034        check_metrics::<JavaParser>(
1035            "class X {
1036                public void a() {}          // +1
1037                class Y {
1038                    public void b() {}      // +1
1039                    class Z {
1040                        public void c() {}  // +1
1041                    }
1042                }
1043            }",
1044            "foo.java",
1045            |metric| {
1046                insta::assert_json_snapshot!(
1047                    metric.npm,
1048                    @r#"
1049                {
1050                  "class_npm_sum": 3,
1051                  "interface_npm_sum": 0,
1052                  "class_methods": 3,
1053                  "interface_methods": 0,
1054                  "class_coa": 1.0,
1055                  "interface_coa": 0.0,
1056                  "total": 3,
1057                  "total_methods": 3,
1058                  "coa": 1.0
1059                }
1060                "#
1061                );
1062            },
1063        );
1064    }
1065
1066    #[test]
1067    fn java_local_inner_classes() {
1068        check_metrics::<JavaParser>(
1069            "class X {
1070                public void a() {                   // +1
1071                    class Y {
1072                        public void b() {           // +1
1073                            class Z {
1074                                public void c() {}  // +1
1075                            }
1076                        }
1077                    }
1078                }
1079            }",
1080            "foo.java",
1081            |metric| {
1082                insta::assert_json_snapshot!(
1083                    metric.npm,
1084                    @r#"
1085                {
1086                  "class_npm_sum": 3,
1087                  "interface_npm_sum": 0,
1088                  "class_methods": 3,
1089                  "interface_methods": 0,
1090                  "class_coa": 1.0,
1091                  "interface_coa": 0.0,
1092                  "total": 3,
1093                  "total_methods": 3,
1094                  "coa": 1.0
1095                }
1096                "#
1097                );
1098            },
1099        );
1100    }
1101
1102    #[test]
1103    fn java_anonymous_inner_classes() {
1104        check_metrics::<JavaParser>(
1105            "abstract class X {
1106                public abstract void a();   // +1
1107            }
1108            abstract class Y {
1109                abstract void b();
1110            }
1111            class Z {
1112                public void c(){            // +1
1113                    X x = new X() {
1114                        @Override
1115                        public void a() {}  // +1
1116                    };
1117                    Y y = new Y() {
1118                        @Override
1119                        void b() {}
1120                    };
1121                }
1122            }",
1123            "foo.java",
1124            |metric| {
1125                insta::assert_json_snapshot!(
1126                    metric.npm,
1127                    @r#"
1128                {
1129                  "class_npm_sum": 3,
1130                  "interface_npm_sum": 0,
1131                  "class_methods": 5,
1132                  "interface_methods": 0,
1133                  "class_coa": 0.6,
1134                  "interface_coa": 0.0,
1135                  "total": 3,
1136                  "total_methods": 5,
1137                  "coa": 0.6
1138                }
1139                "#
1140                );
1141            },
1142        );
1143    }
1144
1145    #[test]
1146    fn java_interface() {
1147        check_metrics::<JavaParser>(
1148            "interface X {
1149                public int a(); // +1
1150                boolean b();    // +1
1151                void c();       // +1
1152            }",
1153            "foo.java",
1154            |metric| {
1155                insta::assert_json_snapshot!(
1156                    metric.npm,
1157                    @r#"
1158                {
1159                  "class_npm_sum": 0,
1160                  "interface_npm_sum": 3,
1161                  "class_methods": 0,
1162                  "interface_methods": 3,
1163                  "class_coa": 0.0,
1164                  "interface_coa": 1.0,
1165                  "total": 3,
1166                  "total_methods": 3,
1167                  "coa": 1.0
1168                }
1169                "#
1170                );
1171            },
1172        );
1173    }
1174
1175    // Regression for issue #280: Java enum bodies hold methods after
1176    // the constants. The Npm body walker recognises
1177    // `EnumBodyDeclarations` and treats it like `ClassBody`.
1178    #[test]
1179    fn java_enum_counts_methods() {
1180        check_metrics::<JavaParser>(
1181            "enum Status {
1182                ACTIVE, INACTIVE;
1183                public int code() { return 0; }     // +1 public
1184                private void reset() {}             // not public
1185            }",
1186            "foo.java",
1187            |metric| {
1188                assert_eq!(metric.npm.class_nm_sum(), 2);
1189                assert_eq!(metric.npm.class_npm_sum(), 1);
1190            },
1191        );
1192    }
1193
1194    // Regression for issue #280: Java records can declare methods in
1195    // their explicit body; they share `ClassBody`'s walker.
1196    #[test]
1197    fn java_record_counts_methods() {
1198        check_metrics::<JavaParser>(
1199            "record Point(int x, int y) {
1200                public int sum() { return x + y; }
1201                public Point() { this(0, 0); }
1202            }",
1203            "foo.java",
1204            |metric| {
1205                // `JavaCode::is_func` accepts both `MethodDeclaration`
1206                // and `ConstructorDeclaration`, so the body contributes
1207                // one method (`sum`) plus one explicit constructor
1208                // (`Point()`) = 2 total, both annotated `public`.
1209                assert_eq!(metric.npm.class_nm_sum(), 2);
1210                assert_eq!(metric.npm.class_npm_sum(), 2);
1211            },
1212        );
1213    }
1214
1215    #[test]
1216    fn java_annotation_type_counts_elements() {
1217        // Asserting only the body-walker counts (`interface_nm_sum`,
1218        // `interface_npm_sum`) would pass vacuously if
1219        // `AnnotationTypeDeclaration` were dropped from
1220        // `JavaCode::is_func_space`: with no `SpaceKind::Interface`
1221        // opened, the file-level Unit would still report 2.0 for both
1222        // sums (the body walker counts `AnnotationTypeElementDeclaration`
1223        // regardless of the surrounding space). The `check_func_space`
1224        // assertion catches that revert by requiring the annotation
1225        // type to actually open an `Interface` FuncSpace.
1226        check_func_space::<JavaParser, _>(
1227            "@interface Marker {
1228                String value() default \"\";
1229                int priority() default 0;
1230            }",
1231            "foo.java",
1232            |func_space| {
1233                assert_eq!(func_space.metrics.npm.interface_nm_sum(), 2);
1234                assert_eq!(func_space.metrics.npm.interface_npm_sum(), 2);
1235                assert_child_space_kind(&func_space, "Marker", SpaceKind::Interface);
1236            },
1237        );
1238    }
1239
1240    #[test]
1241    fn java_interfaces_and_class() {
1242        check_metrics::<JavaParser>(
1243            "interface X {
1244                void a();           // +1
1245            }
1246            interface Y extends X {
1247                void b();           // +1
1248                void c();           // +1
1249            }
1250            class Z implements Y {
1251                @Override
1252                public void a() {}  // +1
1253                @Override
1254                public void b() {}  // +1
1255                @Override
1256                public void c() {}  // +1
1257                void d() {}
1258                void e() {}
1259            }",
1260            "foo.java",
1261            |metric| {
1262                insta::assert_json_snapshot!(
1263                    metric.npm,
1264                    @r#"
1265                {
1266                  "class_npm_sum": 3,
1267                  "interface_npm_sum": 3,
1268                  "class_methods": 5,
1269                  "interface_methods": 3,
1270                  "class_coa": 0.6,
1271                  "interface_coa": 1.0,
1272                  "total": 6,
1273                  "total_methods": 8,
1274                  "coa": 0.75
1275                }
1276                "#
1277                );
1278            },
1279        );
1280    }
1281
1282    #[test]
1283    fn csharp_constructors() {
1284        check_metrics::<CsharpParser>(
1285            "class A {
1286                public A() {}
1287                public A(int x) {}
1288                A(int x, int y) {}
1289            }",
1290            "foo.cs",
1291            |metric| insta::assert_json_snapshot!(metric.npm),
1292        );
1293    }
1294
1295    #[test]
1296    fn csharp_methods_returning_primitive_types() {
1297        check_metrics::<CsharpParser>(
1298            "class A {
1299                public int M1() { return 1; }
1300                public bool M2() { return true; }
1301                public double M3() { return 0.0; }
1302                int M4() { return 0; }
1303            }",
1304            "foo.cs",
1305            |metric| insta::assert_json_snapshot!(metric.npm),
1306        );
1307    }
1308
1309    #[test]
1310    fn csharp_methods_returning_arrays() {
1311        check_metrics::<CsharpParser>(
1312            "class A {
1313                public int[] M1() { return new int[0]; }
1314                public string[] M2() { return new string[0]; }
1315                int[] M3() { return new int[0]; }
1316            }",
1317            "foo.cs",
1318            |metric| insta::assert_json_snapshot!(metric.npm),
1319        );
1320    }
1321
1322    #[test]
1323    fn csharp_methods_returning_objects() {
1324        check_metrics::<CsharpParser>(
1325            "class Point { }
1326             class A {
1327                public Point M1() { return new Point(); }
1328                public string M2() { return \"\"; }
1329                Point M3() { return new Point(); }
1330             }",
1331            "foo.cs",
1332            |metric| insta::assert_json_snapshot!(metric.npm),
1333        );
1334    }
1335
1336    #[test]
1337    fn csharp_methods_with_generic_types() {
1338        check_metrics::<CsharpParser>(
1339            "class A {
1340                public System.Collections.Generic.List<int> M1() { return null; }
1341                public System.Collections.Generic.Dictionary<string, int> M2() { return null; }
1342                System.Collections.Generic.List<string> M3() { return null; }
1343            }",
1344            "foo.cs",
1345            |metric| insta::assert_json_snapshot!(metric.npm),
1346        );
1347    }
1348
1349    #[test]
1350    fn csharp_method_modifiers() {
1351        check_metrics::<CsharpParser>(
1352            "class A {
1353                public void M1() {}
1354                private void M2() {}
1355                protected void M3() {}
1356                internal void M4() {}
1357                public static void M5() {}
1358                public virtual void M6() {}
1359            }",
1360            "foo.cs",
1361            |metric| insta::assert_json_snapshot!(metric.npm),
1362        );
1363    }
1364
1365    #[test]
1366    fn csharp_classes() {
1367        check_metrics::<CsharpParser>(
1368            "class A {
1369                public void M1() {}
1370                public void M2() {}
1371                void M3() {}
1372            }
1373            class B {
1374                public int N() { return 0; }
1375                int Hidden() { return 0; }
1376            }",
1377            "foo.cs",
1378            |metric| insta::assert_json_snapshot!(metric.npm),
1379        );
1380    }
1381
1382    #[test]
1383    fn csharp_nested_inner_classes() {
1384        check_metrics::<CsharpParser>(
1385            "class Outer {
1386                public void M() {}
1387                void Hidden() {}
1388                public class Inner {
1389                    public void N() {}
1390                    void HiddenN() {}
1391                }
1392            }",
1393            "foo.cs",
1394            |metric| insta::assert_json_snapshot!(metric.npm),
1395        );
1396    }
1397
1398    #[test]
1399    fn csharp_property_accessors() {
1400        // EC7 — each property accessor (get/set/init) counts as a method.
1401        // `W` is an expression-bodied property — no AccessorList, just an
1402        // ArrowExpressionClause — and exercises the `.max(1)` fallback in
1403        // `csharp_count_member` that keeps such properties at 1 method.
1404        check_metrics::<CsharpParser>(
1405            "class A {
1406                int _w;
1407                public int X { get; set; }
1408                public int Y { get; }
1409                public int Z { get; init; }
1410                public int W => _w;
1411                int Hidden { get; set; }
1412            }",
1413            "foo.cs",
1414            |metric| insta::assert_json_snapshot!(metric.npm),
1415        );
1416    }
1417
1418    #[test]
1419    fn csharp_narrowed_accessor_visibility() {
1420        // #783 — a C# accessor inherits the member's visibility unless it
1421        // narrows it with its own `private` / `protected` modifier. A
1422        // narrowed accessor still counts as a method (nm) but is NOT a
1423        // public method (npm). Members exercised:
1424        //   X  public { get; private set; }   nm 2, npm 1 (get only)
1425        //   Idx public this[...] { get; protected set; } nm 2, npm 1
1426        //   Y  public { get; set; }           nm 2, npm 2 (unchanged guard)
1427        //   W  public { get; }                nm 1, npm 1 (auto-property)
1428        //   Z  public => 0                    nm 1, npm 1 (expression body)
1429        //   P  (no modifier) { get; set; }    nm 2, npm 0 (private member)
1430        // expected nm  = 2 + 2 + 2 + 1 + 1 + 2 = 10
1431        // expected npm = 1 + 1 + 2 + 1 + 1 + 0 = 6
1432        check_metrics::<CsharpParser>(
1433            "class A {
1434                public int X { get; private set; }
1435                public int this[int i] { get; protected set; }
1436                public int Y { get; set; }
1437                public int W { get; }
1438                public int Z => 0;
1439                int P { get; set; }
1440            }",
1441            "foo.cs",
1442            |metric| {
1443                assert_eq!(metric.npm.class_nm_sum(), 10, "all accessors count as nm");
1444                assert_eq!(
1445                    metric.npm.class_npm_sum(),
1446                    6,
1447                    "narrowed private/protected accessors are not public methods"
1448                );
1449                insta::assert_json_snapshot!(metric.npm);
1450            },
1451        );
1452    }
1453
1454    #[test]
1455    fn csharp_local_functions() {
1456        // Local functions inside a method body are nested function spaces;
1457        // they don't count toward the enclosing class's NoM/NPM. The
1458        // private sibling `Hidden` ensures the visibility gate is also
1459        // exercised: nm should be 2 (Outer + Hidden), npm should be 1
1460        // (only Outer is `public`). If the local function leaked into
1461        // the enclosing class's count, nm would be 3.
1462        check_metrics::<CsharpParser>(
1463            "class A {
1464                public void Outer() {
1465                    void Local() {}
1466                    Local();
1467                }
1468                private void Hidden() {}
1469            }",
1470            "foo.cs",
1471            |metric| {
1472                assert_eq!(metric.npm.class_nm_sum(), 2, "Local must not leak");
1473                assert_eq!(metric.npm.class_npm_sum(), 1, "only Outer is public");
1474                insta::assert_json_snapshot!(metric.npm);
1475            },
1476        );
1477    }
1478
1479    #[test]
1480    fn csharp_interface() {
1481        // EC14 — interface methods default to public.
1482        check_metrics::<CsharpParser>(
1483            "interface I {
1484                int M1();
1485                bool M2();
1486                int X { get; set; }
1487            }",
1488            "foo.cs",
1489            |metric| insta::assert_json_snapshot!(metric.npm),
1490        );
1491    }
1492
1493    #[test]
1494    fn csharp_interfaces_and_class() {
1495        check_metrics::<CsharpParser>(
1496            "interface I1 { int M1(); }
1497            interface I2 { bool M2(); float M3(); }
1498            class A {
1499                public void M() {}
1500                void Hidden() {}
1501            }",
1502            "foo.cs",
1503            |metric| insta::assert_json_snapshot!(metric.npm),
1504        );
1505    }
1506
1507    #[test]
1508    fn php_no_class_methods() {
1509        check_metrics::<PhpParser>(
1510            "<?php class A { public int $x = 0; }",
1511            "foo.php",
1512            |metric| insta::assert_json_snapshot!(metric.npm),
1513        );
1514    }
1515
1516    #[test]
1517    fn php_one_public_method() {
1518        check_metrics::<PhpParser>(
1519            "<?php class A { public function f(): void {} }",
1520            "foo.php",
1521            |metric| insta::assert_json_snapshot!(metric.npm),
1522        );
1523    }
1524
1525    #[test]
1526    fn php_one_private_method() {
1527        check_metrics::<PhpParser>(
1528            "<?php class A { private function f(): void {} }",
1529            "foo.php",
1530            |metric| insta::assert_json_snapshot!(metric.npm),
1531        );
1532    }
1533
1534    #[test]
1535    fn php_one_protected_method() {
1536        check_metrics::<PhpParser>(
1537            "<?php class A { protected function f(): void {} }",
1538            "foo.php",
1539            |metric| insta::assert_json_snapshot!(metric.npm),
1540        );
1541    }
1542
1543    #[test]
1544    fn php_mixed_visibility_methods() {
1545        check_metrics::<PhpParser>(
1546            "<?php
1547            class A {
1548                public function a(): void {}
1549                public function b(): void {}
1550                private function c(): void {}
1551                protected function d(): void {}
1552            }",
1553            "foo.php",
1554            |metric| insta::assert_json_snapshot!(metric.npm),
1555        );
1556    }
1557
1558    #[test]
1559    fn php_static_public_method() {
1560        check_metrics::<PhpParser>(
1561            "<?php class A { public static function f(): void {} }",
1562            "foo.php",
1563            |metric| insta::assert_json_snapshot!(metric.npm),
1564        );
1565    }
1566
1567    #[test]
1568    fn php_abstract_method() {
1569        check_metrics::<PhpParser>(
1570            "<?php abstract class A { abstract public function f(): void; }",
1571            "foo.php",
1572            |metric| insta::assert_json_snapshot!(metric.npm),
1573        );
1574    }
1575
1576    #[test]
1577    fn php_final_public_method() {
1578        check_metrics::<PhpParser>(
1579            "<?php class A { final public function f(): void {} }",
1580            "foo.php",
1581            |metric| insta::assert_json_snapshot!(metric.npm),
1582        );
1583    }
1584
1585    #[test]
1586    fn php_interface_methods() {
1587        // Interface methods are implicitly public.
1588        check_metrics::<PhpParser>(
1589            "<?php
1590            interface I {
1591                public function a(): void;
1592                public function b(): int;
1593            }",
1594            "foo.php",
1595            |metric| insta::assert_json_snapshot!(metric.npm),
1596        );
1597    }
1598
1599    #[test]
1600    fn php_enum_methods() {
1601        // Enum can declare public methods (PHP 8.1+).
1602        check_metrics::<PhpParser>(
1603            "<?php
1604            enum Color {
1605                case Red;
1606                case Green;
1607                public function label(): string {
1608                    return match ($this) {
1609                        Color::Red => 'r',
1610                        Color::Green => 'g',
1611                    };
1612                }
1613            }",
1614            "foo.php",
1615            |metric| insta::assert_json_snapshot!(metric.npm),
1616        );
1617    }
1618
1619    #[test]
1620    fn php_trait_methods() {
1621        check_metrics::<PhpParser>(
1622            "<?php
1623            trait T {
1624                public function a(): void {}
1625                private function b(): void {}
1626            }",
1627            "foo.php",
1628            |metric| insta::assert_json_snapshot!(metric.npm),
1629        );
1630    }
1631
1632    #[test]
1633    fn php_no_explicit_visibility_method_excluded() {
1634        // Methods without explicit visibility (which PHP treats as public)
1635        // are NOT counted under the strict-explicit rule.
1636        check_metrics::<PhpParser>(
1637            "<?php class A { function f(): void {} }",
1638            "foo.php",
1639            |metric| insta::assert_json_snapshot!(metric.npm),
1640        );
1641    }
1642
1643    // --- Kotlin NPM tests -------------------------------------------------
1644
1645    #[test]
1646    fn kotlin_empty_class_no_methods() {
1647        check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
1648            assert_eq!(metric.npm.class_npm_sum(), 0);
1649            assert_eq!(metric.npm.class_nm_sum(), 0);
1650            assert_eq!(metric.npm.interface_nm_sum(), 0);
1651            insta::assert_json_snapshot!(metric.npm);
1652        });
1653    }
1654
1655    #[test]
1656    fn kotlin_public_methods_default() {
1657        // Kotlin default visibility is public — no modifier means public.
1658        check_metrics::<KotlinParser>(
1659            "class C {
1660                fun a() {}
1661                fun b(): Int = 0
1662                fun c(x: Int): Int = x
1663            }",
1664            "foo.kt",
1665            |metric| {
1666                assert_eq!(metric.npm.class_npm_sum(), 3);
1667                assert_eq!(metric.npm.class_nm_sum(), 3);
1668                insta::assert_json_snapshot!(metric.npm);
1669            },
1670        );
1671    }
1672
1673    #[test]
1674    fn kotlin_private_method() {
1675        check_metrics::<KotlinParser>(
1676            "class C {
1677                fun a() {}                  // public
1678                private fun b() {}          // private
1679                fun c() {}                  // public
1680            }",
1681            "foo.kt",
1682            |metric| {
1683                assert_eq!(metric.npm.class_npm_sum(), 2);
1684                assert_eq!(metric.npm.class_nm_sum(), 3);
1685                insta::assert_json_snapshot!(metric.npm);
1686            },
1687        );
1688    }
1689
1690    #[test]
1691    fn kotlin_protected_internal_methods() {
1692        check_metrics::<KotlinParser>(
1693            "open class C {
1694                protected fun a() {}
1695                internal fun b() {}
1696                public fun c() {}
1697            }",
1698            "foo.kt",
1699            |metric| {
1700                assert_eq!(metric.npm.class_npm_sum(), 1);
1701                assert_eq!(metric.npm.class_nm_sum(), 3);
1702                insta::assert_json_snapshot!(metric.npm);
1703            },
1704        );
1705    }
1706
1707    #[test]
1708    fn kotlin_secondary_constructor_counts() {
1709        // Secondary constructors are explicit `secondary_constructor`
1710        // nodes; they count as methods (matching the Java rule).
1711        check_metrics::<KotlinParser>(
1712            "class C {
1713                private var a: Int = 0
1714                constructor(n: Int) { a = n }
1715                constructor(n: Int, m: Int) { a = n + m }
1716                fun get(): Int = a
1717            }",
1718            "foo.kt",
1719            |metric| {
1720                assert_eq!(metric.npm.class_npm_sum(), 3);
1721                assert_eq!(metric.npm.class_nm_sum(), 3);
1722                insta::assert_json_snapshot!(metric.npm);
1723            },
1724        );
1725    }
1726
1727    #[test]
1728    fn kotlin_companion_object_methods() {
1729        // Companion object methods fold into the enclosing class (static
1730        // members).
1731        check_metrics::<KotlinParser>(
1732            "class Holder {
1733                fun memberFn() {}
1734                companion object {
1735                    fun staticFn() {}
1736                    private fun secret() {}
1737                }
1738            }",
1739            "foo.kt",
1740            |metric| {
1741                assert_eq!(metric.npm.class_npm_sum(), 2);
1742                assert_eq!(metric.npm.class_nm_sum(), 3);
1743                insta::assert_json_snapshot!(metric.npm);
1744            },
1745        );
1746    }
1747
1748    #[test]
1749    fn kotlin_data_class_methods() {
1750        // `data class` compiler-generated members are NOT counted —
1751        // only user-written `fun` declarations.
1752        check_metrics::<KotlinParser>(
1753            "data class Point(val x: Int, val y: Int) {
1754                fun manhattan(): Int = x + y
1755                private fun internal_(): Int = 0
1756            }",
1757            "foo.kt",
1758            |metric| {
1759                assert_eq!(metric.npm.class_npm_sum(), 1);
1760                assert_eq!(metric.npm.class_nm_sum(), 2);
1761                insta::assert_json_snapshot!(metric.npm);
1762            },
1763        );
1764    }
1765
1766    #[test]
1767    fn kotlin_object_singleton_methods() {
1768        check_metrics::<KotlinParser>(
1769            "object Util {
1770                fun add(a: Int, b: Int): Int = a + b
1771                private fun helper(): Int = 0
1772            }",
1773            "foo.kt",
1774            |metric| {
1775                assert_eq!(metric.npm.class_npm_sum(), 1);
1776                assert_eq!(metric.npm.class_nm_sum(), 2);
1777                insta::assert_json_snapshot!(metric.npm);
1778            },
1779        );
1780    }
1781
1782    #[test]
1783    fn kotlin_interface_methods() {
1784        check_func_space::<KotlinParser, _>(
1785            "interface I {
1786                fun work(): Int
1787                fun describe(): String
1788            }",
1789            "foo.kt",
1790            |func_space| {
1791                let metric = &func_space.metrics;
1792                assert_eq!(metric.npm.interface_npm_sum(), 2);
1793                assert_eq!(metric.npm.interface_nm_sum(), 2);
1794                assert_eq!(metric.npm.class_nm_sum(), 0);
1795                insta::assert_json_snapshot!(metric.npm);
1796                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
1797            },
1798        );
1799    }
1800
1801    #[test]
1802    fn kotlin_interface_with_default_method() {
1803        check_func_space::<KotlinParser, _>(
1804            "interface I {
1805                fun abs(n: Int): Int {
1806                    return if (n < 0) -n else n
1807                }
1808                fun pure(): Int
1809            }",
1810            "foo.kt",
1811            |func_space| {
1812                let metric = &func_space.metrics;
1813                assert_eq!(metric.npm.interface_npm_sum(), 2);
1814                assert_eq!(metric.npm.interface_nm_sum(), 2);
1815                insta::assert_json_snapshot!(metric.npm);
1816                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
1817            },
1818        );
1819    }
1820
1821    #[test]
1822    fn kotlin_override_fun_counts() {
1823        check_metrics::<KotlinParser>(
1824            "open class Base {
1825                open fun greet(): String = \"hi\"
1826            }
1827            class Sub : Base() {
1828                override fun greet(): String = \"yo\"
1829                private fun secret() {}
1830            }",
1831            "foo.kt",
1832            |metric| {
1833                // Base: 1 method (public).
1834                // Sub: 2 methods — override (public, no visibility modifier
1835                //   so default public) + private secret.
1836                assert_eq!(metric.npm.class_npm_sum(), 2);
1837                assert_eq!(metric.npm.class_nm_sum(), 3);
1838                insta::assert_json_snapshot!(metric.npm);
1839            },
1840        );
1841    }
1842
1843    #[test]
1844    fn kotlin_nested_class_methods() {
1845        check_metrics::<KotlinParser>(
1846            "class Outer {
1847                fun outerM() {}
1848                class Nested {
1849                    fun nestedM() {}
1850                    private fun nestedSecret() {}
1851                }
1852            }",
1853            "foo.kt",
1854            |metric| {
1855                assert_eq!(metric.npm.class_npm_sum(), 2);
1856                assert_eq!(metric.npm.class_nm_sum(), 3);
1857                insta::assert_json_snapshot!(metric.npm);
1858            },
1859        );
1860    }
1861
1862    #[test]
1863    fn kotlin_inner_class_methods() {
1864        check_metrics::<KotlinParser>(
1865            "class Outer {
1866                fun outerM() {}
1867                inner class Inner {
1868                    fun innerM() {}
1869                }
1870            }",
1871            "foo.kt",
1872            |metric| {
1873                assert_eq!(metric.npm.class_npm_sum(), 2);
1874                assert_eq!(metric.npm.class_nm_sum(), 2);
1875                insta::assert_json_snapshot!(metric.npm);
1876            },
1877        );
1878    }
1879
1880    #[test]
1881    fn kotlin_top_level_function_excluded() {
1882        // Top-level `fun` belongs to `Unit`, not any class.
1883        check_metrics::<KotlinParser>(
1884            "fun freeFn() {}
1885class C {
1886    fun m() {}
1887}",
1888            "foo.kt",
1889            |metric| {
1890                assert_eq!(metric.npm.class_npm_sum(), 1);
1891                assert_eq!(metric.npm.class_nm_sum(), 1);
1892                insta::assert_json_snapshot!(metric.npm);
1893            },
1894        );
1895    }
1896
1897    #[test]
1898    fn kotlin_extension_function_excluded() {
1899        // Extension functions parse as top-level `function_declaration`
1900        // with a receiver-type prefix; they belong to the `Unit` space.
1901        check_metrics::<KotlinParser>(
1902            "fun List<Int>.sum2(): Int = this.size
1903class C {
1904    fun m() {}
1905}",
1906            "foo.kt",
1907            |metric| {
1908                assert_eq!(metric.npm.class_npm_sum(), 1);
1909                assert_eq!(metric.npm.class_nm_sum(), 1);
1910                insta::assert_json_snapshot!(metric.npm);
1911            },
1912        );
1913    }
1914
1915    #[test]
1916    fn kotlin_class_in_interface() {
1917        // Interface with nested class — methods count to the right
1918        // bucket. Structural `assert_child_space_kind` guards both
1919        // the outer interface and the nested class against
1920        // `is_func_space` reverts (see #311).
1921        check_func_space::<KotlinParser, _>(
1922            "interface Outer {
1923                fun work(): Int
1924                class Helper {
1925                    fun help() {}
1926                }
1927            }",
1928            "foo.kt",
1929            |func_space| {
1930                let metric = &func_space.metrics;
1931                assert_eq!(metric.npm.interface_npm_sum(), 1);
1932                assert_eq!(metric.npm.class_npm_sum(), 1);
1933                insta::assert_json_snapshot!(metric.npm);
1934                assert_child_space_kind(&func_space, "Outer", SpaceKind::Interface);
1935                let outer = func_space
1936                    .spaces
1937                    .iter()
1938                    .find(|s| s.name.as_deref() == Some("Outer"))
1939                    .expect("Outer FuncSpace");
1940                assert_child_space_kind(outer, "Helper", SpaceKind::Class);
1941            },
1942        );
1943    }
1944
1945    #[test]
1946    fn kotlin_interface_in_class() {
1947        // Class with nested interface — methods count to the right
1948        // bucket. Structural `assert_child_space_kind` guards both
1949        // the outer class and the nested interface against
1950        // `is_func_space` reverts (see #311).
1951        check_func_space::<KotlinParser, _>(
1952            "class Outer {
1953                fun work() {}
1954                interface Sub {
1955                    fun help(): Int
1956                }
1957            }",
1958            "foo.kt",
1959            |func_space| {
1960                let metric = &func_space.metrics;
1961                assert_eq!(metric.npm.class_npm_sum(), 1);
1962                assert_eq!(metric.npm.interface_npm_sum(), 1);
1963                insta::assert_json_snapshot!(metric.npm);
1964                assert_child_space_kind(&func_space, "Outer", SpaceKind::Class);
1965                let outer = func_space
1966                    .spaces
1967                    .iter()
1968                    .find(|s| s.name.as_deref() == Some("Outer"))
1969                    .expect("Outer FuncSpace");
1970                assert_child_space_kind(outer, "Sub", SpaceKind::Interface);
1971            },
1972        );
1973    }
1974
1975    #[test]
1976    fn kotlin_init_block_not_a_method() {
1977        // `init` blocks are anonymous initializers — they are not
1978        // function declarations and don't count toward `nm`/`npm`.
1979        check_metrics::<KotlinParser>(
1980            "class C(val n: Int) {
1981                init { require(n >= 0) }
1982                fun get(): Int = n
1983            }",
1984            "foo.kt",
1985            |metric| {
1986                assert_eq!(metric.npm.class_npm_sum(), 1);
1987                assert_eq!(metric.npm.class_nm_sum(), 1);
1988                insta::assert_json_snapshot!(metric.npm);
1989            },
1990        );
1991    }
1992
1993    // --- TypeScript / TSX NPM tests --------------------------------------
1994    //
1995    // TypeScript class methods are `method_definition` direct children of
1996    // `class_body` (regular methods, static methods, constructors,
1997    // getters, setters). Each `method_definition` counts once.
1998    // `abstract_method_signature` (abstract method declaration with no
1999    // body) is also counted. A `public_field_definition` whose value is
2000    // an `arrow_function` is a class method written as a field
2001    // initializer and counts once. Method overload signatures
2002    // (`method_signature` as class_body children) are NOT counted —
2003    // the implementation `method_definition` is the canonical method.
2004    // Interface methods (`method_signature`, `abstract_method_signature`,
2005    // `construct_signature`) count as implicitly-public interface
2006    // methods.
2007
2008    #[test]
2009    fn typescript_empty_class_no_methods() {
2010        check_metrics::<TypescriptParser>("class C {}", "foo.ts", |metric| {
2011            assert_eq!(metric.npm.class_npm_sum(), 0);
2012            assert_eq!(metric.npm.class_nm_sum(), 0);
2013            insta::assert_json_snapshot!(metric.npm);
2014        });
2015    }
2016
2017    #[test]
2018    fn typescript_default_public_methods() {
2019        check_metrics::<TypescriptParser>(
2020            "class C {
2021                a(): void {}
2022                b(): number { return 0; }
2023                c(x: number): number { return x; }
2024            }",
2025            "foo.ts",
2026            |metric| {
2027                assert_eq!(metric.npm.class_npm_sum(), 3);
2028                assert_eq!(metric.npm.class_nm_sum(), 3);
2029                insta::assert_json_snapshot!(metric.npm);
2030            },
2031        );
2032    }
2033
2034    #[test]
2035    fn typescript_method_visibility() {
2036        check_metrics::<TypescriptParser>(
2037            "class C {
2038                public a(): void {}
2039                private b(): void {}
2040                protected c(): void {}
2041                d(): void {}
2042            }",
2043            "foo.ts",
2044            |metric| {
2045                // public + default-public = 2 npm; 4 nm.
2046                assert_eq!(metric.npm.class_npm_sum(), 2);
2047                assert_eq!(metric.npm.class_nm_sum(), 4);
2048                insta::assert_json_snapshot!(metric.npm);
2049            },
2050        );
2051    }
2052
2053    #[test]
2054    fn typescript_static_methods() {
2055        check_metrics::<TypescriptParser>(
2056            "class C {
2057                static a(): void {}
2058                public static b(): void {}
2059                private static c(): void {}
2060            }",
2061            "foo.ts",
2062            |metric| {
2063                // a (default public) + b (public) = 2 npm.
2064                assert_eq!(metric.npm.class_npm_sum(), 2);
2065                assert_eq!(metric.npm.class_nm_sum(), 3);
2066                insta::assert_json_snapshot!(metric.npm);
2067            },
2068        );
2069    }
2070
2071    #[test]
2072    fn typescript_constructor_counts_as_method() {
2073        // The constructor is a `method_definition` — one method.
2074        check_metrics::<TypescriptParser>(
2075            "class C {
2076                constructor(public x: number) {}
2077                m(): void {}
2078            }",
2079            "foo.ts",
2080            |metric| {
2081                assert_eq!(metric.npm.class_npm_sum(), 2);
2082                assert_eq!(metric.npm.class_nm_sum(), 2);
2083                insta::assert_json_snapshot!(metric.npm);
2084            },
2085        );
2086    }
2087
2088    #[test]
2089    fn typescript_getter_setter_each_count_once() {
2090        // `get x()` and `set x(v)` are distinct `method_definition`
2091        // nodes — each counts as one method.
2092        check_metrics::<TypescriptParser>(
2093            "class C {
2094                private _x: number = 0;
2095                get x(): number { return this._x; }
2096                set x(v: number) { this._x = v; }
2097            }",
2098            "foo.ts",
2099            |metric| {
2100                assert_eq!(metric.npm.class_npm_sum(), 2);
2101                assert_eq!(metric.npm.class_nm_sum(), 2);
2102                insta::assert_json_snapshot!(metric.npm);
2103            },
2104        );
2105    }
2106
2107    #[test]
2108    fn typescript_arrow_field_counts_as_method() {
2109        // `foo = () => {}` is a class method.
2110        check_metrics::<TypescriptParser>(
2111            "class C {
2112                a: number = 0;
2113                arrow = () => this.a;
2114                private secret = () => this.a;
2115            }",
2116            "foo.ts",
2117            |metric| {
2118                // 2 methods (arrow public, secret private). 1 field.
2119                assert_eq!(metric.npm.class_npm_sum(), 1);
2120                assert_eq!(metric.npm.class_nm_sum(), 2);
2121                insta::assert_json_snapshot!(metric.npm);
2122            },
2123        );
2124    }
2125
2126    #[test]
2127    fn typescript_method_overload_counts_once() {
2128        // Only the implementation `method_definition` counts; the two
2129        // signature-only `method_signature` overloads do not.
2130        check_metrics::<TypescriptParser>(
2131            "class C {
2132                m(x: number): void;
2133                m(x: string): void;
2134                m(x: any): void {}
2135            }",
2136            "foo.ts",
2137            |metric| {
2138                assert_eq!(metric.npm.class_npm_sum(), 1);
2139                assert_eq!(metric.npm.class_nm_sum(), 1);
2140                insta::assert_json_snapshot!(metric.npm);
2141            },
2142        );
2143    }
2144
2145    #[test]
2146    fn typescript_abstract_class_methods() {
2147        // Abstract method signatures count; concrete methods count; both
2148        // contribute to `nm`. `public` abstract method is public.
2149        check_metrics::<TypescriptParser>(
2150            "abstract class C {
2151                abstract a(): void;
2152                public abstract b(): number;
2153                protected abstract c(): void;
2154                public m(): void {}
2155                private n(): void {}
2156            }",
2157            "foo.ts",
2158            |metric| {
2159                // a (default public abstract), b (public), m (public) = 3 npm.
2160                // c (protected), n (private) demoted. Total nm = 5.
2161                assert_eq!(metric.npm.class_npm_sum(), 3);
2162                assert_eq!(metric.npm.class_nm_sum(), 5);
2163                insta::assert_json_snapshot!(metric.npm);
2164            },
2165        );
2166    }
2167
2168    #[test]
2169    fn typescript_interface_methods() {
2170        // Interface method signatures are implicitly public.
2171        check_func_space::<TypescriptParser, _>(
2172            "interface I {
2173                a(): void;
2174                b(x: number): number;
2175                c: string;
2176            }",
2177            "foo.ts",
2178            |func_space| {
2179                let metric = &func_space.metrics;
2180                assert_eq!(metric.npm.interface_npm_sum(), 2);
2181                assert_eq!(metric.npm.interface_nm_sum(), 2);
2182                assert_eq!(metric.npm.class_nm_sum(), 0);
2183                insta::assert_json_snapshot!(metric.npm);
2184                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
2185            },
2186        );
2187    }
2188
2189    #[test]
2190    fn typescript_generic_class_methods() {
2191        check_metrics::<TypescriptParser>(
2192            "class Box<T> {
2193                value: T;
2194                set(v: T): void { this.value = v; }
2195                get(): T { return this.value; }
2196            }",
2197            "foo.ts",
2198            |metric| {
2199                assert_eq!(metric.npm.class_npm_sum(), 2);
2200                assert_eq!(metric.npm.class_nm_sum(), 2);
2201                insta::assert_json_snapshot!(metric.npm);
2202            },
2203        );
2204    }
2205
2206    #[test]
2207    fn typescript_multiple_classes_and_interface() {
2208        check_func_space::<TypescriptParser, _>(
2209            "class A { m(): void {} }
2210             class B { private h(): void {} }
2211             interface I { p(): number; }",
2212            "foo.ts",
2213            |func_space| {
2214                let metric = &func_space.metrics;
2215                assert_eq!(metric.npm.class_npm_sum(), 1);
2216                assert_eq!(metric.npm.class_nm_sum(), 2);
2217                assert_eq!(metric.npm.interface_npm_sum(), 1);
2218                assert_eq!(metric.npm.interface_nm_sum(), 1);
2219                insta::assert_json_snapshot!(metric.npm);
2220                assert_child_space_kind(&func_space, "A", SpaceKind::Class);
2221                assert_child_space_kind(&func_space, "B", SpaceKind::Class);
2222                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
2223            },
2224        );
2225    }
2226
2227    // TSX parity
2228
2229    #[test]
2230    fn tsx_empty_class_no_methods() {
2231        check_metrics::<TsxParser>("class C {}", "foo.tsx", |metric| {
2232            assert_eq!(metric.npm.class_npm_sum(), 0);
2233            assert_eq!(metric.npm.class_nm_sum(), 0);
2234            insta::assert_json_snapshot!(metric.npm);
2235        });
2236    }
2237
2238    #[test]
2239    fn tsx_default_public_methods() {
2240        check_metrics::<TsxParser>(
2241            "class C {
2242                a(): void {}
2243                b(): number { return 0; }
2244            }",
2245            "foo.tsx",
2246            |metric| {
2247                assert_eq!(metric.npm.class_npm_sum(), 2);
2248                assert_eq!(metric.npm.class_nm_sum(), 2);
2249                insta::assert_json_snapshot!(metric.npm);
2250            },
2251        );
2252    }
2253
2254    #[test]
2255    fn tsx_method_visibility() {
2256        check_metrics::<TsxParser>(
2257            "class C {
2258                public a(): void {}
2259                private b(): void {}
2260                protected c(): void {}
2261            }",
2262            "foo.tsx",
2263            |metric| {
2264                assert_eq!(metric.npm.class_npm_sum(), 1);
2265                assert_eq!(metric.npm.class_nm_sum(), 3);
2266                insta::assert_json_snapshot!(metric.npm);
2267            },
2268        );
2269    }
2270
2271    #[test]
2272    fn tsx_static_methods() {
2273        check_metrics::<TsxParser>(
2274            "class C {
2275                static a(): void {}
2276                private static b(): void {}
2277            }",
2278            "foo.tsx",
2279            |metric| {
2280                assert_eq!(metric.npm.class_npm_sum(), 1);
2281                assert_eq!(metric.npm.class_nm_sum(), 2);
2282                insta::assert_json_snapshot!(metric.npm);
2283            },
2284        );
2285    }
2286
2287    #[test]
2288    fn tsx_constructor_counts_as_method() {
2289        check_metrics::<TsxParser>(
2290            "class C {
2291                constructor() {}
2292                m(): void {}
2293            }",
2294            "foo.tsx",
2295            |metric| {
2296                assert_eq!(metric.npm.class_npm_sum(), 2);
2297                assert_eq!(metric.npm.class_nm_sum(), 2);
2298                insta::assert_json_snapshot!(metric.npm);
2299            },
2300        );
2301    }
2302
2303    #[test]
2304    fn tsx_getter_setter_each_count_once() {
2305        check_metrics::<TsxParser>(
2306            "class C {
2307                private _x: number = 0;
2308                get x(): number { return this._x; }
2309                set x(v: number) { this._x = v; }
2310            }",
2311            "foo.tsx",
2312            |metric| {
2313                assert_eq!(metric.npm.class_npm_sum(), 2);
2314                assert_eq!(metric.npm.class_nm_sum(), 2);
2315                insta::assert_json_snapshot!(metric.npm);
2316            },
2317        );
2318    }
2319
2320    #[test]
2321    fn tsx_arrow_field_counts_as_method() {
2322        check_metrics::<TsxParser>(
2323            "class C {
2324                arrow = () => 1;
2325                private secret = () => 2;
2326            }",
2327            "foo.tsx",
2328            |metric| {
2329                assert_eq!(metric.npm.class_npm_sum(), 1);
2330                assert_eq!(metric.npm.class_nm_sum(), 2);
2331                insta::assert_json_snapshot!(metric.npm);
2332            },
2333        );
2334    }
2335
2336    #[test]
2337    fn tsx_method_overload_counts_once() {
2338        check_metrics::<TsxParser>(
2339            "class C {
2340                m(x: number): void;
2341                m(x: string): void;
2342                m(x: any): void {}
2343            }",
2344            "foo.tsx",
2345            |metric| {
2346                assert_eq!(metric.npm.class_npm_sum(), 1);
2347                assert_eq!(metric.npm.class_nm_sum(), 1);
2348                insta::assert_json_snapshot!(metric.npm);
2349            },
2350        );
2351    }
2352
2353    #[test]
2354    fn tsx_abstract_class_methods() {
2355        check_metrics::<TsxParser>(
2356            "abstract class C {
2357                abstract a(): void;
2358                public m(): void {}
2359                private n(): void {}
2360            }",
2361            "foo.tsx",
2362            |metric| {
2363                // a (default public) + m (public) = 2 npm; 3 nm.
2364                assert_eq!(metric.npm.class_npm_sum(), 2);
2365                assert_eq!(metric.npm.class_nm_sum(), 3);
2366                insta::assert_json_snapshot!(metric.npm);
2367            },
2368        );
2369    }
2370
2371    #[test]
2372    fn tsx_interface_methods() {
2373        check_func_space::<TsxParser, _>(
2374            "interface I {
2375                a(): void;
2376                b(): number;
2377            }",
2378            "foo.tsx",
2379            |func_space| {
2380                let metric = &func_space.metrics;
2381                assert_eq!(metric.npm.interface_npm_sum(), 2);
2382                assert_eq!(metric.npm.interface_nm_sum(), 2);
2383                insta::assert_json_snapshot!(metric.npm);
2384                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
2385            },
2386        );
2387    }
2388
2389    #[test]
2390    fn tsx_generic_class_methods() {
2391        check_metrics::<TsxParser>(
2392            "class Box<T> { value: T; set(v: T): void { this.value = v; } }",
2393            "foo.tsx",
2394            |metric| {
2395                assert_eq!(metric.npm.class_npm_sum(), 1);
2396                assert_eq!(metric.npm.class_nm_sum(), 1);
2397                insta::assert_json_snapshot!(metric.npm);
2398            },
2399        );
2400    }
2401
2402    #[test]
2403    fn tsx_multiple_classes_and_interface() {
2404        check_func_space::<TsxParser, _>(
2405            "class A { m(): void {} }
2406             class B { private h(): void {} }
2407             interface I { p(): number; }",
2408            "foo.tsx",
2409            |func_space| {
2410                let metric = &func_space.metrics;
2411                assert_eq!(metric.npm.class_npm_sum(), 1);
2412                assert_eq!(metric.npm.class_nm_sum(), 2);
2413                assert_eq!(metric.npm.interface_npm_sum(), 1);
2414                assert_eq!(metric.npm.interface_nm_sum(), 1);
2415                insta::assert_json_snapshot!(metric.npm);
2416                assert_child_space_kind(&func_space, "A", SpaceKind::Class);
2417                assert_child_space_kind(&func_space, "B", SpaceKind::Class);
2418                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
2419            },
2420        );
2421    }
2422
2423    // --- Ruby NPM tests ---------------------------------------------------
2424    //
2425    // Ruby methods default to public. Visibility keywords (`private`,
2426    // `public`, `protected`) appear as bare `identifier` nodes in the
2427    // class body and flip the default for every subsequent declaration.
2428    // The argument-form (`private :foo`, `private def x`) is a `call`
2429    // node and does NOT change the body-wide flag.
2430
2431    #[test]
2432    fn ruby_no_class_methods() {
2433        check_metrics::<RubyParser>("def foo\n  1\nend\n", "foo.rb", |metric| {
2434            assert_eq!(metric.npm.class_npm_sum(), 0);
2435            assert_eq!(metric.npm.class_nm_sum(), 0);
2436            insta::assert_json_snapshot!(metric.npm);
2437        });
2438    }
2439
2440    #[test]
2441    fn ruby_one_public_method() {
2442        // No visibility keyword → default public.
2443        check_metrics::<RubyParser>(
2444            "class A\n  def f\n    1\n  end\nend\n",
2445            "foo.rb",
2446            |metric| {
2447                assert_eq!(metric.npm.class_npm_sum(), 1);
2448                assert_eq!(metric.npm.class_nm_sum(), 1);
2449                insta::assert_json_snapshot!(metric.npm);
2450            },
2451        );
2452    }
2453
2454    #[test]
2455    fn ruby_one_private_method() {
2456        // Bare `private` flips visibility for `f`.
2457        check_metrics::<RubyParser>(
2458            "class A\n  private\n  def f\n    1\n  end\nend\n",
2459            "foo.rb",
2460            |metric| {
2461                assert_eq!(metric.npm.class_npm_sum(), 0);
2462                assert_eq!(metric.npm.class_nm_sum(), 1);
2463                insta::assert_json_snapshot!(metric.npm);
2464            },
2465        );
2466    }
2467
2468    #[test]
2469    fn ruby_one_protected_method() {
2470        check_metrics::<RubyParser>(
2471            "class A\n  protected\n  def f\n    1\n  end\nend\n",
2472            "foo.rb",
2473            |metric| {
2474                assert_eq!(metric.npm.class_npm_sum(), 0);
2475                assert_eq!(metric.npm.class_nm_sum(), 1);
2476                insta::assert_json_snapshot!(metric.npm);
2477            },
2478        );
2479    }
2480
2481    #[test]
2482    fn ruby_mixed_visibility_methods() {
2483        // `a` is public (default). `b` is private. `c` is public again
2484        // because the explicit `public` keyword resets the flag. `d` is
2485        // protected.
2486        check_metrics::<RubyParser>(
2487            "class A\n  def a\n    1\n  end\n  private\n  def b\n    1\n  end\n  public\n  def c\n    1\n  end\n  protected\n  def d\n    1\n  end\nend\n",
2488            "foo.rb",
2489            |metric| {
2490                assert_eq!(metric.npm.class_npm_sum(), 2);
2491                assert_eq!(metric.npm.class_nm_sum(), 4);
2492                insta::assert_json_snapshot!(metric.npm);
2493            },
2494        );
2495    }
2496
2497    #[test]
2498    fn ruby_singleton_method_is_counted() {
2499        // `def self.x` and plain `def x` both count; default is public.
2500        check_metrics::<RubyParser>(
2501            "class A\n  def self.f\n    1\n  end\n  def g\n    1\n  end\nend\n",
2502            "foo.rb",
2503            |metric| {
2504                assert_eq!(metric.npm.class_npm_sum(), 2);
2505                assert_eq!(metric.npm.class_nm_sum(), 2);
2506                insta::assert_json_snapshot!(metric.npm);
2507            },
2508        );
2509    }
2510
2511    #[test]
2512    fn ruby_singleton_class_methods() {
2513        // `class << self` opens a separate class space whose methods
2514        // count there. Outer class A has 0 methods.
2515        check_metrics::<RubyParser>(
2516            "class A\n  class << self\n    def s\n      1\n    end\n    def t\n      2\n    end\n  end\nend\n",
2517            "foo.rb",
2518            |metric| {
2519                assert_eq!(metric.npm.class_npm_sum(), 2);
2520                assert_eq!(metric.npm.class_nm_sum(), 2);
2521                insta::assert_json_snapshot!(metric.npm);
2522            },
2523        );
2524    }
2525
2526    #[test]
2527    fn ruby_argument_form_visibility_does_not_flip() {
2528        // `private :y` is a `call` node (argument form). It does NOT
2529        // change the body-wide visibility, so `z` declared after it
2530        // remains public.
2531        check_metrics::<RubyParser>(
2532            "class A\n  def y\n    1\n  end\n  private :y\n  def z\n    1\n  end\nend\n",
2533            "foo.rb",
2534            |metric| {
2535                assert_eq!(metric.npm.class_npm_sum(), 2);
2536                assert_eq!(metric.npm.class_nm_sum(), 2);
2537                insta::assert_json_snapshot!(metric.npm);
2538            },
2539        );
2540    }
2541
2542    #[test]
2543    fn ruby_multiple_classes() {
2544        check_metrics::<RubyParser>(
2545            "class A\n  def a\n    1\n  end\nend\nclass B\n  private\n  def b\n    1\n  end\n  def c\n    1\n  end\nend\n",
2546            "foo.rb",
2547            |metric| {
2548                // A: 1 public method. B: 0 public, 2 total. Sum = 1/3.
2549                assert_eq!(metric.npm.class_npm_sum(), 1);
2550                assert_eq!(metric.npm.class_nm_sum(), 3);
2551                insta::assert_json_snapshot!(metric.npm);
2552            },
2553        );
2554    }
2555
2556    #[test]
2557    fn ruby_module_methods_not_counted() {
2558        // `Module` is `Namespace`, not `Class` — its methods do not
2559        // contribute to NPM.
2560        check_metrics::<RubyParser>(
2561            "module M\n  def f\n    1\n  end\n  def g\n    1\n  end\nend\n",
2562            "foo.rb",
2563            |metric| {
2564                assert_eq!(metric.npm.class_npm_sum(), 0);
2565                assert_eq!(metric.npm.class_nm_sum(), 0);
2566                insta::assert_json_snapshot!(metric.npm);
2567            },
2568        );
2569    }
2570
2571    #[test]
2572    fn ruby_class_with_inheritance() {
2573        // Inheritance does not change method counts.
2574        check_metrics::<RubyParser>(
2575            "class A < B\n  def f\n    1\n  end\n  def g\n    1\n  end\nend\n",
2576            "foo.rb",
2577            |metric| {
2578                assert_eq!(metric.npm.class_npm_sum(), 2);
2579                assert_eq!(metric.npm.class_nm_sum(), 2);
2580                insta::assert_json_snapshot!(metric.npm);
2581            },
2582        );
2583    }
2584
2585    #[test]
2586    fn ruby_visibility_resets_between_classes() {
2587        // Each class body starts in default-public state regardless of
2588        // the previous body's trailing visibility.
2589        check_metrics::<RubyParser>(
2590            "class A\n  private\n  def a\n    1\n  end\nend\nclass B\n  def b\n    1\n  end\nend\n",
2591            "foo.rb",
2592            |metric| {
2593                // A: 0 public, B: 1 public.
2594                assert_eq!(metric.npm.class_npm_sum(), 1);
2595                assert_eq!(metric.npm.class_nm_sum(), 2);
2596                insta::assert_json_snapshot!(metric.npm);
2597            },
2598        );
2599    }
2600
2601    #[test]
2602    fn ruby_empty_class_no_methods() {
2603        check_metrics::<RubyParser>("class Empty\nend\n", "foo.rb", |metric| {
2604            assert_eq!(metric.npm.class_npm_sum(), 0);
2605            assert_eq!(metric.npm.class_nm_sum(), 0);
2606            insta::assert_json_snapshot!(metric.npm);
2607        });
2608    }
2609
2610    // ---------------------------------------------------------------
2611    // Default-impl placeholder smoke tests (audited in #188).
2612    //
2613    // Each test feeds a class / struct with public methods to a
2614    // language whose `Npm` is currently the default no-op. The
2615    // assertion pins the current 0 value with a TODO pointing at the
2616    // follow-up issue — when the real impl lands the assertion will
2617    // fire and force a test update.
2618    // ---------------------------------------------------------------
2619
2620    // --- Python NPM ---------------------------------------------------
2621
2622    #[test]
2623    fn python_empty_class_no_methods() {
2624        check_metrics::<PythonParser>("class C:\n    pass\n", "foo.py", |metric| {
2625            assert_eq!(metric.npm.class_nm_sum(), 0);
2626            assert_eq!(metric.npm.class_npm_sum(), 0);
2627            insta::assert_json_snapshot!(metric.npm);
2628        });
2629    }
2630
2631    #[test]
2632    fn python_class_methods_count() {
2633        // 3 `def`s inside the class body → 3 methods, all public.
2634        check_metrics::<PythonParser>(
2635            "class C:\n\
2636             \x20   def __init__(self):\n\
2637             \x20       pass\n\
2638             \x20   def m(self):\n\
2639             \x20       pass\n\
2640             \x20   def n(self):\n\
2641             \x20       pass\n",
2642            "foo.py",
2643            |metric| {
2644                assert_eq!(metric.npm.class_nm_sum(), 3);
2645                assert_eq!(metric.npm.class_npm_sum(), 3);
2646                insta::assert_json_snapshot!(metric.npm);
2647            },
2648        );
2649    }
2650
2651    #[test]
2652    fn python_decorated_methods_count() {
2653        // `@property`, `@staticmethod`, `@classmethod`, custom
2654        // decorators all wrap a FunctionDefinition in
2655        // DecoratedDefinition. Each wrapper still counts as one method.
2656        check_metrics::<PythonParser>(
2657            "class C:\n\
2658             \x20   @property\n\
2659             \x20   def p(self):\n\
2660             \x20       return 1\n\
2661             \x20   @staticmethod\n\
2662             \x20   def s():\n\
2663             \x20       return 2\n\
2664             \x20   @classmethod\n\
2665             \x20   def c(cls):\n\
2666             \x20       return 3\n",
2667            "foo.py",
2668            |metric| {
2669                assert_eq!(metric.npm.class_nm_sum(), 3);
2670                insta::assert_json_snapshot!(metric.npm);
2671            },
2672        );
2673    }
2674
2675    #[test]
2676    fn python_async_method_counts() {
2677        // `async def m` parses as a FunctionDefinition with an Async
2678        // keyword child — still a method.
2679        check_metrics::<PythonParser>(
2680            "class C:\n    async def m(self):\n        return 1\n",
2681            "foo.py",
2682            |metric| {
2683                assert_eq!(metric.npm.class_nm_sum(), 1);
2684                insta::assert_json_snapshot!(metric.npm);
2685            },
2686        );
2687    }
2688
2689    #[test]
2690    fn python_nested_class_methods_independent() {
2691        // Outer.method belongs to Outer; Inner.inner_method belongs
2692        // to Inner; class_nm_sum aggregates across the file.
2693        check_metrics::<PythonParser>(
2694            "class Outer:\n\
2695             \x20   def method(self):\n\
2696             \x20       pass\n\
2697             \x20   class Inner:\n\
2698             \x20       def inner_method(self):\n\
2699             \x20           pass\n",
2700            "foo.py",
2701            |metric| {
2702                assert_eq!(metric.npm.class_nm_sum(), 2);
2703                insta::assert_json_snapshot!(metric.npm);
2704            },
2705        );
2706    }
2707
2708    #[test]
2709    fn python_module_level_function_is_not_method() {
2710        // `def f()` outside any class is a top-level function, not a
2711        // method.
2712        check_metrics::<PythonParser>(
2713            "def f():\n    pass\nclass C:\n    def m(self):\n        pass\n",
2714            "foo.py",
2715            |metric| {
2716                // Only `C.m` is a class method.
2717                assert_eq!(metric.npm.class_nm_sum(), 1);
2718                insta::assert_json_snapshot!(metric.npm);
2719            },
2720        );
2721    }
2722
2723    #[test]
2724    fn python_dunder_methods_count() {
2725        // `__init__`, `__repr__`, `__eq__` are dunder methods — public
2726        // by convention.
2727        check_metrics::<PythonParser>(
2728            "class C:\n\
2729             \x20   def __init__(self):\n\
2730             \x20       pass\n\
2731             \x20   def __repr__(self):\n\
2732             \x20       return 'C'\n\
2733             \x20   def __eq__(self, other):\n\
2734             \x20       return True\n",
2735            "foo.py",
2736            |metric| {
2737                assert_eq!(metric.npm.class_nm_sum(), 3);
2738                assert_eq!(metric.npm.class_npm_sum(), 3);
2739                insta::assert_json_snapshot!(metric.npm);
2740            },
2741        );
2742    }
2743
2744    #[test]
2745    fn rust_empty_unit_no_methods() {
2746        check_metrics::<RustParser>("", "empty.rs", |metric| {
2747            assert_eq!(metric.npm.class_nm_sum(), 0);
2748            assert_eq!(metric.npm.class_npm_sum(), 0);
2749            assert_eq!(metric.npm.interface_nm_sum(), 0);
2750            assert_eq!(metric.npm.interface_npm_sum(), 0);
2751            insta::assert_json_snapshot!(metric.npm);
2752        });
2753    }
2754
2755    #[test]
2756    fn rust_impl_methods_count() {
2757        // 3 `fn`s in `impl Foo` body. `pub new` and `pub process` are
2758        // public; `helper` is private. → class_nm=3, class_npm=2.
2759        check_metrics::<RustParser>(
2760            "struct Foo;\n\
2761             impl Foo {\n\
2762             \x20   pub fn new() -> Self { Foo }\n\
2763             \x20   fn helper(&self) -> i32 { 0 }\n\
2764             \x20   pub fn process(&self) -> i32 { 0 }\n\
2765             }\n",
2766            "foo.rs",
2767            |metric| {
2768                assert_eq!(metric.npm.class_nm_sum(), 3);
2769                assert_eq!(metric.npm.class_npm_sum(), 2);
2770                insta::assert_json_snapshot!(metric.npm);
2771            },
2772        );
2773    }
2774
2775    #[test]
2776    fn rust_pub_self_is_private() {
2777        // Regression for #460. `pub(self)` / `pub(in self)` restrict to
2778        // the current module — semantically private, like no modifier.
2779        // Only the forms that widen visibility beyond the module count
2780        // as public: `pub`, `pub(crate)`, `pub(super)`, `pub(in <path>)`.
2781        // → 6 methods, 4 public (b, d, e, f); a, a2, c excluded.
2782        // Pre-fix the `pub(self)`/`pub(in self)` pair over-counted, so
2783        // class_npm_sum was 6 (revert-verified).
2784        check_metrics::<RustParser>(
2785            "struct S;\n\
2786             impl S {\n\
2787             \x20   pub(self) fn a(&self) {}\n\
2788             \x20   pub(in self) fn a2(&self) {}\n\
2789             \x20   pub(crate) fn b(&self) {}\n\
2790             \x20   pub(super) fn d(&self) {}\n\
2791             \x20   pub(in crate::x) fn e(&self) {}\n\
2792             \x20   pub fn f(&self) {}\n\
2793             \x20   fn c(&self) {}\n\
2794             }\n",
2795            "foo.rs",
2796            |metric| {
2797                assert_eq!(metric.npm.class_nm_sum(), 7);
2798                assert_eq!(metric.npm.class_npm_sum(), 4);
2799            },
2800        );
2801    }
2802
2803    #[test]
2804    fn rust_trait_methods_count() {
2805        // `fn draw(&self);` (signature only) + `fn area(&self) -> f64
2806        // { 0.0 }` (default body) → both are interface methods.
2807        // Trait methods are always public. → interface_nm=2,
2808        // interface_npm=2. Structural `assert_child_space_kind`
2809        // pins the trait FuncSpace against an `is_func_space`
2810        // revert (see #311).
2811        check_func_space::<RustParser, _>(
2812            "trait Drawable {\n\
2813             \x20   fn draw(&self);\n\
2814             \x20   fn area(&self) -> f64 { 0.0 }\n\
2815             }\n",
2816            "foo.rs",
2817            |func_space| {
2818                let metric = &func_space.metrics;
2819                assert_eq!(metric.npm.interface_nm_sum(), 2);
2820                assert_eq!(metric.npm.interface_npm_sum(), 2);
2821                assert_eq!(metric.npm.class_nm_sum(), 0);
2822                insta::assert_json_snapshot!(metric.npm);
2823                assert_child_space_kind(&func_space, "Drawable", SpaceKind::Trait);
2824            },
2825        );
2826    }
2827
2828    #[test]
2829    fn rust_module_level_function_not_method() {
2830        // Top-level `fn` is NOT a method. The npa/npm metric on a
2831        // Unit space stays disabled (no class/interface), so the
2832        // method count is zero.
2833        check_metrics::<RustParser>("fn f() {}\nfn g() {}\n", "foo.rs", |metric| {
2834            assert_eq!(metric.npm.class_nm_sum(), 0);
2835            assert_eq!(metric.npm.interface_nm_sum(), 0);
2836            insta::assert_json_snapshot!(metric.npm);
2837        });
2838    }
2839
2840    #[test]
2841    fn rust_multiple_impls_methods_aggregate() {
2842        // Two `impl Foo` blocks contribute 1 + 1 = 2 methods.
2843        check_metrics::<RustParser>(
2844            "struct Foo;\n\
2845             impl Foo { pub fn m1(&self) {} }\n\
2846             impl Foo { fn m2(&self) {} }\n",
2847            "foo.rs",
2848            |metric| {
2849                assert_eq!(metric.npm.class_nm_sum(), 2);
2850                assert_eq!(metric.npm.class_npm_sum(), 1);
2851                insta::assert_json_snapshot!(metric.npm);
2852            },
2853        );
2854    }
2855
2856    #[test]
2857    fn rust_trait_impl_block_counts_methods() {
2858        // `impl Drawable for Foo` is also an `impl_item` — its methods
2859        // count toward class_nm of the impl. Trait impls and inherent
2860        // impls are not distinguished at the AST level (both parse as
2861        // `impl_item`). Structural `assert_child_space_kind` pins the
2862        // trait FuncSpace against an `is_func_space` revert
2863        // (see #311).
2864        check_func_space::<RustParser, _>(
2865            "struct Foo;\n\
2866             trait Drawable { fn draw(&self); }\n\
2867             impl Drawable for Foo { fn draw(&self) {} }\n",
2868            "foo.rs",
2869            |func_space| {
2870                let metric = &func_space.metrics;
2871                // Trait body: 1 signature method → interface_nm = 1.
2872                // Impl body: 1 fn `draw` → class_nm = 1.
2873                assert_eq!(metric.npm.interface_nm_sum(), 1);
2874                assert_eq!(metric.npm.class_nm_sum(), 1);
2875                insta::assert_json_snapshot!(metric.npm);
2876                assert_child_space_kind(&func_space, "Drawable", SpaceKind::Trait);
2877            },
2878        );
2879    }
2880
2881    // ----- Go -----
2882
2883    #[test]
2884    fn go_empty_unit_no_methods() {
2885        // No receiver methods → npm stays disabled, class_nm_sum = 0.
2886        check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
2887            assert_eq!(metric.npm.class_nm_sum(), 0);
2888            insta::assert_json_snapshot!(metric.npm);
2889        });
2890    }
2891
2892    #[test]
2893    fn go_method_declarations_count() {
2894        // Two `func (r Foo) ...` methods on the same receiver type →
2895        // class_nm_sum = 2. Go visibility is lexical (issue #458):
2896        // `DoX` is exported, `doY` is not, so class_npm_sum = 1.
2897        check_metrics::<GoParser>(
2898            "package main\n\
2899             type Foo struct{}\n\
2900             func (f Foo) DoX() {}\n\
2901             func (f Foo) doY() {}\n",
2902            "foo.go",
2903            |metric| {
2904                assert_eq!(metric.npm.class_nm_sum(), 2);
2905                assert_eq!(metric.npm.class_npm_sum(), 1);
2906                insta::assert_json_snapshot!(metric.npm);
2907            },
2908        );
2909    }
2910
2911    #[test]
2912    fn go_free_function_is_not_method() {
2913        // `func g() {}` has no receiver → NOT a method. class_nm_sum
2914        // stays at 0. The file has no method either, so npm stays
2915        // disabled (suppressed from JSON).
2916        check_metrics::<GoParser>(
2917            "package main\nfunc g() {}\nfunc h(x int) int { return x }\n",
2918            "foo.go",
2919            |metric| {
2920                assert_eq!(metric.npm.class_nm_sum(), 0);
2921                insta::assert_json_snapshot!(metric.npm);
2922            },
2923        );
2924    }
2925
2926    #[test]
2927    fn go_methods_on_different_receivers_aggregate_at_unit() {
2928        // Go's flat space model cannot group methods by receiver, so
2929        // methods on `Foo` and `Bar` aggregate at the file level
2930        // → class_nm_sum = 3 (1 + 2).
2931        check_metrics::<GoParser>(
2932            "package main\n\
2933             type Foo struct{}\n\
2934             type Bar struct{}\n\
2935             func (f Foo) M1() {}\n\
2936             func (b Bar) M2() {}\n\
2937             func (b *Bar) M3() {}\n",
2938            "foo.go",
2939            |metric| {
2940                assert_eq!(metric.npm.class_nm_sum(), 3);
2941                insta::assert_json_snapshot!(metric.npm);
2942            },
2943        );
2944    }
2945
2946    #[test]
2947    fn go_interface_methods_count_as_interface_nm() {
2948        // `interface { Read() error; Close() error }` declares two
2949        // method signatures → interface_nm = 2, interface_npm = 2.
2950        // Both names are exported (uppercase first char), so the
2951        // lexical export rule (issue #471) leaves npm == nm here;
2952        // `go_interface_methods_respect_export` covers the mixed case.
2953        //
2954        // Unlike Java / Kotlin / TS, Go interfaces do *not* open a
2955        // FuncSpace (`GoCode::is_func_space` only matches
2956        // `SourceFile` and the function kinds), so there is no
2957        // `SpaceKind::Interface` child to assert against here — the
2958        // body walker counts methods directly from the `interface_type`
2959        // AST node. The failure mode #311 guards against (a vacuous
2960        // pass when `InterfaceDeclaration` is dropped from
2961        // `is_func_space`) therefore does not apply to Go.
2962        check_metrics::<GoParser>(
2963            "package main\ntype RC interface { Read() error; Close() error }\n",
2964            "foo.go",
2965            |metric| {
2966                assert_eq!(metric.npm.interface_nm_sum(), 2);
2967                assert_eq!(metric.npm.interface_npm_sum(), 2);
2968                assert_eq!(metric.npm.class_nm_sum(), 0);
2969                insta::assert_json_snapshot!(metric.npm);
2970            },
2971        );
2972    }
2973
2974    #[test]
2975    fn go_interface_methods_respect_export() {
2976        // Go's lexical export rule applies to interface method names
2977        // too (issue #471). `Foo` and `Ünic` (Unicode uppercase first
2978        // char) are exported; `bar` is not. interface_nm counts all
2979        // three; interface_npm only the two exported. Revert-verified
2980        // against the old all-public arm (interface_npm_sum = 3).
2981        check_metrics::<GoParser>(
2982            "package main\ntype I interface { Foo(); bar(); Ünic() }\n",
2983            "foo.go",
2984            |metric| {
2985                assert_eq!(metric.npm.interface_nm_sum(), 3);
2986                assert_eq!(metric.npm.interface_npm_sum(), 2);
2987                assert_eq!(metric.npm.class_nm_sum(), 0);
2988                insta::assert_json_snapshot!(metric.npm);
2989            },
2990        );
2991    }
2992
2993    #[test]
2994    fn go_pointer_receiver_methods_count() {
2995        // Pointer-receiver methods (`func (r *Foo) M() {}`) parse as
2996        // MethodDeclaration the same way as value-receiver methods
2997        // → class_nm_sum = 2.
2998        check_metrics::<GoParser>(
2999            "package main\n\
3000             type Foo struct{}\n\
3001             func (f *Foo) Set() {}\n\
3002             func (f *Foo) Get() int { return 0 }\n",
3003            "foo.go",
3004            |metric| {
3005                assert_eq!(metric.npm.class_nm_sum(), 2);
3006                insta::assert_json_snapshot!(metric.npm);
3007            },
3008        );
3009    }
3010
3011    #[test]
3012    fn go_npm_excludes_unexported() {
3013        // Mixed exported / unexported methods (issue #458). `Greet`
3014        // and `Ärger` (Unicode uppercase first char) are exported;
3015        // `helper` is not. nm counts all three, npm only the two
3016        // exported. Revert-verified against the old all-public code
3017        // (which scored class_npm_sum = 3).
3018        check_metrics::<GoParser>(
3019            "package main\n\
3020             type T struct{}\n\
3021             func (t *T) Greet() {}\n\
3022             func (t *T) helper() {}\n\
3023             func (t *T) Ärger() {}\n",
3024            "foo.go",
3025            |metric| {
3026                assert_eq!(metric.npm.class_nm_sum(), 3);
3027                assert_eq!(metric.npm.class_npm_sum(), 2);
3028                insta::assert_json_snapshot!(metric.npm);
3029            },
3030        );
3031    }
3032
3033    // ----- Elixir -----
3034
3035    // Issue #275: Elixir `def` is public, `defp` is private. All
3036    // count toward `class_nm`; only the public ones bump `class_npm`.
3037    #[test]
3038    fn elixir_npm_def_is_public_defp_is_private() {
3039        check_metrics::<ElixirParser>(
3040            "defmodule Foo do\n  def pub_one, do: 1\n  defp priv_one, do: 1\n  def pub_two(x), do: x\nend\n",
3041            "foo.ex",
3042            |metric| {
3043                // 3 methods, 2 public.
3044                assert_eq!(metric.npm.class_nm_sum(), 3);
3045                assert_eq!(metric.npm.class_npm_sum(), 2);
3046            },
3047        );
3048    }
3049
3050    #[test]
3051    fn elixir_npm_defmacro_counts_as_public() {
3052        check_metrics::<ElixirParser>(
3053            "defmodule Foo do\n  defmacro pub_macro(x), do: x\n  defmacrop priv_macro(x), do: x\nend\n",
3054            "foo.ex",
3055            |metric| {
3056                // defmacro = public method, defmacrop = private method.
3057                assert_eq!(metric.npm.class_nm_sum(), 2);
3058                assert_eq!(metric.npm.class_npm_sum(), 1);
3059            },
3060        );
3061    }
3062
3063    #[test]
3064    fn elixir_npm_multiple_def_clauses_each_count() {
3065        // Pattern-match clauses each form their own method head.
3066        check_metrics::<ElixirParser>(
3067            "defmodule Foo do\n  def f(0), do: :zero\n  def f(_), do: :other\nend\n",
3068            "foo.ex",
3069            |metric| {
3070                assert_eq!(metric.npm.class_nm_sum(), 2);
3071                assert_eq!(metric.npm.class_npm_sum(), 2);
3072            },
3073        );
3074    }
3075
3076    #[test]
3077    fn elixir_npm_nested_defmodule_each_class() {
3078        check_metrics::<ElixirParser>(
3079            "defmodule Outer do\n  def o, do: 1\n  defmodule Inner do\n    def i, do: 1\n  end\nend\n",
3080            "foo.ex",
3081            |metric| {
3082                // Two classes, one public method each.
3083                assert_eq!(metric.npm.class_nm_sum(), 2);
3084                assert_eq!(metric.npm.class_npm_sum(), 2);
3085            },
3086        );
3087    }
3088
3089    #[test]
3090    fn elixir_npm_user_macro_not_classified_as_method() {
3091        // User-defined `custom_def` is a defmacro (counts) but its
3092        // invocation `custom_def foo, do: ...` must NOT be classified
3093        // as a method.
3094        check_metrics::<ElixirParser>(
3095            "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",
3096            "foo.ex",
3097            |metric| {
3098                // Only `defmacro custom_def` is a method of Foo (the
3099                // inner `def unquote(name)` is wrapped in `quote` so
3100                // it does not lexically appear as a direct child of
3101                // the defmodule do_block).
3102                assert_eq!(metric.npm.class_nm_sum(), 1);
3103                assert_eq!(metric.npm.class_npm_sum(), 1);
3104            },
3105        );
3106    }
3107
3108    #[test]
3109    fn elixir_npm_quoted_defs_do_not_inflate_method_count() {
3110        // Companion to `wmc::tests::elixir_wmc_quoted_defs_do_not_inflate_method_count`
3111        // (#310). The three `def` / `defp` calls inside the `quote do
3112        // … end` template do NOT count as methods of `Foo`. NPM has
3113        // always behaved this way via its direct-children scan; this
3114        // test pins the headline values so a future refactor of NPM
3115        // toward "walk all nested Function spaces" cannot silently
3116        // re-introduce the WMC/NPM disagreement that #310 fixed.
3117        check_metrics::<ElixirParser>(
3118            "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",
3119            "foo.ex",
3120            |metric| {
3121                // Only `defmacro multi` is a method (and public).
3122                assert_eq!(metric.npm.class_nm_sum(), 1);
3123                assert_eq!(metric.npm.class_npm_sum(), 1);
3124            },
3125        );
3126    }
3127
3128    // ----- Objective-C -----
3129
3130    #[test]
3131    fn objc_npm() {
3132        // ObjC has no method-privacy keyword: methods declared in
3133        // `@interface` are public (interface_npm), and every
3134        // `@implementation` method counts as public (class_npm) —
3135        // `privHelper`, defined but never declared, included. A free C
3136        // function (`cFunc`) defined inside `@implementation` is NOT a
3137        // method, so `class_nm` stays 3.
3138        check_metrics::<ObjcParser>(
3139            "@interface Foo : NSObject\n\
3140             - (void)pub1;\n\
3141             - (void)pub2;\n\
3142             @end\n\
3143             @implementation Foo\n\
3144             - (void)pub1 { }\n\
3145             - (void)pub2 { }\n\
3146             - (void)privHelper { }\n\
3147             void cFunc(void) { }\n\
3148             @end\n",
3149            "foo.m",
3150            |metric| {
3151                assert_eq!(metric.npm.interface_nm_sum(), 2);
3152                assert_eq!(metric.npm.interface_npm_sum(), 2);
3153                assert_eq!(metric.npm.class_nm_sum(), 3);
3154                assert_eq!(metric.npm.class_npm_sum(), 3);
3155            },
3156        );
3157    }
3158
3159    #[test]
3160    fn objc_npm_protocol() {
3161        // A `@protocol`'s methods after an `@optional` / `@required`
3162        // marker nest under a `qualified_protocol_interface_declaration`;
3163        // they must still count (regression for the direct-children walk
3164        // that missed `optDraw`).
3165        check_metrics::<ObjcParser>(
3166            "@protocol Drawable <NSObject>\n\
3167             - (void)draw;\n\
3168             @optional\n\
3169             - (void)optDraw;\n\
3170             @end\n",
3171            "foo.m",
3172            |metric| {
3173                assert_eq!(metric.npm.interface_nm_sum(), 2);
3174                assert_eq!(metric.npm.interface_npm_sum(), 2);
3175            },
3176        );
3177    }
3178
3179    // ----- C++ -----
3180
3181    #[test]
3182    fn cpp_empty_unit_no_methods() {
3183        // No code → no class spaces → npm = 0.
3184        check_metrics::<CppParser>("", "empty.cpp", |metric| {
3185            assert_eq!(metric.npm.class_nm_sum(), 0);
3186            assert_eq!(metric.npm.class_npm_sum(), 0);
3187            insta::assert_json_snapshot!(metric.npm);
3188        });
3189    }
3190
3191    #[test]
3192    fn cpp_class_methods_count() {
3193        // Two member functions (one defined inline, one declared only).
3194        // Both count. Defaults to private → class_npm = 0.
3195        check_metrics::<CppParser>(
3196            "class Foo {\n\
3197                 void method1() {}\n\
3198                 void method2();\n\
3199             };",
3200            "foo.cpp",
3201            |metric| {
3202                assert_eq!(metric.npm.class_nm_sum(), 2);
3203                assert_eq!(metric.npm.class_npm_sum(), 0);
3204                insta::assert_json_snapshot!(metric.npm);
3205            },
3206        );
3207    }
3208
3209    #[test]
3210    fn cpp_constructors_and_destructors_count() {
3211        // Constructors and destructors are parsed as `declaration`
3212        // (not `field_declaration`) inside the class body because they
3213        // have no return type. Both still count as methods.
3214        check_metrics::<CppParser>(
3215            "class Foo {\n\
3216                 public:\n\
3217                     Foo();\n\
3218                     ~Foo();\n\
3219                     void method();\n\
3220             };",
3221            "foo.cpp",
3222            |metric| {
3223                assert_eq!(metric.npm.class_nm_sum(), 3);
3224                assert_eq!(metric.npm.class_npm_sum(), 3);
3225                insta::assert_json_snapshot!(metric.npm);
3226            },
3227        );
3228    }
3229
3230    #[test]
3231    fn cpp_template_methods_count() {
3232        // `template<typename T> T foo(T x);` parses as
3233        // `template_declaration` wrapping a `declaration` whose
3234        // `function_declarator` is reached recursively.
3235        check_metrics::<CppParser>(
3236            "class Foo {\n\
3237                 public:\n\
3238                     template<typename T> T fn(T x);\n\
3239             };",
3240            "foo.cpp",
3241            |metric| {
3242                assert_eq!(metric.npm.class_nm_sum(), 1);
3243                assert_eq!(metric.npm.class_npm_sum(), 1);
3244                insta::assert_json_snapshot!(metric.npm);
3245            },
3246        );
3247    }
3248
3249    #[test]
3250    fn cpp_struct_methods_default_public() {
3251        // `struct` defaults to public visibility. All three methods
3252        // count as public.
3253        check_metrics::<CppParser>(
3254            "struct Foo {\n\
3255                 void a();\n\
3256                 void b() {}\n\
3257                 Foo() {}\n\
3258             };",
3259            "foo.cpp",
3260            |metric| {
3261                assert_eq!(metric.npm.class_nm_sum(), 3);
3262                assert_eq!(metric.npm.class_npm_sum(), 3);
3263                insta::assert_json_snapshot!(metric.npm);
3264            },
3265        );
3266    }
3267
3268    #[test]
3269    fn cpp_free_function_is_not_method() {
3270        // Top-level function — not inside any class — does not count
3271        // toward npm. The Unit space is not marked as a class space,
3272        // so npm stays at zero.
3273        check_metrics::<CppParser>("void free_fn() {}\n", "foo.cpp", |metric| {
3274            assert_eq!(metric.npm.class_nm_sum(), 0);
3275            assert_eq!(metric.npm.class_npm_sum(), 0);
3276            insta::assert_json_snapshot!(metric.npm);
3277        });
3278    }
3279
3280    #[test]
3281    fn cpp_mixed_visibility_methods() {
3282        // `class` defaults to private. Public section gets 1 method,
3283        // protected gets 1 (bucketed as non-public for npm), private
3284        // gets 1. Total: class_nm = 3, class_npm = 1.
3285        check_metrics::<CppParser>(
3286            "class Foo {\n\
3287                 public: void a();\n\
3288                 protected: void b();\n\
3289                 private: void c();\n\
3290             };",
3291            "foo.cpp",
3292            |metric| {
3293                assert_eq!(metric.npm.class_nm_sum(), 3);
3294                assert_eq!(metric.npm.class_npm_sum(), 1);
3295                insta::assert_json_snapshot!(metric.npm);
3296            },
3297        );
3298    }
3299
3300    #[test]
3301    fn cpp_multiple_classes_aggregate_at_unit() {
3302        // File-level rollup: Foo has 2 methods, Bar has 1. Unit
3303        // class_nm_sum = 3.
3304        check_metrics::<CppParser>(
3305            "class Foo { public: void a(); void b() {} };\n\
3306             struct Bar { void c(); };",
3307            "foo.cpp",
3308            |metric| {
3309                assert_eq!(metric.npm.class_nm_sum(), 3);
3310                assert_eq!(metric.npm.class_npm_sum(), 3);
3311                insta::assert_json_snapshot!(metric.npm);
3312            },
3313        );
3314    }
3315
3316    #[test]
3317    fn javascript_empty_unit_no_methods() {
3318        check_metrics::<JavascriptParser>("", "empty.js", |metric| {
3319            assert_eq!(metric.npm.class_nm_sum(), 0);
3320            assert_eq!(metric.npm.class_npm_sum(), 0);
3321            insta::assert_json_snapshot!(metric.npm);
3322        });
3323    }
3324
3325    #[test]
3326    fn javascript_class_methods_count() {
3327        // `method_definition` direct children of `class_body` cover
3328        // regular methods, getters/setters, and constructors. JS has
3329        // no visibility — all members are public. nm = npm = 4.
3330        check_metrics::<JavascriptParser>(
3331            "class Foo {\n\
3332                 constructor() {}\n\
3333                 bar() {}\n\
3334                 get baz() { return 1; }\n\
3335                 set baz(v) {}\n\
3336             }",
3337            "foo.js",
3338            |metric| {
3339                assert_eq!(metric.npm.class_nm_sum(), 4);
3340                assert_eq!(metric.npm.class_npm_sum(), 4);
3341                insta::assert_json_snapshot!(metric.npm);
3342            },
3343        );
3344    }
3345
3346    #[test]
3347    fn javascript_arrow_field_is_method() {
3348        // `class Foo { x = () => {} }` is a method written as a field
3349        // initializer. Both arrow functions and `function`
3350        // expressions in field position count as methods.
3351        check_metrics::<JavascriptParser>(
3352            "class Foo { x = () => {}; y = function() {}; z = 1; }",
3353            "foo.js",
3354            |metric| {
3355                // x + y are methods; z is an attribute.
3356                assert_eq!(metric.npm.class_nm_sum(), 2);
3357                assert_eq!(metric.npm.class_npm_sum(), 2);
3358                insta::assert_json_snapshot!(metric.npm);
3359            },
3360        );
3361    }
3362
3363    #[test]
3364    fn javascript_free_function_is_not_method() {
3365        // Top-level functions and arrow functions outside a class
3366        // body are not methods.
3367        check_metrics::<JavascriptParser>(
3368            "function f() {}\nconst g = () => {};\nclass Foo { h() {} }",
3369            "foo.js",
3370            |metric| {
3371                // Only `h` is a method.
3372                assert_eq!(metric.npm.class_nm_sum(), 1);
3373                assert_eq!(metric.npm.class_npm_sum(), 1);
3374                insta::assert_json_snapshot!(metric.npm);
3375            },
3376        );
3377    }
3378
3379    #[test]
3380    fn javascript_multiple_classes_aggregate_at_unit() {
3381        // File-level rollup: Foo has 2 methods, Bar has 1. Unit
3382        // class_nm_sum = 3.
3383        check_metrics::<JavascriptParser>(
3384            "class Foo { a() {} b() {} }\nclass Bar { c() {} }",
3385            "foo.js",
3386            |metric| {
3387                assert_eq!(metric.npm.class_nm_sum(), 3);
3388                assert_eq!(metric.npm.class_npm_sum(), 3);
3389                insta::assert_json_snapshot!(metric.npm);
3390            },
3391        );
3392    }
3393
3394    #[test]
3395    fn mozjs_class_methods_count() {
3396        // Mozjs shares JS's class vocabulary.
3397        check_metrics::<MozjsParser>(
3398            "class Foo {\n\
3399                 constructor() {}\n\
3400                 bar() {}\n\
3401                 get baz() { return 1; }\n\
3402                 set baz(v) {}\n\
3403             }",
3404            "foo.js",
3405            |metric| {
3406                assert_eq!(metric.npm.class_nm_sum(), 4);
3407                assert_eq!(metric.npm.class_npm_sum(), 4);
3408                insta::assert_json_snapshot!(metric.npm);
3409            },
3410        );
3411    }
3412
3413    // Regression for #438: an empty class has zero methods, so the COA
3414    // accessors divide 0.0 / 0.0. Before the zero-guard this yielded NaN
3415    // (serialized to JSON `null`). The defined value is 0.0 — a
3416    // method-less class exposes no public operations. Asserting
3417    // `!is_nan()` proves the guard fires; the `== 0.0` checks pin the
3418    // chosen convention. Exercised across the explicit-visibility OO
3419    // languages (Java, C#, Kotlin, PHP).
3420    #[test]
3421    fn empty_class_coa_is_zero_not_nan() {
3422        let assert_zero = |metric: crate::CodeMetrics| {
3423            assert_eq!(metric.npm.class_nm_sum(), 0);
3424            assert!(!metric.npm.class_coa().is_nan());
3425            assert!(!metric.npm.total_coa().is_nan());
3426            assert_eq!(metric.npm.class_coa(), 0.0);
3427            assert_eq!(metric.npm.total_coa(), 0.0);
3428        };
3429        check_metrics::<JavaParser>("class Foo {}", "foo.java", assert_zero);
3430        check_metrics::<CsharpParser>("class Foo {}", "foo.cs", assert_zero);
3431        check_metrics::<KotlinParser>("class Foo {}", "foo.kt", assert_zero);
3432        check_metrics::<PhpParser>("<?php class Foo {}", "foo.php", assert_zero);
3433    }
3434
3435    // Regression for #438: an empty interface has zero methods; the
3436    // existing all-public guard explicitly excludes the empty case
3437    // (`!= 0`), so without the divisor guard `interface_coa` returned
3438    // 0.0 / 0.0 = NaN. The defined value is 0.0.
3439    #[test]
3440    fn empty_interface_coa_is_zero_not_nan() {
3441        let assert_zero = |metric: crate::CodeMetrics| {
3442            assert_eq!(metric.npm.interface_nm_sum(), 0);
3443            assert!(!metric.npm.interface_coa().is_nan());
3444            assert_eq!(metric.npm.interface_coa(), 0.0);
3445        };
3446        check_metrics::<JavaParser>("interface Foo {}", "foo.java", assert_zero);
3447        check_metrics::<CsharpParser>("interface Foo {}", "foo.cs", assert_zero);
3448    }
3449}