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}
77
78#[derive(Debug, Clone, Deserialize)]
80pub struct DebtWeights {
81 #[serde(default = "default_hint_debt")]
82 pub hint: u32,
83 #[serde(default = "default_warning_debt")]
84 pub warning: u32,
85 #[serde(default = "default_error_debt")]
86 pub error: u32,
87}
88
89fn default_hint_debt() -> u32 {
90 5
91}
92fn default_warning_debt() -> u32 {
93 15
94}
95fn default_error_debt() -> u32 {
96 30
97}
98
99impl Default for DebtWeights {
100 fn default() -> Self {
101 Self {
102 hint: 5,
103 warning: 15,
104 error: 30,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Deserialize)]
111pub struct PluginConfig {
112 #[serde(default = "default_true")]
113 pub enabled: bool,
114 #[serde(flatten)]
115 pub options: HashMap<String, toml::Value>,
116}
117
118fn default_true() -> bool {
119 true
120}
121
122impl Default for PluginConfig {
123 fn default() -> Self {
124 Self {
125 enabled: true,
126 options: HashMap::new(),
127 }
128 }
129}
130
131impl Config {
132 pub fn load(dir: &Path) -> Self {
134 let path = dir.join(".cha.toml");
135 match std::fs::read_to_string(&path) {
136 Ok(content) => toml::from_str(&content).unwrap_or_default(),
137 Err(_) => Self::default(),
138 }
139 }
140
141 pub fn load_for_file(file_path: &Path, project_root: &Path) -> Self {
144 let abs_file = std::fs::canonicalize(file_path).unwrap_or(file_path.to_path_buf());
145 let abs_root = std::fs::canonicalize(project_root).unwrap_or(project_root.to_path_buf());
146 let dir = abs_file.parent().unwrap_or(&abs_root);
147
148 let mut configs = collect_configs_upward(dir, &abs_root);
150 configs.reverse();
151 let mut merged = Config::default();
152 for cfg in configs {
153 merged.merge(cfg);
154 }
155 merged
156 }
157
158 pub fn merge(&mut self, other: Config) {
160 for (name, other_pc) in other.plugins {
161 let entry = self.plugins.entry(name).or_default();
162 entry.enabled = other_pc.enabled;
163 for (k, v) in other_pc.options {
164 entry.options.insert(k, v);
165 }
166 }
167 self.exclude.extend(other.exclude);
168 self.debt_weights = other.debt_weights;
169 self.strictness = other.strictness;
173 for (lang, other_lc) in other.languages {
174 let entry = self.languages.entry(lang).or_default();
175 for (name, other_pc) in other_lc.plugins {
176 let pe = entry.plugins.entry(name).or_default();
177 pe.enabled = other_pc.enabled;
178 for (k, v) in other_pc.options {
179 pe.options.insert(k, v);
180 }
181 }
182 }
183 }
184
185 pub fn resolve_for_language(&self, language: &str) -> Config {
188 let lang_key = language.to_lowercase();
189 let mut resolved = self.clone();
190 self.apply_builtin_profile(&lang_key, &mut resolved);
191 self.apply_user_language_overrides(&lang_key, &mut resolved);
192 resolved
193 }
194
195 fn apply_builtin_profile(&self, lang_key: &str, resolved: &mut Config) {
196 let Some(builtin) = builtin_language_profile(lang_key) else {
197 return;
198 };
199 for (name, enabled, options) in builtin {
200 let user_override = self
201 .languages
202 .get(lang_key)
203 .is_some_and(|lc| lc.plugins.contains_key(name));
204 if user_override {
205 continue;
206 }
207 let entry = resolved.plugins.entry(name.to_string()).or_default();
208 entry.enabled = enabled;
209 for &(k, v) in options {
210 entry
211 .options
212 .entry(k.to_string())
213 .or_insert(toml::Value::Integer(v));
214 }
215 }
216 }
217
218 fn apply_user_language_overrides(&self, lang_key: &str, resolved: &mut Config) {
219 let Some(lc) = self.languages.get(lang_key) else {
220 return;
221 };
222 for (name, lpc) in &lc.plugins {
223 let entry = resolved.plugins.entry(name.clone()).or_default();
224 entry.enabled = lpc.enabled;
225 for (k, v) in &lpc.options {
226 entry.options.insert(k.clone(), v.clone());
227 }
228 }
229 }
230
231 pub fn is_enabled(&self, name: &str) -> bool {
233 self.plugins.get(name).is_none_or(|c| c.enabled)
234 }
235
236 pub fn get_usize(&self, plugin: &str, key: &str) -> Option<usize> {
238 self.plugins
239 .get(plugin)?
240 .options
241 .get(key)?
242 .as_integer()
243 .map(|v| {
244 let scaled = (v as f64 * self.strictness.factor()).round() as usize;
245 scaled.max(1)
246 })
247 }
248
249 pub fn get_str(&self, plugin: &str, key: &str) -> Option<String> {
251 self.plugins
252 .get(plugin)?
253 .options
254 .get(key)?
255 .as_str()
256 .map(|s| s.to_string())
257 }
258
259 pub fn set_strictness(&mut self, s: Strictness) {
261 self.strictness = s;
262 }
263
264 pub fn set_calibration_defaults(&mut self, lines: usize, complexity: usize, cognitive: usize) {
266 self.plugins
267 .entry("length".into())
268 .or_default()
269 .options
270 .entry("max_function_lines".into())
271 .or_insert(toml::Value::Integer(lines as i64));
272 self.plugins
273 .entry("complexity".into())
274 .or_default()
275 .options
276 .entry("max_complexity".into())
277 .or_insert(toml::Value::Integer(complexity as i64));
278 self.plugins
279 .entry("cognitive_complexity".into())
280 .or_default()
281 .options
282 .entry("max_cognitive_complexity".into())
283 .or_insert(toml::Value::Integer(cognitive as i64));
284 }
285}
286
287fn collect_configs_upward(start_dir: &Path, root: &Path) -> Vec<Config> {
289 let mut configs = Vec::new();
290 let mut current = start_dir.to_path_buf();
291 loop {
292 let cfg_path = current.join(".cha.toml");
293 if cfg_path.is_file()
294 && let Ok(content) = std::fs::read_to_string(&cfg_path)
295 && let Ok(cfg) = toml::from_str::<Config>(&content)
296 {
297 configs.push(cfg);
298 }
299 if current == root {
300 break;
301 }
302 match current.parent() {
303 Some(p) if p.starts_with(root) || p == root => current = p.to_path_buf(),
304 _ => break,
305 }
306 }
307 configs
308}
309
310pub type PluginProfile = (&'static str, bool, &'static [(&'static str, i64)]);
312
313pub fn builtin_language_profile(language: &str) -> Option<Vec<PluginProfile>> {
316 match language {
317 "c" | "cpp" => Some(vec![
318 ("naming", false, &[] as &[(&str, i64)]),
319 ("lazy_class", false, &[]),
320 ("data_class", false, &[]),
321 ("builder_pattern", false, &[]),
322 ("null_object_pattern", false, &[]),
323 ("strategy_pattern", false, &[]),
324 (
325 "length",
326 true,
327 &[
328 ("max_function_lines", 100),
329 ("max_file_lines", 2000),
330 ("max_class_lines", 400),
331 ],
332 ),
333 (
334 "complexity",
335 true,
336 &[("warn_threshold", 15), ("error_threshold", 30)],
337 ),
338 ("cognitive_complexity", true, &[("threshold", 25)]),
339 ("coupling", true, &[("max_imports", 25)]),
340 ("long_parameter_list", true, &[("max_params", 7)]),
341 ]),
342 _ => None,
343 }
344}