Skip to main content

big_code_analysis/metrics/
npm.rs

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