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