garbage_code_hunter/treesitter/
rule.rs1use std::path::Path;
2
3use crate::analyzer::CodeIssue;
4use crate::context::{FileContext, ProjectConfig};
5use crate::language::Language;
6
7use super::engine::ParsedFile;
8
9pub trait TreeSitterRule: Send + Sync {
15 fn name(&self) -> &'static str;
17
18 fn supported_languages(&self) -> &'static [Language];
20
21 fn skips_test_files(&self) -> bool {
23 true
24 }
25
26 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue>;
28
29 #[allow(clippy::too_many_arguments)]
33 fn check_with_context(
34 &self,
35 file: &ParsedFile,
36 _is_test_file: bool,
37 _context: &FileContext,
38 _config: &ProjectConfig,
39 ) -> Vec<CodeIssue> {
40 self.check(file)
41 }
42}
43
44pub struct TreeSitterRuleEngine {
49 rules: Vec<Box<dyn TreeSitterRule>>,
50}
51
52impl Default for TreeSitterRuleEngine {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl TreeSitterRuleEngine {
59 pub fn new() -> Self {
60 Self { rules: Vec::new() }
61 }
62
63 pub fn add(&mut self, rule: Box<dyn TreeSitterRule>) {
65 self.rules.push(rule);
66 }
67
68 pub fn add_query_rule(&mut self, query_rule: crate::treesitter::query::QueryRule) {
70 self.rules.push(Box::new(QueryRuleAdapter::new(query_rule)));
71 }
72
73 pub fn add_query_rules(&mut self, query_rules: Vec<crate::treesitter::query::QueryRule>) {
75 for qr in query_rules {
76 self.add_query_rule(qr);
77 }
78 }
79
80 pub fn check_file(&self, file: &ParsedFile, is_test_file: bool) -> Vec<CodeIssue> {
82 self.check_file_with_context(
83 file,
84 is_test_file,
85 &FileContext::from_path(&file.path),
86 &ProjectConfig::default(),
87 )
88 }
89
90 pub fn check_file_with_context(
92 &self,
93 file: &ParsedFile,
94 is_test_file: bool,
95 context: &FileContext,
96 config: &ProjectConfig,
97 ) -> Vec<CodeIssue> {
98 let mut issues = Vec::new();
99 for rule in &self.rules {
100 if is_test_file && rule.skips_test_files() {
101 continue;
102 }
103 if !rule.supported_languages().contains(&file.language) {
104 continue;
105 }
106 if Self::is_rule_disabled(config, rule.name()) {
107 continue;
108 }
109 issues.extend(rule.check_with_context(file, is_test_file, context, config));
110 }
111 issues
112 }
113
114 fn is_rule_disabled(config: &ProjectConfig, rule_name: &str) -> bool {
116 match rule_name {
117 "terrible-naming"
118 | "single-letter-variable"
119 | "meaningless-naming"
120 | "hungarian-notation"
121 | "abbreviation-abuse" => !config.rules.naming.enabled,
122 "unwrap-abuse" => !config.rules.unwrap.enabled,
123 "magic-number" => !config.rules.magic_number.enabled,
124 "println-debugging" => !config.rules.println.enabled,
125 _ => false,
126 }
127 }
128
129 pub fn is_test_file(path: &Path, content: &str) -> bool {
131 let path_str = path.to_string_lossy();
132 let normalized = path_str.strip_prefix("./").unwrap_or(&path_str);
133
134 if normalized.contains("/tests/")
135 || normalized.contains("\\tests\\")
136 || normalized.starts_with("tests/")
137 || normalized.starts_with("tests\\")
138 || normalized.contains("/test/")
139 || normalized.contains("\\test\\")
140 || normalized.ends_with("_test.rs")
141 || normalized.ends_with("_tests.rs")
142 || normalized.ends_with("_test.py")
143 || normalized.ends_with("_test.js")
144 || normalized.ends_with("_test.ts")
145 || normalized.ends_with("_test.go")
146 || normalized.ends_with("_test.java")
147 || normalized.starts_with("test_")
148 {
149 return true;
150 }
151 if normalized.contains("/examples/")
152 || normalized.contains("\\examples\\")
153 || normalized.starts_with("examples/")
154 || normalized.starts_with("examples\\")
155 {
156 return true;
157 }
158 if normalized.contains("/benches/")
159 || normalized.contains("\\benches\\")
160 || normalized.starts_with("benches/")
161 || normalized.starts_with("benches\\")
162 {
163 return true;
164 }
165
166 content.contains("#[cfg(test)]")
167 }
168
169 pub fn rule_names(&self) -> Vec<&'static str> {
170 self.rules.iter().map(|r| r.name()).collect()
171 }
172}
173
174struct QueryRuleAdapter {
179 rule: crate::treesitter::query::QueryRule,
180}
181
182impl QueryRuleAdapter {
183 fn new(rule: crate::treesitter::query::QueryRule) -> Self {
184 Self { rule }
185 }
186}
187
188impl TreeSitterRule for QueryRuleAdapter {
189 fn name(&self) -> &'static str {
190 self.rule.name
191 }
192
193 fn supported_languages(&self) -> &'static [Language] {
194 self.rule.languages
195 }
196
197 fn skips_test_files(&self) -> bool {
198 self.rule.skips_test_files
199 }
200
201 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
202 let candidates = crate::treesitter::query::run_query_rule(file, &self.rule);
203 candidates
204 .into_iter()
205 .map(|c| CodeIssue {
206 file_path: file.path.clone(),
207 line: c.line,
208 column: c.column,
209 rule_name: self.rule.name.to_string(),
210 message: c.message,
211 severity: c.severity,
212 })
213 .collect()
214 }
215
216 fn check_with_context(
217 &self,
218 file: &ParsedFile,
219 _is_test_file: bool,
220 _context: &FileContext,
221 _config: &ProjectConfig,
222 ) -> Vec<CodeIssue> {
223 self.check(file)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::treesitter::engine::TreeSitterEngine;
232
233 struct DummyRule;
234
235 impl TreeSitterRule for DummyRule {
236 fn name(&self) -> &'static str {
237 "dummy"
238 }
239 fn supported_languages(&self) -> &'static [Language] {
240 &[Language::Rust]
241 }
242 fn check(&self, _file: &ParsedFile) -> Vec<CodeIssue> {
243 vec![]
244 }
245 }
246
247 #[test]
250 fn test_rule_language_filtering() {
251 let mut engine = TreeSitterRuleEngine::new();
252 engine.add(Box::new(DummyRule));
253
254 let ts = TreeSitterEngine::new();
255 let file = ts
256 .parse_file(Path::new("test.rs"), "fn main() {}")
257 .expect("Should parse");
258
259 let issues = engine.check_file(&file, false);
260 assert!(issues.is_empty(), "Dummy rule produces no issues");
261
262 assert_eq!(engine.rule_names(), vec!["dummy"]);
263 }
264
265 #[test]
268 fn test_is_test_file_various_patterns() {
269 assert!(TreeSitterRuleEngine::is_test_file(
270 Path::new("src/tests/mod.rs"),
271 ""
272 ));
273 assert!(TreeSitterRuleEngine::is_test_file(
274 Path::new("tests/test_main.rs"),
275 ""
276 ));
277 assert!(TreeSitterRuleEngine::is_test_file(
278 Path::new("foo_test.py"),
279 ""
280 ));
281 assert!(!TreeSitterRuleEngine::is_test_file(
282 Path::new("src/main.rs"),
283 ""
284 ));
285 assert!(TreeSitterRuleEngine::is_test_file(
286 Path::new("src/lib.rs"),
287 "#[cfg(test)]"
288 ));
289 }
290}