use std::fmt;
use mdwright_document::Document;
use crate::LintOptions;
use crate::diagnostic::Diagnostic;
use crate::rule::LintRule;
use crate::stdlib;
use crate::suppression::SuppressionMap;
#[derive(Default)]
pub struct RuleSet {
rules: Vec<Box<dyn LintRule>>,
}
impl RuleSet {
#[must_use]
pub fn new() -> Self {
Self { rules: Vec::new() }
}
#[must_use]
pub fn stdlib_defaults() -> Self {
stdlib::defaults()
}
#[must_use]
pub fn stdlib_all() -> Self {
stdlib::all()
}
pub fn add(&mut self, rule: Box<dyn LintRule>) -> Result<&mut Self, DuplicateRuleName> {
if self.contains(rule.name()) {
return Err(DuplicateRuleName {
name: rule.name().to_owned(),
});
}
self.rules.push(rule);
Ok(self)
}
pub fn remove(&mut self, name: &str) -> bool {
let before = self.rules.len();
self.rules.retain(|r| r.name() != name);
self.rules.len() != before
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.rules.iter().any(|r| r.name() == name)
}
pub fn iter(&self) -> impl Iterator<Item = &dyn LintRule> {
self.rules.iter().map(|b| &**b)
}
#[must_use]
pub fn by_name(&self, name: &str) -> Option<&dyn LintRule> {
self.rules.iter().find(|r| r.name() == name).map(|b| &**b)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.rules.iter().map(|r| r.name())
}
#[must_use]
pub fn check(&self, doc: &Document) -> Vec<Diagnostic> {
self.check_with(doc, LintOptions::default())
}
#[must_use]
pub fn check_with(&self, doc: &Document, opts: LintOptions) -> Vec<Diagnostic> {
let mut out = Vec::new();
for rule in self.iter() {
let before = out.len();
rule.check(doc, &mut out);
let name_owned = rule.name().to_owned();
let advisory = rule.is_advisory();
for d in out.get_mut(before..).into_iter().flatten() {
d.rule = std::borrow::Cow::Owned(name_owned.clone());
d.advisory = advisory;
}
}
if opts.respect_suppressions {
let user_names: Vec<String> = self.iter().map(|r| r.name().to_owned()).collect();
let mut known: Vec<&str> = stdlib::names().collect();
for n in &user_names {
let s: &str = n.as_str();
if !known.contains(&s) {
known.push(s);
}
}
let (map, unknown) = SuppressionMap::build(doc.source(), doc.line_index(), doc.suppressions(), &known);
out.retain(|d| !map.suppresses(&d.rule, &d.span));
out.extend(unknown);
}
out.sort_by(|a, b| {
a.line
.cmp(&b.line)
.then(a.column.cmp(&b.column))
.then_with(|| a.rule.cmp(&b.rule))
});
out
}
#[must_use]
pub fn len(&self) -> usize {
self.rules.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
}
impl IntoIterator for RuleSet {
type Item = Box<dyn LintRule>;
type IntoIter = std::vec::IntoIter<Box<dyn LintRule>>;
fn into_iter(self) -> Self::IntoIter {
self.rules.into_iter()
}
}
impl fmt::Debug for RuleSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RuleSet")
.field("rules", &self.rules.iter().map(|r| r.name()).collect::<Vec<_>>())
.finish()
}
}
#[derive(Debug, Clone)]
pub struct DuplicateRuleName {
pub name: String,
}
impl fmt::Display for DuplicateRuleName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "rule already registered: {}", self.name)
}
}
impl std::error::Error for DuplicateRuleName {}
#[cfg(test)]
mod tests {
use super::{DuplicateRuleName, RuleSet};
use crate::diagnostic::Diagnostic;
use crate::rule::LintRule;
use mdwright_document::Document;
struct Noop(&'static str);
impl LintRule for Noop {
fn name(&self) -> &str {
self.0
}
fn description(&self) -> &str {
"noop"
}
fn check(&self, _doc: &Document, _out: &mut Vec<Diagnostic>) {}
}
#[test]
fn add_and_contains() -> anyhow::Result<()> {
let mut rs = RuleSet::new();
rs.add(Box::new(Noop("a"))).map_err(|e| anyhow::anyhow!("{e}"))?;
assert!(rs.contains("a"));
assert!(!rs.contains("b"));
Ok(())
}
#[test]
fn duplicate_add_errors() -> anyhow::Result<()> {
let mut rs = RuleSet::new();
rs.add(Box::new(Noop("a"))).map_err(|e| anyhow::anyhow!("{e}"))?;
let err = rs.add(Box::new(Noop("a")));
assert!(matches!(err, Err(DuplicateRuleName { ref name }) if name == "a"));
Ok(())
}
#[test]
fn remove_works() -> anyhow::Result<()> {
let mut rs = RuleSet::new();
rs.add(Box::new(Noop("a"))).map_err(|e| anyhow::anyhow!("{e}"))?;
assert!(rs.remove("a"));
assert!(!rs.remove("a"));
assert!(!rs.contains("a"));
Ok(())
}
#[test]
fn by_name_finds_or_returns_none() -> anyhow::Result<()> {
let mut rs = RuleSet::new();
rs.add(Box::new(Noop("a"))).map_err(|e| anyhow::anyhow!("{e}"))?;
rs.add(Box::new(Noop("b"))).map_err(|e| anyhow::anyhow!("{e}"))?;
assert_eq!(rs.by_name("a").map(LintRule::name), Some("a"));
assert_eq!(rs.by_name("b").map(LintRule::name), Some("b"));
assert!(rs.by_name("c").is_none());
Ok(())
}
#[test]
fn names_iterates_in_insertion_order() -> anyhow::Result<()> {
let mut rs = RuleSet::new();
rs.add(Box::new(Noop("alpha"))).map_err(|e| anyhow::anyhow!("{e}"))?;
rs.add(Box::new(Noop("beta"))).map_err(|e| anyhow::anyhow!("{e}"))?;
rs.add(Box::new(Noop("gamma"))).map_err(|e| anyhow::anyhow!("{e}"))?;
let collected: Vec<&str> = rs.names().collect();
assert_eq!(collected, vec!["alpha", "beta", "gamma"]);
Ok(())
}
#[test]
fn into_iter_yields_owned_boxes_in_insertion_order() -> anyhow::Result<()> {
let mut rs = RuleSet::new();
rs.add(Box::new(Noop("first"))).map_err(|e| anyhow::anyhow!("{e}"))?;
rs.add(Box::new(Noop("second"))).map_err(|e| anyhow::anyhow!("{e}"))?;
let names: Vec<String> = rs.into_iter().map(|r| r.name().to_owned()).collect();
assert_eq!(names, vec!["first".to_owned(), "second".to_owned()]);
Ok(())
}
}