use std::{collections::HashMap, fmt, fs, path::Path};
use anyhow::{Context, Result};
use clap::ValueEnum;
use globset::{Glob, GlobSet, GlobSetBuilder};
use serde::{Deserialize, de};
use crate::{for_each_rule, rules::*};
#[derive(Default, Debug, Clone, Copy, ValueEnum, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum OutputFormat {
#[default]
Text,
Json,
Sarif,
Gcc,
GitlabCodequality,
}
pub fn parse_glib_version(version: &str) -> Option<(u32, u32)> {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() != 2 {
return None;
}
let major = parts[0].parse::<u32>().ok()?;
let minor = parts[1].parse::<u32>().ok()?;
Some((major, minor))
}
fn deserialize_glib_version<'de, D>(deserializer: D) -> Result<Option<(u32, u32)>, D::Error>
where
D: serde::Deserializer<'de>,
{
let version_str: Option<String> = Option::deserialize(deserializer)?;
match version_str {
Some(s) => parse_glib_version(&s).map(Some).ok_or_else(|| {
de::Error::custom(format!(
"Invalid GLib version format: '{}'. Expected format: 'major.minor' (e.g., '2.76')",
s
))
}),
None => Ok(None),
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Style {
#[serde(default = "default_true")]
pub space_before_paren: bool,
}
impl Default for Style {
fn default() -> Self {
Self {
space_before_paren: true,
}
}
}
impl Style {
pub fn format_call(&self, func: &str, args: &[&str]) -> String {
let sep = if self.space_before_paren { " (" } else { "(" };
let mut s =
String::with_capacity(func.len() + 2 + args.iter().map(|a| a.len() + 2).sum::<usize>());
s.push_str(func);
s.push_str(sep);
for (i, arg) in args.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
s.push_str(arg);
}
s.push(')');
s
}
pub fn format_call_stmt(&self, func: &str, args: &[&str]) -> String {
let mut s = self.format_call(func, args);
s.push(';');
s
}
pub fn format_addr_call(&self, func: &str, var: &str, extra_args: &[&str]) -> String {
let sep = if self.space_before_paren { " (" } else { "(" };
let mut s = String::with_capacity(
func.len() + 4 + var.len() + extra_args.iter().map(|a| a.len() + 2).sum::<usize>(),
);
s.push_str(func);
s.push_str(sep);
s.push('&');
s.push_str(var);
for arg in extra_args {
s.push_str(", ");
s.push_str(arg);
}
s.push(')');
s
}
pub fn format_addr_call_stmt(&self, func: &str, var: &str, extra_args: &[&str]) -> String {
let mut s = self.format_addr_call(func, var, extra_args);
s.push(';');
s
}
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct Config {
#[serde(default)]
pub style: Style,
#[serde(default)]
pub rules: RulesConfig,
#[serde(default)]
pub ignore: Vec<String>,
#[serde(default, deserialize_with = "deserialize_glib_version")]
pub min_glib_version: Option<(u32, u32)>,
#[serde(default)]
pub msvc_compatible: bool,
pub format: Option<OutputFormat>,
pub editor_url: Option<String>,
pub build_dir: Option<String>,
pub default_level: Option<RuleLevel>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RuleLevel {
Error,
Warn,
Ignore,
}
impl RuleLevel {
pub fn is_enabled(&self) -> bool {
!matches!(self, Self::Ignore)
}
pub fn is_error(&self) -> bool {
matches!(self, Self::Error)
}
pub fn is_warn(&self) -> bool {
matches!(self, Self::Warn)
}
}
#[derive(Debug, Default, Clone)]
pub struct RuleConfig {
pub level: Option<RuleLevel>,
pub ignore: Vec<String>,
pub options: HashMap<String, toml::Value>,
}
impl<'de> Deserialize<'de> for RuleConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct RuleConfigVisitor;
impl<'de> serde::de::Visitor<'de> for RuleConfigVisitor {
type Value = RuleConfig;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter
.write_str("a boolean, \"error\"/\"warn\"/\"ignore\", or a RuleConfig struct")
}
fn visit_bool<E>(self, value: bool) -> Result<RuleConfig, E>
where
E: de::Error,
{
Ok(RuleConfig {
level: Some(if value {
RuleLevel::Error
} else {
RuleLevel::Ignore
}),
ignore: Vec::new(),
options: HashMap::new(),
})
}
fn visit_str<E>(self, value: &str) -> Result<RuleConfig, E>
where
E: de::Error,
{
let level = match value {
"error" => RuleLevel::Error,
"warn" => RuleLevel::Warn,
"ignore" => RuleLevel::Ignore,
_ => {
return Err(de::Error::unknown_variant(
value,
&["error", "warn", "ignore"],
));
}
};
Ok(RuleConfig {
level: Some(level),
ignore: Vec::new(),
options: HashMap::new(),
})
}
fn visit_map<M>(self, mut map: M) -> Result<RuleConfig, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut level: Option<RuleLevel> = None;
let mut ignore = None;
let mut options = HashMap::new();
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"level" => {
if level.is_some() {
return Err(de::Error::duplicate_field("level"));
}
let level_str: String = map.next_value()?;
level = Some(match level_str.as_str() {
"error" => RuleLevel::Error,
"warn" => RuleLevel::Warn,
"ignore" => RuleLevel::Ignore,
_ => {
return Err(de::Error::unknown_variant(
&level_str,
&["error", "warn", "ignore"],
));
}
});
}
"ignore" => {
if ignore.is_some() {
return Err(de::Error::duplicate_field("ignore"));
}
ignore = Some(map.next_value()?);
}
_ => {
let value: toml::Value = map.next_value()?;
options.insert(key, value);
}
}
}
Ok(RuleConfig {
level, ignore: ignore.unwrap_or_default(),
options,
})
}
}
deserializer.deserialize_any(RuleConfigVisitor)
}
}
macro_rules! impl_rules_config {
($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
#[derive(Debug, Clone, Deserialize, Default)]
pub struct RulesConfig {
$(
#[serde(default)]
pub $config_field: RuleConfig,
)*
}
};
}
for_each_rule!(impl_rules_config);
impl Config {
pub fn load(path: &Path) -> Result<Self> {
if !path.exists() {
return Ok(Self::default());
}
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
let config: Self = toml::from_str(&content)
.with_context(|| format!("Failed to parse config file: {}", path.display()))?;
Ok(config)
}
pub fn build_ignore_matcher(&self) -> Result<GlobSet> {
let mut builder = GlobSetBuilder::new();
for pattern in &self.ignore {
let glob = Glob::new(pattern)
.with_context(|| format!("Invalid ignore pattern: {}", pattern))?;
builder.add(glob);
}
builder.build().context("Failed to build ignore matcher")
}
pub fn build_rule_ignore_matcher(&self, rule_config: &RuleConfig) -> Result<GlobSet> {
let mut builder = GlobSetBuilder::new();
for pattern in &self.ignore {
let glob = Glob::new(pattern)
.with_context(|| format!("Invalid ignore pattern: {}", pattern))?;
builder.add(glob);
}
for pattern in &rule_config.ignore {
let glob = Glob::new(pattern)
.with_context(|| format!("Invalid ignore pattern: {}", pattern))?;
builder.add(glob);
}
builder.build().context("Failed to build ignore matcher")
}
pub fn get_rule_config(&self, field_name: &str) -> Option<&RuleConfig> {
macro_rules! impl_get_rule_config {
($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
match field_name {
$(
stringify!($config_field) => Some(&self.rules.$config_field),
)*
_ => None,
}
};
}
for_each_rule!(impl_get_rule_config)
}
pub fn get_rule_config_mut(&mut self, field_name: &str) -> Option<&mut RuleConfig> {
macro_rules! impl_get_rule_config_mut {
($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
match field_name {
$(
stringify!($config_field) => Some(&mut self.rules.$config_field),
)*
_ => None,
}
};
}
for_each_rule!(impl_get_rule_config_mut)
}
pub fn get_string_list(&self, rule_name: &str, key: &str) -> Vec<String> {
self.get_rule_config(rule_name)
.and_then(|rc| rc.options.get(key))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
pub fn enable_only_rules(&mut self, rule_names: &[String]) -> Result<()> {
let valid_rules: Vec<&str> = {
macro_rules! collect_rule_names {
($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
vec![$(stringify!($config_field)),*]
};
}
for_each_rule!(collect_rule_names)
};
for rule_name in rule_names {
if !valid_rules.contains(&rule_name.as_str()) {
anyhow::bail!("Unknown rule: {}", rule_name);
}
}
macro_rules! impl_enable_only_rules {
($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
{
$(
self.rules.$config_field.level = Some(if rule_names.iter().any(|r| r == stringify!($config_field)) {
RuleLevel::Error
} else {
RuleLevel::Ignore
});
)*
}
};
}
for_each_rule!(impl_enable_only_rules);
Ok(())
}
pub fn disable_rules(&mut self, rule_names: &[String]) -> Result<()> {
let valid_rules: Vec<&str> = {
macro_rules! collect_rule_names {
($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
vec![$(stringify!($config_field)),*]
};
}
for_each_rule!(collect_rule_names)
};
for rule_name in rule_names {
if !valid_rules.contains(&rule_name.as_str()) {
anyhow::bail!("Unknown rule: {}", rule_name);
}
}
macro_rules! impl_disable_rules {
($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
{
$(
if rule_names.iter().any(|r| r == stringify!($config_field)) {
self.rules.$config_field.level = Some(RuleLevel::Ignore);
}
)*
}
};
}
for_each_rule!(impl_disable_rules);
Ok(())
}
pub fn filter_by_category(&mut self, category: Category) -> Result<()> {
macro_rules! impl_filter_by_category {
($(($config_field:ident, $rule_type:ident)),* $(,)?) => {
{
$(
self.rules.$config_field.level = Some(if $rule_type.category() == category {
RuleLevel::Error
} else {
RuleLevel::Ignore
});
)*
}
};
}
for_each_rule!(impl_filter_by_category);
Ok(())
}
}