garbage_code_hunter/context/
project_config.rs1use std::fs;
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Deserialize, Serialize, Default)]
8pub struct ProjectConfig {
9 #[serde(default)]
11 pub project_type: Option<ProjectType>,
12
13 #[serde(default)]
15 pub whitelists: Whitelists,
16
17 #[serde(default)]
19 pub rules: RulesConfig,
20
21 #[serde(default)]
23 pub signals: SignalsConfig,
24
25 #[serde(default)]
27 pub overrides: Vec<OverrideConfig>,
28}
29
30impl ProjectConfig {
31 pub fn load_from_file(path: &Path) -> Option<Self> {
33 if !path.exists() {
34 return None;
35 }
36
37 let content = fs::read_to_string(path).ok()?;
38 toml::from_str(&content).ok()
39 }
40
41 pub fn discover(start_dir: &Path) -> Self {
43 let config_name = ".garbage-code-hunter.toml";
44
45 let mut current = Some(start_dir);
47 while let Some(dir) = current {
48 let config_path = dir.join(config_name);
49 if let Some(config) = Self::load_from_file(&config_path) {
50 return config;
51 }
52 current = dir.parent();
53 }
54
55 Self::default()
57 }
58
59 pub fn get_override_for_path(&self, path: &Path) -> Option<&OverrideConfig> {
61 let path_str = path.to_string_lossy();
62
63 self.overrides.iter().find(|override_config| {
64 path_str.contains(&override_config.pattern)
66 || path_str.starts_with(&override_config.pattern)
67 })
68 }
69}
70
71#[derive(Debug, Clone, Deserialize, Serialize)]
72#[serde(rename_all = "kebab-case")]
73pub enum ProjectType {
74 CliTool,
75 Library,
76 WebService,
77 Game,
78 Embedded,
79 Wasm,
80 Other(String),
81}
82
83#[derive(Debug, Clone, Deserialize, Serialize, Default)]
84#[serde(rename_all = "kebab-case")]
85pub struct Whitelists {
86 #[serde(default)]
88 pub magic_numbers: Vec<i64>,
89
90 #[serde(default)]
92 pub variable_names: Vec<String>,
93
94 #[serde(default)]
96 pub directories: Vec<String>,
97
98 #[serde(default)]
100 pub exclude_patterns: Vec<String>,
101}
102
103#[derive(Debug, Clone, Deserialize, Serialize, Default)]
104#[serde(rename_all = "kebab-case")]
105pub struct RulesConfig {
106 #[serde(default)]
108 pub naming: NamingRuleConfig,
109
110 #[serde(default)]
112 pub unwrap: UnwrapRuleConfig,
113
114 #[serde(default, rename = "magic_number")]
116 pub magic_number: MagicNumberRuleConfig,
117
118 #[serde(default)]
120 pub println: PrintlnRuleConfig,
121}
122
123#[derive(Debug, Clone, Deserialize, Serialize)]
124pub struct NamingRuleConfig {
125 #[serde(default = "default_enabled")]
126 pub enabled: bool,
127
128 #[serde(default = "default_severity_mild")]
129 pub severity: SeverityOverride,
130
131 #[serde(default)]
133 pub allowed_names: Vec<String>,
134}
135
136impl Default for NamingRuleConfig {
137 fn default() -> Self {
138 Self {
139 enabled: true,
140 severity: SeverityOverride::Mild,
141 allowed_names: Vec::new(),
142 }
143 }
144}
145
146#[derive(Debug, Clone, Deserialize, Serialize)]
147#[serde(rename_all = "kebab-case")]
148pub struct UnwrapRuleConfig {
149 #[serde(default = "default_enabled")]
150 pub enabled: bool,
151
152 #[serde(default = "default_unwrap_threshold")]
154 pub threshold: usize,
155
156 #[serde(default = "default_nuclear_threshold")]
158 pub nuclear_threshold: usize,
159}
160
161impl Default for UnwrapRuleConfig {
162 fn default() -> Self {
163 Self {
164 enabled: true,
165 threshold: 1,
166 nuclear_threshold: 15,
167 }
168 }
169}
170
171#[derive(Debug, Clone, Deserialize, Serialize)]
172#[serde(rename_all = "kebab-case")]
173pub struct MagicNumberRuleConfig {
174 #[serde(default = "default_enabled")]
175 pub enabled: bool,
176
177 #[serde(default)]
179 pub allowed_numbers: Vec<i64>,
180
181 #[serde(default = "default_ui_numbers")]
183 pub ui_layout_numbers: Vec<i64>,
184}
185
186impl Default for MagicNumberRuleConfig {
187 fn default() -> Self {
188 Self {
189 enabled: true,
190 allowed_numbers: Vec::new(),
191 ui_layout_numbers: default_ui_numbers(),
192 }
193 }
194}
195
196#[derive(Debug, Clone, Deserialize, Serialize)]
197#[serde(rename_all = "kebab-case")]
198pub struct PrintlnRuleConfig {
199 #[serde(default = "default_enabled")]
200 pub enabled: bool,
201
202 #[serde(default = "default_true")]
204 pub allow_in_main_files: bool,
205
206 #[serde(default = "default_println_threshold")]
208 pub threshold: usize,
209}
210
211impl Default for PrintlnRuleConfig {
212 fn default() -> Self {
213 Self {
214 enabled: true,
215 allow_in_main_files: true,
216 threshold: 1,
217 }
218 }
219}
220
221#[derive(Debug, Clone, Default, Deserialize, Serialize)]
223#[serde(rename_all = "kebab-case")]
224pub struct SignalsConfig {
225 #[serde(default)]
228 pub skip_tests: bool,
229}
230
231#[derive(Debug, Clone, Deserialize, Serialize)]
232#[serde(rename_all = "kebab-case")]
233pub struct OverrideConfig {
234 pub pattern: String,
236
237 #[serde(default)]
239 pub context: Option<FileContextType>,
240
241 #[serde(default = "default_one")]
243 pub weight_multiplier: f64,
244
245 #[serde(default)]
247 pub disabled_rules: Vec<String>,
248}
249
250#[derive(Debug, Clone, Deserialize, Serialize)]
251#[serde(rename_all = "kebab-case")]
252pub enum FileContextType {
253 Business,
254 Example,
255 Test,
256 Benchmark,
257 Documentation,
258 Config,
259}
260
261#[derive(Debug, Clone, Deserialize, Serialize)]
262#[serde(rename_all = "kebab-case")]
263pub enum SeverityOverride {
264 Mild,
265 Spicy,
266 Nuclear,
267}
268
269fn default_enabled() -> bool {
272 true
273}
274fn default_severity_mild() -> SeverityOverride {
275 SeverityOverride::Mild
276}
277fn default_unwrap_threshold() -> usize {
278 1
279}
280fn default_nuclear_threshold() -> usize {
281 15
282}
283fn default_ui_numbers() -> Vec<i64> {
284 vec![20, 25, 30, 33, 40, 50, 60, 66, 70, 75, 80, 90, 100]
285}
286fn default_true() -> bool {
287 true
288}
289fn default_println_threshold() -> usize {
290 1
291}
292fn default_one() -> f64 {
293 1.0
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_default_config() {
302 let config = ProjectConfig::default();
303 assert!(config.rules.naming.enabled);
304 assert_eq!(config.rules.unwrap.threshold, 1);
305 assert!(config.rules.println.allow_in_main_files);
306 assert!(!config.signals.skip_tests);
307 }
308
309 #[test]
310 fn test_load_nonexistent_file() {
311 let config = ProjectConfig::load_from_file(Path::new("/nonexistent/config.toml"));
312 assert!(config.is_none());
313 }
314
315 #[test]
316 fn test_discover_without_config() {
317 let temp_dir = tempfile::tempdir().unwrap();
318 let config = ProjectConfig::discover(temp_dir.path());
319
320 assert!(config.project_type.is_none());
321 assert!(config.whitelists.magic_numbers.is_empty());
322 }
323
324 #[test]
325 fn test_load_valid_config() {
326 let toml_content = r#"
327[whitelists]
328magic-numbers = [800, 1000, 2000]
329variable-names = ["data", "info", "ctx"]
330
331[rules.naming]
332enabled = true
333severity = "mild"
334allowed-names = ["e", "ctx"]
335
336[rules.unwrap]
337threshold = 3
338nuclear-threshold = 20
339
340[rules.println]
341allow-in-main-files = true
342threshold = 5
343"#;
344
345 let config: ProjectConfig = toml::from_str(toml_content).expect("Failed to parse config");
346
347 assert_eq!(config.whitelists.magic_numbers.len(), 3);
348 assert_eq!(config.whitelists.variable_names.len(), 3);
349 assert_eq!(config.rules.unwrap.threshold, 3);
350 assert_eq!(config.rules.unwrap.nuclear_threshold, 20);
351 assert_eq!(config.rules.println.threshold, 5);
352 }
354
355 #[test]
356 fn test_magic_number_config_parse() {
357 let toml_content = r#"
358[rules.magic_number]
359allowed-numbers = [3000, 1500, 86400]
360ui-layout-numbers = [100, 200, 300]
361"#;
362 let config: ProjectConfig =
363 toml::from_str(toml_content).expect("Failed to parse magic number config");
364 assert_eq!(
365 config.rules.magic_number.allowed_numbers,
366 vec![3000, 1500, 86400]
367 );
368 assert_eq!(
369 config.rules.magic_number.ui_layout_numbers,
370 vec![100, 200, 300]
371 );
372 assert!(config.rules.magic_number.enabled);
373 }
374}