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    /// Measured cyclomatic complexity.
24    pub cyclomatic: u16,
25    /// Measured cognitive complexity.
26    pub cognitive: u16,
27    /// The function is above the effective cyclomatic threshold.
28    pub exceeds_cyclomatic: bool,
29    /// The function is above the effective cognitive threshold.
30    pub exceeds_cognitive: bool,
31}
32
33/// The functions of the parsed modules that are above a complexity threshold.
34///
35/// This applies the rules of the `fallow health` findings: `health.ignore`,
36/// the `complexity` suppression comments, the module-scope unit that never
37/// becomes a finding, and the effective thresholds of `health.thresholdOverrides`
38/// per file and function. It also applies the `complexity-cyclomatic` and
39/// `complexity-cognitive` rules with `overrides[].rules` for the file: a
40/// function whose cyclomatic and cognitive kinds are all `off` is dropped.
41///
42/// A lens covers only the cyclomatic and cognitive kinds. It has no CRAP
43/// score, so `complexity-crap` has no effect here. A function that the health
44/// report flags only through CRAP, or whose cyclomatic and cognitive rules are
45/// `off` while its CRAP rule is on, has a health finding but no lens.
46#[must_use]
47pub fn inline_complexity(
48    config: &ResolvedConfig,
49    modules: &[ModuleInfo],
50    files: &[DiscoveredFile],
51) -> Vec<InlineComplexity> {
52    let file_paths: rustc_hash::FxHashMap<_, _> =
53        files.iter().map(|file| (file.id, &file.path)).collect();
54    let ignore_set = super::ignore::build_ignore_set(&config.health.ignore);
55    let resolver = ThresholdOverrideResolver::new(
56        &config.health.threshold_overrides,
57        GlobalHealthThresholds {
58            cyclomatic: config.health.max_cyclomatic,
59            cognitive: config.health.max_cognitive,
60            crap: config.health.max_crap,
61            unit_size: config.health.max_unit_size,
62        },
63    );
64    let mut findings = Vec::new();
65
66    for module in modules {
67        let Some(path) = file_paths.get(&module.file_id) else {
68            continue;
69        };
70        let relative = path.strip_prefix(&config.root).unwrap_or(path);
71        if ignore_set.is_match(relative) {
72            continue;
73        }
74        // The rules of this file, resolved one time for all of its functions.
75        let path_rules =
76            (!config.overrides.is_empty()).then(|| config.resolve_rules_for_path(path));
77        let rules = path_rules.as_ref().unwrap_or(&config.rules);
78        for function in &module.complexity {
79            if fallow_types::extract::is_synthetic_module_unit(&function.name)
80                || crate::suppress::is_suppressed(
81                    &module.suppressions,
82                    function.line,
83                    crate::suppress::IssueKind::Complexity,
84                )
85            {
86                continue;
87            }
88            let (applied, _) = resolver.resolve(relative, &function.name);
89            let Some((exceeds_cyclomatic, exceeds_cognitive)) =
90                super::findings::complexity_exceeded(function, applied.effective)
91            else {
92                continue;
93            };
94            // A lens has no CRAP score, so only the two kinds it shows count.
95            if rules.complexity_severity(exceeds_cyclomatic, exceeds_cognitive, false)
96                == Severity::Off
97            {
98                continue;
99            }
100            findings.push(InlineComplexity {
101                path: (*path).clone(),
102                name: function.name.clone(),
103                line: function.line,
104                col: function.col,
105                cyclomatic: function.cyclomatic,
106                cognitive: function.cognitive,
107                exceeds_cyclomatic,
108                exceeds_cognitive,
109            });
110        }
111    }
112
113    findings
114}