big_code_analysis/metrics/mod.rs
1//! Per-metric implementations.
2//!
3//! Each submodule defines one maintainability metric, its per-language
4//! traits, and its `Stats` accumulator. See the crate-level docs for an
5//! overview of the metric suite.
6
7use crate::{Node, SpaceKind};
8
9/// Assignment / Branch / Condition counts.
10pub mod abc;
11/// Cognitive complexity.
12pub mod cognitive;
13/// Cyclomatic complexity.
14pub mod cyclomatic;
15/// Halstead suite (operators, operands, volume, difficulty, effort).
16pub mod halstead;
17/// Lines-of-code variants (SLOC, PLOC, LLOC, CLOC, blank).
18pub mod loc;
19/// Maintainability Index.
20pub mod mi;
21/// Number of arguments per function.
22pub mod nargs;
23/// Exit-point counting.
24pub mod nexits;
25/// Number of methods (functions + closures).
26pub mod nom;
27/// Number of public attributes.
28pub mod npa;
29/// Number of public methods.
30pub mod npm;
31/// Token count.
32pub mod tokens;
33/// Weighted Methods per Class.
34pub mod wmc;
35
36mod elixir_decisions;
37
38#[cfg(test)]
39#[path = "bash_test_connective_tests.rs"]
40mod bash_test_connective_tests;
41// Gated on the union of every feature any test in the module names —
42// the `FIXTURES` rows plus the `bash` / `lua` / `c` row set that
43// `a_language_with_no_member_construct_emits_neither_block` uses. The
44// narrower `FIXTURES` union belongs on the four tests that read
45// `FIXTURES`, not here: a module gate is an outer `cfg`, so naming only
46// the `FIXTURES` features would delete that Bash/Lua/C test from a build
47// enabling exactly the languages it exists to cover
48// (`.claude/rules/testing.md`, #1286).
49#[cfg(test)]
50#[cfg(any(
51 feature = "bash",
52 feature = "c",
53 feature = "cpp",
54 feature = "csharp",
55 feature = "elixir",
56 feature = "go",
57 feature = "groovy",
58 feature = "java",
59 feature = "javascript",
60 feature = "kotlin",
61 feature = "lua",
62 feature = "mozcpp",
63 feature = "mozjs",
64 feature = "objc",
65 feature = "php",
66 feature = "python",
67 feature = "ruby",
68 feature = "rust",
69 feature = "typescript"
70))]
71#[path = "container_scope_tests.rs"]
72mod container_scope_tests;
73#[cfg(test)]
74#[cfg(any(feature = "cpp", feature = "mozcpp"))]
75#[path = "cpp_alternative_tokens_tests.rs"]
76mod cpp_alternative_tokens_tests;
77#[cfg(test)]
78#[path = "elixir_named_operator_tests.rs"]
79mod elixir_named_operator_tests;
80#[cfg(test)]
81#[path = "elixir_spelling_parity_tests.rs"]
82mod elixir_spelling_parity_tests;
83#[cfg(test)]
84#[path = "perl_keyword_key_tests.rs"]
85mod perl_keyword_key_tests;
86
87/// Divides a metric sum by a count, guarding the divisor with `.max(1)`.
88///
89/// Every "average over a count" metric routes through this helper so the
90/// divide-by-zero guard added for [#428] is applied uniformly rather than
91/// per call site. A `count` of `0` degrades to `sum / 1` (the sum itself)
92/// instead of producing `inf`/`NaN`, so a never-observed or count-less
93/// space still serializes a finite number.
94///
95/// The *meaning* of `count` is the caller's choice of denominator
96/// convention; the project uses two:
97///
98/// - **Per-function** averages (`cognitive`, `cyclomatic`, `exit`,
99/// `nargs`) divide by the function/closure count of the subtree, so the
100/// value reads as "average complexity per function". `cognitive`/
101/// `exit`/`nargs` source this count from `Nom`; `cyclomatic` counts its
102/// own function/closure *spaces* (equal in the common case, but
103/// independent of whether `Nom` is selected — see
104/// `cyclomatic::Stats::function_spaces`).
105/// - **Per-space** averages (`nom`, `loc`, `abc`, `tokens`) divide by the
106/// total number of spaces (functions, closures, classes, the file unit,
107/// …). These measure a property of each space rather than of each
108/// function, so a per-function denominator would not match their
109/// meaning (and for `nom` it would be circular — it *is* the function
110/// count).
111///
112/// [#428]: https://github.com/dekobon/big-code-analysis/issues/428
113#[inline]
114#[must_use]
115// `count as f64` is exact for any realistic space count; the cast mirrors
116// the per-metric modules' module-level allowance for count-to-float casts.
117#[allow(clippy::cast_precision_loss)]
118pub(crate) fn average(sum: f64, count: usize) -> f64 {
119 sum / count.max(1) as f64
120}
121
122/// How far the C++ `requires` clause or `requires` expression the walk
123/// is inside reaches, so `cyclomatic`, `cognitive` and `abc` can leave
124/// it unscored in `O(1)` per node.
125///
126/// A constraint is checked at compile time, and a requires-expression's
127/// requirements are never evaluated at all, so neither branches. The
128/// grammar parses `requires A<T> && B<T>` as a `constraint_conjunction`,
129/// which no metric arm matches, but `requires (A<T> && B<T>)` as an
130/// ordinary `binary_expression`. Asking each operator whether it sits in
131/// a clause by climbing to one made a long `&&` chain quadratic (#1533),
132/// so the answer is carried down the walk instead.
133///
134/// It is exact because the walk is pre-order: a clause is visited before
135/// its descendants, and every node visited after the clause starts
136/// before the clause's end byte if and only if it is a descendant. A
137/// nested clause ends within the outer one, so the outer reach already
138/// covers it. Each space keeps its own reach in its metric `Stats`;
139/// C++ lambdas open no space, so a lambda written in a constraint
140/// scores nothing either. Never serialized or merged: it is walk state.
141#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
142pub(crate) struct ConstraintReach {
143 end_byte: usize,
144}
145
146impl ConstraintReach {
147 /// Whether `node` lies in a constraint, recording the reach of the
148 /// one `node` opens when `opens_constraint` says it is a
149 /// `requires_clause` or `requires_expression`.
150 #[inline]
151 pub(crate) fn covers(&mut self, node: &Node, opens_constraint: bool) -> bool {
152 if node.start_byte() < self.end_byte {
153 return true;
154 }
155 if opens_constraint {
156 self.end_byte = node.end_byte();
157 }
158 opens_constraint
159 }
160}
161
162/// Whether the object-oriented member metrics — `wmc`, `npm`, `npa` —
163/// are meaningful on a space of a given [`SpaceKind`].
164///
165/// An extension trait rather than an inherent method because
166/// `SpaceKind` is defined in `big-code-analysis-ast`, which knows
167/// nothing about metrics: "does `wmc` apply here" is a question only
168/// this crate can ask, so it is answered here (#1376).
169pub(crate) trait MemberScopeExt {
170 /// True for every container (which owns methods and attributes) and
171 /// for the file [`SpaceKind::Unit`] (which aggregates its
172 /// containers' counts into a whole-file roll-up). False for a
173 /// function space, which owns neither, and for
174 /// [`SpaceKind::Unknown`].
175 ///
176 /// This is the single definition the three metrics share. `wmc`
177 /// carried it alone as an inline `matches!`; `npm` and `npa` enabled
178 /// themselves from `Checker::is_func_space` instead, which admits
179 /// function spaces and so gave a Kotlin `<get>` or a JavaScript
180 /// method an all-zero block its sibling method did not have (#1197).
181 ///
182 /// Phrased as an exclusion so that a future [`SpaceKind`] variant —
183 /// the enum is `#[non_exhaustive]` — defaults to *carrying* the
184 /// metrics. A new kind is far likelier to be another container than
185 /// another callable, and an extra roll-up is a milder wrong answer
186 /// than a silently missing one.
187 ///
188 /// Takes `&self` rather than a by-value `Copy`: the receiver is
189 /// cost-free either way, and a trait cannot tell
190 /// `clippy::wrong_self_convention` that every implementor is `Copy`,
191 /// so by-value would need a carve-out that buys nothing.
192 fn is_member_scope(&self) -> bool;
193}
194
195impl MemberScopeExt for SpaceKind {
196 #[inline]
197 fn is_member_scope(&self) -> bool {
198 !matches!(*self, SpaceKind::Function | SpaceKind::Unknown)
199 }
200}