Skip to main content

cha_core/plugins/
todo_tracker.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3// cha:ignore todo_comment
4/// Detect leftover task comments (todo/fixme/hack/xxx) in source code.
5///
6/// Severity levels: H/X tags → Warning, F/T tags → Hint.
7pub struct TodoTrackerAnalyzer;
8
9impl Plugin for TodoTrackerAnalyzer {
10    fn name(&self) -> &str {
11        "todo_tracker"
12    }
13
14    fn description(&self) -> &str {
15        "Leftover TODO/FIXME/HACK/XXX comments"
16    }
17
18    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
19        ctx.model
20            .comments
21            .iter()
22            .filter_map(|c| check_comment(c, ctx))
23            .collect()
24    }
25}
26
27fn check_comment(c: &crate::CommentInfo, ctx: &AnalysisContext) -> Option<Finding> {
28    let upper = c.text.to_uppercase();
29    let (tag, severity) = if has_tag(&upper, "HACK") {
30        ("HACK", Severity::Warning)
31    } else if has_tag(&upper, "XXX") {
32        ("XXX", Severity::Warning)
33    } else if has_tag(&upper, "FIXME") {
34        ("FIXME", Severity::Hint)
35    } else if has_tag(&upper, "TODO") {
36        ("TODO", Severity::Hint)
37    } else {
38        return None;
39    };
40    Some(Finding {
41        smell_name: "todo_comment".into(),
42        category: SmellCategory::Dispensables,
43        severity,
44        location: Location {
45            path: ctx.file.path.clone(),
46            start_line: c.line,
47            end_line: c.line,
48            name: None,
49        },
50        message: format!(
51            "{tag}: {}",
52            c.text.trim_start_matches(['/', '#', '*', ' ', '-'])
53        ),
54        suggested_refactorings: vec!["Resolve or create a tracking issue".into()],
55        ..Default::default()
56    })
57}
58
59/// Match tag as a word boundary (e.g. "TAG:" or "TAG " but not "TAGLIST")
60fn has_tag(line: &str, tag: &str) -> bool {
61    if let Some(pos) = line.find(tag) {
62        let after = pos + tag.len();
63        after >= line.len() || !line.as_bytes()[after].is_ascii_alphabetic()
64    } else {
65        false
66    }
67}