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}