Skip to main content

gobject_linter/
config.rs

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    /// Human-readable colorized output (default)
14    #[default]
15    Text,
16    /// JSON format
17    Json,
18    /// SARIF JSON format for GitHub Code Scanning, VS Code, etc.
19    Sarif,
20    /// GCC-compatible format for Emacs, Vim, and other tools
21    Gcc,
22    /// Gitlab specific Code Quality Report
23    GitlabCodequality,
24}
25
26/// Parse a GLib version string like "2.76" into (major, minor)
27pub 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
37/// Deserialize GLib version from string to (major, minor) tuple
38fn 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    /// Minimum supported GLib version as (major, minor)
132    /// Rules requiring newer GLib versions will be automatically disabled
133    #[serde(default, deserialize_with = "deserialize_glib_version")]
134    pub min_glib_version: Option<(u32, u32)>,
135
136    /// Target MSVC-compatible code
137    #[serde(default)]
138    pub msvc_compatible: bool,
139
140    /// Output format
141    pub format: Option<OutputFormat>,
142
143    /// Editor URL format for clickable links
144    /// Available placeholders: {path}, {line}, {column}
145    /// Examples:
146    ///   VSCode: "vscode://file{path}:{line}:{column}"
147    ///   IntelliJ: "idea://open?file={path}&line={line}"
148    ///   Sublime: "subl://open?url=file://{path}&line={line}&column={column}"
149    pub editor_url: Option<String>,
150
151    /// Build directory for meson introspection (used for dead code analysis)
152    /// If not specified, will search for common build directories (build/,
153    /// builddir/, _build/)
154    pub build_dir: Option<String>,
155
156    /// Default severity level for all non-opt-in rules.
157    /// Opt-in rules always default to "ignore" regardless of this setting.
158    /// Per-rule `level` settings override this.
159    pub default_level: Option<RuleLevel>,
160}
161
162/// Rule severity level
163#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
164#[serde(rename_all = "kebab-case")]
165pub enum RuleLevel {
166    /// Report as error and exit with failure code
167    Error,
168    /// Report as warning but don't fail
169    Warn,
170    /// Disabled/ignored
171    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/// Per-rule configuration
189#[derive(Debug, Default, Clone)]
190pub struct RuleConfig {
191    /// Explicitly configured level, or None if the user never set it.
192    /// None is resolved at runtime: opt-in rules default to Ignore, others to
193    /// Warn.
194    pub level: Option<RuleLevel>,
195    pub ignore: Vec<String>,
196    /// Rule-specific options (e.g., config_header for include_order)
197    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                            // Capture unknown fields as rule-specific options
287                            let value: toml::Value = map.next_value()?;
288                            options.insert(key, value);
289                        }
290                    }
291                }
292
293                Ok(RuleConfig {
294                    level, // None when not set
295                    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 default config if file doesn't exist
323            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    /// Build an ignore matcher for a specific rule, combining global and
348    /// per-rule ignores
349    pub fn build_rule_ignore_matcher(&self, rule_config: &RuleConfig) -> Result<GlobSet> {
350        let mut builder = GlobSetBuilder::new();
351
352        // Add global ignore patterns
353        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        // Add per-rule ignore patterns
360        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    /// Get reference to a rule config by field name
370    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    /// Get mutable reference to a rule config by field name
386    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    /// Enable only specific rules, disabling all others
414    pub fn enable_only_rules(&mut self, rule_names: &[String]) -> Result<()> {
415        // First, validate that all provided rule names exist
416        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        // Now enable only the specified rules
432        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    /// Disable specific rules (all others remain enabled according to config)
451    pub fn disable_rules(&mut self, rule_names: &[String]) -> Result<()> {
452        // First, validate that all provided rule names exist
453        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        // Now disable the specified rules
469        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    /// Filter rules by category, disabling all others
486    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}