use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SeverityLevel {
Error,
Warning,
Info,
Off,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default)]
pub struct RuleSetting {
pub enabled: Option<bool>,
pub severity: Option<SeverityLevel>,
#[serde(flatten)]
pub options: BTreeMap<String, toml::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default)]
pub struct LintSettings {
pub enabled: bool,
pub select: Vec<String>,
pub exclude: Vec<String>,
pub exclude_paths: Vec<String>,
pub rules: BTreeMap<String, RuleSetting>,
}
impl Default for LintSettings {
fn default() -> Self {
Self {
enabled: true,
select: Vec::new(),
exclude: Vec::new(),
exclude_paths: Vec::new(),
rules: BTreeMap::new(),
}
}
}
impl LintSettings {
#[must_use]
pub fn is_rule_enabled(&self, code: &str) -> bool {
if !self.enabled {
return false;
}
if let Some(setting) = self.rules.get(code)
&& let Some(enabled) = setting.enabled
{
return enabled;
}
if self.exclude.iter().any(|c| c == code) {
return false;
}
if self.select.is_empty() {
return true;
}
self.select.iter().any(|c| c == code)
}
#[must_use]
pub fn severity_for(&self, code: &str) -> Option<SeverityLevel> {
self.rules.get(code).and_then(|r| r.severity)
}
#[must_use]
pub fn rule_options(&self) -> BTreeMap<String, BTreeMap<String, String>> {
let mut out = BTreeMap::new();
for (code, setting) in &self.rules {
let mut opts = BTreeMap::new();
for (key, value) in &setting.options {
if let Some(s) = toml_value_to_string(value) {
opts.insert(key.clone(), s);
}
}
if !opts.is_empty() {
out.insert(code.clone(), opts);
}
}
out
}
}
fn toml_value_to_string(value: &toml::Value) -> Option<String> {
match value {
toml::Value::String(s) => Some(s.clone()),
toml::Value::Integer(i) => Some(i.to_string()),
toml::Value::Boolean(b) => Some(b.to_string()),
toml::Value::Float(f) => Some(f.to_string()),
toml::Value::Array(items) => {
let parts: Vec<String> = items.iter().filter_map(toml_value_to_string).collect();
Some(parts.join(","))
}
toml::Value::Datetime(_) | toml::Value::Table(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_enables_all() {
let s = LintSettings::default();
assert!(s.is_rule_enabled("RF01"));
}
#[test]
fn master_switch_off() {
let s = LintSettings {
enabled: false,
..Default::default()
};
assert!(!s.is_rule_enabled("RF01"));
}
#[test]
fn select_restricts() {
let s = LintSettings {
select: vec!["RF01".into()],
..Default::default()
};
assert!(s.is_rule_enabled("RF01"));
assert!(!s.is_rule_enabled("CP01"));
}
#[test]
fn per_rule_enabled_wins_over_exclude() {
let mut rules = BTreeMap::new();
rules.insert(
"RF01".to_string(),
RuleSetting {
enabled: Some(true),
..Default::default()
},
);
let s = LintSettings {
exclude: vec!["RF01".into()],
rules,
..Default::default()
};
assert!(s.is_rule_enabled("RF01"));
}
}