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