Skip to main content

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}