1use std::path::Path;
2use syn::{visit::Visit, ExprLit, File, ItemFn, Lit};
3
4use crate::analyzer::{CodeIssue, Severity};
5use crate::context::FileContext;
6use crate::rules::Rule;
7use crate::utils::get_position;
8
9enum MagicNumberCategory {
15 Timeout,
17 BufferSize,
19 PortNumber,
21 Threshold,
23 General,
25}
26
27pub struct MagicNumberRule;
29
30impl Rule for MagicNumberRule {
31 fn name(&self) -> &'static str {
32 "magic-number"
33 }
34
35 fn check(
36 &self,
37 file_path: &Path,
38 syntax_tree: &File,
39 _content: &str,
40 lang: &str,
41 is_test_file: bool,
42 ) -> Vec<CodeIssue> {
43 if is_test_file {
44 return Vec::new();
45 }
46
47 let mut visitor = MagicNumberVisitor::new(file_path.to_path_buf(), lang);
48 visitor.visit_file(syntax_tree);
49 visitor.issues
50 }
51
52 fn check_with_context(
53 &self,
54 file_path: &Path,
55 syntax_tree: &File,
56 content: &str,
57 lang: &str,
58 is_test_file: bool,
59 context: &FileContext,
60 _config: &crate::context::ProjectConfig,
61 ) -> Vec<CodeIssue> {
62 let weight = context.rule_weight_multiplier();
64 if weight < 0.3 {
65 return Vec::new();
66 }
67
68 let issues = self.check(file_path, syntax_tree, content, lang, is_test_file);
70
71 let path_str = file_path.to_string_lossy().to_lowercase();
73 let is_ui_file = path_str.contains("ui")
74 || path_str.contains("tui")
75 || path_str.contains("gui")
76 || path_str.contains("view")
77 || path_str.contains("screen")
78 || path_str.contains("layout")
79 || path_str.contains("display")
80 || path_str.contains("render");
81
82 if matches!(context, FileContext::Business) && is_ui_file {
84 return issues
85 .into_iter()
86 .filter(|issue| matches!(issue.severity, Severity::Spicy | Severity::Nuclear))
87 .collect();
88 }
89
90 issues
91 }
92}
93
94pub struct GodFunctionRule;
96
97impl Rule for GodFunctionRule {
98 fn name(&self) -> &'static str {
99 "god-function"
100 }
101
102 fn check(
103 &self,
104 file_path: &Path,
105 syntax_tree: &File,
106 content: &str,
107 lang: &str,
108 is_test_file: bool,
109 ) -> Vec<CodeIssue> {
110 if is_test_file {
111 return Vec::new();
112 }
113 let mut visitor = GodFunctionVisitor::new(file_path.to_path_buf(), content, lang);
114 visitor.visit_file(syntax_tree);
115 visitor.issues
116 }
117}
118
119pub struct CommentedCodeRule;
121
122impl Rule for CommentedCodeRule {
123 fn name(&self) -> &'static str {
124 "commented-code"
125 }
126
127 fn check(
128 &self,
129 file_path: &Path,
130 _syntax_tree: &File,
131 content: &str,
132 lang: &str,
133 is_test_file: bool,
134 ) -> Vec<CodeIssue> {
135 if is_test_file {
136 return Vec::new();
137 }
138 let mut issues = Vec::new();
139 let lines: Vec<&str> = content.lines().collect();
140
141 let mut _commented_code_blocks = 0;
142 let mut current_block_size = 0;
143
144 for (line_num, line) in lines.iter().enumerate() {
145 let trimmed = line.trim();
146
147 if trimmed.starts_with("//") {
149 let comment_content = trimmed.trim_start_matches("//").trim();
150
151 if is_likely_code(comment_content) {
153 current_block_size += 1;
154 } else if current_block_size > 0 {
155 if current_block_size >= 3 {
157 _commented_code_blocks += 1;
158 issues.push(create_commented_code_issue(
159 file_path,
160 line_num + 1 - current_block_size,
161 current_block_size,
162 lang,
163 ));
164 }
165 current_block_size = 0;
166 }
167 } else if current_block_size > 0 {
168 if current_block_size >= 3 {
170 _commented_code_blocks += 1;
171 issues.push(create_commented_code_issue(
172 file_path,
173 line_num - current_block_size,
174 current_block_size,
175 lang,
176 ));
177 }
178 current_block_size = 0;
179 }
180 }
181
182 if current_block_size >= 3 {
184 issues.push(create_commented_code_issue(
185 file_path,
186 lines.len() - current_block_size,
187 current_block_size,
188 lang,
189 ));
190 }
191
192 issues
193 }
194}
195
196pub struct DeadCodeRule;
198
199impl Rule for DeadCodeRule {
200 fn name(&self) -> &'static str {
201 "dead-code"
202 }
203
204 fn check(
205 &self,
206 file_path: &Path,
207 _syntax_tree: &File,
208 content: &str,
209 lang: &str,
210 is_test_file: bool,
211 ) -> Vec<CodeIssue> {
212 if is_test_file {
213 return Vec::new();
214 }
215
216 let mut issues = Vec::new();
217 let lines: Vec<&str> = content.lines().collect();
218 let mut dead_code_start: Option<usize> = None;
219
220 for (line_num, line) in lines.iter().enumerate() {
221 let trimmed = line.trim();
222
223 if is_control_flow_terminator(trimmed) {
225 dead_code_start = Some(line_num + 1);
227 continue;
228 }
229
230 if dead_code_start.is_some() {
232 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") {
234 continue;
235 }
236
237 if trimmed == "}" || trimmed == "} else {" || trimmed == "} else if" {
239 dead_code_start = None;
241 continue;
242 }
243
244 let messages = if lang == "zh-CN" {
246 vec![
247 "发现死代码,这行永远不会执行",
248 "这行代码比我的社交生活还死",
249 "死代码警告:这里是代码的坟墓",
250 "这行代码已经去世了,建议删除",
251 "发现僵尸代码,需要清理",
252 ]
253 } else {
254 vec![
255 "Dead code detected - this line will never execute",
256 "This code is deader than my social life",
257 "Dead code alert: code graveyard found here",
258 "This line of code has passed away, consider removal",
259 "Zombie code detected, cleanup needed",
260 ]
261 };
262
263 issues.push(CodeIssue {
264 file_path: file_path.to_path_buf(),
265 line: line_num + 1,
266 column: 1,
267 rule_name: "dead-code".to_string(),
268 message: messages[line_num % messages.len()].to_string(),
269 severity: Severity::Mild,
270 });
271
272 dead_code_start = None;
274 }
275 }
276
277 issues
278 }
279}
280
281fn is_likely_code(content: &str) -> bool {
286 let code_patterns = [
288 "let ", "fn ", "if ", "else", "for ", "while ", "match ", "struct ", "enum ", "impl ",
289 "use ", "mod ", "return ", "break", "continue", "{", "}", "(", ")", "[", "]", ";", "=",
290 "==", "!=", "&&", "||", "->", "::",
291 ];
292
293 let rust_keywords = [
294 "pub", "const", "static", "mut", "ref", "move", "async", "await", "unsafe", "extern",
295 "crate",
296 ];
297
298 let pattern_count = code_patterns
300 .iter()
301 .filter(|&&pattern| content.contains(pattern))
302 .count();
303
304 let keyword_count = rust_keywords
305 .iter()
306 .filter(|&&keyword| content.contains(keyword))
307 .count();
308
309 pattern_count >= 2 || keyword_count >= 1
310}
311
312fn create_commented_code_issue(
313 file_path: &Path,
314 line: usize,
315 block_size: usize,
316 lang: &str,
317) -> CodeIssue {
318 let messages = if lang == "zh-CN" {
319 vec![
320 format!("发现 {} 行被注释的代码,是舍不得删除吗?", block_size),
321 format!("{} 行注释代码,版本控制系统不香吗?", block_size),
322 format!("这 {} 行注释代码就像前任,该放手就放手", block_size),
323 format!("{} 行死代码注释,建议断舍离", block_size),
324 format!("注释了 {} 行代码,Git 会记住它们的", block_size),
325 ]
326 } else {
327 vec![
328 format!(
329 "Found {} lines of commented code - can't let go?",
330 block_size
331 ),
332 format!(
333 "{} lines of commented code - isn't version control enough?",
334 block_size
335 ),
336 format!(
337 "These {} commented lines are like an ex - time to let go",
338 block_size
339 ),
340 format!(
341 "{} lines of dead commented code - Marie Kondo would disapprove",
342 block_size
343 ),
344 format!(
345 "Commented {} lines of code - Git remembers them anyway",
346 block_size
347 ),
348 ]
349 };
350
351 let severity = if block_size > 10 {
352 Severity::Spicy
353 } else {
354 Severity::Mild
355 };
356
357 CodeIssue {
358 file_path: file_path.to_path_buf(),
359 line,
360 column: 1,
361 rule_name: "commented-code".to_string(),
362 message: messages[block_size % messages.len()].clone(),
363 severity,
364 }
365}
366
367fn is_control_flow_terminator(line: &str) -> bool {
368 let exact_matches = matches!(
372 line,
373 "return;"
374 | "break;"
375 | "continue;"
376 | "unreachable!()"
377 | "unreachable!();"
378 | "std::process::exit(0);"
379 | "std::process::exit(1);"
380 );
381
382 let has_return_statement =
383 line.starts_with("return ") && line.ends_with(';') && !line.contains("//");
384
385 let has_panic = line.starts_with("panic!(") && line.ends_with(';');
386
387 let has_unreachable = line.starts_with("unreachable!(") && line.ends_with(')');
388
389 exact_matches || has_return_statement || has_panic || has_unreachable
390}
391
392struct MagicNumberVisitor {
397 file_path: std::path::PathBuf,
398 issues: Vec<CodeIssue>,
399 lang: String,
400}
401
402impl MagicNumberVisitor {
403 fn new(file_path: std::path::PathBuf, lang: &str) -> Self {
404 Self {
405 file_path,
406 issues: Vec::new(),
407 lang: lang.to_string(),
408 }
409 }
410
411 fn is_magic_number(&self, value: i64) -> bool {
412 let safe_numbers = [
415 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 16, 20, 24, 32, 64, 100, 128, 256, 365, 512, 1024, 2048, 4096, 8192, 65536, 100000, 1000000, ];
446
447 !safe_numbers.contains(&value)
448 }
449
450 fn create_magic_number_issue(&self, value: i64, line: usize, column: usize) -> CodeIssue {
451 let category = self.categorize_magic_number(value);
453
454 let messages = if self.lang == "zh-CN" {
455 match category {
456 MagicNumberCategory::Timeout => vec![
457 format!("超时值 {}?建议定义为常量如 TIMEOUT_MS", value),
458 format!("硬编码超时 {}ms,维护性-1,建议用常量", value),
459 format!("魔法数字 {} 看起来像超时,提取为命名常量", value),
460 ],
461 MagicNumberCategory::BufferSize => vec![
462 format!("缓冲区大小 {}?这是什么咒语?", value),
463 format!("硬编码缓冲区 {},维护性-1", value),
464 format!("缓冲区大小 {} 从天而降,没人知道它的含义", value),
465 ],
466 MagicNumberCategory::PortNumber => vec![
467 format!("端口号 {}?硬编码端口不安全且难以维护", value),
468 format!("发现硬编码端口 {},建议使用配置文件或环境变量", value),
469 format!("端口号 {} 应该定义为常量或从配置读取", value),
470 ],
471 MagicNumberCategory::Threshold => vec![
472 format!("阈值 {}?这个数字有什么特殊含义?", value),
473 format!("硬编码阈值 {},建议定义为有意义的常量", value),
474 format!("阈值 {} 缺乏语义,维护者会困惑", value),
475 ],
476 MagicNumberCategory::General => vec![
477 format!("魔法数字 {}?这是什么咒语?", value),
478 format!("硬编码数字 {},维护性-1", value),
479 format!("数字 {} 从天而降,没人知道它的含义", value),
480 format!("魔法数字 {},建议定义为常量", value),
481 format!("看到数字 {},我陷入了沉思", value),
482 ],
483 }
484 } else {
485 match category {
486 MagicNumberCategory::Timeout => vec![
487 format!("Timeout value {}? Define as TIMEOUT_MS constant", value),
488 format!("Hardcoded timeout {}ms - extract to constant", value),
489 format!("Magic number {} looks like a timeout, name it", value),
490 ],
491 MagicNumberCategory::BufferSize => vec![
492 format!("Buffer size {}? What spell is this?", value),
493 format!("Hardcoded buffer {} - maintainability -1", value),
494 format!("Buffer size {} fell from sky, no meaning", value),
495 ],
496 MagicNumberCategory::PortNumber => vec![
497 format!("Port {}? Hardcoded ports are unmaintainable", value),
498 format!("Hardcoded port {} - use config or env var", value),
499 format!("Port {} should be constant or config-driven", value),
500 ],
501 MagicNumberCategory::Threshold => vec![
502 format!("Threshold {}? What's special about this?", value),
503 format!("Hardcoded threshold {} - define meaningful const", value),
504 format!("Threshold {} lacks semantics, confusing", value),
505 ],
506 MagicNumberCategory::General => vec![
507 format!("Magic number {}? What spell is this?", value),
508 format!("Hardcoded number {} - maintainability -1", value),
509 format!(
510 "Number {} fell from the sky, nobody knows its meaning",
511 value
512 ),
513 format!("Magic number {} - consider defining as a constant", value),
514 format!("Seeing number {}, I'm lost in thought", value),
515 ],
516 }
517 };
518
519 let severity = match category {
521 MagicNumberCategory::Timeout | MagicNumberCategory::Threshold => {
522 if value > 1000 {
523 Severity::Spicy
524 } else {
525 Severity::Mild
526 }
527 }
528 MagicNumberCategory::PortNumber => Severity::Spicy, _ => {
530 if !(-100..=100).contains(&value)
531 && value != 800
532 && value != 1000
533 && value != 2000
534 && value != 3000
535 && value != 5000
536 {
537 Severity::Spicy
538 } else {
539 Severity::Mild
540 }
541 }
542 };
543
544 CodeIssue {
545 file_path: self.file_path.clone(),
546 line,
547 column,
548 rule_name: "magic-number".to_string(),
549 message: messages[self.issues.len() % messages.len()].clone(),
550 severity,
551 }
552 }
553
554 fn categorize_magic_number(&self, value: i64) -> MagicNumberCategory {
555 if [
557 100, 200, 300, 500, 800, 1000, 1500, 2000, 3000, 5000, 10000, 30000, 60000,
558 ]
559 .contains(&value)
560 {
561 return MagicNumberCategory::Timeout;
562 }
563
564 if [1024, 2048, 4096, 8192, 16384, 32768, 65536].contains(&value) {
566 return MagicNumberCategory::BufferSize;
567 }
568
569 if (3000..=9999).contains(&value) || value == 80 || value == 443 || value == 8080 {
571 return MagicNumberCategory::PortNumber;
572 }
573
574 if [
576 10, 25, 50, 75, 90, 95, 99, 100, 150, 200, 250, 500, 750, 1000,
577 ]
578 .contains(&value)
579 {
580 return MagicNumberCategory::Threshold;
581 }
582
583 MagicNumberCategory::General
584 }
585}
586
587impl<'ast> Visit<'ast> for MagicNumberVisitor {
588 fn visit_expr_lit(&mut self, expr_lit: &'ast ExprLit) {
589 if let Lit::Int(lit_int) = &expr_lit.lit {
590 if let Ok(value) = lit_int.base10_parse::<i64>() {
591 if self.is_magic_number(value) {
592 let (line, column) = get_position(expr_lit);
593 self.issues
594 .push(self.create_magic_number_issue(value, line, column));
595 }
596 }
597 }
598 syn::visit::visit_expr_lit(self, expr_lit);
599 }
600}
601
602struct GodFunctionVisitor {
607 file_path: std::path::PathBuf,
608 issues: Vec<CodeIssue>,
609 _content: String,
610 lang: String,
611}
612
613impl GodFunctionVisitor {
614 fn new(file_path: std::path::PathBuf, content: &str, lang: &str) -> Self {
615 Self {
616 file_path,
617 issues: Vec::new(),
618 _content: content.to_string(),
619 lang: lang.to_string(),
620 }
621 }
622
623 fn analyze_function_complexity(&mut self, func: &ItemFn) {
624 let func_name = func.sig.ident.to_string();
625
626 let mut complexity_score = 0;
628
629 let param_count = func.sig.inputs.len();
631 if param_count > 5 {
632 complexity_score += (param_count - 5) * 2;
633 }
634
635 let func_str = format!("{func:?}");
637 let line_count = func_str.lines().count();
638 if line_count > 50 {
639 complexity_score += (line_count - 50) / 10;
640 }
641
642 let control_keywords = ["if", "else", "for", "while", "match", "loop"];
644 for keyword in &control_keywords {
645 complexity_score += func_str.matches(keyword).count();
646 }
647
648 if complexity_score > 15 {
650 let messages = if self.lang == "zh-CN" {
651 vec![
652 format!("函数 '{}' 做的事情比我一天做的还多", func_name),
653 format!("'{}' 是上帝函数吗?什么都想管", func_name),
654 format!("函数 '{}' 复杂得像我的感情生活", func_name),
655 format!("'{}' 这个函数需要拆分,太臃肿了", func_name),
656 format!("函数 '{}' 违反了单一职责原则", func_name),
657 ]
658 } else {
659 vec![
660 format!(
661 "Function '{}' does more things than I do in a day",
662 func_name
663 ),
664 format!(
665 "Is '{}' a god function? Wants to control everything",
666 func_name
667 ),
668 format!("Function '{}' is as complex as my love life", func_name),
669 format!("Function '{}' needs to be split - too bloated", func_name),
670 format!(
671 "Function '{}' violates single responsibility principle",
672 func_name
673 ),
674 ]
675 };
676
677 let severity = if complexity_score > 25 {
678 Severity::Spicy
679 } else {
680 Severity::Mild
681 };
682
683 let (line, column) = get_position(func);
684 self.issues.push(CodeIssue {
685 file_path: self.file_path.clone(),
686 line,
687 column,
688 rule_name: "god-function".to_string(),
689 message: messages[self.issues.len() % messages.len()].clone(),
690 severity,
691 });
692 }
693 }
694}
695
696impl<'ast> Visit<'ast> for GodFunctionVisitor {
697 fn visit_item_fn(&mut self, func: &'ast ItemFn) {
698 self.analyze_function_complexity(func);
699 syn::visit::visit_item_fn(self, func);
700 }
701}