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