Skip to main content

garbage_code_hunter/context/
project_config.rs

1use std::fs;
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5
6/// Project-level configuration - customizable via .garbage-code-hunter.toml
7#[derive(Debug, Clone, Deserialize, Serialize, Default)]
8pub struct ProjectConfig {
9    /// Project type hint (optional, helps context inference)
10    #[serde(default)]
11    pub project_type: Option<ProjectType>,
12
13    /// Global whitelist configuration
14    #[serde(default)]
15    pub whitelists: Whitelists,
16
17    /// Individual rule configurations
18    #[serde(default)]
19    pub rules: RulesConfig,
20
21    /// File and directory-level override configuration
22    #[serde(default)]
23    pub overrides: Vec<OverrideConfig>,
24}
25
26impl ProjectConfig {
27    /// Load configuration from file, returns default if file doesn't exist
28    pub fn load_from_file(path: &Path) -> Option<Self> {
29        if !path.exists() {
30            return None;
31        }
32
33        let content = fs::read_to_string(path).ok()?;
34        toml::from_str(&content).ok()
35    }
36
37    /// Discover configuration in current directory or parent directories
38    pub fn discover(start_dir: &Path) -> Self {
39        let config_name = ".garbage-code-hunter.toml";
40
41        // Search upward from current directory
42        let mut current = Some(start_dir);
43        while let Some(dir) = current {
44            let config_path = dir.join(config_name);
45            if let Some(config) = Self::load_from_file(&config_path) {
46                return config;
47            }
48            current = dir.parent();
49        }
50
51        // Not found, use default configuration
52        Self::default()
53    }
54
55    /// Get effective override config for the given path (merge all matching overrides)
56    pub fn get_override_for_path(&self, path: &Path) -> Option<&OverrideConfig> {
57        let path_str = path.to_string_lossy();
58
59        self.overrides.iter().find(|override_config| {
60            // Simple string matching (could be upgraded to glob matching in the future)
61            path_str.contains(&override_config.pattern)
62                || path_str.starts_with(&override_config.pattern)
63        })
64    }
65}
66
67#[derive(Debug, Clone, Deserialize, Serialize)]
68#[serde(rename_all = "kebab-case")]
69pub enum ProjectType {
70    CliTool,
71    Library,
72    WebService,
73    Game,
74    Embedded,
75    Wasm,
76    Other(String),
77}
78
79#[derive(Debug, Clone, Deserialize, Serialize, Default)]
80#[serde(rename_all = "kebab-case")]
81pub struct Whitelists {
82    /// Allowed magic numbers (won't report these)
83    #[serde(default)]
84    pub magic_numbers: Vec<i64>,
85
86    /// Allowed variable names (won't report these)
87    #[serde(default)]
88    pub variable_names: Vec<String>,
89
90    /// Directory patterns for sensitivity reduction (glob patterns)
91    #[serde(default)]
92    pub directories: Vec<String>,
93
94    /// Completely excluded directory or file patterns
95    #[serde(default)]
96    pub exclude_patterns: Vec<String>,
97}
98
99#[derive(Debug, Clone, Deserialize, Serialize, Default)]
100#[serde(rename_all = "kebab-case")]
101pub struct RulesConfig {
102    /// Naming rule configuration
103    #[serde(default)]
104    pub naming: NamingRuleConfig,
105
106    /// Unwrap rule configuration
107    #[serde(default)]
108    pub unwrap: UnwrapRuleConfig,
109
110    /// Magic Number rule configuration
111    #[serde(default)]
112    pub magic_number: MagicNumberRuleConfig,
113
114    /// Println rule configuration
115    #[serde(default)]
116    pub println: PrintlnRuleConfig,
117}
118
119#[derive(Debug, Clone, Deserialize, Serialize)]
120pub struct NamingRuleConfig {
121    #[serde(default = "default_enabled")]
122    pub enabled: bool,
123
124    #[serde(default = "default_severity_mild")]
125    pub severity: SeverityOverride,
126
127    /// Additional allowed variable names
128    #[serde(default)]
129    pub allowed_names: Vec<String>,
130}
131
132impl Default for NamingRuleConfig {
133    fn default() -> Self {
134        Self {
135            enabled: true,
136            severity: SeverityOverride::Mild,
137            allowed_names: Vec::new(),
138        }
139    }
140}
141
142#[derive(Debug, Clone, Deserialize, Serialize)]
143#[serde(rename_all = "kebab-case")]
144pub struct UnwrapRuleConfig {
145    #[serde(default = "default_enabled")]
146    pub enabled: bool,
147
148    /// Minimum unwrap count to trigger report (default: 1)
149    #[serde(default = "default_unwrap_threshold")]
150    pub threshold: usize,
151
152    /// Nuclear level threshold (default: 15)
153    #[serde(default = "default_nuclear_threshold")]
154    pub nuclear_threshold: usize,
155}
156
157impl Default for UnwrapRuleConfig {
158    fn default() -> Self {
159        Self {
160            enabled: true,
161            threshold: 1,
162            nuclear_threshold: 15,
163        }
164    }
165}
166
167#[derive(Debug, Clone, Deserialize, Serialize)]
168#[serde(rename_all = "kebab-case")]
169pub struct MagicNumberRuleConfig {
170    #[serde(default = "default_enabled")]
171    pub enabled: bool,
172
173    /// Additional allowed magic numbers
174    #[serde(default)]
175    pub allowed_numbers: Vec<i64>,
176
177    /// Common UI layout values (automatically added to whitelist)
178    #[serde(default = "default_ui_numbers")]
179    pub ui_layout_numbers: Vec<i64>,
180}
181
182impl Default for MagicNumberRuleConfig {
183    fn default() -> Self {
184        Self {
185            enabled: true,
186            allowed_numbers: Vec::new(),
187            ui_layout_numbers: default_ui_numbers(),
188        }
189    }
190}
191
192#[derive(Debug, Clone, Deserialize, Serialize)]
193#[serde(rename_all = "kebab-case")]
194pub struct PrintlnRuleConfig {
195    #[serde(default = "default_enabled")]
196    pub enabled: bool,
197
198    /// Whether to allow println in main.rs/lib.rs
199    #[serde(default = "default_true")]
200    pub allow_in_main_files: bool,
201
202    /// Minimum count to trigger report
203    #[serde(default = "default_println_threshold")]
204    pub threshold: usize,
205}
206
207impl Default for PrintlnRuleConfig {
208    fn default() -> Self {
209        Self {
210            enabled: true,
211            allow_in_main_files: true,
212            threshold: 3,
213        }
214    }
215}
216
217#[derive(Debug, Clone, Deserialize, Serialize)]
218#[serde(rename_all = "kebab-case")]
219pub struct OverrideConfig {
220    /// Matching pattern (glob or path prefix)
221    pub pattern: String,
222
223    /// Forced context type
224    #[serde(default)]
225    pub context: Option<FileContextType>,
226
227    /// Global multiplier for rule weight
228    #[serde(default = "default_one")]
229    pub weight_multiplier: f64,
230
231    /// List of rules to disable
232    #[serde(default)]
233    pub disabled_rules: Vec<String>,
234}
235
236#[derive(Debug, Clone, Deserialize, Serialize)]
237#[serde(rename_all = "kebab-case")]
238pub enum FileContextType {
239    Business,
240    Example,
241    Test,
242    Benchmark,
243    Documentation,
244    Config,
245}
246
247#[derive(Debug, Clone, Deserialize, Serialize)]
248#[serde(rename_all = "kebab-case")]
249pub enum SeverityOverride {
250    Mild,
251    Spicy,
252    Nuclear,
253}
254
255// === Default value functions ===
256
257fn default_enabled() -> bool {
258    true
259}
260fn default_severity_mild() -> SeverityOverride {
261    SeverityOverride::Mild
262}
263fn default_unwrap_threshold() -> usize {
264    1
265}
266fn default_nuclear_threshold() -> usize {
267    15
268}
269fn default_ui_numbers() -> Vec<i64> {
270    vec![20, 25, 30, 33, 40, 50, 60, 66, 70, 75, 80, 90, 100]
271}
272fn default_true() -> bool {
273    true
274}
275fn default_println_threshold() -> usize {
276    3
277}
278fn default_one() -> f64 {
279    1.0
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_default_config() {
288        let config = ProjectConfig::default();
289        assert!(config.rules.naming.enabled);
290        assert_eq!(config.rules.unwrap.threshold, 1);
291        assert!(config.rules.println.allow_in_main_files);
292    }
293
294    #[test]
295    fn test_load_nonexistent_file() {
296        let config = ProjectConfig::load_from_file(Path::new("/nonexistent/config.toml"));
297        assert!(config.is_none());
298    }
299
300    #[test]
301    fn test_discover_without_config() {
302        let temp_dir = tempfile::tempdir().unwrap();
303        let config = ProjectConfig::discover(temp_dir.path());
304
305        assert!(config.project_type.is_none());
306        assert!(config.whitelists.magic_numbers.is_empty());
307    }
308
309    #[test]
310    fn test_load_valid_config() {
311        let toml_content = r#"
312[whitelists]
313magic-numbers = [800, 1000, 2000]
314variable-names = ["data", "info", "ctx"]
315
316[rules.naming]
317enabled = true
318severity = "mild"
319allowed-names = ["e", "ctx"]
320
321[rules.unwrap]
322threshold = 3
323nuclear-threshold = 20
324
325[rules.println]
326allow-in-main-files = true
327threshold = 5
328"#;
329
330        let config: ProjectConfig = toml::from_str(toml_content).expect("Failed to parse config");
331
332        assert_eq!(config.whitelists.magic_numbers.len(), 3);
333        assert_eq!(config.whitelists.variable_names.len(), 3);
334        assert_eq!(config.rules.unwrap.threshold, 3);
335        assert_eq!(config.rules.unwrap.nuclear_threshold, 20);
336        assert_eq!(config.rules.println.threshold, 5);
337        // Note: overrides parsing tested separately to isolate potential issues
338    }
339}