1use crate::inline_config::InlineConfig;
7use crate::rule::{Fix, LintWarning};
8use crate::utils::ensure_consistent_line_endings;
9
10pub fn filter_warnings_by_inline_config(
15 warnings: Vec<LintWarning>,
16 inline_config: &InlineConfig,
17 rule_name: &str,
18) -> Vec<LintWarning> {
19 let base_rule_name = if let Some(dash_pos) = rule_name.find('-') {
20 let prefix = &rule_name[..dash_pos];
23 if prefix.starts_with("MD") { prefix } else { rule_name }
24 } else {
25 rule_name
26 };
27
28 warnings
29 .into_iter()
30 .filter(|w| {
31 let end = if w.end_line >= w.line { w.end_line } else { w.line };
32 !(w.line..=end).any(|line| inline_config.is_rule_disabled(base_rule_name, line))
33 })
34 .collect()
35}
36
37pub fn apply_warning_fixes(content: &str, warnings: &[LintWarning]) -> Result<String, String> {
40 let mut fixes: Vec<(usize, &Fix)> = warnings
41 .iter()
42 .enumerate()
43 .filter_map(|(i, w)| w.fix.as_ref().map(|fix| (i, fix)))
44 .collect();
45
46 if fixes.is_empty() {
51 return Ok(content.to_string());
52 }
53
54 fixes.sort_by(|(_, fix_a), (_, fix_b)| {
57 let range_cmp = fix_a.range.start.cmp(&fix_b.range.start);
58 if range_cmp != std::cmp::Ordering::Equal {
59 return range_cmp;
60 }
61 fix_a.range.end.cmp(&fix_b.range.end)
62 });
63
64 let mut deduplicated = Vec::new();
65 let mut i = 0;
66 while i < fixes.len() {
67 let (idx, current_fix) = fixes[i];
68 deduplicated.push((idx, current_fix));
69
70 while i + 1 < fixes.len() {
72 let (_, next_fix) = fixes[i + 1];
73 if current_fix.range == next_fix.range && current_fix.replacement == next_fix.replacement {
74 i += 1; } else {
76 break;
77 }
78 }
79 i += 1;
80 }
81
82 let mut fixes = deduplicated;
83
84 fixes.sort_by(|(idx_a, fix_a), (idx_b, fix_b)| {
87 let range_cmp = fix_b.range.start.cmp(&fix_a.range.start);
89 if range_cmp != std::cmp::Ordering::Equal {
90 return range_cmp;
91 }
92
93 let end_cmp = fix_b.range.end.cmp(&fix_a.range.end);
95 if end_cmp != std::cmp::Ordering::Equal {
96 return end_cmp;
97 }
98
99 idx_a.cmp(idx_b)
101 });
102
103 let mut result = content.to_string();
104
105 let mut min_applied_start = usize::MAX;
110
111 for (_, fix) in fixes {
112 if fix.range.end > result.len() {
114 return Err(format!(
115 "Fix range end {} exceeds content length {}",
116 fix.range.end,
117 result.len()
118 ));
119 }
120
121 if fix.range.start > fix.range.end {
122 return Err(format!(
123 "Invalid fix range: start {} > end {}",
124 fix.range.start, fix.range.end
125 ));
126 }
127
128 if fix.range.end > min_applied_start {
131 continue;
132 }
133
134 result.replace_range(fix.range.clone(), &fix.replacement);
136 min_applied_start = fix.range.start;
137 }
138
139 Ok(ensure_consistent_line_endings(content, &result))
141}
142
143pub fn warning_fix_to_edit(content: &str, warning: &LintWarning) -> Result<(usize, usize, String), String> {
146 if let Some(fix) = &warning.fix {
147 if fix.range.end > content.len() {
149 return Err(format!(
150 "Fix range end {} exceeds content length {}",
151 fix.range.end,
152 content.len()
153 ));
154 }
155
156 Ok((fix.range.start, fix.range.end, fix.replacement.clone()))
157 } else {
158 Err("Warning has no fix".to_string())
159 }
160}
161
162pub fn validate_fix_range(content: &str, fix: &Fix) -> Result<(), String> {
164 if fix.range.start > content.len() {
165 return Err(format!(
166 "Fix range start {} exceeds content length {}",
167 fix.range.start,
168 content.len()
169 ));
170 }
171
172 if fix.range.end > content.len() {
173 return Err(format!(
174 "Fix range end {} exceeds content length {}",
175 fix.range.end,
176 content.len()
177 ));
178 }
179
180 if fix.range.start > fix.range.end {
181 return Err(format!(
182 "Invalid fix range: start {} > end {}",
183 fix.range.start, fix.range.end
184 ));
185 }
186
187 Ok(())
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::rule::{Fix, LintWarning, Severity};
194
195 #[test]
196 fn test_apply_single_fix() {
197 let content = "1. Multiple spaces";
198 let warning = LintWarning {
199 message: "Too many spaces".to_string(),
200 line: 1,
201 column: 3,
202 end_line: 1,
203 end_column: 5,
204 severity: Severity::Warning,
205 fix: Some(Fix {
206 range: 2..4, replacement: " ".to_string(), }),
209 rule_name: Some("MD030".to_string()),
210 };
211
212 let result = apply_warning_fixes(content, &[warning]).unwrap();
213 assert_eq!(result, "1. Multiple spaces");
214 }
215
216 #[test]
217 fn test_apply_multiple_fixes() {
218 let content = "1. First\n* Second";
219 let warnings = vec![
220 LintWarning {
221 message: "Too many spaces".to_string(),
222 line: 1,
223 column: 3,
224 end_line: 1,
225 end_column: 5,
226 severity: Severity::Warning,
227 fix: Some(Fix {
228 range: 2..4, replacement: " ".to_string(),
230 }),
231 rule_name: Some("MD030".to_string()),
232 },
233 LintWarning {
234 message: "Too many spaces".to_string(),
235 line: 2,
236 column: 2,
237 end_line: 2,
238 end_column: 5,
239 severity: Severity::Warning,
240 fix: Some(Fix {
241 range: 11..14, replacement: " ".to_string(),
243 }),
244 rule_name: Some("MD030".to_string()),
245 },
246 ];
247
248 let result = apply_warning_fixes(content, &warnings).unwrap();
249 assert_eq!(result, "1. First\n* Second");
250 }
251
252 #[test]
253 fn test_apply_non_overlapping_fixes() {
254 let content = "Test multiple spaces";
259 let warnings = vec![
260 LintWarning {
261 message: "Too many spaces".to_string(),
262 line: 1,
263 column: 5,
264 end_line: 1,
265 end_column: 7,
266 severity: Severity::Warning,
267 fix: Some(Fix {
268 range: 4..6, replacement: " ".to_string(),
270 }),
271 rule_name: Some("MD009".to_string()),
272 },
273 LintWarning {
274 message: "Too many spaces".to_string(),
275 line: 1,
276 column: 15,
277 end_line: 1,
278 end_column: 19,
279 severity: Severity::Warning,
280 fix: Some(Fix {
281 range: 14..18, replacement: " ".to_string(),
283 }),
284 rule_name: Some("MD009".to_string()),
285 },
286 ];
287
288 let result = apply_warning_fixes(content, &warnings).unwrap();
289 assert_eq!(result, "Test multiple spaces");
290 }
291
292 #[test]
293 fn test_apply_duplicate_fixes() {
294 let content = "Test content";
295 let warnings = vec![
296 LintWarning {
297 message: "Fix 1".to_string(),
298 line: 1,
299 column: 5,
300 end_line: 1,
301 end_column: 7,
302 severity: Severity::Warning,
303 fix: Some(Fix {
304 range: 4..6,
305 replacement: " ".to_string(),
306 }),
307 rule_name: Some("MD009".to_string()),
308 },
309 LintWarning {
310 message: "Fix 2 (duplicate)".to_string(),
311 line: 1,
312 column: 5,
313 end_line: 1,
314 end_column: 7,
315 severity: Severity::Warning,
316 fix: Some(Fix {
317 range: 4..6,
318 replacement: " ".to_string(),
319 }),
320 rule_name: Some("MD009".to_string()),
321 },
322 ];
323
324 let result = apply_warning_fixes(content, &warnings).unwrap();
326 assert_eq!(result, "Test content");
327 }
328
329 #[test]
330 fn test_apply_fixes_with_windows_line_endings() {
331 let content = "1. First\r\n* Second\r\n";
332 let warnings = vec![
333 LintWarning {
334 message: "Too many spaces".to_string(),
335 line: 1,
336 column: 3,
337 end_line: 1,
338 end_column: 5,
339 severity: Severity::Warning,
340 fix: Some(Fix {
341 range: 2..4,
342 replacement: " ".to_string(),
343 }),
344 rule_name: Some("MD030".to_string()),
345 },
346 LintWarning {
347 message: "Too many spaces".to_string(),
348 line: 2,
349 column: 2,
350 end_line: 2,
351 end_column: 5,
352 severity: Severity::Warning,
353 fix: Some(Fix {
354 range: 12..15, replacement: " ".to_string(),
356 }),
357 rule_name: Some("MD030".to_string()),
358 },
359 ];
360
361 let result = apply_warning_fixes(content, &warnings).unwrap();
362 assert!(result.contains("1. First"));
365 assert!(result.contains("* Second"));
366 }
367
368 #[test]
369 fn test_apply_fix_with_invalid_range() {
370 let content = "Short";
371 let warning = LintWarning {
372 message: "Invalid fix".to_string(),
373 line: 1,
374 column: 1,
375 end_line: 1,
376 end_column: 10,
377 severity: Severity::Warning,
378 fix: Some(Fix {
379 range: 0..100, replacement: "Replacement".to_string(),
381 }),
382 rule_name: Some("TEST".to_string()),
383 };
384
385 let result = apply_warning_fixes(content, &[warning]);
386 assert!(result.is_err());
387 assert!(result.unwrap_err().contains("exceeds content length"));
388 }
389
390 #[test]
391 fn test_apply_fix_with_reversed_range() {
392 let content = "Hello world";
393 let warning = LintWarning {
394 message: "Invalid fix".to_string(),
395 line: 1,
396 column: 5,
397 end_line: 1,
398 end_column: 3,
399 severity: Severity::Warning,
400 fix: Some(Fix {
401 #[allow(clippy::reversed_empty_ranges)]
402 range: 10..5, replacement: "Test".to_string(),
404 }),
405 rule_name: Some("TEST".to_string()),
406 };
407
408 let result = apply_warning_fixes(content, &[warning]);
409 assert!(result.is_err());
410 assert!(result.unwrap_err().contains("Invalid fix range"));
411 }
412
413 #[test]
414 fn test_apply_no_fixes() {
415 let content = "No changes needed";
416 let warnings = vec![LintWarning {
417 message: "Warning without fix".to_string(),
418 line: 1,
419 column: 1,
420 end_line: 1,
421 end_column: 5,
422 severity: Severity::Warning,
423 fix: None,
424 rule_name: Some("TEST".to_string()),
425 }];
426
427 let result = apply_warning_fixes(content, &warnings).unwrap();
428 assert_eq!(result, content);
429 }
430
431 #[test]
432 fn test_overlapping_fixes_skip_outer() {
433 let content = "[  ](url) suffix";
438 let warnings = vec](url)".to_string(),
449 }),
450 rule_name: Some("MD039".to_string()),
451 },
452 LintWarning {
453 message: "Inner image".to_string(),
454 line: 1,
455 column: 3,
456 end_line: 1,
457 end_column: 15,
458 severity: Severity::Warning,
459 fix: Some(Fix {
460 range: 2..15,
461 replacement: "".to_string(),
462 }),
463 rule_name: Some("MD039".to_string()),
464 },
465 ];
466
467 let result = apply_warning_fixes(content, &warnings).unwrap();
468 assert_eq!(result, "[  ](url) suffix");
471 }
472
473 #[test]
474 fn test_warning_fix_to_edit() {
475 let content = "Hello world";
476 let warning = LintWarning {
477 message: "Test".to_string(),
478 line: 1,
479 column: 1,
480 end_line: 1,
481 end_column: 5,
482 severity: Severity::Warning,
483 fix: Some(Fix {
484 range: 0..5,
485 replacement: "Hi".to_string(),
486 }),
487 rule_name: Some("TEST".to_string()),
488 };
489
490 let edit = warning_fix_to_edit(content, &warning).unwrap();
491 assert_eq!(edit, (0, 5, "Hi".to_string()));
492 }
493
494 #[test]
495 fn test_warning_fix_to_edit_no_fix() {
496 let content = "Hello world";
497 let warning = LintWarning {
498 message: "Test".to_string(),
499 line: 1,
500 column: 1,
501 end_line: 1,
502 end_column: 5,
503 severity: Severity::Warning,
504 fix: None,
505 rule_name: Some("TEST".to_string()),
506 };
507
508 let result = warning_fix_to_edit(content, &warning);
509 assert!(result.is_err());
510 assert_eq!(result.unwrap_err(), "Warning has no fix");
511 }
512
513 #[test]
514 fn test_warning_fix_to_edit_invalid_range() {
515 let content = "Short";
516 let warning = LintWarning {
517 message: "Test".to_string(),
518 line: 1,
519 column: 1,
520 end_line: 1,
521 end_column: 10,
522 severity: Severity::Warning,
523 fix: Some(Fix {
524 range: 0..100,
525 replacement: "Long replacement".to_string(),
526 }),
527 rule_name: Some("TEST".to_string()),
528 };
529
530 let result = warning_fix_to_edit(content, &warning);
531 assert!(result.is_err());
532 assert!(result.unwrap_err().contains("exceeds content length"));
533 }
534
535 #[test]
536 fn test_validate_fix_range() {
537 let content = "Hello world";
538
539 let valid_fix = Fix {
541 range: 0..5,
542 replacement: "Hi".to_string(),
543 };
544 assert!(validate_fix_range(content, &valid_fix).is_ok());
545
546 let invalid_fix = Fix {
548 range: 0..20,
549 replacement: "Hi".to_string(),
550 };
551 assert!(validate_fix_range(content, &invalid_fix).is_err());
552
553 let start = 5;
555 let end = 3;
556 let invalid_fix2 = Fix {
557 range: start..end,
558 replacement: "Hi".to_string(),
559 };
560 assert!(validate_fix_range(content, &invalid_fix2).is_err());
561 }
562
563 #[test]
564 fn test_validate_fix_range_edge_cases() {
565 let content = "Test";
566
567 let fix1 = Fix {
569 range: 0..0,
570 replacement: "Insert".to_string(),
571 };
572 assert!(validate_fix_range(content, &fix1).is_ok());
573
574 let fix2 = Fix {
576 range: 4..4,
577 replacement: " append".to_string(),
578 };
579 assert!(validate_fix_range(content, &fix2).is_ok());
580
581 let fix3 = Fix {
583 range: 0..4,
584 replacement: "Replace".to_string(),
585 };
586 assert!(validate_fix_range(content, &fix3).is_ok());
587
588 let fix4 = Fix {
590 range: 10..11,
591 replacement: "Invalid".to_string(),
592 };
593 let result = validate_fix_range(content, &fix4);
594 assert!(result.is_err());
595 assert!(result.unwrap_err().contains("start 10 exceeds"));
596 }
597
598 #[test]
599 fn test_fix_ordering_stability() {
600 let content = "Test content here";
602 let warnings = vec![
603 LintWarning {
604 message: "First warning".to_string(),
605 line: 1,
606 column: 6,
607 end_line: 1,
608 end_column: 13,
609 severity: Severity::Warning,
610 fix: Some(Fix {
611 range: 5..12, replacement: "stuff".to_string(),
613 }),
614 rule_name: Some("MD001".to_string()),
615 },
616 LintWarning {
617 message: "Second warning".to_string(),
618 line: 1,
619 column: 6,
620 end_line: 1,
621 end_column: 13,
622 severity: Severity::Warning,
623 fix: Some(Fix {
624 range: 5..12, replacement: "stuff".to_string(),
626 }),
627 rule_name: Some("MD002".to_string()),
628 },
629 ];
630
631 let result = apply_warning_fixes(content, &warnings).unwrap();
633 assert_eq!(result, "Test stuff here");
634 }
635
636 #[test]
637 fn test_line_ending_preservation() {
638 let content_unix = "Line 1\nLine 2\n";
640 let warning = LintWarning {
641 message: "Add text".to_string(),
642 line: 1,
643 column: 7,
644 end_line: 1,
645 end_column: 7,
646 severity: Severity::Warning,
647 fix: Some(Fix {
648 range: 6..6,
649 replacement: " added".to_string(),
650 }),
651 rule_name: Some("TEST".to_string()),
652 };
653
654 let result = apply_warning_fixes(content_unix, &[warning]).unwrap();
655 assert_eq!(result, "Line 1 added\nLine 2\n");
656
657 let content_windows = "Line 1\r\nLine 2\r\n";
659 let warning_windows = LintWarning {
660 message: "Add text".to_string(),
661 line: 1,
662 column: 7,
663 end_line: 1,
664 end_column: 7,
665 severity: Severity::Warning,
666 fix: Some(Fix {
667 range: 6..6,
668 replacement: " added".to_string(),
669 }),
670 rule_name: Some("TEST".to_string()),
671 };
672
673 let result_windows = apply_warning_fixes(content_windows, &[warning_windows]).unwrap();
674 assert!(result_windows.starts_with("Line 1 added"));
676 assert!(result_windows.contains("Line 2"));
677 }
678
679 fn make_warning(line: usize, end_line: usize, rule_name: &str) -> LintWarning {
680 LintWarning {
681 message: "test".to_string(),
682 line,
683 column: 1,
684 end_line,
685 end_column: 1,
686 severity: Severity::Warning,
687 fix: Some(Fix {
688 range: 0..1,
689 replacement: "x".to_string(),
690 }),
691 rule_name: Some(rule_name.to_string()),
692 }
693 }
694
695 #[test]
696 fn test_filter_warnings_disable_enable_block() {
697 let content =
698 "# Heading\n\n<!-- rumdl-disable MD013 -->\nlong line\n<!-- rumdl-enable MD013 -->\nanother long line\n";
699 let inline_config = InlineConfig::from_content(content);
700
701 let warnings = vec![
702 make_warning(4, 4, "MD013"), make_warning(6, 6, "MD013"), ];
705
706 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
707 assert_eq!(filtered.len(), 1);
708 assert_eq!(filtered[0].line, 6);
709 }
710
711 #[test]
712 fn test_filter_warnings_disable_line() {
713 let content = "line one <!-- rumdl-disable-line MD009 -->\nline two\n";
714 let inline_config = InlineConfig::from_content(content);
715
716 let warnings = vec![
717 make_warning(1, 1, "MD009"), make_warning(2, 2, "MD009"), ];
720
721 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD009");
722 assert_eq!(filtered.len(), 1);
723 assert_eq!(filtered[0].line, 2);
724 }
725
726 #[test]
727 fn test_filter_warnings_disable_next_line() {
728 let content = "<!-- rumdl-disable-next-line MD034 -->\nhttp://example.com\nhttp://other.com\n";
729 let inline_config = InlineConfig::from_content(content);
730
731 let warnings = vec![
732 make_warning(2, 2, "MD034"), make_warning(3, 3, "MD034"), ];
735
736 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD034");
737 assert_eq!(filtered.len(), 1);
738 assert_eq!(filtered[0].line, 3);
739 }
740
741 #[test]
742 fn test_filter_warnings_sub_rule_name() {
743 let content = "<!-- rumdl-disable MD029 -->\nline\n<!-- rumdl-enable MD029 -->\nline\n";
744 let inline_config = InlineConfig::from_content(content);
745
746 let warnings = vec![make_warning(2, 2, "MD029"), make_warning(4, 4, "MD029")];
748
749 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD029-style");
750 assert_eq!(filtered.len(), 1);
751 assert_eq!(filtered[0].line, 4);
752 }
753
754 #[test]
755 fn test_filter_warnings_multi_line_warning() {
756 let content = "line 1\nline 2\nline 3\n<!-- rumdl-disable-line MD013 -->\nline 5\nline 6\n";
758 let inline_config = InlineConfig::from_content(content);
759
760 let warnings = vec![
761 make_warning(3, 5, "MD013"), make_warning(6, 6, "MD013"), ];
764
765 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
766 assert_eq!(filtered.len(), 1);
768 assert_eq!(filtered[0].line, 6);
769 }
770
771 #[test]
772 fn test_filter_warnings_empty_input() {
773 let inline_config = InlineConfig::from_content("");
774 let filtered = filter_warnings_by_inline_config(vec![], &inline_config, "MD013");
775 assert!(filtered.is_empty());
776 }
777
778 #[test]
779 fn test_filter_warnings_none_disabled() {
780 let content = "line 1\nline 2\n";
781 let inline_config = InlineConfig::from_content(content);
782
783 let warnings = vec![make_warning(1, 1, "MD013"), make_warning(2, 2, "MD013")];
784
785 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
786 assert_eq!(filtered.len(), 2);
787 }
788
789 #[test]
790 fn test_filter_warnings_all_disabled() {
791 let content = "<!-- rumdl-disable MD013 -->\nline 1\nline 2\n";
792 let inline_config = InlineConfig::from_content(content);
793
794 let warnings = vec![make_warning(2, 2, "MD013"), make_warning(3, 3, "MD013")];
795
796 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
797 assert!(filtered.is_empty());
798 }
799
800 #[test]
801 fn test_filter_warnings_end_line_zero_fallback() {
802 let content = "<!-- rumdl-disable-line MD013 -->\nline 2\n";
804 let inline_config = InlineConfig::from_content(content);
805
806 let warnings = vec![make_warning(1, 0, "MD013")]; let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
809 assert!(filtered.is_empty());
810 }
811
812 #[test]
813 fn test_filter_non_md_rule_name_preserves_dash() {
814 let content = "line 1\nline 2\n";
817 let inline_config = InlineConfig::from_content(content);
818
819 let warnings = vec![make_warning(1, 1, "custom-rule")];
820
821 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "custom-rule");
823 assert_eq!(filtered.len(), 1, "Non-MD rule name with dash should not be split");
824 }
825
826 #[test]
827 fn test_filter_md_sub_rule_name_is_split() {
828 let content = "<!-- rumdl-disable MD029 -->\nline\n<!-- rumdl-enable MD029 -->\nline\n";
830 let inline_config = InlineConfig::from_content(content);
831
832 let warnings = vec![
833 make_warning(2, 2, "MD029"), make_warning(4, 4, "MD029"), ];
836
837 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD029-style");
839 assert_eq!(filtered.len(), 1);
840 assert_eq!(filtered[0].line, 4);
841 }
842
843 #[test]
844 fn test_filter_warnings_capture_restore() {
845 let content = "<!-- rumdl-disable MD013 -->\nline 1\n<!-- rumdl-capture -->\n<!-- rumdl-enable MD013 -->\nline 4\n<!-- rumdl-restore -->\nline 6\n";
846 let inline_config = InlineConfig::from_content(content);
847
848 let warnings = vec![
849 make_warning(2, 2, "MD013"), make_warning(5, 5, "MD013"), make_warning(7, 7, "MD013"), ];
853
854 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
855 assert_eq!(filtered.len(), 1);
856 assert_eq!(filtered[0].line, 5);
857 }
858}