1use crate::rule::{Fix, LintWarning};
7
8pub fn apply_warning_fixes(content: &str, warnings: &[LintWarning]) -> Result<String, String> {
11 let original_line_ending = crate::utils::detect_line_ending(content);
12 let mut fixes: Vec<(usize, &Fix)> = warnings
13 .iter()
14 .enumerate()
15 .filter_map(|(i, w)| w.fix.as_ref().map(|fix| (i, fix)))
16 .collect();
17
18 fixes.sort_by(|(_, fix_a), (_, fix_b)| {
21 let range_cmp = fix_a.range.start.cmp(&fix_b.range.start);
22 if range_cmp != std::cmp::Ordering::Equal {
23 return range_cmp;
24 }
25 fix_a.range.end.cmp(&fix_b.range.end)
26 });
27
28 let mut deduplicated = Vec::new();
29 let mut i = 0;
30 while i < fixes.len() {
31 let (idx, current_fix) = fixes[i];
32 deduplicated.push((idx, current_fix));
33
34 while i + 1 < fixes.len() {
36 let (_, next_fix) = fixes[i + 1];
37 if current_fix.range == next_fix.range && current_fix.replacement == next_fix.replacement {
38 i += 1; } else {
40 break;
41 }
42 }
43 i += 1;
44 }
45
46 let mut fixes = deduplicated;
47
48 fixes.sort_by(|(idx_a, fix_a), (idx_b, fix_b)| {
51 let range_cmp = fix_b.range.start.cmp(&fix_a.range.start);
53 if range_cmp != std::cmp::Ordering::Equal {
54 return range_cmp;
55 }
56
57 let end_cmp = fix_b.range.end.cmp(&fix_a.range.end);
59 if end_cmp != std::cmp::Ordering::Equal {
60 return end_cmp;
61 }
62
63 idx_a.cmp(idx_b)
65 });
66
67 let mut result = content.to_string();
68
69 for (_, fix) in fixes {
70 if fix.range.end > result.len() {
72 return Err(format!(
73 "Fix range end {} exceeds content length {}",
74 fix.range.end,
75 result.len()
76 ));
77 }
78
79 if fix.range.start > fix.range.end {
80 return Err(format!(
81 "Invalid fix range: start {} > end {}",
82 fix.range.start, fix.range.end
83 ));
84 }
85
86 let normalized_replacement = if original_line_ending == "\r\n" && !fix.replacement.contains("\r\n") {
89 fix.replacement.replace('\n', "\r\n")
90 } else {
91 fix.replacement.clone()
92 };
93
94 result.replace_range(fix.range.clone(), &normalized_replacement);
95 }
96
97 let normalized_result = if original_line_ending == "\r\n" {
100 result.replace('\n', "\r\n")
101 } else {
102 result.replace("\r\n", "\n")
103 };
104
105 Ok(normalized_result)
106}
107
108pub fn warning_fix_to_edit(content: &str, warning: &LintWarning) -> Result<(usize, usize, String), String> {
111 if let Some(fix) = &warning.fix {
112 if fix.range.end > content.len() {
114 return Err(format!(
115 "Fix range end {} exceeds content length {}",
116 fix.range.end,
117 content.len()
118 ));
119 }
120
121 Ok((fix.range.start, fix.range.end, fix.replacement.clone()))
122 } else {
123 Err("Warning has no fix".to_string())
124 }
125}
126
127pub fn validate_fix_range(content: &str, fix: &Fix) -> Result<(), String> {
129 if fix.range.start > content.len() {
130 return Err(format!(
131 "Fix range start {} exceeds content length {}",
132 fix.range.start,
133 content.len()
134 ));
135 }
136
137 if fix.range.end > content.len() {
138 return Err(format!(
139 "Fix range end {} exceeds content length {}",
140 fix.range.end,
141 content.len()
142 ));
143 }
144
145 if fix.range.start > fix.range.end {
146 return Err(format!(
147 "Invalid fix range: start {} > end {}",
148 fix.range.start, fix.range.end
149 ));
150 }
151
152 Ok(())
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::rule::{Fix, LintWarning, Severity};
159
160 #[test]
161 fn test_apply_single_fix() {
162 let content = "1. Multiple spaces";
163 let warning = LintWarning {
164 message: "Too many spaces".to_string(),
165 line: 1,
166 column: 3,
167 end_line: 1,
168 end_column: 5,
169 severity: Severity::Warning,
170 fix: Some(Fix {
171 range: 2..4, replacement: " ".to_string(), }),
174 rule_name: Some("MD030"),
175 };
176
177 let result = apply_warning_fixes(content, &[warning]).unwrap();
178 assert_eq!(result, "1. Multiple spaces");
179 }
180
181 #[test]
182 fn test_apply_multiple_fixes() {
183 let content = "1. First\n* Second";
184 let warnings = vec![
185 LintWarning {
186 message: "Too many spaces".to_string(),
187 line: 1,
188 column: 3,
189 end_line: 1,
190 end_column: 5,
191 severity: Severity::Warning,
192 fix: Some(Fix {
193 range: 2..4, replacement: " ".to_string(),
195 }),
196 rule_name: Some("MD030"),
197 },
198 LintWarning {
199 message: "Too many spaces".to_string(),
200 line: 2,
201 column: 2,
202 end_line: 2,
203 end_column: 5,
204 severity: Severity::Warning,
205 fix: Some(Fix {
206 range: 11..14, replacement: " ".to_string(),
208 }),
209 rule_name: Some("MD030"),
210 },
211 ];
212
213 let result = apply_warning_fixes(content, &warnings).unwrap();
214 assert_eq!(result, "1. First\n* Second");
215 }
216
217 #[test]
218 fn test_apply_non_overlapping_fixes() {
219 let content = "Test multiple spaces";
224 let warnings = vec![
225 LintWarning {
226 message: "Too many spaces".to_string(),
227 line: 1,
228 column: 5,
229 end_line: 1,
230 end_column: 7,
231 severity: Severity::Warning,
232 fix: Some(Fix {
233 range: 4..6, replacement: " ".to_string(),
235 }),
236 rule_name: Some("MD009"),
237 },
238 LintWarning {
239 message: "Too many spaces".to_string(),
240 line: 1,
241 column: 15,
242 end_line: 1,
243 end_column: 19,
244 severity: Severity::Warning,
245 fix: Some(Fix {
246 range: 14..18, replacement: " ".to_string(),
248 }),
249 rule_name: Some("MD009"),
250 },
251 ];
252
253 let result = apply_warning_fixes(content, &warnings).unwrap();
254 assert_eq!(result, "Test multiple spaces");
255 }
256
257 #[test]
258 fn test_apply_duplicate_fixes() {
259 let content = "Test content";
260 let warnings = vec![
261 LintWarning {
262 message: "Fix 1".to_string(),
263 line: 1,
264 column: 5,
265 end_line: 1,
266 end_column: 7,
267 severity: Severity::Warning,
268 fix: Some(Fix {
269 range: 4..6,
270 replacement: " ".to_string(),
271 }),
272 rule_name: Some("MD009"),
273 },
274 LintWarning {
275 message: "Fix 2 (duplicate)".to_string(),
276 line: 1,
277 column: 5,
278 end_line: 1,
279 end_column: 7,
280 severity: Severity::Warning,
281 fix: Some(Fix {
282 range: 4..6,
283 replacement: " ".to_string(),
284 }),
285 rule_name: Some("MD009"),
286 },
287 ];
288
289 let result = apply_warning_fixes(content, &warnings).unwrap();
291 assert_eq!(result, "Test content");
292 }
293
294 #[test]
295 fn test_apply_fixes_with_windows_line_endings() {
296 let content = "1. First\r\n* Second\r\n";
297 let warnings = vec![
298 LintWarning {
299 message: "Too many spaces".to_string(),
300 line: 1,
301 column: 3,
302 end_line: 1,
303 end_column: 5,
304 severity: Severity::Warning,
305 fix: Some(Fix {
306 range: 2..4,
307 replacement: " ".to_string(),
308 }),
309 rule_name: Some("MD030"),
310 },
311 LintWarning {
312 message: "Too many spaces".to_string(),
313 line: 2,
314 column: 2,
315 end_line: 2,
316 end_column: 5,
317 severity: Severity::Warning,
318 fix: Some(Fix {
319 range: 12..15, replacement: " ".to_string(),
321 }),
322 rule_name: Some("MD030"),
323 },
324 ];
325
326 let result = apply_warning_fixes(content, &warnings).unwrap();
327 assert!(result.contains("1. First"));
330 assert!(result.contains("* Second"));
331 }
332
333 #[test]
334 fn test_apply_fix_with_invalid_range() {
335 let content = "Short";
336 let warning = LintWarning {
337 message: "Invalid fix".to_string(),
338 line: 1,
339 column: 1,
340 end_line: 1,
341 end_column: 10,
342 severity: Severity::Warning,
343 fix: Some(Fix {
344 range: 0..100, replacement: "Replacement".to_string(),
346 }),
347 rule_name: Some("TEST"),
348 };
349
350 let result = apply_warning_fixes(content, &[warning]);
351 assert!(result.is_err());
352 assert!(result.unwrap_err().contains("exceeds content length"));
353 }
354
355 #[test]
356 fn test_apply_fix_with_reversed_range() {
357 let content = "Hello world";
358 let warning = LintWarning {
359 message: "Invalid fix".to_string(),
360 line: 1,
361 column: 5,
362 end_line: 1,
363 end_column: 3,
364 severity: Severity::Warning,
365 fix: Some(Fix {
366 #[allow(clippy::reversed_empty_ranges)]
367 range: 10..5, replacement: "Test".to_string(),
369 }),
370 rule_name: Some("TEST"),
371 };
372
373 let result = apply_warning_fixes(content, &[warning]);
374 assert!(result.is_err());
375 assert!(result.unwrap_err().contains("Invalid fix range"));
376 }
377
378 #[test]
379 fn test_apply_no_fixes() {
380 let content = "No changes needed";
381 let warnings = vec![LintWarning {
382 message: "Warning without fix".to_string(),
383 line: 1,
384 column: 1,
385 end_line: 1,
386 end_column: 5,
387 severity: Severity::Warning,
388 fix: None,
389 rule_name: Some("TEST"),
390 }];
391
392 let result = apply_warning_fixes(content, &warnings).unwrap();
393 assert_eq!(result, content);
394 }
395
396 #[test]
397 fn test_warning_fix_to_edit() {
398 let content = "Hello world";
399 let warning = LintWarning {
400 message: "Test".to_string(),
401 line: 1,
402 column: 1,
403 end_line: 1,
404 end_column: 5,
405 severity: Severity::Warning,
406 fix: Some(Fix {
407 range: 0..5,
408 replacement: "Hi".to_string(),
409 }),
410 rule_name: Some("TEST"),
411 };
412
413 let edit = warning_fix_to_edit(content, &warning).unwrap();
414 assert_eq!(edit, (0, 5, "Hi".to_string()));
415 }
416
417 #[test]
418 fn test_warning_fix_to_edit_no_fix() {
419 let content = "Hello world";
420 let warning = LintWarning {
421 message: "Test".to_string(),
422 line: 1,
423 column: 1,
424 end_line: 1,
425 end_column: 5,
426 severity: Severity::Warning,
427 fix: None,
428 rule_name: Some("TEST"),
429 };
430
431 let result = warning_fix_to_edit(content, &warning);
432 assert!(result.is_err());
433 assert_eq!(result.unwrap_err(), "Warning has no fix");
434 }
435
436 #[test]
437 fn test_warning_fix_to_edit_invalid_range() {
438 let content = "Short";
439 let warning = LintWarning {
440 message: "Test".to_string(),
441 line: 1,
442 column: 1,
443 end_line: 1,
444 end_column: 10,
445 severity: Severity::Warning,
446 fix: Some(Fix {
447 range: 0..100,
448 replacement: "Long replacement".to_string(),
449 }),
450 rule_name: Some("TEST"),
451 };
452
453 let result = warning_fix_to_edit(content, &warning);
454 assert!(result.is_err());
455 assert!(result.unwrap_err().contains("exceeds content length"));
456 }
457
458 #[test]
459 fn test_validate_fix_range() {
460 let content = "Hello world";
461
462 let valid_fix = Fix {
464 range: 0..5,
465 replacement: "Hi".to_string(),
466 };
467 assert!(validate_fix_range(content, &valid_fix).is_ok());
468
469 let invalid_fix = Fix {
471 range: 0..20,
472 replacement: "Hi".to_string(),
473 };
474 assert!(validate_fix_range(content, &invalid_fix).is_err());
475
476 let start = 5;
478 let end = 3;
479 let invalid_fix2 = Fix {
480 range: start..end,
481 replacement: "Hi".to_string(),
482 };
483 assert!(validate_fix_range(content, &invalid_fix2).is_err());
484 }
485
486 #[test]
487 fn test_validate_fix_range_edge_cases() {
488 let content = "Test";
489
490 let fix1 = Fix {
492 range: 0..0,
493 replacement: "Insert".to_string(),
494 };
495 assert!(validate_fix_range(content, &fix1).is_ok());
496
497 let fix2 = Fix {
499 range: 4..4,
500 replacement: " append".to_string(),
501 };
502 assert!(validate_fix_range(content, &fix2).is_ok());
503
504 let fix3 = Fix {
506 range: 0..4,
507 replacement: "Replace".to_string(),
508 };
509 assert!(validate_fix_range(content, &fix3).is_ok());
510
511 let fix4 = Fix {
513 range: 10..11,
514 replacement: "Invalid".to_string(),
515 };
516 let result = validate_fix_range(content, &fix4);
517 assert!(result.is_err());
518 assert!(result.unwrap_err().contains("start 10 exceeds"));
519 }
520
521 #[test]
522 fn test_fix_ordering_stability() {
523 let content = "Test content here";
525 let warnings = vec![
526 LintWarning {
527 message: "First warning".to_string(),
528 line: 1,
529 column: 6,
530 end_line: 1,
531 end_column: 13,
532 severity: Severity::Warning,
533 fix: Some(Fix {
534 range: 5..12, replacement: "stuff".to_string(),
536 }),
537 rule_name: Some("MD001"),
538 },
539 LintWarning {
540 message: "Second warning".to_string(),
541 line: 1,
542 column: 6,
543 end_line: 1,
544 end_column: 13,
545 severity: Severity::Warning,
546 fix: Some(Fix {
547 range: 5..12, replacement: "stuff".to_string(),
549 }),
550 rule_name: Some("MD002"),
551 },
552 ];
553
554 let result = apply_warning_fixes(content, &warnings).unwrap();
556 assert_eq!(result, "Test stuff here");
557 }
558
559 #[test]
560 fn test_line_ending_preservation() {
561 let content_unix = "Line 1\nLine 2\n";
563 let warning = LintWarning {
564 message: "Add text".to_string(),
565 line: 1,
566 column: 7,
567 end_line: 1,
568 end_column: 7,
569 severity: Severity::Warning,
570 fix: Some(Fix {
571 range: 6..6,
572 replacement: " added".to_string(),
573 }),
574 rule_name: Some("TEST"),
575 };
576
577 let result = apply_warning_fixes(content_unix, &[warning]).unwrap();
578 assert_eq!(result, "Line 1 added\nLine 2\n");
579
580 let content_windows = "Line 1\r\nLine 2\r\n";
582 let warning_windows = LintWarning {
583 message: "Add text".to_string(),
584 line: 1,
585 column: 7,
586 end_line: 1,
587 end_column: 7,
588 severity: Severity::Warning,
589 fix: Some(Fix {
590 range: 6..6,
591 replacement: " added".to_string(),
592 }),
593 rule_name: Some("TEST"),
594 };
595
596 let result_windows = apply_warning_fixes(content_windows, &[warning_windows]).unwrap();
597 assert!(result_windows.starts_with("Line 1 added"));
599 assert!(result_windows.contains("Line 2"));
600 }
601}