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