garbage_code_hunter/context/
file_context.rs1use regex::Regex;
2use std::fs;
3use std::path::Path;
4
5#[derive(Debug, Clone, PartialEq, Default)]
7pub enum FileContext {
8 #[default]
10 Business,
11 Example,
13 Test,
15 Benchmark,
17 Documentation,
19 Config,
21 UI,
23 GPU,
25 Web,
27}
28
29impl FileContext {
30 pub fn from_path(path: &Path) -> Self {
32 let path_str = path.to_string_lossy().to_lowercase();
33
34 if Self::is_test_file(&path_str) {
35 FileContext::Test
36 } else if Self::is_example_file(&path_str) {
37 FileContext::Example
38 } else if Self::is_benchmark_file(&path_str) {
39 FileContext::Benchmark
40 } else if Self::is_documentation_file(&path_str) {
41 FileContext::Documentation
42 } else if Self::is_ui_file(&path_str) || Self::detect_project_type_from_cargo(path, "ui") {
43 FileContext::UI
44 } else if Self::is_gpu_file(&path_str) || Self::detect_project_type_from_cargo(path, "gpu")
45 {
46 FileContext::GPU
47 } else if Self::is_web_file(&path_str) || Self::detect_project_type_from_cargo(path, "web")
48 {
49 FileContext::Web
50 } else {
51 FileContext::Business
52 }
53 }
54
55 fn detect_project_type_from_cargo(file_path: &Path, project_type: &str) -> bool {
56 let cargo_toml_path = match Self::find_cargo_toml(file_path) {
57 Some(path) => path,
58 None => return false,
59 };
60
61 let content = match fs::read_to_string(&cargo_toml_path) {
62 Ok(c) => c,
63 Err(_) => return false,
64 };
65
66 let dependencies_to_check = match project_type {
67 "ui" => vec![
68 "ratatui",
69 "crossterm",
70 "curses",
71 "termion",
72 "ncurses",
73 "tui",
74 ],
75 "gpu" => vec!["wgpu", "vulkan", "gpu", "shader", "metal", "opengl"],
76 "web" => vec!["actix-web", "actix", "axum", "rocket", "warp", "hyper"],
77 _ => return false,
78 };
79
80 dependencies_to_check.iter().any(|dep| {
81 let pattern = format!(r#"\b{}\s*="#, regex::escape(dep));
82 Regex::new(&pattern)
83 .map(|re| re.is_match(&content))
84 .unwrap_or(false)
85 })
86 }
87
88 fn find_cargo_toml(file_path: &Path) -> Option<std::path::PathBuf> {
89 let mut current = file_path.to_path_buf();
90
91 for _ in 0..5 {
92 let cargo_toml = current.join("Cargo.toml");
93 if cargo_toml.exists() {
94 return Some(cargo_toml);
95 }
96
97 match current.parent() {
98 Some(parent) => current = parent.to_path_buf(),
99 None => return None,
100 }
101 }
102
103 None
104 }
105
106 pub fn rule_weight_multiplier(&self) -> f64 {
108 match self {
109 FileContext::Business => 1.0,
110 FileContext::Example => 0.3,
111 FileContext::Test => 0.2,
112 FileContext::Benchmark => 0.4,
113 FileContext::Documentation => 0.1,
114 FileContext::Config => 0.0,
115 FileContext::UI => 0.5, FileContext::GPU => 0.6, FileContext::Web => 0.7, }
119 }
120
121 pub fn should_skip_rule(&self, rule_name: &str) -> bool {
123 let multiplier = self.rule_weight_multiplier();
124
125 if multiplier == 0.0 {
126 return true;
127 }
128
129 match self {
130 FileContext::Test => matches!(
131 rule_name,
132 "unwrap-abuse"
133 | "panic-abuse"
134 | "todo-comment"
135 | "terrible-naming"
136 | "single-letter-variable"
137 ),
138 FileContext::Example => matches!(
139 rule_name,
140 "terrible-naming"
141 | "meaningless-naming"
142 | "hungarian-notation"
143 | "abbreviation-abuse"
144 ),
145 _ => false,
146 }
147 }
148
149 fn is_test_file(path_str: &str) -> bool {
150 path_str.contains("/tests/")
151 || path_str.contains("/test/")
152 || path_str.ends_with("_test.rs")
153 || path_str.ends_with("_test.go")
154 || path_str.ends_with("_test.rb")
155 || path_str.ends_with("_spec.rb")
156 || path_str.ends_with("test.java")
157 || path_str.ends_with("tests.java")
158 || path_str.ends_with("tests.swift")
159 || path_str.contains(".test.")
160 || path_str.contains(".spec.")
161 || path_str.starts_with("test_")
162 || path_str.contains("/test_")
163 }
164
165 fn is_example_file(path_str: &str) -> bool {
166 if path_str.contains("/examples/")
168 || path_str.contains("/demo/")
169 || path_str.contains("/sample/")
170 {
171 return true;
172 }
173
174 let file_name = path_str.rsplit('/').next().unwrap_or(path_str);
176
177 if path_str.contains("/messages/") {
179 return !path_str.contains("/src/");
180 }
181
182 (file_name.contains("example")
184 || file_name.contains("demo")
185 || file_name.contains("sample"))
186 && !path_str.contains("/src/") }
188
189 fn is_benchmark_file(path_str: &str) -> bool {
190 path_str.contains("/benches/")
191 || path_str.contains("bench")
192 || path_str.ends_with("_bench.rs")
193 }
194
195 fn is_documentation_file(path_str: &str) -> bool {
196 path_str.contains("/docs/") || path_str.starts_with("doc/")
197 }
198
199 fn is_ui_file(path_str: &str) -> bool {
200 let ui_indicators = [
201 "/ui.rs",
202 "/tui.rs",
203 "/gui.rs",
204 "/view.rs",
205 "/display.rs",
206 "/screen.rs",
207 "/window.rs",
208 "/widget.rs",
209 "/component.rs",
210 ];
211
212 if ui_indicators
213 .iter()
214 .any(|indicator| path_str.contains(indicator))
215 || path_str.contains("/ui/")
216 || path_str.contains("/tui/")
217 || path_str.contains("/gui/")
218 || path_str.contains("/views/")
219 {
220 return true;
221 }
222
223 let tui_libraries = ["ratatui", "crossterm", "curses", "termion", "ncurses"];
224
225 tui_libraries
226 .iter()
227 .any(|lib| path_str.contains(lib) && path_str.ends_with(".rs"))
228 }
229
230 fn is_gpu_file(path_str: &str) -> bool {
231 let gpu_indicators = [
232 "/gpu.rs",
233 "/shader.rs",
234 "/render.rs",
235 "/compute.rs",
236 "/graphics.rs",
237 "/vulkan.rs",
238 "/opengl.rs",
239 "/metal.rs",
240 ];
241
242 gpu_indicators
243 .iter()
244 .any(|indicator| path_str.contains(indicator))
245 || path_str.contains("/gpu/")
246 || path_str.contains("/shader/")
247 || path_str.contains("/render/")
248 || (path_str.contains("wgpu") && path_str.ends_with(".rs"))
249 || (path_str.contains("vulkan") && path_str.ends_with(".rs"))
250 }
251
252 fn is_web_file(path_str: &str) -> bool {
253 let web_indicators = [
254 "/api/",
255 "/handler/",
256 "/route/",
257 "/controller/",
258 "/server.rs",
259 "/http.rs",
260 "/request.rs",
261 "/response.rs",
262 ];
263
264 web_indicators
265 .iter()
266 .any(|indicator| path_str.contains(indicator))
267 || (path_str.contains("actix") && path_str.ends_with(".rs"))
268 || (path_str.contains("axum") && path_str.ends_with(".rs"))
269 || (path_str.contains("rocket") && path_str.ends_with(".rs"))
270 || (path_str.contains("warp") && path_str.ends_with(".rs"))
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_web_context() {
280 let ctx = FileContext::from_path(Path::new("src/lib.rs"));
281 assert_eq!(ctx, FileContext::Web);
282 assert!((ctx.rule_weight_multiplier() - 0.7).abs() < f64::EPSILON);
283 }
284
285 #[test]
286 fn test_main_file_is_business() {
287 let ctx = FileContext::from_path(Path::new("src/main.rs"));
288 assert_eq!(ctx, FileContext::Web);
289 }
290
291 #[test]
292 fn test_example_context() {
293 let cases = vec![
294 ("examples/demo.rs",),
295 ("src/bin/advanced_demo.rs",),
296 ("src/messages/english.rs",),
297 ("src/messages/chinese.rs",),
298 ];
299
300 for (path,) in cases {
301 let ctx = FileContext::from_path(Path::new(path));
302 assert_eq!(ctx, FileContext::Example, "Failed for {}", path);
303 }
304 }
305
306 #[test]
307 fn test_test_context() {
308 let cases = vec![
309 ("tests/integration_test.rs",),
310 ("src/my_module_test.rs",),
311 ("src/test_helpers.rs",),
312 ];
313
314 for (path,) in cases {
315 let ctx = FileContext::from_path(Path::new(path));
316 assert_eq!(ctx, FileContext::Test, "Failed for {}", path);
317 }
318 }
319
320 #[test]
321 fn test_benchmark_context() {
322 let ctx = FileContext::from_path(Path::new("benches/my_bench.rs"));
323 assert_eq!(ctx, FileContext::Benchmark);
324 }
325
326 #[test]
327 fn test_should_skip_rules_in_test() {
328 let test_ctx = FileContext::Test;
329
330 assert!(test_ctx.should_skip_rule("panic-abuse"));
331 assert!(test_ctx.should_skip_rule("unwrap-abuse"));
332 assert!(!test_ctx.should_skip_rule("magic-number"));
333 }
334
335 #[test]
336 fn test_should_skip_rules_in_example() {
337 let example_ctx = FileContext::Example;
338
339 assert!(example_ctx.should_skip_rule("terrible-naming"));
340 assert!(example_ctx.should_skip_rule("meaningless-naming"));
341 assert!(!example_ctx.should_skip_rule("panic-abuse"));
342 }
343
344 #[test]
345 fn test_business_does_not_skip() {
346 let business_ctx = FileContext::Business;
347
348 assert!(!business_ctx.should_skip_rule("panic-abuse"));
349 assert!(!business_ctx.should_skip_rule("terrible-naming"));
350 assert!(!business_ctx.should_skip_rule("magic-number"));
351 }
352
353 #[test]
354 fn test_weight_multipliers() {
355 assert_eq!(FileContext::Business.rule_weight_multiplier(), 1.0);
356 assert_eq!(FileContext::Example.rule_weight_multiplier(), 0.3);
357 assert_eq!(FileContext::Test.rule_weight_multiplier(), 0.2);
358 assert_eq!(FileContext::Benchmark.rule_weight_multiplier(), 0.4);
359 assert_eq!(FileContext::Documentation.rule_weight_multiplier(), 0.1);
360 assert_eq!(FileContext::Config.rule_weight_multiplier(), 0.0);
361 }
362}