Skip to main content

garbage_code_hunter/context/
file_context.rs

1use regex::Regex;
2use std::fs;
3use std::path::Path;
4
5/// File context type - used to adjust rule sensitivity
6#[derive(Debug, Clone, PartialEq, Default)]
7pub enum FileContext {
8    /// Business code (default) - normal detection intensity
9    #[default]
10    Business,
11    /// Example/demo code - 70% sensitivity reduction
12    Example,
13    /// Test code - 80% sensitivity reduction
14    Test,
15    /// Performance benchmark code - 60% sensitivity reduction
16    Benchmark,
17    /// Documentation code - 90% sensitivity reduction
18    Documentation,
19    /// Config files (non-Rust) - skip most rules
20    Config,
21    /// UI/TUI application code - relaxed naming rules for coordinates, colors, etc.
22    UI,
23    /// GPU/graphics/system programming code - relaxed naming for indices, coordinates
24    GPU,
25    /// Web server/handler code - relaxed naming for request/response objects
26    Web,
27}
28
29impl FileContext {
30    /// Infer context type from file path
31    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    /// Returns the rule weight multiplier for this context (0.0 = skip completely, 1.0 = normal)
107    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,  // Relaxed but not disabled
116            FileContext::GPU => 0.6, // GPU code has specific conventions
117            FileContext::Web => 0.7, // Web code slightly relaxed
118        }
119    }
120
121    /// Determine whether a specific rule should be skipped
122    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        // Check for standard example/demo directories first
158        if path_str.contains("/examples/")
159            || path_str.contains("/demo/")
160            || path_str.contains("/sample/")
161        {
162            return true;
163        }
164
165        // Check for example-like file names (but not in src/ main code)
166        let file_name = path_str.rsplit('/').next().unwrap_or(path_str);
167
168        // Message/example files (but exclude src/)
169        if path_str.contains("/messages/") {
170            return !path_str.contains("/src/");
171        }
172
173        // Only match if filename explicitly contains these patterns
174        (file_name.contains("example")
175            || file_name.contains("demo")
176            || file_name.contains("sample"))
177            && !path_str.contains("/src/") // Exclude main source code
178    }
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}