Skip to main content

big_code_analysis/metrics/
npa.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8// Metric counts (token, function, branch, argument, etc.) are stored as
9// `usize` and crossed with `f64` averages, ratios, and Halstead scores
10// across the cyclomatic / MI / Halstead computations. The `usize as f64`
11// and `f64 as usize` casts are intentional and snapshot-anchored — every
12// site is bounded by the count it came from. Allowing the lints at the
13// module level keeps the metric arithmetic legible.
14#![allow(
15    clippy::cast_precision_loss,
16    clippy::cast_possible_truncation,
17    clippy::cast_sign_loss
18)]
19
20use serde::Serialize;
21use serde::ser::{SerializeStruct, Serializer};
22use std::fmt;
23
24use crate::checker::Checker;
25use crate::langs::*;
26use crate::macros::{csharp_var_decl_kinds, csharp_var_declarator_kinds, implement_metric_trait};
27use crate::node::Node;
28use crate::*;
29
30/// The `Npa` metric.
31///
32/// This metric counts the number of public attributes
33/// of classes/interfaces.
34#[derive(Clone, Debug, Default)]
35pub struct Stats {
36    class_npa: usize,
37    interface_npa: usize,
38    class_na: usize,
39    interface_na: usize,
40    class_npa_sum: usize,
41    interface_npa_sum: usize,
42    class_na_sum: usize,
43    interface_na_sum: usize,
44    is_class_space: bool,
45}
46
47impl Serialize for Stats {
48    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
49    where
50        S: Serializer,
51    {
52        let mut st = serializer.serialize_struct("npa", 9)?;
53        st.serialize_field("classes", &self.class_npa_sum())?;
54        st.serialize_field("interfaces", &self.interface_npa_sum())?;
55        st.serialize_field("class_attributes", &self.class_na_sum())?;
56        st.serialize_field("interface_attributes", &self.interface_na_sum())?;
57        st.serialize_field("classes_average", &self.class_cda())?;
58        st.serialize_field("interfaces_average", &self.interface_cda())?;
59        st.serialize_field("total", &self.total_npa())?;
60        st.serialize_field("total_attributes", &self.total_na())?;
61        st.serialize_field("average", &self.total_cda())?;
62        st.end()
63    }
64}
65
66impl fmt::Display for Stats {
67    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
68        write!(
69            f,
70            "classes: {}, interfaces: {}, class_attributes: {}, interface_attributes: {}, classes_average: {}, interfaces_average: {}, total: {}, total_attributes: {}, average: {}",
71            self.class_npa_sum(),
72            self.interface_npa_sum(),
73            self.class_na_sum(),
74            self.interface_na_sum(),
75            self.class_cda(),
76            self.interface_cda(),
77            self.total_npa(),
78            self.total_na(),
79            self.total_cda()
80        )
81    }
82}
83
84impl Stats {
85    /// Merges a second `Npa` metric into the first one
86    pub fn merge(&mut self, other: &Stats) {
87        self.class_npa_sum += other.class_npa_sum;
88        self.interface_npa_sum += other.interface_npa_sum;
89        self.class_na_sum += other.class_na_sum;
90        self.interface_na_sum += other.interface_na_sum;
91    }
92
93    /// Returns the number of class public attributes in a space.
94    #[inline]
95    #[must_use]
96    pub fn class_npa(&self) -> f64 {
97        self.class_npa as f64
98    }
99
100    /// Returns the number of interface public attributes in a space.
101    #[inline]
102    #[must_use]
103    pub fn interface_npa(&self) -> f64 {
104        self.interface_npa as f64
105    }
106
107    /// Returns the number of class attributes in a space.
108    #[inline]
109    #[must_use]
110    pub fn class_na(&self) -> f64 {
111        self.class_na as f64
112    }
113
114    /// Returns the number of interface attributes in a space.
115    #[inline]
116    #[must_use]
117    pub fn interface_na(&self) -> f64 {
118        self.interface_na as f64
119    }
120
121    /// Returns the number of class public attributes sum in a space.
122    #[inline]
123    #[must_use]
124    pub fn class_npa_sum(&self) -> f64 {
125        self.class_npa_sum as f64
126    }
127
128    /// Returns the number of interface public attributes sum in a space.
129    #[inline]
130    #[must_use]
131    pub fn interface_npa_sum(&self) -> f64 {
132        self.interface_npa_sum as f64
133    }
134
135    /// Returns the number of class attributes sum in a space.
136    #[inline]
137    #[must_use]
138    pub fn class_na_sum(&self) -> f64 {
139        self.class_na_sum as f64
140    }
141
142    /// Returns the number of interface attributes sum in a space.
143    #[inline]
144    #[must_use]
145    pub fn interface_na_sum(&self) -> f64 {
146        self.interface_na_sum as f64
147    }
148
149    /// Returns the class `Cda` metric value
150    ///
151    /// The `Class Data Accessibility` metric value for a class
152    /// is computed by dividing the `Npa` value of the class
153    /// by the total number of attributes defined in the class.
154    ///
155    /// This metric is an adaptation of the `Classified Class Data Accessibility` (`CCDA`)
156    /// security metric for not classified attributes.
157    /// Paper: <https://ieeexplore.ieee.org/abstract/document/5381538>
158    #[inline]
159    #[must_use]
160    pub fn class_cda(&self) -> f64 {
161        self.class_npa_sum() / self.class_na_sum as f64
162    }
163
164    /// Returns the interface `Cda` metric value
165    ///
166    /// The `Class Data Accessibility` metric value for an interface
167    /// is computed by dividing the `Npa` value of the interface
168    /// by the total number of attributes defined in the interface.
169    ///
170    /// This metric is an adaptation of the `Classified Class Data Accessibility` (`CCDA`)
171    /// security metric for not classified attributes.
172    /// Paper: <https://ieeexplore.ieee.org/abstract/document/5381538>
173    #[inline]
174    #[must_use]
175    pub fn interface_cda(&self) -> f64 {
176        // For the Java language it's not necessary to compute the metric value
177        // The metric value in Java can only be 1.0 or f64:NAN
178        if self.interface_npa_sum == self.interface_na_sum && self.interface_npa_sum != 0 {
179            1.0
180        } else {
181            self.interface_npa_sum() / self.interface_na_sum()
182        }
183    }
184
185    /// Returns the total `Cda` metric value
186    ///
187    /// The total `Class Data Accessibility` metric value
188    /// is computed by dividing the total `Npa` value
189    /// by the total number of attributes.
190    ///
191    /// This metric is an adaptation of the `Classified Class Data Accessibility` (`CCDA`)
192    /// security metric for not classified attributes.
193    /// Paper: <https://ieeexplore.ieee.org/abstract/document/5381538>
194    #[inline]
195    #[must_use]
196    pub fn total_cda(&self) -> f64 {
197        self.total_npa() / self.total_na()
198    }
199
200    /// Returns the total number of public attributes in a space.
201    #[inline]
202    #[must_use]
203    pub fn total_npa(&self) -> f64 {
204        self.class_npa_sum() + self.interface_npa_sum()
205    }
206
207    /// Returns the total number of attributes in a space.
208    #[inline]
209    #[must_use]
210    pub fn total_na(&self) -> f64 {
211        self.class_na_sum() + self.interface_na_sum()
212    }
213
214    // Accumulates the number of class and interface
215    // public and not public attributes into the sums
216    #[inline]
217    pub(crate) fn compute_sum(&mut self) {
218        self.class_npa_sum += self.class_npa;
219        self.interface_npa_sum += self.interface_npa;
220        self.class_na_sum += self.class_na;
221        self.interface_na_sum += self.interface_na;
222    }
223
224    // Checks if the `Npa` metric is disabled
225    #[inline]
226    pub(crate) fn is_disabled(&self) -> bool {
227        !self.is_class_space
228    }
229}
230
231#[doc(hidden)]
232/// Per-language counting of public attributes.
233pub trait Npa
234where
235    Self: Checker,
236{
237    /// Walk `node` and update `stats` with this metric for the language
238    /// implementing the trait.
239    ///
240    /// `code` is the raw source-bytes buffer; languages whose visibility
241    /// rules are encoded in identifier text (Ruby's keyword-style
242    /// `private` / `public` / `protected`) read identifier text from
243    /// it. Languages whose visibility rules are encoded purely in
244    /// distinct token kinds (Java's `Public` / `Private`, PHP's
245    /// `VisibilityModifier`) ignore the parameter.
246    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats);
247}
248
249// Java and Groovy share their grammar tokens for class/interface
250// bodies, so `Npa::compute` differs only by the language enum.
251// `impl_npa_java_like!` emits the same body against each enum
252// (issue #280).
253//
254// `ClassBody` covers classes and records (records reuse `class_body`
255// for their explicit declaration body). Record components in
256// `formal_parameters` are implicit public final fields, but only
257// explicit body members are counted here for parity with C#'s record
258// handling (lesson 11). `EnumBodyDeclarations` is the optional
259// declarations block inside `EnumBody`, following the enum constants.
260// Annotation type bodies hold `ConstantDeclaration`s with the same
261// implicit `public static final` rule as interfaces
262// (https://docs.oracle.com/javase/specs/jls/se7/html/jls-9.html).
263//
264// Groovy note: `def field` at class scope is parsed as a
265// `FieldDeclaration` with `Def` in the modifiers list (no `Public`),
266// so it's correctly excluded from `class_npa` unless explicitly
267// annotated `public` — consistent with Groovy's access semantics
268// (default class members are package-private under `@CompileStatic`,
269// public otherwise; we conservatively follow Java).
270macro_rules! impl_npa_java_like {
271    ($code:ty, $lang:ident) => {
272        impl Npa for $code {
273            fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
274                use $lang::*;
275
276                if Self::is_func_space(node) && stats.is_disabled() {
277                    stats.is_class_space = true;
278                }
279
280                match node.kind_id().into() {
281                    ClassBody | EnumBodyDeclarations => {
282                        for declaration in node
283                            .children()
284                            .filter(|n| matches!(n.kind_id().into(), FieldDeclaration))
285                        {
286                            let attributes = declaration
287                                .children()
288                                .filter(|n| matches!(n.kind_id().into(), VariableDeclarator))
289                                .count();
290                            stats.class_na += attributes;
291                            // The first child node contains the list of
292                            // attribute modifiers. Source:
293                            // https://docs.oracle.com/javase/tutorial/reflect/member/fieldModifiers.html
294                            if declaration.child(0).is_some_and(|modifiers| {
295                                matches!(modifiers.kind_id().into(), Modifiers)
296                                    && modifiers.first_child(|id| id == Public).is_some()
297                            }) {
298                                stats.class_npa += attributes;
299                            }
300                        }
301                    }
302                    InterfaceBody | AnnotationTypeBody => {
303                        stats.interface_na += node
304                            .children()
305                            .filter(|n| matches!(n.kind_id().into(), ConstantDeclaration))
306                            .flat_map(|n| n.children())
307                            .filter(|n| matches!(n.kind_id().into(), VariableDeclarator))
308                            .count();
309                        stats.interface_npa = stats.interface_na;
310                    }
311                    _ => {}
312                }
313            }
314        }
315    };
316}
317
318impl_npa_java_like!(JavaCode, Java);
319
320// Groovy uses the dekobon grammar, which models class/interface/trait/
321// annotation-type/record bodies as a single `class_body` node and
322// flattens modifiers as direct children of the declaration (the
323// `_modifier` rule is hidden — no `Modifiers` wrapper). That rules out
324// the Java macro, so an explicit impl is required.
325//
326// `def field` at class scope parses as a `FieldDeclaration` with `Def`
327// in the modifier slot and no `Public`, so it's correctly excluded from
328// `class_npa` unless explicitly annotated `public` — consistent with
329// Groovy's access semantics (we conservatively follow Java).
330impl Npa for GroovyCode {
331    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
332        use Groovy::*;
333
334        if Self::is_func_space(node) && stats.is_disabled() {
335            stats.is_class_space = true;
336        }
337
338        match node.kind_id().into() {
339            ClassBody | EnumBody => {
340                let is_interface_like = groovy_body_is_interface_like(node);
341
342                for declaration in node
343                    .children()
344                    .filter(|n| matches!(n.kind_id().into(), FieldDeclaration))
345                {
346                    let attributes = declaration
347                        .children()
348                        .filter(|n| matches!(n.kind_id().into(), VariableDeclarator))
349                        .count();
350                    if is_interface_like {
351                        stats.interface_na += attributes;
352                        stats.interface_npa += attributes;
353                    } else {
354                        stats.class_na += attributes;
355                        if groovy_has_explicit_public(&declaration) {
356                            stats.class_npa += attributes;
357                        }
358                    }
359                }
360            }
361            _ => {}
362        }
363    }
364}
365
366// Distinguishes interface-like containers (interface, trait, annotation
367// type) — whose members are implicitly public — from class-like
368// containers (class, enum, record) that need an explicit `public`
369// modifier. The dekobon grammar models all of these bodies as
370// `class_body`, so the discriminant lives on the parent. Shared with
371// `impl Npm for GroovyCode` (`metrics::npm`).
372pub(crate) fn groovy_body_is_interface_like(body: &Node) -> bool {
373    use Groovy::*;
374    body.parent().is_some_and(|p| {
375        matches!(
376            p.kind_id().into(),
377            InterfaceDeclaration | TraitDeclaration | AnnotationTypeDeclaration
378        )
379    })
380}
381
382// Detects an explicit `public` modifier on a class member declaration.
383// The dekobon grammar flattens the `_modifier` rule, so modifier
384// tokens appear as direct children of the declaration — no `Modifiers`
385// wrapper to descend into. Shared with `impl Npm for GroovyCode`.
386pub(crate) fn groovy_has_explicit_public(declaration: &Node) -> bool {
387    declaration.first_child(|id| id == Groovy::Public).is_some()
388}
389
390// C# uses individual `Modifier` nodes (not wrapped under a single
391// `modifiers` node like Java); detecting `public` requires scanning
392// every Modifier child of the declaration for a `public` keyword.
393pub(crate) fn csharp_is_explicit_public(declaration: &Node) -> bool {
394    declaration.children().any(|child| {
395        matches!(child.kind_id().into(), Csharp::Modifier)
396            && child.first_child(|id| id == Csharp::Public).is_some()
397    })
398}
399
400impl Npa for CsharpCode {
401    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
402        use Csharp::*;
403
404        if Self::is_func_space(node) && stats.is_disabled() {
405            stats.is_class_space = true;
406        }
407
408        // Class / struct / record / interface bodies all share
409        // `DeclarationList`; the parent kind disambiguates.
410        if !matches!(node.kind_id().into(), DeclarationList) {
411            return;
412        }
413        let Some(parent_kind) = node.parent().map(|p| p.kind_id().into()) else {
414            return;
415        };
416        match parent_kind {
417            // For `RecordDeclaration`, only explicit body fields are
418            // counted. The implicit `parameter_list` of a positional
419            // record (`record Person(string Name, int Age);`) is not
420            // walked here — its parameters become auto-generated public
421            // properties at the IL level, but modelling them would
422            // require synthesizing nodes that don't appear in the AST.
423            ClassDeclaration | StructDeclaration | RecordDeclaration => {
424                for declaration in node
425                    .children()
426                    .filter(|c| matches!(c.kind_id().into(), FieldDeclaration))
427                {
428                    let attributes = csharp_count_field_declarators(&declaration);
429                    stats.class_na += attributes;
430                    if csharp_is_explicit_public(&declaration) {
431                        stats.class_npa += attributes;
432                    }
433                }
434            }
435            // C# 8+ interfaces can declare fields with explicit modifiers
436            // (rare); members declared without an explicit modifier default
437            // to public, mirroring Java's interface convention.
438            InterfaceDeclaration => {
439                for declaration in node
440                    .children()
441                    .filter(|c| matches!(c.kind_id().into(), FieldDeclaration))
442                {
443                    let attributes = csharp_count_field_declarators(&declaration);
444                    stats.interface_na += attributes;
445                    stats.interface_npa = stats.interface_na;
446                }
447            }
448            _ => {}
449        }
450    }
451}
452
453// Count `VariableDeclarator`s nested under any aliased `VariableDeclaration`
454// inside a C# `FieldDeclaration`. Both kinds emit two aliased `kind_id`s
455// each; the macros centralize the alias union (lesson #2).
456fn csharp_count_field_declarators(field_decl: &Node) -> usize {
457    field_decl
458        .children()
459        .filter(|c| matches!(c.kind_id().into(), csharp_var_decl_kinds!()))
460        .flat_map(|c| c.children())
461        .filter(|c| matches!(c.kind_id().into(), csharp_var_declarator_kinds!()))
462        .count()
463}
464
465// PHP's strict-explicit visibility rule (mirroring Java's pattern): a
466// declaration is treated as public only when it carries an explicit
467// `public` modifier. Modifier-less declarations — deprecated for
468// properties since PHP 8 and merely conventional for methods — are NOT
469// counted, even though PHP semantically defaults methods to public.
470pub(crate) fn php_is_explicit_public(declaration: &Node) -> bool {
471    declaration.children().any(|child| {
472        matches!(child.kind_id().into(), Php::VisibilityModifier)
473            && child.first_child(|id| id == Php::Public).is_some()
474    })
475}
476
477// Counts the number of symbol arguments passed to an `attr_accessor` /
478// `attr_reader` / `attr_writer` macro `Call` node. `attr_accessor :a,
479// :b, :c` exposes three attributes; an `attr_*` call with no arguments
480// is ill-formed Ruby but defensively returns zero rather than one.
481pub(crate) fn ruby_attr_macro_symbol_count(call: &Node) -> usize {
482    use Ruby::*;
483
484    call.children()
485        .find(|c| matches!(c.kind_id().into(), ArgumentList | ArgumentList2))
486        .map_or(0, |args| {
487            args.children()
488                .filter(|c| {
489                    matches!(
490                        c.kind_id().into(),
491                        SimpleSymbol | DelimitedSymbol | HashKeySymbol | BareSymbol
492                    )
493                })
494                .count()
495        })
496}
497
498// Ruby class-body visibility state. `private` / `public` / `protected`
499// keywords flip this flag for every subsequent declaration in the same
500// body until another marker overrides them. The default at the top of
501// every class body is `Public`.
502#[derive(Clone, Copy, PartialEq, Eq)]
503pub(crate) enum RubyVisibility {
504    Public,
505    Private,
506    Protected,
507}
508
509// Recognises a bare visibility-keyword `identifier` child of a Ruby
510// class body (`private` / `public` / `protected` with no arguments).
511// tree-sitter-ruby emits the keyword-form as a literal `identifier`
512// token; the argument-form (`private :foo`, `private def bar`) is a
513// `Call` node instead and does NOT flip the body-wide flag.
514pub(crate) fn ruby_visibility_marker(node: &Node, source: &[u8]) -> Option<RubyVisibility> {
515    if !matches!(node.kind_id().into(), Ruby::Identifier) {
516        return None;
517    }
518    match node.utf8_text(source)? {
519        "private" => Some(RubyVisibility::Private),
520        "public" => Some(RubyVisibility::Public),
521        "protected" => Some(RubyVisibility::Protected),
522        _ => None,
523    }
524}
525
526// Identifies the `attr_*` macro family on a Ruby `Call` node. Each
527// macro takes a list of attribute symbols and synthesises the matching
528// reader / writer / accessor methods on the enclosing class.
529pub(crate) fn ruby_attr_macro_name(call: &Node, source: &[u8]) -> Option<&'static str> {
530    let ident = call
531        .children()
532        .find(|c| matches!(c.kind_id().into(), Ruby::Identifier))?;
533    match ident.utf8_text(source)? {
534        "attr_accessor" => Some("attr_accessor"),
535        "attr_reader" => Some("attr_reader"),
536        "attr_writer" => Some("attr_writer"),
537        _ => None,
538    }
539}
540
541// Walks the direct children of a Ruby class / singleton-class body
542// (`BodyStatement` under `Class` / `SingletonClass`) tallying:
543// - class-scope assignments to `@var` (`InstanceVariable`) and
544//   `@@var` (`ClassVariable`) — one attribute per assignment, regardless
545//   of whether the RHS is a constant or another expression.
546// - `attr_accessor` / `attr_reader` / `attr_writer` macros — one
547//   attribute per symbol argument.
548//
549// Visibility flags follow Ruby's keyword-marker convention: a bare
550// `private` / `public` / `protected` identifier flips the default for
551// every subsequent declaration in the body. The default visibility at
552// the top of every class body is `public`. The argument-form of those
553// keywords (`private :foo`, `private def x`) does not flip the body-
554// wide flag — matching Ruby's runtime behaviour.
555//
556// Attribute assignments to instance/class variables are visible only
557// via the methods that wrap them, so the visibility flag at the point
558// of declaration is what `npa` should reflect.
559pub(crate) fn ruby_walk_class_body(body: &Node, source: &[u8], stats: &mut Stats) {
560    use Ruby::*;
561
562    let mut visibility = RubyVisibility::Public;
563    for child in body.children() {
564        if let Some(marker) = ruby_visibility_marker(&child, source) {
565            visibility = marker;
566            continue;
567        }
568        match child.kind_id().into() {
569            Assignment | Assignment2 => {
570                let Some(lhs) = child.children().next() else {
571                    continue;
572                };
573                if matches!(lhs.kind_id().into(), InstanceVariable | ClassVariable) {
574                    stats.class_na += 1;
575                    if visibility == RubyVisibility::Public {
576                        stats.class_npa += 1;
577                    }
578                }
579            }
580            Call | Call2 | Call3 | Call4 if ruby_attr_macro_name(&child, source).is_some() => {
581                let count = ruby_attr_macro_symbol_count(&child);
582                stats.class_na += count;
583                if visibility == RubyVisibility::Public {
584                    stats.class_npa += count;
585                }
586            }
587            _ => {}
588        }
589    }
590}
591
592impl Npa for RubyCode {
593    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
594        use Ruby::*;
595
596        if Self::is_func_space(node) && stats.is_disabled() {
597            stats.is_class_space = true;
598        }
599
600        if !matches!(node.kind_id().into(), BodyStatement | BodyStatement2) {
601            return;
602        }
603        let Some(parent_kind) = node.parent().map(|p| p.kind_id().into()) else {
604            return;
605        };
606        if !matches!(parent_kind, Class | SingletonClass) {
607            return;
608        }
609        ruby_walk_class_body(node, code, stats);
610    }
611}
612
613impl Npa for PhpCode {
614    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
615        use Php::*;
616
617        // Enables the `Npa` metric if computing stats of a class-like space.
618        if Self::is_func_space(node) && stats.is_disabled() {
619            stats.is_class_space = true;
620        }
621
622        match node.kind_id().into() {
623            // Class / trait / anonymous-class / interface bodies all share
624            // the `DeclarationList` kind; the parent kind disambiguates.
625            DeclarationList => {
626                let Some(parent_kind) = node.parent().map(|p| p.kind_id().into()) else {
627                    return;
628                };
629                match parent_kind {
630                    ClassDeclaration | TraitDeclaration | AnonymousClass => {
631                        for declaration in node
632                            .children()
633                            .filter(|c| matches!(c.kind_id().into(), PropertyDeclaration))
634                        {
635                            let attributes = declaration
636                                .children()
637                                .filter(|c| matches!(c.kind_id().into(), PropertyElement))
638                                .count();
639                            stats.class_na += attributes;
640                            if php_is_explicit_public(&declaration) {
641                                stats.class_npa += attributes;
642                            }
643                        }
644                    }
645                    // Interfaces cannot declare properties but can declare
646                    // class constants, which are implicitly public.
647                    InterfaceDeclaration => {
648                        let count: usize = node
649                            .children()
650                            .filter(|c| {
651                                matches!(c.kind_id().into(), ConstDeclaration | ConstDeclaration2)
652                            })
653                            .map(|decl| {
654                                decl.children()
655                                    .filter(|n| {
656                                        matches!(n.kind_id().into(), ConstElement | ConstElement2)
657                                    })
658                                    .count()
659                            })
660                            .sum();
661                        stats.interface_na += count;
662                        stats.interface_npa = stats.interface_na;
663                    }
664                    _ => {}
665                }
666            }
667            // Enum cases are public read-only constants of the enum.
668            EnumDeclarationList => {
669                let count = node
670                    .children()
671                    .filter(|c| matches!(c.kind_id().into(), EnumCase))
672                    .count();
673                stats.class_na += count;
674                stats.class_npa += count;
675            }
676            _ => {}
677        }
678    }
679}
680
681// Python attribute counting.
682//
683// Python has two flavours of class attributes:
684// 1. Class-level (a.k.a. static): direct assignments inside the class
685//    body — `class C: x = 1` or `class C: x: int = 1`.
686// 2. Instance attributes: `self.x = …` assigned inside any method
687//    body, conventionally inside `__init__`.
688//
689// Python has no visibility keyword. The PEP-8 convention `_x` for
690// "internal" and `__x` for "name-mangled private" is purely advisory
691// and not represented in the AST. `Npa::compute` is also called
692// without access to the source bytes (only the `Node`), so reading
693// the identifier text is not possible from this trait. We therefore
694// treat every class attribute as public — `class_npa == class_na` —
695// matching the Python ethos of "consenting adults". Documented as
696// part of the trait contract for Python.
697//
698// Strategy: when the visitor hits a `ClassDefinition`, walk the body
699// once and tally both class-level assignments and the `self.X = …`
700// targets introduced by any method body. Counting on the
701// `ClassDefinition` node (not its enclosed function spaces) keeps the
702// attribution local to the surrounding class space, even though
703// `self.X = …` lives inside a child `FunctionDefinition` space whose
704// own `npa` stats are not class spaces.
705impl Npa for PythonCode {
706    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
707        use Python::*;
708
709        // Gate on `ClassDefinition` specifically: `is_func_space` is
710        // also true for `Module` / `FunctionDefinition`, which would
711        // over-eagerly mark every space as a class space.
712        if !matches!(node.kind_id().into(), ClassDefinition) {
713            return;
714        }
715
716        // Mark the current space as a class space so the metric is
717        // emitted (otherwise it is suppressed by `is_disabled`).
718        if stats.is_disabled() {
719            stats.is_class_space = true;
720        }
721
722        let Some(body) = python_class_body(node) else {
723            return;
724        };
725
726        // Counts of distinct class attributes (class-level + self.*).
727        // `self.x` may appear in several methods — and in different
728        // branches of the same method — but per Fitzpatrick's intent
729        // each *attribute* counts once. We deduplicate by the
730        // attribute identifier text (read via the `code` bytes
731        // widened into the trait by #219), so:
732        //   class C:
733        //       def __init__(self): self.value = None
734        //       def reset(self):    self.value = None
735        // counts `value` once, not twice. Closes #215.
736        let class_level = python_count_class_level_attrs(&body);
737        let self_attrs = python_count_unique_self_attrs(&body, code);
738        let total = class_level + self_attrs;
739
740        stats.class_na += total;
741        // No visibility keyword in Python — every attribute is "public".
742        stats.class_npa += total;
743    }
744}
745
746// Rust attribute counting.
747//
748// Rust's "class" maps to a `struct` plus its `impl` blocks. Since each
749// `impl` block opens its own func_space (`SpaceKind::Impl`), the
750// natural place to record attributes per "class" is at the impl space
751// and at the struct itself:
752//
753// 1. `StructItem`: every direct child in the struct's
754//    `field_declaration_list` (named fields) or
755//    `ordered_field_declaration_list` (tuple-struct positional fields)
756//    is one attribute. Because `struct_item` is NOT a func_space, the
757//    fields are attributed to whichever func_space is on the stack
758//    when the StructItem is visited (typically `Unit`). The enclosing
759//    space is marked as a class space so the npa metric is emitted.
760//
761// 2. `ImplItem`: every `ConstItem` and `StaticItem` direct child of the
762//    impl's `declaration_list` is one associated attribute. These
763//    accumulate on the Impl space (which is itself a class-style
764//    func_space).
765//
766// 3. `TraitItem`: every `ConstItem`, `StaticItem`, and `AssociatedType`
767//    direct child of the trait's `declaration_list` is one attribute.
768//    Trait members are always visible to implementers, so they are
769//    counted as public (`interface_npa == interface_na`), mirroring
770//    Java's interface-body rule.
771//
772// Limitations (documented):
773// - Multiple `impl Foo` blocks each open their own Impl space and
774//   accumulate independently. Their `_sum` accumulators roll up to
775//   the parent during finalisation, so the file-level
776//   `class_npa_sum` is the sum across every impl.
777// - Struct fields are attributed to the enclosing func_space (usually
778//   Unit), not to a per-struct space. Two structs in the same module
779//   therefore contribute to the same `class_na` bucket on that Unit.
780//   This matches the issue's intent of "count struct fields + impl
781//   associated consts" without inventing a synthetic per-struct
782//   space.
783// - Enum variants are NOT counted as attributes (they are sum-type
784//   tags, not data fields), mirroring Kotlin's `enum_class_body`
785//   treatment.
786impl Npa for RustCode {
787    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
788        use Rust::*;
789
790        // Mark Impl / Trait spaces as class spaces so the metric is
791        // emitted on them.
792        if matches!(node.kind_id().into(), ImplItem | TraitItem) && stats.is_disabled() {
793            stats.is_class_space = true;
794        }
795
796        match node.kind_id().into() {
797            // Counted on the StructItem so each struct's fields are
798            // tallied exactly once. The enclosing func_space (Unit or
799            // nested) is the recipient — marking it a class space
800            // makes the npa metric visible.
801            StructItem => {
802                let mut attrs = 0;
803                let mut public_attrs = 0;
804                for body in node.children() {
805                    match body.kind_id().into() {
806                        // Named-field struct: each `field_declaration`
807                        // is one attribute. Visibility is the
808                        // `visibility_modifier` first child.
809                        FieldDeclarationList => {
810                            for field in body
811                                .children()
812                                .filter(|c| matches!(c.kind_id().into(), FieldDeclaration))
813                            {
814                                attrs += 1;
815                                if rust_item_is_public(&field) {
816                                    public_attrs += 1;
817                                }
818                            }
819                        }
820                        // Tuple struct: the field count is positional.
821                        // The grammar emits each field as either a
822                        // type-bearing node (`primitive_type`,
823                        // `type_identifier`, `generic_type`, ...) or a
824                        // `visibility_modifier` followed by such a
825                        // node. We count one attribute per non-token
826                        // child that is not a delimiter, comma, or
827                        // visibility modifier.
828                        OrderedFieldDeclarationList => {
829                            let (count, public) = rust_count_tuple_struct_fields(&body);
830                            attrs += count;
831                            public_attrs += public;
832                        }
833                        _ => {}
834                    }
835                }
836                if attrs > 0 {
837                    if stats.is_disabled() {
838                        stats.is_class_space = true;
839                    }
840                    stats.class_na += attrs;
841                    stats.class_npa += public_attrs;
842                }
843            }
844            // Associated const/static declared in an `impl` block.
845            // The current top-of-stack is the Impl space (because we
846            // are inside its body), so attribution lands there.
847            ConstItem | StaticItem => {
848                let Some(parent) = node.parent() else {
849                    return;
850                };
851                let Some(grand) = parent.parent() else {
852                    return;
853                };
854                match grand.kind_id().into() {
855                    ImplItem if matches!(parent.kind_id().into(), DeclarationList) => {
856                        stats.class_na += 1;
857                        if rust_item_is_public(node) {
858                            stats.class_npa += 1;
859                        }
860                    }
861                    TraitItem if matches!(parent.kind_id().into(), DeclarationList) => {
862                        stats.interface_na += 1;
863                        stats.interface_npa = stats.interface_na;
864                    }
865                    _ => {}
866                }
867            }
868            // `type Foo;` inside a trait body is an associated type —
869            // a placeholder bound that the implementer must supply.
870            // Counted as an interface attribute, public by default.
871            AssociatedType => {
872                let Some(parent) = node.parent() else {
873                    return;
874                };
875                let Some(grand) = parent.parent() else {
876                    return;
877                };
878                if matches!(grand.kind_id().into(), TraitItem)
879                    && matches!(parent.kind_id().into(), DeclarationList)
880                {
881                    stats.interface_na += 1;
882                    stats.interface_npa = stats.interface_na;
883                }
884            }
885            _ => {}
886        }
887    }
888}
889
890// Go attribute counting.
891//
892// Go has no `class` concept; struct types declared at file scope
893// (`type Foo struct { … }`) play that role. Methods live separately
894// as `MethodDeclaration` nodes attached to a receiver type. Because
895// `StructType` is NOT a func_space (per `Checker::is_func_space`),
896// the iterator visits it with the enclosing func_space's stats
897// (typically the file-level `Unit`). Each direct `FieldDeclaration`
898// child of the struct's `FieldDeclarationList` counts as one
899// attribute, including embedded types (an embedded type parses as a
900// `FieldDeclaration` with no name field, just a type — still one
901// attribute per the issue spec).
902//
903// Visibility note: Go exports identifiers whose first character is
904// uppercase. The `Npa::compute` trait signature does not include the
905// source byte slice, so reading the identifier text from the node
906// alone is not possible. We therefore treat every counted attribute
907// as public (`class_npa == class_na`), matching the choice Python's
908// Npm makes when no visibility token is present in the AST. The
909// alternative — adding a `code: &[u8]` parameter to the trait — is a
910// cross-language API change out of scope for this fix.
911//
912// Limitations:
913// - Struct fields are attributed to the enclosing func_space (the
914//   file's `Unit`, or a local function space for `type T struct{…}`
915//   declared inside a function body). Multiple structs at the same
916//   level contribute to the same `class_na` bucket. This mirrors the
917//   Rust impl's "fields land on the enclosing space" approach.
918// - Interface methods (`interface { Foo() }`) are not attributes —
919//   they are method signatures, counted by Npm under
920//   `interface_nm`, not by Npa.
921impl Npa for GoCode {
922    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
923        use Go as G;
924
925        if !matches!(node.kind_id().into(), G::StructType) {
926            return;
927        }
928
929        // The struct body is the `field_declaration_list` direct
930        // child. An empty struct (`struct{}`) has the list with no
931        // FieldDeclaration children → 0 attributes.
932        let Some(body) = node
933            .children()
934            .find(|c| matches!(c.kind_id().into(), G::FieldDeclarationList))
935        else {
936            return;
937        };
938
939        let attrs = body
940            .children()
941            .filter(|c| matches!(c.kind_id().into(), G::FieldDeclaration))
942            .count();
943
944        if attrs == 0 {
945            return;
946        }
947
948        if stats.is_disabled() {
949            stats.is_class_space = true;
950        }
951        stats.class_na += attrs;
952        // Visibility cannot be detected without the source bytes;
953        // every field is treated as public (see module-level note).
954        stats.class_npa += attrs;
955    }
956}
957
958impl Npa for CppCode {
959    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
960        use Cpp::*;
961
962        // Mark class / struct spaces as class spaces so the metric is
963        // emitted on them.
964        if matches!(node.kind_id().into(), ClassSpecifier | StructSpecifier) && stats.is_disabled()
965        {
966            stats.is_class_space = true;
967        }
968
969        if !matches!(node.kind_id().into(), FieldDeclarationList) {
970            return;
971        }
972        let Some(parent) = node.parent() else {
973            return;
974        };
975        // C++ `class` defaults to private; `struct` defaults to public.
976        let mut current_is_public = match parent.kind_id().into() {
977            ClassSpecifier => false,
978            StructSpecifier => true,
979            _ => return,
980        };
981
982        for child in node.children() {
983            match child.kind_id().into() {
984                AccessSpecifier => {
985                    // Update the current visibility to the access
986                    // specifier's keyword. `protected` is bucketed with
987                    // `private` for `npa` purposes (matches Java's
988                    // "non-public" treatment), so any keyword other
989                    // than `public` flips us back to private.
990                    current_is_public = child
991                        .first_child(|id| {
992                            id == Cpp::Public || id == Cpp::Protected || id == Cpp::Private
993                        })
994                        .is_some_and(|tok| tok.kind_id() == Cpp::Public);
995                }
996                FieldDeclaration => {
997                    // Member functions surface as `field_declaration`
998                    // when declared without a body. They are counted
999                    // by `Npm`, not as attributes — detect them by
1000                    // their `function_declarator` and skip.
1001                    if cpp_has_function_declarator(&child) {
1002                        continue;
1003                    }
1004                    // Data field — count every `field_identifier` in
1005                    // the declarator subtree. Pointer (`int* p`),
1006                    // array (`int a[N]`), and plain (`int x`) forms
1007                    // all reduce to one or more `field_identifier`
1008                    // leaves; the comma-separated form `int b, c`
1009                    // adds them as siblings.
1010                    let count = cpp_count_field_identifiers(&child);
1011                    stats.class_na += count;
1012                    if current_is_public {
1013                        stats.class_npa += count;
1014                    }
1015                }
1016                _ => {}
1017            }
1018        }
1019    }
1020}
1021
1022pub(crate) fn cpp_has_function_declarator(node: &Node) -> bool {
1023    use Cpp::*;
1024    node.children().any(|child| match child.kind_id().into() {
1025        FunctionDeclarator | FunctionDeclarator2 | FunctionDeclarator3 => true,
1026        // Recurse through declarator wrappers that can sit above the
1027        // function_declarator (`Foo* operator->()`,
1028        // `template<...> T fn();`, constructor / destructor
1029        // `declaration`s inside a class body).
1030        PointerDeclarator | PointerDeclarator2 | ReferenceDeclarator | ReferenceDeclarator2
1031        | ReferenceDeclarator3 | ReferenceDeclarator4 | Declaration | Declaration2
1032        | Declaration3 | Declaration4 => cpp_has_function_declarator(&child),
1033        _ => false,
1034    })
1035}
1036
1037pub(crate) fn cpp_count_field_identifiers(node: &Node) -> usize {
1038    use Cpp::*;
1039    let mut count = 0;
1040    for child in node.children() {
1041        match child.kind_id().into() {
1042            FieldIdentifier => count += 1,
1043            PointerDeclarator | PointerDeclarator2 | ArrayDeclarator | ArrayDeclarator2
1044            | ArrayDeclarator3 | InitDeclarator | ReferenceDeclarator | ReferenceDeclarator2
1045            | ReferenceDeclarator3 | ReferenceDeclarator4 => {
1046                count += cpp_count_field_identifiers(&child);
1047            }
1048            _ => {}
1049        }
1050    }
1051    count
1052}
1053
1054// Counts positional fields inside an `ordered_field_declaration_list`
1055// (tuple struct). Each non-token child that is a type node represents
1056// one field. A leading `visibility_modifier` may decorate the field;
1057// counts that field as public. Returns `(total_count, public_count)`.
1058fn rust_count_tuple_struct_fields(list: &Node) -> (usize, usize) {
1059    use Rust::*;
1060
1061    let mut total = 0;
1062    let mut public = 0;
1063    let mut pending_pub = false;
1064    for child in list.children() {
1065        match child.kind_id().into() {
1066            // Open / close parens and comma separators — skipped.
1067            LPAREN | RPAREN | COMMA => {
1068                pending_pub = false;
1069            }
1070            // `pub` / `pub(crate)` / `pub(super)` / ... — applies to
1071            // the next type child.
1072            VisibilityModifier => {
1073                pending_pub = true;
1074            }
1075            // `attribute_item` decorates the next field but does not
1076            // contribute to visibility. Skip without resetting the
1077            // pending-pub flag. `line_comment` / `block_comment` may
1078            // sit between fields (e.g. `pub struct Foo(/* x */ i32);`)
1079            // and similarly must not count as a field.
1080            AttributeItem | LineComment | BlockComment => {}
1081            // Any other child is treated as a positional field type
1082            // (primitive_type, type_identifier, generic_type,
1083            // reference_type, tuple_type, ...). One increment per
1084            // type child.
1085            _ => {
1086                total += 1;
1087                if pending_pub {
1088                    public += 1;
1089                }
1090                pending_pub = false;
1091            }
1092        }
1093    }
1094    (total, public)
1095}
1096
1097// Returns `true` if `pat` contains exactly one `UNDERSCORE` token
1098// (identified by `underscore_id`) and no other named children.
1099// Anonymous tokens such as a leading `|` in a Rust or-pattern
1100// (`| _ => ...`) are skipped — they do not change the semantic
1101// meaning of the pattern.
1102//
1103// Shared between languages whose `default:`-equivalent wildcard
1104// pattern is a single `_`:
1105//   - Rust `match_pattern` (`Cyclomatic` and `Abc` for `RustCode`)
1106//   - Python `case_pattern` (`Abc` for `PythonCode`)
1107//
1108// The Rust caller passes its grammar's `UNDERSCORE` kind id; Python
1109// passes its own. Guard handling is the caller's responsibility —
1110// in Rust the guard is a sibling inside `match_pattern` and so adds
1111// a named child here (this helper returns `false`); in Python the
1112// guard is an `if_clause` sibling on the enclosing `case_clause`,
1113// so the caller must check the surrounding node separately.
1114pub(crate) fn pattern_is_bare_underscore(pat: &Node, underscore_id: u16) -> bool {
1115    let mut found_underscore = false;
1116    for child in pat.children() {
1117        if child.kind_id() == underscore_id {
1118            if found_underscore {
1119                return false;
1120            }
1121            found_underscore = true;
1122        } else if child.is_named() {
1123            return false;
1124        }
1125        // else: anonymous non-`_` token (like `|`) — skip.
1126    }
1127    found_underscore
1128}
1129
1130// Returns `true` iff a Python `case_clause` should count as a
1131// non-trivial decision: either the pattern is not a bare `_`, or
1132// the clause carries an `if`-guard (`case _ if g:`).
1133//
1134// Shared between the `Cyclomatic` and `Abc` implementations for
1135// `PythonCode`. The bare wildcard without a guard is Python's
1136// `default:`-equivalent and is filtered out, matching Rust's bare-`_`
1137// MatchArm rule and Java/C#'s `default:` rule.
1138//
1139// `underscore_id` is the grammar's `Python::UNDERSCORE` kind id,
1140// passed in so the helper does not assume a particular module-path
1141// to the language enum.
1142pub(crate) fn python_case_clause_counts(node: &Node, underscore_id: u16) -> bool {
1143    let mut bare_underscore = false;
1144    for child in node.children() {
1145        match child.kind_id().into() {
1146            Python::IfClause => return true,
1147            Python::CasePattern => {
1148                bare_underscore = pattern_is_bare_underscore(&child, underscore_id);
1149                if !bare_underscore {
1150                    return true;
1151                }
1152            }
1153            _ => {}
1154        }
1155    }
1156    !bare_underscore
1157}
1158
1159// Returns true if `node`'s first child is a `visibility_modifier`
1160// containing the `pub` keyword. Matches Rust's "public-only-when-`pub`"
1161// model — `pub(crate)` / `pub(super)` / `pub(in path)` are also
1162// `visibility_modifier` and count as public for ABC purposes
1163// (`pub(crate)` is still "public to its crate"); only the absence of
1164// `pub` means private.
1165pub(crate) fn rust_item_is_public(node: &Node) -> bool {
1166    node.children()
1167        .any(|c| c.kind_id() == Rust::VisibilityModifier)
1168}
1169
1170// Returns the `Block2` body child of a `ClassDefinition` if present.
1171// `ClassDefinition` children are: `class` keyword, identifier,
1172// optional type-parameters, optional argument-list (base classes),
1173// `:`, `Block2`. The body is always the final child.
1174fn python_class_body<'a>(class_def: &Node<'a>) -> Option<Node<'a>> {
1175    class_def.children().find(|c| c.kind_id() == Python::Block2)
1176}
1177
1178// Counts class-level attribute assignments: direct
1179// `ExpressionStatement` children of the class body whose contained
1180// `Assignment` carries an `=` token (excluding bare type-only
1181// annotations like `x: int`, which parse as `Assignment` without an
1182// `=` — these declare a type but bind nothing and are not counted as
1183// attributes).
1184fn python_count_class_level_attrs(body: &Node) -> usize {
1185    use Python::*;
1186
1187    let mut count = 0_usize;
1188    for stmt in body.children() {
1189        if stmt.kind_id() != ExpressionStatement {
1190            continue;
1191        }
1192        for child in stmt.children() {
1193            if child.kind_id() == Assignment && child.first_child(|id| id == EQ).is_some() {
1194                count += 1;
1195            }
1196        }
1197    }
1198    count
1199}
1200
1201// Like `python_count_self_assignments` but deduplicates by the
1202// attribute identifier text. Walks every method body once and
1203// collects the set of unique `self.<attr>` names; the count is the
1204// size of that set. Fixes #215 — re-binding `self.x` across methods
1205// or across branches no longer inflates the attribute count.
1206//
1207// Capacity hint: typical Python classes declare under a dozen
1208// instance attributes (often documented as a class-level
1209// `__slots__`); `with_capacity(8)` covers the common case without
1210// any rehash and costs negligibly when a class has fewer.
1211fn python_count_unique_self_attrs(body: &Node, code: &[u8]) -> usize {
1212    let mut seen: std::collections::HashSet<&[u8]> = std::collections::HashSet::with_capacity(8);
1213    for stmt in body.children() {
1214        if let Some(func) = python_unwrap_function(&stmt) {
1215            python_collect_self_attrs_in_subtree(&func, code, &mut seen);
1216        }
1217    }
1218    seen.len()
1219}
1220
1221fn python_collect_self_attrs_in_subtree<'a>(
1222    root: &Node<'a>,
1223    code: &'a [u8],
1224    seen: &mut std::collections::HashSet<&'a [u8]>,
1225) {
1226    use Python::*;
1227
1228    let mut stack: Vec<Node<'a>> = Vec::with_capacity(32);
1229    for child in root.children() {
1230        stack.push(child);
1231    }
1232    while let Some(node) = stack.pop() {
1233        // Boundary: do not descend into nested classes, functions, or
1234        // lambdas. Their attributes belong to their inner scope.
1235        if matches!(
1236            node.kind_id().into(),
1237            FunctionDefinition | ClassDefinition | DecoratedDefinition | Lambda
1238        ) {
1239            continue;
1240        }
1241
1242        if node.kind_id() == Assignment
1243            && python_lhs_is_self_attribute(&node)
1244            && let Some(name) = python_self_attr_name_bytes(&node, code)
1245        {
1246            seen.insert(name);
1247        }
1248
1249        for child in node.children() {
1250            stack.push(child);
1251        }
1252    }
1253}
1254
1255// Returns the byte slice for the attribute identifier in a
1256// `self.<attr> = …` assignment. The LHS is an `Attribute` node whose
1257// last named child is the attribute identifier (the `.` and the
1258// preceding `self` are siblings; the identifier comes last).
1259// Borrows directly from `code` so the returned slice is the
1260// canonical key for deduplication — two `self.value` assignments
1261// share the same identifier text and therefore the same key.
1262fn python_self_attr_name_bytes<'a>(assignment: &Node<'a>, code: &'a [u8]) -> Option<&'a [u8]> {
1263    // Fully-qualified `Python::Attribute` / `Python::Identifier` — this
1264    // function deliberately does NOT `use Python::*;` so unqualified
1265    // `None` keeps its `Option` meaning rather than being shadowed by
1266    // the `Python::None` token kind.
1267    let target = assignment.child(0)?;
1268    if target.kind_id() != Python::Attribute {
1269        return None;
1270    }
1271    // The grammar guarantees the trailing identifier is the last
1272    // `Identifier` child of the Attribute node; `.last()` walks the
1273    // children once and yields the right one.
1274    let id = target
1275        .children()
1276        .filter(|c| c.kind_id() == Python::Identifier)
1277        .last()?;
1278    // First `Identifier` was the receiver (`self`); only count when
1279    // we have a distinct second Identifier (the attribute name).
1280    let receiver = target.child(0)?;
1281    if id.start_byte() == receiver.start_byte() {
1282        return None;
1283    }
1284    code.get(id.start_byte()..id.end_byte())
1285}
1286
1287fn python_unwrap_function<'a>(node: &Node<'a>) -> Option<Node<'a>> {
1288    // Use fully-qualified names here: `use Python::*` would shadow
1289    // `Option::None` with `Python::None` and break the last arm.
1290    match node.kind_id().into() {
1291        Python::FunctionDefinition => Some(*node),
1292        Python::DecoratedDefinition => node
1293            .children()
1294            .find(|c| c.kind_id() == Python::FunctionDefinition),
1295        _ => None,
1296    }
1297}
1298
1299// Checks whether the LHS of an `Assignment` is `self.<identifier>`.
1300// `Assignment` children are: target, optional `:` + type, `=`, value.
1301// The target is the first child; for `self.x` it parses as an
1302// `Attribute` node with three children: identifier "self", `.`, and
1303// the attribute identifier. We cannot read the "self" text without
1304// source bytes, so we use the structural shape (Attribute whose
1305// first child is an Identifier) as a robust proxy. Standard Python
1306// style binds instance attributes via the *only* available alias
1307// inside a method body — the first parameter, conventionally called
1308// `self` — so the structural check is a safe under-approximation:
1309// it captures `self.x`, `this.x`, `cls.x` (i.e. classmethod alias),
1310// and any user-renamed first parameter alike. All three are
1311// idiomatic forms of "instance / class attribute assignment".
1312fn python_lhs_is_self_attribute(assignment: &Node) -> bool {
1313    use Python::*;
1314
1315    let Some(target) = assignment.child(0) else {
1316        return false;
1317    };
1318    if target.kind_id() != Attribute {
1319        return false;
1320    }
1321    target.child(0).is_some_and(|c| c.kind_id() == Identifier)
1322}
1323
1324// Kotlin's grammar models classes and interfaces under a single
1325// `class_declaration` node; the `class` / `interface` keyword child
1326// disambiguates. A `ClassBody` belongs to an interface iff its parent
1327// `class_declaration` has an `interface` keyword child.
1328pub(crate) fn kotlin_class_body_is_interface(class_body: &Node) -> bool {
1329    class_body.parent().is_some_and(|p| {
1330        matches!(p.kind_id().into(), Kotlin::ClassDeclaration)
1331            && p.first_child(|id| id == Kotlin::Interface).is_some()
1332    })
1333}
1334
1335// Counts how many `VariableDeclaration`s a Kotlin `PropertyDeclaration`
1336// introduces. Kotlin allows destructuring (`val (a, b) = pair`) via
1337// `MultiVariableDeclaration`; each leaf binding counts as one attribute.
1338// Empty multi-variable destructurings cannot occur in well-formed Kotlin,
1339// but a defensive `.max(1)` keeps `property_declaration` at ≥1 attribute
1340// (matches the C# accessor-counting fallback).
1341fn kotlin_count_property_attrs(decl: &Node) -> usize {
1342    use Kotlin::*;
1343    decl.children()
1344        .map(|c| match c.kind_id().into() {
1345            VariableDeclaration => 1,
1346            MultiVariableDeclaration => c
1347                .children()
1348                .filter(|n| matches!(n.kind_id().into(), VariableDeclaration))
1349                .count(),
1350            _ => 0,
1351        })
1352        .sum::<usize>()
1353        .max(1)
1354}
1355
1356// Kotlin's default visibility is `public`. A declaration is non-public
1357// only when it carries an explicit `private` / `protected` / `internal`
1358// modifier under its `Modifiers` child. Returns `true` for missing
1359// `Modifiers`, missing `VisibilityModifier`, or an explicit `public`
1360// modifier.
1361pub(crate) fn kotlin_is_public(decl: &Node) -> bool {
1362    let Some(modifiers) = decl.first_child(|id| id == Kotlin::Modifiers) else {
1363        return true;
1364    };
1365    let Some(visibility) = modifiers.first_child(|id| id == Kotlin::VisibilityModifier) else {
1366        return true;
1367    };
1368    // The visibility modifier holds exactly one keyword child; absence or
1369    // an explicit `public` both mean public.
1370    visibility
1371        .first_child(|id| {
1372            matches!(
1373                id.into(),
1374                Kotlin::Private | Kotlin::Protected | Kotlin::Internal
1375            )
1376        })
1377        .is_none()
1378}
1379
1380impl Npa for KotlinCode {
1381    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1382        use Kotlin::*;
1383
1384        // Enables the `Npa` metric for both class and interface spaces
1385        // (and `object` singletons, which `Getter` reports as `Class`).
1386        if Self::is_func_space(node) && stats.is_disabled() {
1387            stats.is_class_space = true;
1388        }
1389
1390        match node.kind_id().into() {
1391            // A `ClassParameter` carrying `val` / `var` is a Kotlin
1392            // primary-constructor parameter property — counts once toward
1393            // the enclosing class. Parameters without `val`/`var` are plain
1394            // constructor arguments, not attributes.
1395            ClassParameter
1396                if node
1397                    .children()
1398                    .any(|c| matches!(c.kind_id().into(), Val | Var)) =>
1399            {
1400                stats.class_na += 1;
1401                if kotlin_is_public(node) {
1402                    stats.class_npa += 1;
1403                }
1404            }
1405            // Every `ClassBody` we visit attributes its direct
1406            // `property_declaration` children to whichever func_space is
1407            // currently on the state stack. Companion objects are not
1408            // func_spaces, so companion `val`/`var` declarations land on
1409            // the enclosing class — matching Kotlin's "static members"
1410            // semantics. Nested class / interface bodies start a new
1411            // func_space (handled by `spaces.rs`), so they do NOT leak
1412            // attributes into their outer space.
1413            ClassBody => {
1414                let is_interface = kotlin_class_body_is_interface(node);
1415                // tree-sitter-kotlin elides the `class_member_declaration`
1416                // and `declaration` rule layers when those rules are pure
1417                // forwarding choices, so property declarations appear as
1418                // direct children of `class_body`.
1419                for prop in node
1420                    .children()
1421                    .filter(|c| matches!(c.kind_id().into(), PropertyDeclaration))
1422                {
1423                    let attrs = kotlin_count_property_attrs(&prop);
1424                    if is_interface {
1425                        stats.interface_na += attrs;
1426                        // Interface members are always public.
1427                        stats.interface_npa += attrs;
1428                    } else {
1429                        stats.class_na += attrs;
1430                        if kotlin_is_public(&prop) {
1431                            stats.class_npa += attrs;
1432                        }
1433                    }
1434                }
1435            }
1436            _ => {}
1437        }
1438    }
1439}
1440
1441// TypeScript / TSX share the same OOP node shape: `class_declaration`
1442// and `abstract_class_declaration` both contain a `class_body`;
1443// `interface_declaration` contains an `interface_body`. The
1444// `ts_npa_compute!` macro expands the same compute logic for each enum,
1445// so TS and TSX cannot drift.
1446//
1447// Visibility rule: a `public_field_definition` or `method_definition`
1448// is considered public unless it carries an explicit
1449// `accessibility_modifier` child whose only child is `private` or
1450// `protected`. Default (no modifier) is public, matching TypeScript's
1451// own semantics.
1452//
1453// Parameter properties (`constructor(private x: number)`) are class
1454// attributes: each `required_parameter` carrying an
1455// `accessibility_modifier` adds one to the enclosing class's `na`
1456// (and to `npa` when the modifier is `public` or absent). The
1457// grammar allows accessibility modifiers on parameters of any
1458// `method_definition`, not only `constructor` — TypeScript itself
1459// rejects that at type-check time, but accepting any method here
1460// avoids fragile name-matching against the `constructor` identifier
1461// (the grammar does not expose a dedicated constructor token).
1462//
1463// Interface decision: `property_signature` children of
1464// `interface_body` count toward `interface_npa` / `interface_na`.
1465// All interface members are implicitly public (TypeScript spec).
1466// `index_signature` and `method_signature` are NOT attributes — they
1467// belong to `npm`.
1468macro_rules! ts_npa_compute {
1469    ($lang:ident) => {
1470        fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1471            use $lang::*;
1472
1473            if Self::is_func_space(node) && stats.is_disabled() {
1474                stats.is_class_space = true;
1475            }
1476
1477            match node.kind_id().into() {
1478                ClassBody => {
1479                    for member in node.children() {
1480                        match member.kind_id().into() {
1481                            // Plain field declaration (`x: T = expr;`, `private x: T;`,
1482                            // `static x: T = expr;`). Each is one attribute.
1483                            // Skip fields whose initializer is an arrow function or
1484                            // function expression — those are methods written as
1485                            // field initializers and are counted by `npm` instead.
1486                            PublicFieldDefinition
1487                                if member
1488                                    .first_child(|id| {
1489                                        id == $lang::ArrowFunction
1490                                            || id == $lang::FunctionExpression
1491                                    })
1492                                    .is_none() =>
1493                            {
1494                                stats.class_na += 1;
1495                                if ts_member_is_public!($lang, member) {
1496                                    stats.class_npa += 1;
1497                                }
1498                            }
1499                            // Parameter properties on any `method_definition`. In
1500                            // practice these only appear on the constructor.
1501                            // Scan formal_parameters at the class-body level so
1502                            // the attribute lands on the class space, not the
1503                            // method's own function space.
1504                            MethodDefinition => {
1505                                let Some(params) =
1506                                    member.first_child(|id| id == $lang::FormalParameters)
1507                                else {
1508                                    continue;
1509                                };
1510                                for param in params.children().filter(|c| {
1511                                    matches!(
1512                                        c.kind_id().into(),
1513                                        RequiredParameter | RequiredParameter2
1514                                    )
1515                                }) {
1516                                    if param
1517                                        .first_child(|id| id == $lang::AccessibilityModifier)
1518                                        .is_some()
1519                                    {
1520                                        stats.class_na += 1;
1521                                        if ts_member_is_public!($lang, param) {
1522                                            stats.class_npa += 1;
1523                                        }
1524                                    }
1525                                }
1526                            }
1527                            _ => {}
1528                        }
1529                    }
1530                }
1531                InterfaceBody => {
1532                    let count = node
1533                        .children()
1534                        .filter(|c| matches!(c.kind_id().into(), PropertySignature))
1535                        .count();
1536                    stats.interface_na += count;
1537                    stats.interface_npa = stats.interface_na;
1538                }
1539                _ => {}
1540            }
1541        }
1542    };
1543}
1544
1545// Class members are public unless they declare an explicit
1546// `accessibility_modifier` whose only child is `private` or `protected`.
1547// Missing modifier means public, matching TypeScript's spec. The helper
1548// is a macro rather than a generic function so both TS and TSX expand
1549// the same code against their own enum without a marker trait.
1550macro_rules! ts_member_is_public {
1551    ($lang:ident, $member:expr) => {{
1552        match $member.first_child(|id| id == $lang::AccessibilityModifier) {
1553            None => true,
1554            Some(m) => m
1555                .first_child(|id| id == $lang::Private || id == $lang::Protected)
1556                .is_none(),
1557        }
1558    }};
1559}
1560pub(crate) use ts_member_is_public;
1561
1562impl Npa for TypescriptCode {
1563    ts_npa_compute!(Typescript);
1564}
1565
1566impl Npa for TsxCode {
1567    ts_npa_compute!(Tsx);
1568}
1569
1570// JavaScript / Mozjs share the same class vocabulary. JS has no
1571// `accessibility_modifier` — every class member is public, so each
1572// class field maps 1:1 to both `na` and `npa`.
1573//
1574// We count ES2022 class fields (`class Foo { x = 1; }`):
1575// `field_definition` direct children of `class_body`. Fields whose
1576// initializer is an `arrow_function` or `function_expression` are
1577// methods written as field initializers and belong to `Npm`, not
1578// `Npa`.
1579//
1580// Prototype-based attribute assignments (`Foo.prototype.x = 5;`)
1581// would also be legitimate JS attributes per Fenton's metric
1582// taxonomy, but detecting them requires matching the `prototype`
1583// property-identifier text. The `Npa::compute` trait signature
1584// does not carry source bytes, so prototype-shaped attributes are
1585// intentionally not counted by this impl. Modern ES2015+ class
1586// syntax — the dominant style — is unaffected; legacy prototype-
1587// only files under-report. A follow-up that widens the trait
1588// signature to `(node, code, stats)` would unlock prototype
1589// detection (see `Abc::compute` for the existing pattern).
1590
1591macro_rules! js_npa_compute {
1592    ($lang:ident) => {
1593        fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
1594            use $lang::*;
1595
1596            if Self::is_func_space(node) && stats.is_disabled() {
1597                stats.is_class_space = true;
1598            }
1599
1600            if !matches!(node.kind_id().into(), ClassBody) {
1601                return;
1602            }
1603
1604            for member in node.children() {
1605                if matches!(member.kind_id().into(), FieldDefinition)
1606                    && member
1607                        .first_child(|id| {
1608                            id == $lang::ArrowFunction || id == $lang::FunctionExpression
1609                        })
1610                        .is_none()
1611                {
1612                    stats.class_na += 1;
1613                    stats.class_npa += 1;
1614                }
1615            }
1616        }
1617    };
1618}
1619
1620impl Npa for JavascriptCode {
1621    js_npa_compute!(Javascript);
1622}
1623
1624impl Npa for MozjsCode {
1625    js_npa_compute!(Mozjs);
1626}
1627
1628// Default no-op `Npa` impls. Audited in #188.
1629//
1630// Real defaults (no first-class class / OO grammar construct, so the
1631// metric is genuinely 0):
1632//   - PreprocCode, CcommentCode: no executable code.
1633//   - BashCode: shell has no class concept.
1634//   - PerlCode, LuaCode, TclCode: prototype / table / package-based
1635//     OO is convention-only, not a grammar construct the audit treats
1636//     as class-shaped.
1637// Elixir Npa is implemented below (#275).
1638implement_metric_trait!(
1639    Npa,
1640    PreprocCode,
1641    CcommentCode,
1642    PerlCode,
1643    BashCode,
1644    LuaCode,
1645    TclCode
1646);
1647
1648// Elixir Npa (#275). `defmodule` is treated as a class via source-aware
1649// Checker dispatch; `defstruct` is its closest analog to a field-set
1650// declaration. When entering a `defmodule` Class space we look for a
1651// direct-child `defstruct` Call in the `do_block` and count its
1652// field arguments. Three syntactic forms are accepted, matching the
1653// Elixir docs (https://hexdocs.pm/elixir/Kernel.html#defstruct/1):
1654//
1655// - `defstruct [:a, :b]` — bracketed list of atoms.
1656// - `defstruct a: 1, b: 2` — bare keyword list (the most common form).
1657// - `defstruct [a: 1, b: 2]` — bracketed keyword list.
1658//
1659// All fields are counted as public (`class_npa`); Elixir struct fields
1660// have no Java-style visibility modifier and the runtime exposes every
1661// field via pattern matching and `Map.get/2`.
1662impl Npa for ElixirCode {
1663    fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
1664        use crate::metrics::cognitive::{elixir_call_keyword, elixir_do_block_call_children};
1665
1666        if !stats.is_disabled() || !Self::is_func_space_with_code(node, code) {
1667            return;
1668        }
1669        if !matches!(elixir_call_keyword(node, code), Some("defmodule")) {
1670            return;
1671        }
1672
1673        stats.is_class_space = true;
1674
1675        for stmt in elixir_do_block_call_children(node) {
1676            if matches!(elixir_call_keyword(&stmt, code), Some("defstruct")) {
1677                let fields = count_defstruct_fields(&stmt);
1678                stats.class_na += fields;
1679                stats.class_npa += fields;
1680            }
1681        }
1682    }
1683}
1684
1685// Counts the field entries of an Elixir `defstruct` Call's arguments.
1686// `defstruct` accepts three syntactic forms:
1687//   * `defstruct [:a, :b]` — a `List` of atoms.
1688//   * `defstruct a: 1, b: 2` — a bare `Keywords` keyword list, which
1689//     in the tree-sitter-elixir grammar appears directly inside
1690//     `Arguments` without an extra wrapper.
1691//   * `defstruct [a: 1, b: 2]` — a `List` wrapping a `Keywords`.
1692// We descend through the `Arguments` / `List` / `Keywords` wrapper
1693// nodes (skipping the leading `target` Identifier that names the
1694// macro itself) and tally `Atom` leaves (bare-list form) and `Pair`s
1695// (keyword form). `defstruct nil` and an empty `defstruct` correctly
1696// return 0.
1697fn count_defstruct_fields(call: &Node) -> usize {
1698    use Elixir as E;
1699
1700    // `Arguments` is the wrapper around the macro's positional
1701    // arguments. `List` is the bracketed form. Keyword pairs without
1702    // brackets appear directly inside `Arguments` (no `Keywords`
1703    // wrapper) in the tree-sitter-elixir grammar. The leading
1704    // `target` Identifier is never one of these kinds, so no
1705    // explicit target-skip filter is needed.
1706    call.children()
1707        .filter(|child| matches!(child.kind_id().into(), E::Arguments | E::List | E::Keywords))
1708        .map(|child| count_field_entries(&child))
1709        .sum()
1710}
1711
1712fn count_field_entries(node: &Node) -> usize {
1713    use Elixir as E;
1714
1715    node.children()
1716        .map(|child| match child.kind_id().into() {
1717            // Bare-list form (`defstruct [:a, :b]`): each atom is a
1718            // field. Keyword form (`defstruct a: 1, b: 2`): each
1719            // `Pair` is a field.
1720            E::Atom | E::QuotedAtom | E::Atom2 | E::Pair => 1,
1721            // A `List` or `Keywords` may wrap the entries one level
1722            // deeper (`defstruct [a: 1, b: 2]` puts a `List` inside
1723            // `Arguments`, which then contains a `Keywords`).
1724            E::List | E::Keywords => count_field_entries(&child),
1725            _ => 0,
1726        })
1727        .sum()
1728}
1729
1730#[cfg(test)]
1731#[allow(
1732    clippy::float_cmp,
1733    clippy::cast_precision_loss,
1734    clippy::cast_possible_truncation,
1735    clippy::cast_sign_loss,
1736    clippy::similar_names,
1737    clippy::doc_markdown,
1738    clippy::needless_raw_string_hashes,
1739    clippy::too_many_lines
1740)]
1741mod tests {
1742    use crate::tools::{assert_child_space_kind, check_func_space, check_metrics};
1743
1744    use super::*;
1745
1746    #[test]
1747    fn java_single_attributes() {
1748        check_metrics::<JavaParser>(
1749            "class X {
1750                public byte a;      // +1
1751                public short b;     // +1
1752                public int c;       // +1
1753                public long d;      // +1
1754                public float e;     // +1
1755                public double f;    // +1
1756                public boolean g;   // +1
1757                public char h;      // +1
1758                byte i;
1759                short j;
1760                int k;
1761                long l;
1762                float m;
1763                double n;
1764                boolean o;
1765                char p;
1766            }",
1767            "foo.java",
1768            |metric| {
1769                insta::assert_json_snapshot!(
1770                    metric.npa,
1771                    @r###"
1772                    {
1773                      "classes": 8.0,
1774                      "interfaces": 0.0,
1775                      "class_attributes": 16.0,
1776                      "interface_attributes": 0.0,
1777                      "classes_average": 0.5,
1778                      "interfaces_average": null,
1779                      "total": 8.0,
1780                      "total_attributes": 16.0,
1781                      "average": 0.5
1782                    }"###
1783                );
1784            },
1785        );
1786    }
1787
1788    #[test]
1789    fn java_multiple_attributes() {
1790        check_metrics::<JavaParser>(
1791            "class X {
1792                public byte a1;                 // +1
1793                public short b1, b2;            // +2
1794                public int c1, c2, c3;          // +3
1795                public long d1, d2, d3, d4;     // +4
1796                public float e1, e2, e3, e4;    // +4
1797                public double f1, f2, f3;       // +3
1798                public boolean g1, g2;          // +2
1799                public char h1;                 // +1
1800                byte i1, i2, i3, i4;
1801                short j1, j2, j3;
1802                int k1, k2;
1803                long l1;
1804                float m1;
1805                double n1, n2;
1806                boolean o1, o2, o3;
1807                char p1, p2, p3, p4;
1808            }",
1809            "foo.java",
1810            |metric| {
1811                insta::assert_json_snapshot!(
1812                    metric.npa,
1813                    @r###"
1814                    {
1815                      "classes": 20.0,
1816                      "interfaces": 0.0,
1817                      "class_attributes": 40.0,
1818                      "interface_attributes": 0.0,
1819                      "classes_average": 0.5,
1820                      "interfaces_average": null,
1821                      "total": 20.0,
1822                      "total_attributes": 40.0,
1823                      "average": 0.5
1824                    }"###
1825                );
1826            },
1827        );
1828    }
1829
1830    #[test]
1831    fn java_initialized_attributes() {
1832        check_metrics::<JavaParser>(
1833            "class X {
1834                public byte a1 = 1;                             // +1
1835                public short b1 = 2, b2;                        // +2
1836                public int c1, c2 = 3, c3;                      // +3
1837                public long d1 = 4, d2, d3, d4 = 5;             // +4
1838                public float e1, e2 = 6.0f, e3 = 7.0f, e4;      // +4
1839                public double f1 = 8.0, f2 = 9.0, f3 = 10.0;    // +3
1840                public boolean g1 = true, g2;                   // +2
1841                public char h1 = 'a';                           // +1
1842                byte i1 = 1, i2 = 2, i3 = 3, i4 = 4;
1843                short j1 = 5, j2, j3 = 6;
1844                int k1, k2 = 7;
1845                long l1 = 8;
1846                float m1 = 9.0f;
1847                double n1, n2 = 10.0;
1848                boolean o1, o2 = false, o3;
1849                char p1 = 'a', p2 = 'b', p3 = 'c', p4 = 'd';
1850            }",
1851            "foo.java",
1852            |metric| {
1853                insta::assert_json_snapshot!(
1854                    metric.npa,
1855                    @r###"
1856                    {
1857                      "classes": 20.0,
1858                      "interfaces": 0.0,
1859                      "class_attributes": 40.0,
1860                      "interface_attributes": 0.0,
1861                      "classes_average": 0.5,
1862                      "interfaces_average": null,
1863                      "total": 20.0,
1864                      "total_attributes": 40.0,
1865                      "average": 0.5
1866                    }"###
1867                );
1868            },
1869        );
1870    }
1871
1872    #[test]
1873    fn java_array_attributes() {
1874        check_metrics::<JavaParser>(
1875            "class X {
1876                public byte[] a1, a2, a3, a4;                       // +4
1877                public short b1[], b2[], b3[];                      // +3
1878                public int[] c1 = { 1 }, c2;                        // +2
1879                public long d1[] = { 1 };                           // +1
1880                public float[] e1 = { 1.0f, 2.0f, 3.0f };           // +1
1881                public double f1[] = { 1.0, 2.0, 3.0 }, f2[];       // +2
1882                public boolean[] g1 = new boolean[5], g2, g3;       // +3
1883                public char[] h1 = new char[5], h2[], h3[], h4[];   // +4
1884                byte[] i1;
1885                short j1[], j2[];
1886                int[] k1, k2, k3 = { 1 };
1887                long l1[], l2[] = { 1 }, l3[] = { 2 }, l4[];
1888                float[] m1, m2, m3, m4 = { 1.0f, 2.0f, 3.0f };
1889                double n1[], n2[] = { 1.0, 2.0, 3.0 }, n3[];
1890                boolean[] o1, o2 = new boolean[5];
1891                char[] p1 = new char[5];
1892            }",
1893            "foo.java",
1894            |metric| {
1895                insta::assert_json_snapshot!(
1896                    metric.npa,
1897                    @r###"
1898                    {
1899                      "classes": 20.0,
1900                      "interfaces": 0.0,
1901                      "class_attributes": 40.0,
1902                      "interface_attributes": 0.0,
1903                      "classes_average": 0.5,
1904                      "interfaces_average": null,
1905                      "total": 20.0,
1906                      "total_attributes": 40.0,
1907                      "average": 0.5
1908                    }"###
1909                );
1910            },
1911        );
1912    }
1913
1914    #[test]
1915    fn java_object_attributes() {
1916        check_metrics::<JavaParser>(
1917            "class X {
1918                public Integer[] a1 = { 1 };                                    // +1
1919                public Integer b1, b2;                                          // +2
1920                public String[] c1 = { \"Hello\" }, c2, c3 = { \"World!\" };    // +3
1921                public String d1[][] = { { \"Hello\" }, { \"World!\" } };       // +1
1922                public Y[] e1, e2[];                                            // +2
1923                public Y f1[], f2[][], f3[][][];                                // +3
1924                Integer[] g1 = { new Integer(1) };
1925                Integer h1 = new Integer(1), h2 = new Integer(2);
1926                String[] i1, i2 = { \"Hello World!\" }, i3;
1927                String j1 = \"Hello World!\";
1928                Y[] k1[], k2;
1929                Y l1[][], l2[], l3 = new Y();
1930            }",
1931            "foo.java",
1932            |metric| {
1933                insta::assert_json_snapshot!(
1934                    metric.npa,
1935                    @r###"
1936                    {
1937                      "classes": 12.0,
1938                      "interfaces": 0.0,
1939                      "class_attributes": 24.0,
1940                      "interface_attributes": 0.0,
1941                      "classes_average": 0.5,
1942                      "interfaces_average": null,
1943                      "total": 12.0,
1944                      "total_attributes": 24.0,
1945                      "average": 0.5
1946                    }"###
1947                );
1948            },
1949        );
1950    }
1951
1952    #[test]
1953    fn groovy_no_attributes() {
1954        check_metrics::<GroovyParser>("class A { void foo() {} }", "foo.groovy", |metric| {
1955            assert_eq!(metric.npa.total_na(), 0.0);
1956            assert_eq!(metric.npa.total_npa(), 0.0);
1957        });
1958    }
1959
1960    #[test]
1961    fn groovy_public_attributes() {
1962        check_metrics::<GroovyParser>(
1963            "class A {
1964                public int x
1965                public String name
1966                private int hidden
1967            }",
1968            "foo.groovy",
1969            |metric| {
1970                // 3 total attributes, 2 public
1971                assert_eq!(metric.npa.class_na_sum(), 3.0);
1972                assert_eq!(metric.npa.class_npa_sum(), 2.0);
1973            },
1974        );
1975    }
1976
1977    #[test]
1978    fn groovy_def_attributes_not_public() {
1979        // `def field` at class scope is a FieldDeclaration whose
1980        // modifier list contains `Def`, not `Public`. Mirror Java's
1981        // semantics: only explicit `public` is counted.
1982        check_metrics::<GroovyParser>(
1983            "class A {
1984                def field1
1985                def field2
1986            }",
1987            "foo.groovy",
1988            |metric| {
1989                // Both `def` fields parse as FieldDeclarations.
1990                assert_eq!(metric.npa.class_na_sum(), 2.0);
1991                assert_eq!(metric.npa.class_npa_sum(), 0.0);
1992            },
1993        );
1994    }
1995
1996    #[test]
1997    fn groovy_interface_attributes() {
1998        // Structural `assert_child_space_kind` guards against an
1999        // `InterfaceDeclaration` revert in `GroovyCode::is_func_space`
2000        // — see #311.
2001        check_func_space::<GroovyParser, _>(
2002            "interface I {
2003                public static final int A = 1
2004                public static final int B = 2
2005            }",
2006            "foo.groovy",
2007            |func_space| {
2008                let metric = &func_space.metrics;
2009                // Interface fields are implicitly public+static+final.
2010                assert_eq!(metric.npa.interface_na_sum(), 2.0);
2011                assert_eq!(metric.npa.interface_npa_sum(), 2.0);
2012                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
2013            },
2014        );
2015    }
2016
2017    #[test]
2018    fn groovy_no_attributes_in_unit_scope() {
2019        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
2020            assert_eq!(metric.npa.total_na(), 0.0);
2021        });
2022    }
2023
2024    #[test]
2025    fn groovy_multiple_classes() {
2026        check_metrics::<GroovyParser>(
2027            "class A { public int a }
2028            class B { public int b }",
2029            "foo.groovy",
2030            |metric| {
2031                assert_eq!(metric.npa.class_na_sum(), 2.0);
2032                assert_eq!(metric.npa.class_npa_sum(), 2.0);
2033            },
2034        );
2035    }
2036
2037    #[test]
2038    fn groovy_initialized_attributes() {
2039        // Mirror of `java_initialized_attributes`: each
2040        // `variable_declarator` inside a `field_declaration` counts
2041        // as one attribute, with or without an initializer; `public`
2042        // modifier promotes them all to NPA.
2043        check_metrics::<GroovyParser>(
2044            "class X {
2045                public int a1 = 1, a2
2046                public int b1 = 2
2047                int c1, c2 = 3
2048            }",
2049            "foo.groovy",
2050            |metric| {
2051                // 5 attributes total, 3 public.
2052                assert_eq!(metric.npa.class_na_sum(), 5.0);
2053                assert_eq!(metric.npa.class_npa_sum(), 3.0);
2054            },
2055        );
2056    }
2057
2058    #[test]
2059    fn groovy_object_attributes() {
2060        // Object-typed attributes (boxed primitives, user types,
2061        // String, arrays). Each declarator is one attribute.
2062        check_metrics::<GroovyParser>(
2063            "class X {
2064                public Integer a1
2065                public String b1 = 'hello'
2066                public Y[] c1
2067            }",
2068            "foo.groovy",
2069            |metric| {
2070                assert_eq!(metric.npa.class_na_sum(), 3.0);
2071                assert_eq!(metric.npa.class_npa_sum(), 3.0);
2072            },
2073        );
2074    }
2075
2076    #[test]
2077    fn groovy_attribute_modifiers() {
2078        // Multiple modifier orderings (public/static/final/transient/
2079        // volatile etc.) must all be detected — what matters for NPA
2080        // is whether the `Modifiers` block contains `Public`.
2081        check_metrics::<GroovyParser>(
2082            "class X {
2083                public static int a
2084                static public int b
2085                public final int c = 1
2086                final public int d = 2
2087                private static int e
2088                int f
2089            }",
2090            "foo.groovy",
2091            |metric| {
2092                // 6 attributes total, 4 public (regardless of order).
2093                assert_eq!(metric.npa.class_na_sum(), 6.0);
2094                assert_eq!(metric.npa.class_npa_sum(), 4.0);
2095            },
2096        );
2097    }
2098
2099    #[test]
2100    #[ignore = "dekobon Groovy grammar v1 does not yet support inner classes inside class bodies (https://github.com/dekobon/tree-sitter-groovy SPECIFICATION.md §4 — 'Field declarations, static initialisers, and inner classes land later')"]
2101    fn groovy_nested_inner_classes() {
2102        // Each nested `class` declaration is its own class space
2103        // with its own NPA. Mirrors `java_nested_inner_classes`.
2104        check_metrics::<GroovyParser>(
2105            "class X {
2106                public int a
2107                class Y {
2108                    public boolean b
2109                    class Z {
2110                        public char c
2111                    }
2112                }
2113            }",
2114            "foo.groovy",
2115            |metric| {
2116                // 3 classes, 3 public attributes.
2117                assert_eq!(metric.npa.class_na_sum(), 3.0);
2118                assert_eq!(metric.npa.class_npa_sum(), 3.0);
2119            },
2120        );
2121    }
2122
2123    #[test]
2124    fn groovy_array_attributes() {
2125        // Array-typed attributes. Mirrors `java_array_attributes`.
2126        check_metrics::<GroovyParser>(
2127            "class X {
2128                public int[] a
2129                public String[] b
2130                int[] c
2131            }",
2132            "foo.groovy",
2133            |metric| {
2134                assert_eq!(metric.npa.class_na_sum(), 3.0);
2135                assert_eq!(metric.npa.class_npa_sum(), 2.0);
2136            },
2137        );
2138    }
2139
2140    #[test]
2141    fn groovy_anonymous_inner_class() {
2142        // Object-creation expression containing a `class_body` —
2143        // anonymous inner class. Its attributes are counted in a
2144        // separate class space.
2145        check_metrics::<GroovyParser>(
2146            "class X {
2147                public Runnable r = new Runnable() {
2148                    public int x
2149                    void run() {}
2150                }
2151            }",
2152            "foo.groovy",
2153            |metric| {
2154                // outer X has 1 public attr `r`; inner anonymous
2155                // has 1 public attr `x` => total 2.
2156                assert_eq!(metric.npa.class_na_sum(), 2.0);
2157                assert_eq!(metric.npa.class_npa_sum(), 2.0);
2158            },
2159        );
2160    }
2161
2162    // Regression for issue #280: Groovy mirrors Java's enum / record /
2163    // annotation handling. Record support in the dekobon Groovy grammar
2164    // lags behind groovyc, but the grammar exposes `record_declaration`
2165    // and the `Npa` body walker treats it identically.
2166    #[test]
2167    fn groovy_enum_counts_explicit_public_fields() {
2168        check_metrics::<GroovyParser>(
2169            "enum Status {
2170                ACTIVE, INACTIVE;
2171                public int code;
2172                private int hidden;
2173            }",
2174            "foo.groovy",
2175            |metric| {
2176                assert_eq!(metric.npa.class_na_sum(), 2.0);
2177                assert_eq!(metric.npa.class_npa_sum(), 1.0);
2178            },
2179        );
2180    }
2181
2182    #[test]
2183    fn groovy_annotation_type_counts_constants_as_implicit_public() {
2184        // The dekobon Groovy grammar parses `@interface` like Java
2185        // (modifier required, statements terminated with `;`). Mirror of
2186        // `java_annotation_type_counts_constants_as_implicit_public`
2187        // — the body-walker count is identical whether or not
2188        // Groovy's `AnnotationTypeDeclaration` is wired into
2189        // `is_func_space`, so the structural `check_func_space`
2190        // assertion is what catches a revert.
2191        check_func_space::<GroovyParser, _>(
2192            "public @interface Marker {
2193                int VERSION = 1;
2194                String NAME = \"x\";
2195            }",
2196            "foo.groovy",
2197            |func_space| {
2198                assert_eq!(func_space.metrics.npa.interface_na_sum(), 2.0);
2199                assert_eq!(func_space.metrics.npa.interface_npa_sum(), 2.0);
2200                assert_child_space_kind(&func_space, "Marker", SpaceKind::Interface);
2201            },
2202        );
2203    }
2204
2205    #[test]
2206    fn java_generic_attributes() {
2207        check_metrics::<JavaParser>(
2208            "class X<T, S extends T> {
2209                public T a1;                            // +1
2210                public Entry<T, S> b1, b2[];            // +2
2211                public ArrayList<T> c1, c2, c3;         // +3
2212                public HashMap<Long, Double> d1, d2;    // +2
2213                public TreeSet<String> e1;              // +1
2214                S f1;
2215                Entry<S, T> g1[], g2;
2216                ArrayList<S> h1, h2, h3;
2217                HashMap<Long, Float> i1, i2;
2218                TreeSet<Entry<S, T>> j1;
2219            }",
2220            "foo.java",
2221            |metric| {
2222                insta::assert_json_snapshot!(
2223                    metric.npa,
2224                    @r###"
2225                    {
2226                      "classes": 9.0,
2227                      "interfaces": 0.0,
2228                      "class_attributes": 18.0,
2229                      "interface_attributes": 0.0,
2230                      "classes_average": 0.5,
2231                      "interfaces_average": null,
2232                      "total": 9.0,
2233                      "total_attributes": 18.0,
2234                      "average": 0.5
2235                    }"###
2236                );
2237            },
2238        );
2239    }
2240
2241    #[test]
2242    fn java_attribute_modifiers() {
2243        check_metrics::<JavaParser>(
2244            "class X {
2245                public transient volatile static int a;     // +1
2246                transient public volatile static int b;     // +1
2247                transient volatile public static int c;     // +1
2248                transient volatile static public int d;     // +1
2249                public transient static final int e = 1;    // +1
2250                transient public static final int f = 2;    // +1
2251                transient static public final int g = 3;    // +1
2252                transient static final public int h = 4;    // +1
2253                protected transient volatile static int i;
2254                transient volatile static protected int j;
2255                private transient volatile static int k;
2256                transient volatile static private int l;
2257                transient volatile static int m;
2258                transient static final int n = 5;
2259                static public final int o = 6;              // +1
2260                final public int p = 7;                     // +1
2261            }",
2262            "foo.java",
2263            |metric| {
2264                insta::assert_json_snapshot!(
2265                    metric.npa,
2266                    @r###"
2267                    {
2268                      "classes": 10.0,
2269                      "interfaces": 0.0,
2270                      "class_attributes": 16.0,
2271                      "interface_attributes": 0.0,
2272                      "classes_average": 0.625,
2273                      "interfaces_average": null,
2274                      "total": 10.0,
2275                      "total_attributes": 16.0,
2276                      "average": 0.625
2277                    }"###
2278                );
2279            },
2280        );
2281    }
2282
2283    #[test]
2284    fn java_classes() {
2285        check_metrics::<JavaParser>(
2286            "class X {
2287                public int a;       // +1
2288                public boolean b;   // +1
2289                private char c;
2290            }
2291            class Y {
2292                private double d;
2293                private long e;
2294                public float f;      // +1
2295            }",
2296            "foo.java",
2297            |metric| {
2298                insta::assert_json_snapshot!(
2299                    metric.npa,
2300                    @r###"
2301                    {
2302                      "classes": 3.0,
2303                      "interfaces": 0.0,
2304                      "class_attributes": 6.0,
2305                      "interface_attributes": 0.0,
2306                      "classes_average": 0.5,
2307                      "interfaces_average": null,
2308                      "total": 3.0,
2309                      "total_attributes": 6.0,
2310                      "average": 0.5
2311                    }"###
2312                );
2313            },
2314        );
2315    }
2316
2317    #[test]
2318    fn java_nested_inner_classes() {
2319        check_metrics::<JavaParser>(
2320            "class X {
2321                public int a;           // +1
2322                class Y {
2323                    public boolean b;   // +1
2324                    class Z {
2325                        public char c;  // +1
2326                    }
2327                }
2328            }",
2329            "foo.java",
2330            |metric| {
2331                insta::assert_json_snapshot!(
2332                    metric.npa,
2333                    @r###"
2334                    {
2335                      "classes": 3.0,
2336                      "interfaces": 0.0,
2337                      "class_attributes": 3.0,
2338                      "interface_attributes": 0.0,
2339                      "classes_average": 1.0,
2340                      "interfaces_average": null,
2341                      "total": 3.0,
2342                      "total_attributes": 3.0,
2343                      "average": 1.0
2344                    }"###
2345                );
2346            },
2347        );
2348    }
2349
2350    #[test]
2351    fn java_local_inner_classes() {
2352        check_metrics::<JavaParser>(
2353            "class X {
2354                public int a;                   // +1
2355                void x() {
2356                    class Y {
2357                        public boolean b;       // +1
2358                        void y() {
2359                            class Z {
2360                                public char c;  // +1
2361                                void z() {}
2362                            }
2363                        }
2364                    }
2365                }
2366            }",
2367            "foo.java",
2368            |metric| {
2369                insta::assert_json_snapshot!(
2370                    metric.npa,
2371                    @r###"
2372                    {
2373                      "classes": 3.0,
2374                      "interfaces": 0.0,
2375                      "class_attributes": 3.0,
2376                      "interface_attributes": 0.0,
2377                      "classes_average": 1.0,
2378                      "interfaces_average": null,
2379                      "total": 3.0,
2380                      "total_attributes": 3.0,
2381                      "average": 1.0
2382                    }"###
2383                );
2384            },
2385        );
2386    }
2387
2388    #[test]
2389    fn java_anonymous_inner_classes() {
2390        check_metrics::<JavaParser>(
2391            "abstract class X {
2392                public int a;               // +1
2393            }
2394            abstract class Y {
2395                boolean b;
2396            }
2397            class Z {
2398                public char c;              // +1
2399                public void z(){
2400                    X x1 = new X() {
2401                        public double d;    // +1
2402                    };
2403                    Y y1 = new Y() {
2404                        long e;
2405                    };
2406                }
2407            }",
2408            "foo.java",
2409            |metric| {
2410                insta::assert_json_snapshot!(
2411                    metric.npa,
2412                    @r###"
2413                    {
2414                      "classes": 3.0,
2415                      "interfaces": 0.0,
2416                      "class_attributes": 5.0,
2417                      "interface_attributes": 0.0,
2418                      "classes_average": 0.6,
2419                      "interfaces_average": null,
2420                      "total": 3.0,
2421                      "total_attributes": 5.0,
2422                      "average": 0.6
2423                    }"###
2424                );
2425            },
2426        );
2427    }
2428
2429    #[test]
2430    fn java_interface() {
2431        check_metrics::<JavaParser>(
2432            "interface X {
2433                public int a = 0;           // +1
2434                static boolean b = false;   // +1
2435                final char c = ' ';         // +1
2436            }",
2437            "foo.java",
2438            |metric| {
2439                insta::assert_json_snapshot!(
2440                    metric.npa,
2441                    @r###"
2442                    {
2443                      "classes": 0.0,
2444                      "interfaces": 3.0,
2445                      "class_attributes": 0.0,
2446                      "interface_attributes": 3.0,
2447                      "classes_average": null,
2448                      "interfaces_average": 1.0,
2449                      "total": 3.0,
2450                      "total_attributes": 3.0,
2451                      "average": 1.0
2452                    }"###
2453                );
2454            },
2455        );
2456    }
2457
2458    // Regression for issue #280: Java `EnumDeclaration` must be
2459    // classified as a class space so `Npa` walks its body and counts
2460    // explicit public fields declared after the enum constants.
2461    #[test]
2462    fn java_enum_counts_explicit_public_fields() {
2463        check_metrics::<JavaParser>(
2464            "enum Status {
2465                ACTIVE, INACTIVE;
2466                public static final int FLAG = 1;   // implicit static final, still public
2467                public int code;                    // +1 explicit public
2468                private int hidden;                 // not public
2469            }",
2470            "foo.java",
2471            |metric| {
2472                // 1 class space (the enum), 3 total fields, 2 explicit public.
2473                assert_eq!(metric.npa.class_na_sum(), 3.0);
2474                assert_eq!(metric.npa.class_npa_sum(), 2.0);
2475            },
2476        );
2477    }
2478
2479    // Regression for issue #280: Java `RecordDeclaration` reuses
2480    // `ClassBody` for its explicit body, so explicit fields declared
2481    // inside it count. Record components in the parameter list are
2482    // implicit public final fields at the bytecode level but are NOT
2483    // counted here, matching the C# precedent (only explicit body
2484    // members count).
2485    #[test]
2486    fn java_record_counts_explicit_body_fields() {
2487        check_metrics::<JavaParser>(
2488            "record Point(int x, int y) {
2489                public static int origin = 0;       // explicit body, public
2490                private int cached;                 // explicit body, private
2491            }",
2492            "foo.java",
2493            |metric| {
2494                // Only explicit body fields are counted; the `x` / `y`
2495                // record components are not.
2496                assert_eq!(metric.npa.class_na_sum(), 2.0);
2497                assert_eq!(metric.npa.class_npa_sum(), 1.0);
2498            },
2499        );
2500    }
2501
2502    #[test]
2503    fn java_annotation_type_counts_constants_as_implicit_public() {
2504        // Asserting only `interface_na_sum` / `interface_npa_sum`
2505        // would pass vacuously if `AnnotationTypeDeclaration` were
2506        // dropped from `JavaCode::is_func_space`: the body walker
2507        // counts annotation-type constants regardless of the
2508        // surrounding FuncSpace kind, so the file-level Unit would
2509        // still report 2.0 for both. The `check_func_space`
2510        // assertion catches that revert by requiring the annotation
2511        // type to actually open an `Interface` FuncSpace.
2512        check_func_space::<JavaParser, _>(
2513            "@interface Marker {
2514                int VERSION = 1;        // implicit public static final
2515                String NAME = \"x\";    // implicit public static final
2516            }",
2517            "foo.java",
2518            |func_space| {
2519                assert_eq!(func_space.metrics.npa.interface_na_sum(), 2.0);
2520                assert_eq!(func_space.metrics.npa.interface_npa_sum(), 2.0);
2521                assert_child_space_kind(&func_space, "Marker", SpaceKind::Interface);
2522            },
2523        );
2524    }
2525
2526    #[test]
2527    fn php_no_class_attributes() {
2528        check_metrics::<PhpParser>(
2529            "<?php class A { public function f(): void {} }",
2530            "foo.php",
2531            |metric| insta::assert_json_snapshot!(metric.npa),
2532        );
2533    }
2534
2535    #[test]
2536    fn csharp_single_attributes() {
2537        check_metrics::<CsharpParser>(
2538            "class X {
2539                public byte a;
2540                public short b;
2541                public int c;
2542                public long d;
2543                public float e;
2544                public double f;
2545                public bool g;
2546                public char h;
2547                byte i;
2548                short j;
2549                int k;
2550                long l;
2551                float m;
2552                double n;
2553                bool o;
2554                char p;
2555            }",
2556            "foo.cs",
2557            |metric| {
2558                assert_eq!(metric.npa.class_npa_sum(), 8.0);
2559                assert_eq!(metric.npa.class_na_sum(), 16.0);
2560                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2561                insta::assert_json_snapshot!(metric.npa);
2562            },
2563        );
2564    }
2565
2566    #[test]
2567    fn csharp_multiple_attributes() {
2568        check_metrics::<CsharpParser>(
2569            "class X {
2570                public byte a1;
2571                public short b1, b2;
2572                public int c1, c2, c3;
2573                public long d1, d2, d3, d4;
2574                public bool g1, g2;
2575                byte i1, i2, i3, i4;
2576                int k1, k2;
2577            }",
2578            "foo.cs",
2579            |metric| {
2580                assert_eq!(metric.npa.class_npa_sum(), 12.0);
2581                assert_eq!(metric.npa.class_na_sum(), 18.0);
2582                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2583                insta::assert_json_snapshot!(metric.npa);
2584            },
2585        );
2586    }
2587
2588    #[test]
2589    fn csharp_initialized_attributes() {
2590        check_metrics::<CsharpParser>(
2591            "class X {
2592                public int a = 1;
2593                public bool b = true;
2594                public string c = \"hello\";
2595                public double d = 3.14;
2596                int e = 0;
2597            }",
2598            "foo.cs",
2599            |metric| {
2600                assert_eq!(metric.npa.class_npa_sum(), 4.0);
2601                assert_eq!(metric.npa.class_na_sum(), 5.0);
2602                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2603                insta::assert_json_snapshot!(metric.npa);
2604            },
2605        );
2606    }
2607
2608    #[test]
2609    fn csharp_array_attributes() {
2610        check_metrics::<CsharpParser>(
2611            "class X {
2612                public int[] a;
2613                public string[] b = new string[5];
2614                int[] c;
2615            }",
2616            "foo.cs",
2617            |metric| {
2618                assert_eq!(metric.npa.class_npa_sum(), 2.0);
2619                assert_eq!(metric.npa.class_na_sum(), 3.0);
2620                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2621                insta::assert_json_snapshot!(metric.npa);
2622            },
2623        );
2624    }
2625
2626    #[test]
2627    fn csharp_object_attributes() {
2628        check_metrics::<CsharpParser>(
2629            "class Point { public int X, Y; }
2630             class Shape {
2631                public Point origin;
2632                public Point endpoint = new Point();
2633                Point hidden;
2634             }",
2635            "foo.cs",
2636            |metric| {
2637                assert_eq!(metric.npa.class_npa_sum(), 4.0);
2638                assert_eq!(metric.npa.class_na_sum(), 5.0);
2639                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2640                insta::assert_json_snapshot!(metric.npa);
2641            },
2642        );
2643    }
2644
2645    #[test]
2646    fn csharp_generic_attributes() {
2647        check_metrics::<CsharpParser>(
2648            "class X {
2649                public System.Collections.Generic.List<int> a;
2650                public System.Collections.Generic.Dictionary<string, int> b;
2651                System.Collections.Generic.List<string> c;
2652            }",
2653            "foo.cs",
2654            |metric| {
2655                assert_eq!(metric.npa.class_npa_sum(), 2.0);
2656                assert_eq!(metric.npa.class_na_sum(), 3.0);
2657                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2658                insta::assert_json_snapshot!(metric.npa);
2659            },
2660        );
2661    }
2662
2663    #[test]
2664    fn csharp_attribute_modifiers() {
2665        check_metrics::<CsharpParser>(
2666            "class X {
2667                public int a;
2668                private int b;
2669                protected int c;
2670                internal int d;
2671                public static int e;
2672                public readonly int f;
2673                public const int g = 1;
2674            }",
2675            "foo.cs",
2676            |metric| {
2677                // Modifiers test: 4 of 7 fields are explicitly `public`. The
2678                // visibility-filter split is the spec.
2679                assert_eq!(metric.npa.class_npa_sum(), 4.0);
2680                assert_eq!(metric.npa.class_na_sum(), 7.0);
2681                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2682                insta::assert_json_snapshot!(metric.npa);
2683            },
2684        );
2685    }
2686
2687    #[test]
2688    fn csharp_classes() {
2689        check_metrics::<CsharpParser>(
2690            "class A {
2691                public int a;
2692                public int b;
2693                int c;
2694            }
2695            class B {
2696                public string s;
2697                int n;
2698            }",
2699            "foo.cs",
2700            |metric| {
2701                assert_eq!(metric.npa.class_npa_sum(), 3.0);
2702                assert_eq!(metric.npa.class_na_sum(), 5.0);
2703                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2704                insta::assert_json_snapshot!(metric.npa);
2705            },
2706        );
2707    }
2708
2709    #[test]
2710    fn csharp_nested_inner_classes() {
2711        check_metrics::<CsharpParser>(
2712            "class Outer {
2713                public int a;
2714                int b;
2715                public class Inner {
2716                    public string s;
2717                    int n;
2718                }
2719            }",
2720            "foo.cs",
2721            |metric| {
2722                assert_eq!(metric.npa.class_npa_sum(), 2.0);
2723                assert_eq!(metric.npa.class_na_sum(), 4.0);
2724                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2725                insta::assert_json_snapshot!(metric.npa);
2726            },
2727        );
2728    }
2729
2730    #[test]
2731    fn csharp_struct_attributes() {
2732        // C#-only: structs declare fields like classes; visibility rule
2733        // applies the same way (default is private).
2734        check_metrics::<CsharpParser>(
2735            "struct Point {
2736                public int X;
2737                public int Y;
2738                int Hidden;
2739            }",
2740            "foo.cs",
2741            |metric| {
2742                assert_eq!(metric.npa.class_npa_sum(), 2.0);
2743                assert_eq!(metric.npa.class_na_sum(), 3.0);
2744                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2745                insta::assert_json_snapshot!(metric.npa);
2746            },
2747        );
2748    }
2749
2750    #[test]
2751    fn csharp_record_attributes() {
2752        // C#-only: records can declare body fields just like classes.
2753        // Positional record properties are not modelled (EC9).
2754        check_metrics::<CsharpParser>(
2755            "record Person {
2756                public string Name;
2757                int Age;
2758            }",
2759            "foo.cs",
2760            |metric| {
2761                assert_eq!(metric.npa.class_npa_sum(), 1.0);
2762                assert_eq!(metric.npa.class_na_sum(), 2.0);
2763                assert_eq!(metric.npa.interface_na_sum(), 0.0);
2764                insta::assert_json_snapshot!(metric.npa);
2765            },
2766        );
2767    }
2768
2769    #[test]
2770    fn csharp_interface() {
2771        // EC14 — interface members default to public; all fields count.
2772        // Structural `assert_child_space_kind` guards against an
2773        // `InterfaceDeclaration` revert in `CsharpCode::is_func_space`
2774        // — see #311.
2775        check_func_space::<CsharpParser, _>(
2776            "interface I {
2777                static int A = 1;
2778                static string B = \"hello\";
2779            }",
2780            "foo.cs",
2781            |func_space| {
2782                let metric = &func_space.metrics;
2783                assert_eq!(metric.npa.class_na_sum(), 0.0);
2784                assert_eq!(metric.npa.interface_na_sum(), 2.0);
2785                insta::assert_json_snapshot!(metric.npa);
2786                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
2787            },
2788        );
2789    }
2790
2791    #[test]
2792    fn php_one_public_attribute() {
2793        check_metrics::<PhpParser>(
2794            "<?php class A { public int $x = 0; }",
2795            "foo.php",
2796            |metric| insta::assert_json_snapshot!(metric.npa),
2797        );
2798    }
2799
2800    #[test]
2801    fn php_one_private_attribute() {
2802        check_metrics::<PhpParser>(
2803            "<?php class A { private int $x = 0; }",
2804            "foo.php",
2805            |metric| insta::assert_json_snapshot!(metric.npa),
2806        );
2807    }
2808
2809    #[test]
2810    fn php_one_protected_attribute() {
2811        check_metrics::<PhpParser>(
2812            "<?php class A { protected int $x = 0; }",
2813            "foo.php",
2814            |metric| insta::assert_json_snapshot!(metric.npa),
2815        );
2816    }
2817
2818    #[test]
2819    fn php_mixed_visibility_attributes() {
2820        check_metrics::<PhpParser>(
2821            "<?php
2822            class A {
2823                public int $a = 0;
2824                public int $b = 0;
2825                private int $c = 0;
2826                protected int $d = 0;
2827            }",
2828            "foo.php",
2829            |metric| insta::assert_json_snapshot!(metric.npa),
2830        );
2831    }
2832
2833    #[test]
2834    fn php_static_public_attribute() {
2835        check_metrics::<PhpParser>(
2836            "<?php class A { public static int $x = 0; }",
2837            "foo.php",
2838            |metric| insta::assert_json_snapshot!(metric.npa),
2839        );
2840    }
2841
2842    #[test]
2843    fn php_readonly_public_attribute() {
2844        check_metrics::<PhpParser>(
2845            "<?php class A { public readonly int $x; }",
2846            "foo.php",
2847            |metric| insta::assert_json_snapshot!(metric.npa),
2848        );
2849    }
2850
2851    #[test]
2852    fn php_multiple_attributes_per_declaration() {
2853        // A single property_declaration can declare several
2854        // property_elements; each counts.
2855        check_metrics::<PhpParser>(
2856            "<?php class A { public int $a = 0, $b = 0, $c = 0; }",
2857            "foo.php",
2858            |metric| insta::assert_json_snapshot!(metric.npa),
2859        );
2860    }
2861
2862    #[test]
2863    fn php_interface_constants() {
2864        // Interface constants are implicitly public.
2865        check_metrics::<PhpParser>(
2866            "<?php
2867            interface I {
2868                const A = 1;
2869                const B = 2;
2870            }",
2871            "foo.php",
2872            |metric| insta::assert_json_snapshot!(metric.npa),
2873        );
2874    }
2875
2876    #[test]
2877    fn php_enum_cases() {
2878        // Enum cases are public read-only constants.
2879        check_metrics::<PhpParser>(
2880            "<?php
2881            enum Color {
2882                case Red;
2883                case Green;
2884                case Blue;
2885            }",
2886            "foo.php",
2887            |metric| insta::assert_json_snapshot!(metric.npa),
2888        );
2889    }
2890
2891    #[test]
2892    fn php_trait_attributes() {
2893        check_metrics::<PhpParser>(
2894            "<?php
2895            trait T {
2896                public int $a = 0;
2897                private int $b = 0;
2898            }",
2899            "foo.php",
2900            |metric| insta::assert_json_snapshot!(metric.npa),
2901        );
2902    }
2903
2904    #[test]
2905    fn php_no_explicit_visibility_excluded() {
2906        // PHP 8.x deprecates implicit-public for properties; we follow
2907        // Java's strict-explicit rule and do NOT count properties without
2908        // an explicit `public` modifier.
2909        check_metrics::<PhpParser>("<?php class A { var $x = 0; }", "foo.php", |metric| {
2910            // The property is excluded from the public-count (npa) because
2911            // `var` is not an explicit `public` modifier, but still
2912            // contributes to the total-count (na). This split is the spec.
2913            assert_eq!(metric.npa.class_npa_sum(), 0.0);
2914            assert_eq!(metric.npa.class_na_sum(), 1.0);
2915            assert_eq!(metric.npa.interface_na_sum(), 0.0);
2916            insta::assert_json_snapshot!(metric.npa);
2917        });
2918    }
2919
2920    #[test]
2921    fn php_anonymous_class_attributes() {
2922        // Anonymous classes have their own DeclarationList space and
2923        // their public properties count. The Npa impl branches on
2924        // `parent_kind == AnonymousClass` and this test exercises that
2925        // arm.
2926        check_metrics::<PhpParser>(
2927            "<?php
2928            $obj = new class {
2929                public int $a = 0;
2930                private int $b = 0;
2931            };",
2932            "foo.php",
2933            |metric| insta::assert_json_snapshot!(metric.npa),
2934        );
2935    }
2936
2937    #[test]
2938    fn php_property_promotion_excluded() {
2939        // Constructor property promotion (PHP 8.0+) declares both a
2940        // parameter AND a property in one syntax. The promoted property
2941        // lives under `formal_parameters`, NOT under
2942        // `declaration_list`, so the current Npa impl naturally
2943        // excludes it. This is a documented limitation; this test
2944        // pins the behavior so a future change that starts counting
2945        // promoted properties has to update the test deliberately.
2946        check_metrics::<PhpParser>(
2947            "<?php
2948            class A {
2949                public function __construct(public string $x, public int $y) {}
2950            }",
2951            "foo.php",
2952            |metric| insta::assert_json_snapshot!(metric.npa),
2953        );
2954    }
2955
2956    // --- Kotlin NPA tests -------------------------------------------------
2957    //
2958    // Reference: Kotlin properties (`val` / `var`) declared inside a class
2959    // body are attributes. Default visibility is `public`. Primary
2960    // constructor parameters carrying `val` / `var` are parameter
2961    // properties and count. Companion-object members fold into the
2962    // enclosing class. Top-level properties belong to the `Unit` space
2963    // and are excluded.
2964
2965    #[test]
2966    fn kotlin_empty_class_no_attributes() {
2967        check_metrics::<KotlinParser>("class C {}", "foo.kt", |metric| {
2968            assert_eq!(metric.npa.class_npa_sum(), 0.0);
2969            assert_eq!(metric.npa.class_na_sum(), 0.0);
2970            assert_eq!(metric.npa.interface_na_sum(), 0.0);
2971            insta::assert_json_snapshot!(metric.npa);
2972        });
2973    }
2974
2975    #[test]
2976    fn kotlin_public_val_var_default() {
2977        // Kotlin's default visibility is public — no modifier means public.
2978        check_metrics::<KotlinParser>(
2979            "class C {
2980                val a: Int = 1
2981                var b: Int = 2
2982                val c: String = \"hi\"
2983            }",
2984            "foo.kt",
2985            |metric| {
2986                assert_eq!(metric.npa.class_npa_sum(), 3.0);
2987                assert_eq!(metric.npa.class_na_sum(), 3.0);
2988                insta::assert_json_snapshot!(metric.npa);
2989            },
2990        );
2991    }
2992
2993    #[test]
2994    fn kotlin_private_val_var() {
2995        // Private properties contribute to total `na` but not to `npa`.
2996        check_metrics::<KotlinParser>(
2997            "class C {
2998                val a: Int = 1               // public
2999                private val b: Int = 2       // not public
3000                var c: Int = 3               // public
3001                private var d: Int = 4       // not public
3002            }",
3003            "foo.kt",
3004            |metric| {
3005                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3006                assert_eq!(metric.npa.class_na_sum(), 4.0);
3007                insta::assert_json_snapshot!(metric.npa);
3008            },
3009        );
3010    }
3011
3012    #[test]
3013    fn kotlin_protected_internal_excluded_from_public() {
3014        check_metrics::<KotlinParser>(
3015            "open class C {
3016                protected val a: Int = 1
3017                internal val b: Int = 2
3018                public val c: Int = 3        // explicit public
3019            }",
3020            "foo.kt",
3021            |metric| {
3022                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3023                assert_eq!(metric.npa.class_na_sum(), 3.0);
3024                insta::assert_json_snapshot!(metric.npa);
3025            },
3026        );
3027    }
3028
3029    #[test]
3030    fn kotlin_primary_constructor_parameter_property() {
3031        // `val`/`var` on primary constructor parameters declares both a
3032        // parameter AND a property. Bare `name: Type` parameters are NOT
3033        // attributes.
3034        check_metrics::<KotlinParser>(
3035            "class C(val a: Int, var b: Int, c: Int) {
3036                val d: Int = c
3037            }",
3038            "foo.kt",
3039            |metric| {
3040                // a, b, d -> public; c -> not an attribute (no val/var)
3041                assert_eq!(metric.npa.class_npa_sum(), 3.0);
3042                assert_eq!(metric.npa.class_na_sum(), 3.0);
3043                insta::assert_json_snapshot!(metric.npa);
3044            },
3045        );
3046    }
3047
3048    #[test]
3049    fn kotlin_primary_constructor_private_param_property() {
3050        check_metrics::<KotlinParser>(
3051            "class C(private val a: Int, val b: Int)",
3052            "foo.kt",
3053            |metric| {
3054                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3055                assert_eq!(metric.npa.class_na_sum(), 2.0);
3056                insta::assert_json_snapshot!(metric.npa);
3057            },
3058        );
3059    }
3060
3061    #[test]
3062    fn kotlin_secondary_constructor_does_not_add_attrs() {
3063        // Secondary constructors are methods, not attribute declarations.
3064        check_metrics::<KotlinParser>(
3065            "class C {
3066                private var a: Int = 0
3067                constructor(n: Int) { a = n }
3068            }",
3069            "foo.kt",
3070            |metric| {
3071                assert_eq!(metric.npa.class_npa_sum(), 0.0);
3072                assert_eq!(metric.npa.class_na_sum(), 1.0);
3073                insta::assert_json_snapshot!(metric.npa);
3074            },
3075        );
3076    }
3077
3078    #[test]
3079    fn kotlin_companion_object_attributes() {
3080        // Companion-object properties fold into the enclosing class as
3081        // "static" attributes.
3082        check_metrics::<KotlinParser>(
3083            "class Holder {
3084                val instance: Int = 1
3085                companion object {
3086                    val SCALE: Int = 10
3087                    private val SECRET: Int = 7
3088                }
3089            }",
3090            "foo.kt",
3091            |metric| {
3092                // instance (public) + SCALE (public) = 2 public
3093                // SECRET counts toward total na but not npa
3094                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3095                assert_eq!(metric.npa.class_na_sum(), 3.0);
3096                insta::assert_json_snapshot!(metric.npa);
3097            },
3098        );
3099    }
3100
3101    #[test]
3102    fn kotlin_data_class_attributes() {
3103        // `data class` parameters are the canonical positional attributes.
3104        check_metrics::<KotlinParser>(
3105            "data class Point(val x: Int, val y: Int)",
3106            "foo.kt",
3107            |metric| {
3108                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3109                assert_eq!(metric.npa.class_na_sum(), 2.0);
3110                insta::assert_json_snapshot!(metric.npa);
3111            },
3112        );
3113    }
3114
3115    #[test]
3116    fn kotlin_object_singleton_attributes() {
3117        check_metrics::<KotlinParser>(
3118            "object Config {
3119                val DEFAULT: Int = 42
3120                private val SEED: Int = 0
3121                var debug: Boolean = false
3122            }",
3123            "foo.kt",
3124            |metric| {
3125                // DEFAULT, debug -> public; SEED -> not.
3126                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3127                assert_eq!(metric.npa.class_na_sum(), 3.0);
3128                insta::assert_json_snapshot!(metric.npa);
3129            },
3130        );
3131    }
3132
3133    #[test]
3134    fn kotlin_interface_attributes() {
3135        // Interface members are implicitly public; all properties count
3136        // toward `interface_npa` and `interface_na`. Structural
3137        // `assert_child_space_kind` guards against an
3138        // `InterfaceDeclaration` revert in `KotlinCode::is_func_space`
3139        // — see #311.
3140        check_func_space::<KotlinParser, _>(
3141            "interface I {
3142                val a: Int
3143                val b: String
3144            }",
3145            "foo.kt",
3146            |func_space| {
3147                let metric = &func_space.metrics;
3148                assert_eq!(metric.npa.interface_npa_sum(), 2.0);
3149                assert_eq!(metric.npa.interface_na_sum(), 2.0);
3150                assert_eq!(metric.npa.class_na_sum(), 0.0);
3151                insta::assert_json_snapshot!(metric.npa);
3152                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
3153            },
3154        );
3155    }
3156
3157    #[test]
3158    fn kotlin_nested_class_attributes() {
3159        // Each class space has its own attribute count; nested class
3160        // attributes do not leak into the outer class.
3161        check_metrics::<KotlinParser>(
3162            "class Outer {
3163                val o1: Int = 1
3164                class Nested {
3165                    val n1: Int = 1
3166                    val n2: Int = 2
3167                }
3168            }",
3169            "foo.kt",
3170            |metric| {
3171                // 2 classes total — Outer's 1 + Nested's 2 = 3 attributes
3172                assert_eq!(metric.npa.class_npa_sum(), 3.0);
3173                assert_eq!(metric.npa.class_na_sum(), 3.0);
3174                insta::assert_json_snapshot!(metric.npa);
3175            },
3176        );
3177    }
3178
3179    #[test]
3180    fn kotlin_inner_class_attributes() {
3181        check_metrics::<KotlinParser>(
3182            "class Outer {
3183                val o1: Int = 1
3184                inner class Inner {
3185                    val i1: Int = 1
3186                }
3187            }",
3188            "foo.kt",
3189            |metric| {
3190                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3191                assert_eq!(metric.npa.class_na_sum(), 2.0);
3192                insta::assert_json_snapshot!(metric.npa);
3193            },
3194        );
3195    }
3196
3197    #[test]
3198    fn kotlin_top_level_properties_excluded() {
3199        // Top-level `val` belongs to `Unit`, not a class — must not
3200        // contribute to `class_na`.
3201        check_metrics::<KotlinParser>(
3202            "val topVal: Int = 0
3203            var topVar: Int = 1
3204            class C { val x: Int = 0 }",
3205            "foo.kt",
3206            |metric| {
3207                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3208                assert_eq!(metric.npa.class_na_sum(), 1.0);
3209                insta::assert_json_snapshot!(metric.npa);
3210            },
3211        );
3212    }
3213
3214    #[test]
3215    fn kotlin_multiple_classes_attributes() {
3216        check_metrics::<KotlinParser>(
3217            "class A {
3218                val a1: Int = 0
3219                var a2: Int = 0
3220            }
3221            class B {
3222                val b1: Int = 0
3223                private val b2: Int = 0
3224            }",
3225            "foo.kt",
3226            |metric| {
3227                // A: 2 public; B: 1 public + 1 private = 2 total, 1 public
3228                assert_eq!(metric.npa.class_npa_sum(), 3.0);
3229                assert_eq!(metric.npa.class_na_sum(), 4.0);
3230                insta::assert_json_snapshot!(metric.npa);
3231            },
3232        );
3233    }
3234
3235    #[test]
3236    fn kotlin_class_with_methods_no_attrs() {
3237        // Methods are not attributes.
3238        check_metrics::<KotlinParser>(
3239            "class C {
3240                fun m1() {}
3241                fun m2(): Int = 0
3242            }",
3243            "foo.kt",
3244            |metric| {
3245                assert_eq!(metric.npa.class_npa_sum(), 0.0);
3246                assert_eq!(metric.npa.class_na_sum(), 0.0);
3247                insta::assert_json_snapshot!(metric.npa);
3248            },
3249        );
3250    }
3251
3252    // --- TypeScript / TSX NPA tests --------------------------------------
3253    //
3254    // TypeScript class fields are `public_field_definition` direct children
3255    // of `class_body`. Default visibility is public; an explicit
3256    // `accessibility_modifier` whose only child is `private`/`protected`
3257    // demotes a field. Constructor parameter properties
3258    // (`constructor(private x: number)`) count as class attributes.
3259    // Fields whose initializer is an arrow function are methods, not
3260    // attributes. Interface property signatures count as implicitly
3261    // public attributes.
3262
3263    #[test]
3264    fn typescript_empty_class_no_attributes() {
3265        check_metrics::<TypescriptParser>("class C {}", "foo.ts", |metric| {
3266            assert_eq!(metric.npa.class_npa_sum(), 0.0);
3267            assert_eq!(metric.npa.class_na_sum(), 0.0);
3268            insta::assert_json_snapshot!(metric.npa);
3269        });
3270    }
3271
3272    #[test]
3273    fn typescript_default_public_fields() {
3274        // No accessibility modifier means public.
3275        check_metrics::<TypescriptParser>(
3276            "class C {
3277                a: number = 1;
3278                b: string = \"\";
3279                c: boolean = false;
3280            }",
3281            "foo.ts",
3282            |metric| {
3283                assert_eq!(metric.npa.class_npa_sum(), 3.0);
3284                assert_eq!(metric.npa.class_na_sum(), 3.0);
3285                insta::assert_json_snapshot!(metric.npa);
3286            },
3287        );
3288    }
3289
3290    #[test]
3291    fn typescript_visibility_modifiers() {
3292        // Public / private / protected. Default public.
3293        check_metrics::<TypescriptParser>(
3294            "class C {
3295                public a: number = 1;
3296                private b: number = 2;
3297                protected c: number = 3;
3298                d: number = 4;
3299            }",
3300            "foo.ts",
3301            |metric| {
3302                // public + default(public) = 2 npa; total na = 4.
3303                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3304                assert_eq!(metric.npa.class_na_sum(), 4.0);
3305                insta::assert_json_snapshot!(metric.npa);
3306            },
3307        );
3308    }
3309
3310    #[test]
3311    fn typescript_static_fields() {
3312        // `static` is orthogonal to visibility — the field still counts.
3313        check_metrics::<TypescriptParser>(
3314            "class C {
3315                static a: number = 0;
3316                public static b: number = 0;
3317                private static c: number = 0;
3318            }",
3319            "foo.ts",
3320            |metric| {
3321                // a (default public) + b (public) = 2 npa; c is private.
3322                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3323                assert_eq!(metric.npa.class_na_sum(), 3.0);
3324                insta::assert_json_snapshot!(metric.npa);
3325            },
3326        );
3327    }
3328
3329    #[test]
3330    fn typescript_parameter_properties() {
3331        // Constructor parameter properties are class attributes.
3332        check_metrics::<TypescriptParser>(
3333            "class C {
3334                constructor(public a: number, private b: string, c: boolean) {}
3335            }",
3336            "foo.ts",
3337            |metric| {
3338                // a, b are parameter properties (modifiered); c is a plain
3339                // parameter and does NOT count. a is public, b is private.
3340                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3341                assert_eq!(metric.npa.class_na_sum(), 2.0);
3342                insta::assert_json_snapshot!(metric.npa);
3343            },
3344        );
3345    }
3346
3347    #[test]
3348    fn typescript_readonly_field() {
3349        // `readonly` is a non-visibility modifier — the field still counts
3350        // and stays public unless paired with private/protected.
3351        check_metrics::<TypescriptParser>(
3352            "class C {
3353                readonly a: number = 1;
3354                private readonly b: number = 2;
3355            }",
3356            "foo.ts",
3357            |metric| {
3358                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3359                assert_eq!(metric.npa.class_na_sum(), 2.0);
3360                insta::assert_json_snapshot!(metric.npa);
3361            },
3362        );
3363    }
3364
3365    #[test]
3366    fn typescript_abstract_class_attributes() {
3367        // `abstract_class_declaration` opens its own class space; fields
3368        // count just like a concrete class.
3369        check_metrics::<TypescriptParser>(
3370            "abstract class C {
3371                public a: number = 1;
3372                protected b: number = 2;
3373                abstract m(): void;
3374            }",
3375            "foo.ts",
3376            |metric| {
3377                // a (public) + b (protected) = 2 attrs; npa = 1.
3378                // `abstract m()` is a method, not an attribute.
3379                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3380                assert_eq!(metric.npa.class_na_sum(), 2.0);
3381                insta::assert_json_snapshot!(metric.npa);
3382            },
3383        );
3384    }
3385
3386    #[test]
3387    fn typescript_arrow_field_is_method_not_attribute() {
3388        // A field whose initializer is an arrow function is counted by
3389        // npm, not npa.
3390        check_metrics::<TypescriptParser>(
3391            "class C {
3392                a: number = 0;
3393                arrow = () => this.a;
3394            }",
3395            "foo.ts",
3396            |metric| {
3397                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3398                assert_eq!(metric.npa.class_na_sum(), 1.0);
3399                insta::assert_json_snapshot!(metric.npa);
3400            },
3401        );
3402    }
3403
3404    #[test]
3405    fn typescript_interface_property_signatures() {
3406        // Interface property signatures count as implicitly-public
3407        // attributes; method signatures are not attributes.
3408        // Structural `assert_child_space_kind` guards against an
3409        // `InterfaceDeclaration` revert in
3410        // `TypescriptCode::is_func_space` — see #311.
3411        check_func_space::<TypescriptParser, _>(
3412            "interface I {
3413                a: number;
3414                b: string;
3415                m(): void;
3416            }",
3417            "foo.ts",
3418            |func_space| {
3419                let metric = &func_space.metrics;
3420                assert_eq!(metric.npa.interface_npa_sum(), 2.0);
3421                assert_eq!(metric.npa.interface_na_sum(), 2.0);
3422                assert_eq!(metric.npa.class_na_sum(), 0.0);
3423                insta::assert_json_snapshot!(metric.npa);
3424                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
3425            },
3426        );
3427    }
3428
3429    #[test]
3430    fn typescript_generic_class_attributes() {
3431        // Type parameters on the class do not contribute attributes.
3432        check_metrics::<TypescriptParser>(
3433            "class Box<T, U> {
3434                value: T;
3435                other: U;
3436                constructor(v: T, o: U) { this.value = v; this.other = o; }
3437            }",
3438            "foo.ts",
3439            |metric| {
3440                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3441                assert_eq!(metric.npa.class_na_sum(), 2.0);
3442                insta::assert_json_snapshot!(metric.npa);
3443            },
3444        );
3445    }
3446
3447    #[test]
3448    fn typescript_getters_setters_not_attributes() {
3449        // `get x()` / `set x(v)` are method_definitions, not attributes.
3450        check_metrics::<TypescriptParser>(
3451            "class C {
3452                private _x: number = 0;
3453                get x(): number { return this._x; }
3454                set x(v: number) { this._x = v; }
3455            }",
3456            "foo.ts",
3457            |metric| {
3458                // Only `_x` counts as an attribute (private → not public).
3459                assert_eq!(metric.npa.class_npa_sum(), 0.0);
3460                assert_eq!(metric.npa.class_na_sum(), 1.0);
3461                insta::assert_json_snapshot!(metric.npa);
3462            },
3463        );
3464    }
3465
3466    #[test]
3467    fn typescript_multiple_classes_and_interface() {
3468        check_func_space::<TypescriptParser, _>(
3469            "class A { x: number = 0; }
3470             class B { private y: number = 0; }
3471             interface I { z: number; }",
3472            "foo.ts",
3473            |func_space| {
3474                let metric = &func_space.metrics;
3475                // A: 1 npa / 1 na (public). B: 0 npa / 1 na (private).
3476                // I: 1 interface_npa / 1 interface_na.
3477                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3478                assert_eq!(metric.npa.class_na_sum(), 2.0);
3479                assert_eq!(metric.npa.interface_npa_sum(), 1.0);
3480                assert_eq!(metric.npa.interface_na_sum(), 1.0);
3481                insta::assert_json_snapshot!(metric.npa);
3482                assert_child_space_kind(&func_space, "A", SpaceKind::Class);
3483                assert_child_space_kind(&func_space, "B", SpaceKind::Class);
3484                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
3485            },
3486        );
3487    }
3488
3489    #[test]
3490    fn typescript_nested_class_attributes_independent() {
3491        // Each class space tracks its own attributes; the outer class's
3492        // sum gets the inner-class sum via merge. The Outer class has
3493        // two `public_field_definition` direct children — `a` and the
3494        // `Inner` static field whose value is a class expression.
3495        // The class expression itself opens a separate `class` space
3496        // with its own two fields. Total counted across both spaces:
3497        // 2 (Outer: a + Inner) + 2 (inner anonymous class: b, c) = 4.
3498        check_metrics::<TypescriptParser>(
3499            "class Outer {
3500                a: number = 0;
3501                static Inner = class {
3502                    b: number = 0;
3503                    c: number = 0;
3504                };
3505            }",
3506            "foo.ts",
3507            |metric| {
3508                assert_eq!(metric.npa.class_npa_sum(), 4.0);
3509                assert_eq!(metric.npa.class_na_sum(), 4.0);
3510                insta::assert_json_snapshot!(metric.npa);
3511            },
3512        );
3513    }
3514
3515    // TSX parity tests — mirror the TS rules to confirm the shared helper
3516    // expansion behaves identically on the TSX grammar.
3517
3518    #[test]
3519    fn tsx_empty_class_no_attributes() {
3520        check_metrics::<TsxParser>("class C {}", "foo.tsx", |metric| {
3521            assert_eq!(metric.npa.class_npa_sum(), 0.0);
3522            assert_eq!(metric.npa.class_na_sum(), 0.0);
3523            insta::assert_json_snapshot!(metric.npa);
3524        });
3525    }
3526
3527    #[test]
3528    fn tsx_default_public_fields() {
3529        check_metrics::<TsxParser>(
3530            "class C {
3531                a: number = 1;
3532                b: string = \"\";
3533            }",
3534            "foo.tsx",
3535            |metric| {
3536                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3537                assert_eq!(metric.npa.class_na_sum(), 2.0);
3538                insta::assert_json_snapshot!(metric.npa);
3539            },
3540        );
3541    }
3542
3543    #[test]
3544    fn tsx_visibility_modifiers() {
3545        check_metrics::<TsxParser>(
3546            "class C {
3547                public a: number = 1;
3548                private b: number = 2;
3549                protected c: number = 3;
3550            }",
3551            "foo.tsx",
3552            |metric| {
3553                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3554                assert_eq!(metric.npa.class_na_sum(), 3.0);
3555                insta::assert_json_snapshot!(metric.npa);
3556            },
3557        );
3558    }
3559
3560    #[test]
3561    fn tsx_parameter_properties() {
3562        check_metrics::<TsxParser>(
3563            "class C {
3564                constructor(public a: number, private b: string) {}
3565            }",
3566            "foo.tsx",
3567            |metric| {
3568                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3569                assert_eq!(metric.npa.class_na_sum(), 2.0);
3570                insta::assert_json_snapshot!(metric.npa);
3571            },
3572        );
3573    }
3574
3575    #[test]
3576    fn tsx_abstract_class_attributes() {
3577        check_metrics::<TsxParser>(
3578            "abstract class C {
3579                public a: number = 1;
3580                private b: number = 2;
3581                abstract m(): void;
3582            }",
3583            "foo.tsx",
3584            |metric| {
3585                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3586                assert_eq!(metric.npa.class_na_sum(), 2.0);
3587                insta::assert_json_snapshot!(metric.npa);
3588            },
3589        );
3590    }
3591
3592    #[test]
3593    fn tsx_interface_property_signatures() {
3594        check_func_space::<TsxParser, _>(
3595            "interface I {
3596                a: number;
3597                b: string;
3598                m(): void;
3599            }",
3600            "foo.tsx",
3601            |func_space| {
3602                let metric = &func_space.metrics;
3603                assert_eq!(metric.npa.interface_npa_sum(), 2.0);
3604                assert_eq!(metric.npa.interface_na_sum(), 2.0);
3605                insta::assert_json_snapshot!(metric.npa);
3606                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
3607            },
3608        );
3609    }
3610
3611    #[test]
3612    fn tsx_arrow_field_is_method_not_attribute() {
3613        check_metrics::<TsxParser>(
3614            "class C {
3615                a: number = 0;
3616                arrow = () => this.a;
3617            }",
3618            "foo.tsx",
3619            |metric| {
3620                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3621                assert_eq!(metric.npa.class_na_sum(), 1.0);
3622                insta::assert_json_snapshot!(metric.npa);
3623            },
3624        );
3625    }
3626
3627    #[test]
3628    fn tsx_static_fields() {
3629        check_metrics::<TsxParser>(
3630            "class C {
3631                static a: number = 0;
3632                private static b: number = 0;
3633            }",
3634            "foo.tsx",
3635            |metric| {
3636                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3637                assert_eq!(metric.npa.class_na_sum(), 2.0);
3638                insta::assert_json_snapshot!(metric.npa);
3639            },
3640        );
3641    }
3642
3643    #[test]
3644    fn tsx_readonly_field() {
3645        check_metrics::<TsxParser>(
3646            "class C {
3647                readonly a: number = 1;
3648                private readonly b: number = 2;
3649            }",
3650            "foo.tsx",
3651            |metric| {
3652                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3653                assert_eq!(metric.npa.class_na_sum(), 2.0);
3654                insta::assert_json_snapshot!(metric.npa);
3655            },
3656        );
3657    }
3658
3659    #[test]
3660    fn tsx_generic_class_attributes() {
3661        check_metrics::<TsxParser>("class Box<T> { value: T; }", "foo.tsx", |metric| {
3662            assert_eq!(metric.npa.class_npa_sum(), 1.0);
3663            assert_eq!(metric.npa.class_na_sum(), 1.0);
3664            insta::assert_json_snapshot!(metric.npa);
3665        });
3666    }
3667
3668    #[test]
3669    fn tsx_getters_setters_not_attributes() {
3670        check_metrics::<TsxParser>(
3671            "class C {
3672                private _x: number = 0;
3673                get x(): number { return this._x; }
3674                set x(v: number) { this._x = v; }
3675            }",
3676            "foo.tsx",
3677            |metric| {
3678                assert_eq!(metric.npa.class_npa_sum(), 0.0);
3679                assert_eq!(metric.npa.class_na_sum(), 1.0);
3680                insta::assert_json_snapshot!(metric.npa);
3681            },
3682        );
3683    }
3684
3685    #[test]
3686    fn tsx_multiple_classes_and_interface() {
3687        check_func_space::<TsxParser, _>(
3688            "class A { x: number = 0; }
3689             class B { private y: number = 0; }
3690             interface I { z: number; }",
3691            "foo.tsx",
3692            |func_space| {
3693                let metric = &func_space.metrics;
3694                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3695                assert_eq!(metric.npa.class_na_sum(), 2.0);
3696                assert_eq!(metric.npa.interface_npa_sum(), 1.0);
3697                assert_eq!(metric.npa.interface_na_sum(), 1.0);
3698                insta::assert_json_snapshot!(metric.npa);
3699                assert_child_space_kind(&func_space, "A", SpaceKind::Class);
3700                assert_child_space_kind(&func_space, "B", SpaceKind::Class);
3701                assert_child_space_kind(&func_space, "I", SpaceKind::Interface);
3702            },
3703        );
3704    }
3705
3706    // --- Ruby NPA tests ---------------------------------------------------
3707    //
3708    // Ruby has no field-declaration syntax; class-scope instance and
3709    // class variables are introduced by direct assignment in the class
3710    // body (`@var = …`, `@@var = …`). `attr_accessor` / `attr_reader`
3711    // / `attr_writer` macros synthesise reader/writer pairs and also
3712    // introduce attributes. Visibility flows from keyword markers as
3713    // in `Npm`.
3714
3715    #[test]
3716    fn ruby_no_class_attributes() {
3717        check_metrics::<RubyParser>(
3718            "class A\n  def f\n    1\n  end\nend\n",
3719            "foo.rb",
3720            |metric| {
3721                assert_eq!(metric.npa.class_npa_sum(), 0.0);
3722                assert_eq!(metric.npa.class_na_sum(), 0.0);
3723                insta::assert_json_snapshot!(metric.npa);
3724            },
3725        );
3726    }
3727
3728    #[test]
3729    fn ruby_instance_variable_attribute() {
3730        // Bare `@x = …` at class scope is one public attribute.
3731        check_metrics::<RubyParser>("class A\n  @x = 1\nend\n", "foo.rb", |metric| {
3732            assert_eq!(metric.npa.class_npa_sum(), 1.0);
3733            assert_eq!(metric.npa.class_na_sum(), 1.0);
3734            insta::assert_json_snapshot!(metric.npa);
3735        });
3736    }
3737
3738    #[test]
3739    fn ruby_class_variable_attribute() {
3740        // `@@y = …` at class scope is one attribute.
3741        check_metrics::<RubyParser>("class A\n  @@y = 1\nend\n", "foo.rb", |metric| {
3742            assert_eq!(metric.npa.class_npa_sum(), 1.0);
3743            assert_eq!(metric.npa.class_na_sum(), 1.0);
3744            insta::assert_json_snapshot!(metric.npa);
3745        });
3746    }
3747
3748    #[test]
3749    fn ruby_attr_accessor_counts_symbols() {
3750        // `attr_accessor :x, :y, :z` declares three attributes.
3751        check_metrics::<RubyParser>(
3752            "class A\n  attr_accessor :x, :y, :z\nend\n",
3753            "foo.rb",
3754            |metric| {
3755                assert_eq!(metric.npa.class_npa_sum(), 3.0);
3756                assert_eq!(metric.npa.class_na_sum(), 3.0);
3757                insta::assert_json_snapshot!(metric.npa);
3758            },
3759        );
3760    }
3761
3762    #[test]
3763    fn ruby_attr_reader_and_writer() {
3764        check_metrics::<RubyParser>(
3765            "class A\n  attr_reader :r1, :r2\n  attr_writer :w\nend\n",
3766            "foo.rb",
3767            |metric| {
3768                assert_eq!(metric.npa.class_npa_sum(), 3.0);
3769                assert_eq!(metric.npa.class_na_sum(), 3.0);
3770                insta::assert_json_snapshot!(metric.npa);
3771            },
3772        );
3773    }
3774
3775    #[test]
3776    fn ruby_mixed_attributes_and_assignments() {
3777        check_metrics::<RubyParser>(
3778            "class A\n  attr_accessor :x, :y\n  @z = 1\n  @@w = 2\nend\n",
3779            "foo.rb",
3780            |metric| {
3781                assert_eq!(metric.npa.class_npa_sum(), 4.0);
3782                assert_eq!(metric.npa.class_na_sum(), 4.0);
3783                insta::assert_json_snapshot!(metric.npa);
3784            },
3785        );
3786    }
3787
3788    #[test]
3789    fn ruby_private_attributes() {
3790        // Bare `private` flips visibility for the subsequent attr.
3791        check_metrics::<RubyParser>(
3792            "class A\n  attr_accessor :pub\n  private\n  attr_accessor :hidden\nend\n",
3793            "foo.rb",
3794            |metric| {
3795                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3796                assert_eq!(metric.npa.class_na_sum(), 2.0);
3797                insta::assert_json_snapshot!(metric.npa);
3798            },
3799        );
3800    }
3801
3802    #[test]
3803    fn ruby_visibility_public_resets_private() {
3804        // `private` then `public` returns to default-public.
3805        check_metrics::<RubyParser>(
3806            "class A\n  attr_reader :a\n  private\n  attr_reader :b\n  public\n  attr_reader :c\nend\n",
3807            "foo.rb",
3808            |metric| {
3809                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3810                assert_eq!(metric.npa.class_na_sum(), 3.0);
3811                insta::assert_json_snapshot!(metric.npa);
3812            },
3813        );
3814    }
3815
3816    #[test]
3817    fn ruby_method_scope_assignments_excluded() {
3818        // `@x = 1` inside a method does NOT count — it's a method-local
3819        // instance-variable write, not a class-scope attribute
3820        // declaration.
3821        check_metrics::<RubyParser>(
3822            "class A\n  def init\n    @x = 1\n    @@y = 2\n  end\nend\n",
3823            "foo.rb",
3824            |metric| {
3825                assert_eq!(metric.npa.class_npa_sum(), 0.0);
3826                assert_eq!(metric.npa.class_na_sum(), 0.0);
3827                insta::assert_json_snapshot!(metric.npa);
3828            },
3829        );
3830    }
3831
3832    #[test]
3833    fn ruby_module_attributes_not_counted() {
3834        // `module M` is a `Namespace` space — its attr_* macros and
3835        // class-variable assignments do NOT contribute to NPA.
3836        check_metrics::<RubyParser>(
3837            "module M\n  attr_accessor :x\n  @@m = 1\nend\n",
3838            "foo.rb",
3839            |metric| {
3840                assert_eq!(metric.npa.class_npa_sum(), 0.0);
3841                assert_eq!(metric.npa.class_na_sum(), 0.0);
3842                insta::assert_json_snapshot!(metric.npa);
3843            },
3844        );
3845    }
3846
3847    #[test]
3848    fn ruby_inheritance_attributes() {
3849        // Inheritance does not change the attribute count for this class.
3850        check_metrics::<RubyParser>(
3851            "class A < B\n  attr_accessor :x\n  @y = 0\nend\n",
3852            "foo.rb",
3853            |metric| {
3854                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3855                assert_eq!(metric.npa.class_na_sum(), 2.0);
3856                insta::assert_json_snapshot!(metric.npa);
3857            },
3858        );
3859    }
3860
3861    #[test]
3862    fn ruby_constant_assignments_excluded() {
3863        // `CONST = …` at class scope binds a constant, not an
3864        // attribute; the LHS is a `Constant`, not an
3865        // `InstanceVariable` / `ClassVariable`.
3866        check_metrics::<RubyParser>(
3867            "class A\n  PI = 3.14\n  E = 2.71\n  attr_reader :x\nend\n",
3868            "foo.rb",
3869            |metric| {
3870                // Only `attr_reader :x` counts.
3871                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3872                assert_eq!(metric.npa.class_na_sum(), 1.0);
3873                insta::assert_json_snapshot!(metric.npa);
3874            },
3875        );
3876    }
3877
3878    #[test]
3879    fn ruby_multiple_classes_attribute_rollup() {
3880        check_metrics::<RubyParser>(
3881            "class A\n  attr_accessor :x\nend\nclass B\n  private\n  attr_accessor :y\nend\n",
3882            "foo.rb",
3883            |metric| {
3884                // A: 1 public attr. B: 0 public, 1 total.
3885                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3886                assert_eq!(metric.npa.class_na_sum(), 2.0);
3887                insta::assert_json_snapshot!(metric.npa);
3888            },
3889        );
3890    }
3891
3892    // ---------------------------------------------------------------
3893    // Default-impl placeholder smoke tests (audited in #188).
3894    //
3895    // Each test feeds a class / struct with public attributes to a
3896    // language whose `Npa` is currently the default no-op. The
3897    // assertion pins the current 0 value with a TODO pointing at the
3898    // follow-up issue — when the real impl lands the assertion will
3899    // fire and force a test update, which is the gate.
3900    // ---------------------------------------------------------------
3901
3902    // --- Python NPA ---------------------------------------------------
3903
3904    #[test]
3905    fn python_empty_class_no_attributes() {
3906        check_metrics::<PythonParser>("class C:\n    pass\n", "foo.py", |metric| {
3907            assert_eq!(metric.npa.class_na_sum(), 0.0);
3908            assert_eq!(metric.npa.class_npa_sum(), 0.0);
3909            assert_eq!(metric.npa.interface_na_sum(), 0.0);
3910            insta::assert_json_snapshot!(metric.npa);
3911        });
3912    }
3913
3914    #[test]
3915    fn python_class_level_assignments_are_attributes() {
3916        // Two class-level `=` assignments → 2 attributes, all public
3917        // (Python has no visibility keyword).
3918        check_metrics::<PythonParser>("class C:\n    x = 1\n    y = 2\n", "foo.py", |metric| {
3919            assert_eq!(metric.npa.class_na_sum(), 2.0);
3920            assert_eq!(metric.npa.class_npa_sum(), 2.0);
3921            insta::assert_json_snapshot!(metric.npa);
3922        });
3923    }
3924
3925    #[test]
3926    fn python_bare_type_annotation_not_attribute() {
3927        // `x: int` is a bare annotation (declares a type, binds
3928        // nothing); only `y: int = 2` actually creates an attribute.
3929        check_metrics::<PythonParser>(
3930            "class C:\n    x: int\n    y: int = 2\n",
3931            "foo.py",
3932            |metric| {
3933                assert_eq!(metric.npa.class_na_sum(), 1.0);
3934                insta::assert_json_snapshot!(metric.npa);
3935            },
3936        );
3937    }
3938
3939    #[test]
3940    fn python_self_attributes_in_init() {
3941        // `self.x` and `self.y` assigned in `__init__` → 2 instance
3942        // attributes attributed to the class space.
3943        check_metrics::<PythonParser>(
3944            "class C:\n    def __init__(self):\n        self.x = 1\n        self.y = 2\n",
3945            "foo.py",
3946            |metric| {
3947                assert_eq!(metric.npa.class_na_sum(), 2.0);
3948                assert_eq!(metric.npa.class_npa_sum(), 2.0);
3949                insta::assert_json_snapshot!(metric.npa);
3950            },
3951        );
3952    }
3953
3954    #[test]
3955    fn python_self_attributes_in_nested_control_flow() {
3956        // `self.z = 1` and `self.z = 2` in if/else now count once —
3957        // #215 added identifier-text deduplication. Both branches
3958        // bind the same attribute `z`, so `class_na == 1`.
3959        check_metrics::<PythonParser>(
3960            "class C:\n    def __init__(self, flag):\n        if flag:\n            self.z = 1\n        else:\n            self.z = 2\n",
3961            "foo.py",
3962            |metric| {
3963                assert_eq!(metric.npa.class_na_sum(), 1.0);
3964                insta::assert_json_snapshot!(metric.npa);
3965            },
3966        );
3967    }
3968
3969    /// Regression #215: `self.value = …` bound in `__init__` and again
3970    /// in `reset()` should count the attribute exactly once. Before
3971    /// identifier-text deduplication, each binding inflated
3972    /// `class_na` by one — the defensive re-init pattern reported 2.
3973    ///
3974    /// The two assignments use DIFFERENT right-hand sides (`None`
3975    /// vs `0`) so a hypothetical byte-content-of-Assignment dedup
3976    /// (rather than identifier-name dedup) would NOT collapse them.
3977    /// This pins the rule to the attribute *name*, not the
3978    /// assignment text.
3979    #[test]
3980    fn python_defensive_reinit_self_attribute_counts_once() {
3981        check_metrics::<PythonParser>(
3982            "class C:\n    def __init__(self):\n        self.value = None\n    def reset(self):\n        self.value = 0\n",
3983            "foo.py",
3984            |metric| {
3985                assert_eq!(metric.npa.class_na_sum(), 1.0);
3986                assert_eq!(metric.npa.class_npa_sum(), 1.0);
3987                insta::assert_json_snapshot!(metric.npa);
3988            },
3989        );
3990    }
3991
3992    /// Distinct attribute names still accumulate normally — the
3993    /// dedup is per-name, not per-method.
3994    #[test]
3995    fn python_distinct_self_attributes_count_independently() {
3996        check_metrics::<PythonParser>(
3997            "class C:\n    def __init__(self):\n        self.x = 1\n        self.y = 2\n        self.z = 3\n",
3998            "foo.py",
3999            |metric| {
4000                assert_eq!(metric.npa.class_na_sum(), 3.0);
4001                insta::assert_json_snapshot!(metric.npa);
4002            },
4003        );
4004    }
4005
4006    /// Annotated `self.x: int = 1` inside a method body parses as
4007    /// `Assignment(target=Attribute(self, x), type, value)` in
4008    /// tree-sitter-python — the same node type as plain `self.x = 1`.
4009    /// The dedup helper must see both forms and treat them as the
4010    /// same attribute. Regression guard for the review finding on
4011    /// #215: ensure annotated assignments aren't missed.
4012    #[test]
4013    fn python_self_attribute_annotated_assignment_dedupes() {
4014        check_metrics::<PythonParser>(
4015            "class C:\n    def __init__(self):\n        self.value: int = 1\n    def reset(self):\n        self.value = 0\n",
4016            "foo.py",
4017            |metric| {
4018                assert_eq!(metric.npa.class_na_sum(), 1.0);
4019                insta::assert_json_snapshot!(metric.npa);
4020            },
4021        );
4022    }
4023
4024    #[test]
4025    fn python_class_level_and_self_attrs_combine() {
4026        // 1 class-level + 2 instance = 3 total attributes.
4027        check_metrics::<PythonParser>(
4028            "class C:\n    counter = 0\n    def __init__(self):\n        self.name = 'a'\n        self.value = 1\n",
4029            "foo.py",
4030            |metric| {
4031                assert_eq!(metric.npa.class_na_sum(), 3.0);
4032                insta::assert_json_snapshot!(metric.npa);
4033            },
4034        );
4035    }
4036
4037    #[test]
4038    fn python_self_attrs_isolated_per_class() {
4039        // Nested class `Inner` opens its own class space; its
4040        // `self.z = …` belongs to Inner. The class_na_sum aggregates
4041        // across class spaces in the file, so we see both attributes
4042        // (Outer.x + Inner.z) in the unit-level sum; the snapshot
4043        // pins the per-space breakdown.
4044        check_metrics::<PythonParser>(
4045            "class Outer:\n\
4046             \x20   def __init__(self):\n\
4047             \x20       self.x = 1\n\
4048             \x20   class Inner:\n\
4049             \x20       def __init__(self):\n\
4050             \x20           self.z = 2\n",
4051            "foo.py",
4052            |metric| {
4053                assert_eq!(metric.npa.class_na_sum(), 2.0);
4054                insta::assert_json_snapshot!(metric.npa);
4055            },
4056        );
4057    }
4058
4059    #[test]
4060    fn python_decorated_methods_do_not_inflate_attrs() {
4061        // `@property` / `@staticmethod` wrap a `FunctionDefinition` in
4062        // `DecoratedDefinition`. These contribute methods, not
4063        // attributes — Npa must stay at 0.
4064        check_metrics::<PythonParser>(
4065            "class C:\n\
4066             \x20   @property\n\
4067             \x20   def p(self):\n\
4068             \x20       return 1\n\
4069             \x20   @staticmethod\n\
4070             \x20   def s():\n\
4071             \x20       return 2\n",
4072            "foo.py",
4073            |metric| {
4074                assert_eq!(metric.npa.class_na_sum(), 0.0);
4075                insta::assert_json_snapshot!(metric.npa);
4076            },
4077        );
4078    }
4079
4080    #[test]
4081    fn python_module_level_assignments_not_attributes() {
4082        // `x = 1` at module scope is not a class attribute.
4083        check_metrics::<PythonParser>("x = 1\ny = 2\nclass C:\n    a = 3\n", "foo.py", |metric| {
4084            // Only `a = 3` lives in the class space.
4085            assert_eq!(metric.npa.class_na_sum(), 1.0);
4086            insta::assert_json_snapshot!(metric.npa);
4087        });
4088    }
4089
4090    #[test]
4091    fn rust_empty_unit_no_attributes() {
4092        check_metrics::<RustParser>("", "empty.rs", |metric| {
4093            assert_eq!(metric.npa.class_na_sum(), 0.0);
4094            assert_eq!(metric.npa.class_npa_sum(), 0.0);
4095            assert_eq!(metric.npa.interface_na_sum(), 0.0);
4096            assert_eq!(metric.npa.interface_npa_sum(), 0.0);
4097            insta::assert_json_snapshot!(metric.npa);
4098        });
4099    }
4100
4101    #[test]
4102    fn rust_struct_fields_are_attributes() {
4103        // 3 named fields → class_na = 3. `pub a` and `pub c` are public
4104        // → class_npa = 2. `b` is private, so it's not in `npa`.
4105        check_metrics::<RustParser>(
4106            "struct Foo { pub a: i32, b: String, pub c: bool }",
4107            "foo.rs",
4108            |metric| {
4109                assert_eq!(metric.npa.class_na_sum(), 3.0);
4110                assert_eq!(metric.npa.class_npa_sum(), 2.0);
4111                insta::assert_json_snapshot!(metric.npa);
4112            },
4113        );
4114    }
4115
4116    #[test]
4117    fn rust_tuple_struct_fields_are_attributes() {
4118        // Tuple-struct field counting is positional. `Bar(pub i32,
4119        // String)` → 2 fields, 1 public.
4120        check_metrics::<RustParser>("struct Bar(pub i32, String);", "foo.rs", |metric| {
4121            assert_eq!(metric.npa.class_na_sum(), 2.0);
4122            assert_eq!(metric.npa.class_npa_sum(), 1.0);
4123            insta::assert_json_snapshot!(metric.npa);
4124        });
4125    }
4126
4127    #[test]
4128    fn rust_unit_struct_has_no_attributes() {
4129        // `struct Empty;` is a unit struct (no fields). 0 attributes.
4130        check_metrics::<RustParser>("struct Empty;", "foo.rs", |metric| {
4131            assert_eq!(metric.npa.class_na_sum(), 0.0);
4132            insta::assert_json_snapshot!(metric.npa);
4133        });
4134    }
4135
4136    #[test]
4137    fn rust_empty_struct_body_has_no_attributes() {
4138        // `struct Empty {}` is named-field with zero fields.
4139        check_metrics::<RustParser>("struct Empty { }", "foo.rs", |metric| {
4140            assert_eq!(metric.npa.class_na_sum(), 0.0);
4141            insta::assert_json_snapshot!(metric.npa);
4142        });
4143    }
4144
4145    #[test]
4146    fn rust_impl_associated_consts_are_attributes() {
4147        // `const X` and `pub const Y` and `static Z` and `pub static W`
4148        // → 4 associated attributes, 2 public.
4149        check_metrics::<RustParser>(
4150            "struct Foo;\n\
4151             impl Foo {\n\
4152             \x20   const X: i32 = 1;\n\
4153             \x20   pub const Y: i32 = 2;\n\
4154             \x20   static Z: i32 = 3;\n\
4155             \x20   pub static W: i32 = 4;\n\
4156             }\n",
4157            "foo.rs",
4158            |metric| {
4159                // The Impl-space class_na is 4; rolled up to Unit
4160                // class_na_sum it is also 4 (no struct fields in `Foo;`).
4161                assert_eq!(metric.npa.class_na_sum(), 4.0);
4162                assert_eq!(metric.npa.class_npa_sum(), 2.0);
4163                insta::assert_json_snapshot!(metric.npa);
4164            },
4165        );
4166    }
4167
4168    #[test]
4169    fn rust_trait_consts_and_associated_types_are_attributes() {
4170        // `const DEFAULT_COLOR` + `type Item` → 2 interface attributes,
4171        // both public by trait convention. Structural
4172        // `assert_child_space_kind` pins the trait FuncSpace against
4173        // an `is_func_space` revert (see #311).
4174        check_func_space::<RustParser, _>(
4175            "trait Drawable { const DEFAULT_COLOR: u32; type Item; }",
4176            "foo.rs",
4177            |func_space| {
4178                let metric = &func_space.metrics;
4179                assert_eq!(metric.npa.interface_na_sum(), 2.0);
4180                assert_eq!(metric.npa.interface_npa_sum(), 2.0);
4181                assert_eq!(metric.npa.class_na_sum(), 0.0);
4182                insta::assert_json_snapshot!(metric.npa);
4183                assert_child_space_kind(&func_space, "Drawable", SpaceKind::Trait);
4184            },
4185        );
4186    }
4187
4188    #[test]
4189    fn rust_multiple_impls_aggregate() {
4190        // Two `impl Foo` blocks each have one associated const. The
4191        // unit-level rollup should be class_na_sum = 2.
4192        check_metrics::<RustParser>(
4193            "struct Foo;\n\
4194             impl Foo { const X: i32 = 1; }\n\
4195             impl Foo { pub const Y: i32 = 2; }\n",
4196            "foo.rs",
4197            |metric| {
4198                assert_eq!(metric.npa.class_na_sum(), 2.0);
4199                assert_eq!(metric.npa.class_npa_sum(), 1.0);
4200                insta::assert_json_snapshot!(metric.npa);
4201            },
4202        );
4203    }
4204
4205    #[test]
4206    fn rust_module_level_consts_not_attributes() {
4207        // `const PI: f64 = 3.14;` at file scope is a free-standing
4208        // constant — NOT a class attribute. Only consts INSIDE an
4209        // `impl` / `trait` body count.
4210        check_metrics::<RustParser>(
4211            "const PI: f64 = 3.14;\nstatic Q: i32 = 0;\n",
4212            "foo.rs",
4213            |metric| {
4214                assert_eq!(metric.npa.class_na_sum(), 0.0);
4215                assert_eq!(metric.npa.interface_na_sum(), 0.0);
4216                insta::assert_json_snapshot!(metric.npa);
4217            },
4218        );
4219    }
4220
4221    // ----- Go -----
4222
4223    #[test]
4224    fn go_empty_unit_no_attributes() {
4225        // Package-only file declares no struct → npa stays disabled,
4226        // class_na_sum = 0.
4227        check_metrics::<GoParser>("package main\n", "empty.go", |metric| {
4228            assert_eq!(metric.npa.class_na_sum(), 0.0);
4229            insta::assert_json_snapshot!(metric.npa);
4230        });
4231    }
4232
4233    #[test]
4234    fn go_empty_struct_has_no_attributes() {
4235        // `type Empty struct{}` has an empty FieldDeclarationList →
4236        // 0 fields → npa stays disabled.
4237        check_metrics::<GoParser>("package main\ntype Empty struct{}\n", "foo.go", |metric| {
4238            assert_eq!(metric.npa.class_na_sum(), 0.0);
4239            insta::assert_json_snapshot!(metric.npa);
4240        });
4241    }
4242
4243    #[test]
4244    fn go_struct_fields_are_attributes() {
4245        // Three named fields: `X int`, `y string`, `Z float64` → 3
4246        // attributes. Visibility is by identifier case in Go, but the
4247        // trait signature does not give us source bytes, so every
4248        // field is counted as public: class_npa == class_na.
4249        check_metrics::<GoParser>(
4250            "package main\ntype Foo struct { X int; y string; Z float64 }\n",
4251            "foo.go",
4252            |metric| {
4253                assert_eq!(metric.npa.class_na_sum(), 3.0);
4254                assert_eq!(metric.npa.class_npa_sum(), 3.0);
4255                insta::assert_json_snapshot!(metric.npa);
4256            },
4257        );
4258    }
4259
4260    #[test]
4261    fn go_grouped_struct_fields_each_count() {
4262        // `X, Y int` parses as ONE field_declaration with two name
4263        // identifiers — counted as 1 attribute per the
4264        // "FieldDeclaration is the unit" rule. The trailing `Z` is a
4265        // separate field_declaration → 2 attributes total. This
4266        // mirrors Rust's per-FieldDeclaration counting.
4267        check_metrics::<GoParser>(
4268            "package main\ntype Point struct { X, Y int; Z float64 }\n",
4269            "foo.go",
4270            |metric| {
4271                assert_eq!(metric.npa.class_na_sum(), 2.0);
4272                insta::assert_json_snapshot!(metric.npa);
4273            },
4274        );
4275    }
4276
4277    #[test]
4278    fn go_embedded_type_counts_as_attribute() {
4279        // `io.Reader` and `*Foo` are embedded types — field
4280        // declarations with no name, just a type. Each is one
4281        // attribute per the issue spec ("Embedded types: a field
4282        // with no name, just a type — count as one field"). Plus
4283        // `n int` → 3 attributes total.
4284        check_metrics::<GoParser>(
4285            "package main\nimport \"io\"\ntype Bar struct { io.Reader; *Foo; n int }\ntype Foo struct {}\n",
4286            "foo.go",
4287            |metric| {
4288                assert_eq!(metric.npa.class_na_sum(), 3.0);
4289                insta::assert_json_snapshot!(metric.npa);
4290            },
4291        );
4292    }
4293
4294    #[test]
4295    fn go_multiple_structs_aggregate_at_unit() {
4296        // Two structs declared at file scope each contribute their
4297        // fields to the same Unit space (no per-receiver class
4298        // grouping in Go). `Foo` has 1 field, `Bar` has 2 → total
4299        // class_na_sum = 3.
4300        check_metrics::<GoParser>(
4301            "package main\ntype Foo struct { x int }\ntype Bar struct { a int; b string }\n",
4302            "foo.go",
4303            |metric| {
4304                assert_eq!(metric.npa.class_na_sum(), 3.0);
4305                insta::assert_json_snapshot!(metric.npa);
4306            },
4307        );
4308    }
4309
4310    #[test]
4311    fn go_top_level_var_const_not_attributes() {
4312        // Package-level `var` and `const` declarations are NOT
4313        // struct fields — they are free-standing identifiers.
4314        // Expected class_na_sum = 0.
4315        check_metrics::<GoParser>(
4316            "package main\nvar Counter int\nconst Pi = 3.14\n",
4317            "foo.go",
4318            |metric| {
4319                assert_eq!(metric.npa.class_na_sum(), 0.0);
4320                insta::assert_json_snapshot!(metric.npa);
4321            },
4322        );
4323    }
4324
4325    // ----- Elixir -----
4326
4327    // Issue #275: `defstruct` is Elixir's closest analog to a class
4328    // field-set declaration. We count its field arguments as
4329    // (public) attributes.
4330    #[test]
4331    fn elixir_npa_defstruct_keyword_list() {
4332        check_metrics::<ElixirParser>(
4333            "defmodule User do\n  defstruct name: nil, age: 0, email: nil\nend\n",
4334            "foo.ex",
4335            |metric| {
4336                // Three keyword pairs → 3 fields, all public.
4337                assert_eq!(metric.npa.class_na_sum(), 3.0);
4338                assert_eq!(metric.npa.class_npa_sum(), 3.0);
4339            },
4340        );
4341    }
4342
4343    #[test]
4344    fn elixir_npa_defstruct_atom_list() {
4345        check_metrics::<ElixirParser>(
4346            "defmodule User do\n  defstruct [:name, :age, :email]\nend\n",
4347            "foo.ex",
4348            |metric| {
4349                assert_eq!(metric.npa.class_na_sum(), 3.0);
4350                assert_eq!(metric.npa.class_npa_sum(), 3.0);
4351            },
4352        );
4353    }
4354
4355    #[test]
4356    fn elixir_npa_defstruct_bracketed_keyword_list() {
4357        check_metrics::<ElixirParser>(
4358            "defmodule User do\n  defstruct [name: nil, age: 0]\nend\n",
4359            "foo.ex",
4360            |metric| {
4361                assert_eq!(metric.npa.class_na_sum(), 2.0);
4362                assert_eq!(metric.npa.class_npa_sum(), 2.0);
4363            },
4364        );
4365    }
4366
4367    #[test]
4368    fn elixir_npa_defstruct_single_field() {
4369        check_metrics::<ElixirParser>(
4370            "defmodule Box do\n  defstruct value: nil\nend\n",
4371            "foo.ex",
4372            |metric| {
4373                assert_eq!(metric.npa.class_na_sum(), 1.0);
4374                assert_eq!(metric.npa.class_npa_sum(), 1.0);
4375            },
4376        );
4377    }
4378
4379    #[test]
4380    fn elixir_npa_no_defstruct_is_zero() {
4381        check_metrics::<ElixirParser>(
4382            "defmodule Foo do\n  def m, do: :ok\nend\n",
4383            "foo.ex",
4384            |metric| {
4385                assert_eq!(metric.npa.class_na_sum(), 0.0);
4386                assert_eq!(metric.npa.class_npa_sum(), 0.0);
4387            },
4388        );
4389    }
4390
4391    // ----- C++ -----
4392
4393    #[test]
4394    fn cpp_empty_unit_no_attributes() {
4395        // No code → no class spaces → npa = 0. Establishes the trait
4396        // is wired and the per-language compute is reachable.
4397        check_metrics::<CppParser>("", "empty.cpp", |metric| {
4398            assert_eq!(metric.npa.class_na_sum(), 0.0);
4399            assert_eq!(metric.npa.class_npa_sum(), 0.0);
4400            insta::assert_json_snapshot!(metric.npa);
4401        });
4402    }
4403
4404    #[test]
4405    fn cpp_empty_class_no_attributes() {
4406        // `class Foo {};` has no fields. Marked as class space (npa
4407        // becomes visible) but counts stay at 0.
4408        check_metrics::<CppParser>("class Foo {};", "foo.cpp", |metric| {
4409            assert_eq!(metric.npa.class_na_sum(), 0.0);
4410            assert_eq!(metric.npa.class_npa_sum(), 0.0);
4411            insta::assert_json_snapshot!(metric.npa);
4412        });
4413    }
4414
4415    #[test]
4416    fn cpp_class_public_attributes() {
4417        // `class` defaults to private. `public:` flips visibility →
4418        // `int a; int b, c;` becomes 3 public attributes (multi-
4419        // declarator declaration emits one `field_identifier` per
4420        // name). Total: class_na = 3, class_npa = 3.
4421        check_metrics::<CppParser>(
4422            "class Foo { public: int a; int b, c; };",
4423            "foo.cpp",
4424            |metric| {
4425                assert_eq!(metric.npa.class_na_sum(), 3.0);
4426                assert_eq!(metric.npa.class_npa_sum(), 3.0);
4427                insta::assert_json_snapshot!(metric.npa);
4428            },
4429        );
4430    }
4431
4432    #[test]
4433    fn cpp_class_private_default_visibility() {
4434        // No access specifier → `class` keeps its default private
4435        // visibility → `int value_;` counts as 1 attribute but 0 are
4436        // public. class_na = 1, class_npa = 0.
4437        check_metrics::<CppParser>("class Foo { int value_; };", "foo.cpp", |metric| {
4438            assert_eq!(metric.npa.class_na_sum(), 1.0);
4439            assert_eq!(metric.npa.class_npa_sum(), 0.0);
4440            insta::assert_json_snapshot!(metric.npa);
4441        });
4442    }
4443
4444    #[test]
4445    fn cpp_struct_default_public_visibility() {
4446        // `struct` defaults to public — opposite of `class`. The same
4447        // field counts once and is public.
4448        check_metrics::<CppParser>("struct Bar { int value_; };", "foo.cpp", |metric| {
4449            assert_eq!(metric.npa.class_na_sum(), 1.0);
4450            assert_eq!(metric.npa.class_npa_sum(), 1.0);
4451            insta::assert_json_snapshot!(metric.npa);
4452        });
4453    }
4454
4455    #[test]
4456    fn cpp_mixed_visibility_sections() {
4457        // Public section: 1 field. Protected section (bucketed with
4458        // private for npa): 1 field. Private section: 1 field.
4459        // class_na = 3, class_npa = 1.
4460        check_metrics::<CppParser>(
4461            "class Foo {\n\
4462                 public: int a;\n\
4463                 protected: int b;\n\
4464                 private: int c;\n\
4465             };",
4466            "foo.cpp",
4467            |metric| {
4468                assert_eq!(metric.npa.class_na_sum(), 3.0);
4469                assert_eq!(metric.npa.class_npa_sum(), 1.0);
4470                insta::assert_json_snapshot!(metric.npa);
4471            },
4472        );
4473    }
4474
4475    #[test]
4476    fn cpp_methods_not_counted_as_attributes() {
4477        // Inline-defined methods (`function_definition`) and
4478        // declaration-only methods (`field_declaration` containing
4479        // `function_declarator`) must NOT be counted as attributes.
4480        // Only the data field `value_` adds to `class_na`.
4481        check_metrics::<CppParser>(
4482            "class Foo {\n\
4483                 public:\n\
4484                     void method1() {}\n\
4485                     void method2();\n\
4486                 private:\n\
4487                     int value_;\n\
4488             };",
4489            "foo.cpp",
4490            |metric| {
4491                assert_eq!(metric.npa.class_na_sum(), 1.0);
4492                assert_eq!(metric.npa.class_npa_sum(), 0.0);
4493                insta::assert_json_snapshot!(metric.npa);
4494            },
4495        );
4496    }
4497
4498    #[test]
4499    fn cpp_pointer_array_fields_count() {
4500        // `int* p;` wraps the `field_identifier` inside
4501        // `pointer_declarator`. `int a[10];` wraps it inside
4502        // `array_declarator`. Both must be reached by the recursive
4503        // helper. Plus a plain `int x;` → 3 attributes total.
4504        check_metrics::<CppParser>(
4505            "struct S {\n\
4506                 int* p;\n\
4507                 int a[10];\n\
4508                 int x;\n\
4509             };",
4510            "foo.cpp",
4511            |metric| {
4512                assert_eq!(metric.npa.class_na_sum(), 3.0);
4513                // Struct → all public.
4514                assert_eq!(metric.npa.class_npa_sum(), 3.0);
4515                insta::assert_json_snapshot!(metric.npa);
4516            },
4517        );
4518    }
4519
4520    #[test]
4521    fn cpp_multiple_classes_aggregate_at_unit() {
4522        // Two classes in one file. Each contributes to its own
4523        // class space; the file-level (Unit) class_na_sum aggregates
4524        // both. Foo has 2 attrs (1 public, 1 private). Bar has 1.
4525        // Total class_na_sum at Unit = 3.
4526        check_metrics::<CppParser>(
4527            "class Foo { public: int a; private: int b; };\nstruct Bar { int c; };",
4528            "foo.cpp",
4529            |metric| {
4530                assert_eq!(metric.npa.class_na_sum(), 3.0);
4531                // Public: Foo::a (1) + Bar::c (1) = 2.
4532                assert_eq!(metric.npa.class_npa_sum(), 2.0);
4533                insta::assert_json_snapshot!(metric.npa);
4534            },
4535        );
4536    }
4537
4538    #[test]
4539    fn javascript_empty_unit_no_attributes() {
4540        // Wires up the trait and ensures no spurious attribute counts
4541        // on an empty file.
4542        check_metrics::<JavascriptParser>("", "empty.js", |metric| {
4543            assert_eq!(metric.npa.class_na_sum(), 0.0);
4544            assert_eq!(metric.npa.class_npa_sum(), 0.0);
4545            insta::assert_json_snapshot!(metric.npa);
4546        });
4547    }
4548
4549    #[test]
4550    fn javascript_empty_class_no_attributes() {
4551        // A class with no body and no fields has zero attributes.
4552        check_metrics::<JavascriptParser>("class Foo {}", "foo.js", |metric| {
4553            assert_eq!(metric.npa.class_na_sum(), 0.0);
4554            assert_eq!(metric.npa.class_npa_sum(), 0.0);
4555            insta::assert_json_snapshot!(metric.npa);
4556        });
4557    }
4558
4559    #[test]
4560    fn javascript_class_fields_count() {
4561        // ES2022 class fields: `class Foo { x = 1; y; static z = 2; }`.
4562        // All three are `field_definition` direct children of
4563        // `class_body`. JS has no visibility — everything is public.
4564        // class_na = class_npa = 3.
4565        check_metrics::<JavascriptParser>(
4566            "class Foo { x = 1; y; static z = 2; }",
4567            "foo.js",
4568            |metric| {
4569                assert_eq!(metric.npa.class_na_sum(), 3.0);
4570                assert_eq!(metric.npa.class_npa_sum(), 3.0);
4571                insta::assert_json_snapshot!(metric.npa);
4572            },
4573        );
4574    }
4575
4576    #[test]
4577    fn javascript_arrow_field_is_method_not_attribute() {
4578        // `class Foo { x = () => {} }` declares a method, not an
4579        // attribute. The arrow function initializer makes this an
4580        // `Npm` member, not an `Npa` member.
4581        check_metrics::<JavascriptParser>(
4582            "class Foo { x = () => {}; y = function() {}; z = 1; }",
4583            "foo.js",
4584            |metric| {
4585                // Only `z = 1` is an attribute.
4586                assert_eq!(metric.npa.class_na_sum(), 1.0);
4587                assert_eq!(metric.npa.class_npa_sum(), 1.0);
4588                insta::assert_json_snapshot!(metric.npa);
4589            },
4590        );
4591    }
4592
4593    #[test]
4594    fn javascript_methods_not_counted_as_attributes() {
4595        // `method_definition` direct children of `class_body` are
4596        // methods, not fields. They must not show up in `npa`.
4597        check_metrics::<JavascriptParser>(
4598            "class Foo { constructor() {} bar() {} get baz() { return 1; } x = 1; }",
4599            "foo.js",
4600            |metric| {
4601                // Only `x = 1` is a true attribute.
4602                assert_eq!(metric.npa.class_na_sum(), 1.0);
4603                assert_eq!(metric.npa.class_npa_sum(), 1.0);
4604                insta::assert_json_snapshot!(metric.npa);
4605            },
4606        );
4607    }
4608
4609    #[test]
4610    fn javascript_multiple_classes_aggregate_at_unit() {
4611        // Two classes contribute their attribute counts to the
4612        // Unit-level rollup. Foo has 2 fields; Bar has 1. Total
4613        // class_na_sum = 3.
4614        check_metrics::<JavascriptParser>(
4615            "class Foo { a = 1; b = 2; }\nclass Bar { c = 3; }",
4616            "foo.js",
4617            |metric| {
4618                assert_eq!(metric.npa.class_na_sum(), 3.0);
4619                assert_eq!(metric.npa.class_npa_sum(), 3.0);
4620                insta::assert_json_snapshot!(metric.npa);
4621            },
4622        );
4623    }
4624
4625    #[test]
4626    fn mozjs_class_fields_count() {
4627        // Mozjs shares JS's class vocabulary. Same expectation as the
4628        // JS parity test above.
4629        check_metrics::<MozjsParser>(
4630            "class Foo { x = 1; y; static z = 2; }",
4631            "foo.js",
4632            |metric| {
4633                assert_eq!(metric.npa.class_na_sum(), 3.0);
4634                assert_eq!(metric.npa.class_npa_sum(), 3.0);
4635                insta::assert_json_snapshot!(metric.npa);
4636            },
4637        );
4638    }
4639}