Skip to main content

mollify_core/
coverage.rs

1//! Runtime-coverage merge — the "cold path" signal. Cross-references the static
2//! function map against a `coverage.py` JSON report (`coverage json`): a function
3//! that is statically reachable but has **zero executed lines** is a strong
4//! delete/triage candidate. This is fallow's paid differentiator, here free
5//! (RESEARCH.md §6) — Python makes it cheap (PEP 669 / SlipCover).
6
7use crate::fingerprint::fingerprint;
8use camino::Utf8Path;
9use mollify_graph::ModuleGraph;
10use mollify_types::{Action, Category, Confidence, Finding, Location, Severity};
11use rustc_hash::{FxHashMap, FxHashSet};
12
13/// Analyze cold code given a `coverage.py` JSON report at `coverage_path`.
14pub fn analyze(root: &Utf8Path, graph: &ModuleGraph, coverage_path: &Utf8Path) -> Vec<Finding> {
15    let Ok(text) = std::fs::read_to_string(coverage_path) else {
16        return Vec::new();
17    };
18    let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) else {
19        return Vec::new();
20    };
21    let Some(files) = json.get("files").and_then(|f| f.as_object()) else {
22        return Vec::new();
23    };
24
25    // Map coverage entries by both full key and trailing file name.
26    let mut by_key: FxHashMap<String, FxHashSet<u32>> = FxHashMap::default();
27    for (key, val) in files {
28        let mut set = FxHashSet::default();
29        if let Some(lines) = val.get("executed_lines").and_then(|l| l.as_array()) {
30            for l in lines {
31                if let Some(n) = l.as_u64() {
32                    set.insert(n as u32);
33                }
34            }
35        }
36        by_key.insert(key.clone(), set);
37    }
38
39    let mut findings = Vec::new();
40    for m in &graph.modules {
41        let executed = match_coverage(root, &m.path, &by_key);
42        let Some(executed) = executed else {
43            continue; // no coverage data for this file → no claim
44        };
45        for f in &m.parsed.functions {
46            let ran = (f.line..=f.end_line).any(|ln| executed.contains(&ln));
47            if ran {
48                continue;
49            }
50            let rule = "cold-code";
51            findings.push(Finding {
52                fingerprint: fingerprint(rule, &[m.path.as_str(), &f.name, &f.line.to_string()]),
53                rule: rule.into(),
54                category: Category::DeadCode,
55                severity: Severity::Warn,
56                confidence: Confidence::Likely,
57                attribution: None,
58                reason: format!(
59                    "function `{}` is reachable but never executed in the provided coverage (cold path)",
60                    f.name
61                ),
62                location: Location {
63                    path: m.path.clone(),
64                    line: f.line,
65                    column: 0,
66                    end_line: Some(f.end_line),
67                },
68                actions: vec![Action {
69                    kind: "review-cold-code".into(),
70                    description: format!(
71                        "`{}` ran zero times in this coverage — verify it's dead before removing",
72                        f.name
73                    ),
74                    auto_fixable: false,
75                    suppression_comment: Some("# mollify: ignore[cold-code]".into()),
76                }],
77            });
78        }
79    }
80    findings
81}
82
83/// Find the executed-line set for a module by exact key, then rel-path, then
84/// trailing file-name match.
85fn match_coverage<'a>(
86    root: &Utf8Path,
87    path: &Utf8Path,
88    by_key: &'a FxHashMap<String, FxHashSet<u32>>,
89) -> Option<&'a FxHashSet<u32>> {
90    if let Some(s) = by_key.get(path.as_str()) {
91        return Some(s);
92    }
93    let rel = path
94        .strip_prefix(root)
95        .unwrap_or(path)
96        .as_str()
97        .trim_start_matches("./");
98    if let Some(s) = by_key.get(rel) {
99        return Some(s);
100    }
101    let name = path.file_name()?;
102    by_key
103        .iter()
104        .find(|(k, _)| k.ends_with(name))
105        .map(|(_, v)| v)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use camino::Utf8PathBuf;
112    use mollify_graph::discover_python_files;
113
114    fn temp(tag: &str) -> Utf8PathBuf {
115        let base =
116            std::env::temp_dir().join(format!("mollify-core-cov-{}-{tag}", std::process::id()));
117        let _ = std::fs::remove_dir_all(&base);
118        std::fs::create_dir_all(&base).unwrap();
119        Utf8PathBuf::from_path_buf(base).unwrap()
120    }
121
122    #[test]
123    fn flags_cold_function() {
124        let d = temp("cov");
125        // hot() on lines 1-2, cold() on lines 4-5.
126        std::fs::write(
127            d.join("app.py"),
128            "def hot():\n    return 1\n\ndef cold():\n    return 2\n",
129        )
130        .unwrap();
131        // coverage report: only line 2 executed.
132        let cov = d.join("coverage.json");
133        std::fs::write(&cov, r#"{"files":{"app.py":{"executed_lines":[1,2]}}}"#).unwrap();
134        let files = discover_python_files(&d);
135        let g = ModuleGraph::build(&d, &files);
136        let f = analyze(&d, &g, &cov);
137        assert!(
138            f.iter()
139                .any(|x| x.rule == "cold-code" && x.reason.contains("cold")),
140            "got {f:?}"
141        );
142        assert!(!f.iter().any(|x| x.reason.contains("`hot`")));
143        std::fs::remove_dir_all(&d).ok();
144    }
145}