Skip to main content

garbage_code_hunter/treesitter/rules/
base_rules.rs

1use crate::analyzer::{CodeIssue, Severity};
2use crate::language::Language;
3use crate::treesitter::engine::ParsedFile;
4use crate::treesitter::query::collect_captures;
5use crate::treesitter::rule::TreeSitterRule;
6
7/// Node types that contribute to nesting depth, across all supported languages.
8pub(crate) const BLOCK_PARENT_TYPES: &[&str] = &[
9    // Rust
10    "if_expression",
11    "for_expression",
12    "while_expression",
13    "loop_expression",
14    "match_expression",
15    "match_arm",
16    // Python
17    "if_statement",
18    "for_statement",
19    "while_statement",
20    "try_statement",
21    "with_statement",
22    "function_definition",
23    "class_definition",
24    // JavaScript
25    "if_statement",
26    "for_statement",
27    "while_statement",
28    "do_statement",
29    "try_statement",
30    "switch_case",
31    "function_declaration",
32    // C/C++
33    "if_statement",
34    "for_statement",
35    "while_statement",
36    "do_statement",
37    "switch_statement",
38    "case_statement",
39];
40
41/// Block-like node kinds across languages (where nesting depth increments).
42pub(crate) const BLOCK_KINDS: &[&str] = &[
43    "block",              // Rust, Python
44    "statement_block",    // JavaScript
45    "compound_statement", // C/C++
46];
47
48/// Count-and-report rule: runs a tree-sitter query, counts matches,
49/// reports a single aggregated issue when count exceeds threshold.
50pub(crate) struct CountRule {
51    pub(crate) name: &'static str,
52    pub(crate) pattern: &'static str,
53    pub(crate) threshold: usize,
54    pub(crate) severity: Severity,
55    pub(crate) message_fn: fn(usize) -> String,
56    pub(crate) languages: &'static [Language],
57}
58
59impl TreeSitterRule for CountRule {
60    fn name(&self) -> &'static str {
61        self.name
62    }
63
64    fn supported_languages(&self) -> &'static [Language] {
65        self.languages
66    }
67
68    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
69        let captures = match collect_captures(file, self.pattern) {
70            Ok(c) => c,
71            Err(_) => return vec![],
72        };
73        let count: usize = captures.iter().map(|c| c.len()).sum();
74        if count > self.threshold {
75            vec![CodeIssue {
76                file_path: file.path.clone(),
77                line: 1,
78                column: 1,
79                rule_name: self.name.to_string(),
80                message: (self.message_fn)(count),
81                severity: self.severity.clone(),
82            }]
83        } else {
84            vec![]
85        }
86    }
87}
88
89/// Macro-count rule: finds `name!()` calls by querying macro_invocation
90/// with identifier capture, then filters by macro name in Rust code.
91pub(crate) struct MacroRule {
92    pub(crate) name: &'static str,
93    pub(crate) macro_name: &'static str,
94    pub(crate) threshold: usize,
95    pub(crate) severity: Severity,
96    pub(crate) message_fn: fn(usize) -> String,
97}
98
99impl TreeSitterRule for MacroRule {
100    fn name(&self) -> &'static str {
101        self.name
102    }
103
104    fn supported_languages(&self) -> &'static [Language] {
105        &[Language::Rust]
106    }
107
108    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
109        let pattern = "(macro_invocation macro: (identifier) @m)";
110        let captures = match collect_captures(file, pattern) {
111            Ok(c) => c,
112            Err(_) => return vec![],
113        };
114        let count = captures
115            .iter()
116            .filter_map(|group| group.first())
117            .filter(|cap| cap.text == self.macro_name)
118            .count();
119        if count > self.threshold {
120            vec![CodeIssue {
121                file_path: file.path.clone(),
122                line: 1,
123                column: 1,
124                rule_name: self.name.to_string(),
125                message: (self.message_fn)(count),
126                severity: self.severity.clone(),
127            }]
128        } else {
129            vec![]
130        }
131    }
132}
133
134/// Method-call-count rule: finds `.method()` calls by querying field_expression
135/// with identifier capture, then filters by method name in Rust code.
136pub(crate) struct MethodCallRule {
137    pub(crate) name: &'static str,
138    pub(crate) method_name: &'static str,
139    pub(crate) threshold: usize,
140    pub(crate) severity_fn: fn(usize) -> Severity,
141    pub(crate) message_fn: fn(usize) -> String,
142}
143
144impl TreeSitterRule for MethodCallRule {
145    fn name(&self) -> &'static str {
146        self.name
147    }
148
149    fn supported_languages(&self) -> &'static [Language] {
150        &[Language::Rust]
151    }
152
153    fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
154        // Query all field accesses, then filter by method name
155        let pattern = "(field_expression field: (field_identifier) @method)";
156        let captures = match collect_captures(file, pattern) {
157            Ok(c) => c,
158            Err(_) => return vec![],
159        };
160        let count = captures
161            .iter()
162            .filter_map(|group| group.first())
163            .filter(|cap| cap.text == self.method_name)
164            .count();
165        if count > self.threshold {
166            vec![CodeIssue {
167                file_path: file.path.clone(),
168                line: 1,
169                column: 1,
170                rule_name: self.name.to_string(),
171                message: (self.message_fn)(count),
172                severity: (self.severity_fn)(count),
173            }]
174        } else {
175            vec![]
176        }
177    }
178}
179
180pub(crate) fn find_function_name(node: tree_sitter::Node, content: &[u8]) -> String {
181    let mut cursor = node.walk();
182    for child in node.named_children(&mut cursor) {
183        if child.kind() == "identifier" {
184            if let Ok(text) = child.utf8_text(content) {
185                return text.to_string();
186            }
187        }
188    }
189    "<anonymous>".to_string()
190}
191
192pub(crate) fn count_parameters(func_node: tree_sitter::Node) -> u32 {
193    let mut cursor = func_node.walk();
194    for child in func_node.named_children(&mut cursor) {
195        if child.kind() == "parameters" {
196            return child.named_child_count() as u32;
197        }
198    }
199    0
200}
201
202pub(crate) fn count_descendants_of_types(node: tree_sitter::Node, types: &[&str]) -> u32 {
203    let mut count = 0;
204    let mut cursor = node.walk();
205    let mut visited_children = false;
206
207    loop {
208        let current = cursor.node();
209        if !visited_children && types.contains(&current.kind()) {
210            count += 1;
211        }
212
213        if (!visited_children && cursor.goto_first_child()) || cursor.goto_next_sibling() {
214            visited_children = false;
215        } else if !cursor.goto_parent() {
216            break;
217        } else {
218            visited_children = true;
219        }
220    }
221
222    count
223}
224
225pub(crate) fn closure_depth(node: tree_sitter::Node) -> u32 {
226    let mut depth = 1;
227    let mut current = node;
228    while let Some(parent) = current.parent() {
229        if parent.kind() == "closure_expression" {
230            depth += 1;
231        }
232        current = parent;
233    }
234    depth
235}