use std::collections::{HashMap, HashSet};
use super::config::{RulesConfig, SeverityOverride};
use crate::utils::types::LintIssue;
#[derive(Debug, Clone)]
pub struct RuleFilter {
disabled_codes: HashSet<String>,
disabled_prefixes: Vec<String>,
severity_overrides: HashMap<String, SeverityOverride>,
}
impl Default for RuleFilter {
fn default() -> Self {
Self::new()
}
}
impl RuleFilter {
pub fn new() -> Self {
Self {
disabled_codes: HashSet::new(),
disabled_prefixes: Vec::new(),
severity_overrides: HashMap::new(),
}
}
pub fn from_config(config: &RulesConfig) -> Self {
let mut filter = Self::new();
for code in &config.disable {
if let Some(prefix) = code.strip_suffix("/*") {
filter.disabled_prefixes.push(prefix.to_string());
} else if let Some(prefix) = code.strip_suffix('*') {
filter.disabled_prefixes.push(prefix.to_string());
} else {
filter.disabled_codes.insert(code.clone());
}
}
filter.severity_overrides = config.severity.clone();
filter
}
pub fn merge_language_config(&mut self, lang_rules: Option<&RulesConfig>) {
if let Some(rules) = lang_rules {
for code in &rules.disable {
if let Some(prefix) = code.strip_suffix("/*") {
if !self.disabled_prefixes.contains(&prefix.to_string()) {
self.disabled_prefixes.push(prefix.to_string());
}
} else if let Some(prefix) = code.strip_suffix('*') {
if !self.disabled_prefixes.contains(&prefix.to_string()) {
self.disabled_prefixes.push(prefix.to_string());
}
} else {
self.disabled_codes.insert(code.clone());
}
}
self.severity_overrides.extend(rules.severity.clone());
}
}
pub fn is_disabled(&self, code: &str) -> bool {
if self.disabled_codes.contains(code) {
return true;
}
if let Some(SeverityOverride::Off) = self.severity_overrides.get(code) {
return true;
}
for prefix in &self.disabled_prefixes {
if code.starts_with(prefix) {
return true;
}
}
false
}
pub fn get_severity_override(&self, code: &str) -> Option<SeverityOverride> {
self.severity_overrides.get(code).copied()
}
pub fn filter_issues(&self, issues: Vec<LintIssue>) -> Vec<LintIssue> {
issues
.into_iter()
.filter_map(|mut issue| {
let code = issue.code.as_deref().unwrap_or("unknown");
if self.is_disabled(code) {
return None;
}
if let Some(override_severity) = self.get_severity_override(code) {
match override_severity {
SeverityOverride::Off => return None,
SeverityOverride::Error => {
issue.severity = crate::utils::types::Severity::Error
}
SeverityOverride::Warning => {
issue.severity = crate::utils::types::Severity::Warning
}
SeverityOverride::Info => {
issue.severity = crate::utils::types::Severity::Info
}
}
}
Some(issue)
})
.collect()
}
pub fn stats(&self) -> FilterStats {
FilterStats {
disabled_codes: self.disabled_codes.len(),
disabled_prefixes: self.disabled_prefixes.len(),
severity_overrides: self.severity_overrides.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct FilterStats {
pub disabled_codes: usize,
pub disabled_prefixes: usize,
pub severity_overrides: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::types::Severity;
use std::path::PathBuf;
fn make_issue(code: &str, severity: Severity) -> LintIssue {
let mut issue = LintIssue::new(
PathBuf::from("test.rs"),
1,
"Test message".to_string(),
severity,
);
issue.code = Some(code.to_string());
issue
}
#[test]
fn test_filter_disabled_exact() {
let mut config = RulesConfig::default();
config.disable.push("E501".to_string());
config.disable.push("W001".to_string());
let filter = RuleFilter::from_config(&config);
assert!(filter.is_disabled("E501"));
assert!(filter.is_disabled("W001"));
assert!(!filter.is_disabled("E502"));
}
#[test]
fn test_filter_disabled_prefix() {
let mut config = RulesConfig::default();
config.disable.push("whitespace/*".to_string());
config.disable.push("clippy::needless_*".to_string());
let filter = RuleFilter::from_config(&config);
assert!(filter.is_disabled("whitespace/trailing"));
assert!(filter.is_disabled("whitespace/indent"));
assert!(filter.is_disabled("clippy::needless_return"));
assert!(!filter.is_disabled("formatting/indent"));
}
#[test]
fn test_filter_severity_override() {
let mut config = RulesConfig::default();
config
.severity
.insert("W001".to_string(), SeverityOverride::Error);
config
.severity
.insert("E001".to_string(), SeverityOverride::Info);
config
.severity
.insert("W002".to_string(), SeverityOverride::Off);
let filter = RuleFilter::from_config(&config);
assert_eq!(
filter.get_severity_override("W001"),
Some(SeverityOverride::Error)
);
assert_eq!(
filter.get_severity_override("E001"),
Some(SeverityOverride::Info)
);
assert_eq!(
filter.get_severity_override("W002"),
Some(SeverityOverride::Off)
);
assert_eq!(filter.get_severity_override("X001"), None);
assert!(filter.is_disabled("W002"));
}
#[test]
fn test_filter_issues() {
let mut config = RulesConfig::default();
config.disable.push("E501".to_string());
config
.severity
.insert("W001".to_string(), SeverityOverride::Error);
config
.severity
.insert("W002".to_string(), SeverityOverride::Off);
let filter = RuleFilter::from_config(&config);
let issues = vec![
make_issue("E501", Severity::Error), make_issue("W001", Severity::Warning), make_issue("W002", Severity::Warning), make_issue("E001", Severity::Error), ];
let filtered = filter.filter_issues(issues);
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].code.as_deref(), Some("W001"));
assert_eq!(filtered[0].severity, Severity::Error);
assert_eq!(filtered[1].code.as_deref(), Some("E001"));
assert_eq!(filtered[1].severity, Severity::Error);
}
#[test]
fn test_merge_language_config() {
let mut global_config = RulesConfig::default();
global_config.disable.push("E501".to_string());
global_config
.severity
.insert("W001".to_string(), SeverityOverride::Info);
let mut lang_config = RulesConfig::default();
lang_config.disable.push("E502".to_string());
lang_config
.severity
.insert("W001".to_string(), SeverityOverride::Error);
let mut filter = RuleFilter::from_config(&global_config);
filter.merge_language_config(Some(&lang_config));
assert!(filter.is_disabled("E501")); assert!(filter.is_disabled("E502"));
assert_eq!(
filter.get_severity_override("W001"),
Some(SeverityOverride::Error)
);
}
#[test]
fn test_filter_stats() {
let mut config = RulesConfig::default();
config.disable.push("E501".to_string());
config.disable.push("whitespace/*".to_string());
config
.severity
.insert("W001".to_string(), SeverityOverride::Error);
let filter = RuleFilter::from_config(&config);
let stats = filter.stats();
assert_eq!(stats.disabled_codes, 1);
assert_eq!(stats.disabled_prefixes, 1);
assert_eq!(stats.severity_overrides, 1);
}
}