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.ends_with("_test.rs")
152 || path_str.contains("test_")
153 || path_str.contains(".test.")
154 }
155
156 fn is_example_file(path_str: &str) -> bool {
157 if path_str.contains("/examples/")
159 || path_str.contains("/demo/")
160 || path_str.contains("/sample/")
161 {
162 return true;
163 }
164
165 let file_name = path_str.rsplit('/').next().unwrap_or(path_str);
167
168 if path_str.contains("/messages/") {
170 return !path_str.contains("/src/");
171 }
172
173 (file_name.contains("example")
175 || file_name.contains("demo")
176 || file_name.contains("sample"))
177 && !path_str.contains("/src/") }
179
180 fn is_benchmark_file(path_str: &str) -> bool {
181 path_str.contains("/benches/")
182 || path_str.contains("bench")
183 || path_str.ends_with("_bench.rs")
184 }
185
186 fn is_documentation_file(path_str: &str) -> bool {
187 path_str.contains("/docs/") || path_str.starts_with("doc/")
188 }
189
190 fn is_ui_file(path_str: &str) -> bool {
191 let ui_indicators = [
192 "/ui.rs",
193 "/tui.rs",
194 "/gui.rs",
195 "/view.rs",
196 "/display.rs",
197 "/screen.rs",
198 "/window.rs",
199 "/widget.rs",
200 "/component.rs",
201 ];
202
203 if ui_indicators
204 .iter()
205 .any(|indicator| path_str.contains(indicator))
206 || path_str.contains("/ui/")
207 || path_str.contains("/tui/")
208 || path_str.contains("/gui/")
209 || path_str.contains("/views/")
210 {
211 return true;
212 }
213
214 let tui_libraries = ["ratatui", "crossterm", "curses", "termion", "ncurses"];
215
216 tui_libraries
217 .iter()
218 .any(|lib| path_str.contains(lib) && path_str.ends_with(".rs"))
219 }
220
221 fn is_gpu_file(path_str: &str) -> bool {
222 let gpu_indicators = [
223 "/gpu.rs",
224 "/shader.rs",
225 "/render.rs",
226 "/compute.rs",
227 "/graphics.rs",
228 "/vulkan.rs",
229 "/opengl.rs",
230 "/metal.rs",
231 ];
232
233 gpu_indicators
234 .iter()
235 .any(|indicator| path_str.contains(indicator))
236 || path_str.contains("/gpu/")
237 || path_str.contains("/shader/")
238 || path_str.contains("/render/")
239 || (path_str.contains("wgpu") && path_str.ends_with(".rs"))
240 || (path_str.contains("vulkan") && path_str.ends_with(".rs"))
241 }
242
243 fn is_web_file(path_str: &str) -> bool {
244 let web_indicators = [
245 "/api/",
246 "/handler/",
247 "/route/",
248 "/controller/",
249 "/server.rs",
250 "/http.rs",
251 "/request.rs",
252 "/response.rs",
253 ];
254
255 web_indicators
256 .iter()
257 .any(|indicator| path_str.contains(indicator))
258 || (path_str.contains("actix") && path_str.ends_with(".rs"))
259 || (path_str.contains("axum") && path_str.ends_with(".rs"))
260 || (path_str.contains("rocket") && path_str.ends_with(".rs"))
261 || (path_str.contains("warp") && path_str.ends_with(".rs"))
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_business_context() {
271 let ctx = FileContext::from_path(Path::new("src/lib.rs"));
272 assert_eq!(ctx, FileContext::Business);
273 assert!((ctx.rule_weight_multiplier() - 1.0).abs() < f64::EPSILON);
274 }
275
276 #[test]
277 fn test_main_file_is_business() {
278 let ctx = FileContext::from_path(Path::new("src/main.rs"));
279 assert_eq!(ctx, FileContext::Business);
280 }
281
282 #[test]
283 fn test_example_context() {
284 let cases = vec![
285 ("examples/demo.rs",),
286 ("src/bin/advanced_demo.rs",),
287 ("src/messages/english.rs",),
288 ("src/messages/chinese.rs",),
289 ];
290
291 for (path,) in cases {
292 let ctx = FileContext::from_path(Path::new(path));
293 assert_eq!(ctx, FileContext::Example, "Failed for {}", path);
294 }
295 }
296
297 #[test]
298 fn test_test_context() {
299 let cases = vec![
300 ("tests/integration_test.rs",),
301 ("src/my_module_test.rs",),
302 ("src/test_helpers.rs",),
303 ];
304
305 for (path,) in cases {
306 let ctx = FileContext::from_path(Path::new(path));
307 assert_eq!(ctx, FileContext::Test, "Failed for {}", path);
308 }
309 }
310
311 #[test]
312 fn test_benchmark_context() {
313 let ctx = FileContext::from_path(Path::new("benches/my_bench.rs"));
314 assert_eq!(ctx, FileContext::Benchmark);
315 }
316
317 #[test]
318 fn test_should_skip_rules_in_test() {
319 let test_ctx = FileContext::Test;
320
321 assert!(test_ctx.should_skip_rule("panic-abuse"));
322 assert!(test_ctx.should_skip_rule("unwrap-abuse"));
323 assert!(!test_ctx.should_skip_rule("magic-number"));
324 }
325
326 #[test]
327 fn test_should_skip_rules_in_example() {
328 let example_ctx = FileContext::Example;
329
330 assert!(example_ctx.should_skip_rule("terrible-naming"));
331 assert!(example_ctx.should_skip_rule("meaningless-naming"));
332 assert!(!example_ctx.should_skip_rule("panic-abuse"));
333 }
334
335 #[test]
336 fn test_business_does_not_skip() {
337 let business_ctx = FileContext::Business;
338
339 assert!(!business_ctx.should_skip_rule("panic-abuse"));
340 assert!(!business_ctx.should_skip_rule("terrible-naming"));
341 assert!(!business_ctx.should_skip_rule("magic-number"));
342 }
343
344 #[test]
345 fn test_weight_multipliers() {
346 assert_eq!(FileContext::Business.rule_weight_multiplier(), 1.0);
347 assert_eq!(FileContext::Example.rule_weight_multiplier(), 0.3);
348 assert_eq!(FileContext::Test.rule_weight_multiplier(), 0.2);
349 assert_eq!(FileContext::Benchmark.rule_weight_multiplier(), 0.4);
350 assert_eq!(FileContext::Documentation.rule_weight_multiplier(), 0.1);
351 assert_eq!(FileContext::Config.rule_weight_multiplier(), 0.0);
352 }
353}