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
190 if let Some(builtin) = builtin_language_profile(&lang_key) {
192 for (name, enabled) in builtin {
193 let user_override = self
195 .languages
196 .get(&lang_key)
197 .is_some_and(|lc| lc.plugins.contains_key(name));
198 if !user_override {
199 resolved
200 .plugins
201 .entry(name.to_string())
202 .or_default()
203 .enabled = enabled;
204 }
205 }
206 }
207
208 if let Some(lc) = self.languages.get(&lang_key) {
210 for (name, lpc) in &lc.plugins {
211 let entry = resolved.plugins.entry(name.clone()).or_default();
212 entry.enabled = lpc.enabled;
213 for (k, v) in &lpc.options {
214 entry.options.insert(k.clone(), v.clone());
215 }
216 }
217 }
218
219 resolved
220 }
221
222 pub fn is_enabled(&self, name: &str) -> bool {
224 self.plugins.get(name).is_none_or(|c| c.enabled)
225 }
226
227 pub fn get_usize(&self, plugin: &str, key: &str) -> Option<usize> {
229 self.plugins
230 .get(plugin)?
231 .options
232 .get(key)?
233 .as_integer()
234 .map(|v| {
235 let scaled = (v as f64 * self.strictness.factor()).round() as usize;
236 scaled.max(1)
237 })
238 }
239
240 pub fn get_str(&self, plugin: &str, key: &str) -> Option<String> {
242 self.plugins
243 .get(plugin)?
244 .options
245 .get(key)?
246 .as_str()
247 .map(|s| s.to_string())
248 }
249
250 pub fn set_strictness(&mut self, s: Strictness) {
252 self.strictness = s;
253 }
254}
255
256fn collect_configs_upward(start_dir: &Path, root: &Path) -> Vec<Config> {
258 let mut configs = Vec::new();
259 let mut current = start_dir.to_path_buf();
260 loop {
261 let cfg_path = current.join(".cha.toml");
262 if cfg_path.is_file()
263 && let Ok(content) = std::fs::read_to_string(&cfg_path)
264 && let Ok(cfg) = toml::from_str::<Config>(&content)
265 {
266 configs.push(cfg);
267 }
268 if current == root {
269 break;
270 }
271 match current.parent() {
272 Some(p) if p.starts_with(root) || p == root => current = p.to_path_buf(),
273 _ => break,
274 }
275 }
276 configs
277}
278
279pub fn builtin_language_profile(language: &str) -> Option<Vec<(&'static str, bool)>> {
282 match language {
283 "c" => Some(vec![
284 ("naming", false),
285 ("lazy_class", false),
286 ("data_class", false),
287 ("builder_pattern", false),
288 ("null_object_pattern", false),
289 ("strategy_pattern", false),
290 ]),
291 _ => None,
292 }
293}