1use serde::Deserialize;
2use std::collections::HashMap;
3use std::path::Path;
4
5#[derive(Debug, Clone, Deserialize)]
10#[serde(untagged)]
11pub enum Strictness {
12 Named(StrictnessLevel),
13 Custom(f64),
14}
15
16#[derive(Debug, Clone, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum StrictnessLevel {
19 Relaxed,
20 Default,
21 Strict,
22}
23
24impl Default for Strictness {
25 fn default() -> Self {
26 Strictness::Named(StrictnessLevel::Default)
27 }
28}
29
30impl Strictness {
31 pub fn factor(&self) -> f64 {
32 match self {
33 Strictness::Named(StrictnessLevel::Relaxed) => 2.0,
34 Strictness::Named(StrictnessLevel::Default) => 1.0,
35 Strictness::Named(StrictnessLevel::Strict) => 0.5,
36 Strictness::Custom(v) => *v,
37 }
38 }
39
40 pub fn parse(s: &str) -> Option<Self> {
42 match s {
43 "relaxed" => Some(Strictness::Named(StrictnessLevel::Relaxed)),
44 "default" => Some(Strictness::Named(StrictnessLevel::Default)),
45 "strict" => Some(Strictness::Named(StrictnessLevel::Strict)),
46 _ => s.parse::<f64>().ok().map(Strictness::Custom),
47 }
48 }
49}
50
51#[derive(Debug, Default, Clone, Deserialize)]
53pub struct LanguageConfig {
54 #[serde(default)]
55 pub plugins: HashMap<String, PluginConfig>,
56}
57
58#[derive(Debug, Default, Clone, Deserialize)]
60pub struct Config {
62 #[serde(default)]
63 pub plugins: HashMap<String, PluginConfig>,
64 #[serde(default)]
66 pub exclude: Vec<String>,
67 #[serde(default)]
69 pub debt_weights: DebtWeights,
70 #[serde(default)]
72 pub strictness: Strictness,
73 #[serde(default)]
75 pub languages: HashMap<String, LanguageConfig>,
76 #[serde(default)]
78 pub layers: LayersConfig,
79}
80
81#[derive(Debug, Default, Clone, Deserialize)]
83pub struct LayersConfig {
84 #[serde(default)]
86 pub modules: HashMap<String, Vec<String>>,
87 #[serde(default)]
89 pub tiers: Vec<TierConfig>,
90}
91
92#[derive(Debug, Clone, Deserialize)]
94pub struct TierConfig {
95 pub name: String,
96 pub modules: Vec<String>,
97}
98
99#[derive(Debug, Clone, Deserialize)]
101pub struct DebtWeights {
102 #[serde(default = "default_hint_debt")]
103 pub hint: u32,
104 #[serde(default = "default_warning_debt")]
105 pub warning: u32,
106 #[serde(default = "default_error_debt")]
107 pub error: u32,
108}
109
110fn default_hint_debt() -> u32 {
111 5
112}
113fn default_warning_debt() -> u32 {
114 15
115}
116fn default_error_debt() -> u32 {
117 30
118}
119
120impl Default for DebtWeights {
121 fn default() -> Self {
122 Self {
123 hint: 5,
124 warning: 15,
125 error: 30,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Deserialize)]
132pub struct PluginConfig {
133 #[serde(default = "default_true")]
134 pub enabled: bool,
135 #[serde(flatten)]
136 pub options: HashMap<String, toml::Value>,
137}
138
139fn default_true() -> bool {
140 true
141}
142
143impl Default for PluginConfig {
144 fn default() -> Self {
145 Self {
146 enabled: true,
147 options: HashMap::new(),
148 }
149 }
150}
151
152impl Config {
153 pub fn load(dir: &Path) -> Self {
155 let path = dir.join(".cha.toml");
156 match std::fs::read_to_string(&path) {
157 Ok(content) => toml::from_str(&content).unwrap_or_default(),
158 Err(_) => Self::default(),
159 }
160 }
161
162 pub fn load_file(path: &Path) -> Self {
164 match std::fs::read_to_string(path) {
165 Ok(content) => toml::from_str(&content).unwrap_or_default(),
166 Err(_) => Self::default(),
167 }
168 }
169
170 pub fn load_for_file(file_path: &Path, project_root: &Path) -> Self {
173 let abs_file = std::fs::canonicalize(file_path).unwrap_or(file_path.to_path_buf());
174 let abs_root = std::fs::canonicalize(project_root).unwrap_or(project_root.to_path_buf());
175 let dir = abs_file.parent().unwrap_or(&abs_root);
176
177 let mut configs = collect_configs_upward(dir, &abs_root);
179 configs.reverse();
180 let mut merged = Config::default();
181 for cfg in configs {
182 merged.merge(cfg);
183 }
184 merged
185 }
186
187 pub fn merge(&mut self, other: Config) {
189 for (name, other_pc) in other.plugins {
190 let entry = self.plugins.entry(name).or_default();
191 entry.enabled = other_pc.enabled;
192 for (k, v) in other_pc.options {
193 entry.options.insert(k, v);
194 }
195 }
196 self.exclude.extend(other.exclude);
197 self.debt_weights = other.debt_weights;
198 self.strictness = other.strictness;
202 for (lang, other_lc) in other.languages {
203 let entry = self.languages.entry(lang).or_default();
204 for (name, other_pc) in other_lc.plugins {
205 let pe = entry.plugins.entry(name).or_default();
206 pe.enabled = other_pc.enabled;
207 for (k, v) in other_pc.options {
208 pe.options.insert(k, v);
209 }
210 }
211 }
212 }
213
214 pub fn resolve_for_language(&self, language: &str) -> Config {
217 let lang_key = language.to_lowercase();
218 let mut resolved = self.clone();
219 self.apply_builtin_profile(&lang_key, &mut resolved);
220 self.apply_user_language_overrides(&lang_key, &mut resolved);
221 resolved
222 }
223
224 fn apply_builtin_profile(&self, lang_key: &str, resolved: &mut Config) {
225 let Some(builtin) = builtin_language_profile(lang_key) else {
226 return;
227 };
228 for (name, enabled, options) in builtin {
229 let user_override = self
230 .languages
231 .get(lang_key)
232 .is_some_and(|lc| lc.plugins.contains_key(name));
233 if user_override {
234 continue;
235 }
236 let entry = resolved.plugins.entry(name.to_string()).or_default();
237 entry.enabled = enabled;
238 for &(k, v) in options {
239 entry
240 .options
241 .entry(k.to_string())
242 .or_insert(toml::Value::Integer(v));
243 }
244 }
245 }
246
247 fn apply_user_language_overrides(&self, lang_key: &str, resolved: &mut Config) {
248 let Some(lc) = self.languages.get(lang_key) else {
249 return;
250 };
251 for (name, lpc) in &lc.plugins {
252 let entry = resolved.plugins.entry(name.clone()).or_default();
253 entry.enabled = lpc.enabled;
254 for (k, v) in &lpc.options {
255 entry.options.insert(k.clone(), v.clone());
256 }
257 }
258 }
259
260 pub fn is_enabled(&self, name: &str) -> bool {
262 self.plugins.get(name).is_none_or(|c| c.enabled)
263 }
264
265 pub fn get_usize(&self, plugin: &str, key: &str) -> Option<usize> {
267 self.plugins
268 .get(plugin)?
269 .options
270 .get(key)?
271 .as_integer()
272 .map(|v| {
273 let scaled = (v as f64 * self.strictness.factor()).round() as usize;
274 scaled.max(1)
275 })
276 }
277
278 pub fn get_str(&self, plugin: &str, key: &str) -> Option<String> {
280 self.plugins
281 .get(plugin)?
282 .options
283 .get(key)?
284 .as_str()
285 .map(|s| s.to_string())
286 }
287
288 pub fn set_strictness(&mut self, s: Strictness) {
290 self.strictness = s;
291 }
292
293 pub fn set_calibration_defaults(&mut self, lines: usize, complexity: usize, cognitive: usize) {
295 self.plugins
296 .entry("length".into())
297 .or_default()
298 .options
299 .entry("max_function_lines".into())
300 .or_insert(toml::Value::Integer(lines as i64));
301 self.plugins
302 .entry("complexity".into())
303 .or_default()
304 .options
305 .entry("max_complexity".into())
306 .or_insert(toml::Value::Integer(complexity as i64));
307 self.plugins
308 .entry("cognitive_complexity".into())
309 .or_default()
310 .options
311 .entry("max_cognitive_complexity".into())
312 .or_insert(toml::Value::Integer(cognitive as i64));
313 }
314}
315
316fn collect_configs_upward(start_dir: &Path, root: &Path) -> Vec<Config> {
318 let mut configs = Vec::new();
319 let mut current = start_dir.to_path_buf();
320 loop {
321 let cfg_path = current.join(".cha.toml");
322 if cfg_path.is_file()
323 && let Ok(content) = std::fs::read_to_string(&cfg_path)
324 && let Ok(cfg) = toml::from_str::<Config>(&content)
325 {
326 configs.push(cfg);
327 }
328 if current == root {
329 break;
330 }
331 match current.parent() {
332 Some(p) if p.starts_with(root) || p == root => current = p.to_path_buf(),
333 _ => break,
334 }
335 }
336 configs
337}
338
339pub type PluginProfile = (&'static str, bool, &'static [(&'static str, i64)]);
341
342pub fn builtin_language_profile(language: &str) -> Option<Vec<PluginProfile>> {
345 match language {
346 "c" | "cpp" => Some(vec![
347 ("naming", false, &[] as &[(&str, i64)]),
348 ("lazy_class", false, &[]),
349 ("data_class", false, &[]),
350 ("builder_pattern", false, &[]),
351 ("null_object_pattern", false, &[]),
352 ("strategy_pattern", false, &[]),
353 (
354 "length",
355 true,
356 &[
357 ("max_function_lines", 100),
358 ("max_file_lines", 2000),
359 ("max_class_lines", 400),
360 ],
361 ),
362 (
363 "complexity",
364 true,
365 &[("warn_threshold", 15), ("error_threshold", 30)],
366 ),
367 ("cognitive_complexity", true, &[("threshold", 25)]),
368 ("coupling", true, &[("max_imports", 25)]),
369 ("long_parameter_list", true, &[("max_params", 7)]),
370 ]),
371 _ => None,
372 }
373}