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///
23/// `Ord` follows declaration order, not the [`Display`](fmt::Display)
24/// spelling. It exists so [`Metric`] can key a `BTreeSet` — notably the
25/// suppression scope (`SuppressionScope::Some`) — with a deterministic,
26/// stable iteration order across runs. Do not rely on the ordering being
27/// alphabetical; reorder the variants only with a deliberate review of
28/// every serialized `BTreeSet<Metric>` snapshot.
29#[non_exhaustive]
30#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
31pub enum Metric {
32    /// Cognitive complexity ([`crate::cognitive::Stats`]).
33    Cognitive,
34    /// Cyclomatic complexity ([`crate::cyclomatic::Stats`]).
35    Cyclomatic,
36    /// Halstead ([`crate::halstead::Stats`]).
37    Halstead,
38    /// LoC family ([`crate::loc::Stats`]).
39    Loc,
40    /// Number of methods ([`crate::nom::Stats`]).
41    Nom,
42    /// Token counts ([`crate::tokens::Stats`]).
43    Tokens,
44    /// Number of arguments ([`crate::nargs::Stats`]).
45    Nargs,
46    /// Exit-point count ([`crate::nexits::Stats`]).
47    Nexits,
48    /// ABC ([`crate::abc::Stats`]).
49    Abc,
50    /// Number of public methods ([`crate::npm::Stats`]).
51    Npm,
52    /// Number of public attributes ([`crate::npa::Stats`]).
53    Npa,
54    /// Maintainability index ([`crate::mi::Stats`]). Derived metric:
55    /// selecting only `Mi` via
56    /// [`MetricsOptions::with_only`](crate::MetricsOptions::with_only)
57    /// also pulls in [`Metric::Loc`], [`Metric::Cyclomatic`], and
58    /// [`Metric::Halstead`].
59    Mi,
60    /// Weighted methods per class ([`crate::wmc::Stats`]). Derived
61    /// metric: selecting `Wmc` also pulls in [`Metric::Cyclomatic`]
62    /// and [`Metric::Nom`].
63    Wmc,
64}
65
66impl Metric {
67    // Bit position used inside [`MetricSet`]. The ordering is
68    // intentionally arbitrary — the only contract is that each
69    // variant maps to a distinct bit.
70    //
71    // Returns `u32` to match [`MetricSet`]'s storage width: at `u16`
72    // the bitfield would overflow once a 17th variant landed (debug
73    // panic / release wrap), and `Metric` is `#[non_exhaustive]`
74    // specifically so new variants can land additively.
75    #[inline]
76    const fn bit(self) -> u32 {
77        1 << (self as u32)
78    }
79
80    /// Returns the slice of metrics this metric depends on.
81    ///
82    /// Derived and averaged metrics consume the outputs of other
83    /// metrics during the finalize step; selecting one without its
84    /// dependencies would leave the dependency's `Stats` at default
85    /// (zero) values and silently corrupt the result. Callers
86    /// typically reach this through
87    /// [`MetricsOptions::with_only`](crate::MetricsOptions::with_only),
88    /// which auto-resolves the closure transparently.
89    #[must_use]
90    pub const fn dependencies(self) -> &'static [Metric] {
91        match self {
92            // Mi = function(Loc, Cyclomatic, Halstead). All three must
93            // be computed for the MI formula to be meaningful.
94            Self::Mi => &[Self::Loc, Self::Cyclomatic, Self::Halstead],
95            // Wmc aggregates per-method cyclomatic complexity and
96            // needs Nom to count those methods.
97            Self::Wmc => &[Self::Cyclomatic, Self::Nom],
98            // Cognitive, Nexits, and Nargs each expose a per-function
99            // average whose divisor is the function/closure count
100            // sourced from Nom (see `spaces::compute_averages`).
101            // Without Nom the divisor would be the `Stats` default
102            // (zero), producing inf/NaN averages (#428).
103            Self::Cognitive | Self::Nexits | Self::Nargs => &[Self::Nom],
104            _ => &[],
105        }
106    }
107
108    /// Canonical user-facing name for each metric — the single
109    /// source of truth shared by the Python bindings'
110    /// `bca.METRIC_NAMES` constant, the JSON output keys, and any
111    /// downstream Rust consumer that parses user input into a
112    /// [`MetricSet`]. A selection may also be spelled with a
113    /// `bca list-metrics` row name or a dotted id; see
114    /// [`crate::metric_catalog::resolve_selector`].
115    ///
116    /// Each entry round-trips through [`Metric::from_str`]. Every
117    /// metric uses one canonical spelling end-to-end:
118    /// [`Metric::Nexits`] is `"nexits"` in `Display`, in this table,
119    /// and as the JSON output key (the `CodeMetrics::Serialize` impl
120    /// in `src/spaces.rs`).
121    ///
122    /// Alphabetised. The drift between this table and the
123    /// `FromStr` arms (or the `Metric` enum itself) is guarded by
124    /// `names_table_parses_to_every_variant` and
125    /// `names_table_is_alphabetised` in the test module below.
126    pub const NAMES: &'static [&'static str] = &[
127        "abc",
128        "cognitive",
129        "cyclomatic",
130        "halstead",
131        "loc",
132        "mi",
133        "nargs",
134        "nexits",
135        "nom",
136        "npa",
137        "npm",
138        "tokens",
139        "wmc",
140    ];
141
142    /// Every metric except [`Metric::Tokens`], in declaration order.
143    ///
144    /// `tokens` is the one metric that cannot be named in a suppression
145    /// marker (`bca: suppress(tokens)` is rejected). It *is* a
146    /// configurable threshold — `bca check --threshold tokens=N` gates
147    /// on it — so the exclusion is about suppressibility alone. Reading
148    /// it as "no threshold either" is the trap #1113 had to work
149    /// around: deriving a metric selection from
150    /// [`crate::threshold_metric_for_name`], which returns `None` here,
151    /// silently leaves `tokens` uncomputed and disarms that gate.
152    ///
153    /// This is the single source
154    /// of truth for the suppressible vocabulary: the suppression
155    /// parser's "known metrics" hint and the threshold-name resolver
156    /// both derive from it rather than hardcoding the list.
157    pub fn suppressible() -> impl Iterator<Item = Metric> {
158        Self::ALL.iter().copied().filter(|m| *m != Self::Tokens)
159    }
160
161    /// Every [`Metric`] variant, in declaration order. Drives
162    /// [`Metric::suppressible`] and any consumer that needs to iterate
163    /// the full set without re-deriving it from `NAMES`.
164    ///
165    /// `pub(crate)` so in-crate tests that must cover *every* metric —
166    /// notably `metric_selection_parity` in `src/spaces_tests.rs` —
167    /// enumerate this list rather than a hand-copied one that a new
168    /// variant would silently escape.
169    pub(crate) const ALL: &'static [Self] = &[
170        Self::Cognitive,
171        Self::Cyclomatic,
172        Self::Halstead,
173        Self::Loc,
174        Self::Nom,
175        Self::Tokens,
176        Self::Nargs,
177        Self::Nexits,
178        Self::Abc,
179        Self::Npm,
180        Self::Npa,
181        Self::Mi,
182        Self::Wmc,
183    ];
184}
185
186impl fmt::Display for Metric {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        let s = match self {
189            Self::Cognitive => "cognitive",
190            Self::Cyclomatic => "cyclomatic",
191            Self::Halstead => "halstead",
192            Self::Loc => "loc",
193            Self::Nom => "nom",
194            Self::Tokens => "tokens",
195            Self::Nargs => "nargs",
196            Self::Nexits => "nexits",
197            Self::Abc => "abc",
198            Self::Npm => "npm",
199            Self::Npa => "npa",
200            Self::Mi => "mi",
201            Self::Wmc => "wmc",
202        };
203        f.write_str(s)
204    }
205}
206
207/// Error returned by [`Metric::from_str`] when the input
208/// is not a recognised metric name.
209///
210/// Holds the offending input verbatim. This type deliberately stays
211/// out of message policy and only carries the rejected input, so the
212/// wrapper layer can format the user-facing message however it wants.
213/// A front end parsing a user's metric *selection* wants
214/// [`crate::metric_catalog::resolve_selector`] instead, which takes
215/// the wider CLI vocabulary and whose error carries a did-you-mean
216/// hint.
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct ParseMetricError(String);
219
220impl ParseMetricError {
221    /// The rejected input that failed to parse as a [`Metric`] name.
222    ///
223    /// Lets callers recover the offending string programmatically
224    /// rather than scraping it out of the [`Display`](fmt::Display)
225    /// output.
226    #[must_use]
227    pub fn input(&self) -> &str {
228        &self.0
229    }
230}
231
232impl fmt::Display for ParseMetricError {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        write!(f, "unknown metric: {}", self.0)
235    }
236}
237
238impl std::error::Error for ParseMetricError {}
239
240impl FromStr for Metric {
241    type Err = ParseMetricError;
242
243    /// Parse a [`Metric`] from its [`fmt::Display`] spelling.
244    ///
245    /// Strict lowercase: `"Loc"` is rejected. Every metric has exactly
246    /// one accepted spelling, matching its `Display` form, `NAMES`
247    /// entry, and JSON output key.
248    fn from_str(s: &str) -> Result<Self, Self::Err> {
249        match s {
250            "cognitive" => Ok(Self::Cognitive),
251            "cyclomatic" => Ok(Self::Cyclomatic),
252            "halstead" => Ok(Self::Halstead),
253            "loc" => Ok(Self::Loc),
254            "nom" => Ok(Self::Nom),
255            "tokens" => Ok(Self::Tokens),
256            "nargs" => Ok(Self::Nargs),
257            "nexits" => Ok(Self::Nexits),
258            "abc" => Ok(Self::Abc),
259            "npm" => Ok(Self::Npm),
260            "npa" => Ok(Self::Npa),
261            "mi" => Ok(Self::Mi),
262            "wmc" => Ok(Self::Wmc),
263            _ => Err(ParseMetricError(s.to_owned())),
264        }
265    }
266}
267
268// Serialize/Deserialize are hand-written rather than derived so the wire
269// form is the canonical [`Display`] spelling (`nargs`, `nexits`,
270// `tokens`, …) — the same vocabulary used in JSON output keys, error
271// messages, and `Metric::NAMES`. A `#[derive(Serialize)]` with
272// `rename_all = "snake_case"` would emit `n_args` / `n_exits` instead,
273// diverging from every other surface. Routing through `Display`/`FromStr`
274// keeps the spelling single-sourced. `Metric` reaches the wire as the
275// element type of `SuppressionScope::Some`'s `BTreeSet`.
276impl serde::Serialize for Metric {
277    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
278        serializer.serialize_str(&self.to_string())
279    }
280}
281
282impl<'de> serde::Deserialize<'de> for Metric {
283    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
284        // `Cow<str>` borrows from self-describing, zero-copy formats
285        // (JSON without escapes) and owns from reader-based or
286        // non-borrowing ones (CBOR, YAML, TOML, JSON with escapes).
287        // `<&str>` would reject every non-borrowing format, which is
288        // exactly how `BTreeSet<Metric>` reaches the wire inside a
289        // `SuppressionScope::Some` CBOR/YAML/TOML payload.
290        let s = std::borrow::Cow::<str>::deserialize(deserializer)?;
291        s.parse().map_err(serde::de::Error::custom)
292    }
293}
294
295/// Bitfield of selected metrics.
296///
297/// Stored on [`MetricsOptions`](crate::MetricsOptions) (controls
298/// which metrics the walker computes) and on
299/// [`CodeMetrics`](crate::CodeMetrics) (controls which fields the
300/// `Serialize` impl emits).
301///
302/// `MetricSet::all()` is the default: every metric enabled, matching
303/// the pre-#257 behaviour.
304#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
305pub struct MetricSet(u32);
306
307impl MetricSet {
308    // All-metrics mask: OR together every variant's bit. Kept
309    // explicit (rather than `(1 << N) - 1`) so adding a new variant
310    // requires a deliberate edit here and surfaces in code review.
311    const ALL_BITS: u32 = Metric::Cognitive.bit()
312        | Metric::Cyclomatic.bit()
313        | Metric::Halstead.bit()
314        | Metric::Loc.bit()
315        | Metric::Nom.bit()
316        | Metric::Tokens.bit()
317        | Metric::Nargs.bit()
318        | Metric::Nexits.bit()
319        | Metric::Abc.bit()
320        | Metric::Npm.bit()
321        | Metric::Npa.bit()
322        | Metric::Mi.bit()
323        | Metric::Wmc.bit();
324
325    /// Empty set (no metrics selected).
326    #[inline]
327    #[must_use]
328    pub const fn empty() -> Self {
329        Self(0)
330    }
331
332    /// Full set (every metric selected). This is the default for
333    /// [`MetricsOptions`](crate::MetricsOptions), preserving the
334    /// pre-#257 "compute everything" behaviour.
335    #[inline]
336    #[must_use]
337    pub const fn all() -> Self {
338        Self(Self::ALL_BITS)
339    }
340
341    /// Returns `true` if `metric` is in the set.
342    #[inline]
343    #[must_use]
344    pub const fn contains(self, metric: Metric) -> bool {
345        (self.0 & metric.bit()) != 0
346    }
347
348    /// Returns a new set with `metric` inserted.
349    #[inline]
350    #[must_use]
351    pub const fn with(self, metric: Metric) -> Self {
352        Self(self.0 | metric.bit())
353    }
354
355    /// Returns the union of two sets.
356    #[inline]
357    #[must_use]
358    pub const fn union(self, other: Self) -> Self {
359        Self(self.0 | other.0)
360    }
361
362    /// Insert `metric` (in place).
363    #[inline]
364    pub fn insert(&mut self, metric: Metric) {
365        self.0 |= metric.bit();
366    }
367
368    /// Build a `MetricSet` from a slice, auto-adding the transitive
369    /// dependencies of each selected metric.
370    ///
371    /// This is the workhorse behind
372    /// [`MetricsOptions::with_only`](crate::MetricsOptions::with_only):
373    /// the caller-facing builder enforces the full dependency closure
374    /// so a request for `Mi` alone still computes
375    /// `Loc + Cyclomatic + Halstead`. Exposed `pub` because
376    /// downstream consumers (notably the `bca` Python bindings'
377    /// `parse_metric_names` helper) parse user input into a
378    /// `Vec<Metric>` and need the same closure-resolution semantics
379    /// without re-implementing the worklist.
380    ///
381    /// Implementation note: uses a worklist rather than a single pass
382    /// so a future derived metric whose dependency is itself derived
383    /// still resolves the complete closure. The loop terminates
384    /// because each iteration either inserts a new bit or the
385    /// worklist drains; the bitfield is bounded at `Metric` variant
386    /// count.
387    #[must_use]
388    pub fn from_slice_with_deps(metrics: &[Metric]) -> Self {
389        let mut set = Self::empty();
390        for &m in metrics {
391            set.insert(m);
392        }
393        set.resolved()
394    }
395
396    /// Returns this set closed under [`Metric::dependencies`].
397    ///
398    /// Every selected metric's transitive dependencies are added so a
399    /// set carrying a derived metric (e.g. [`Metric::Mi`]) also carries
400    /// the inputs that metric's finalize step consumes
401    /// ([`Metric::Loc`], [`Metric::Cyclomatic`], [`Metric::Halstead`]).
402    /// Resolving an already-closed set is a no-op, so the operation is
403    /// idempotent: `set.resolved().resolved() == set.resolved()`.
404    ///
405    /// This is the set-in/set-out counterpart of
406    /// [`MetricSet::from_slice_with_deps`] and is what
407    /// [`MetricsOptions::with_metric_set`](crate::MetricsOptions::with_metric_set)
408    /// applies so a caller-supplied set can never select a derived
409    /// metric without its prerequisites (#743).
410    ///
411    /// Implementation note: uses a worklist rather than a single pass
412    /// so a future derived metric whose dependency is itself derived
413    /// still resolves the complete closure. The loop terminates
414    /// because each iteration either inserts a new bit or the worklist
415    /// drains; the bitfield is bounded at `Metric` variant count.
416    #[must_use]
417    pub fn resolved(self) -> Self {
418        let mut set = self;
419        let mut worklist: Vec<Metric> = Metric::ALL
420            .iter()
421            .copied()
422            .filter(|&m| self.contains(m))
423            .collect();
424        while let Some(m) = worklist.pop() {
425            for &dep in m.dependencies() {
426                if !set.contains(dep) {
427                    set.insert(dep);
428                    worklist.push(dep);
429                }
430            }
431        }
432        set
433    }
434}
435
436impl Default for MetricSet {
437    /// Default = every metric selected, matching the pre-#257
438    /// behaviour of [`MetricsOptions::default`](crate::MetricsOptions::default).
439    #[inline]
440    fn default() -> Self {
441        Self::all()
442    }
443}
444
445#[cfg(test)]
446#[path = "metric_set_tests.rs"]
447mod tests;