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::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
36// Gated on the union of every feature any test in the module names —
37// the `FIXTURES` rows plus the `bash` / `lua` / `c` row set that
38// `a_language_with_no_member_construct_emits_neither_block` uses. The
39// narrower `FIXTURES` union belongs on the four tests that read
40// `FIXTURES`, not here: a module gate is an outer `cfg`, so naming only
41// the `FIXTURES` features would delete that Bash/Lua/C test from a build
42// enabling exactly the languages it exists to cover
43// (`.claude/rules/testing.md`, #1286).
44#[cfg(test)]
45#[cfg(any(
46    feature = "bash",
47    feature = "c",
48    feature = "cpp",
49    feature = "csharp",
50    feature = "elixir",
51    feature = "go",
52    feature = "groovy",
53    feature = "java",
54    feature = "javascript",
55    feature = "kotlin",
56    feature = "lua",
57    feature = "mozcpp",
58    feature = "mozjs",
59    feature = "objc",
60    feature = "php",
61    feature = "python",
62    feature = "ruby",
63    feature = "rust",
64    feature = "typescript"
65))]
66#[path = "container_scope_tests.rs"]
67mod container_scope_tests;
68
69/// Divides a metric sum by a count, guarding the divisor with `.max(1)`.
70///
71/// Every "average over a count" metric routes through this helper so the
72/// divide-by-zero guard added for [#428] is applied uniformly rather than
73/// per call site. A `count` of `0` degrades to `sum / 1` (the sum itself)
74/// instead of producing `inf`/`NaN`, so a never-observed or count-less
75/// space still serializes a finite number.
76///
77/// The *meaning* of `count` is the caller's choice of denominator
78/// convention; the project uses two:
79///
80/// - **Per-function** averages (`cognitive`, `cyclomatic`, `exit`,
81///   `nargs`) divide by the function/closure count of the subtree, so the
82///   value reads as "average complexity per function". `cognitive`/
83///   `exit`/`nargs` source this count from `Nom`; `cyclomatic` counts its
84///   own function/closure *spaces* (equal in the common case, but
85///   independent of whether `Nom` is selected — see
86///   `cyclomatic::Stats::function_spaces`).
87/// - **Per-space** averages (`nom`, `loc`, `abc`, `tokens`) divide by the
88///   total number of spaces (functions, closures, classes, the file unit,
89///   …). These measure a property of each space rather than of each
90///   function, so a per-function denominator would not match their
91///   meaning (and for `nom` it would be circular — it *is* the function
92///   count).
93///
94/// [#428]: https://github.com/dekobon/big-code-analysis/issues/428
95#[inline]
96#[must_use]
97// `count as f64` is exact for any realistic space count; the cast mirrors
98// the per-metric modules' module-level allowance for count-to-float casts.
99#[allow(clippy::cast_precision_loss)]
100pub(crate) fn average(sum: f64, count: usize) -> f64 {
101    sum / count.max(1) as f64
102}
103
104/// Whether the object-oriented member metrics — `wmc`, `npm`, `npa` —
105/// are meaningful on a space of a given [`SpaceKind`].
106///
107/// An extension trait rather than an inherent method because
108/// `SpaceKind` is defined in `big-code-analysis-ast`, which knows
109/// nothing about metrics: "does `wmc` apply here" is a question only
110/// this crate can ask, so it is answered here (#1376).
111pub(crate) trait MemberScopeExt {
112    /// True for every container (which owns methods and attributes) and
113    /// for the file [`SpaceKind::Unit`] (which aggregates its
114    /// containers' counts into a whole-file roll-up). False for a
115    /// function space, which owns neither, and for
116    /// [`SpaceKind::Unknown`].
117    ///
118    /// This is the single definition the three metrics share. `wmc`
119    /// carried it alone as an inline `matches!`; `npm` and `npa` enabled
120    /// themselves from `Checker::is_func_space` instead, which admits
121    /// function spaces and so gave a Kotlin `<get>` or a JavaScript
122    /// method an all-zero block its sibling method did not have (#1197).
123    ///
124    /// Phrased as an exclusion so that a future [`SpaceKind`] variant —
125    /// the enum is `#[non_exhaustive]` — defaults to *carrying* the
126    /// metrics. A new kind is far likelier to be another container than
127    /// another callable, and an extra roll-up is a milder wrong answer
128    /// than a silently missing one.
129    ///
130    /// Takes `&self` rather than a by-value `Copy`: the receiver is
131    /// cost-free either way, and a trait cannot tell
132    /// `clippy::wrong_self_convention` that every implementor is `Copy`,
133    /// so by-value would need a carve-out that buys nothing.
134    fn is_member_scope(&self) -> bool;
135}
136
137impl MemberScopeExt for SpaceKind {
138    #[inline]
139    fn is_member_scope(&self) -> bool {
140        !matches!(*self, SpaceKind::Function | SpaceKind::Unknown)
141    }
142}