Skip to main content

garbage_code_hunter/treesitter/rules/
complex_rules.rs

1use regex::Regex;
2
3use crate::analyzer::{CodeIssue, Severity};
4use crate::language::Language;
5use crate::treesitter::engine::ParsedFile;
6use crate::treesitter::query::collect_captures;
7use crate::treesitter::rule::TreeSitterRule;
8
9use super::base_rules::{
10    closure_depth, count_descendants_of_types, count_parameters, find_function_name, BLOCK_KINDS,
11    BLOCK_PARENT_TYPES,
12};
13
14// ============================================================================
15// Deep nesting detection
16// ============================================================================
17
18pub(crate) struct DeepNestingRule;
19
20impl TreeSitterRule for DeepNestingRule {
21    fn name(&self) -> &'static str {
22        "deep-nesting"
23    }
24
25    fn supported_languages(&self) -> &'static [Language] {
26        crate::language::LANGUAGES_WITH_GRAMMAR
27    }
28
29    fn skips_test_files(&self) -> bool {
30        false
31    }
32
33    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
34        let mut issues = Vec::new();
35        let root = file.root_node();
36        check_nesting_recursive(file, root, 0, &mut issues);
37        issues
38    }
39}
40
41fn check_nesting_recursive(
42    file: &ParsedFile,
43    node: tree_sitter::Node,
44    depth: usize,
45    issues: &mut Vec<CodeIssue>,
46) {
47    let node_type = node.kind();
48    let is_block_parent = BLOCK_PARENT_TYPES.contains(&node_type);
49    let is_block_kind = BLOCK_KINDS.contains(&node_type);
50
51    let new_depth = if is_block_kind && depth > 0 {
52        // This block is nested inside a control flow structure
53        if depth > 5 {
54            let messages = [
55                "Nesting deeper than Russian dolls, are you writing a maze?",
56                "Nesting so deep, trying to dig to the Earth's core?",
57                "Code nested like an onion, makes me want to cry",
58                "Nesting level exceeded! Consider refactoring",
59                "This nesting depth could apply for a Guinness World Record",
60            ];
61            let severity = if depth > 8 {
62                Severity::Nuclear
63            } else if depth > 6 {
64                Severity::Spicy
65            } else {
66                Severity::Mild
67            };
68            let pos = node.start_position();
69            issues.push(CodeIssue {
70                file_path: file.path.clone(),
71                line: pos.row + 1,
72                column: pos.column + 1,
73                rule_name: "deep-nesting".to_string(),
74                message: format!(
75                    "{} (nesting depth: {})",
76                    messages[issues.len() % messages.len()],
77                    depth
78                ),
79                severity,
80            });
81        }
82        depth
83    } else if is_block_parent {
84        depth + 1
85    } else {
86        depth
87    };
88
89    let mut cursor = node.walk();
90    for child in node.children(&mut cursor) {
91        check_nesting_recursive(file, child, new_depth, issues);
92    }
93}
94
95fn function_query(lang: Language) -> &'static str {
96    match lang {
97        Language::Rust => "(function_item) @fn",
98        Language::Python => "(function_definition) @fn",
99        Language::JavaScript => "(function_declaration) @fn",
100        Language::C | Language::Cpp => "(function_definition) @fn",
101        _ => "(function_item) @fn",
102    }
103}
104
105// ============================================================================
106// Long function detection
107// ============================================================================
108
109pub(crate) struct LongFunctionRule;
110
111impl TreeSitterRule for LongFunctionRule {
112    fn name(&self) -> &'static str {
113        "long-function"
114    }
115
116    fn supported_languages(&self) -> &'static [Language] {
117        crate::language::LANGUAGES_WITH_GRAMMAR
118    }
119
120    fn skips_test_files(&self) -> bool {
121        false
122    }
123
124    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
125        let query = function_query(file.language);
126        let captures = match collect_captures(file, query) {
127            Ok(c) => c,
128            Err(_) => return vec![],
129        };
130
131        let is_test = file.path.to_string_lossy().contains("test");
132        let threshold: u32 = if is_test { 150 } else { 80 };
133        let content_bytes = file.content.as_bytes();
134        let mut issues = Vec::new();
135
136        for group in &captures {
137            if let Some(cap) = group.first() {
138                let node = cap.node;
139                let start_line = node.start_position().row as u32;
140                let end_line = node.end_position().row as u32;
141                let line_count = end_line.saturating_sub(start_line) + 1;
142
143                if line_count > threshold {
144                    let func_name = find_function_name(node, content_bytes);
145                    let messages = [
146                        "Function '{}' has {} lines? This isn't a function, it's a novel!",
147                        "'{}' function is {} lines long, consider splitting into smaller functions",
148                        "Function '{}' is longer than my patience ({} lines), consider refactoring",
149                    ];
150                    let severity = if line_count > threshold * 2 {
151                        Severity::Nuclear
152                    } else if line_count > threshold + threshold / 2 {
153                        Severity::Spicy
154                    } else {
155                        Severity::Mild
156                    };
157                    let pos = cap.node.start_position();
158                    issues.push(CodeIssue {
159                        file_path: file.path.clone(),
160                        line: pos.row + 1,
161                        column: pos.column + 1,
162                        rule_name: "long-function".to_string(),
163                        message: format!(
164                            "{} ({} lines)",
165                            messages[issues.len() % messages.len()].replace("'{}'", &func_name),
166                            line_count
167                        ),
168                        severity,
169                    });
170                }
171            }
172        }
173
174        issues
175    }
176}
177
178// ============================================================================
179// God function detection
180// ============================================================================
181
182pub(crate) struct GodFunctionRule;
183
184impl TreeSitterRule for GodFunctionRule {
185    fn name(&self) -> &'static str {
186        "god-function"
187    }
188
189    fn supported_languages(&self) -> &'static [Language] {
190        crate::language::LANGUAGES_WITH_GRAMMAR
191    }
192
193    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
194        let query = function_query(file.language);
195        let captures = match collect_captures(file, query) {
196            Ok(c) => c,
197            Err(_) => return vec![],
198        };
199
200        let control_flow_types = [
201            "if_expression",
202            "for_expression",
203            "while_expression",
204            "match_expression",
205            "loop_expression",
206        ];
207
208        let content_bytes = file.content.as_bytes();
209        let mut issues = Vec::new();
210
211        for group in &captures {
212            if let Some(cap) = group.first() {
213                let node = cap.node;
214                let mut score: u32 = 0;
215
216                // Line count
217                let start_line = node.start_position().row as u32;
218                let end_line = node.end_position().row as u32;
219                let line_count = end_line.saturating_sub(start_line) + 1;
220                if line_count > 50 {
221                    score += (line_count - 50) / 10;
222                }
223
224                // Parameter count
225                let param_count = count_parameters(node);
226                if param_count > 5 {
227                    score += (param_count - 5) * 2;
228                }
229
230                // Control flow nodes
231                score += count_descendants_of_types(node, &control_flow_types);
232
233                if score > 15 {
234                    let func_name = find_function_name(node, content_bytes);
235                    let messages = [
236                        "Function '{}' does more things than I do in a day",
237                        "Is '{}' a god function? Wants to control everything",
238                        "Function '{}' is as complex as my love life",
239                        "Function '{}' needs to be split - too bloated",
240                        "Function '{}' violates single responsibility principle",
241                    ];
242                    let severity = if score > 25 {
243                        Severity::Spicy
244                    } else {
245                        Severity::Mild
246                    };
247                    let pos = cap.node.start_position();
248                    issues.push(CodeIssue {
249                        file_path: file.path.clone(),
250                        line: pos.row + 1,
251                        column: pos.column + 1,
252                        rule_name: "god-function".to_string(),
253                        message: format!(
254                            "{} (score: {})",
255                            messages[issues.len() % messages.len()].replace("'{}'", &func_name),
256                            score
257                        ),
258                        severity,
259                    });
260                }
261            }
262        }
263
264        issues
265    }
266}
267
268// ============================================================================
269// Complex closure detection
270// ============================================================================
271
272pub(crate) struct ComplexClosureRule;
273
274impl TreeSitterRule for ComplexClosureRule {
275    fn name(&self) -> &'static str {
276        "complex-closure"
277    }
278
279    fn supported_languages(&self) -> &'static [Language] {
280        &[Language::Rust]
281    }
282
283    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
284        let captures = match collect_captures(file, "(closure_expression) @closure") {
285            Ok(c) => c,
286            Err(_) => return vec![],
287        };
288
289        let mut issues = Vec::new();
290
291        for group in &captures {
292            if let Some(cap) = group.first() {
293                let closure_node = cap.node;
294
295                // Check nesting depth
296                let depth = closure_depth(closure_node);
297                if depth > 2 {
298                    let messages = [
299                        "Closures within closures? Are you writing Russian nesting dolls?",
300                        "Nested closures are more complex than my relationships",
301                        "These closures are nested like an onion - peel one layer, cry once",
302                        "Closures too deeply nested - consider splitting into separate functions",
303                    ];
304                    let pos = closure_node.start_position();
305                    issues.push(CodeIssue {
306                        file_path: file.path.clone(),
307                        line: pos.row + 1,
308                        column: pos.column + 1,
309                        rule_name: "complex-closure".to_string(),
310                        message: format!(
311                            "{} (depth: {})",
312                            messages[issues.len() % messages.len()],
313                            depth
314                        ),
315                        severity: Severity::Spicy,
316                    });
317                }
318
319                // Check parameter count
320                let param_count = closure_node
321                    .children(&mut closure_node.walk())
322                    .filter(|c| c.kind() == "closure_parameters")
323                    .map(|p| p.named_child_count() as u32)
324                    .next()
325                    .unwrap_or(0);
326
327                if param_count > 5 {
328                    let messages = [
329                        "This closure has more parameters than my excuses",
330                        "Too many parameters for a closure - are you sure this isn't a function?",
331                        "So many parameters - consider making this a proper function",
332                    ];
333                    let pos = closure_node.start_position();
334                    issues.push(CodeIssue {
335                        file_path: file.path.clone(),
336                        line: pos.row + 1,
337                        column: pos.column + 1,
338                        rule_name: "complex-closure".to_string(),
339                        message: messages[issues.len() % messages.len()].to_string(),
340                        severity: Severity::Mild,
341                    });
342                }
343            }
344        }
345
346        issues
347    }
348}
349
350// ============================================================================
351// Terrible naming detection (query + regex)
352// ============================================================================
353
354pub(crate) fn variable_name_query(lang: Language) -> &'static str {
355    match lang {
356        Language::Rust => "(let_declaration pattern: (identifier) @name)",
357        Language::Python => "(assignment left: (identifier) @name)",
358        Language::JavaScript => "(variable_declarator name: (identifier) @name)",
359        _ => "(identifier) @name",
360    }
361}
362
363pub(crate) struct TerribleNamingRule;
364
365impl TreeSitterRule for TerribleNamingRule {
366    fn name(&self) -> &'static str {
367        "terrible-naming"
368    }
369
370    fn supported_languages(&self) -> &'static [Language] {
371        crate::language::LANGUAGES_WITH_GRAMMAR
372    }
373
374    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
375        let query = variable_name_query(file.language);
376        let captures = match collect_captures(file, query) {
377            Ok(c) => c,
378            Err(_) => return vec![],
379        };
380
381        let Ok(terrible_re) = Regex::new(
382            r"^(data|info|temp|tmp|val|value|thing|stuff|obj|object|manager|handler|helper|util|utils)(\d+)?$",
383        ) else {
384            tracing::error!("Failed to compile terrible naming regex");
385            return vec![];
386        };
387
388        let mut issues = Vec::new();
389
390        for group in &captures {
391            if let Some(cap) = group.first() {
392                let name = cap.text;
393                if terrible_re.is_match(&name.to_lowercase()) {
394                    let msgs = [
395                        format!(
396                            "Variable '{}' - more abstract than my programming skills",
397                            name
398                        ),
399                        format!(
400                            "Variable '{}' - this name tells me you've given up on naming",
401                            name
402                        ),
403                        format!(
404                            "Variable '{}' - trying to make maintainers cry and quit?",
405                            name
406                        ),
407                        format!(
408                            "Variable '{}' - congrats on inventing the most meaningless identifier",
409                            name
410                        ),
411                        format!(
412                            "Variable '{}' - creativity level of naming a kid 'Child'",
413                            name
414                        ),
415                    ];
416                    let pos = cap.node.start_position();
417                    issues.push(CodeIssue {
418                        file_path: file.path.clone(),
419                        line: pos.row + 1,
420                        column: pos.column + 1,
421                        rule_name: "terrible-naming".to_string(),
422                        message: msgs[issues.len() % msgs.len()].clone(),
423                        severity: Severity::Spicy,
424                    });
425                }
426            }
427        }
428
429        issues
430    }
431}
432
433// ============================================================================
434// Single letter variable detection (query + exclude list)
435// ============================================================================
436
437fn single_letter_query(lang: Language) -> &'static str {
438    match lang {
439        Language::Rust => {
440            r#"(let_declaration pattern: (identifier) @var (#match? @var "^[a-z]$"))"#
441        }
442        Language::Python => r#"(assignment left: (identifier) @var (#match? @var "^[a-z]$"))"#,
443        Language::JavaScript => {
444            r#"(variable_declarator name: (identifier) @var (#match? @var "^[a-z]$"))"#
445        }
446        Language::Go => {
447            r#"(short_variable_declaration left: (identifier) @var (#match? @var "^[a-z]$"))"#
448        }
449        Language::Java => {
450            r#"(variable_declarator name: (identifier) @var (#match? @var "^[a-z]$"))"#
451        }
452        Language::Ruby => r#"(assignment left: (identifier) @var (#match? @var "^[a-z]$"))"#,
453        Language::C | Language::Cpp => {
454            r#"(init_declarator declarator: (identifier) @var (#match? @var "^[a-z]$"))"#
455        }
456        _ => r#"(identifier) @var"#,
457    }
458}
459
460/// Check if a node is a loop counter variable (not a body variable).
461/// For for-loops: the loop variable is the first named child.
462/// For while/loop/do-while: no loop variable exists.
463fn is_loop_counter(node: tree_sitter::Node) -> bool {
464    let mut current = node;
465    loop {
466        let kind = current.kind();
467        if matches!(kind, "for_expression" | "for_statement") {
468            // The loop variable is the first named child of the for node
469            // (Rust: pattern, C: declaration, JS/Go: initializer, Python: left)
470            if let Some(first) = current.named_child(0) {
471                let ns = node.start_byte();
472                let ne = node.end_byte();
473                let vs = first.start_byte();
474                let ve = first.end_byte();
475                if ns >= vs && ne <= ve {
476                    return true; // node IS the loop variable
477                }
478            }
479            return false; // not a loop variable (might be in body)
480        }
481        if matches!(
482            kind,
483            "while_statement" | "while_expression" | "loop_expression" | "do_statement"
484        ) {
485            return false; // these loops have no explicit counter variable
486        }
487        match current.parent() {
488            Some(p) => current = p,
489            None => return false,
490        }
491    }
492}
493
494/// Check if an identifier is a C++ template parameter.
495fn is_template_param(node: tree_sitter::Node) -> bool {
496    let mut parent = node.parent();
497    // Walk up to find template_parameter_declaration
498    while let Some(p) = parent {
499        if p.kind() == "template_parameter_declaration" || p.kind() == "type_parameter" {
500            return true;
501        }
502        // If we hit a function/class/struct boundary, stop
503        if matches!(
504            p.kind(),
505            "function_definition"
506                | "function_declaration"
507                | "class_specifier"
508                | "struct_specifier"
509                | "translation_unit"
510        ) {
511            break;
512        }
513        parent = p.parent();
514    }
515    false
516}
517
518pub(crate) struct SingleLetterTsRule;
519
520impl TreeSitterRule for SingleLetterTsRule {
521    fn name(&self) -> &'static str {
522        "single-letter-variable"
523    }
524
525    fn supported_languages(&self) -> &'static [Language] {
526        crate::language::LANGUAGES_WITH_GRAMMAR
527    }
528
529    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
530        let pattern = single_letter_query(file.language);
531        let captures = match collect_captures(file, pattern) {
532            Ok(c) => c,
533            Err(_) => return vec![],
534        };
535
536        // Loop counters are handled by inside_loop() check per-capture
537
538        let mut issues = Vec::new();
539
540        for group in &captures {
541            if let Some(cap) = group.first() {
542                let name = cap.text;
543                // Only flag single-character identifiers
544                if name.len() != 1 {
545                    continue;
546                }
547
548                // Skip identifiers that are loop counters (not body variables)
549                // Tree-based detection — no whitelist needed
550                if is_loop_counter(cap.node) {
551                    continue;
552                }
553
554                // Skip C++ template parameters
555                if file.language == Language::Cpp && is_template_param(cap.node) {
556                    continue;
557                }
558
559                let msgs = [
560                    format!(
561                        "Single-letter variable '{}'? Writing math formulas or torturing readers?",
562                        name
563                    ),
564                    format!(
565                        "Variable '{}'? Is this a name or did your keyboard break?",
566                        name
567                    ),
568                    format!(
569                        "Using '{}' as a variable name? You need a book on naming",
570                        name
571                    ),
572                    format!(
573                        "Single-letter variable '{}': harder to read than hieroglyphics",
574                        name
575                    ),
576                    format!("Variable '{}' has about as much info as a period", name),
577                ];
578                let pos = cap.node.start_position();
579                issues.push(CodeIssue {
580                    file_path: file.path.clone(),
581                    line: pos.row + 1,
582                    column: pos.column + 1,
583                    rule_name: "single-letter-variable".to_string(),
584                    message: msgs[issues.len() % msgs.len()].clone(),
585                    severity: Severity::Mild,
586                });
587            }
588        }
589
590        issues
591    }
592}
593
594// ============================================================================
595// Hungarian notation detection (query + regex)
596// ============================================================================
597
598pub(crate) struct HungarianNotationTsRule;
599
600impl TreeSitterRule for HungarianNotationTsRule {
601    fn name(&self) -> &'static str {
602        "hungarian-notation"
603    }
604
605    fn supported_languages(&self) -> &'static [Language] {
606        crate::language::LANGUAGES_WITH_GRAMMAR
607    }
608
609    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
610        let captures = match collect_captures(file, "(identifier) @id") {
611            Ok(c) => c,
612            Err(_) => return vec![],
613        };
614
615        let prefixes = [
616            "str", "int", "bool", "float", "double", "char", "arr", "vec", "list", "map", "set",
617        ];
618        let scope_prefixes = ["g_", "m_", "s_", "p_"];
619
620        let mut issues = Vec::new();
621
622        for group in &captures {
623            if let Some(cap) = group.first() {
624                let name = cap.text;
625                let is_hungarian = scope_prefixes.iter().any(|p| name.starts_with(p))
626                    || prefixes.iter().any(|&prefix| {
627                        name.starts_with(prefix)
628                            && name.len() > prefix.len()
629                            && name
630                                .chars()
631                                .nth(prefix.len())
632                                .is_some_and(|c| c.is_uppercase())
633                            && !name[prefix.len()..].starts_with("ify")
634                            && !name[prefix.len()..].starts_with("nal")
635                            && !name[prefix.len()..].starts_with("ean")
636                    });
637
638                if is_hungarian {
639                    let messages = [
640                        format!(
641                            "'{}' uses Hungarian notation? This isn't the 1990s anymore",
642                            name
643                        ),
644                        format!(
645                            "Seeing '{}' makes me nostalgic for the dark ages of C++",
646                            name
647                        ),
648                        format!(
649                            "'{}' - Hungarian notation is as outdated as my haircut",
650                            name
651                        ),
652                        format!(
653                            "Hungarian notation '{}'? Rust's type system has got you covered",
654                            name
655                        ),
656                    ];
657                    let pos = cap.node.start_position();
658                    issues.push(CodeIssue {
659                        file_path: file.path.clone(),
660                        line: pos.row + 1,
661                        column: pos.column + 1,
662                        rule_name: "hungarian-notation".to_string(),
663                        message: messages[issues.len() % messages.len()].clone(),
664                        severity: Severity::Mild,
665                    });
666                }
667            }
668        }
669
670        issues
671    }
672}
673
674// ============================================================================
675// Abbreviation abuse detection (query + lookup)
676// ============================================================================
677
678pub(crate) struct AbbreviationAbuseTsRule;
679
680impl TreeSitterRule for AbbreviationAbuseTsRule {
681    fn name(&self) -> &'static str {
682        "abbreviation-abuse"
683    }
684
685    fn supported_languages(&self) -> &'static [Language] {
686        crate::language::LANGUAGES_WITH_GRAMMAR
687    }
688
689    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
690        let captures = match collect_captures(file, "(identifier) @id") {
691            Ok(c) => c,
692            Err(_) => return vec![],
693        };
694
695        let bad_abbrevs: &[(&str, &str)] = &[
696            ("mgr", "manager"),
697            ("mngr", "manager"),
698            ("ctrl", "controller"),
699            ("hdlr", "handler"),
700            ("usr", "user"),
701            ("pwd", "password"),
702            ("prefs", "preferences"),
703            ("btn", "button"),
704            ("lbl", "label"),
705            ("pic", "picture"),
706            ("tbl", "table"),
707            ("col", "column"),
708            ("cnt", "count"),
709        ];
710
711        let mut issues = Vec::new();
712
713        for group in &captures {
714            if let Some(cap) = group.first() {
715                let name = cap.text;
716                let name_lower = name.to_lowercase();
717
718                for &(abbrev, full) in bad_abbrevs {
719                    if name_lower == abbrev || name_lower.starts_with(&format!("{}_", abbrev)) {
720                        let messages = [
721                            format!("'{}' is too abbreviated, consider '{}'", name, full),
722                            format!(
723                                "Seeing '{}' makes me feel like I'm decoding, just use '{}'",
724                                name, full
725                            ),
726                            format!(
727                                "'{}' saves a few letters but kills readability, use '{}'",
728                                name, full
729                            ),
730                        ];
731                        let pos = cap.node.start_position();
732                        issues.push(CodeIssue {
733                            file_path: file.path.clone(),
734                            line: pos.row + 1,
735                            column: pos.column + 1,
736                            rule_name: "abbreviation-abuse".to_string(),
737                            message: messages[issues.len() % messages.len()].clone(),
738                            severity: Severity::Mild,
739                        });
740                        break;
741                    }
742                }
743            }
744        }
745
746        issues
747    }
748}
749
750fn print_debug_query(lang: Language) -> &'static str {
751    match lang {
752        Language::Rust => r#"(macro_invocation macro: (identifier) @m (#eq? @m "println"))"#,
753        Language::Python => r#"(call function: (identifier) @f (#eq? @f "print"))"#,
754        Language::JavaScript => {
755            r#"(call_expression function: (member_expression object: (identifier) @o property: (property_identifier) @p) (#eq? @o "console") (#eq? @p "log"))"#
756        }
757        _ => "",
758    }
759}
760
761pub(crate) struct PrintlnDebuggingRule;
762
763impl TreeSitterRule for PrintlnDebuggingRule {
764    fn name(&self) -> &'static str {
765        "println-debugging"
766    }
767
768    fn supported_languages(&self) -> &'static [Language] {
769        crate::language::LANGUAGES_WITH_GRAMMAR
770    }
771
772    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
773        let query = print_debug_query(file.language);
774        if query.is_empty() {
775            return vec![];
776        }
777        let captures = match collect_captures(file, query) {
778            Ok(c) => c,
779            Err(_) => return vec![],
780        };
781        let mut issues = Vec::new();
782        for group in &captures {
783            if let Some(cap) = group.first() {
784                let pos = cap.node.start_position();
785                issues.push(CodeIssue {
786                    file_path: file.path.clone(),
787                    line: pos.row + 1,
788                    column: pos.column + 1,
789                    rule_name: "println-debugging".to_string(),
790                    message: format!("{} - use proper logging instead", cap.text),
791                    severity: Severity::Spicy,
792                });
793            }
794        }
795        issues
796    }
797}
798
799fn number_literal_query(lang: Language) -> &'static str {
800    match lang {
801        Language::Rust => "(integer_literal) @num",
802        Language::Python => "(integer) @num",
803        Language::JavaScript | Language::TypeScript => "(number) @num",
804        Language::C | Language::Cpp => "(number_literal) @num",
805        _ => "",
806    }
807}
808
809pub(crate) struct MagicNumberRule;
810
811impl TreeSitterRule for MagicNumberRule {
812    fn name(&self) -> &'static str {
813        "magic-number"
814    }
815
816    fn supported_languages(&self) -> &'static [Language] {
817        crate::language::LANGUAGES_WITH_GRAMMAR
818    }
819
820    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
821        let query = number_literal_query(file.language);
822        if query.is_empty() {
823            return vec![];
824        }
825        let captures = match collect_captures(file, query) {
826            Ok(c) => c,
827            Err(_) => return vec![],
828        };
829        // Named-constant parent kinds: skip literals directly assigned to const/let
830        let named_parents: &[&str] = &[
831            "const_item",
832            "let_declaration",
833            "assignment",
834            "variable_declarator",
835        ];
836        // Switch case labels: skip literals that are case values
837        let case_label_kinds: &[&str] = &["case", "switch_case", "case_statement"];
838        let common: &[&str] = &["0", "1", "-1", "2", "100", "0.0", "1.0", "10", "60", "24"];
839        let mut issues = Vec::new();
840        for group in &captures {
841            if let Some(cap) = group.first() {
842                let text = cap.text.trim();
843                // Skip literals assigned to named constants
844                let parent = cap.node.parent();
845                if parent.is_some_and(|p| named_parents.contains(&p.kind())) {
846                    continue;
847                }
848                // Skip literals that are switch case labels (not magic numbers)
849                if parent.is_some_and(|p| case_label_kinds.contains(&p.kind())) {
850                    continue;
851                }
852                if !common.contains(&text) && text.parse::<f64>().is_ok() {
853                    let pos = cap.node.start_position();
854                    issues.push(CodeIssue {
855                        file_path: file.path.clone(),
856                        line: pos.row + 1,
857                        column: pos.column + 1,
858                        rule_name: "magic-number".to_string(),
859                        message: format!("Magic number '{}' - consider a named constant", text),
860                        severity: Severity::Mild,
861                    });
862                }
863            }
864        }
865        issues
866    }
867}