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    let col = upper.find(tag).unwrap_or(0);
41    Some(Finding {
42        smell_name: "todo_comment".into(),
43        category: SmellCategory::Dispensables,
44        severity,
45        location: Location {
46            path: ctx.file.path.clone(),
47            start_line: c.line,
48            start_col: col,
49            end_line: c.line,
50            end_col: col + tag.len(),
51            name: None,
52        },
53        message: format!(
54            "{tag}: {}",
55            c.text.trim_start_matches(['/', '#', '*', ' ', '-'])
56        ),
57        suggested_refactorings: vec!["Resolve or create a tracking issue".into()],
58        ..Default::default()
59    })
60}
61
62/// Match tag as a word boundary (e.g. "TAG:" or "TAG " but not "TAGLIST")
63fn has_tag(line: &str, tag: &str) -> bool {
64    if let Some(pos) = line.find(tag) {
65        let after = pos + tag.len();
66        after >= line.len() || !line.as_bytes()[after].is_ascii_alphabetic()
67    } else {
68        false
69    }
70}