1use crate::config::Config;
4use crate::error::Result;
5use crate::registry::RuleRegistry;
6use serde_json::Value;
7
8pub trait RuleProvider: Send + Sync {
10 fn provider_id(&self) -> &'static str;
12
13 fn description(&self) -> &'static str;
15
16 fn version(&self) -> &'static str;
18
19 fn register_rules(&self, registry: &mut RuleRegistry);
21
22 fn config_schema(&self) -> Option<Value> {
24 None
25 }
26
27 fn rule_ids(&self) -> Vec<&'static str> {
29 Vec::new()
30 }
31
32 fn initialize(&self) -> Result<()> {
34 Ok(())
35 }
36
37 fn register_rules_with_config(&self, registry: &mut RuleRegistry, _config: Option<&Config>) {
41 self.register_rules(registry);
43 }
44}
45
46#[derive(Default)]
48pub struct PluginRegistry {
49 providers: Vec<Box<dyn RuleProvider>>,
50}
51
52impl PluginRegistry {
53 pub fn new() -> Self {
55 Self {
56 providers: Vec::new(),
57 }
58 }
59
60 pub fn register_provider(&mut self, provider: Box<dyn RuleProvider>) -> Result<()> {
62 provider.initialize()?;
64
65 let provider_id = provider.provider_id();
67 if self
68 .providers
69 .iter()
70 .any(|p| p.provider_id() == provider_id)
71 {
72 return Err(crate::error::MdBookLintError::plugin_error(format!(
73 "Provider with ID '{provider_id}' is already registered"
74 )));
75 }
76
77 self.providers.push(provider);
78 Ok(())
79 }
80
81 pub fn providers(&self) -> &[Box<dyn RuleProvider>] {
83 &self.providers
84 }
85
86 pub fn get_provider(&self, id: &str) -> Option<&dyn RuleProvider> {
88 self.providers
89 .iter()
90 .find(|p| p.provider_id() == id)
91 .map(|p| p.as_ref())
92 }
93
94 pub fn create_rule_registry(&self) -> Result<RuleRegistry> {
96 self.create_rule_registry_with_config(None)
97 }
98
99 pub fn create_rule_registry_with_config(
101 &self,
102 config: Option<&Config>,
103 ) -> Result<RuleRegistry> {
104 let mut registry = RuleRegistry::new();
105
106 for provider in &self.providers {
107 provider.register_rules_with_config(&mut registry, config);
108 }
109
110 Ok(registry)
111 }
112
113 pub fn create_engine(&self) -> Result<LintEngine> {
115 self.create_engine_with_config(None)
116 }
117
118 pub fn create_engine_with_config(&self, config: Option<&Config>) -> Result<LintEngine> {
120 let registry = self.create_rule_registry_with_config(config)?;
121 Ok(LintEngine::with_registry(registry))
122 }
123
124 pub fn available_rule_ids(&self) -> Vec<String> {
126 let mut rule_ids = Vec::new();
127
128 for provider in &self.providers {
129 for rule_id in provider.rule_ids() {
130 rule_ids.push(rule_id.to_string());
131 }
132 }
133
134 rule_ids.sort();
135 rule_ids.dedup();
136 rule_ids
137 }
138
139 pub fn provider_info(&self) -> Vec<ProviderInfo> {
141 self.providers
142 .iter()
143 .map(|p| ProviderInfo {
144 id: p.provider_id().to_string(),
145 description: p.description().to_string(),
146 version: p.version().to_string(),
147 rule_count: p.rule_ids().len(),
148 })
149 .collect()
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct ProviderInfo {
156 pub id: String,
157 pub description: String,
158 pub version: String,
159 pub rule_count: usize,
160}
161
162pub struct LintEngine {
164 registry: RuleRegistry,
165}
166
167impl LintEngine {
168 pub fn new() -> Self {
170 Self {
171 registry: RuleRegistry::new(),
172 }
173 }
174
175 pub fn with_registry(registry: RuleRegistry) -> Self {
177 Self { registry }
178 }
179
180 pub fn registry(&self) -> &RuleRegistry {
182 &self.registry
183 }
184
185 pub fn registry_mut(&mut self) -> &mut RuleRegistry {
187 &mut self.registry
188 }
189
190 pub fn lint_document(&self, document: &crate::Document) -> Result<Vec<crate::Violation>> {
192 self.registry.check_document_optimized(document)
193 }
194
195 pub fn lint_document_with_config(
197 &self,
198 document: &crate::Document,
199 config: &crate::Config,
200 ) -> Result<Vec<crate::Violation>> {
201 self.registry
202 .check_document_optimized_with_config(document, config)
203 }
204
205 pub fn lint_content(&self, content: &str, source_label: &str) -> Result<Vec<crate::Violation>> {
212 let document =
213 crate::Document::new(content.to_string(), std::path::PathBuf::from(source_label))?;
214 self.lint_document(&document)
215 }
216
217 pub fn apply_fix(&self, content: &str, violation: &crate::Violation) -> Option<String> {
233 let fix = violation.fix.as_ref()?;
234
235 let range = fix.byte_range(content)?;
236 let replacement = fix.replacement.as_deref().unwrap_or("");
237 let mut result = content.to_string();
238 result.replace_range(range, replacement);
239 Some(result)
240 }
241
242 pub fn apply_fixes(
258 &self,
259 content: &str,
260 violations: &[crate::Violation],
261 ) -> (String, Vec<crate::Violation>) {
262 if violations.is_empty() {
263 return (content.to_string(), Vec::new());
264 }
265
266 struct PlannedFix<'a> {
267 violation_index: usize,
268 start: usize,
269 end: usize,
270 replacement: &'a str,
271 }
272
273 let mut planned = Vec::new();
276 for (violation_index, violation) in violations.iter().enumerate() {
277 let Some(fix) = violation.fix.as_ref() else {
278 continue;
279 };
280 let Some(range) = fix.byte_range(content) else {
281 continue;
282 };
283
284 let replacement = fix.replacement.as_deref().unwrap_or("");
285 planned.push(PlannedFix {
286 violation_index,
287 start: range.start,
288 end: range.end,
289 replacement,
290 });
291 }
292
293 let mut conflicted = vec![false; planned.len()];
296 for left in 0..planned.len() {
297 for right in (left + 1)..planned.len() {
298 let a = &planned[left];
299 let b = &planned[right];
300 let duplicate =
301 a.start == b.start && a.end == b.end && a.replacement == b.replacement;
302 let overlap = (a.start < b.end && b.start < a.end) || a.start == b.start;
303
304 if !duplicate && overlap {
305 conflicted[left] = true;
306 conflicted[right] = true;
307 }
308 }
309 }
310
311 let mut unique_edits: Vec<&PlannedFix<'_>> = Vec::new();
312 let mut applied_indices = std::collections::HashSet::new();
313 for (planned_index, edit) in planned.iter().enumerate() {
314 if conflicted[planned_index] {
315 continue;
316 }
317
318 applied_indices.insert(edit.violation_index);
319 if !unique_edits.iter().any(|existing| {
320 existing.start == edit.start
321 && existing.end == edit.end
322 && existing.replacement == edit.replacement
323 }) {
324 unique_edits.push(edit);
325 }
326 }
327
328 unique_edits.sort_by(|a, b| b.start.cmp(&a.start).then_with(|| b.end.cmp(&a.end)));
330
331 let mut result = content.to_string();
332 for edit in unique_edits {
333 result.replace_range(edit.start..edit.end, edit.replacement);
334 }
335
336 let unfixed: Vec<crate::Violation> = violations
338 .iter()
339 .enumerate()
340 .filter(|(idx, v)| v.fix.is_none() || !applied_indices.contains(idx))
341 .map(|(_, v)| v.clone())
342 .collect();
343
344 (result, unfixed)
345 }
346
347 pub fn available_rules(&self) -> Vec<&'static str> {
349 self.registry.rule_ids()
350 }
351
352 pub fn enabled_rules(&self, config: &crate::Config) -> Vec<&dyn crate::rule::Rule> {
354 self.registry.get_enabled_rules(config)
355 }
356
357 pub fn lint_collection(&self, documents: &[crate::Document]) -> Result<Vec<crate::Violation>> {
362 self.registry.check_collection(documents)
363 }
364
365 pub fn lint_collection_with_config(
367 &self,
368 documents: &[crate::Document],
369 config: &crate::Config,
370 ) -> Result<Vec<crate::Violation>> {
371 self.registry
372 .check_collection_with_config(documents, config)
373 }
374
375 pub fn available_collection_rules(&self) -> Vec<&'static str> {
377 self.registry.collection_rule_ids()
378 }
379
380 pub fn has_collection_rules(&self) -> bool {
382 self.registry.has_collection_rules()
383 }
384}
385
386impl Default for LintEngine {
387 fn default() -> Self {
388 Self::new()
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use crate::rule::{Rule, RuleCategory, RuleMetadata};
396 use std::path::PathBuf;
397
398 struct TestRule;
400
401 impl Rule for TestRule {
402 fn id(&self) -> &'static str {
403 "TEST001"
404 }
405 fn name(&self) -> &'static str {
406 "test-rule"
407 }
408 fn description(&self) -> &'static str {
409 "A test rule"
410 }
411 fn metadata(&self) -> RuleMetadata {
412 RuleMetadata::stable(RuleCategory::Structure)
413 }
414 fn check_with_ast<'a>(
415 &self,
416 _document: &crate::Document,
417 _ast: Option<&'a comrak::nodes::AstNode<'a>>,
418 ) -> Result<Vec<crate::Violation>> {
419 Ok(vec![])
420 }
421 }
422
423 struct TestProvider;
425
426 impl RuleProvider for TestProvider {
427 fn provider_id(&self) -> &'static str {
428 "test-provider"
429 }
430 fn description(&self) -> &'static str {
431 "Test provider"
432 }
433 fn version(&self) -> &'static str {
434 "0.1.0"
435 }
436
437 fn register_rules(&self, registry: &mut RuleRegistry) {
438 registry.register(Box::new(TestRule));
439 }
440
441 fn rule_ids(&self) -> Vec<&'static str> {
442 vec!["TEST001"]
443 }
444 }
445
446 #[test]
447 fn test_plugin_registry_basic() {
448 let mut registry = PluginRegistry::new();
449 assert_eq!(registry.providers().len(), 0);
450
451 registry.register_provider(Box::new(TestProvider)).unwrap();
452 assert_eq!(registry.providers().len(), 1);
453
454 let provider = registry.get_provider("test-provider").unwrap();
455 assert_eq!(provider.provider_id(), "test-provider");
456 assert_eq!(provider.description(), "Test provider");
457 }
458
459 #[test]
460 fn test_plugin_registry_duplicate_id() {
461 let mut registry = PluginRegistry::new();
462 registry.register_provider(Box::new(TestProvider)).unwrap();
463
464 let result = registry.register_provider(Box::new(TestProvider));
466 assert!(result.is_err());
467 assert!(
468 result
469 .unwrap_err()
470 .to_string()
471 .contains("already registered")
472 );
473 }
474
475 #[test]
476 fn test_create_engine_from_registry() {
477 let mut registry = PluginRegistry::new();
478 registry.register_provider(Box::new(TestProvider)).unwrap();
479
480 let engine = registry.create_engine().unwrap();
481 let rule_ids = engine.available_rules();
482 assert!(rule_ids.contains(&"TEST001"));
483 }
484
485 #[test]
486 fn test_available_rule_ids() {
487 let mut registry = PluginRegistry::new();
488 registry.register_provider(Box::new(TestProvider)).unwrap();
489
490 let rule_ids = registry.available_rule_ids();
491 assert_eq!(rule_ids, vec!["TEST001"]);
492 }
493
494 #[test]
495 fn test_provider_info() {
496 let mut registry = PluginRegistry::new();
497 registry.register_provider(Box::new(TestProvider)).unwrap();
498
499 let info = registry.provider_info();
500 assert_eq!(info.len(), 1);
501 assert_eq!(info[0].id, "test-provider");
502 assert_eq!(info[0].description, "Test provider");
503 assert_eq!(info[0].version, "0.1.0");
504 assert_eq!(info[0].rule_count, 1);
505 }
506
507 #[test]
508 fn test_get_provider_not_found() {
509 let registry = PluginRegistry::new();
510 assert!(registry.get_provider("nonexistent").is_none());
511 }
512
513 #[test]
514 fn test_create_rule_registry() {
515 let mut registry = PluginRegistry::new();
516 registry.register_provider(Box::new(TestProvider)).unwrap();
517
518 let rule_registry = registry.create_rule_registry().unwrap();
519 assert!(!rule_registry.is_empty());
520 }
521
522 struct FailingProvider;
524
525 impl RuleProvider for FailingProvider {
526 fn provider_id(&self) -> &'static str {
527 "failing-provider"
528 }
529 fn description(&self) -> &'static str {
530 "Failing test provider"
531 }
532 fn version(&self) -> &'static str {
533 "0.1.0"
534 }
535 fn register_rules(&self, _registry: &mut RuleRegistry) {}
536 fn initialize(&self) -> Result<()> {
537 Err(crate::error::MdBookLintError::plugin_error(
538 "Initialization failed",
539 ))
540 }
541 }
542
543 #[test]
544 fn test_provider_initialization_failure() {
545 let mut registry = PluginRegistry::new();
546 let result = registry.register_provider(Box::new(FailingProvider));
547 assert!(result.is_err());
548 assert!(
549 result
550 .unwrap_err()
551 .to_string()
552 .contains("Initialization failed")
553 );
554 }
555
556 struct ConfigurableProvider;
558
559 impl RuleProvider for ConfigurableProvider {
560 fn provider_id(&self) -> &'static str {
561 "configurable-provider"
562 }
563 fn description(&self) -> &'static str {
564 "Configurable test provider"
565 }
566 fn version(&self) -> &'static str {
567 "0.1.0"
568 }
569 fn register_rules(&self, _registry: &mut RuleRegistry) {}
570 fn config_schema(&self) -> Option<Value> {
571 Some(serde_json::json!({
572 "type": "object",
573 "properties": {
574 "enabled": {"type": "boolean"}
575 }
576 }))
577 }
578 }
579
580 #[test]
581 fn test_provider_with_config_schema() {
582 let provider = ConfigurableProvider;
583 let schema = provider.config_schema();
584 assert!(schema.is_some());
585 let schema = schema.unwrap();
586 assert_eq!(schema["type"], "object");
587 }
588
589 #[test]
590 fn test_lint_engine_with_registry() {
591 let mut rule_registry = RuleRegistry::new();
592 rule_registry.register(Box::new(TestRule));
593
594 let engine = LintEngine::with_registry(rule_registry);
595 let rules = engine.available_rules();
596 assert!(rules.contains(&"TEST001"));
597 }
598
599 #[test]
600 fn test_lint_engine_api() {
601 let mut registry = PluginRegistry::new();
602 registry.register_provider(Box::new(TestProvider)).unwrap();
603 let engine = registry.create_engine().unwrap();
604
605 let _violations = engine.lint_content("# Test\n", "test.md").unwrap();
607
608 let document =
610 crate::Document::new("# Test".to_string(), PathBuf::from("test.md")).unwrap();
611 let _violations = engine.lint_document(&document).unwrap();
612 }
613
614 #[test]
615 fn test_position_to_offset() {
616 let text = "line1\nline2\nline3";
617
618 assert_eq!(
620 crate::violation::Position { line: 1, column: 1 }.to_byte_offset(text),
621 Some(0)
622 );
623
624 assert_eq!(
626 crate::violation::Position { line: 1, column: 3 }.to_byte_offset(text),
627 Some(2)
628 );
629
630 assert_eq!(
632 crate::violation::Position { line: 2, column: 1 }.to_byte_offset(text),
633 Some(6)
634 );
635
636 assert_eq!(
638 crate::violation::Position { line: 3, column: 1 }.to_byte_offset(text),
639 Some(12)
640 );
641
642 assert_eq!(
644 crate::violation::Position {
645 line: 10,
646 column: 1,
647 }
648 .to_byte_offset(text),
649 None
650 );
651 }
652
653 #[test]
654 fn test_apply_fix_simple() {
655 let engine = LintEngine::new();
656 let content = "hello world";
657
658 let violation = crate::Violation {
660 rule_id: "TEST".to_string(),
661 rule_name: "test".to_string(),
662 message: "test".to_string(),
663 line: 1,
664 column: 7,
665 severity: crate::Severity::Warning,
666 fix: Some(crate::violation::Fix {
667 description: "Replace world with rust".to_string(),
668 replacement: Some("rust".to_string()),
669 start: crate::violation::Position { line: 1, column: 7 },
670 end: crate::violation::Position {
671 line: 1,
672 column: 12,
673 },
674 }),
675 };
676
677 let result = engine.apply_fix(content, &violation);
678 assert_eq!(result, Some("hello rust".to_string()));
679 }
680
681 #[test]
682 fn test_apply_fix_no_fix() {
683 let engine = LintEngine::new();
684 let content = "hello world";
685
686 let violation = crate::Violation {
687 rule_id: "TEST".to_string(),
688 rule_name: "test".to_string(),
689 message: "test".to_string(),
690 line: 1,
691 column: 1,
692 severity: crate::Severity::Warning,
693 fix: None,
694 };
695
696 let result = engine.apply_fix(content, &violation);
697 assert_eq!(result, None);
698 }
699
700 #[test]
701 fn test_apply_fixes_multiple() {
702 let engine = LintEngine::new();
703 let content = "aaa bbb ccc";
704
705 let violations = vec![
706 crate::Violation {
707 rule_id: "TEST".to_string(),
708 rule_name: "test".to_string(),
709 message: "test".to_string(),
710 line: 1,
711 column: 1,
712 severity: crate::Severity::Warning,
713 fix: Some(crate::violation::Fix {
714 description: "Replace aaa with AAA".to_string(),
715 replacement: Some("AAA".to_string()),
716 start: crate::violation::Position { line: 1, column: 1 },
717 end: crate::violation::Position { line: 1, column: 4 },
718 }),
719 },
720 crate::Violation {
721 rule_id: "TEST".to_string(),
722 rule_name: "test".to_string(),
723 message: "test".to_string(),
724 line: 1,
725 column: 9,
726 severity: crate::Severity::Warning,
727 fix: Some(crate::violation::Fix {
728 description: "Replace ccc with CCC".to_string(),
729 replacement: Some("CCC".to_string()),
730 start: crate::violation::Position { line: 1, column: 9 },
731 end: crate::violation::Position {
732 line: 1,
733 column: 12,
734 },
735 }),
736 },
737 ];
738
739 let (fixed, unfixed) = engine.apply_fixes(content, &violations);
740 assert_eq!(fixed, "AAA bbb CCC");
741 assert!(unfixed.is_empty());
742 }
743
744 #[test]
745 fn test_apply_fixes_mixed() {
746 let engine = LintEngine::new();
747 let content = "hello world";
748
749 let violations = vec![
750 crate::Violation {
751 rule_id: "TEST1".to_string(),
752 rule_name: "test".to_string(),
753 message: "has fix".to_string(),
754 line: 1,
755 column: 7,
756 severity: crate::Severity::Warning,
757 fix: Some(crate::violation::Fix {
758 description: "Replace".to_string(),
759 replacement: Some("rust".to_string()),
760 start: crate::violation::Position { line: 1, column: 7 },
761 end: crate::violation::Position {
762 line: 1,
763 column: 12,
764 },
765 }),
766 },
767 crate::Violation {
768 rule_id: "TEST2".to_string(),
769 rule_name: "test".to_string(),
770 message: "no fix".to_string(),
771 line: 1,
772 column: 1,
773 severity: crate::Severity::Warning,
774 fix: None,
775 },
776 ];
777
778 let (fixed, unfixed) = engine.apply_fixes(content, &violations);
779 assert_eq!(fixed, "hello rust");
780 assert_eq!(unfixed.len(), 1);
781 assert_eq!(unfixed[0].rule_id, "TEST2");
782 }
783
784 #[test]
785 fn test_apply_fix_newline_handling() {
786 let engine = LintEngine::new();
788
789 let content = "# Old Heading\nNext line\n";
791
792 let violation = crate::Violation {
794 rule_id: "TEST".to_string(),
795 rule_name: "test".to_string(),
796 message: "Replace heading".to_string(),
797 line: 1,
798 column: 1,
799 severity: crate::Severity::Warning,
800 fix: Some(crate::violation::Fix {
801 description: "Replace heading".to_string(),
802 start: crate::violation::Position { line: 1, column: 1 },
803 end: crate::violation::Position { line: 2, column: 1 },
806 replacement: Some("# New Heading\n".to_string()),
807 }),
808 };
809
810 let result = engine.apply_fix(content, &violation);
811 assert!(result.is_some());
812 let fixed = result.unwrap();
813
814 assert_eq!(fixed, "# New Heading\nNext line\n");
816 assert!(!fixed.contains("\n\n"), "Should not have double newlines");
817 }
818
819 #[test]
820 fn test_apply_fix_does_not_infer_newline_consumption() {
821 let engine = LintEngine::new();
822 let content = "old\nnext\n";
823 let violation = crate::Violation {
824 rule_id: "TEST".to_string(),
825 rule_name: "test".to_string(),
826 message: "Replace line content only".to_string(),
827 line: 1,
828 column: 1,
829 severity: crate::Severity::Warning,
830 fix: Some(crate::violation::Fix {
831 description: "Replace line content only".to_string(),
832 start: crate::violation::Position::line_start(1),
833 end: crate::violation::Position::line_end(1, "old"),
834 replacement: Some("new\n".to_string()),
835 }),
836 };
837
838 assert_eq!(
841 engine.apply_fix(content, &violation),
842 Some("new\n\nnext\n".to_string())
843 );
844 }
845
846 #[test]
847 fn test_apply_fix_no_newline_no_adjustment() {
848 let engine = LintEngine::new();
850 let content = "hello world";
851
852 let violation = crate::Violation {
853 rule_id: "TEST".to_string(),
854 rule_name: "test".to_string(),
855 message: "Replace word".to_string(),
856 line: 1,
857 column: 7,
858 severity: crate::Severity::Warning,
859 fix: Some(crate::violation::Fix {
860 description: "Replace word".to_string(),
861 start: crate::violation::Position { line: 1, column: 7 },
862 end: crate::violation::Position {
863 line: 1,
864 column: 12,
865 },
866 replacement: Some("rust".to_string()),
867 }),
868 };
869
870 let result = engine.apply_fix(content, &violation);
871 assert!(result.is_some());
872 assert_eq!(result.unwrap(), "hello rust");
873 }
874
875 #[test]
876 fn test_apply_fixes_skips_conflicting_ranges() {
877 let engine = LintEngine::new();
878 let content = "### C#\ntext\n";
879 let violations = vec![
880 crate::Violation {
881 rule_id: "MD003".to_string(),
882 rule_name: "heading-style".to_string(),
883 message: "test".to_string(),
884 line: 1,
885 column: 1,
886 severity: crate::Severity::Error,
887 fix: Some(crate::violation::Fix {
888 description: "First whole-line replacement".to_string(),
889 replacement: Some("### C\n".to_string()),
890 start: crate::violation::Position { line: 1, column: 1 },
891 end: crate::violation::Position { line: 1, column: 7 },
892 }),
893 },
894 crate::Violation {
895 rule_id: "MD020".to_string(),
896 rule_name: "no-missing-space-closed-atx".to_string(),
897 message: "test".to_string(),
898 line: 1,
899 column: 1,
900 severity: crate::Severity::Warning,
901 fix: Some(crate::violation::Fix {
902 description: "Second whole-line replacement".to_string(),
903 replacement: Some("###C#\n".to_string()),
904 start: crate::violation::Position { line: 1, column: 1 },
905 end: crate::violation::Position { line: 1, column: 7 },
906 }),
907 },
908 ];
909
910 let (fixed, unfixed) = engine.apply_fixes(content, &violations);
911 assert_eq!(fixed, content);
912 assert_eq!(unfixed.len(), 2);
913 }
914
915 #[test]
916 fn test_apply_fixes_deduplicates_identical_ranges() {
917 let engine = LintEngine::new();
918 let content = "#Bad#\nnext\n";
919 let make_violation = |rule_id: &str| crate::Violation {
920 rule_id: rule_id.to_string(),
921 rule_name: "test".to_string(),
922 message: "test".to_string(),
923 line: 1,
924 column: 1,
925 severity: crate::Severity::Warning,
926 fix: Some(crate::violation::Fix {
927 description: "Normalize heading".to_string(),
928 replacement: Some("# Bad #\n".to_string()),
929 start: crate::violation::Position { line: 1, column: 1 },
930 end: crate::violation::Position { line: 2, column: 1 },
931 }),
932 };
933 let violations = vec![make_violation("TEST1"), make_violation("TEST2")];
934
935 let (fixed, unfixed) = engine.apply_fixes(content, &violations);
936 assert_eq!(fixed, "# Bad #\nnext\n");
937 assert!(unfixed.is_empty());
938 }
939}