1use crate::utils::range_utils::{LineIndex, calculate_match_range};
7
8use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
9use crate::utils::document_structure::DocumentStructure;
10use std::collections::HashMap;
12use toml;
13
14#[derive(Clone)]
16pub struct MD005ListIndent;
17
18impl MD005ListIndent {
19 #[inline]
22 fn get_expected_indent(level: usize, parent_text_position: Option<usize>) -> usize {
23 if level == 1 {
24 0 } else if let Some(pos) = parent_text_position {
26 pos
28 } else {
29 2 * (level - 1)
31 }
32 }
33
34 fn get_parent_text_position(
37 &self,
38 ctx: &crate::lint_context::LintContext,
39 current_line: usize,
40 current_indent: usize,
41 ) -> Option<usize> {
42 for line_idx in (1..current_line).rev() {
44 if let Some(line_info) = ctx.line_info(line_idx) {
45 if let Some(list_item) = &line_info.list_item {
46 if list_item.marker_column < current_indent {
48 if list_item.is_ordered {
50 let text_start_pos = list_item.marker_column + list_item.marker.len() + 1; return Some(text_start_pos);
53 } else {
54 let text_start_pos = list_item.marker_column + 2; return Some(text_start_pos);
57 }
58 }
59 }
60 else if !line_info.is_blank && line_info.indent == 0 {
62 break;
63 }
64 }
65 }
66 None
67 }
68
69 fn group_related_list_blocks<'a>(
71 &self,
72 list_blocks: &'a [crate::lint_context::ListBlock],
73 ) -> Vec<Vec<&'a crate::lint_context::ListBlock>> {
74 if list_blocks.is_empty() {
75 return Vec::new();
76 }
77
78 let mut groups = Vec::new();
79 let mut current_group = vec![&list_blocks[0]];
80
81 for i in 1..list_blocks.len() {
82 let prev_block = &list_blocks[i - 1];
83 let current_block = &list_blocks[i];
84
85 let line_gap = current_block.start_line.saturating_sub(prev_block.end_line);
87
88 if line_gap <= 2 {
91 current_group.push(current_block);
92 } else {
93 groups.push(current_group);
95 current_group = vec![current_block];
96 }
97 }
98 groups.push(current_group);
99
100 groups
101 }
102
103 fn check_list_block_group(
105 &self,
106 ctx: &crate::lint_context::LintContext,
107 group: &[&crate::lint_context::ListBlock],
108 warnings: &mut Vec<LintWarning>,
109 ) -> Result<(), LintError> {
110 let line_index = LineIndex::new(ctx.content.to_string());
111
112 let mut all_list_items = Vec::new();
114
115 for list_block in group {
116 for &item_line in &list_block.item_lines {
117 if let Some(line_info) = ctx.line_info(item_line)
118 && let Some(list_item) = &line_info.list_item
119 {
120 let effective_indent = if let Some(blockquote) = &line_info.blockquote {
122 list_item.marker_column.saturating_sub(blockquote.nesting_level * 2)
124 } else {
125 list_item.marker_column
127 };
128
129 all_list_items.push((item_line, effective_indent, line_info, list_item));
130 }
131 }
132 }
133
134 if all_list_items.is_empty() {
135 return Ok(());
136 }
137
138 all_list_items.sort_by_key(|(line_num, _, _, _)| *line_num);
140
141 let mut indent_to_level: HashMap<usize, usize> = HashMap::new();
143
144 for (_line_num, indent, _line_info, _list_item) in &all_list_items {
146 let _level = if indent_to_level.is_empty() {
147 indent_to_level.insert(*indent, 1);
149 1
150 } else if let Some(&existing_level) = indent_to_level.get(indent) {
151 existing_level
153 } else {
154 let mut level = 1;
156 for (&existing_indent, &existing_level) in &indent_to_level {
157 if existing_indent < *indent {
158 level = level.max(existing_level + 1);
159 }
160 }
161 indent_to_level.insert(*indent, level);
162 level
163 };
164 }
165
166 let mut level_groups: HashMap<usize, Vec<(usize, usize, &crate::lint_context::LineInfo)>> = HashMap::new();
168 for (line_num, indent, line_info, _list_item) in &all_list_items {
169 let level = indent_to_level[indent];
170 level_groups
171 .entry(level)
172 .or_default()
173 .push((*line_num, *indent, *line_info));
174 }
175
176 for (level, mut group) in level_groups {
178 group.sort_by_key(|(line_num, _, _)| *line_num);
180
181 let parent_text_position = if level > 1 {
183 if let Some((line_num, indent, _)) = group.first() {
185 self.get_parent_text_position(ctx, *line_num, *indent)
186 } else {
187 None
188 }
189 } else {
190 None
191 };
192
193 let expected_indent = Self::get_expected_indent(level, parent_text_position);
194
195 let indents: std::collections::HashSet<usize> = group.iter().map(|(_, indent, _)| *indent).collect();
197
198 if indents.len() > 1 {
199 for (line_num, indent, line_info) in &group {
201 let inconsistent_message = format!(
202 "Expected indentation of {} {}, found {}",
203 expected_indent,
204 if expected_indent == 1 { "space" } else { "spaces" },
205 indent
206 );
207
208 let (start_line, start_col, end_line, end_col) = if *indent > 0 {
209 calculate_match_range(*line_num, &line_info.content, 0, *indent)
210 } else {
211 calculate_match_range(*line_num, &line_info.content, 0, 1)
212 };
213
214 let fix_range = if *indent > 0 {
215 let start_byte = line_index.line_col_to_byte_range(*line_num, 1).start;
216 let end_byte = line_index.line_col_to_byte_range(*line_num, *indent + 1).start;
217 start_byte..end_byte
218 } else {
219 let byte_pos = line_index.line_col_to_byte_range(*line_num, 1).start;
220 byte_pos..byte_pos
221 };
222
223 let replacement = if expected_indent > 0 {
224 " ".repeat(expected_indent)
225 } else {
226 String::new()
227 };
228
229 warnings.push(LintWarning {
230 rule_name: Some(self.name()),
231 line: start_line,
232 column: start_col,
233 end_line,
234 end_column: end_col,
235 message: inconsistent_message,
236 severity: Severity::Warning,
237 fix: Some(Fix {
238 range: fix_range,
239 replacement,
240 }),
241 });
242 }
243 } else {
244 let actual_indent = indents.iter().next().unwrap();
246 if *actual_indent != expected_indent {
247 for (line_num, indent, line_info) in &group {
248 let inconsistent_message = format!(
249 "Expected indentation of {} {}, found {}",
250 expected_indent,
251 if expected_indent == 1 { "space" } else { "spaces" },
252 indent
253 );
254
255 let (start_line, start_col, end_line, end_col) = if *indent > 0 {
256 calculate_match_range(*line_num, &line_info.content, 0, *indent)
257 } else {
258 calculate_match_range(*line_num, &line_info.content, 0, 1)
259 };
260
261 let fix_range = if *indent > 0 {
262 let start_byte = line_index.line_col_to_byte_range(*line_num, 1).start;
263 let end_byte = line_index.line_col_to_byte_range(*line_num, *indent + 1).start;
264 start_byte..end_byte
265 } else {
266 let byte_pos = line_index.line_col_to_byte_range(*line_num, 1).start;
267 byte_pos..byte_pos
268 };
269
270 let replacement = if expected_indent > 0 {
271 " ".repeat(expected_indent)
272 } else {
273 String::new()
274 };
275
276 warnings.push(LintWarning {
277 rule_name: Some(self.name()),
278 line: start_line,
279 column: start_col,
280 end_line,
281 end_column: end_col,
282 message: inconsistent_message,
283 severity: Severity::Warning,
284 fix: Some(Fix {
285 range: fix_range,
286 replacement,
287 }),
288 });
289 }
290 }
291 }
292 }
293
294 Ok(())
295 }
296
297 fn check_optimized(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
299 let content = ctx.content;
300
301 if content.is_empty() {
303 return Ok(Vec::new());
304 }
305
306 if ctx.list_blocks.is_empty() {
308 return Ok(Vec::new());
309 }
310
311 let mut warnings = Vec::new();
312
313 let block_groups = self.group_related_list_blocks(&ctx.list_blocks);
316
317 for group in block_groups {
318 self.check_list_block_group(ctx, &group, &mut warnings)?;
319 }
320
321 Ok(warnings)
322 }
323}
324
325impl Default for MD005ListIndent {
326 fn default() -> Self {
327 Self
328 }
329}
330
331impl Rule for MD005ListIndent {
332 fn name(&self) -> &'static str {
333 "MD005"
334 }
335
336 fn description(&self) -> &'static str {
337 "List indentation should be consistent"
338 }
339
340 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
341 self.check_optimized(ctx)
343 }
344
345 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
346 let warnings = self.check(ctx)?;
347 if warnings.is_empty() {
348 return Ok(ctx.content.to_string());
349 }
350
351 let mut warnings_with_fixes: Vec<_> = warnings
353 .into_iter()
354 .filter_map(|w| w.fix.clone().map(|fix| (w, fix)))
355 .collect();
356 warnings_with_fixes.sort_by_key(|(_, fix)| std::cmp::Reverse(fix.range.start));
357
358 let mut content = ctx.content.to_string();
360 for (_, fix) in warnings_with_fixes {
361 if fix.range.start <= content.len() && fix.range.end <= content.len() {
362 content.replace_range(fix.range, &fix.replacement);
363 }
364 }
365
366 Ok(content)
367 }
368
369 fn category(&self) -> RuleCategory {
370 RuleCategory::List
371 }
372
373 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
375 ctx.content.is_empty() || !ctx.lines.iter().any(|line| line.list_item.is_some())
377 }
378
379 fn check_with_structure(
381 &self,
382 ctx: &crate::lint_context::LintContext,
383 structure: &DocumentStructure,
384 ) -> LintResult {
385 if structure.list_lines.is_empty() {
387 return Ok(Vec::new());
388 }
389
390 self.check_optimized(ctx)
392 }
393
394 fn as_any(&self) -> &dyn std::any::Any {
395 self
396 }
397
398 fn as_maybe_document_structure(&self) -> Option<&dyn crate::rule::MaybeDocumentStructure> {
399 Some(self)
400 }
401
402 fn default_config_section(&self) -> Option<(String, toml::Value)> {
403 None
404 }
405
406 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
407 where
408 Self: Sized,
409 {
410 Box::new(MD005ListIndent)
411 }
412}
413
414impl crate::utils::document_structure::DocumentStructureExtensions for MD005ListIndent {
415 fn has_relevant_elements(
416 &self,
417 _ctx: &crate::lint_context::LintContext,
418 doc_structure: &crate::utils::document_structure::DocumentStructure,
419 ) -> bool {
420 !doc_structure.list_lines.is_empty()
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use crate::lint_context::LintContext;
428 use crate::utils::document_structure::DocumentStructureExtensions;
429
430 #[test]
431 fn test_valid_unordered_list() {
432 let rule = MD005ListIndent;
433 let content = "\
434* Item 1
435* Item 2
436 * Nested 1
437 * Nested 2
438* Item 3";
439 let ctx = LintContext::new(content);
440 let result = rule.check(&ctx).unwrap();
441 assert!(result.is_empty());
442 }
443
444 #[test]
445 fn test_valid_ordered_list() {
446 let rule = MD005ListIndent;
447 let content = "\
4481. Item 1
4492. Item 2
450 1. Nested 1
451 2. Nested 2
4523. Item 3";
453 let ctx = LintContext::new(content);
454 let result = rule.check(&ctx).unwrap();
455 assert!(result.is_empty());
458 }
459
460 #[test]
461 fn test_invalid_unordered_indent() {
462 let rule = MD005ListIndent;
463 let content = "\
464* Item 1
465 * Item 2
466 * Nested 1";
467 let ctx = LintContext::new(content);
468 let result = rule.check(&ctx).unwrap();
469 assert_eq!(result.len(), 1);
472 let fixed = rule.fix(&ctx).unwrap();
473 assert_eq!(fixed, "* Item 1\n * Item 2\n * Nested 1");
474 }
475
476 #[test]
477 fn test_invalid_ordered_indent() {
478 let rule = MD005ListIndent;
479 let content = "\
4801. Item 1
481 2. Item 2
482 1. Nested 1";
483 let ctx = LintContext::new(content);
484 let result = rule.check(&ctx).unwrap();
485 assert_eq!(result.len(), 1);
486 let fixed = rule.fix(&ctx).unwrap();
487 assert_eq!(fixed, "1. Item 1\n 2. Item 2\n 1. Nested 1");
491 }
492
493 #[test]
494 fn test_mixed_list_types() {
495 let rule = MD005ListIndent;
496 let content = "\
497* Item 1
498 1. Nested ordered
499 * Nested unordered
500* Item 2";
501 let ctx = LintContext::new(content);
502 let result = rule.check(&ctx).unwrap();
503 assert!(result.is_empty());
504 }
505
506 #[test]
507 fn test_multiple_levels() {
508 let rule = MD005ListIndent;
509 let content = "\
510* Level 1
511 * Level 2
512 * Level 3";
513 let ctx = LintContext::new(content);
514 let result = rule.check(&ctx).unwrap();
515 assert_eq!(result.len(), 2);
516 let fixed = rule.fix(&ctx).unwrap();
517 assert_eq!(
521 fixed,
522 "\
523* Level 1
524 * Level 2
525 * Level 3"
526 );
527 }
528
529 #[test]
530 fn test_empty_lines() {
531 let rule = MD005ListIndent;
532 let content = "\
533* Item 1
534
535 * Nested 1
536
537* Item 2";
538 let ctx = LintContext::new(content);
539 let result = rule.check(&ctx).unwrap();
540 assert!(result.is_empty());
541 }
542
543 #[test]
544 fn test_no_lists() {
545 let rule = MD005ListIndent;
546 let content = "\
547Just some text
548More text
549Even more text";
550 let ctx = LintContext::new(content);
551 let result = rule.check(&ctx).unwrap();
552 assert!(result.is_empty());
553 }
554
555 #[test]
556 fn test_complex_nesting() {
557 let rule = MD005ListIndent;
558 let content = "\
559* Level 1
560 * Level 2
561 * Level 3
562 * Back to 2
563 1. Ordered 3
564 2. Still 3
565* Back to 1";
566 let ctx = LintContext::new(content);
567 let result = rule.check(&ctx).unwrap();
568 assert!(result.is_empty());
569 }
570
571 #[test]
572 fn test_invalid_complex_nesting() {
573 let rule = MD005ListIndent;
574 let content = "\
575* Level 1
576 * Level 2
577 * Level 3
578 * Back to 2
579 1. Ordered 3
580 2. Still 3
581* Back to 1";
582 let ctx = LintContext::new(content);
583 let result = rule.check(&ctx).unwrap();
584 assert_eq!(result.len(), 3);
588 let fixed = rule.fix(&ctx).unwrap();
589 assert_eq!(
590 fixed,
591 "* Level 1\n * Level 2\n * Level 3\n * Back to 2\n 1. Ordered 3\n 2. Still 3\n* Back to 1"
592 );
593 }
594
595 #[test]
596 fn test_with_document_structure() {
597 let rule = MD005ListIndent;
598
599 let content = "* Item 1\n* Item 2\n * Nested item\n * Another nested item";
601 let structure = DocumentStructure::new(content);
602 let ctx = LintContext::new(content);
603 let result = rule.check_with_structure(&ctx, &structure).unwrap();
604 assert!(result.is_empty());
605
606 let content = "* Item 1\n* Item 2\n * Nested item\n * Another nested item";
608 let structure = DocumentStructure::new(content);
609 let ctx = LintContext::new(content);
610 let result = rule.check_with_structure(&ctx, &structure).unwrap();
611 assert!(!result.is_empty()); let content = "* Item 1\n * Nested item\n * Another nested item with wrong indent";
615 let structure = DocumentStructure::new(content);
616 let ctx = LintContext::new(content);
617 let result = rule.check_with_structure(&ctx, &structure).unwrap();
618 assert!(!result.is_empty()); }
620
621 #[test]
623 fn test_list_with_continuations() {
624 let rule = MD005ListIndent;
625 let content = "\
626* Item 1
627 This is a continuation
628 of the first item
629 * Nested item
630 with its own continuation
631* Item 2";
632 let ctx = LintContext::new(content);
633 let result = rule.check(&ctx).unwrap();
634 assert!(result.is_empty());
635 }
636
637 #[test]
638 fn test_list_in_blockquote() {
639 let rule = MD005ListIndent;
640 let content = "\
641> * Item 1
642> * Nested 1
643> * Nested 2
644> * Item 2";
645 let ctx = LintContext::new(content);
646 let result = rule.check(&ctx).unwrap();
647
648 assert!(
650 result.is_empty(),
651 "Expected no warnings for correctly indented blockquote list, got: {result:?}"
652 );
653 }
654
655 #[test]
656 fn test_list_with_code_blocks() {
657 let rule = MD005ListIndent;
658 let content = "\
659* Item 1
660 ```
661 code block
662 ```
663 * Nested item
664* Item 2";
665 let ctx = LintContext::new(content);
666 let result = rule.check(&ctx).unwrap();
667 assert!(result.is_empty());
668 }
669
670 #[test]
671 fn test_list_with_tabs() {
672 let rule = MD005ListIndent;
673 let content = "* Item 1\n\t* Tab indented\n * Space indented";
674 let ctx = LintContext::new(content);
675 let result = rule.check(&ctx).unwrap();
676 assert!(!result.is_empty());
678 }
679
680 #[test]
681 fn test_inconsistent_at_same_level() {
682 let rule = MD005ListIndent;
683 let content = "\
684* Item 1
685 * Nested 1
686 * Nested 2
687 * Wrong indent for same level
688 * Nested 3";
689 let ctx = LintContext::new(content);
690 let result = rule.check(&ctx).unwrap();
691 assert!(!result.is_empty());
692 assert!(result.iter().any(|w| w.line == 4));
694 }
695
696 #[test]
697 fn test_zero_indent_top_level() {
698 let rule = MD005ListIndent;
699 let content = "\
700 * Wrong indent
701* Correct
702 * Nested";
703 let ctx = LintContext::new(content);
704 let result = rule.check(&ctx).unwrap();
705
706 assert_eq!(result.len(), 0);
710 }
711
712 #[test]
713 fn test_fix_preserves_content() {
714 let rule = MD005ListIndent;
715 let content = "\
716* Item with **bold** and *italic*
717 * Wrong indent with `code`
718 * Also wrong with [link](url)";
719 let ctx = LintContext::new(content);
720 let fixed = rule.fix(&ctx).unwrap();
721 assert!(fixed.contains("**bold**"));
722 assert!(fixed.contains("*italic*"));
723 assert!(fixed.contains("`code`"));
724 assert!(fixed.contains("[link](url)"));
725 }
726
727 #[test]
728 fn test_deeply_nested_lists() {
729 let rule = MD005ListIndent;
730 let content = "\
731* L1
732 * L2
733 * L3
734 * L4
735 * L5
736 * L6";
737 let ctx = LintContext::new(content);
738 let result = rule.check(&ctx).unwrap();
739 assert!(result.is_empty());
740 }
741
742 #[test]
743 fn test_fix_multiple_issues() {
744 let rule = MD005ListIndent;
745 let content = "\
746* Item 1
747 * Wrong 1
748 * Wrong 2
749 * Wrong 3
750 * Correct
751 * Wrong 4";
752 let ctx = LintContext::new(content);
753 let fixed = rule.fix(&ctx).unwrap();
754 let lines: Vec<&str> = fixed.lines().collect();
756 assert_eq!(lines[0], "* Item 1");
757 assert_eq!(lines[1], " * Wrong 1");
758 assert_eq!(lines[2], " * Wrong 2"); assert_eq!(lines[3], " * Wrong 3"); assert_eq!(lines[4], " * Correct"); assert_eq!(lines[5], " * Wrong 4"); }
763
764 #[test]
765 fn test_performance_large_document() {
766 let rule = MD005ListIndent;
767 let mut content = String::new();
768 for i in 0..100 {
769 content.push_str(&format!("* Item {i}\n"));
770 content.push_str(&format!(" * Nested {i}\n"));
771 }
772 let ctx = LintContext::new(&content);
773 let result = rule.check(&ctx).unwrap();
774 assert!(result.is_empty());
775 }
776
777 #[test]
778 fn test_column_positions() {
779 let rule = MD005ListIndent;
780 let content = " * Wrong indent";
781 let ctx = LintContext::new(content);
782 let result = rule.check(&ctx).unwrap();
783 assert_eq!(result.len(), 1);
784 assert_eq!(result[0].column, 1);
785 assert_eq!(result[0].end_column, 2);
786 }
787
788 #[test]
789 fn test_should_skip() {
790 let rule = MD005ListIndent;
791
792 let ctx = LintContext::new("");
794 assert!(rule.should_skip(&ctx));
795
796 let ctx = LintContext::new("Just plain text");
798 assert!(rule.should_skip(&ctx));
799
800 let ctx = LintContext::new("* List item");
802 assert!(!rule.should_skip(&ctx));
803
804 let ctx = LintContext::new("1. Ordered list");
805 assert!(!rule.should_skip(&ctx));
806 }
807
808 #[test]
809 fn test_has_relevant_elements() {
810 let rule = MD005ListIndent;
811 let content = "* List item";
812 let ctx = LintContext::new(content);
813 let doc_structure = DocumentStructure::new(content);
814 assert!(rule.has_relevant_elements(&ctx, &doc_structure));
815
816 let content = "No lists here";
817 let ctx = LintContext::new(content);
818 let doc_structure = DocumentStructure::new(content);
819 assert!(!rule.has_relevant_elements(&ctx, &doc_structure));
820 }
821
822 #[test]
823 fn test_edge_case_single_space_indent() {
824 let rule = MD005ListIndent;
825 let content = "\
826* Item 1
827 * Single space - wrong
828 * Two spaces - correct";
829 let ctx = LintContext::new(content);
830 let result = rule.check(&ctx).unwrap();
831 assert_eq!(result.len(), 2);
834 assert!(result.iter().any(|w| w.line == 2 && w.message.contains("found 1")));
835 }
836
837 #[test]
838 fn test_edge_case_three_space_indent() {
839 let rule = MD005ListIndent;
840 let content = "\
841* Item 1
842 * Three spaces - wrong
843 * Two spaces - correct";
844 let ctx = LintContext::new(content);
845 let result = rule.check(&ctx).unwrap();
846 assert_eq!(result.len(), 2);
848 assert!(result.iter().any(|w| w.line == 2 && w.message.contains("found 3")));
849 }
850
851 #[test]
852 fn test_nested_bullets_under_numbered_items() {
853 let rule = MD005ListIndent;
854 let content = "\
8551. **Active Directory/LDAP**
856 - User authentication and directory services
857 - LDAP for user information and validation
858
8592. **Oracle Unified Directory (OUD)**
860 - Extended user directory services
861 - Verification of project account presence and changes";
862 let ctx = LintContext::new(content);
863 let result = rule.check(&ctx).unwrap();
864 assert!(
866 result.is_empty(),
867 "Expected no warnings for bullets with 3 spaces under numbered items, got: {result:?}"
868 );
869 }
870
871 #[test]
872 fn test_nested_bullets_under_numbered_items_wrong_indent() {
873 let rule = MD005ListIndent;
874 let content = "\
8751. **Active Directory/LDAP**
876 - Wrong: only 2 spaces
877 - Correct: 3 spaces";
878 let ctx = LintContext::new(content);
879 let result = rule.check(&ctx).unwrap();
880 assert_eq!(result.len(), 2); assert!(result.iter().any(|w| w.line == 2 && w.message.contains("found 2")));
883 }
884
885 #[test]
886 fn test_regular_nested_bullets_still_work() {
887 let rule = MD005ListIndent;
888 let content = "\
889* Top level
890 * Second level (2 spaces is correct for bullets under bullets)
891 * Third level (4 spaces)";
892 let ctx = LintContext::new(content);
893 let result = rule.check(&ctx).unwrap();
894 assert!(
896 result.is_empty(),
897 "Expected no warnings for regular bullet nesting, got: {result:?}"
898 );
899 }
900
901 #[test]
902 fn test_fix_range_accuracy() {
903 let rule = MD005ListIndent;
904 let content = " * Wrong indent";
905 let ctx = LintContext::new(content);
906 let result = rule.check(&ctx).unwrap();
907 assert_eq!(result.len(), 1);
908
909 let fix = result[0].fix.as_ref().unwrap();
910 assert_eq!(fix.replacement, "");
912 }
913}