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 overrides: Vec<OverrideConfig>,
24}
25
26impl ProjectConfig {
27 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 pub fn discover(start_dir: &Path) -> Self {
39 let config_name = ".garbage-code-hunter.toml";
40
41 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 Self::default()
53 }
54
55 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 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 #[serde(default)]
84 pub magic_numbers: Vec<i64>,
85
86 #[serde(default)]
88 pub variable_names: Vec<String>,
89
90 #[serde(default)]
92 pub directories: Vec<String>,
93
94 #[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 #[serde(default)]
104 pub naming: NamingRuleConfig,
105
106 #[serde(default)]
108 pub unwrap: UnwrapRuleConfig,
109
110 #[serde(default)]
112 pub magic_number: MagicNumberRuleConfig,
113
114 #[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 #[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 #[serde(default = "default_unwrap_threshold")]
150 pub threshold: usize,
151
152 #[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 #[serde(default)]
175 pub allowed_numbers: Vec<i64>,
176
177 #[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 #[serde(default = "default_true")]
200 pub allow_in_main_files: bool,
201
202 #[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 pub pattern: String,
222
223 #[serde(default)]
225 pub context: Option<FileContextType>,
226
227 #[serde(default = "default_one")]
229 pub weight_multiplier: f64,
230
231 #[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
255fn 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 }
339}