1use crate::inline_config::InlineConfig;
7use crate::rule::{Fix, LintWarning};
8use crate::utils::ensure_consistent_line_endings;
9use std::borrow::Cow;
10use std::ops::Range;
11
12pub fn filter_warnings_by_inline_config(
17 warnings: Vec<LintWarning>,
18 inline_config: &InlineConfig,
19 rule_name: &str,
20) -> Vec<LintWarning> {
21 let base_rule_name = if let Some(dash_pos) = rule_name.find('-') {
22 let prefix = &rule_name[..dash_pos];
25 if prefix.starts_with("MD") { prefix } else { rule_name }
26 } else {
27 rule_name
28 };
29
30 warnings
31 .into_iter()
32 .filter(|w| {
33 let end = if w.end_line >= w.line { w.end_line } else { w.line };
34 !(w.line..=end).any(|line| inline_config.is_rule_disabled(base_rule_name, line))
35 })
36 .collect()
37}
38
39pub fn apply_warning_fixes(content: &str, warnings: &[LintWarning]) -> Result<String, String> {
42 let mut fixes: Vec<(usize, &Fix)> = warnings
43 .iter()
44 .enumerate()
45 .filter_map(|(i, w)| w.fix.as_ref().map(|fix| (i, fix)))
46 .flat_map(|(i, fix)| {
47 std::iter::once((i, fix)).chain(fix.additional_edits.iter().map(move |e| (i, e)))
52 })
53 .collect();
54
55 if fixes.is_empty() {
60 return Ok(content.to_string());
61 }
62
63 fixes.sort_by(|(idx_a, fix_a), (idx_b, fix_b)| {
67 let range_cmp = fix_a.range.start.cmp(&fix_b.range.start);
68 if range_cmp != std::cmp::Ordering::Equal {
69 return range_cmp;
70 }
71 let end_cmp = fix_a.range.end.cmp(&fix_b.range.end);
72 if end_cmp != std::cmp::Ordering::Equal {
73 return end_cmp;
74 }
75 idx_a.cmp(idx_b)
76 });
77
78 let mut applicable: Vec<ApplicableEdit<'_>> = Vec::with_capacity(fixes.len());
90 let mut i = 0;
91 while i < fixes.len() {
92 let (_, current) = fixes[i];
93 let mut combined: Option<String> = None;
94 let is_zero_width = current.range.start == current.range.end;
95 let mut j = i + 1;
96 while j < fixes.len() {
97 let (_, next) = fixes[j];
98 if next.range != current.range {
99 break;
100 }
101 if next.replacement == current.replacement {
102 j += 1;
104 continue;
105 }
106 if !is_zero_width {
107 break;
112 }
113 let buf = combined.get_or_insert_with(|| current.replacement.clone());
115 buf.push_str(&next.replacement);
116 j += 1;
117 }
118
119 applicable.push(ApplicableEdit {
120 range: current.range.clone(),
121 replacement: match combined {
122 Some(owned) => Cow::Owned(owned),
123 None => Cow::Borrowed(current.replacement.as_str()),
124 },
125 });
126 i = j;
127 }
128
129 applicable.sort_by(|a, b| {
133 let cmp = b.range.start.cmp(&a.range.start);
134 if cmp != std::cmp::Ordering::Equal {
135 return cmp;
136 }
137 b.range.end.cmp(&a.range.end)
138 });
139
140 let mut result = content.to_string();
141
142 let mut min_applied_start = usize::MAX;
147
148 for edit in applicable {
149 if edit.range.end > result.len() {
150 return Err(format!(
151 "Fix range end {} exceeds content length {}",
152 edit.range.end,
153 result.len()
154 ));
155 }
156
157 if edit.range.start > edit.range.end {
158 return Err(format!(
159 "Invalid fix range: start {} > end {}",
160 edit.range.start, edit.range.end
161 ));
162 }
163
164 if !result.is_char_boundary(edit.range.start) || !result.is_char_boundary(edit.range.end) {
168 return Err(format!(
169 "Fix range {}..{} does not lie on UTF-8 char boundaries",
170 edit.range.start, edit.range.end
171 ));
172 }
173
174 if edit.range.end > min_applied_start {
177 continue;
178 }
179
180 result.replace_range(edit.range.clone(), &edit.replacement);
181 min_applied_start = edit.range.start;
182 }
183
184 Ok(ensure_consistent_line_endings(content, &result))
186}
187
188struct ApplicableEdit<'a> {
192 range: Range<usize>,
193 replacement: Cow<'a, str>,
194}
195
196pub fn warning_fix_to_edit(content: &str, warning: &LintWarning) -> Result<(usize, usize, String), String> {
199 if let Some(fix) = &warning.fix {
200 if fix.range.end > content.len() {
202 return Err(format!(
203 "Fix range end {} exceeds content length {}",
204 fix.range.end,
205 content.len()
206 ));
207 }
208
209 Ok((fix.range.start, fix.range.end, fix.replacement.clone()))
210 } else {
211 Err("Warning has no fix".to_string())
212 }
213}
214
215pub fn validate_fix_range(content: &str, fix: &Fix) -> Result<(), String> {
217 if fix.range.start > content.len() {
218 return Err(format!(
219 "Fix range start {} exceeds content length {}",
220 fix.range.start,
221 content.len()
222 ));
223 }
224
225 if fix.range.end > content.len() {
226 return Err(format!(
227 "Fix range end {} exceeds content length {}",
228 fix.range.end,
229 content.len()
230 ));
231 }
232
233 if fix.range.start > fix.range.end {
234 return Err(format!(
235 "Invalid fix range: start {} > end {}",
236 fix.range.start, fix.range.end
237 ));
238 }
239
240 if !content.is_char_boundary(fix.range.start) || !content.is_char_boundary(fix.range.end) {
243 return Err(format!(
244 "Fix range {}..{} does not lie on UTF-8 char boundaries",
245 fix.range.start, fix.range.end
246 ));
247 }
248
249 Ok(())
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::rule::{Fix, LintWarning, Severity};
256
257 #[test]
258 fn test_validate_fix_range_rejects_non_char_boundary() {
259 let content = "é world";
261 let fix = Fix::new(1..2, "x".to_string());
262 assert!(
263 validate_fix_range(content, &fix).is_err(),
264 "validate_fix_range must reject a range that splits a codepoint"
265 );
266 let ok_fix = Fix::new(0..2, "x".to_string());
268 assert!(validate_fix_range(content, &ok_fix).is_ok());
269 }
270
271 #[test]
272 fn test_apply_single_fix() {
273 let content = "1. Multiple spaces";
274 let warning = LintWarning {
275 message: "Too many spaces".to_string(),
276 line: 1,
277 column: 3,
278 end_line: 1,
279 end_column: 5,
280 severity: Severity::Warning,
281 fix: Some(Fix::new(2..4, " ".to_string())),
282 rule_name: Some("MD030".to_string()),
283 };
284
285 let result = apply_warning_fixes(content, &[warning]).unwrap();
286 assert_eq!(result, "1. Multiple spaces");
287 }
288
289 #[test]
290 fn test_apply_fix_with_non_char_boundary_range_does_not_panic() {
291 let content = "é world";
295 let warning = LintWarning {
296 message: "bad range".to_string(),
297 line: 1,
298 column: 1,
299 end_line: 1,
300 end_column: 2,
301 severity: Severity::Warning,
302 fix: Some(Fix::new(1..2, "x".to_string())),
303 rule_name: Some("MDTEST".to_string()),
304 };
305
306 let result = apply_warning_fixes(content, &[warning]);
307 assert!(
308 result.is_err(),
309 "non-char-boundary range must be rejected, got {result:?}"
310 );
311 }
312
313 #[test]
314 fn test_apply_multiple_fixes() {
315 let content = "1. First\n* Second";
316 let warnings = vec![
317 LintWarning {
318 message: "Too many spaces".to_string(),
319 line: 1,
320 column: 3,
321 end_line: 1,
322 end_column: 5,
323 severity: Severity::Warning,
324 fix: Some(Fix::new(2..4, " ".to_string())),
325 rule_name: Some("MD030".to_string()),
326 },
327 LintWarning {
328 message: "Too many spaces".to_string(),
329 line: 2,
330 column: 2,
331 end_line: 2,
332 end_column: 5,
333 severity: Severity::Warning,
334 fix: Some(Fix::new(11..14, " ".to_string())),
335 rule_name: Some("MD030".to_string()),
336 },
337 ];
338
339 let result = apply_warning_fixes(content, &warnings).unwrap();
340 assert_eq!(result, "1. First\n* Second");
341 }
342
343 #[test]
344 fn test_apply_non_overlapping_fixes() {
345 let content = "Test multiple spaces";
350 let warnings = vec![
351 LintWarning {
352 message: "Too many spaces".to_string(),
353 line: 1,
354 column: 5,
355 end_line: 1,
356 end_column: 7,
357 severity: Severity::Warning,
358 fix: Some(Fix::new(4..6, " ".to_string())),
359 rule_name: Some("MD009".to_string()),
360 },
361 LintWarning {
362 message: "Too many spaces".to_string(),
363 line: 1,
364 column: 15,
365 end_line: 1,
366 end_column: 19,
367 severity: Severity::Warning,
368 fix: Some(Fix::new(14..18, " ".to_string())),
369 rule_name: Some("MD009".to_string()),
370 },
371 ];
372
373 let result = apply_warning_fixes(content, &warnings).unwrap();
374 assert_eq!(result, "Test multiple spaces");
375 }
376
377 #[test]
378 fn test_apply_duplicate_fixes() {
379 let content = "Test content";
380 let warnings = vec![
381 LintWarning {
382 message: "Fix 1".to_string(),
383 line: 1,
384 column: 5,
385 end_line: 1,
386 end_column: 7,
387 severity: Severity::Warning,
388 fix: Some(Fix::new(4..6, " ".to_string())),
389 rule_name: Some("MD009".to_string()),
390 },
391 LintWarning {
392 message: "Fix 2 (duplicate)".to_string(),
393 line: 1,
394 column: 5,
395 end_line: 1,
396 end_column: 7,
397 severity: Severity::Warning,
398 fix: Some(Fix::new(4..6, " ".to_string())),
399 rule_name: Some("MD009".to_string()),
400 },
401 ];
402
403 let result = apply_warning_fixes(content, &warnings).unwrap();
405 assert_eq!(result, "Test content");
406 }
407
408 #[test]
409 fn test_apply_fixes_with_windows_line_endings() {
410 let content = "1. First\r\n* Second\r\n";
411 let warnings = vec![
412 LintWarning {
413 message: "Too many spaces".to_string(),
414 line: 1,
415 column: 3,
416 end_line: 1,
417 end_column: 5,
418 severity: Severity::Warning,
419 fix: Some(Fix::new(2..4, " ".to_string())),
420 rule_name: Some("MD030".to_string()),
421 },
422 LintWarning {
423 message: "Too many spaces".to_string(),
424 line: 2,
425 column: 2,
426 end_line: 2,
427 end_column: 5,
428 severity: Severity::Warning,
429 fix: Some(Fix::new(12..15, " ".to_string())),
430 rule_name: Some("MD030".to_string()),
431 },
432 ];
433
434 let result = apply_warning_fixes(content, &warnings).unwrap();
435 assert!(result.contains("1. First"));
438 assert!(result.contains("* Second"));
439 }
440
441 #[test]
442 fn test_apply_fix_with_invalid_range() {
443 let content = "Short";
444 let warning = LintWarning {
445 message: "Invalid fix".to_string(),
446 line: 1,
447 column: 1,
448 end_line: 1,
449 end_column: 10,
450 severity: Severity::Warning,
451 fix: Some(Fix::new(0..100, "Replacement".to_string())),
452 rule_name: Some("TEST".to_string()),
453 };
454
455 let result = apply_warning_fixes(content, &[warning]);
456 assert!(result.is_err());
457 assert!(result.unwrap_err().contains("exceeds content length"));
458 }
459
460 #[test]
461 #[allow(clippy::reversed_empty_ranges)]
462 fn test_apply_fix_with_reversed_range() {
463 let content = "Hello world";
464 let warning = LintWarning {
465 message: "Invalid fix".to_string(),
466 line: 1,
467 column: 5,
468 end_line: 1,
469 end_column: 3,
470 severity: Severity::Warning,
471 fix: Some(Fix::new(10..5, "Test".to_string())),
472 rule_name: Some("TEST".to_string()),
473 };
474
475 let result = apply_warning_fixes(content, &[warning]);
476 assert!(result.is_err());
477 assert!(result.unwrap_err().contains("Invalid fix range"));
478 }
479
480 #[test]
481 fn test_apply_no_fixes() {
482 let content = "No changes needed";
483 let warnings = vec![LintWarning {
484 message: "Warning without fix".to_string(),
485 line: 1,
486 column: 1,
487 end_line: 1,
488 end_column: 5,
489 severity: Severity::Warning,
490 fix: None,
491 rule_name: Some("TEST".to_string()),
492 }];
493
494 let result = apply_warning_fixes(content, &warnings).unwrap();
495 assert_eq!(result, content);
496 }
497
498 #[test]
499 fn test_overlapping_fixes_skip_outer() {
500 let content = "[  ](url) suffix";
505 let warnings = vec](url)".to_string())),
514 rule_name: Some("MD039".to_string()),
515 },
516 LintWarning {
517 message: "Inner image".to_string(),
518 line: 1,
519 column: 3,
520 end_line: 1,
521 end_column: 15,
522 severity: Severity::Warning,
523 fix: Some(Fix::new(2..15, "".to_string())),
524 rule_name: Some("MD039".to_string()),
525 },
526 ];
527
528 let result = apply_warning_fixes(content, &warnings).unwrap();
529 assert_eq!(result, "[  ](url) suffix");
532 }
533
534 #[test]
535 fn test_apply_fix_with_additional_edits_atomic() {
536 let content = "See [docs](https://example.com) for details.\n";
541 let primary_range = content.find("[docs](https://example.com)").unwrap()..content.find(" for details").unwrap();
542 let appended = "\n[docs]: https://example.com\n".to_string();
543 let warnings = vec![LintWarning {
544 message: "Inconsistent link style".to_string(),
545 line: 1,
546 column: 5,
547 end_line: 1,
548 end_column: 32,
549 severity: Severity::Warning,
550 fix: Some(Fix::with_additional_edits(
551 primary_range,
552 "[docs]".to_string(),
553 vec![Fix::new(content.len()..content.len(), appended)],
554 )),
555 rule_name: Some("MD054".to_string()),
556 }];
557
558 let result = apply_warning_fixes(content, &warnings).unwrap();
559 assert!(
560 result.contains("See [docs] for details."),
561 "primary edit must rewrite the inline link in place: {result:?}"
562 );
563 assert!(
564 result.contains("[docs]: https://example.com"),
565 "additional edit must append the ref-def at EOF: {result:?}"
566 );
567 assert!(
568 !result.contains("[docs](https://example.com)"),
569 "the inline form must be gone after the atomic fix: {result:?}"
570 );
571 }
572
573 #[test]
574 fn test_apply_two_ref_emit_fixes_preserve_source_order() {
575 let content = "See [a](https://a.com) and [b](https://b.com).\n";
588 let span_a = content.find("[a](https://a.com)").unwrap()
589 ..content.find("](https://a.com)").unwrap() + "](https://a.com)".len();
590 let span_b = content.find("[b](https://b.com)").unwrap()
591 ..content.find("](https://b.com)").unwrap() + "](https://b.com)".len();
592 let warnings = vec![
593 LintWarning {
594 message: "Inconsistent link style".to_string(),
595 line: 1,
596 column: 5,
597 end_line: 1,
598 end_column: 0,
599 severity: Severity::Warning,
600 fix: Some(Fix::with_additional_edits(
601 span_a,
602 "[a]".to_string(),
603 vec![Fix::new(
604 content.len()..content.len(),
605 "[a]: https://a.com\n".to_string(),
606 )],
607 )),
608 rule_name: Some("MD054".to_string()),
609 },
610 LintWarning {
611 message: "Inconsistent link style".to_string(),
612 line: 1,
613 column: 28,
614 end_line: 1,
615 end_column: 0,
616 severity: Severity::Warning,
617 fix: Some(Fix::with_additional_edits(
618 span_b,
619 "[b]".to_string(),
620 vec![Fix::new(
621 content.len()..content.len(),
622 "[b]: https://b.com\n".to_string(),
623 )],
624 )),
625 rule_name: Some("MD054".to_string()),
626 },
627 ];
628
629 let result = apply_warning_fixes(content, &warnings).unwrap();
630
631 assert!(
633 result.contains("See [a] and [b]."),
634 "primary rewrites missing: {result:?}"
635 );
636 assert!(!result.contains("[a](https://a.com)"));
637 assert!(!result.contains("[b](https://b.com)"));
638
639 let pos_a = result.find("[a]: https://a.com").expect("ref-def for [a] missing");
641 let pos_b = result.find("[b]: https://b.com").expect("ref-def for [b] missing");
642 assert!(
643 pos_a < pos_b,
644 "ref-defs must appear in source order ([a] before [b]); got result:\n{result}"
645 );
646 }
647
648 #[test]
649 fn test_warning_fix_to_edit() {
650 let content = "Hello world";
651 let warning = LintWarning {
652 message: "Test".to_string(),
653 line: 1,
654 column: 1,
655 end_line: 1,
656 end_column: 5,
657 severity: Severity::Warning,
658 fix: Some(Fix::new(0..5, "Hi".to_string())),
659 rule_name: Some("TEST".to_string()),
660 };
661
662 let edit = warning_fix_to_edit(content, &warning).unwrap();
663 assert_eq!(edit, (0, 5, "Hi".to_string()));
664 }
665
666 #[test]
667 fn test_warning_fix_to_edit_no_fix() {
668 let content = "Hello world";
669 let warning = LintWarning {
670 message: "Test".to_string(),
671 line: 1,
672 column: 1,
673 end_line: 1,
674 end_column: 5,
675 severity: Severity::Warning,
676 fix: None,
677 rule_name: Some("TEST".to_string()),
678 };
679
680 let result = warning_fix_to_edit(content, &warning);
681 assert!(result.is_err());
682 assert_eq!(result.unwrap_err(), "Warning has no fix");
683 }
684
685 #[test]
686 fn test_warning_fix_to_edit_invalid_range() {
687 let content = "Short";
688 let warning = LintWarning {
689 message: "Test".to_string(),
690 line: 1,
691 column: 1,
692 end_line: 1,
693 end_column: 10,
694 severity: Severity::Warning,
695 fix: Some(Fix::new(0..100, "Long replacement".to_string())),
696 rule_name: Some("TEST".to_string()),
697 };
698
699 let result = warning_fix_to_edit(content, &warning);
700 assert!(result.is_err());
701 assert!(result.unwrap_err().contains("exceeds content length"));
702 }
703
704 #[test]
705 fn test_validate_fix_range() {
706 let content = "Hello world";
707
708 let valid_fix = Fix::new(0..5, "Hi".to_string());
710 assert!(validate_fix_range(content, &valid_fix).is_ok());
711
712 let invalid_fix = Fix::new(0..20, "Hi".to_string());
714 assert!(validate_fix_range(content, &invalid_fix).is_err());
715
716 let start = 5;
718 let end = 3;
719 let invalid_fix2 = Fix::new(start..end, "Hi".to_string());
720 assert!(validate_fix_range(content, &invalid_fix2).is_err());
721 }
722
723 #[test]
724 fn test_validate_fix_range_edge_cases() {
725 let content = "Test";
726
727 let fix1 = Fix::new(0..0, "Insert".to_string());
729 assert!(validate_fix_range(content, &fix1).is_ok());
730
731 let fix2 = Fix::new(4..4, " append".to_string());
733 assert!(validate_fix_range(content, &fix2).is_ok());
734
735 let fix3 = Fix::new(0..4, "Replace".to_string());
737 assert!(validate_fix_range(content, &fix3).is_ok());
738
739 let fix4 = Fix::new(10..11, "Invalid".to_string());
741 let result = validate_fix_range(content, &fix4);
742 assert!(result.is_err());
743 assert!(result.unwrap_err().contains("start 10 exceeds"));
744 }
745
746 #[test]
747 fn test_fix_ordering_stability() {
748 let content = "Test content here";
750 let warnings = vec![
751 LintWarning {
752 message: "First warning".to_string(),
753 line: 1,
754 column: 6,
755 end_line: 1,
756 end_column: 13,
757 severity: Severity::Warning,
758 fix: Some(Fix::new(5..12, "stuff".to_string())),
759 rule_name: Some("MD001".to_string()),
760 },
761 LintWarning {
762 message: "Second warning".to_string(),
763 line: 1,
764 column: 6,
765 end_line: 1,
766 end_column: 13,
767 severity: Severity::Warning,
768 fix: Some(Fix::new(5..12, "stuff".to_string())),
769 rule_name: Some("MD002".to_string()),
770 },
771 ];
772
773 let result = apply_warning_fixes(content, &warnings).unwrap();
775 assert_eq!(result, "Test stuff here");
776 }
777
778 #[test]
779 fn test_line_ending_preservation() {
780 let content_unix = "Line 1\nLine 2\n";
782 let warning = LintWarning {
783 message: "Add text".to_string(),
784 line: 1,
785 column: 7,
786 end_line: 1,
787 end_column: 7,
788 severity: Severity::Warning,
789 fix: Some(Fix::new(6..6, " added".to_string())),
790 rule_name: Some("TEST".to_string()),
791 };
792
793 let result = apply_warning_fixes(content_unix, &[warning]).unwrap();
794 assert_eq!(result, "Line 1 added\nLine 2\n");
795
796 let content_windows = "Line 1\r\nLine 2\r\n";
798 let warning_windows = LintWarning {
799 message: "Add text".to_string(),
800 line: 1,
801 column: 7,
802 end_line: 1,
803 end_column: 7,
804 severity: Severity::Warning,
805 fix: Some(Fix::new(6..6, " added".to_string())),
806 rule_name: Some("TEST".to_string()),
807 };
808
809 let result_windows = apply_warning_fixes(content_windows, &[warning_windows]).unwrap();
810 assert!(result_windows.starts_with("Line 1 added"));
812 assert!(result_windows.contains("Line 2"));
813 }
814
815 fn make_warning(line: usize, end_line: usize, rule_name: &str) -> LintWarning {
816 LintWarning {
817 message: "test".to_string(),
818 line,
819 column: 1,
820 end_line,
821 end_column: 1,
822 severity: Severity::Warning,
823 fix: Some(Fix::new(0..1, "x".to_string())),
824 rule_name: Some(rule_name.to_string()),
825 }
826 }
827
828 #[test]
829 fn test_filter_warnings_disable_enable_block() {
830 let content =
831 "# Heading\n\n<!-- rumdl-disable MD013 -->\nlong line\n<!-- rumdl-enable MD013 -->\nanother long line\n";
832 let inline_config = InlineConfig::from_content(content);
833
834 let warnings = vec![
835 make_warning(4, 4, "MD013"), make_warning(6, 6, "MD013"), ];
838
839 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
840 assert_eq!(filtered.len(), 1);
841 assert_eq!(filtered[0].line, 6);
842 }
843
844 #[test]
845 fn test_filter_warnings_disable_line() {
846 let content = "line one <!-- rumdl-disable-line MD009 -->\nline two\n";
847 let inline_config = InlineConfig::from_content(content);
848
849 let warnings = vec![
850 make_warning(1, 1, "MD009"), make_warning(2, 2, "MD009"), ];
853
854 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD009");
855 assert_eq!(filtered.len(), 1);
856 assert_eq!(filtered[0].line, 2);
857 }
858
859 #[test]
860 fn test_filter_warnings_disable_next_line() {
861 let content = "<!-- rumdl-disable-next-line MD034 -->\nhttp://example.com\nhttp://other.com\n";
862 let inline_config = InlineConfig::from_content(content);
863
864 let warnings = vec![
865 make_warning(2, 2, "MD034"), make_warning(3, 3, "MD034"), ];
868
869 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD034");
870 assert_eq!(filtered.len(), 1);
871 assert_eq!(filtered[0].line, 3);
872 }
873
874 #[test]
875 fn test_filter_warnings_sub_rule_name() {
876 let content = "<!-- rumdl-disable MD029 -->\nline\n<!-- rumdl-enable MD029 -->\nline\n";
877 let inline_config = InlineConfig::from_content(content);
878
879 let warnings = vec![make_warning(2, 2, "MD029"), make_warning(4, 4, "MD029")];
881
882 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD029-style");
883 assert_eq!(filtered.len(), 1);
884 assert_eq!(filtered[0].line, 4);
885 }
886
887 #[test]
888 fn test_filter_warnings_multi_line_warning() {
889 let content = "line 1\nline 2\nline 3\n<!-- rumdl-disable-line MD013 -->\nline 5\nline 6\n";
891 let inline_config = InlineConfig::from_content(content);
892
893 let warnings = vec![
894 make_warning(3, 5, "MD013"), make_warning(6, 6, "MD013"), ];
897
898 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
899 assert_eq!(filtered.len(), 1);
901 assert_eq!(filtered[0].line, 6);
902 }
903
904 #[test]
905 fn test_filter_warnings_empty_input() {
906 let inline_config = InlineConfig::from_content("");
907 let filtered = filter_warnings_by_inline_config(vec![], &inline_config, "MD013");
908 assert!(filtered.is_empty());
909 }
910
911 #[test]
912 fn test_filter_warnings_none_disabled() {
913 let content = "line 1\nline 2\n";
914 let inline_config = InlineConfig::from_content(content);
915
916 let warnings = vec![make_warning(1, 1, "MD013"), make_warning(2, 2, "MD013")];
917
918 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
919 assert_eq!(filtered.len(), 2);
920 }
921
922 #[test]
923 fn test_filter_warnings_all_disabled() {
924 let content = "<!-- rumdl-disable MD013 -->\nline 1\nline 2\n";
925 let inline_config = InlineConfig::from_content(content);
926
927 let warnings = vec![make_warning(2, 2, "MD013"), make_warning(3, 3, "MD013")];
928
929 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
930 assert!(filtered.is_empty());
931 }
932
933 #[test]
934 fn test_filter_warnings_end_line_zero_fallback() {
935 let content = "<!-- rumdl-disable-line MD013 -->\nline 2\n";
937 let inline_config = InlineConfig::from_content(content);
938
939 let warnings = vec![make_warning(1, 0, "MD013")]; let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
942 assert!(filtered.is_empty());
943 }
944
945 #[test]
946 fn test_filter_non_md_rule_name_preserves_dash() {
947 let content = "line 1\nline 2\n";
950 let inline_config = InlineConfig::from_content(content);
951
952 let warnings = vec![make_warning(1, 1, "custom-rule")];
953
954 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "custom-rule");
956 assert_eq!(filtered.len(), 1, "Non-MD rule name with dash should not be split");
957 }
958
959 #[test]
960 fn test_filter_md_sub_rule_name_is_split() {
961 let content = "<!-- rumdl-disable MD029 -->\nline\n<!-- rumdl-enable MD029 -->\nline\n";
963 let inline_config = InlineConfig::from_content(content);
964
965 let warnings = vec![
966 make_warning(2, 2, "MD029"), make_warning(4, 4, "MD029"), ];
969
970 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD029-style");
972 assert_eq!(filtered.len(), 1);
973 assert_eq!(filtered[0].line, 4);
974 }
975
976 #[test]
977 fn test_filter_warnings_capture_restore() {
978 let content = "<!-- rumdl-disable MD013 -->\nline 1\n<!-- rumdl-capture -->\n<!-- rumdl-enable MD013 -->\nline 4\n<!-- rumdl-restore -->\nline 6\n";
979 let inline_config = InlineConfig::from_content(content);
980
981 let warnings = vec![
982 make_warning(2, 2, "MD013"), make_warning(5, 5, "MD013"), make_warning(7, 7, "MD013"), ];
986
987 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
988 assert_eq!(filtered.len(), 1);
989 assert_eq!(filtered[0].line, 5);
990 }
991}