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}