Skip to main content

garbage_code_hunter/rules/
code_smells.rs

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
9// ============================================================================
10// Magic number detection
11// ============================================================================
12
13/// Categories of magic numbers for better error messages
14enum MagicNumberCategory {
15    /// Timeout values in milliseconds (100, 500, 1000, etc.)
16    Timeout,
17    /// Buffer sizes (1024, 2048, 4096, etc.)
18    BufferSize,
19    /// Network port numbers (80, 443, 3000, 8080, etc.)
20    PortNumber,
21    /// Threshold/limit values (10, 50, 100, etc.)
22    Threshold,
23    /// Unclassified magic numbers
24    General,
25}
26
27/// Detect magic numbers (hardcoded numeric constants)
28pub 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        // Test/Documentation/Benchmark: 完全跳过
63        let weight = context.rule_weight_multiplier();
64        if weight < 0.3 {
65            return Vec::new();
66        }
67
68        // 获取所有问题
69        let issues = self.check(file_path, syntax_tree, content, lang, is_test_file);
70
71        // UI/TUI 文件过滤:检测到 UI 相关路径时,仅保留严重问题
72        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        // 如果是 UI 上下文或 UI 文件,过滤掉 Mild 级别的 magic-number 问题
83        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
94/// Detect functions that do too much (god functions)
95pub 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
119/// Detect commented-out code blocks
120pub 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            // Detect commented code lines
148            if trimmed.starts_with("//") {
149                let comment_content = trimmed.trim_start_matches("//").trim();
150
151                // Check if it looks like code (contains common code patterns)
152                if is_likely_code(comment_content) {
153                    current_block_size += 1;
154                } else if current_block_size > 0 {
155                    // End a code block
156                    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                // Non-comment line, end current block
169                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        // Handle code block at end of file
183        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
196/// Detect obvious dead code
197pub 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            // Check if this line is a control flow terminator
224            if is_control_flow_terminator(trimmed) {
225                // Mark that subsequent lines are dead code
226                dead_code_start = Some(line_num + 1);
227                continue;
228            }
229
230            // If we're in a dead code region and this line has actual code
231            if dead_code_start.is_some() {
232                // Skip empty lines and comments
233                if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") {
234                    continue;
235                }
236
237                // Check if this line closes a block (could be end of if/match/etc)
238                if trimmed == "}" || trimmed == "} else {" || trimmed == "} else if" {
239                    // Reset dead code region - we've exited the block
240                    dead_code_start = None;
241                    continue;
242                }
243
244                // This is actual dead code after a terminator
245                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                // Only report one dead code block per terminator
273                dead_code_start = None;
274            }
275        }
276
277        issues
278    }
279}
280
281// ============================================================================
282// Helper functions
283// ============================================================================
284
285fn is_likely_code(content: &str) -> bool {
286    // Patterns that look like code
287    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    // If it contains multiple code patterns, it's likely code
299    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    // Check if this line is a pure control flow terminator
369    // (the line itself terminates execution, not just contains these keywords)
370    // Explicit parentheses used to clarify operator precedence (&& binds tighter than ||)
371    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
392// ============================================================================
393// Visitor implementations
394// ============================================================================
395
396struct 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        // Common NON-magic numbers (powers of 2, common sizes, small counts, etc.)
413        // These are so common they're almost always intentional
414        let safe_numbers = [
415            -1,      // Sentinel/error value
416            0,       // Zero/initial/default
417            1,       // Single/first/true
418            2,       // Pair/dual/binary
419            3,       // Triple/RGB/xyz
420            4,       // Quad/nibble
421            5,       // Quint/count
422            6,       // Hex/week
423            7,       // Week/oct
424            8,       // Byte/octet
425            9,       // Max digit
426            10,      // Decimal base
427            12,      // Dozen/months
428            16,      // Nibble/4 bits
429            20,      // Score
430            24,      // Hours/day
431            32,      // 5 bits
432            64,      // 6 bits
433            100,     // Percent/century
434            128,     // 7 bits
435            256,     // Byte/8 bits
436            365,     // Days/year (approx)
437            512,     // 9 bits
438            1024,    // KB/10 bits
439            2048,    // 11 bits
440            4096,    // 12 bits / page size
441            8192,    // 13 bits
442            65536,   // u16 max / 16 bits
443            100000,  // 100K
444            1000000, // 1M
445        ];
446
447        !safe_numbers.contains(&value)
448    }
449
450    fn create_magic_number_issue(&self, value: i64, line: usize, column: usize) -> CodeIssue {
451        // Categorize the magic number for better messages
452        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        // Severity based on value magnitude and category
520        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, // Ports are always important to name
529            _ => {
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        // Common timeout values (milliseconds)
556        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        // Common buffer sizes
565        if [1024, 2048, 4096, 8192, 16384, 32768, 65536].contains(&value) {
566            return MagicNumberCategory::BufferSize;
567        }
568
569        // Common port numbers
570        if (3000..=9999).contains(&value) || value == 80 || value == 443 || value == 8080 {
571            return MagicNumberCategory::PortNumber;
572        }
573
574        // Common threshold values
575        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
602// ============================================================================
603// God function detection
604// ============================================================================
605
606struct 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        // Calculate various complexity metrics for the function
627        let mut complexity_score = 0;
628
629        // 1. Parameter count
630        let param_count = func.sig.inputs.len();
631        if param_count > 5 {
632            complexity_score += (param_count - 5) * 2;
633        }
634
635        // 2. Function body size (estimated via string analysis)
636        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        // 3. Nesting depth and control flow complexity
643        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 is too high, report the issue
649        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}