Skip to main content

big_code_analysis/
metric_set.rs

1//! Per-metric selection: the [`Metric`] enum and the
2//! [`MetricSet`] bitfield it gates.
3//!
4//! Used by [`MetricsOptions::with_only`](crate::MetricsOptions::with_only)
5//! to restrict which metrics are computed during a walk, and by
6//! [`CodeMetrics`](crate::CodeMetrics)'s `Serialize` impl to elide
7//! fields the caller did not select.
8
9use std::fmt;
10use std::str::FromStr;
11
12/// One metric computed by the analysis walker.
13///
14/// Pass a slice of these to
15/// [`MetricsOptions::with_only`](crate::MetricsOptions::with_only) to
16/// restrict computation to the listed metrics.
17///
18/// `#[non_exhaustive]` so future metrics can land additively. Use
19/// `match` against the existing variants and either a wildcard arm or
20/// the `m if !MetricSet::all().contains(m)` guard to stay
21/// forwards-compatible.
22#[non_exhaustive]
23#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
24pub enum Metric {
25    /// Cognitive complexity ([`crate::cognitive::Stats`]).
26    Cognitive,
27    /// Cyclomatic complexity ([`crate::cyclomatic::Stats`]).
28    Cyclomatic,
29    /// Halstead ([`crate::halstead::Stats`]).
30    Halstead,
31    /// LoC family ([`crate::loc::Stats`]).
32    Loc,
33    /// Number of methods ([`crate::nom::Stats`]).
34    Nom,
35    /// Token counts ([`crate::tokens::Stats`]).
36    Tokens,
37    /// Number of arguments ([`crate::nargs::Stats`]).
38    NArgs,
39    /// Exit-point count ([`crate::exit::Stats`]).
40    Exit,
41    /// ABC ([`crate::abc::Stats`]).
42    Abc,
43    /// Number of public methods ([`crate::npm::Stats`]).
44    Npm,
45    /// Number of public attributes ([`crate::npa::Stats`]).
46    Npa,
47    /// Maintainability index ([`crate::mi::Stats`]). Derived metric:
48    /// selecting only `Mi` via
49    /// [`MetricsOptions::with_only`](crate::MetricsOptions::with_only)
50    /// also pulls in [`Metric::Loc`], [`Metric::Cyclomatic`], and
51    /// [`Metric::Halstead`].
52    Mi,
53    /// Weighted methods per class ([`crate::wmc::Stats`]). Derived
54    /// metric: selecting `Wmc` also pulls in [`Metric::Cyclomatic`]
55    /// and [`Metric::Nom`].
56    Wmc,
57}
58
59impl Metric {
60    // Bit position used inside [`MetricSet`]. The ordering is
61    // intentionally arbitrary — the only contract is that each
62    // variant maps to a distinct bit.
63    //
64    // Returns `u32` to match [`MetricSet`]'s storage width: at `u16`
65    // the bitfield would overflow once a 17th variant landed (debug
66    // panic / release wrap), and `Metric` is `#[non_exhaustive]`
67    // specifically so new variants can land additively.
68    #[inline]
69    const fn bit(self) -> u32 {
70        1 << (self as u32)
71    }
72
73    /// Returns the slice of metrics this metric depends on.
74    ///
75    /// Derived metrics (`Mi`, `Wmc`) consume the outputs of other
76    /// metrics during the finalize step; selecting one without its
77    /// dependencies would leave the dependency's `Stats` at default
78    /// (zero) values and silently corrupt the derived value. Callers
79    /// typically reach this through
80    /// [`MetricsOptions::with_only`](crate::MetricsOptions::with_only),
81    /// which auto-resolves the closure transparently.
82    #[must_use]
83    pub const fn dependencies(self) -> &'static [Metric] {
84        match self {
85            // Mi = function(Loc, Cyclomatic, Halstead). All three must
86            // be computed for the MI formula to be meaningful.
87            Self::Mi => &[Self::Loc, Self::Cyclomatic, Self::Halstead],
88            // Wmc aggregates per-method cyclomatic complexity and
89            // needs Nom to count those methods.
90            Self::Wmc => &[Self::Cyclomatic, Self::Nom],
91            _ => &[],
92        }
93    }
94
95    /// Canonical user-facing name for each metric — the single
96    /// source of truth shared by the Python bindings'
97    /// `bca.METRIC_NAMES` constant, the `unknown metric: <bad>;
98    /// valid: …` error message, and any downstream Rust consumer
99    /// that parses user input into a [`MetricSet`].
100    ///
101    /// Each entry round-trips through [`Metric::from_str`]. The table uses the
102    /// JSON-output-key spelling for [`Metric::Exit`] (`"nexits"`,
103    /// matching the `CodeMetrics::Serialize` impl in
104    /// `src/spaces.rs`) rather than the [`fmt::Display`] spelling
105    /// (`"exit"`); both parse to [`Metric::Exit`] via the alias
106    /// arm in `FromStr`, but the canonical spelling exposed here
107    /// is the JSON one so callers see the same name in
108    /// `Metric::NAMES`, in the output dict, and in error
109    /// messages.
110    ///
111    /// Alphabetised. The drift between this table and the
112    /// `FromStr` arms (or the `Metric` enum itself) is guarded by
113    /// `names_table_parses_to_every_variant` and
114    /// `names_table_is_alphabetised` in the test module below.
115    pub const NAMES: &'static [&'static str] = &[
116        "abc",
117        "cognitive",
118        "cyclomatic",
119        "halstead",
120        "loc",
121        "mi",
122        "nargs",
123        "nexits",
124        "nom",
125        "npa",
126        "npm",
127        "tokens",
128        "wmc",
129    ];
130}
131
132impl fmt::Display for Metric {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        let s = match self {
135            Self::Cognitive => "cognitive",
136            Self::Cyclomatic => "cyclomatic",
137            Self::Halstead => "halstead",
138            Self::Loc => "loc",
139            Self::Nom => "nom",
140            Self::Tokens => "tokens",
141            Self::NArgs => "nargs",
142            Self::Exit => "exit",
143            Self::Abc => "abc",
144            Self::Npm => "npm",
145            Self::Npa => "npa",
146            Self::Mi => "mi",
147            Self::Wmc => "wmc",
148        };
149        f.write_str(s)
150    }
151}
152
153/// Error returned by [`Metric::from_str`] when the input
154/// is not a recognised metric name.
155///
156/// Holds the offending input verbatim. Downstream consumers that own
157/// the canonical name table (e.g. the `bca` Python bindings'
158/// `METRIC_NAMES` constant) typically compose this with a
159/// `valid: <list>` suffix from their own source of truth; this type
160/// deliberately stays out of that policy and only carries the
161/// rejected input so the wrapper layer can format the user-facing
162/// message however it wants.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct ParseMetricError(String);
165
166impl fmt::Display for ParseMetricError {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "unknown metric: {}", self.0)
169    }
170}
171
172impl std::error::Error for ParseMetricError {}
173
174impl FromStr for Metric {
175    type Err = ParseMetricError;
176
177    /// Parse a [`Metric`] from its [`fmt::Display`] spelling.
178    ///
179    /// Strict lowercase: `"Loc"` is rejected. The single alias is
180    /// `"nexits"`, which parses to [`Metric::Exit`] — this matches
181    /// the JSON output key the metric's `Stats` serialises under,
182    /// so downstream consumers can use either the enum-Display
183    /// spelling or the JSON-key spelling interchangeably.
184    fn from_str(s: &str) -> Result<Self, Self::Err> {
185        match s {
186            "cognitive" => Ok(Self::Cognitive),
187            "cyclomatic" => Ok(Self::Cyclomatic),
188            "halstead" => Ok(Self::Halstead),
189            "loc" => Ok(Self::Loc),
190            "nom" => Ok(Self::Nom),
191            "tokens" => Ok(Self::Tokens),
192            "nargs" => Ok(Self::NArgs),
193            "exit" | "nexits" => Ok(Self::Exit),
194            "abc" => Ok(Self::Abc),
195            "npm" => Ok(Self::Npm),
196            "npa" => Ok(Self::Npa),
197            "mi" => Ok(Self::Mi),
198            "wmc" => Ok(Self::Wmc),
199            _ => Err(ParseMetricError(s.to_owned())),
200        }
201    }
202}
203
204/// Bitfield of selected metrics.
205///
206/// Stored on [`MetricsOptions`](crate::MetricsOptions) (controls
207/// which metrics the walker computes) and on
208/// [`CodeMetrics`](crate::CodeMetrics) (controls which fields the
209/// `Serialize` impl emits).
210///
211/// `MetricSet::all()` is the default: every metric enabled, matching
212/// the pre-#257 behaviour.
213#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
214pub struct MetricSet(u32);
215
216impl MetricSet {
217    // All-metrics mask: OR together every variant's bit. Kept
218    // explicit (rather than `(1 << N) - 1`) so adding a new variant
219    // requires a deliberate edit here and surfaces in code review.
220    const ALL_BITS: u32 = Metric::Cognitive.bit()
221        | Metric::Cyclomatic.bit()
222        | Metric::Halstead.bit()
223        | Metric::Loc.bit()
224        | Metric::Nom.bit()
225        | Metric::Tokens.bit()
226        | Metric::NArgs.bit()
227        | Metric::Exit.bit()
228        | Metric::Abc.bit()
229        | Metric::Npm.bit()
230        | Metric::Npa.bit()
231        | Metric::Mi.bit()
232        | Metric::Wmc.bit();
233
234    /// Empty set (no metrics selected).
235    #[inline]
236    #[must_use]
237    pub const fn empty() -> Self {
238        Self(0)
239    }
240
241    /// Full set (every metric selected). This is the default for
242    /// [`MetricsOptions`](crate::MetricsOptions), preserving the
243    /// pre-#257 "compute everything" behaviour.
244    #[inline]
245    #[must_use]
246    pub const fn all() -> Self {
247        Self(Self::ALL_BITS)
248    }
249
250    /// Returns `true` if `metric` is in the set.
251    #[inline]
252    #[must_use]
253    pub const fn contains(self, metric: Metric) -> bool {
254        (self.0 & metric.bit()) != 0
255    }
256
257    /// Returns a new set with `metric` inserted.
258    #[inline]
259    #[must_use]
260    pub const fn with(self, metric: Metric) -> Self {
261        Self(self.0 | metric.bit())
262    }
263
264    /// Returns the union of two sets.
265    #[inline]
266    #[must_use]
267    pub const fn union(self, other: Self) -> Self {
268        Self(self.0 | other.0)
269    }
270
271    /// Insert `metric` (in place).
272    #[inline]
273    pub fn insert(&mut self, metric: Metric) {
274        self.0 |= metric.bit();
275    }
276
277    /// Build a `MetricSet` from a slice, auto-adding the transitive
278    /// dependencies of each selected metric.
279    ///
280    /// This is the workhorse behind
281    /// [`MetricsOptions::with_only`](crate::MetricsOptions::with_only):
282    /// the caller-facing builder enforces the full dependency closure
283    /// so a request for `Mi` alone still computes
284    /// `Loc + Cyclomatic + Halstead`. Exposed `pub` because
285    /// downstream consumers (notably the `bca` Python bindings'
286    /// `parse_metric_names` helper) parse user input into a
287    /// `Vec<Metric>` and need the same closure-resolution semantics
288    /// without re-implementing the worklist.
289    ///
290    /// Implementation note: uses a worklist rather than a single pass
291    /// so a future derived metric whose dependency is itself derived
292    /// still resolves the complete closure. The loop terminates
293    /// because each iteration either inserts a new bit or the
294    /// worklist drains; the bitfield is bounded at `Metric` variant
295    /// count.
296    #[must_use]
297    pub fn from_slice_with_deps(metrics: &[Metric]) -> Self {
298        let mut set = Self::empty();
299        let mut worklist: Vec<Metric> = metrics.to_vec();
300        while let Some(m) = worklist.pop() {
301            if set.contains(m) {
302                continue;
303            }
304            set.insert(m);
305            for &dep in m.dependencies() {
306                if !set.contains(dep) {
307                    worklist.push(dep);
308                }
309            }
310        }
311        set
312    }
313}
314
315impl Default for MetricSet {
316    /// Default = every metric selected, matching the pre-#257
317    /// behaviour of [`MetricsOptions::default`](crate::MetricsOptions::default).
318    #[inline]
319    fn default() -> Self {
320        Self::all()
321    }
322}
323
324#[cfg(test)]
325#[path = "metric_set_tests.rs"]
326mod tests;