cha_core/plugins/
todo_tracker.rs1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub struct TodoTrackerAnalyzer;
8
9impl Plugin for TodoTrackerAnalyzer {
10 fn name(&self) -> &str {
11 "todo_tracker"
12 }
13
14 fn smells(&self) -> Vec<String> {
15 vec!["todo_comment".into()]
16 }
17
18 fn description(&self) -> &str {
19 "Leftover TODO/FIXME/HACK/XXX comments"
20 }
21
22 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
23 ctx.model
24 .comments
25 .iter()
26 .filter_map(|c| check_comment(c, ctx))
27 .collect()
28 }
29}
30
31fn check_comment(c: &crate::CommentInfo, ctx: &AnalysisContext) -> Option<Finding> {
32 let upper = c.text.to_uppercase();
33 let (tag, severity) = if has_tag(&upper, "HACK") {
34 ("HACK", Severity::Warning)
35 } else if has_tag(&upper, "XXX") {
36 ("XXX", Severity::Warning)
37 } else if has_tag(&upper, "FIXME") {
38 ("FIXME", Severity::Hint)
39 } else if has_tag(&upper, "TODO") {
40 ("TODO", Severity::Hint)
41 } else {
42 return None;
43 };
44 let col = upper.find(tag).unwrap_or(0);
45 Some(Finding {
46 smell_name: "todo_comment".into(),
47 category: SmellCategory::Dispensables,
48 severity,
49 location: Location {
50 path: ctx.file.path.clone(),
51 start_line: c.line,
52 start_col: col,
53 end_line: c.line,
54 end_col: col + tag.len(),
55 name: None,
56 },
57 message: format!(
58 "{tag}: {}",
59 c.text.trim_start_matches(['/', '#', '*', ' ', '-'])
60 ),
61 suggested_refactorings: vec!["Resolve or create a tracking issue".into()],
62 ..Default::default()
63 })
64}
65
66fn has_tag(line: &str, tag: &str) -> bool {
68 if let Some(pos) = line.find(tag) {
69 let after = pos + tag.len();
70 after >= line.len() || !line.as_bytes()[after].is_ascii_alphabetic()
71 } else {
72 false
73 }
74}