use crate::{Result, parser::UnifiedRule};
#[derive(Debug, Default)]
pub struct Linter {
check_duplicates: bool,
check_vague: bool,
}
impl Linter {
pub fn new() -> Self {
Self {
check_duplicates: true,
check_vague: true,
}
}
pub fn with_duplicate_check(mut self, enabled: bool) -> Self {
self.check_duplicates = enabled;
self
}
pub fn lint(&self, rules: &[UnifiedRule]) -> Result<Vec<LintResult>> {
let mut results = Vec::new();
if self.check_duplicates {
results.extend(self.check_for_duplicates(rules));
}
if self.check_vague {
results.extend(self.check_for_vague(rules));
}
results.extend(self.check_for_empty(rules));
Ok(results)
}
fn check_for_duplicates(&self, rules: &[UnifiedRule]) -> Vec<LintResult> {
let mut results = Vec::new();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for rule in rules {
if let UnifiedRule::Standard { description, .. } = rule {
if !seen.insert(description.clone()) {
results.push(LintResult {
severity: LintSeverity::Warning,
message: format!("Duplicate rule: {}", description),
suggestion: Some("Remove one of the duplicate rules".to_string()),
});
}
}
}
results
}
fn check_for_vague(&self, rules: &[UnifiedRule]) -> Vec<LintResult> {
let mut results = Vec::new();
let vague_words = ["good", "proper", "appropriate", "nice", "clean", "better"];
for rule in rules {
if let UnifiedRule::Standard { description, .. } = rule {
let lower = description.to_lowercase();
for word in &vague_words {
if lower.contains(word) && description.len() < 30 {
results.push(LintResult {
severity: LintSeverity::Info,
message: format!(
"Rule may be too vague: '{}' contains '{}'",
description, word
),
suggestion: Some("Consider being more specific".to_string()),
});
break;
}
}
}
}
results
}
fn check_for_empty(&self, rules: &[UnifiedRule]) -> Vec<LintResult> {
let mut results = Vec::new();
for rule in rules {
match rule {
UnifiedRule::Standard { description, .. } if description.trim().is_empty() => {
results.push(LintResult {
severity: LintSeverity::Error,
message: "Empty rule description".to_string(),
suggestion: Some("Add a meaningful description".to_string()),
});
}
UnifiedRule::Persona { name, role, .. }
if name.trim().is_empty() || role.trim().is_empty() =>
{
results.push(LintResult {
severity: LintSeverity::Error,
message: "Persona missing name or role".to_string(),
suggestion: Some("Add name and role to persona".to_string()),
});
}
_ => {}
}
}
results
}
}
#[derive(Debug, Clone)]
pub struct LintResult {
pub severity: LintSeverity,
pub message: String,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LintSeverity {
Error,
Warning,
Info,
}
impl std::fmt::Display for LintSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LintSeverity::Error => write!(f, "error"),
LintSeverity::Warning => write!(f, "warning"),
LintSeverity::Info => write!(f, "info"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::RuleCategory;
#[test]
fn test_linter_empty() {
let linter = Linter::new();
let results = linter.lint(&[]).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_linter_detects_duplicates() {
let linter = Linter::new();
let rules = vec![
UnifiedRule::Standard {
category: RuleCategory::Style,
priority: 0,
description: "Same rule".to_string(),
pattern: None,
},
UnifiedRule::Standard {
category: RuleCategory::Style,
priority: 1,
description: "Same rule".to_string(),
pattern: None,
},
];
let results = linter.lint(&rules).unwrap();
assert!(!results.is_empty());
assert!(results.iter().any(|r| r.message.contains("Duplicate")));
}
#[test]
fn test_linter_detects_empty() {
let linter = Linter::new();
let rules = vec![UnifiedRule::Standard {
category: RuleCategory::Style,
priority: 0,
description: "".to_string(),
pattern: None,
}];
let results = linter.lint(&rules).unwrap();
assert!(!results.is_empty());
assert!(
results
.iter()
.any(|r| matches!(r.severity, LintSeverity::Error))
);
}
}