Skip to main content

garbage_code_hunter/rules/
student_code.rs

1use std::path::Path;
2use syn::{visit::Visit, ExprMacro, File};
3
4use crate::analyzer::{CodeIssue, Severity};
5use crate::context::FileContext;
6use crate::rules::Rule;
7use crate::utils::{count_non_comment_matches, find_line_of_str, get_position};
8
9/// Detect println! debugging statements everywhere
10pub struct PrintlnDebuggingRule;
11
12impl Rule for PrintlnDebuggingRule {
13    fn name(&self) -> &'static str {
14        "println-debugging"
15    }
16
17    fn check(
18        &self,
19        file_path: &Path,
20        _syntax_tree: &File,
21        content: &str,
22        lang: &str,
23        is_test_file: bool,
24    ) -> Vec<CodeIssue> {
25        if is_test_file {
26            return Vec::new();
27        }
28
29        let mut issues = Vec::new();
30
31        // Check if this is a main.rs or lib.rs file (CLI tools legitimately use println!)
32        let file_name = file_path.file_name().and_then(|f| f.to_str()).unwrap_or("");
33        let is_main_file = file_name == "main.rs" || file_name == "lib.rs";
34
35        // Count different types of println! calls (excluding comments)
36        let total_println = count_non_comment_matches(content, "println!");
37        let total_eprintln = count_non_comment_matches(content, "eprintln!");
38
39        // Patterns that indicate DEBUGGING println! (not normal output)
40        let debug_patterns = [
41            // Pure debug statements with no meaningful content
42            r#"println!("debug"#,
43            r#"println!("check"#,
44            r#"println!("test"#,
45            r#"println!("DEBUG"#,
46            r#"println!("here)"#,
47            r#"println!("checkpoint"#,
48            r#"println!("step"#,
49            r#"println!("line "#,
50            r#"println!("=== "#,
51            r#"println!("--- "#,
52            r#"println!(">>> "#,
53            // Print with simple variables (likely debug)
54            r#"println!("{:?}"#,    // Debug formatting
55            r#"println!("{:#?}"#,   // Pretty debug
56            r#"println!("x = "#,    // Variable dump
57            r#"println!("val:"#,    // Value check
58            r#"println!("result:"#, // Result check
59            r#"println!("res:"#,    // Short result
60            r#"println!("i = "#,    // Loop variable
61            r#"println!("j = "#,    // Loop variable
62            r#"println!("k = "#,    // Loop variable
63            r#"println!("count"#,   // Count debugging
64            r#"println!("len("#,    // Length debugging
65            r#"println!("size"#,    // Size debugging
66            // Empty or minimal prints
67            r#"println!("")"#,
68            r#"println!()"#,
69            // Array/vec debugging
70            r#"println!("{:?"#,  // Debug format start
71            r#"println!("vec!"#, // Vec printing
72        ];
73
74        let mut debug_count = 0;
75        for pattern in &debug_patterns {
76            debug_count += count_non_comment_matches(content, pattern);
77        }
78
79        // Patterns that indicate NORMAL OUTPUT println!
80        let output_patterns = [
81            // Error handling (eprintln is legitimate)
82            r#"eprintln!("Error"#,
83            r#"eprintln!("Warning"#,
84            r#"eprintln!("Failed"#,
85            r#"eprintln!("error:"#,
86            r#"eprintln!("warn:"#,
87            // UI/CLI output with emojis (legitimate user-facing messages)
88            r#"println!("📊"#,      // Stats/metrics output
89            r#"println!("🏆"#,      // Score/results
90            r#"println!("🗑️"#,      // Tool branding
91            r#"println!("✅"#,      // Success messages
92            r#"println!("❌"#,      // Error indicators
93            r#"println!("⚠️"#,      // Warnings
94            r#"println!("🎓"#,      // Educational
95            r#"println!("💡"#,      // Tips
96            r#"println!("🔥"#,      // Hall of shame
97            r#"println!("📍"#,      // Location markers
98            r#"println!("🔍"#,      // Search indicators
99            r#"println!("⏱️"#,      // Performance
100            r#"println!("💾"#,      // File operations
101            r#"println!("📝"#,      // Notes
102            r#"println!("🎯"#,      // Target/goal
103            r#"println!("🚀"#,      // Launch/start
104            r#"println!("✨"#,      // Sparkles/new
105            r#"println!("🎨"#,      // Art/styling
106            r#"println!("📈"#,      // Charts/growth
107            r#"println!("─"#,       // Separator lines (repeat)
108            r#"println!("{}", "─"#, // Separator with repeat
109            // JSON/formatted output (structured data export)
110            r#"serde_json::to"#,
111            r#"println!("{{"#, // JSON start
112            // User-facing messages in quotes (meaningful output)
113            r#"Total files"#,
114            r#"issues found"#,
115            r#"analyzed"#,
116            r#"score"#,
117            r#"result"#,
118            r#"Usage:"#,      // CLI usage info
119            r#"Arguments:"#,  // CLI arguments
120            r#"Options:"#,    // CLI options
121            r#"Version:"#,    // Version info
122            r#"Help:"#,       // Help text
123            r#"Example:"#,    // Examples
124            r#"Note:"#,       // Notes to users
125            r#"Tip:"#,        // Tips for users
126            r#"Warning:"#,    // Warnings (println version)
127            r#"Error:"#,      // Errors (println version)
128            r#"Success:"#,    // Success messages
129            r#"Failed:"#,     // Failure messages
130            r#"Completed:"#,  // Completion messages
131            r#"Started:"#,    // Start messages
132            r#"Finished:"#,   // Finish messages
133            r#"Processing:"#, // Processing status
134            r#"Loading:"#,    // Loading status
135            r#"Saving:"#,     // Saving status
136            r#"Reading:"#,    // Reading status
137            r#"Writing:"#,    // Writing status
138            r#"Found "#,      // Found items
139            r#"Missing "#,    // Missing items
140            r#"Invalid "#,    // Invalid items
141            r#"Unknown "#,    // Unknown items
142            // Formatted tables/lists
143            r#"| "#,  // Table format
144            r#"- ─"#, // Table separator (dash + em dash)
145            // Progress indicators
146            r#"%"#, // Percentage
147            r#"/"#, // Progress fraction
148            // Time/date output
149            r#"ms)"#,      // Milliseconds
150            r#"seconds)"#, // Seconds
151            r#"minutes)"#, // Minutes
152        ];
153
154        let mut output_count = 0;
155        for pattern in &output_patterns {
156            output_count += count_non_comment_matches(content, pattern);
157        }
158
159        // Heuristic: remaining println! are suspicious (ensure non-negative)
160        let suspicious_count = total_println
161            .saturating_add(total_eprintln)
162            .saturating_sub(debug_count)
163            .saturating_sub(output_count);
164
165        // Rule 1: Flag excessive debug-style println! even in main files
166        if debug_count > 3 || (!is_main_file && suspicious_count > 0) {
167            let count_to_report = if debug_count > 0 {
168                debug_count
169            } else {
170                suspicious_count
171            };
172
173            let severity = if count_to_report > 10 {
174                Severity::Spicy
175            } else {
176                Severity::Mild
177            };
178
179            let messages = if lang == "zh-CN" {
180                vec![
181                    format!(
182                        "发现 {} 个疑似调试用 println!,上线前记得删掉",
183                        count_to_report
184                    ),
185                    format!("{} 个 println! 看起来像调试代码", count_to_report),
186                    format!(
187                        "{} 个打印语句,这是要开演唱会吗?",
188                        total_println + total_eprintln
189                    ),
190                    format!("建议用 log::info! 或 eprintln! 替代调试用的 println!",),
191                ]
192            } else {
193                vec![
194                    format!(
195                        "Found {}疑似 debug println!s - remove before shipping",
196                        count_to_report
197                    ),
198                    format!("{} println!s look like debug code", count_to_report),
199                    format!(
200                        "{} print statements - hosting a concert?",
201                        total_println + total_eprintln
202                    ),
203                    format!("Consider using log::info! or eprintln! for debug prints"),
204                ]
205            };
206
207            let line = find_line_of_str(content, "println!");
208
209            issues.push(CodeIssue {
210                file_path: file_path.to_path_buf(),
211                line,
212                column: 1,
213                rule_name: "println-debugging".to_string(),
214                message: messages[issues.len() % messages.len()].clone(),
215                severity,
216            });
217        }
218
219        // Rule 2: Flag excessive TOTAL println! in any file (> 20 is too many)
220        let total = total_println + total_eprintln;
221        if total > 20 {
222            let messages = if lang == "zh-CN" {
223                vec![
224                    format!("{} 个 println!/eprintln!控制台要爆炸了", total),
225                    format!("{} 个打印语句,考虑提取到输出模块", total),
226                    format!("这么多输出语句,维护性-10",),
227                ]
228            } else {
229                vec![
230                    format!("{} println!/eprintln!s! Console explosion imminent", total),
231                    format!(
232                        "{} print statements - consider extracting to output module",
233                        total
234                    ),
235                    format!("So many output statements, maintainability -10",),
236                ]
237            };
238
239            let line = find_line_of_str(content, "println!");
240
241            issues.push(CodeIssue {
242                file_path: file_path.to_path_buf(),
243                line,
244                column: 1,
245                rule_name: "println-debugging".to_string(),
246                message: messages[issues.len() % messages.len()].clone(),
247                severity: Severity::Spicy,
248            });
249        }
250
251        issues
252    }
253
254    fn check_with_context(
255        &self,
256        file_path: &Path,
257        syntax_tree: &File,
258        content: &str,
259        lang: &str,
260        is_test_file: bool,
261        context: &FileContext,
262        _config: &crate::context::ProjectConfig,
263    ) -> Vec<CodeIssue> {
264        // Example, Test, Benchmark, Documentation: skip completely
265        let weight = context.rule_weight_multiplier();
266        if weight < 0.5 {
267            return Vec::new();
268        }
269
270        // main.rs/lib.rs files: allow more println (normal for CLI tools)
271        let file_name = file_path.file_name().and_then(|f| f.to_str()).unwrap_or("");
272        if file_name == "main.rs" || file_name == "lib.rs" {
273            // For entry files, only report Nuclear level issues (excessive debug output)
274            let issues = self.check(file_path, syntax_tree, content, lang, is_test_file);
275            return issues
276                .into_iter()
277                .filter(|issue| issue.severity == Severity::Nuclear)
278                .collect();
279        }
280
281        self.check(file_path, syntax_tree, content, lang, is_test_file)
282    }
283}
284
285/// Detect casual use of panic! and unwrap()
286pub struct PanicAbuseRule;
287
288impl Rule for PanicAbuseRule {
289    fn name(&self) -> &'static str {
290        "panic-abuse"
291    }
292
293    fn check(
294        &self,
295        file_path: &Path,
296        syntax_tree: &File,
297        content: &str,
298        lang: &str,
299        is_test_file: bool,
300    ) -> Vec<CodeIssue> {
301        if is_test_file {
302            return Vec::new();
303        }
304        let mut visitor = PanicAbuseVisitor::new(file_path.to_path_buf(), lang);
305        visitor.visit_file(syntax_tree);
306
307        // Check panic! macro calls in content (not strings/comments)
308        // Use "panic!(" to match actual macro calls, not "panic!" in strings
309        let panic_count = count_non_comment_matches(content, "panic!(");
310
311        if panic_count > 2 {
312            let line = find_line_of_str(content, "panic!(");
313            visitor.add_excessive_panic_issue(panic_count, line);
314        }
315
316        visitor.issues
317    }
318}
319
320/// Detect excessive TODO comments (both macro calls and comment markers)
321pub struct TodoCommentRule;
322
323impl Rule for TodoCommentRule {
324    fn name(&self) -> &'static str {
325        "todo-comment"
326    }
327
328    fn check(
329        &self,
330        file_path: &Path,
331        _syntax_tree: &File,
332        content: &str,
333        lang: &str,
334        is_test_file: bool,
335    ) -> Vec<CodeIssue> {
336        if is_test_file {
337            return Vec::new();
338        }
339        let mut issues = Vec::new();
340
341        // 1. Check macro calls that cause panics (todo!, unimplemented!, etc.)
342        let todo_macros = ["todo!", "unimplemented!", "unreachable!"];
343
344        let mut macro_todos = 0;
345        for pattern in &todo_macros {
346            macro_todos += count_non_comment_matches(content, pattern);
347        }
348
349        // 2. Check comment markers (TODO, FIXME, HACK, XXX, BUG, OPTIMIZE, etc.)
350        let mut comment_todos = Vec::new(); // Store (line_number, marker_type)
351        for (line_num, line) in content.lines().enumerate() {
352            let trimmed = line.trim();
353
354            // Check for TODO-style comments
355            if let Some(comment_start) = trimmed.find("//") {
356                let comment_content = &trimmed[comment_start + 2..].trim();
357
358                // Check for various TODO markers (case-insensitive)
359                let upper = comment_content.to_uppercase();
360                let is_todo_marker = upper.starts_with("TODO")
361                    || upper.contains(" TODO ")
362                    || upper.starts_with("FIXME")
363                    || upper.contains(" FIXME ")
364                    || upper.starts_with("HACK")
365                    || upper.contains(" HACK ")
366                    || upper.starts_with("XXX")
367                    || upper.contains(" XXX ")
368                    || upper.starts_with("BUG")
369                    || upper.contains(" BUG ")
370                    || upper.starts_with("OPTIMIZE")
371                    || upper.contains(" OPTIMIZE ")
372                    || upper.starts_with("TEMP")
373                    || upper.contains(" TEMP ")
374                    || upper.contains("WORKAROUND")
375                    || upper.contains("TEMPORARY");
376
377                if is_todo_marker {
378                    let marker_type = if upper.starts_with("TODO") || upper.contains(" TODO ") {
379                        "TODO"
380                    } else if upper.starts_with("FIXME") || upper.contains(" FIXME ") {
381                        "FIXME"
382                    } else if upper.starts_with("HACK") || upper.contains(" HACK ") {
383                        "HACK"
384                    } else if upper.starts_with("XXX") || upper.contains(" XXX ") {
385                        "XXX"
386                    } else if upper.starts_with("BUG") || upper.contains(" BUG ") {
387                        "BUG"
388                    } else if upper.starts_with("OPTIMIZE") || upper.contains(" OPTIMIZE ") {
389                        "OPTIMIZE"
390                    } else {
391                        "TEMP"
392                    };
393                    comment_todos.push((line_num + 1, marker_type));
394                }
395            }
396        }
397
398        let total_todos = macro_todos + comment_todos.len();
399
400        // Report issue if there are too many TODOs
401        if total_todos > 3 {
402            let messages =
403                if lang == "zh-CN" {
404                    vec![
405                        format!(
406                            "发现 {} 个 TODO/FIXME({}个宏 + {}个注释),这是代码还是购物清单?",
407                            total_todos,
408                            macro_todos,
409                            comment_todos.len()
410                        ),
411                        format!("{} 个未完成标记?你是在写代码还是在记日记?", total_todos),
412                        format!("TODO 比实际代码还多,建议改名叫 'TODO Hunter'"),
413                        format!("{} 个 TODO,看来这个项目还在'施工中'", total_todos),
414                        format!(
415                            "这么多 TODO({}个 {},{}个 {}),是不是该考虑清理了?",
416                            total_todos,
417                            if comment_todos.iter().any(|&(_, t)| t == "TODO") {
418                                "TODO"
419                            } else {
420                                "标记"
421                            },
422                            comment_todos.iter().filter(|&&(_, t)| t == "TODO").count(),
423                            if comment_todos.iter().any(|&(_, t)| t == "FIXME") {
424                                "FIXME"
425                            } else {
426                                "标记"
427                            },
428                        ),
429                    ]
430                } else {
431                    vec![
432                        format!(
433                        "Found {} TODOs/FIXMEs ({} macros + {} comments) - code or shopping list?",
434                        total_todos, macro_todos, comment_todos.len()
435                    ),
436                        format!(
437                            "{} unfinished items? Are you coding or journaling?",
438                            total_todos
439                        ),
440                        format!("More TODOs than actual code, consider renaming to 'TODO Hunter'"),
441                        format!(
442                            "{} TODOs - looks like this project is still 'under construction'",
443                            total_todos
444                        ),
445                        format!(
446                            "So many TODOs ({} {}, {} {}) - time for cleanup?",
447                            total_todos,
448                            if comment_todos.iter().any(|&(_, t)| t == "TODO") {
449                                "TODOs"
450                            } else {
451                                "markers"
452                            },
453                            comment_todos.iter().filter(|&&(_, t)| t == "TODO").count(),
454                            if comment_todos.iter().any(|&(_, t)| t == "FIXME") {
455                                "FIXMEs"
456                            } else {
457                                "markers"
458                            },
459                        ),
460                    ]
461                };
462
463            let severity = if total_todos > 10 {
464                Severity::Spicy
465            } else {
466                Severity::Mild
467            };
468
469            // Find the first TODO/FIXME line (prefer comment markers over macros)
470            let line = if !comment_todos.is_empty() {
471                comment_todos[0].0
472            } else {
473                todo_macros
474                    .iter()
475                    .filter_map(|p| {
476                        let l = find_line_of_str(content, p);
477                        if l > 1 {
478                            Some(l)
479                        } else if content.contains(p) {
480                            Some(1)
481                        } else {
482                            None
483                        }
484                    })
485                    .min()
486                    .unwrap_or(1)
487            };
488
489            issues.push(CodeIssue {
490                file_path: file_path.to_path_buf(),
491                line,
492                column: 1,
493                rule_name: "todo-comment".to_string(),
494                message: messages[total_todos % messages.len()].clone(),
495                severity,
496            });
497        }
498
499        // Also report individual stale TODOs (older than 3 months or with specific markers)
500        for &(line_num, marker_type) in &comment_todos {
501            // Always report FIXME and BUG markers as they're more urgent
502            if marker_type == "FIXME" || marker_type == "BUG" {
503                let _line_content = content.lines().nth(line_num - 1).unwrap_or("");
504                let messages = if lang == "zh-CN" {
505                    match marker_type {
506                        "FIXME" => vec![
507                            format!("FIXME: 这里有已知问题需要修复",),
508                            format!("发现 FIXME 标记,请尽快处理",),
509                            format!("FIXME 警告:代码有缺陷待修复",),
510                        ],
511                        "BUG" => vec![
512                            format!("🐛 BUG: 这里确认有 bug!",),
513                            format!("发现 BUG 标记,这可不是好兆头",),
514                            format!("BUG 标记:生产环境前必须修复!",),
515                        ],
516                        "HACK" => vec![
517                            format!("⚡ HACK: 这是一个 workaround,需要重构",),
518                            format!("发现 HACK 标记,临时方案该清理了",),
519                            format!("HACK 警告:这里的技术债该还了",),
520                        ],
521                        _ => vec![format!("{} 标记需要关注", marker_type)],
522                    }
523                } else {
524                    match marker_type {
525                        "FIXME" => vec![
526                            format!("FIXME: Known issue needs fixing",),
527                            format!("FIXME found - please address soon",),
528                            format!("FIXME alert: Code defect pending fix",),
529                        ],
530                        "BUG" => vec![
531                            format!("🐛 BUG: Confirmed bug here!",),
532                            format!("BUG found - this isn't a good sign",),
533                            format!("BUG marker: Must fix before production!",),
534                        ],
535                        "HACK" => vec![
536                            format!("⚡ HACK: This is a workaround, needs refactoring",),
537                            format!("HACK found - time to clean up temporary solution",),
538                            format!("HACK alert: Technical debt to be paid",),
539                        ],
540                        _ => vec![format!("{} marker needs attention", marker_type)],
541                    }
542                };
543
544                let severity = match marker_type {
545                    "BUG" => Severity::Spicy,
546                    "FIXME" => Severity::Mild,
547                    _ => Severity::Mild,
548                };
549
550                issues.push(CodeIssue {
551                    file_path: file_path.to_path_buf(),
552                    line: line_num,
553                    column: 1,
554                    rule_name: format!("todo-{}", marker_type.to_lowercase()),
555                    message: messages[line_num % messages.len()].clone(),
556                    severity,
557                });
558            }
559        }
560
561        issues
562    }
563}
564
565// ============================================================================
566// Panic abuse detection
567// ============================================================================
568
569struct PanicAbuseVisitor {
570    file_path: std::path::PathBuf,
571    issues: Vec<CodeIssue>,
572    lang: String,
573}
574
575impl PanicAbuseVisitor {
576    fn new(file_path: std::path::PathBuf, lang: &str) -> Self {
577        Self {
578            file_path,
579            issues: Vec::new(),
580            lang: lang.to_string(),
581        }
582    }
583
584    fn add_excessive_panic_issue(&mut self, count: usize, line: usize) {
585        let messages = if self.lang == "zh-CN" {
586            vec![
587                format!("{} 个 panic!?你的程序是定时炸弹吗?", count),
588                format!("这么多 panic!,用户体验堪忧",),
589                format!("{} 个 panic!,建议学学错误处理", count),
590                format!("panic! 用得这么随意,Rust 编译器都要哭了",),
591            ]
592        } else {
593            vec![
594                format!("{} panic!s? Is your program a time bomb?", count),
595                format!("So many panic!s, user experience is questionable"),
596                format!("{} panic!s - time to learn error handling", count),
597                format!("Using panic! so casually, even Rust compiler is crying"),
598            ]
599        };
600
601        self.issues.push(CodeIssue {
602            file_path: self.file_path.clone(),
603            line,
604            column: 1,
605            rule_name: "panic-abuse".to_string(),
606            message: messages[count % messages.len()].clone(),
607            severity: Severity::Nuclear,
608        });
609    }
610}
611
612impl<'ast> Visit<'ast> for PanicAbuseVisitor {
613    fn visit_expr_macro(&mut self, expr_macro: &'ast ExprMacro) {
614        if let Some(ident) = expr_macro.mac.path.get_ident() {
615            if ident == "panic" {
616                let messages = if self.lang == "zh-CN" {
617                    vec![
618                        "发现一个 panic!,程序要爆炸了",
619                        "panic! 出现,用户体验-1",
620                        "又见 panic!,优雅的错误处理在哪里?",
621                        "panic! 大法好,但是用户不这么想",
622                    ]
623                } else {
624                    vec![
625                        "Found a panic! - program is about to explode",
626                        "panic! spotted, user experience -1",
627                        "Another panic! - where's the graceful error handling?",
628                        "panic! is great, but users disagree",
629                    ]
630                };
631
632                let (line, column) = get_position(expr_macro);
633                self.issues.push(CodeIssue {
634                    file_path: self.file_path.clone(),
635                    line,
636                    column,
637                    rule_name: "panic-abuse".to_string(),
638                    message: messages[self.issues.len() % messages.len()].to_string(),
639                    severity: Severity::Spicy,
640                });
641            }
642        }
643        syn::visit::visit_expr_macro(self, expr_macro);
644    }
645}