Skip to main content

big_code_analysis/
spaces.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8// Metric counts (token, function, branch, argument, etc.) are stored as
9// `usize` and crossed with `f64` averages, ratios, and Halstead scores
10// across the cyclomatic / MI / Halstead computations. The `usize as f64`
11// and `f64 as usize` casts are intentional and snapshot-anchored — every
12// site is bounded by the count it came from. Allowing the lints at the
13// module level keeps the metric arithmetic legible.
14#![allow(
15    clippy::cast_precision_loss,
16    clippy::cast_possible_truncation,
17    clippy::cast_sign_loss
18)]
19
20use std::borrow::Cow;
21use std::collections::HashMap;
22
23use serde::{Deserialize, Serialize};
24use std::fmt;
25use std::path::Path;
26use std::sync::Arc;
27
28use crate::langs::LANG;
29use crate::metric_set::{Metric, MetricSet};
30use crate::preproc::PreprocResults;
31
32use crate::checker::Checker;
33use crate::error::MetricsError;
34use crate::node::{Cursor, Node};
35use crate::suppression::{
36    Suppression, SuppressionKind, SuppressionScope, parse_marker as parse_suppression_marker,
37};
38
39use crate::abc::{self, Abc};
40use crate::cognitive::{self, Cognitive};
41use crate::cyclomatic::{self, Cyclomatic};
42use crate::getter::Getter;
43use crate::halstead::{self, Halstead, HalsteadMaps};
44use crate::loc::{self, Loc};
45use crate::mi::{self, Mi};
46use crate::nargs::{self, NArgs};
47use crate::nexits::{self, Exit};
48use crate::nom::{self, Nom};
49use crate::npa::{self, Npa};
50use crate::npm::{self, Npm};
51use crate::tokens::{self, Tokens};
52use crate::wmc::{self, Wmc};
53
54use crate::traits::*;
55
56mod compute;
57
58// Inherent / trait impl blocks for the public types defined below live
59// in per-type child modules to keep `spaces.rs` under its size budget.
60// Method and trait resolution is by type, not module path, so every
61// public path (`crate::spaces::Ast::parse`, etc.) is preserved.
62mod ast;
63mod code_metrics;
64mod options;
65mod source;
66mod space_kind;
67
68// `analyze` is `pub` — re-exported from `lib.rs`, so it must stay
69// reachable at `crate::spaces::analyze`.
70pub use compute::analyze;
71// `metrics_inner` and `push_children` are `pub(crate)` — `metrics_inner`
72// is re-exported from `lib.rs` and `push_children` is consumed by
73// `crate::ops`, so both must stay reachable at their `crate::spaces::`
74// paths.
75pub(crate) use compute::{metrics_inner, push_children};
76// The inline `mod tests` drives `apply_suppression` via
77// `super::apply_suppression`; re-import the name into this module
78// (test-only, so it does not warn as unused in production builds) to
79// keep that path resolving after the move into `compute`.
80#[cfg(test)]
81use compute::apply_suppression;
82
83/// The list of supported space kinds.
84// New space kinds land as languages are added (a future module-, mixin-,
85// or enum-style space), so this is marked `#[non_exhaustive]` to keep
86// such additions additive rather than a 2.0 break. CLI/web consumers
87// matching on it already carry a `_ =>` arm.
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90#[non_exhaustive]
91pub enum SpaceKind {
92    /// An unknown space
93    #[default]
94    Unknown,
95    /// A function space
96    Function,
97    /// A class space
98    Class,
99    /// A struct space
100    Struct,
101    /// A `Rust` trait space
102    Trait,
103    /// A `Rust` implementation space
104    Impl,
105    /// A general space
106    Unit,
107    /// A `C/C++` namespace
108    Namespace,
109    /// An interface
110    Interface,
111}
112
113/// All metrics data.
114///
115/// The set of metrics actually computed is governed by
116/// [`MetricsOptions::with_only`]. By default every metric is
117/// populated; when `with_only` restricts the set, unselected fields
118/// remain at their `Default` value and are elided from
119/// `Serialize` output. The `selected` mask is the source of truth
120/// for which fields are populated — read it via
121/// [`CodeMetrics::selected`].
122#[derive(Default, Debug, Clone, PartialEq)]
123pub struct CodeMetrics {
124    /// `NArgs` data
125    pub nargs: nargs::Stats,
126    /// `NExits` data
127    pub nexits: nexits::Stats,
128    /// `Cognitive` data
129    pub cognitive: cognitive::Stats,
130    /// `Cyclomatic` data
131    pub cyclomatic: cyclomatic::Stats,
132    /// `Halstead` data
133    pub halstead: halstead::Stats,
134    /// `Loc` data
135    pub loc: loc::Stats,
136    /// `Nom` data
137    pub nom: nom::Stats,
138    /// `Tokens` data
139    pub tokens: tokens::Stats,
140    /// `Mi` data
141    pub mi: mi::Stats,
142    /// `Abc` data
143    pub abc: abc::Stats,
144    /// `Wmc` data
145    pub wmc: wmc::Stats,
146    /// `Npm` data
147    pub npm: npm::Stats,
148    /// `Npa` data
149    pub npa: npa::Stats,
150    /// Change-history (VCS) data for this space.
151    ///
152    /// Unlike every other field, this is *not* AST-derived and *not*
153    /// computed during the analysis walk: it is a signal set injected by
154    /// the caller after [`analyze`]. The top-level (file-level)
155    /// [`FuncSpace`] carries the per-file block projected from a
156    /// [`crate::vcs::HistoryIndex`]; nested function / method / class
157    /// spaces carry a per-function block derived from `git blame` only
158    /// when the caller opts into per-function attribution
159    /// ([`crate::vcs::PerFunctionBlame`], issue #329), and stay `None`
160    /// otherwise. Note the two levels use **different** computations:
161    /// the file block is windowed added+deleted churn, the per-function
162    /// block is current-blame surviving-line attribution, so their
163    /// `churn` figures are not comparable. `None` also distinguishes an
164    /// untracked file from a tracked one with zero in-window activity.
165    /// Gated behind the `vcs-git` backend feature.
166    #[cfg(feature = "vcs-git")]
167    pub vcs: Option<crate::vcs::Stats>,
168    /// Which metrics were actually computed for this space.
169    ///
170    /// Default is [`MetricSet::all`] — every metric was run, matching
171    /// the pre-#257 behaviour. After
172    /// [`MetricsOptions::with_only`] the bitfield is restricted to the
173    /// caller's selection plus auto-added dependencies.
174    ///
175    /// The [`Serialize`] impl consults this set to elide fields the
176    /// caller did not select. The field itself is not serialized.
177    pub selected: MetricSet,
178}
179
180/// Function space data.
181///
182/// `Serialize` is provided in [`crate::wire`] (it delegates to
183/// [`crate::wire::FuncSpace`], the single definition of the output shape);
184/// read the wire form back with `serde` via that module.
185#[derive(Debug, Clone, PartialEq)]
186pub struct FuncSpace {
187    /// The name of a function space.
188    ///
189    /// For the top-level (file-level) `FuncSpace`, this is the value
190    /// supplied via `Source::name` to [`analyze`] — typically a file
191    /// path or other display identifier chosen by the caller. The
192    /// library no longer derives this from a `&Path` or applies lossy
193    /// UTF-8 conversion; callers are expected to pass an
194    /// already-stringified identifier (or `None` if they have no
195    /// meaningful name to attach).
196    ///
197    /// For nested spaces, `None` means an error occurred in parsing the
198    /// name of the function space from the AST.
199    pub name: Option<String>,
200    /// The first line of a function space
201    pub start_line: usize,
202    /// The last line of a function space
203    pub end_line: usize,
204    /// The space kind
205    pub kind: SpaceKind,
206    /// All subspaces contained in a function space
207    pub spaces: Vec<FuncSpace>,
208    /// All metrics of a function space
209    pub metrics: CodeMetrics,
210    /// In-source suppression markers that apply to this space.
211    ///
212    /// Populated during the spaces pass from comment-embedded
213    /// directives. Each marker carries a [`SuppressionScope`] naming
214    /// the metrics it silences. The top-level (file-level) `FuncSpace`
215    /// aggregates every file-scoped marker; nested function spaces
216    /// aggregate every function-scoped marker whose comment lies
217    /// inside their source range. Metric computation itself is
218    /// unaffected — this field is consumed by downstream
219    /// *threshold-check* code (e.g. `bca check`), which consults a
220    /// [`crate::SuppressionPolicy`] to decide whether to honour the
221    /// markers or surface every violation regardless.
222    ///
223    /// Defaults to `SuppressionScope::default()` (an empty `Some`), so
224    /// pre-existing code paths that do not honor suppressions see no
225    /// behaviour change. The field is elided from JSON output when
226    /// empty so the existing schema is unchanged for files without
227    /// markers.
228    pub suppressed: SuppressionScope,
229}
230
231impl FuncSpace {
232    /// Project this space into its [`crate::wire::FuncSpace`] form — the
233    /// plain, `Deserialize`-capable record that defines the serialized
234    /// shape. Serializing a `FuncSpace` produces exactly the same bytes as
235    /// serializing `self.to_wire()`.
236    #[must_use]
237    pub fn to_wire(&self) -> crate::wire::FuncSpace {
238        crate::wire::FuncSpace::from(self)
239    }
240
241    fn new<T: Getter>(node: &Node, code: &[u8], kind: SpaceKind, selected: MetricSet) -> Self {
242        let (start_position, end_position) = match kind {
243            SpaceKind::Unit => {
244                if node.child_count() == 0 {
245                    (0, 0)
246                } else {
247                    (node.start_row() + 1, node.end_row())
248                }
249            }
250            _ => (node.start_row() + 1, node.end_row() + 1),
251        };
252
253        // The top-level Unit's name is overwritten by `metrics_inner`
254        // with the caller-supplied name before returning, so computing
255        // it here is wasted work. Other kinds keep the AST-derived name.
256        let name = (kind != SpaceKind::Unit)
257            .then(|| {
258                T::get_func_space_name(node, code)
259                    .map(|name| name.split_whitespace().collect::<Vec<_>>().join(" "))
260            })
261            .flatten();
262
263        let mut metrics = CodeMetrics::with_selected(selected);
264        // Seed the cyclomatic per-function divisor: each function/closure
265        // space contributes 1 to `function_spaces`, which `Stats::merge`
266        // then sums across the subtree. Sourced here from the space kind
267        // rather than from the `Nom` metric so the cyclomatic averages
268        // stay correct even when `Nom` is not selected (#512).
269        if kind == SpaceKind::Function {
270            metrics.cyclomatic.note_function_space();
271        }
272
273        Self {
274            name,
275            spaces: Vec::new(),
276            metrics,
277            kind,
278            start_line: start_position,
279            end_line: end_position,
280            suppressed: SuppressionScope::default(),
281        }
282    }
283}
284
285#[derive(Debug, Clone)]
286struct State<'a> {
287    space: FuncSpace,
288    halstead_maps: HalsteadMaps<'a>,
289}
290
291/// In-memory source bundle handed to [`analyze`].
292///
293/// `Source` decouples the *display name* of the top-level
294/// [`FuncSpace`] (`Source::name`) from the optional *filesystem path*
295/// used by the C++ preprocessor lookup (`Source::preproc_path`). For
296/// in-memory snippets, code fetched over the network, or test
297/// fixtures, callers pass `Source` directly without manufacturing a
298/// `Path`.
299///
300/// Marked `#[non_exhaustive]` so future input fields can land
301/// additively. Downstream callers must construct via
302/// [`Source::new`] plus the `with_*` builder setters rather than
303/// struct-literal syntax (rustc rejects external struct literals on
304/// non-exhaustive types with E0639).
305///
306/// # Examples
307///
308/// Analysing an in-memory snippet with no on-disk path:
309///
310/// ```
311/// use big_code_analysis::{analyze, MetricsOptions, Source, LANG};
312///
313/// let source = Source::new(LANG::Rust, b"fn main() {}")
314///     .with_name(Some("snippet.rs".to_owned()));
315/// let space = analyze(source, MetricsOptions::default()).unwrap();
316/// assert_eq!(space.name.as_deref(), Some("snippet.rs"));
317/// ```
318#[non_exhaustive]
319#[derive(Debug, Clone)]
320pub struct Source<'a> {
321    /// The source language used to select the parser.
322    pub(crate) lang: LANG,
323    /// Raw source bytes, borrowed ([`Source::new`]) or owned
324    /// ([`Source::from_bytes`]). The parser needs an owned buffer:
325    /// borrowed bytes are copied at parse time, owned bytes move
326    /// through without a copy (the CLI walk's hot path).
327    pub(crate) code: Cow<'a, [u8]>,
328    /// Display / identifier name for the top-level [`FuncSpace`].
329    /// If `None`, the top-level [`FuncSpace::name`] is left `None`.
330    pub(crate) name: Option<String>,
331    /// Optional path used only by the C++ preprocessor lookup
332    /// (`get_fake_code`) to resolve macro definitions in
333    /// [`PreprocResults`]. For non-C++ languages this is ignored.
334    /// Defaults to `None`.
335    pub(crate) preproc_path: Option<&'a Path>,
336    /// Preprocessor results paired with `Source::preproc_path`.
337    /// Same shape as the `pr` arg on the deprecated entry points.
338    pub(crate) preproc: Option<Arc<PreprocResults>>,
339}
340
341/// Parse-once, compute-many handle.
342///
343/// Owns the parsed [`tree_sitter::Tree`] and the source bytes it was parsed
344/// from, so callers can run [`Ast::metrics`] repeatedly against the same
345/// parse — with different [`MetricsOptions`] subsets, interleaved with
346/// custom `tree_sitter` traversal via [`Ast::as_tree_sitter`], or cached
347/// across configuration changes in an analysis pipeline.
348///
349/// Build one via [`Ast::parse`] (the seam behind [`analyze`]) or
350/// [`Ast::from_tree_sitter`] to reuse a caller-supplied
351/// [`tree_sitter::Tree`], carrying an explicit display name.
352///
353/// `Ast` is a snapshot — it does not pick up changes to the source after
354/// construction. Incremental reparse via [`tree_sitter::InputEdit`] is out
355/// of scope for this seam.
356///
357/// # C++ preprocessor
358///
359/// When [`Ast::parse`] is given a [`Source`] carrying preprocessor inputs
360/// and the language is [`LANG::Cpp`], [`Ast::source`] returns the *expanded*
361/// bytes the parser actually saw (the macro pre-pass runs before
362/// `tree-sitter` does). [`Ast::from_tree_sitter`] adopts whatever tree the
363/// caller supplied; whatever expansion they applied before building it is
364/// what [`Ast::source`] reflects.
365///
366/// # Examples
367///
368/// Parse once, run two disjoint metric subsets without re-parsing:
369///
370/// ```
371/// use big_code_analysis::{Ast, LANG, Metric, MetricsOptions, Source};
372///
373/// let ast = Ast::parse(
374///     Source::new(LANG::Rust, b"fn f() { if true { 1 } else { 2 }; }"),
375/// )
376/// .expect("rust feature enabled");
377///
378/// let loc = ast
379///     .metrics(MetricsOptions::default().with_only(&[Metric::Loc]))
380///     .expect("walker succeeds");
381/// let cyc = ast
382///     .metrics(MetricsOptions::default().with_only(&[Metric::Cyclomatic]))
383///     .expect("walker succeeds");
384/// // Each call's `with_only` filters to its requested family — the other
385/// // metric stays at its `Default` (zero) value, confirming options are
386/// // honored per call rather than carried over.
387/// assert!(loc.metrics.loc.ploc() > 0);
388/// assert_eq!(loc.metrics.cyclomatic.cyclomatic_sum(), 0);
389/// assert!(cyc.metrics.cyclomatic.cyclomatic_sum() > 0);
390/// assert_eq!(cyc.metrics.loc.ploc(), 0);
391/// ```
392///
393/// Walk the underlying `tree_sitter::Tree` and then run metrics on the
394/// same parse:
395///
396/// ```
397/// use big_code_analysis::{Ast, LANG, MetricsOptions, Source};
398///
399/// let ast = Ast::parse(Source::new(LANG::Rust, b"fn f() {}"))
400///     .expect("rust feature enabled");
401/// let root = ast.as_tree_sitter().root_node();
402/// assert_eq!(root.kind(), "source_file");
403/// let _ = ast.metrics(MetricsOptions::default()).expect("walker succeeds");
404/// ```
405pub struct Ast {
406    inner: crate::langs::AstInner,
407    name: Option<String>,
408}
409
410// `impl fmt::Debug for Ast` and `impl Ast` live in `spaces/ast.rs`.
411
412/// Per-traversal options for [`analyze`] / [`Ast::metrics`].
413///
414/// Marked `#[non_exhaustive]` so future option fields can land
415/// additively. Downstream callers must construct via the builder
416/// methods rather than struct-literal syntax (rustc rejects external
417/// struct literals on non-exhaustive types with E0639, including the
418/// `..Default::default()` spread form). The defaults preserve every
419/// metric value emitted by the pre-#182 [`analyze`] entry point.
420///
421/// ```
422/// use big_code_analysis::MetricsOptions;
423/// let opts = MetricsOptions::default().with_exclude_tests(true);
424/// ```
425#[derive(Clone, Copy, Debug, PartialEq, Eq)]
426#[non_exhaustive]
427pub struct MetricsOptions {
428    /// When true, the traversal asks the language module to skip
429    /// test-only subtrees (e.g. Rust `#[test]` / `#[cfg(test)]`
430    /// functions and modules). Only languages that override the
431    /// internal `should_skip_subtree` hook honor this; others ignore
432    /// the flag.
433    pub(crate) exclude_tests: bool,
434    /// Which metrics to compute. Defaults to [`MetricSet::all`] —
435    /// every metric is enabled, matching the pre-#257 behaviour.
436    /// Restrict via [`MetricsOptions::with_only`].
437    pub(crate) metrics: MetricSet,
438    /// When true (the default), Rust's `?` operator (the
439    /// `try_expression` grammar node) contributes `+1` to both
440    /// standard and modified cyclomatic complexity, matching upstream
441    /// rust-code-analysis. Set to `false` (via
442    /// [`MetricsOptions::with_count_cyclomatic_try`]) to treat `?` as
443    /// linear error propagation rather than a branch — useful when
444    /// cyclomatic is used as a maintainability gate that should not
445    /// penalize fallible-but-linear code. Rust-only: no other
446    /// language emits `try_expression`, so the flag is inert
447    /// elsewhere. Defaulting to `true` keeps every published metric
448    /// value unchanged (#409).
449    pub(crate) count_cyclomatic_try: bool,
450}
451
452#[cfg(test)]
453// The lossy-path / synthetic-Unit tests below drive the internal
454// `metrics_inner` walk core directly (the `Ast`-seam-friendly
455// counterpart of the retired path-positional entry points) so they
456// keep regression coverage on the synthetic top-level Unit and the
457// lossy-name handling.
458#[allow(
459    clippy::float_cmp,
460    clippy::cast_precision_loss,
461    clippy::cast_possible_truncation,
462    clippy::cast_sign_loss,
463    clippy::similar_names,
464    clippy::doc_markdown,
465    clippy::needless_raw_string_hashes,
466    clippy::too_many_lines
467)]
468#[path = "spaces_tests.rs"]
469mod tests;