Skip to main content

mollify_core/
commented.rs

1//! Commented-out-code detection (eradicate / flake8-eradicate E800). Flags
2//! comment lines whose stripped text parses as Python code (`import`, `def`,
3//! `return`, assignments, control flow) rather than prose. Tool directives
4//! (`noqa`, `type:`, `mypy:`, `TODO`, `mollify:`, shebangs) are never flagged.
5//! Orthogonal to reachability — it's about dead *text*, not dead symbols.
6
7use crate::fingerprint::fingerprint;
8use mollify_graph::ModuleGraph;
9use mollify_types::{Action, Category, Confidence, Finding, Location, Severity};
10
11/// Directive prefixes that are legitimate comments, never commented-out code.
12const DIRECTIVES: &[&str] = &[
13    "noqa", "type:", "mypy", "pylint", "pyright", "ruff", "flake8", "isort", "todo", "fixme",
14    "xxx", "hack", "note", "mollify", "nosec", "pragma", "!",
15];
16
17/// Heuristic: does a comment's stripped body look like Python code?
18fn looks_like_code(body: &str) -> bool {
19    let b = body.trim();
20    if b.len() < 3 {
21        return false;
22    }
23    let lower = b.to_ascii_lowercase();
24    if DIRECTIVES.iter().any(|d| lower.starts_with(d)) {
25        return false;
26    }
27    // Statement keywords at the start.
28    let starters = [
29        "import ", "from ", "def ", "class ", "return", "if ", "elif ", "else:", "for ", "while ",
30        "try:", "except", "finally:", "with ", "raise ", "assert ", "print(", "del ", "yield ",
31        "async ", "await ", "lambda ",
32    ];
33    if starters.iter().any(|s| b.starts_with(s)) {
34        return true;
35    }
36    // `name = value` / `name(...)` / `obj.method(...)` shaped lines (with a
37    // trailing colon or paren/operator), excluding prose-like sentences.
38    let codeish = (b.contains(" = ") || b.contains("=="))
39        || (b.ends_with(':') && !b.contains(' '))
40        || (b.ends_with(')') && b.contains('('))
41        || b.ends_with('\\');
42    codeish && !b.ends_with('.') && b.split_whitespace().count() <= 12
43}
44
45/// Emit a `commented-code` finding per comment line that looks like code.
46pub fn analyze(graph: &ModuleGraph) -> Vec<Finding> {
47    let mut findings = Vec::new();
48    for m in &graph.modules {
49        if let Some(src) = mollify_graph::read_source(&m.path) {
50            findings.extend(analyze_source(&m.path, &src));
51        }
52    }
53    findings
54}
55
56/// Commented-code findings from a file's source text (also the live LSP path).
57pub fn analyze_source(path: &camino::Utf8Path, src: &str) -> Vec<Finding> {
58    let mut findings = Vec::new();
59    for (i, line) in src.lines().enumerate() {
60        let trimmed = line.trim_start();
61        let Some(body) = trimmed.strip_prefix('#') else {
62            continue;
63        };
64        if !looks_like_code(body) {
65            continue;
66        }
67        let rule = "commented-code";
68        let line_no = i as u32 + 1;
69        findings.push(Finding {
70            fingerprint: fingerprint(rule, &[path.as_str(), &line_no.to_string()]),
71            rule: rule.into(),
72            category: Category::DeadCode,
73            severity: Severity::Warn,
74            confidence: Confidence::Likely,
75            attribution: None,
76            reason: format!("commented-out code: `{}`", body.trim()),
77            location: Location {
78                path: path.to_owned(),
79                line: line_no,
80                column: 0,
81                end_line: None,
82            },
83            actions: vec![Action {
84                kind: "remove-commented-code".into(),
85                description: "Delete the commented-out code (version control remembers it)".into(),
86                auto_fixable: false,
87                suppression_comment: Some("# mollify: ignore[commented-code]".into()),
88            }],
89        });
90    }
91    findings
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn flags_code_not_prose_or_directives() {
100        assert!(looks_like_code(" import os"));
101        assert!(looks_like_code(" return x + 1"));
102        assert!(looks_like_code(" x = compute()"));
103        assert!(looks_like_code(" def helper():"));
104        assert!(!looks_like_code(" this explains why we do the thing."));
105        assert!(!looks_like_code(" noqa: F401"));
106        assert!(!looks_like_code(" type: ignore"));
107        assert!(!looks_like_code(" TODO: fix this later"));
108    }
109}