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 fixes.sort_by(|(_, fix_a), (_, fix_b)| {
49 let range_cmp = fix_a.range.start.cmp(&fix_b.range.start);
50 if range_cmp != std::cmp::Ordering::Equal {
51 return range_cmp;
52 }
53 fix_a.range.end.cmp(&fix_b.range.end)
54 });
55
56 let mut deduplicated = Vec::new();
57 let mut i = 0;
58 while i < fixes.len() {
59 let (idx, current_fix) = fixes[i];
60 deduplicated.push((idx, current_fix));
61
62 while i + 1 < fixes.len() {
64 let (_, next_fix) = fixes[i + 1];
65 if current_fix.range == next_fix.range && current_fix.replacement == next_fix.replacement {
66 i += 1; } else {
68 break;
69 }
70 }
71 i += 1;
72 }
73
74 let mut fixes = deduplicated;
75
76 fixes.sort_by(|(idx_a, fix_a), (idx_b, fix_b)| {
79 let range_cmp = fix_b.range.start.cmp(&fix_a.range.start);
81 if range_cmp != std::cmp::Ordering::Equal {
82 return range_cmp;
83 }
84
85 let end_cmp = fix_b.range.end.cmp(&fix_a.range.end);
87 if end_cmp != std::cmp::Ordering::Equal {
88 return end_cmp;
89 }
90
91 idx_a.cmp(idx_b)
93 });
94
95 let mut result = content.to_string();
96
97 for (_, fix) in fixes {
98 if fix.range.end > result.len() {
100 return Err(format!(
101 "Fix range end {} exceeds content length {}",
102 fix.range.end,
103 result.len()
104 ));
105 }
106
107 if fix.range.start > fix.range.end {
108 return Err(format!(
109 "Invalid fix range: start {} > end {}",
110 fix.range.start, fix.range.end
111 ));
112 }
113
114 result.replace_range(fix.range.clone(), &fix.replacement);
116 }
117
118 Ok(ensure_consistent_line_endings(content, &result))
120}
121
122pub fn warning_fix_to_edit(content: &str, warning: &LintWarning) -> Result<(usize, usize, String), String> {
125 if let Some(fix) = &warning.fix {
126 if fix.range.end > content.len() {
128 return Err(format!(
129 "Fix range end {} exceeds content length {}",
130 fix.range.end,
131 content.len()
132 ));
133 }
134
135 Ok((fix.range.start, fix.range.end, fix.replacement.clone()))
136 } else {
137 Err("Warning has no fix".to_string())
138 }
139}
140
141pub fn validate_fix_range(content: &str, fix: &Fix) -> Result<(), String> {
143 if fix.range.start > content.len() {
144 return Err(format!(
145 "Fix range start {} exceeds content length {}",
146 fix.range.start,
147 content.len()
148 ));
149 }
150
151 if fix.range.end > content.len() {
152 return Err(format!(
153 "Fix range end {} exceeds content length {}",
154 fix.range.end,
155 content.len()
156 ));
157 }
158
159 if fix.range.start > fix.range.end {
160 return Err(format!(
161 "Invalid fix range: start {} > end {}",
162 fix.range.start, fix.range.end
163 ));
164 }
165
166 Ok(())
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::rule::{Fix, LintWarning, Severity};
173
174 #[test]
175 fn test_apply_single_fix() {
176 let content = "1. Multiple spaces";
177 let warning = LintWarning {
178 message: "Too many spaces".to_string(),
179 line: 1,
180 column: 3,
181 end_line: 1,
182 end_column: 5,
183 severity: Severity::Warning,
184 fix: Some(Fix {
185 range: 2..4, replacement: " ".to_string(), }),
188 rule_name: Some("MD030".to_string()),
189 };
190
191 let result = apply_warning_fixes(content, &[warning]).unwrap();
192 assert_eq!(result, "1. Multiple spaces");
193 }
194
195 #[test]
196 fn test_apply_multiple_fixes() {
197 let content = "1. First\n* Second";
198 let warnings = vec![
199 LintWarning {
200 message: "Too many spaces".to_string(),
201 line: 1,
202 column: 3,
203 end_line: 1,
204 end_column: 5,
205 severity: Severity::Warning,
206 fix: Some(Fix {
207 range: 2..4, replacement: " ".to_string(),
209 }),
210 rule_name: Some("MD030".to_string()),
211 },
212 LintWarning {
213 message: "Too many spaces".to_string(),
214 line: 2,
215 column: 2,
216 end_line: 2,
217 end_column: 5,
218 severity: Severity::Warning,
219 fix: Some(Fix {
220 range: 11..14, replacement: " ".to_string(),
222 }),
223 rule_name: Some("MD030".to_string()),
224 },
225 ];
226
227 let result = apply_warning_fixes(content, &warnings).unwrap();
228 assert_eq!(result, "1. First\n* Second");
229 }
230
231 #[test]
232 fn test_apply_non_overlapping_fixes() {
233 let content = "Test multiple spaces";
238 let warnings = vec![
239 LintWarning {
240 message: "Too many spaces".to_string(),
241 line: 1,
242 column: 5,
243 end_line: 1,
244 end_column: 7,
245 severity: Severity::Warning,
246 fix: Some(Fix {
247 range: 4..6, replacement: " ".to_string(),
249 }),
250 rule_name: Some("MD009".to_string()),
251 },
252 LintWarning {
253 message: "Too many spaces".to_string(),
254 line: 1,
255 column: 15,
256 end_line: 1,
257 end_column: 19,
258 severity: Severity::Warning,
259 fix: Some(Fix {
260 range: 14..18, replacement: " ".to_string(),
262 }),
263 rule_name: Some("MD009".to_string()),
264 },
265 ];
266
267 let result = apply_warning_fixes(content, &warnings).unwrap();
268 assert_eq!(result, "Test multiple spaces");
269 }
270
271 #[test]
272 fn test_apply_duplicate_fixes() {
273 let content = "Test content";
274 let warnings = vec![
275 LintWarning {
276 message: "Fix 1".to_string(),
277 line: 1,
278 column: 5,
279 end_line: 1,
280 end_column: 7,
281 severity: Severity::Warning,
282 fix: Some(Fix {
283 range: 4..6,
284 replacement: " ".to_string(),
285 }),
286 rule_name: Some("MD009".to_string()),
287 },
288 LintWarning {
289 message: "Fix 2 (duplicate)".to_string(),
290 line: 1,
291 column: 5,
292 end_line: 1,
293 end_column: 7,
294 severity: Severity::Warning,
295 fix: Some(Fix {
296 range: 4..6,
297 replacement: " ".to_string(),
298 }),
299 rule_name: Some("MD009".to_string()),
300 },
301 ];
302
303 let result = apply_warning_fixes(content, &warnings).unwrap();
305 assert_eq!(result, "Test content");
306 }
307
308 #[test]
309 fn test_apply_fixes_with_windows_line_endings() {
310 let content = "1. First\r\n* Second\r\n";
311 let warnings = vec![
312 LintWarning {
313 message: "Too many spaces".to_string(),
314 line: 1,
315 column: 3,
316 end_line: 1,
317 end_column: 5,
318 severity: Severity::Warning,
319 fix: Some(Fix {
320 range: 2..4,
321 replacement: " ".to_string(),
322 }),
323 rule_name: Some("MD030".to_string()),
324 },
325 LintWarning {
326 message: "Too many spaces".to_string(),
327 line: 2,
328 column: 2,
329 end_line: 2,
330 end_column: 5,
331 severity: Severity::Warning,
332 fix: Some(Fix {
333 range: 12..15, replacement: " ".to_string(),
335 }),
336 rule_name: Some("MD030".to_string()),
337 },
338 ];
339
340 let result = apply_warning_fixes(content, &warnings).unwrap();
341 assert!(result.contains("1. First"));
344 assert!(result.contains("* Second"));
345 }
346
347 #[test]
348 fn test_apply_fix_with_invalid_range() {
349 let content = "Short";
350 let warning = LintWarning {
351 message: "Invalid fix".to_string(),
352 line: 1,
353 column: 1,
354 end_line: 1,
355 end_column: 10,
356 severity: Severity::Warning,
357 fix: Some(Fix {
358 range: 0..100, replacement: "Replacement".to_string(),
360 }),
361 rule_name: Some("TEST".to_string()),
362 };
363
364 let result = apply_warning_fixes(content, &[warning]);
365 assert!(result.is_err());
366 assert!(result.unwrap_err().contains("exceeds content length"));
367 }
368
369 #[test]
370 fn test_apply_fix_with_reversed_range() {
371 let content = "Hello world";
372 let warning = LintWarning {
373 message: "Invalid fix".to_string(),
374 line: 1,
375 column: 5,
376 end_line: 1,
377 end_column: 3,
378 severity: Severity::Warning,
379 fix: Some(Fix {
380 #[allow(clippy::reversed_empty_ranges)]
381 range: 10..5, replacement: "Test".to_string(),
383 }),
384 rule_name: Some("TEST".to_string()),
385 };
386
387 let result = apply_warning_fixes(content, &[warning]);
388 assert!(result.is_err());
389 assert!(result.unwrap_err().contains("Invalid fix range"));
390 }
391
392 #[test]
393 fn test_apply_no_fixes() {
394 let content = "No changes needed";
395 let warnings = vec![LintWarning {
396 message: "Warning without fix".to_string(),
397 line: 1,
398 column: 1,
399 end_line: 1,
400 end_column: 5,
401 severity: Severity::Warning,
402 fix: None,
403 rule_name: Some("TEST".to_string()),
404 }];
405
406 let result = apply_warning_fixes(content, &warnings).unwrap();
407 assert_eq!(result, content);
408 }
409
410 #[test]
411 fn test_warning_fix_to_edit() {
412 let content = "Hello world";
413 let warning = LintWarning {
414 message: "Test".to_string(),
415 line: 1,
416 column: 1,
417 end_line: 1,
418 end_column: 5,
419 severity: Severity::Warning,
420 fix: Some(Fix {
421 range: 0..5,
422 replacement: "Hi".to_string(),
423 }),
424 rule_name: Some("TEST".to_string()),
425 };
426
427 let edit = warning_fix_to_edit(content, &warning).unwrap();
428 assert_eq!(edit, (0, 5, "Hi".to_string()));
429 }
430
431 #[test]
432 fn test_warning_fix_to_edit_no_fix() {
433 let content = "Hello world";
434 let warning = LintWarning {
435 message: "Test".to_string(),
436 line: 1,
437 column: 1,
438 end_line: 1,
439 end_column: 5,
440 severity: Severity::Warning,
441 fix: None,
442 rule_name: Some("TEST".to_string()),
443 };
444
445 let result = warning_fix_to_edit(content, &warning);
446 assert!(result.is_err());
447 assert_eq!(result.unwrap_err(), "Warning has no fix");
448 }
449
450 #[test]
451 fn test_warning_fix_to_edit_invalid_range() {
452 let content = "Short";
453 let warning = LintWarning {
454 message: "Test".to_string(),
455 line: 1,
456 column: 1,
457 end_line: 1,
458 end_column: 10,
459 severity: Severity::Warning,
460 fix: Some(Fix {
461 range: 0..100,
462 replacement: "Long replacement".to_string(),
463 }),
464 rule_name: Some("TEST".to_string()),
465 };
466
467 let result = warning_fix_to_edit(content, &warning);
468 assert!(result.is_err());
469 assert!(result.unwrap_err().contains("exceeds content length"));
470 }
471
472 #[test]
473 fn test_validate_fix_range() {
474 let content = "Hello world";
475
476 let valid_fix = Fix {
478 range: 0..5,
479 replacement: "Hi".to_string(),
480 };
481 assert!(validate_fix_range(content, &valid_fix).is_ok());
482
483 let invalid_fix = Fix {
485 range: 0..20,
486 replacement: "Hi".to_string(),
487 };
488 assert!(validate_fix_range(content, &invalid_fix).is_err());
489
490 let start = 5;
492 let end = 3;
493 let invalid_fix2 = Fix {
494 range: start..end,
495 replacement: "Hi".to_string(),
496 };
497 assert!(validate_fix_range(content, &invalid_fix2).is_err());
498 }
499
500 #[test]
501 fn test_validate_fix_range_edge_cases() {
502 let content = "Test";
503
504 let fix1 = Fix {
506 range: 0..0,
507 replacement: "Insert".to_string(),
508 };
509 assert!(validate_fix_range(content, &fix1).is_ok());
510
511 let fix2 = Fix {
513 range: 4..4,
514 replacement: " append".to_string(),
515 };
516 assert!(validate_fix_range(content, &fix2).is_ok());
517
518 let fix3 = Fix {
520 range: 0..4,
521 replacement: "Replace".to_string(),
522 };
523 assert!(validate_fix_range(content, &fix3).is_ok());
524
525 let fix4 = Fix {
527 range: 10..11,
528 replacement: "Invalid".to_string(),
529 };
530 let result = validate_fix_range(content, &fix4);
531 assert!(result.is_err());
532 assert!(result.unwrap_err().contains("start 10 exceeds"));
533 }
534
535 #[test]
536 fn test_fix_ordering_stability() {
537 let content = "Test content here";
539 let warnings = vec![
540 LintWarning {
541 message: "First warning".to_string(),
542 line: 1,
543 column: 6,
544 end_line: 1,
545 end_column: 13,
546 severity: Severity::Warning,
547 fix: Some(Fix {
548 range: 5..12, replacement: "stuff".to_string(),
550 }),
551 rule_name: Some("MD001".to_string()),
552 },
553 LintWarning {
554 message: "Second warning".to_string(),
555 line: 1,
556 column: 6,
557 end_line: 1,
558 end_column: 13,
559 severity: Severity::Warning,
560 fix: Some(Fix {
561 range: 5..12, replacement: "stuff".to_string(),
563 }),
564 rule_name: Some("MD002".to_string()),
565 },
566 ];
567
568 let result = apply_warning_fixes(content, &warnings).unwrap();
570 assert_eq!(result, "Test stuff here");
571 }
572
573 #[test]
574 fn test_line_ending_preservation() {
575 let content_unix = "Line 1\nLine 2\n";
577 let warning = LintWarning {
578 message: "Add text".to_string(),
579 line: 1,
580 column: 7,
581 end_line: 1,
582 end_column: 7,
583 severity: Severity::Warning,
584 fix: Some(Fix {
585 range: 6..6,
586 replacement: " added".to_string(),
587 }),
588 rule_name: Some("TEST".to_string()),
589 };
590
591 let result = apply_warning_fixes(content_unix, &[warning]).unwrap();
592 assert_eq!(result, "Line 1 added\nLine 2\n");
593
594 let content_windows = "Line 1\r\nLine 2\r\n";
596 let warning_windows = LintWarning {
597 message: "Add text".to_string(),
598 line: 1,
599 column: 7,
600 end_line: 1,
601 end_column: 7,
602 severity: Severity::Warning,
603 fix: Some(Fix {
604 range: 6..6,
605 replacement: " added".to_string(),
606 }),
607 rule_name: Some("TEST".to_string()),
608 };
609
610 let result_windows = apply_warning_fixes(content_windows, &[warning_windows]).unwrap();
611 assert!(result_windows.starts_with("Line 1 added"));
613 assert!(result_windows.contains("Line 2"));
614 }
615
616 fn make_warning(line: usize, end_line: usize, rule_name: &str) -> LintWarning {
617 LintWarning {
618 message: "test".to_string(),
619 line,
620 column: 1,
621 end_line,
622 end_column: 1,
623 severity: Severity::Warning,
624 fix: Some(Fix {
625 range: 0..1,
626 replacement: "x".to_string(),
627 }),
628 rule_name: Some(rule_name.to_string()),
629 }
630 }
631
632 #[test]
633 fn test_filter_warnings_disable_enable_block() {
634 let content =
635 "# Heading\n\n<!-- rumdl-disable MD013 -->\nlong line\n<!-- rumdl-enable MD013 -->\nanother long line\n";
636 let inline_config = InlineConfig::from_content(content);
637
638 let warnings = vec![
639 make_warning(4, 4, "MD013"), make_warning(6, 6, "MD013"), ];
642
643 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
644 assert_eq!(filtered.len(), 1);
645 assert_eq!(filtered[0].line, 6);
646 }
647
648 #[test]
649 fn test_filter_warnings_disable_line() {
650 let content = "line one <!-- rumdl-disable-line MD009 -->\nline two\n";
651 let inline_config = InlineConfig::from_content(content);
652
653 let warnings = vec![
654 make_warning(1, 1, "MD009"), make_warning(2, 2, "MD009"), ];
657
658 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD009");
659 assert_eq!(filtered.len(), 1);
660 assert_eq!(filtered[0].line, 2);
661 }
662
663 #[test]
664 fn test_filter_warnings_disable_next_line() {
665 let content = "<!-- rumdl-disable-next-line MD034 -->\nhttp://example.com\nhttp://other.com\n";
666 let inline_config = InlineConfig::from_content(content);
667
668 let warnings = vec![
669 make_warning(2, 2, "MD034"), make_warning(3, 3, "MD034"), ];
672
673 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD034");
674 assert_eq!(filtered.len(), 1);
675 assert_eq!(filtered[0].line, 3);
676 }
677
678 #[test]
679 fn test_filter_warnings_sub_rule_name() {
680 let content = "<!-- rumdl-disable MD029 -->\nline\n<!-- rumdl-enable MD029 -->\nline\n";
681 let inline_config = InlineConfig::from_content(content);
682
683 let warnings = vec![make_warning(2, 2, "MD029"), make_warning(4, 4, "MD029")];
685
686 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD029-style");
687 assert_eq!(filtered.len(), 1);
688 assert_eq!(filtered[0].line, 4);
689 }
690
691 #[test]
692 fn test_filter_warnings_multi_line_warning() {
693 let content = "line 1\nline 2\nline 3\n<!-- rumdl-disable-line MD013 -->\nline 5\nline 6\n";
695 let inline_config = InlineConfig::from_content(content);
696
697 let warnings = vec![
698 make_warning(3, 5, "MD013"), make_warning(6, 6, "MD013"), ];
701
702 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
703 assert_eq!(filtered.len(), 1);
705 assert_eq!(filtered[0].line, 6);
706 }
707
708 #[test]
709 fn test_filter_warnings_empty_input() {
710 let inline_config = InlineConfig::from_content("");
711 let filtered = filter_warnings_by_inline_config(vec![], &inline_config, "MD013");
712 assert!(filtered.is_empty());
713 }
714
715 #[test]
716 fn test_filter_warnings_none_disabled() {
717 let content = "line 1\nline 2\n";
718 let inline_config = InlineConfig::from_content(content);
719
720 let warnings = vec![make_warning(1, 1, "MD013"), make_warning(2, 2, "MD013")];
721
722 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
723 assert_eq!(filtered.len(), 2);
724 }
725
726 #[test]
727 fn test_filter_warnings_all_disabled() {
728 let content = "<!-- rumdl-disable MD013 -->\nline 1\nline 2\n";
729 let inline_config = InlineConfig::from_content(content);
730
731 let warnings = vec![make_warning(2, 2, "MD013"), make_warning(3, 3, "MD013")];
732
733 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
734 assert!(filtered.is_empty());
735 }
736
737 #[test]
738 fn test_filter_warnings_end_line_zero_fallback() {
739 let content = "<!-- rumdl-disable-line MD013 -->\nline 2\n";
741 let inline_config = InlineConfig::from_content(content);
742
743 let warnings = vec![make_warning(1, 0, "MD013")]; let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
746 assert!(filtered.is_empty());
747 }
748
749 #[test]
750 fn test_filter_non_md_rule_name_preserves_dash() {
751 let content = "line 1\nline 2\n";
754 let inline_config = InlineConfig::from_content(content);
755
756 let warnings = vec![make_warning(1, 1, "custom-rule")];
757
758 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "custom-rule");
760 assert_eq!(filtered.len(), 1, "Non-MD rule name with dash should not be split");
761 }
762
763 #[test]
764 fn test_filter_md_sub_rule_name_is_split() {
765 let content = "<!-- rumdl-disable MD029 -->\nline\n<!-- rumdl-enable MD029 -->\nline\n";
767 let inline_config = InlineConfig::from_content(content);
768
769 let warnings = vec![
770 make_warning(2, 2, "MD029"), make_warning(4, 4, "MD029"), ];
773
774 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD029-style");
776 assert_eq!(filtered.len(), 1);
777 assert_eq!(filtered[0].line, 4);
778 }
779
780 #[test]
781 fn test_filter_warnings_capture_restore() {
782 let content = "<!-- rumdl-disable MD013 -->\nline 1\n<!-- rumdl-capture -->\n<!-- rumdl-enable MD013 -->\nline 4\n<!-- rumdl-restore -->\nline 6\n";
783 let inline_config = InlineConfig::from_content(content);
784
785 let warnings = vec![
786 make_warning(2, 2, "MD013"), make_warning(5, 5, "MD013"), make_warning(7, 7, "MD013"), ];
790
791 let filtered = filter_warnings_by_inline_config(warnings, &inline_config, "MD013");
792 assert_eq!(filtered.len(), 1);
793 assert_eq!(filtered[0].line, 5);
794 }
795}