use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::utils::types::Severity;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RulesConfig {
#[serde(default)]
pub custom: Vec<CustomRule>,
#[serde(default)]
pub disable: Vec<String>,
#[serde(default)]
pub severity: HashMap<String, SeverityOverride>,
}
impl RulesConfig {
pub fn new() -> Self {
Self::default()
}
pub fn has_custom_rules(&self) -> bool {
!self.custom.is_empty()
}
pub fn has_modifications(&self) -> bool {
!self.disable.is_empty() || !self.severity.is_empty()
}
pub fn merge(&mut self, other: RulesConfig) {
self.custom.extend(other.custom);
self.disable.extend(other.disable);
self.severity.extend(other.severity);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomRule {
pub code: String,
pub pattern: String,
pub message: String,
#[serde(default = "default_warning")]
pub severity: Severity,
#[serde(default)]
pub suggestion: Option<String>,
#[serde(default)]
pub extensions: Vec<String>,
#[serde(default)]
pub languages: Vec<String>,
#[serde(default = "default_true")]
pub enabled: bool,
}
fn default_warning() -> Severity {
Severity::Warning
}
fn default_true() -> bool {
true
}
impl CustomRule {
pub fn new(
code: impl Into<String>,
pattern: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
code: code.into(),
pattern: pattern.into(),
message: message.into(),
severity: Severity::Warning,
suggestion: None,
extensions: Vec::new(),
languages: Vec::new(),
enabled: true,
}
}
pub fn with_severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
self.extensions = extensions;
self
}
pub fn with_languages(mut self, languages: Vec<String>) -> Self {
self.languages = languages;
self
}
pub fn applies_to_extension(&self, ext: &str) -> bool {
self.extensions.is_empty() || self.extensions.iter().any(|e| e.eq_ignore_ascii_case(ext))
}
pub fn applies_to_language(&self, lang: &str) -> bool {
self.languages.is_empty() || self.languages.iter().any(|l| l.eq_ignore_ascii_case(lang))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SeverityOverride {
Error,
Warning,
Info,
Off,
}
impl SeverityOverride {
pub fn to_severity(self) -> Option<Severity> {
match self {
SeverityOverride::Error => Some(Severity::Error),
SeverityOverride::Warning => Some(Severity::Warning),
SeverityOverride::Info => Some(Severity::Info),
SeverityOverride::Off => None,
}
}
}
impl From<SeverityOverride> for Option<Severity> {
fn from(override_val: SeverityOverride) -> Self {
override_val.to_severity()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rules_config_default() {
let config = RulesConfig::default();
assert!(config.custom.is_empty());
assert!(config.disable.is_empty());
assert!(config.severity.is_empty());
assert!(!config.has_custom_rules());
assert!(!config.has_modifications());
}
#[test]
fn test_rules_config_merge() {
let mut config1 = RulesConfig::default();
config1.disable.push("E001".to_string());
let mut config2 = RulesConfig::default();
config2.disable.push("W001".to_string());
config2
.custom
.push(CustomRule::new("custom/test", "TODO", "Found TODO"));
config1.merge(config2);
assert_eq!(config1.disable.len(), 2);
assert_eq!(config1.custom.len(), 1);
}
#[test]
fn test_custom_rule_builder() {
let rule = CustomRule::new("custom/no-print", r"print\(", "No print statements")
.with_severity(Severity::Error)
.with_suggestion("Use logging instead")
.with_extensions(vec!["py".to_string()])
.with_languages(vec!["python".to_string()]);
assert_eq!(rule.code, "custom/no-print");
assert_eq!(rule.severity, Severity::Error);
assert!(rule.suggestion.is_some());
assert!(rule.applies_to_extension("py"));
assert!(!rule.applies_to_extension("rs"));
assert!(rule.applies_to_language("python"));
assert!(!rule.applies_to_language("rust"));
}
#[test]
fn test_custom_rule_applies_to_all() {
let rule = CustomRule::new("custom/test", "test", "Test message");
assert!(rule.applies_to_extension("rs"));
assert!(rule.applies_to_extension("py"));
assert!(rule.applies_to_language("rust"));
assert!(rule.applies_to_language("python"));
}
#[test]
fn test_severity_override_to_severity() {
assert_eq!(SeverityOverride::Error.to_severity(), Some(Severity::Error));
assert_eq!(
SeverityOverride::Warning.to_severity(),
Some(Severity::Warning)
);
assert_eq!(SeverityOverride::Info.to_severity(), Some(Severity::Info));
assert_eq!(SeverityOverride::Off.to_severity(), None);
}
#[test]
fn test_deserialize_rules_config() {
let toml = r#"
disable = ["E501", "W001"]
[severity]
"E001" = "error"
"W002" = "off"
[[custom]]
code = "custom/no-todo"
pattern = "TODO"
message = "Found TODO comment"
severity = "warning"
"#;
let config: RulesConfig = toml::from_str(toml).unwrap();
assert_eq!(config.disable.len(), 2);
assert_eq!(config.severity.len(), 2);
assert_eq!(config.custom.len(), 1);
assert_eq!(config.custom[0].code, "custom/no-todo");
}
}