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 edit.range.end > min_applied_start {
167 continue;
168 }
169
170 result.replace_range(edit.range.clone(), &edit.replacement);
171 min_applied_start = edit.range.start;
172 }
173
174 Ok(ensure_consistent_line_endings(content, &result))
176}
177
178struct ApplicableEdit<'a> {
182 range: Range<usize>,
183 replacement: Cow<'a, str>,
184}
185
186pub fn warning_fix_to_edit(content: &str, warning: &LintWarning) -> Result<(usize, usize, String), String> {
189 if let Some(fix) = &warning.fix {
190 if fix.range.end > content.len() {
192 return Err(format!(
193 "Fix range end {} exceeds content length {}",
194 fix.range.end,
195 content.len()
196 ));
197 }
198
199 Ok((fix.range.start, fix.range.end, fix.replacement.clone()))
200 } else {
201 Err("Warning has no fix".to_string())
202 }
203}
204
205pub fn validate_fix_range(content: &str, fix: &Fix) -> Result<(), String> {
207 if fix.range.start > content.len() {
208 return Err(format!(
209 "Fix range start {} exceeds content length {}",
210 fix.range.start,
211 content.len()
212 ));
213 }
214
215 if fix.range.end > content.len() {
216 return Err(format!(
217 "Fix range end {} exceeds content length {}",
218 fix.range.end,
219 content.len()
220 ));
221 }
222
223 if fix.range.start > fix.range.end {
224 return Err(format!(
225 "Invalid fix range: start {} > end {}",
226 fix.range.start, fix.range.end
227 ));
228 }
229
230 Ok(())
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::rule::{Fix, LintWarning, Severity};
237
238 #[test]
239 fn test_apply_single_fix() {
240 let content = "1. Multiple spaces";
241 let warning = LintWarning {
242 message: "Too many spaces".to_string(),
243 line: 1,
244 column: 3,
245 end_line: 1,
246 end_column: 5,
247 severity: Severity::Warning,
248 fix: Some(Fix::new(2..4, " ".to_string())),
249 rule_name: Some("MD030".to_string()),
250 };
251
252 let result = apply_warning_fixes(content, &[warning]).unwrap();
253 assert_eq!(result, "1. Multiple spaces");
254 }
255
256 #[test]
257 fn test_apply_multiple_fixes() {
258 let content = "1. First\n* Second";
259 let warnings = vec![
260 LintWarning {
261 message: "Too many spaces".to_string(),
262 line: 1,
263 column: 3,
264 end_line: 1,
265 end_column: 5,
266 severity: Severity::Warning,
267 fix: Some(Fix::new(2..4, " ".to_string())),
268 rule_name: Some("MD030".to_string()),
269 },
270 LintWarning {
271 message: "Too many spaces".to_string(),
272 line: 2,
273 column: 2,
274 end_line: 2,
275 end_column: 5,
276 severity: Severity::Warning,
277 fix: Some(Fix::new(11..14, " ".to_string())),
278 rule_name: Some("MD030".to_string()),
279 },
280 ];
281
282 let result = apply_warning_fixes(content, &warnings).unwrap();
283 assert_eq!(result, "1. First\n* Second");
284 }
285
286 #[test]
287 fn test_apply_non_overlapping_fixes() {
288 let content = "Test multiple spaces";
293 let warnings = vec![
294 LintWarning {
295 message: "Too many spaces".to_string(),
296 line: 1,
297 column: 5,
298 end_line: 1,
299 end_column: 7,
300 severity: Severity::Warning,
301 fix: Some(Fix::new(4..6, " ".to_string())),
302 rule_name: Some("MD009".to_string()),
303 },
304 LintWarning {
305 message: "Too many spaces".to_string(),
306 line: 1,
307 column: 15,
308 end_line: 1,
309 end_column: 19,
310 severity: Severity::Warning,
311 fix: Some(Fix::new(14..18, " ".to_string())),
312 rule_name: Some("MD009".to_string()),
313 },
314 ];
315
316 let result = apply_warning_fixes(content, &warnings).unwrap();
317 assert_eq!(result, "Test multiple spaces");
318 }
319
320 #[test]
321 fn test_apply_duplicate_fixes() {
322 let content = "Test content";
323 let warnings = vec![
324 LintWarning {
325 message: "Fix 1".to_string(),
326 line: 1,
327 column: 5,
328 end_line: 1,
329 end_column: 7,
330 severity: Severity::Warning,
331 fix: Some(Fix::new(4..6, " ".to_string())),
332 rule_name: Some("MD009".to_string()),
333 },
334 LintWarning {
335 message: "Fix 2 (duplicate)".to_string(),
336 line: 1,
337 column: 5,
338 end_line: 1,
339 end_column: 7,
340 severity: Severity::Warning,
341 fix: Some(Fix::new(4..6, " ".to_string())),
342 rule_name: Some("MD009".to_string()),
343 },
344 ];
345
346 let result = apply_warning_fixes(content, &warnings).unwrap();
348 assert_eq!(result, "Test content");
349 }
350
351 #[test]
352 fn test_apply_fixes_with_windows_line_endings() {
353 let content = "1. First\r\n* Second\r\n";
354 let warnings = vec![
355 LintWarning {
356 message: "Too many spaces".to_string(),
357 line: 1,
358 column: 3,
359 end_line: 1,
360 end_column: 5,
361 severity: Severity::Warning,
362 fix: Some(Fix::new(2..4, " ".to_string())),
363 rule_name: Some("MD030".to_string()),
364 },
365 LintWarning {
366 message: "Too many spaces".to_string(),
367 line: 2,
368 column: 2,
369 end_line: 2,
370 end_column: 5,
371 severity: Severity::Warning,
372 fix: Some(Fix::new(12..15, " ".to_string())),
373 rule_name: Some("MD030".to_string()),
374 },
375 ];
376
377 let result = apply_warning_fixes(content, &warnings).unwrap();
378 assert!(result.contains("1. First"));
381 assert!(result.contains("* Second"));
382 }
383
384 #[test]
385 fn test_apply_fix_with_invalid_range() {
386 let content = "Short";
387 let warning = LintWarning {
388 message: "Invalid fix".to_string(),
389 line: 1,
390 column: 1,
391 end_line: 1,
392 end_column: 10,
393 severity: Severity::Warning,
394 fix: Some(Fix::new(0..100, "Replacement".to_string())),
395 rule_name: Some("TEST".to_string()),
396 };
397
398 let result = apply_warning_fixes(content, &[warning]);
399 assert!(result.is_err());
400 assert!(result.unwrap_err().contains("exceeds content length"));
401 }
402
403 #[test]
404 #[allow(clippy::reversed_empty_ranges)]
405 fn test_apply_fix_with_reversed_range() {
406 let content = "Hello world";
407 let warning = LintWarning {
408 message: "Invalid fix".to_string(),
409 line: 1,
410 column: 5,
411 end_line: 1,
412 end_column: 3,
413 severity: Severity::Warning,
414 fix: Some(Fix::new(10..5, "Test".to_string())),
415 rule_name: Some("TEST".to_string()),
416 };
417
418 let result = apply_warning_fixes(content, &[warning]);
419 assert!(result.is_err());
420 assert!(result.unwrap_err().contains("Invalid fix range"));
421 }
422
423 #[test]
424 fn test_apply_no_fixes() {
425 let content = "No changes needed";
426 let warnings = vec![LintWarning {
427 message: "Warning without fix".to_string(),
428 line: 1,
429 column: 1,
430 end_line: 1,
431 end_column: 5,
432 severity: Severity::Warning,
433 fix: None,
434 rule_name: Some("TEST".to_string()),
435 }];
436
437 let result = apply_warning_fixes(content, &warnings).unwrap();
438 assert_eq!(result, content);
439 }
440
441 #[test]
442 fn test_overlapping_fixes_skip_outer() {
443 let content = "[  ](url) suffix";
448 let warnings = vec](url)".to_string())),
457 rule_name: Some("MD039".to_string()),
458 },
459 LintWarning {
460 message: "Inner image".to_string(),
461 line: 1,
462 column: 3,
463 end_line: 1,
464 end_column: 15,
465 severity: Severity::Warning,
466 fix: Some(Fix::new(2..15, "".to_string())),
467 rule_name: Some("MD039".to_string()),
468 },
469 ];
470
471 let result = apply_warning_fixes(content, &warnings).unwrap();
472 assert_eq!(result, "[  ](url) suffix");
475 }
476
477 #[test]
478 fn test_apply_fix_with_additional_edits_atomic() {
479 let content = "See [docs](https://example.com) for details.\n";
484 let primary_range = content.find("[docs](https://example.com)").unwrap()..content.find(" for details").unwrap();
485 let appended = "\n[docs]: https://example.com\n".to_string();
486 let warnings = vec![LintWarning {
487 message: "Inconsistent link style".to_string(),
488 line: 1,
489 column: 5,
490 end_line: 1,
491 end_column: 32,
492 severity: Severity::Warning,
493 fix: Some(Fix::with_additional_edits(
494 primary_range,
495 "[docs]".to_string(),
496 vec![Fix::new(content.len()..content.len(), appended)],
497 )),
498 rule_name: Some("MD054".to_string()),
499 }];
500
501 let result = apply_warning_fixes(content, &warnings).unwrap();
502 assert!(
503 result.contains("See [docs] for details."),
504 "primary edit must rewrite the inline link in place: {result:?}"
505 );
506 assert!(
507 result.contains("[docs]: https://example.com"),
508 "additional edit must append the ref-def at EOF: {result:?}"
509 );
510 assert!(
511 !result.contains("[docs](https://example.com)"),
512 "the inline form must be gone after the atomic fix: {result:?}"
513 );
514 }
515
516 #[test]
517 fn test_apply_two_ref_emit_fixes_preserve_source_order() {
518 let content = "See [a](https://a.com) and [b](https://b.com).\n";
531 let span_a = content.find("[a](https://a.com)").unwrap()
532 ..content.find("](https://a.com)").unwrap() + "](https://a.com)".len();
533 let span_b = content.find("[b](https://b.com)").unwrap()
534 ..content.find("](https://b.com)").unwrap() + "](https://b.com)".len();
535 let warnings = vec![
536 LintWarning {
537 message: "Inconsistent link style".to_string(),
538 line: 1,
539 column: 5,
540 end_line: 1,
541 end_column: 0,
542 severity: Severity::Warning,
543 fix: Some(Fix::with_additional_edits(
544 span_a,
545 "[a]".to_string(),
546 vec![Fix::new(
547 content.len()..content.len(),
548 "[a]: https://a.com\n".to_string(),
549 )],
550 )),
551 rule_name: Some("MD054".to_string()),
552 },
553 LintWarning {
554 message: "Inconsistent link style".to_string(),
555 line: 1,
556 column: 28,
557 end_line: 1,
558 end_column: 0,
559 severity: Severity::Warning,
560 fix: Some(Fix::with_additional_edits(
561 span_b,
562 "[b]".to_string(),
563 vec![Fix::new(
564 content.len()..content.len(),
565 "[b]: https://b.com\n".to_string(),
566 )],
567 )),
568 rule_name: Some("MD054".to_string()),
569 },
570 ];
571
572 let result = apply_warning_fixes(content, &warnings).unwrap();
573
574 assert!(
576 result.contains("See [a] and [b]."),
577 "primary rewrites missing: {result:?}"
578 );
579 assert!(!result.contains("[a](https://a.com)"));
580 assert!(!result.contains("[b](https://b.com)"));
581
582 let pos_a = result.find("[a]: https://a.com").expect("ref-def for [a] missing");
584 let pos_b = result.find("[b]: https://b.com").expect("ref-def for [b] missing");
585 assert!(
586 pos_a < pos_b,
587 "ref-defs must appear in source order ([a] before [b]); got result:\n{result}"
588 );
589 }
590
591 #[test]
592 fn test_warning_fix_to_edit() {
593 let content = "Hello world";
594 let warning = LintWarning {
595 message: "Test".to_string(),
596 line: 1,
597 column: 1,
598 end_line: 1,
599 end_column: 5,
600 severity: Severity::Warning,
601 fix: Some(Fix::new(0..5, "Hi".to_string())),
602 rule_name: Some("TEST".to_string()),
603 };
604
605 let edit = warning_fix_to_edit(content, &warning).unwrap();
606 assert_eq!(edit, (0, 5, "Hi".to_string()));
607 }
608
609 #[test]
610 fn test_warning_fix_to_edit_no_fix() {
611 let content = "Hello world";
612 let warning = LintWarning {
613 message: "Test".to_string(),
614 line: 1,
615 column: 1,
616 end_line: 1,
617 end_column: 5,
618 severity: Severity::Warning,
619 fix: None,
620 rule_name: Some("TEST".to_string()),
621 };
622
623 let result = warning_fix_to_edit(content, &warning);
624 assert!(result.is_err());
625 assert_eq!(result.unwrap_err(), "Warning has no fix");
626 }
627
628 #[test]
629 fn test_warning_fix_to_edit_invalid_range() {
630 let content = "Short";
631 let warning = LintWarning {
632 message: "Test".to_string(),
633 line: 1,
634 column: 1,
635 end_line: 1,
636 end_column: 10,
637 severity: Severity::Warning,
638 fix: Some(Fix::new(0..100, "Long replacement".to_string())),
639 rule_name: Some("TEST".to_string()),
640 };
641
642 let result = warning_fix_to_edit(content, &warning);
643 assert!(result.is_err());
644 assert!(result.unwrap_err().contains("exceeds content length"));
645 }
646
647 #[test]
648 fn test_validate_fix_range() {
649 let content = "Hello world";
650
651 let valid_fix = Fix::new(0..5, "Hi".to_string());
653 assert!(validate_fix_range(content, &valid_fix).is_ok());
654
655 let invalid_fix = Fix::new(0..20, "Hi".to_string());
657 assert!(validate_fix_range(content, &invalid_fix).is_err());
658
659 let start = 5;
661 let end = 3;
662 let invalid_fix2 = Fix::new(start..end, "Hi".to_string());
663 assert!(validate_fix_range(content, &invalid_fix2).is_err());
664 }
665
666 #[test]
667 fn test_validate_fix_range_edge_cases() {
668 let content = "Test";
669
670 let fix1 = Fix::new(0..0, "Insert".to_string());
672 assert!(validate_fix_range(content, &fix1).is_ok());
673
674 let fix2 = Fix::new(4..4, " append".to_string());
676 assert!(validate_fix_range(content, &fix2).is_ok());
677
678 let fix3 = Fix::new(0..4, "Replace".to_string());
680 assert!(validate_fix_range(content, &fix3).is_ok());
681
682 let fix4 = Fix::new(10..11, "Invalid".to_string());
684 let result = validate_fix_range(content, &fix4);
685 assert!(result.is_err());
686 assert!(result.unwrap_err().contains("start 10 exceeds"));
687 }
688
689 #[test]
690 fn test_fix_ordering_stability() {
691 let content = "Test content here";
693 let warnings = vec![
694 LintWarning {
695 message: "First warning".to_string(),
696 line: 1,
697 column: 6,
698 end_line: 1,
699 end_column: 13,
700 severity: Severity::Warning,
701 fix: Some(Fix::new(5..12, "stuff".to_string())),
702 rule_name: Some("MD001".to_string()),
703 },
704 LintWarning {
705 message: "Second warning".to_string(),
706 line: 1,
707 column: 6,
708 end_line: 1,
709 end_column: 13,
710 severity: Severity::Warning,
711 fix: Some(Fix::new(5..12, "stuff".to_string())),
712 rule_name: Some("MD002".to_string()),
713 },
714 ];
715
716 let result = apply_warning_fixes(content, &warnings).unwrap();
718 assert_eq!(result, "Test stuff here");
719 }
720
721 #[test]
722 fn test_line_ending_preservation() {
723 let content_unix = "Line 1\nLine 2\n";
725 let warning = LintWarning {
726 message: "Add text".to_string(),
727 line: 1,
728 column: 7,
729 end_line: 1,
730 end_column: 7,
731 severity: Severity::Warning,
732 fix: Some(Fix::new(6..6, " added".to_string())),
733 rule_name: Some("TEST".to_string()),
734 };
735
736 let result = apply_warning_fixes(content_unix, &[warning]).unwrap();
737 assert_eq!(result, "Line 1 added\nLine 2\n");
738
739 let content_windows = "Line 1\r\nLine 2\r\n";
741 let warning_windows = LintWarning {
742 message: "Add text".to_string(),
743 line: 1,
744 column: 7,
745 end_line: 1,
746 end_column: 7,
747 severity: Severity::Warning,
748 fix: Some(Fix::new(6..6, " added".to_string())),
749 rule_name: Some("TEST".to_string()),
750 };
751
752 let result_windows = apply_warning_fixes(content_windows, &[warning_windows]).unwrap();
753 assert!(result_windows.starts_with("Line 1 added"));
755 assert!(result_windows.contains("Line 2"));
756 }
757
758 fn make_warning(line: usize, end_line: usize, rule_name: &str) -> LintWarning {
759 LintWarning {
760 message: "test".to_string(),
761 line,
762 column: 1,
763 end_line,
764 end_column: 1,
765 severity: Severity::Warning,
766 fix: Some(Fix::new(0..1, "x".to_string())),
767 rule_name: Some(rule_name.to_string()),
768 }
769 }
770
771 #[test]
772 fn test_filter_warnings_disable_enable_block() {
773 let content =
774 "# Heading\n\n<!-- rumdl-disable MD013 -->\nlong line\n<!-- rumdl-enable MD013 -->\nanother long line\n";
775 let inline_config = InlineConfig::from_content(content);
776
777 let warnings = vec![
778 make_warning(4, 4, "MD013"), make_warning(6, 6, "MD013"), ];
781
782 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
783 assert_eq!(filtered.len(), 1);
784 assert_eq!(filtered[0].line, 6);
785 }
786
787 #[test]
788 fn test_filter_warnings_disable_line() {
789 let content = "line one <!-- rumdl-disable-line MD009 -->\nline two\n";
790 let inline_config = InlineConfig::from_content(content);
791
792 let warnings = vec![
793 make_warning(1, 1, "MD009"), make_warning(2, 2, "MD009"), ];
796
797 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD009");
798 assert_eq!(filtered.len(), 1);
799 assert_eq!(filtered[0].line, 2);
800 }
801
802 #[test]
803 fn test_filter_warnings_disable_next_line() {
804 let content = "<!-- rumdl-disable-next-line MD034 -->\nhttp://example.com\nhttp://other.com\n";
805 let inline_config = InlineConfig::from_content(content);
806
807 let warnings = vec![
808 make_warning(2, 2, "MD034"), make_warning(3, 3, "MD034"), ];
811
812 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD034");
813 assert_eq!(filtered.len(), 1);
814 assert_eq!(filtered[0].line, 3);
815 }
816
817 #[test]
818 fn test_filter_warnings_sub_rule_name() {
819 let content = "<!-- rumdl-disable MD029 -->\nline\n<!-- rumdl-enable MD029 -->\nline\n";
820 let inline_config = InlineConfig::from_content(content);
821
822 let warnings = vec![make_warning(2, 2, "MD029"), make_warning(4, 4, "MD029")];
824
825 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD029-style");
826 assert_eq!(filtered.len(), 1);
827 assert_eq!(filtered[0].line, 4);
828 }
829
830 #[test]
831 fn test_filter_warnings_multi_line_warning() {
832 let content = "line 1\nline 2\nline 3\n<!-- rumdl-disable-line MD013 -->\nline 5\nline 6\n";
834 let inline_config = InlineConfig::from_content(content);
835
836 let warnings = vec![
837 make_warning(3, 5, "MD013"), make_warning(6, 6, "MD013"), ];
840
841 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
842 assert_eq!(filtered.len(), 1);
844 assert_eq!(filtered[0].line, 6);
845 }
846
847 #[test]
848 fn test_filter_warnings_empty_input() {
849 let inline_config = InlineConfig::from_content("");
850 let filtered = filter_warnings_by_inline_config(vec![], &inline_config, "MD013");
851 assert!(filtered.is_empty());
852 }
853
854 #[test]
855 fn test_filter_warnings_none_disabled() {
856 let content = "line 1\nline 2\n";
857 let inline_config = InlineConfig::from_content(content);
858
859 let warnings = vec![make_warning(1, 1, "MD013"), make_warning(2, 2, "MD013")];
860
861 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
862 assert_eq!(filtered.len(), 2);
863 }
864
865 #[test]
866 fn test_filter_warnings_all_disabled() {
867 let content = "<!-- rumdl-disable MD013 -->\nline 1\nline 2\n";
868 let inline_config = InlineConfig::from_content(content);
869
870 let warnings = vec![make_warning(2, 2, "MD013"), make_warning(3, 3, "MD013")];
871
872 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
873 assert!(filtered.is_empty());
874 }
875
876 #[test]
877 fn test_filter_warnings_end_line_zero_fallback() {
878 let content = "<!-- rumdl-disable-line MD013 -->\nline 2\n";
880 let inline_config = InlineConfig::from_content(content);
881
882 let warnings = vec![make_warning(1, 0, "MD013")]; let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
885 assert!(filtered.is_empty());
886 }
887
888 #[test]
889 fn test_filter_non_md_rule_name_preserves_dash() {
890 let content = "line 1\nline 2\n";
893 let inline_config = InlineConfig::from_content(content);
894
895 let warnings = vec![make_warning(1, 1, "custom-rule")];
896
897 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "custom-rule");
899 assert_eq!(filtered.len(), 1, "Non-MD rule name with dash should not be split");
900 }
901
902 #[test]
903 fn test_filter_md_sub_rule_name_is_split() {
904 let content = "<!-- rumdl-disable MD029 -->\nline\n<!-- rumdl-enable MD029 -->\nline\n";
906 let inline_config = InlineConfig::from_content(content);
907
908 let warnings = vec![
909 make_warning(2, 2, "MD029"), make_warning(4, 4, "MD029"), ];
912
913 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD029-style");
915 assert_eq!(filtered.len(), 1);
916 assert_eq!(filtered[0].line, 4);
917 }
918
919 #[test]
920 fn test_filter_warnings_capture_restore() {
921 let content = "<!-- rumdl-disable MD013 -->\nline 1\n<!-- rumdl-capture -->\n<!-- rumdl-enable MD013 -->\nline 4\n<!-- rumdl-restore -->\nline 6\n";
922 let inline_config = InlineConfig::from_content(content);
923
924 let warnings = vec![
925 make_warning(2, 2, "MD013"), make_warning(5, 5, "MD013"), make_warning(7, 7, "MD013"), ];
929
930 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
931 assert_eq!(filtered.len(), 1);
932 assert_eq!(filtered[0].line, 5);
933 }
934}