1use crate::{Document, config::Config, error::Result, rule::Rule, violation::Violation};
2
3pub struct RuleRegistry {
5 rules: Vec<Box<dyn Rule>>,
6}
7
8impl RuleRegistry {
9 pub fn new() -> Self {
11 Self { rules: Vec::new() }
12 }
13
14 pub fn register(&mut self, rule: Box<dyn Rule>) {
19 self.rules.push(rule);
20 }
21
22 pub fn rules(&self) -> &[Box<dyn Rule>] {
24 &self.rules
25 }
26
27 pub fn get_rule(&self, id: &str) -> Option<&dyn Rule> {
31 self.rules.iter().find(|r| r.id() == id).map(|r| r.as_ref())
32 }
33
34 pub fn rule_ids(&self) -> Vec<&'static str> {
38 self.rules.iter().map(|r| r.id()).collect()
39 }
40
41 pub fn get_enabled_rules(&self, config: &Config) -> Vec<&dyn Rule> {
49 self.rules
50 .iter()
51 .filter(|rule| self.should_run_rule(rule.as_ref(), config))
52 .map(|rule| rule.as_ref())
53 .collect()
54 }
55
56 pub fn get_enabled_rules_with_overrides(
62 &self,
63 document: &Document,
64 config: &Config,
65 ) -> Vec<&dyn Rule> {
66 let mut enabled_rules: Vec<&dyn Rule> = self
67 .rules
68 .iter()
69 .filter(|rule| self.should_run_rule(rule.as_ref(), config))
70 .map(|rule| rule.as_ref())
71 .collect();
72
73 let mut rules_to_remove = Vec::new();
75
76 for rule in &enabled_rules {
77 let metadata = rule.metadata();
78 if let Some(overrides_rule_id) = metadata.overrides {
79 if self.is_override_applicable(rule.id(), document) {
81 rules_to_remove.push(overrides_rule_id);
83 }
84 }
85 }
86
87 enabled_rules.retain(|rule| !rules_to_remove.contains(&rule.id()));
89
90 enabled_rules
91 }
92
93 fn is_override_applicable(&self, rule_id: &str, document: &Document) -> bool {
97 match rule_id {
98 "MDBOOK025" => {
99 document
101 .path
102 .file_name()
103 .and_then(|name| name.to_str())
104 .map(|name| name == "SUMMARY.md")
105 .unwrap_or(false)
106 }
107 _ => false,
108 }
109 }
110
111 pub fn should_run_rule(&self, rule: &dyn Rule, config: &Config) -> bool {
119 let rule_id = rule.id();
120 let metadata = rule.metadata();
121
122 if config.disabled_rules.contains(&rule_id.to_string()) {
124 return false;
125 }
126
127 if config.enabled_rules.contains(&rule_id.to_string()) {
129 if metadata.deprecated {
131 self.show_deprecation_warning(rule, config);
132 }
133 return true;
134 }
135
136 if !config.enabled_rules.is_empty() {
138 return false;
139 }
140
141 if config.markdownlint_compatible && rule_id == "MD044" {
143 return false; }
145
146 let category_name = self.category_to_string(&metadata.category);
148
149 if config.disabled_categories.contains(&category_name) {
151 return false;
152 }
153
154 if !config.enabled_categories.is_empty()
156 && !config.enabled_categories.contains(&category_name)
157 {
158 return false;
159 }
160
161 !metadata.deprecated
163 }
164
165 fn category_to_string(&self, category: &crate::rule::RuleCategory) -> String {
167 match category {
168 crate::rule::RuleCategory::Structure => "structure".to_string(),
169 crate::rule::RuleCategory::Formatting => "style".to_string(),
170 crate::rule::RuleCategory::Content => "code".to_string(),
171 crate::rule::RuleCategory::Links => "links".to_string(),
172 crate::rule::RuleCategory::Accessibility => "accessibility".to_string(),
173 crate::rule::RuleCategory::MdBook => "mdbook".to_string(),
174 }
175 }
176
177 fn show_deprecation_warning(&self, rule: &dyn Rule, config: &Config) {
181 let metadata = rule.metadata();
182
183 if !metadata.deprecated {
184 return;
185 }
186
187 let message = if let Some(replacement) = metadata.replacement {
188 format!(
189 "Rule {} is deprecated - {}. Consider using {} instead.",
190 rule.id(),
191 metadata
192 .deprecated_reason
193 .unwrap_or("superseded by newer implementation"),
194 replacement
195 )
196 } else {
197 format!(
198 "Rule {} is deprecated - {}.",
199 rule.id(),
200 metadata
201 .deprecated_reason
202 .unwrap_or("no longer recommended")
203 )
204 };
205
206 match config.deprecated_warning {
207 crate::config::DeprecatedWarningLevel::Warn => {
208 eprintln!("Warning: {message}");
209 }
210 crate::config::DeprecatedWarningLevel::Info => {
211 eprintln!("Info: {message}");
212 }
213 crate::config::DeprecatedWarningLevel::Silent => {
214 }
216 }
217 }
218
219 pub fn check_document_optimized_with_config(
221 &self,
222 document: &Document,
223 config: &Config,
224 ) -> Result<Vec<Violation>> {
225 use comrak::Arena;
226
227 let arena = Arena::new();
229 let ast = document.parse_ast(&arena);
230
231 let mut all_violations = Vec::new();
232 let enabled_rules = self.get_enabled_rules_with_overrides(document, config);
233
234 for rule in enabled_rules {
236 let violations = rule.check_with_ast(document, Some(ast))?;
237 all_violations.extend(violations);
238 }
239
240 let dedup_config = crate::deduplication::DeduplicationConfig::default();
242 let deduplicated_violations =
243 crate::deduplication::deduplicate_violations(all_violations, &dedup_config);
244
245 Ok(deduplicated_violations)
246 }
247
248 pub fn check_document_with_config(
250 &self,
251 document: &Document,
252 config: &Config,
253 ) -> Result<Vec<Violation>> {
254 let mut all_violations = Vec::new();
255 let enabled_rules = self.get_enabled_rules_with_overrides(document, config);
256
257 for rule in enabled_rules {
258 let violations = rule.check(document)?;
259 all_violations.extend(violations);
260 }
261
262 let dedup_config = crate::deduplication::DeduplicationConfig::default();
264 let deduplicated_violations =
265 crate::deduplication::deduplicate_violations(all_violations, &dedup_config);
266
267 Ok(deduplicated_violations)
268 }
269
270 pub fn check_document_optimized(&self, document: &Document) -> Result<Vec<Violation>> {
272 let default_config = Config::default();
274 self.check_document_optimized_with_config(document, &default_config)
275 }
276
277 pub fn check_document(&self, document: &Document) -> Result<Vec<Violation>> {
279 let mut all_violations = Vec::new();
280
281 for rule in &self.rules {
282 let violations = rule.check(document)?;
283 all_violations.extend(violations);
284 }
285
286 let dedup_config = crate::deduplication::DeduplicationConfig::default();
288 let deduplicated_violations =
289 crate::deduplication::deduplicate_violations(all_violations, &dedup_config);
290
291 Ok(deduplicated_violations)
292 }
293
294 pub fn len(&self) -> usize {
296 self.rules.len()
297 }
298
299 pub fn is_empty(&self) -> bool {
301 self.rules.is_empty()
302 }
303}
304
305impl Default for RuleRegistry {
306 fn default() -> Self {
312 Self::new()
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::rule::{Rule, RuleCategory, RuleMetadata};
320 use std::path::PathBuf;
321
322 struct TestRule {
324 id: &'static str,
325 name: &'static str,
326 }
327
328 impl TestRule {
329 fn new(id: &'static str, name: &'static str) -> Self {
330 Self { id, name }
331 }
332 }
333
334 impl Rule for TestRule {
335 fn id(&self) -> &'static str {
336 self.id
337 }
338
339 fn name(&self) -> &'static str {
340 self.name
341 }
342
343 fn description(&self) -> &'static str {
344 "A test rule for testing"
345 }
346
347 fn metadata(&self) -> RuleMetadata {
348 RuleMetadata::stable(RuleCategory::Structure)
349 }
350
351 fn check_with_ast<'a>(
352 &self,
353 _document: &Document,
354 _ast: Option<&'a comrak::nodes::AstNode<'a>>,
355 ) -> Result<Vec<Violation>> {
356 Ok(vec![self.create_violation(
357 format!("Test violation from {}", self.id),
358 1,
359 1,
360 crate::violation::Severity::Warning,
361 )])
362 }
363 }
364
365 #[test]
366 fn test_empty_registry() {
367 let registry = RuleRegistry::new();
368 assert_eq!(registry.len(), 0);
369 assert!(registry.is_empty());
370 assert_eq!(registry.rule_ids(), Vec::<&str>::new());
371 }
372
373 #[test]
374 fn test_rule_registration() {
375 let mut registry = RuleRegistry::new();
376 registry.register(Box::new(TestRule::new("TEST001", "test-rule-1")));
377 registry.register(Box::new(TestRule::new("TEST002", "test-rule-2")));
378
379 assert_eq!(registry.len(), 2);
380 assert!(!registry.is_empty());
381 assert_eq!(registry.rule_ids(), vec!["TEST001", "TEST002"]);
382 }
383
384 #[test]
385 fn test_get_rule() {
386 let mut registry = RuleRegistry::new();
387 registry.register(Box::new(TestRule::new("TEST001", "test-rule")));
388
389 let rule = registry.get_rule("TEST001").unwrap();
390 assert_eq!(rule.id(), "TEST001");
391 assert_eq!(rule.name(), "test-rule");
392
393 assert!(registry.get_rule("NONEXISTENT").is_none());
394 }
395
396 #[test]
397 fn test_rule_filtering_with_config() {
398 let mut registry = RuleRegistry::new();
399 registry.register(Box::new(TestRule::new("TEST001", "test-rule-1")));
400 registry.register(Box::new(TestRule::new("TEST002", "test-rule-2")));
401
402 let config = Config::default();
404 let enabled = registry.get_enabled_rules(&config);
405 assert_eq!(enabled.len(), 2);
406
407 let config = Config {
409 enabled_rules: vec!["TEST001".to_string()],
410 ..Default::default()
411 };
412 let enabled = registry.get_enabled_rules(&config);
413 assert_eq!(enabled.len(), 1);
414 assert_eq!(enabled[0].id(), "TEST001");
415
416 let config = Config {
418 disabled_rules: vec!["TEST002".to_string()],
419 ..Default::default()
420 };
421 let enabled = registry.get_enabled_rules(&config);
422 assert_eq!(enabled.len(), 1);
423 assert_eq!(enabled[0].id(), "TEST001");
424 }
425
426 #[test]
427 fn test_document_checking() {
428 let mut registry = RuleRegistry::new();
429 registry.register(Box::new(TestRule::new("TEST001", "test-rule")));
430
431 let document = Document::new("# Test".to_string(), PathBuf::from("test.md")).unwrap();
432
433 let violations = registry.check_document_optimized(&document).unwrap();
435 assert_eq!(violations.len(), 1);
436 assert_eq!(violations[0].rule_id, "TEST001");
437
438 let violations = registry.check_document(&document).unwrap();
440 assert_eq!(violations.len(), 1);
441 assert_eq!(violations[0].rule_id, "TEST001");
442
443 let config = Config::default();
445 let violations = registry
446 .check_document_optimized_with_config(&document, &config)
447 .unwrap();
448 assert_eq!(violations.len(), 1);
449 assert_eq!(violations[0].rule_id, "TEST001");
450 }
451
452 #[test]
453 fn test_default_registry_is_empty() {
454 let registry = RuleRegistry::default();
455 assert!(registry.is_empty());
456 assert_eq!(registry.len(), 0);
457 }
458}