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