1use std::{collections::HashMap, fmt, fs, path::Path};
2
3use anyhow::{Context, Result};
4use clap::ValueEnum;
5use globset::{Glob, GlobSet, GlobSetBuilder};
6use serde::{Deserialize, de};
7
8use crate::{for_each_rule, rules::*};
9
10#[derive(Default, Debug, Clone, Copy, ValueEnum, Deserialize)]
11#[serde(rename_all = "kebab-case")]
12pub enum OutputFormat {
13 #[default]
15 Text,
16 Json,
18 Sarif,
20 Gcc,
22 GitlabCodequality,
24}
25
26pub fn parse_glib_version(version: &str) -> Option<(u32, u32)> {
28 let parts: Vec<&str> = version.split('.').collect();
29 if parts.len() != 2 {
30 return None;
31 }
32 let major = parts[0].parse::<u32>().ok()?;
33 let minor = parts[1].parse::<u32>().ok()?;
34 Some((major, minor))
35}
36
37fn deserialize_glib_version<'de, D>(deserializer: D) -> Result<Option<(u32, u32)>, D::Error>
39where
40 D: serde::Deserializer<'de>,
41{
42 let version_str: Option<String> = Option::deserialize(deserializer)?;
43
44 match version_str {
45 Some(s) => parse_glib_version(&s).map(Some).ok_or_else(|| {
46 de::Error::custom(format!(
47 "Invalid GLib version format: '{}'. Expected format: 'major.minor' (e.g., '2.76')",
48 s
49 ))
50 }),
51 None => Ok(None),
52 }
53}
54
55#[derive(Debug, Clone, Deserialize)]
56pub struct Style {
57 #[serde(default = "default_true")]
58 pub space_before_paren: bool,
59}
60
61impl Default for Style {
62 fn default() -> Self {
63 Self {
64 space_before_paren: true,
65 }
66 }
67}
68
69impl Style {
70 pub fn format_call(&self, func: &str, args: &[&str]) -> String {
71 let sep = if self.space_before_paren { " (" } else { "(" };
72 let mut s =
73 String::with_capacity(func.len() + 2 + args.iter().map(|a| a.len() + 2).sum::<usize>());
74 s.push_str(func);
75 s.push_str(sep);
76 for (i, arg) in args.iter().enumerate() {
77 if i > 0 {
78 s.push_str(", ");
79 }
80 s.push_str(arg);
81 }
82 s.push(')');
83 s
84 }
85
86 pub fn format_call_stmt(&self, func: &str, args: &[&str]) -> String {
87 let mut s = self.format_call(func, args);
88 s.push(';');
89 s
90 }
91
92 pub fn format_addr_call(&self, func: &str, var: &str, extra_args: &[&str]) -> String {
93 let sep = if self.space_before_paren { " (" } else { "(" };
94 let mut s = String::with_capacity(
95 func.len() + 4 + var.len() + extra_args.iter().map(|a| a.len() + 2).sum::<usize>(),
96 );
97 s.push_str(func);
98 s.push_str(sep);
99 s.push('&');
100 s.push_str(var);
101 for arg in extra_args {
102 s.push_str(", ");
103 s.push_str(arg);
104 }
105 s.push(')');
106 s
107 }
108
109 pub fn format_addr_call_stmt(&self, func: &str, var: &str, extra_args: &[&str]) -> String {
110 let mut s = self.format_addr_call(func, var, extra_args);
111 s.push(';');
112 s
113 }
114}
115
116fn default_true() -> bool {
117 true
118}
119
120#[derive(Debug, Clone, Deserialize, Default)]
121pub struct Config {
122 #[serde(default)]
123 pub style: Style,
124
125 #[serde(default)]
126 pub rules: RulesConfig,
127
128 #[serde(default)]
129 pub ignore: Vec<String>,
130
131 #[serde(default, deserialize_with = "deserialize_glib_version")]
134 pub min_glib_version: Option<(u32, u32)>,
135
136 #[serde(default)]
138 pub msvc_compatible: bool,
139
140 pub format: Option<OutputFormat>,
142
143 pub editor_url: Option<String>,
150
151 pub build_dir: Option<String>,
155
156 pub default_level: Option<RuleLevel>,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
164#[serde(rename_all = "kebab-case")]
165pub enum RuleLevel {
166 Error,
168 Warn,
170 Ignore,
172}
173
174impl RuleLevel {
175 pub fn is_enabled(&self) -> bool {
176 !matches!(self, Self::Ignore)
177 }
178
179 pub fn is_error(&self) -> bool {
180 matches!(self, Self::Error)
181 }
182
183 pub fn is_warn(&self) -> bool {
184 matches!(self, Self::Warn)
185 }
186}
187
188#[derive(Debug, Default, Clone)]
190pub struct RuleConfig {
191 pub level: Option<RuleLevel>,
195 pub ignore: Vec<String>,
196 pub options: HashMap<String, toml::Value>,
198}
199
200impl<'de> Deserialize<'de> for RuleConfig {
201 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202 where
203 D: serde::Deserializer<'de>,
204 {
205 struct RuleConfigVisitor;
206
207 impl<'de> serde::de::Visitor<'de> for RuleConfigVisitor {
208 type Value = RuleConfig;
209
210 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
211 formatter
212 .write_str("a boolean, \"error\"/\"warn\"/\"ignore\", or a RuleConfig struct")
213 }
214
215 fn visit_bool<E>(self, value: bool) -> Result<RuleConfig, E>
216 where
217 E: de::Error,
218 {
219 Ok(RuleConfig {
220 level: Some(if value {
221 RuleLevel::Error
222 } else {
223 RuleLevel::Ignore
224 }),
225 ignore: Vec::new(),
226 options: HashMap::new(),
227 })
228 }
229
230 fn visit_str<E>(self, value: &str) -> Result<RuleConfig, E>
231 where
232 E: de::Error,
233 {
234 let level = match value {
235 "error" => RuleLevel::Error,
236 "warn" => RuleLevel::Warn,
237 "ignore" => RuleLevel::Ignore,
238 _ => {
239 return Err(de::Error::unknown_variant(
240 value,
241 &["error", "warn", "ignore"],
242 ));
243 }
244 };
245 Ok(RuleConfig {
246 level: Some(level),
247 ignore: Vec::new(),
248 options: HashMap::new(),
249 })
250 }
251
252 fn visit_map<M>(self, mut map: M) -> Result<RuleConfig, M::Error>
253 where
254 M: serde::de::MapAccess<'de>,
255 {
256 let mut level: Option<RuleLevel> = None;
257 let mut ignore = None;
258 let mut options = HashMap::new();
259
260 while let Some(key) = map.next_key::<String>()? {
261 match key.as_str() {
262 "level" => {
263 if level.is_some() {
264 return Err(de::Error::duplicate_field("level"));
265 }
266 let level_str: String = map.next_value()?;
267 level = Some(match level_str.as_str() {
268 "error" => RuleLevel::Error,
269 "warn" => RuleLevel::Warn,
270 "ignore" => RuleLevel::Ignore,
271 _ => {
272 return Err(de::Error::unknown_variant(
273 &level_str,
274 &["error", "warn", "ignore"],
275 ));
276 }
277 });
278 }
279 "ignore" => {
280 if ignore.is_some() {
281 return Err(de::Error::duplicate_field("ignore"));
282 }
283 ignore = Some(map.next_value()?);
284 }
285 _ => {
286 let value: toml::Value = map.next_value()?;
288 options.insert(key, value);
289 }
290 }
291 }
292
293 Ok(RuleConfig {
294 level, ignore: ignore.unwrap_or_default(),
296 options,
297 })
298 }
299 }
300
301 deserializer.deserialize_any(RuleConfigVisitor)
302 }
303}
304
305macro_rules! impl_rules_config {
306 ($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
307 #[derive(Debug, Clone, Deserialize, Default)]
308 pub struct RulesConfig {
309 $(
310 #[serde(default)]
311 pub $config_field: RuleConfig,
312 )*
313 }
314 };
315}
316
317for_each_rule!(impl_rules_config);
318
319impl Config {
320 pub fn load(path: &Path) -> Result<Self> {
321 if !path.exists() {
322 return Ok(Self::default());
324 }
325
326 let content = fs::read_to_string(path)
327 .with_context(|| format!("Failed to read config file: {}", path.display()))?;
328
329 let config: Self = toml::from_str(&content)
330 .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
331
332 Ok(config)
333 }
334
335 pub fn build_ignore_matcher(&self) -> Result<GlobSet> {
336 let mut builder = GlobSetBuilder::new();
337
338 for pattern in &self.ignore {
339 let glob = Glob::new(pattern)
340 .with_context(|| format!("Invalid ignore pattern: {}", pattern))?;
341 builder.add(glob);
342 }
343
344 builder.build().context("Failed to build ignore matcher")
345 }
346
347 pub fn build_rule_ignore_matcher(&self, rule_config: &RuleConfig) -> Result<GlobSet> {
350 let mut builder = GlobSetBuilder::new();
351
352 for pattern in &self.ignore {
354 let glob = Glob::new(pattern)
355 .with_context(|| format!("Invalid ignore pattern: {}", pattern))?;
356 builder.add(glob);
357 }
358
359 for pattern in &rule_config.ignore {
361 let glob = Glob::new(pattern)
362 .with_context(|| format!("Invalid ignore pattern: {}", pattern))?;
363 builder.add(glob);
364 }
365
366 builder.build().context("Failed to build ignore matcher")
367 }
368
369 pub fn get_rule_config(&self, field_name: &str) -> Option<&RuleConfig> {
371 macro_rules! impl_get_rule_config {
372 ($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
373 match field_name {
374 $(
375 stringify!($config_field) => Some(&self.rules.$config_field),
376 )*
377 _ => None,
378 }
379 };
380 }
381
382 for_each_rule!(impl_get_rule_config)
383 }
384
385 pub fn get_rule_config_mut(&mut self, field_name: &str) -> Option<&mut RuleConfig> {
387 macro_rules! impl_get_rule_config_mut {
388 ($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
389 match field_name {
390 $(
391 stringify!($config_field) => Some(&mut self.rules.$config_field),
392 )*
393 _ => None,
394 }
395 };
396 }
397
398 for_each_rule!(impl_get_rule_config_mut)
399 }
400
401 pub fn get_string_list(&self, rule_name: &str, key: &str) -> Vec<String> {
402 self.get_rule_config(rule_name)
403 .and_then(|rc| rc.options.get(key))
404 .and_then(|v| v.as_array())
405 .map(|arr| {
406 arr.iter()
407 .filter_map(|v| v.as_str().map(String::from))
408 .collect()
409 })
410 .unwrap_or_default()
411 }
412
413 pub fn enable_only_rules(&mut self, rule_names: &[String]) -> Result<()> {
415 let valid_rules: Vec<&str> = {
417 macro_rules! collect_rule_names {
418 ($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
419 vec![$(stringify!($config_field)),*]
420 };
421 }
422 for_each_rule!(collect_rule_names)
423 };
424
425 for rule_name in rule_names {
426 if !valid_rules.contains(&rule_name.as_str()) {
427 anyhow::bail!("Unknown rule: {}", rule_name);
428 }
429 }
430
431 macro_rules! impl_enable_only_rules {
433 ($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
434 {
435 $(
436 self.rules.$config_field.level = Some(if rule_names.iter().any(|r| r == stringify!($config_field)) {
437 RuleLevel::Error
438 } else {
439 RuleLevel::Ignore
440 });
441 )*
442 }
443 };
444 }
445
446 for_each_rule!(impl_enable_only_rules);
447 Ok(())
448 }
449
450 pub fn disable_rules(&mut self, rule_names: &[String]) -> Result<()> {
452 let valid_rules: Vec<&str> = {
454 macro_rules! collect_rule_names {
455 ($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
456 vec![$(stringify!($config_field)),*]
457 };
458 }
459 for_each_rule!(collect_rule_names)
460 };
461
462 for rule_name in rule_names {
463 if !valid_rules.contains(&rule_name.as_str()) {
464 anyhow::bail!("Unknown rule: {}", rule_name);
465 }
466 }
467
468 macro_rules! impl_disable_rules {
470 ($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
471 {
472 $(
473 if rule_names.iter().any(|r| r == stringify!($config_field)) {
474 self.rules.$config_field.level = Some(RuleLevel::Ignore);
475 }
476 )*
477 }
478 };
479 }
480
481 for_each_rule!(impl_disable_rules);
482 Ok(())
483 }
484
485 pub fn filter_by_category(&mut self, category: Category) -> Result<()> {
487 macro_rules! impl_filter_by_category {
488 ($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
489 {
490 $(
491 self.rules.$config_field.level = Some(if $rule_type.category() == category {
492 RuleLevel::Error
493 } else {
494 RuleLevel::Ignore
495 });
496 )*
497 }
498 };
499 }
500
501 for_each_rule!(impl_filter_by_category);
502 Ok(())
503 }
504}