mdwright_lint/
rule_set.rs1use std::fmt;
15
16use mdwright_document::Document;
17
18use crate::LintOptions;
19use crate::diagnostic::Diagnostic;
20use crate::rule::LintRule;
21use crate::stdlib;
22use crate::suppression::SuppressionMap;
23
24#[derive(Default)]
26pub struct RuleSet {
27 rules: Vec<Box<dyn LintRule>>,
28}
29
30impl RuleSet {
31 #[must_use]
33 pub fn new() -> Self {
34 Self { rules: Vec::new() }
35 }
36
37 #[must_use]
40 pub fn stdlib_defaults() -> Self {
41 stdlib::defaults()
42 }
43
44 #[must_use]
47 pub fn stdlib_all() -> Self {
48 stdlib::all()
49 }
50
51 pub fn add(&mut self, rule: Box<dyn LintRule>) -> Result<&mut Self, DuplicateRuleName> {
58 if self.contains(rule.name()) {
59 return Err(DuplicateRuleName {
60 name: rule.name().to_owned(),
61 });
62 }
63 self.rules.push(rule);
64 Ok(self)
65 }
66
67 pub fn remove(&mut self, name: &str) -> bool {
70 let before = self.rules.len();
71 self.rules.retain(|r| r.name() != name);
72 self.rules.len() != before
73 }
74
75 #[must_use]
76 pub fn contains(&self, name: &str) -> bool {
77 self.rules.iter().any(|r| r.name() == name)
78 }
79
80 pub fn iter(&self) -> impl Iterator<Item = &dyn LintRule> {
81 self.rules.iter().map(|b| &**b)
82 }
83
84 #[must_use]
86 pub fn by_name(&self, name: &str) -> Option<&dyn LintRule> {
87 self.rules.iter().find(|r| r.name() == name).map(|b| &**b)
88 }
89
90 pub fn names(&self) -> impl Iterator<Item = &str> {
92 self.rules.iter().map(|r| r.name())
93 }
94
95 #[must_use]
97 pub fn check(&self, doc: &Document) -> Vec<Diagnostic> {
98 self.check_with(doc, LintOptions::default())
99 }
100
101 #[must_use]
103 pub fn check_with(&self, doc: &Document, opts: LintOptions) -> Vec<Diagnostic> {
104 let mut out = Vec::new();
105 for rule in self.iter() {
106 let before = out.len();
107 rule.check(doc, &mut out);
108 let name_owned = rule.name().to_owned();
109 let advisory = rule.is_advisory();
110 for d in out.get_mut(before..).into_iter().flatten() {
115 if d.rule.is_empty() {
116 d.rule = std::borrow::Cow::Owned(name_owned.clone());
117 }
118 d.advisory = advisory;
119 }
120 }
121
122 if opts.respect_suppressions {
123 let user_names: Vec<String> = self.iter().map(|r| r.name().to_owned()).collect();
124 let mut known: Vec<&str> = stdlib::names().collect();
125 for n in &user_names {
126 let s: &str = n.as_str();
127 if !known.contains(&s) {
128 known.push(s);
129 }
130 }
131 let (map, unknown) = SuppressionMap::build(doc.source(), doc.line_index(), doc.suppressions(), &known);
132 out.retain(|d| !map.suppresses(&d.rule, &d.span));
133 out.extend(unknown);
134 }
135
136 out.sort_by(|a, b| {
137 a.line
138 .cmp(&b.line)
139 .then(a.column.cmp(&b.column))
140 .then_with(|| a.rule.cmp(&b.rule))
141 });
142 out
143 }
144
145 #[must_use]
146 pub fn len(&self) -> usize {
147 self.rules.len()
148 }
149
150 #[must_use]
151 pub fn is_empty(&self) -> bool {
152 self.rules.is_empty()
153 }
154}
155
156impl IntoIterator for RuleSet {
162 type Item = Box<dyn LintRule>;
163 type IntoIter = std::vec::IntoIter<Box<dyn LintRule>>;
164
165 fn into_iter(self) -> Self::IntoIter {
166 self.rules.into_iter()
167 }
168}
169
170impl fmt::Debug for RuleSet {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 f.debug_struct("RuleSet")
173 .field("rules", &self.rules.iter().map(|r| r.name()).collect::<Vec<_>>())
174 .finish()
175 }
176}
177
178#[derive(Debug, Clone)]
181pub struct DuplicateRuleName {
182 pub name: String,
183}
184
185impl fmt::Display for DuplicateRuleName {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 write!(f, "rule already registered: {}", self.name)
188 }
189}
190
191impl std::error::Error for DuplicateRuleName {}
192
193#[cfg(test)]
194mod tests {
195 use super::{DuplicateRuleName, RuleSet};
196 use crate::diagnostic::Diagnostic;
197 use crate::rule::LintRule;
198 use mdwright_document::Document;
199
200 struct Noop(&'static str);
201 impl LintRule for Noop {
202 fn name(&self) -> &str {
203 self.0
204 }
205 fn description(&self) -> &str {
206 "noop"
207 }
208 fn check(&self, _doc: &Document, _out: &mut Vec<Diagnostic>) {}
209 }
210
211 #[test]
212 fn add_and_contains() -> anyhow::Result<()> {
213 let mut rs = RuleSet::new();
214 rs.add(Box::new(Noop("a"))).map_err(|e| anyhow::anyhow!("{e}"))?;
215 assert!(rs.contains("a"));
216 assert!(!rs.contains("b"));
217 Ok(())
218 }
219
220 #[test]
221 fn duplicate_add_errors() -> anyhow::Result<()> {
222 let mut rs = RuleSet::new();
223 rs.add(Box::new(Noop("a"))).map_err(|e| anyhow::anyhow!("{e}"))?;
224 let err = rs.add(Box::new(Noop("a")));
225 assert!(matches!(err, Err(DuplicateRuleName { ref name }) if name == "a"));
226 Ok(())
227 }
228
229 #[test]
230 fn remove_works() -> anyhow::Result<()> {
231 let mut rs = RuleSet::new();
232 rs.add(Box::new(Noop("a"))).map_err(|e| anyhow::anyhow!("{e}"))?;
233 assert!(rs.remove("a"));
234 assert!(!rs.remove("a"));
235 assert!(!rs.contains("a"));
236 Ok(())
237 }
238
239 #[test]
240 fn by_name_finds_or_returns_none() -> anyhow::Result<()> {
241 let mut rs = RuleSet::new();
242 rs.add(Box::new(Noop("a"))).map_err(|e| anyhow::anyhow!("{e}"))?;
243 rs.add(Box::new(Noop("b"))).map_err(|e| anyhow::anyhow!("{e}"))?;
244 assert_eq!(rs.by_name("a").map(LintRule::name), Some("a"));
245 assert_eq!(rs.by_name("b").map(LintRule::name), Some("b"));
246 assert!(rs.by_name("c").is_none());
247 Ok(())
248 }
249
250 #[test]
251 fn names_iterates_in_insertion_order() -> anyhow::Result<()> {
252 let mut rs = RuleSet::new();
253 rs.add(Box::new(Noop("alpha"))).map_err(|e| anyhow::anyhow!("{e}"))?;
254 rs.add(Box::new(Noop("beta"))).map_err(|e| anyhow::anyhow!("{e}"))?;
255 rs.add(Box::new(Noop("gamma"))).map_err(|e| anyhow::anyhow!("{e}"))?;
256 let collected: Vec<&str> = rs.names().collect();
257 assert_eq!(collected, vec!["alpha", "beta", "gamma"]);
258 Ok(())
259 }
260
261 struct MultiCode;
262 impl LintRule for MultiCode {
263 fn name(&self) -> &str {
264 "umbrella"
265 }
266 fn description(&self) -> &str {
267 "emits diagnostics under several rule codes"
268 }
269 fn check(&self, _doc: &Document, out: &mut Vec<Diagnostic>) {
270 out.push(Diagnostic {
271 rule: std::borrow::Cow::Borrowed("umbrella/sub-a"),
272 line: 1,
273 column: 1,
274 span: 0..0,
275 message: String::new(),
276 fix: None,
277 advisory: false,
278 });
279 out.push(Diagnostic {
280 rule: std::borrow::Cow::Borrowed(""),
281 line: 1,
282 column: 2,
283 span: 0..0,
284 message: String::new(),
285 fix: None,
286 advisory: false,
287 });
288 }
289 }
290
291 #[test]
292 fn pre_stamped_rule_codes_survive_dispatch() -> anyhow::Result<()> {
293 let mut rs = RuleSet::new();
294 rs.add(Box::new(MultiCode)).map_err(|e| anyhow::anyhow!("{e}"))?;
295 let doc = Document::parse("")?;
296 let diagnostics = rs.check(&doc);
297 let codes: Vec<&str> = diagnostics.iter().map(|d| d.rule.as_ref()).collect();
298 assert_eq!(codes, vec!["umbrella/sub-a", "umbrella"]);
299 Ok(())
300 }
301
302 #[test]
303 fn into_iter_yields_owned_boxes_in_insertion_order() -> anyhow::Result<()> {
304 let mut rs = RuleSet::new();
305 rs.add(Box::new(Noop("first"))).map_err(|e| anyhow::anyhow!("{e}"))?;
306 rs.add(Box::new(Noop("second"))).map_err(|e| anyhow::anyhow!("{e}"))?;
307 let names: Vec<String> = rs.into_iter().map(|r| r.name().to_owned()).collect();
308 assert_eq!(names, vec!["first".to_owned(), "second".to_owned()]);
309 Ok(())
310 }
311}