Skip to main content

fallow_engine/health/
inline.rs

1//! Complexity signals for editors, from the same threshold rule as
2//! `fallow health`.
3
4use std::path::PathBuf;
5
6use fallow_config::{ResolvedConfig, Severity};
7use fallow_types::discover::DiscoveredFile;
8
9use super::threshold_overrides::{GlobalHealthThresholds, ThresholdOverrideResolver};
10use crate::source::ModuleInfo;
11
12/// One function above a complexity threshold, for an editor code lens.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct InlineComplexity {
15    /// Absolute path of the file that declares the function.
16    pub path: PathBuf,
17    /// Function name as extracted from the source.
18    pub name: String,
19    /// One-based line of the function declaration.
20    pub line: u32,
21    /// Zero-based column of the function declaration.
22    pub col: u32,
23    /// Cyclomatic complexity as the gate reads it under
24    /// `health.cyclomaticPolicy`.
25    pub cyclomatic: u16,
26    /// Measured cognitive complexity.
27    pub cognitive: u16,
28    /// The function is above the effective cyclomatic threshold.
29    pub exceeds_cyclomatic: bool,
30    /// The function is above the effective cognitive threshold.
31    pub exceeds_cognitive: bool,
32}
33
34/// The functions of the parsed modules that are above a complexity threshold.
35///
36/// This applies the rules of the `fallow health` findings: `health.ignore`,
37/// the `complexity` suppression comments, the module-scope unit that never
38/// becomes a finding, and the effective thresholds of `health.thresholdOverrides`
39/// per file and function. It also applies the `complexity-cyclomatic` and
40/// `complexity-cognitive` rules with `overrides[].rules` for the file: a
41/// function whose cyclomatic and cognitive kinds are all `off` is dropped.
42///
43/// A lens covers only the cyclomatic and cognitive kinds. It has no CRAP
44/// score, so `complexity-crap` has no effect here. A function that the health
45/// report flags only through CRAP, or whose cyclomatic and cognitive rules are
46/// `off` while its CRAP rule is on, has a health finding but no lens.
47#[must_use]
48pub fn inline_complexity(
49    config: &ResolvedConfig,
50    modules: &[ModuleInfo],
51    files: &[DiscoveredFile],
52) -> Vec<InlineComplexity> {
53    let file_paths: rustc_hash::FxHashMap<_, _> =
54        files.iter().map(|file| (file.id, &file.path)).collect();
55    let ignore_set = super::ignore::build_ignore_set(&config.health.ignore);
56    let resolver = ThresholdOverrideResolver::new(
57        &config.health.threshold_overrides,
58        GlobalHealthThresholds {
59            cyclomatic: config.health.max_cyclomatic,
60            cognitive: config.health.max_cognitive,
61            crap: config.health.max_crap,
62            unit_size: config.health.max_unit_size,
63        },
64    )
65    .with_cyclomatic_policy(config.health.cyclomatic_policy);
66    let mut findings = Vec::new();
67
68    for module in modules {
69        let Some(path) = file_paths.get(&module.file_id) else {
70            continue;
71        };
72        let relative = path.strip_prefix(&config.root).unwrap_or(path);
73        if ignore_set.is_match(relative) {
74            continue;
75        }
76        // The rules of this file, resolved one time for all of its functions.
77        let path_rules =
78            (!config.overrides.is_empty()).then(|| config.resolve_rules_for_path(path));
79        let rules = path_rules.as_ref().unwrap_or(&config.rules);
80        for function in &module.complexity {
81            if fallow_types::extract::is_synthetic_module_unit(&function.name)
82                || crate::suppress::is_suppressed(
83                    &module.suppressions,
84                    function.line,
85                    crate::suppress::IssueKind::Complexity,
86                )
87            {
88                continue;
89            }
90            let (applied, _) = resolver.resolve(relative, &function.name);
91            let Some((exceeds_cyclomatic, exceeds_cognitive)) =
92                super::findings::complexity_exceeded(
93                    function,
94                    resolver.cyclomatic_policy(),
95                    applied.effective,
96                )
97            else {
98                continue;
99            };
100            // A lens has no CRAP score, so only the two kinds it shows count.
101            if rules.complexity_severity(exceeds_cyclomatic, exceeds_cognitive, false)
102                == Severity::Off
103            {
104                continue;
105            }
106            findings.push(InlineComplexity {
107                path: (*path).clone(),
108                name: function.name.clone(),
109                line: function.line,
110                col: function.col,
111                cyclomatic: resolver.gate_cyclomatic(function),
112                cognitive: function.cognitive,
113                exceeds_cyclomatic,
114                exceeds_cognitive,
115            });
116        }
117    }
118
119    findings
120}