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