use crate::fingerprint::fingerprint;
use mollify_graph::ModuleGraph;
use mollify_types::{Action, Category, Confidence, Finding, Location, Severity};
const DIRECTIVES: &[&str] = &[
"noqa", "type:", "mypy", "pylint", "pyright", "ruff", "flake8", "isort", "todo", "fixme",
"xxx", "hack", "note", "mollify", "nosec", "pragma", "!",
];
fn looks_like_code(body: &str) -> bool {
let b = body.trim();
if b.len() < 3 {
return false;
}
let lower = b.to_ascii_lowercase();
if DIRECTIVES.iter().any(|d| lower.starts_with(d)) {
return false;
}
let starters = [
"import ", "from ", "def ", "class ", "return", "if ", "elif ", "else:", "for ", "while ",
"try:", "except", "finally:", "with ", "raise ", "assert ", "print(", "del ", "yield ",
"async ", "await ", "lambda ",
];
if starters.iter().any(|s| b.starts_with(s)) {
return true;
}
let codeish = (b.contains(" = ") || b.contains("=="))
|| (b.ends_with(':') && !b.contains(' '))
|| (b.ends_with(')') && b.contains('('))
|| b.ends_with('\\');
codeish && !b.ends_with('.') && b.split_whitespace().count() <= 12
}
pub fn analyze(graph: &ModuleGraph) -> Vec<Finding> {
let mut findings = Vec::new();
for m in &graph.modules {
if let Some(src) = mollify_graph::read_source(&m.path) {
findings.extend(analyze_source(&m.path, &src));
}
}
findings
}
pub fn analyze_source(path: &camino::Utf8Path, src: &str) -> Vec<Finding> {
let mut findings = Vec::new();
for (i, line) in src.lines().enumerate() {
let trimmed = line.trim_start();
let Some(body) = trimmed.strip_prefix('#') else {
continue;
};
if !looks_like_code(body) {
continue;
}
let rule = "commented-code";
let line_no = i as u32 + 1;
findings.push(Finding {
fingerprint: fingerprint(rule, &[path.as_str(), &line_no.to_string()]),
rule: rule.into(),
category: Category::DeadCode,
severity: Severity::Warn,
confidence: Confidence::Likely,
attribution: None,
reason: format!("commented-out code: `{}`", body.trim()),
location: Location {
path: path.to_owned(),
line: line_no,
column: 0,
end_line: None,
},
actions: vec![Action {
kind: "remove-commented-code".into(),
description: "Delete the commented-out code (version control remembers it)".into(),
auto_fixable: false,
suppression_comment: Some("# mollify: ignore[commented-code]".into()),
}],
});
}
findings
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flags_code_not_prose_or_directives() {
assert!(looks_like_code(" import os"));
assert!(looks_like_code(" return x + 1"));
assert!(looks_like_code(" x = compute()"));
assert!(looks_like_code(" def helper():"));
assert!(!looks_like_code(" this explains why we do the thing."));
assert!(!looks_like_code(" noqa: F401"));
assert!(!looks_like_code(" type: ignore"));
assert!(!looks_like_code(" TODO: fix this later"));
}
}