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
9pub 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 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 let total_println = count_non_comment_matches(content, "println!");
37 let total_eprintln = count_non_comment_matches(content, "eprintln!");
38
39 let debug_patterns = [
41 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 r#"println!("{:?}"#, r#"println!("{:#?}"#, r#"println!("x = "#, r#"println!("val:"#, r#"println!("result:"#, r#"println!("res:"#, r#"println!("i = "#, r#"println!("j = "#, r#"println!("k = "#, r#"println!("count"#, r#"println!("len("#, r#"println!("size"#, r#"println!("")"#,
68 r#"println!()"#,
69 r#"println!("{:?"#, r#"println!("vec!"#, ];
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 let output_patterns = [
81 r#"eprintln!("Error"#,
83 r#"eprintln!("Warning"#,
84 r#"eprintln!("Failed"#,
85 r#"eprintln!("error:"#,
86 r#"eprintln!("warn:"#,
87 r#"println!("📊"#, r#"println!("🏆"#, r#"println!("🗑️"#, r#"println!("✅"#, r#"println!("❌"#, r#"println!("⚠️"#, r#"println!("🎓"#, r#"println!("💡"#, r#"println!("🔥"#, r#"println!("📍"#, r#"println!("🔍"#, r#"println!("⏱️"#, r#"println!("💾"#, r#"println!("📝"#, r#"println!("🎯"#, r#"println!("🚀"#, r#"println!("✨"#, r#"println!("🎨"#, r#"println!("📈"#, r#"println!("─"#, r#"println!("{}", "─"#, r#"serde_json::to"#,
111 r#"println!("{{"#, r#"Total files"#,
114 r#"issues found"#,
115 r#"analyzed"#,
116 r#"score"#,
117 r#"result"#,
118 r#"Usage:"#, r#"Arguments:"#, r#"Options:"#, r#"Version:"#, r#"Help:"#, r#"Example:"#, r#"Note:"#, r#"Tip:"#, r#"Warning:"#, r#"Error:"#, r#"Success:"#, r#"Failed:"#, r#"Completed:"#, r#"Started:"#, r#"Finished:"#, r#"Processing:"#, r#"Loading:"#, r#"Saving:"#, r#"Reading:"#, r#"Writing:"#, r#"Found "#, r#"Missing "#, r#"Invalid "#, r#"Unknown "#, r#"| "#, r#"- ─"#, r#"%"#, r#"/"#, r#"ms)"#, r#"seconds)"#, r#"minutes)"#, ];
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 let suspicious_count = total_println
161 .saturating_add(total_eprintln)
162 .saturating_sub(debug_count)
163 .saturating_sub(output_count);
164
165 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 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 let weight = context.rule_weight_multiplier();
266 if weight < 0.5 {
267 return Vec::new();
268 }
269
270 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 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
285pub 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 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
320pub 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 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 let mut comment_todos = Vec::new(); for (line_num, line) in content.lines().enumerate() {
352 let trimmed = line.trim();
353
354 if let Some(comment_start) = trimmed.find("//") {
356 let comment_content = &trimmed[comment_start + 2..].trim();
357
358 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 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 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 for &(line_num, marker_type) in &comment_todos {
501 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
565struct 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}