Skip to main content

garbage_code_hunter/treesitter/rules/
remaining_rules.rs

1use crate::analyzer::{CodeIssue, Severity};
2use crate::language::Language;
3use crate::treesitter::engine::ParsedFile;
4use crate::treesitter::query::collect_captures;
5use crate::treesitter::rule::TreeSitterRule;
6
7use super::complex_rules::variable_name_query;
8
9/// Meaningless naming: detects placeholder names like foo, bar, aaa, data, temp.
10pub(crate) struct MeaninglessRule;
11
12impl TreeSitterRule for MeaninglessRule {
13    fn name(&self) -> &'static str {
14        "meaningless-naming"
15    }
16
17    fn supported_languages(&self) -> &'static [Language] {
18        crate::language::LANGUAGES_WITH_GRAMMAR
19    }
20
21    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
22        let query = variable_name_query(file.language);
23        let captures = match collect_captures(file, query) {
24            Ok(c) => c,
25            Err(_) => return vec![],
26        };
27
28        let meaningless: &[&str] = &[
29            "foo", "bar", "baz", "qux", "quux", "quuz", "aaa", "bbb", "ccc", "ddd", "eee", "xxx",
30            "yyy", "zzz", "test1", "test2", "test3",
31        ];
32
33        let mut issues = Vec::new();
34
35        for group in &captures {
36            if let Some(cap) = group.first() {
37                let name = cap.text.to_lowercase();
38                let chars: Vec<char> = name.chars().collect();
39                let is_repeating = chars.len() >= 3 && chars.iter().all(|c| *c == chars[0]);
40                let is_meaningless = meaningless.contains(&name.as_str()) || is_repeating;
41                if is_meaningless {
42                    let msgs = [
43                        format!(
44                            "Variable '{}'? Did you fall asleep on the keyboard?",
45                            cap.text
46                        ),
47                        format!("'{}'? Naming is hard, but this is just sad", cap.text),
48                        format!(
49                            "A variable named '{}'? I've seen better names in random tests",
50                            cap.text
51                        ),
52                        format!(
53                            "'{}' is not a real variable name, it's a cry for help",
54                            cap.text
55                        ),
56                        format!(
57                            "Congratulations on naming a variable '{}' — truly innovative",
58                            cap.text
59                        ),
60                    ];
61                    let pos = cap.node.start_position();
62                    let severity = if matches!(name.as_str(), "foo" | "bar" | "baz") {
63                        Severity::Spicy
64                    } else {
65                        Severity::Mild
66                    };
67                    issues.push(CodeIssue {
68                        file_path: file.path.clone(),
69                        line: pos.row + 1,
70                        column: pos.column + 1,
71                        rule_name: "meaningless-naming".to_string(),
72                        message: msgs[issues.len() % msgs.len()].clone(),
73                        severity,
74                    });
75                }
76            }
77        }
78        issues
79    }
80}
81
82/// Commented code: detects blocks of commented-out code.
83pub(crate) struct CommentedCodeRule;
84
85impl TreeSitterRule for CommentedCodeRule {
86    fn name(&self) -> &'static str {
87        "commented-code"
88    }
89
90    fn supported_languages(&self) -> &'static [Language] {
91        crate::language::LANGUAGES_WITH_GRAMMAR
92    }
93
94    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
95        let mut issues = Vec::new();
96        let mut block_start = 0;
97        let mut block_size = 0;
98
99        let line_comment = file.language.line_comment();
100
101        for (line_num, line) in file.content.lines().enumerate() {
102            let trimmed = line.trim();
103            if trimmed.starts_with(line_comment) {
104                let comment_text = trimmed.strip_prefix(line_comment).unwrap_or("").trim();
105                if is_likely_code(comment_text) {
106                    if block_size == 0 {
107                        block_start = line_num + 1;
108                    }
109                    block_size += 1;
110                } else if block_size > 0 {
111                    emit_comment_block(&mut issues, file, block_start, block_size);
112                    block_size = 0;
113                }
114            } else if !trimmed.is_empty() && block_size > 0 {
115                emit_comment_block(&mut issues, file, block_start, block_size);
116                block_size = 0;
117            }
118        }
119        if block_size > 0 {
120            emit_comment_block(&mut issues, file, block_start, block_size);
121        }
122        issues
123    }
124}
125
126fn is_likely_code(text: &str) -> bool {
127    let code_patterns = [
128        "fn ", "if ", "else", "for ", "while ", "match ", "struct ", "enum ", "impl ", "let ",
129        "return ", "use ", "mod ", "break", "continue", "{", "}", "(", ")", "[", "]", ";", "=",
130        "==", "!=", "&&", "||", "->", "::",
131    ];
132    let keywords = [
133        "pub", "const", "static", "mut", "ref", "move", "async", "await", "unsafe", "extern",
134        "crate", "def", "class", "import", "from", "lambda", "function", "var", "let", "const",
135        "if", "else",
136    ];
137    let pattern_count = code_patterns.iter().filter(|p| text.contains(*p)).count();
138    let keyword_count = keywords.iter().filter(|k| text.contains(*k)).count();
139    pattern_count >= 2 || keyword_count >= 1
140}
141
142fn emit_comment_block(
143    issues: &mut Vec<CodeIssue>,
144    file: &ParsedFile,
145    start_line: usize,
146    size: usize,
147) {
148    if size < 3 {
149        return;
150    }
151    let msgs = [
152        format!(
153            "{} lines of commented-out code — commit or delete, don't hoard",
154            size
155        ),
156        format!(
157            "Found {} lines of dead comment code. Git exists for a reason",
158            size
159        ),
160        format!(
161            "Commenting out code is like keeping an ex's photos. Let it go ({} lines)",
162            size
163        ),
164    ];
165    let severity = if size > 10 {
166        Severity::Spicy
167    } else {
168        Severity::Mild
169    };
170    issues.push(CodeIssue {
171        file_path: file.path.clone(),
172        line: start_line,
173        column: 1,
174        rule_name: "commented-code".to_string(),
175        message: msgs[issues.len() % msgs.len()].clone(),
176        severity,
177    });
178}
179
180/// Dead code: detects unreachable code after return/break/continue/panic.
181pub(crate) struct DeadCodeRule;
182
183impl TreeSitterRule for DeadCodeRule {
184    fn name(&self) -> &'static str {
185        "dead-code"
186    }
187
188    fn supported_languages(&self) -> &'static [Language] {
189        &[Language::Rust]
190    }
191
192    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
193        let mut issues = Vec::new();
194        let mut dead_start: Option<usize> = None;
195        let mut reported = false;
196
197        for (line_num, line) in file.content.lines().enumerate() {
198            let trimmed = line.trim();
199
200            if is_terminator(trimmed) {
201                dead_start = Some(line_num + 2);
202                reported = false;
203                continue;
204            }
205
206            if let Some(start) = dead_start {
207                if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") {
208                    continue;
209                }
210                if trimmed == "}" || trimmed.starts_with("} else") {
211                    dead_start = None;
212                    continue;
213                }
214                if !reported && line_num + 1 >= start {
215                    let msgs = [
216                        "Dead code detected — this code never executes, like my健身计划",
217                        "Unreachable code! Return already happened, this is just decoration",
218                        "Dead code walking... nothing after 'return' ever runs",
219                    ];
220                    issues.push(CodeIssue {
221                        file_path: file.path.clone(),
222                        line: line_num + 1,
223                        column: 1,
224                        rule_name: "dead-code".to_string(),
225                        message: msgs[issues.len() % msgs.len()].to_string(),
226                        severity: Severity::Mild,
227                    });
228                    reported = true;
229                }
230            }
231        }
232        issues
233    }
234}
235
236fn is_terminator(line: &str) -> bool {
237    let trimmed = line.trim();
238    matches!(
239        trimmed,
240        "return;" | "break;" | "continue;" | "unreachable!()" | "unreachable!();"
241    ) || (trimmed.starts_with("return ") && trimmed.ends_with(';'))
242        || (trimmed.starts_with("panic!(") && trimmed.ends_with(';'))
243        || (trimmed.starts_with("unreachable!(") && trimmed.ends_with(')'))
244}
245
246/// TODO/FIXME comment detection.
247pub(crate) struct TodoCommentRule;
248
249impl TreeSitterRule for TodoCommentRule {
250    fn name(&self) -> &'static str {
251        "todo-comment"
252    }
253
254    fn supported_languages(&self) -> &'static [Language] {
255        crate::language::LANGUAGES_WITH_GRAMMAR
256    }
257
258    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
259        let mut issues = Vec::new();
260        let mut total_todos = 0;
261
262        // Count todo!(), unimplemented!(), unreachable!() calls via query
263        for pattern in &[
264            "(macro_invocation macro: (identifier) @m (#eq? @m \"todo\"))",
265            "(macro_invocation macro: (identifier) @m (#eq? @m \"unimplemented\"))",
266            "(macro_invocation macro: (identifier) @m (#eq? @m \"unreachable\"))",
267        ] {
268            if let Ok(caps) = collect_captures(file, pattern) {
269                total_todos += caps.iter().map(|c| c.len()).sum::<usize>();
270            }
271        }
272
273        let line_comment = file.language.line_comment();
274        for (line_num, line) in file.content.lines().enumerate() {
275            let trimmed = line.trim();
276            if let Some(pos) = trimmed.find(line_comment) {
277                let comment = trimmed[pos + line_comment.len()..].trim();
278                let upper = comment.to_uppercase();
279
280                let has_todo = upper.starts_with("TODO") || upper.contains(" TODO ");
281                let has_fixme = upper.starts_with("FIXME") || upper.contains(" FIXME ");
282                let has_bug = upper.starts_with("BUG") || upper.contains(" BUG ");
283                let has_hack = upper.starts_with("HACK") || upper.contains(" HACK ");
284
285                if has_todo {
286                    total_todos += 1;
287                }
288                if has_fixme {
289                    issues.push(CodeIssue {
290                        file_path: file.path.clone(),
291                        line: line_num + 1,
292                        column: pos + 1,
293                        rule_name: "todo-fixme".to_string(),
294                        message: format!("FIXME: {}", comment.trim()),
295                        severity: Severity::Mild,
296                    });
297                }
298                if has_bug {
299                    issues.push(CodeIssue {
300                        file_path: file.path.clone(),
301                        line: line_num + 1,
302                        column: pos + 1,
303                        rule_name: "todo-bug".to_string(),
304                        message: format!("BUG: {}", comment.trim()),
305                        severity: Severity::Spicy,
306                    });
307                }
308                if has_hack {
309                    issues.push(CodeIssue {
310                        file_path: file.path.clone(),
311                        line: line_num + 1,
312                        column: pos + 1,
313                        rule_name: "todo-hack".to_string(),
314                        message: format!("HACK: {}", comment.trim()),
315                        severity: Severity::Mild,
316                    });
317                }
318            }
319        }
320
321        if total_todos > 3 {
322            let sev = if total_todos > 10 {
323                Severity::Spicy
324            } else {
325                Severity::Mild
326            };
327            let msgs = [
328                format!(
329                    "Found {} TODO markers — your backlog must be terrifying",
330                    total_todos
331                ),
332                format!(
333                    "{} TODOs left in code. Future you will hate present you",
334                    total_todos
335                ),
336                format!("{} unfinished tasks. Ship now, fix later?", total_todos),
337            ];
338            issues.push(CodeIssue {
339                file_path: file.path.clone(),
340                line: 1,
341                column: 1,
342                rule_name: "todo-comment".to_string(),
343                message: msgs[issues.len() % msgs.len()].clone(),
344                severity: sev,
345            });
346        }
347
348        issues
349    }
350}
351
352/// Duplicate imports detection.
353pub(crate) struct DuplicateImportsRule;
354
355impl TreeSitterRule for DuplicateImportsRule {
356    fn name(&self) -> &'static str {
357        "duplicate-imports"
358    }
359
360    fn supported_languages(&self) -> &'static [Language] {
361        &[Language::Rust]
362    }
363
364    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
365        let mut seen = std::collections::HashSet::new();
366        let mut issues = Vec::new();
367        let mut first_use_line = None;
368
369        for (line_num, line) in file.content.lines().enumerate() {
370            let trimmed = line.trim();
371            if trimmed.starts_with("use ") {
372                if first_use_line.is_none() {
373                    first_use_line = Some(line_num + 1);
374                }
375                if !seen.insert(trimmed.to_string()) {
376                    let msgs = [
377                        format!(
378                            "Duplicate import '{}' — reading comprehension matters",
379                            trimmed
380                        ),
381                        format!(
382                            "Importing '{}' twice doesn't make it more imported",
383                            trimmed
384                        ),
385                        format!("You already imported '{}' once. That was enough", trimmed),
386                    ];
387                    issues.push(CodeIssue {
388                        file_path: file.path.clone(),
389                        line: line_num + 1,
390                        column: 1,
391                        rule_name: "duplicate-imports".to_string(),
392                        message: msgs[issues.len() % msgs.len()].clone(),
393                        severity: Severity::Mild,
394                    });
395                }
396            }
397        }
398        issues
399    }
400}
401
402/// File too long detection.
403pub(crate) struct FileTooLongRule;
404
405impl TreeSitterRule for FileTooLongRule {
406    fn name(&self) -> &'static str {
407        "file-too-long"
408    }
409
410    fn supported_languages(&self) -> &'static [Language] {
411        crate::language::LANGUAGES_WITH_GRAMMAR
412    }
413
414    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
415        let line_count = file.content.lines().count();
416        let is_test = file.path.to_string_lossy().contains("test");
417        let threshold = if is_test { 2000 } else { 1000 };
418
419        if line_count > threshold {
420            let msgs = [
421                format!(
422                    "{} lines! Is this a file or a novel? Split it up",
423                    line_count
424                ),
425                format!(
426                    "This file has {} lines. Your editor is judging you",
427                    line_count
428                ),
429                format!(
430                    "{} lines in one file — that's not 'modular', that's a disaster",
431                    line_count
432                ),
433            ];
434            let severity = if line_count > 2000 {
435                Severity::Nuclear
436            } else if line_count > 1500 {
437                Severity::Spicy
438            } else {
439                Severity::Mild
440            };
441            vec![CodeIssue {
442                file_path: file.path.clone(),
443                line: 1,
444                column: 1,
445                rule_name: "file-too-long".to_string(),
446                message: msgs[0].clone(),
447                severity,
448            }]
449        } else {
450            vec![]
451        }
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use crate::treesitter::TreeSitterEngine;
459    use std::path::Path;
460
461    fn parse_rust(code: &str) -> ParsedFile {
462        let engine = TreeSitterEngine::new();
463        engine
464            .parse_file(Path::new("test.rs"), code)
465            .expect("Should parse")
466    }
467
468    #[test]
469    fn test_meaningless_names_detected() {
470        let file = parse_rust("fn main() { let foo = 1; let aaa = 2; let xxx = 3; }");
471        let rule = MeaninglessRule;
472        let issues = rule.check(&file);
473        assert!(issues.len() >= 3, "Should detect foo, aaa, xxx");
474    }
475
476    #[test]
477    fn test_meaningful_names_clean() {
478        let file = parse_rust("fn main() { let user_count = 1; let max_retries = 3; }");
479        let rule = MeaninglessRule;
480        let issues = rule.check(&file);
481        assert!(issues.is_empty(), "Good names should not trigger");
482    }
483
484    #[test]
485    fn test_commented_code_detected() {
486        let file = parse_rust(
487            r#"
488fn main() {
489    // let x = foo();
490    // let y = bar();
491    // let z = baz();
492    println!("real");
493}
494"#,
495        );
496        let rule = CommentedCodeRule;
497        let issues = rule.check(&file);
498        assert!(!issues.is_empty(), "Should detect commented-out code");
499    }
500
501    #[test]
502    fn test_todo_comment_detected() {
503        let file = parse_rust(
504            r#"
505fn main() {
506    // TODO: implement this
507    // FIXME: this is broken
508    // TODO: also this
509    // TODO: and one more
510    // XXX: cleanup required
511    todo!();
512}
513"#,
514        );
515        let rule = TodoCommentRule;
516        let issues = rule.check(&file);
517        let rule_names: Vec<_> = issues.iter().map(|i| i.rule_name.as_str()).collect();
518        assert!(rule_names.contains(&"todo-fixme"), "Should detect FIXME");
519        assert!(rule_names.contains(&"todo-comment"), "Should detect TODO");
520    }
521
522    #[test]
523    fn test_dead_code_detected() {
524        let file = parse_rust(
525            r#"
526fn main() {
527    return;
528    let x = 1;
529}
530"#,
531        );
532        let rule = DeadCodeRule;
533        let issues = rule.check(&file);
534        assert!(!issues.is_empty(), "Should detect dead code after return");
535    }
536
537    #[test]
538    fn test_duplicate_imports_detected() {
539        let file = parse_rust(
540            r#"
541use std::collections::HashMap;
542use std::collections::HashMap;
543fn main() {}
544"#,
545        );
546        let rule = DuplicateImportsRule;
547        let issues = rule.check(&file);
548        assert!(!issues.is_empty(), "Should detect duplicate import");
549    }
550}