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 = "perl_keyword_key_tests.rs"]
82mod perl_keyword_key_tests;
83
84/// Divides a metric sum by a count, guarding the divisor with `.max(1)`.
85///
86/// Every "average over a count" metric routes through this helper so the
87/// divide-by-zero guard added for [#428] is applied uniformly rather than
88/// per call site. A `count` of `0` degrades to `sum / 1` (the sum itself)
89/// instead of producing `inf`/`NaN`, so a never-observed or count-less
90/// space still serializes a finite number.
91///
92/// The *meaning* of `count` is the caller's choice of denominator
93/// convention; the project uses two:
94///
95/// - **Per-function** averages (`cognitive`, `cyclomatic`, `exit`,
96///   `nargs`) divide by the function/closure count of the subtree, so the
97///   value reads as "average complexity per function". `cognitive`/
98///   `exit`/`nargs` source this count from `Nom`; `cyclomatic` counts its
99///   own function/closure *spaces* (equal in the common case, but
100///   independent of whether `Nom` is selected — see
101///   `cyclomatic::Stats::function_spaces`).
102/// - **Per-space** averages (`nom`, `loc`, `abc`, `tokens`) divide by the
103///   total number of spaces (functions, closures, classes, the file unit,
104///   …). These measure a property of each space rather than of each
105///   function, so a per-function denominator would not match their
106///   meaning (and for `nom` it would be circular — it *is* the function
107///   count).
108///
109/// [#428]: https://github.com/dekobon/big-code-analysis/issues/428
110#[inline]
111#[must_use]
112// `count as f64` is exact for any realistic space count; the cast mirrors
113// the per-metric modules' module-level allowance for count-to-float casts.
114#[allow(clippy::cast_precision_loss)]
115pub(crate) fn average(sum: f64, count: usize) -> f64 {
116    sum / count.max(1) as f64
117}
118
119/// How far the C++ `requires` clause or `requires` expression the walk
120/// is inside reaches, so `cyclomatic`, `cognitive` and `abc` can leave
121/// it unscored in `O(1)` per node.
122///
123/// A constraint is checked at compile time, and a requires-expression's
124/// requirements are never evaluated at all, so neither branches. The
125/// grammar parses `requires A<T> && B<T>` as a `constraint_conjunction`,
126/// which no metric arm matches, but `requires (A<T> && B<T>)` as an
127/// ordinary `binary_expression`. Asking each operator whether it sits in
128/// a clause by climbing to one made a long `&&` chain quadratic (#1533),
129/// so the answer is carried down the walk instead.
130///
131/// It is exact because the walk is pre-order: a clause is visited before
132/// its descendants, and every node visited after the clause starts
133/// before the clause's end byte if and only if it is a descendant. A
134/// nested clause ends within the outer one, so the outer reach already
135/// covers it. Each space keeps its own reach in its metric `Stats`;
136/// C++ lambdas open no space, so a lambda written in a constraint
137/// scores nothing either. Never serialized or merged: it is walk state.
138#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
139pub(crate) struct ConstraintReach {
140    end_byte: usize,
141}
142
143impl ConstraintReach {
144    /// Whether `node` lies in a constraint, recording the reach of the
145    /// one `node` opens when `opens_constraint` says it is a
146    /// `requires_clause` or `requires_expression`.
147    #[inline]
148    pub(crate) fn covers(&mut self, node: &Node, opens_constraint: bool) -> bool {
149        if node.start_byte() < self.end_byte {
150            return true;
151        }
152        if opens_constraint {
153            self.end_byte = node.end_byte();
154        }
155        opens_constraint
156    }
157}
158
159/// Whether the object-oriented member metrics — `wmc`, `npm`, `npa` —
160/// are meaningful on a space of a given [`SpaceKind`].
161///
162/// An extension trait rather than an inherent method because
163/// `SpaceKind` is defined in `big-code-analysis-ast`, which knows
164/// nothing about metrics: "does `wmc` apply here" is a question only
165/// this crate can ask, so it is answered here (#1376).
166pub(crate) trait MemberScopeExt {
167    /// True for every container (which owns methods and attributes) and
168    /// for the file [`SpaceKind::Unit`] (which aggregates its
169    /// containers' counts into a whole-file roll-up). False for a
170    /// function space, which owns neither, and for
171    /// [`SpaceKind::Unknown`].
172    ///
173    /// This is the single definition the three metrics share. `wmc`
174    /// carried it alone as an inline `matches!`; `npm` and `npa` enabled
175    /// themselves from `Checker::is_func_space` instead, which admits
176    /// function spaces and so gave a Kotlin `<get>` or a JavaScript
177    /// method an all-zero block its sibling method did not have (#1197).
178    ///
179    /// Phrased as an exclusion so that a future [`SpaceKind`] variant —
180    /// the enum is `#[non_exhaustive]` — defaults to *carrying* the
181    /// metrics. A new kind is far likelier to be another container than
182    /// another callable, and an extra roll-up is a milder wrong answer
183    /// than a silently missing one.
184    ///
185    /// Takes `&self` rather than a by-value `Copy`: the receiver is
186    /// cost-free either way, and a trait cannot tell
187    /// `clippy::wrong_self_convention` that every implementor is `Copy`,
188    /// so by-value would need a carve-out that buys nothing.
189    fn is_member_scope(&self) -> bool;
190}
191
192impl MemberScopeExt for SpaceKind {
193    #[inline]
194    fn is_member_scope(&self) -> bool {
195        !matches!(*self, SpaceKind::Function | SpaceKind::Unknown)
196    }
197}