Skip to main content

mollify_core/
complexity.rs

1//! Complexity engine. Flags functions whose cyclomatic or cognitive complexity
2//! exceeds a threshold. (Churn × complexity hotspot ranking — the unfilled FOSS
3//! Python niche — is planned via `git log --numstat`; PLAN.md §3.5.)
4
5use crate::fingerprint::fingerprint;
6use mollify_graph::ModuleGraph;
7use mollify_types::{Action, Category, Confidence, Finding, Location, Severity};
8
9/// Default thresholds (tunable via config later).
10pub const DEFAULT_CYCLOMATIC: u32 = 10;
11pub const DEFAULT_COGNITIVE: u32 = 15;
12
13pub fn analyze(graph: &ModuleGraph) -> Vec<Finding> {
14    analyze_with(graph, DEFAULT_CYCLOMATIC, DEFAULT_COGNITIVE)
15}
16
17pub fn analyze_with(graph: &ModuleGraph, max_cyclo: u32, max_cog: u32) -> Vec<Finding> {
18    let mut findings = Vec::new();
19    for m in &graph.modules {
20        for f in &m.parsed.functions {
21            let over_cyclo = f.cyclomatic > max_cyclo;
22            let over_cog = f.cognitive > max_cog;
23            if !over_cyclo && !over_cog {
24                continue;
25            }
26            let rule = "high-complexity";
27            let reason = format!(
28                "function `{}` is complex (cyclomatic {}, cognitive {}); thresholds {}/{}",
29                f.name, f.cyclomatic, f.cognitive, max_cyclo, max_cog
30            );
31            findings.push(Finding {
32                fingerprint: fingerprint(rule, &[m.path.as_str(), &f.name]),
33                rule: rule.into(),
34                category: Category::Complexity,
35                severity: Severity::Warn,
36                // The metric is exact; the *judgement* of "too complex" is the
37                // user's threshold, but the measurement is certain.
38                confidence: Confidence::Certain,
39                attribution: None,
40                reason,
41                location: Location {
42                    path: m.path.clone(),
43                    line: f.line,
44                    column: 0,
45                    end_line: None,
46                },
47                actions: vec![Action {
48                    kind: "refactor".into(),
49                    description: format!(
50                        "Refactor `{}` to reduce complexity (extract helpers, flatten nesting)",
51                        f.name
52                    ),
53                    auto_fixable: false,
54                    suppression_comment: Some("# mollify: ignore[high-complexity]".into()),
55                }],
56            });
57        }
58    }
59    findings
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use camino::{Utf8Path, Utf8PathBuf};
66    use mollify_graph::discover_python_files;
67
68    fn temp(tag: &str) -> Utf8PathBuf {
69        let base =
70            std::env::temp_dir().join(format!("mollify-core-cx-{}-{tag}", std::process::id()));
71        let _ = std::fs::remove_dir_all(&base);
72        Utf8PathBuf::from_path_buf(base).unwrap()
73    }
74    fn write(dir: &Utf8Path, rel: &str, src: &str) {
75        let p = dir.join(rel);
76        std::fs::create_dir_all(p.parent().unwrap()).unwrap();
77        std::fs::write(p, src).unwrap();
78    }
79
80    #[test]
81    fn flags_complex_function() {
82        let d = temp("cx");
83        // A deliberately branchy function.
84        let mut body = String::from("def big(x):\n");
85        for i in 0..12 {
86            body.push_str(&format!("    if x == {i} and x:\n        x += {i}\n"));
87        }
88        body.push_str("    return x\n");
89        write(&d, "__init__.py", &body);
90        let files = discover_python_files(&d);
91        let g = ModuleGraph::build(&d, &files);
92        let f = analyze(&g);
93        assert!(
94            f.iter()
95                .any(|x| x.rule == "high-complexity" && x.reason.contains("big")),
96            "got {f:?}"
97        );
98        std::fs::remove_dir_all(&d).ok();
99    }
100
101    #[test]
102    fn ignores_simple_function() {
103        let d = temp("simple");
104        write(&d, "__init__.py", "def small(x):\n    return x + 1\n");
105        let files = discover_python_files(&d);
106        let g = ModuleGraph::build(&d, &files);
107        assert!(analyze(&g).is_empty());
108        std::fs::remove_dir_all(&d).ok();
109    }
110}