1use crate::filtered_lines::FilteredLinesExt;
2use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
3use crate::utils::range_utils::calculate_match_range;
7
8pub mod md010_config;
9pub use md010_config::MD010Config;
10
11#[derive(Clone, Default)]
13pub struct MD010NoHardTabs {
14 config: MD010Config,
15}
16
17impl MD010NoHardTabs {
18 pub fn new(spaces_per_tab: usize) -> Self {
19 Self {
20 config: MD010Config {
21 spaces_per_tab: crate::types::PositiveUsize::from_const(spaces_per_tab),
22 code_blocks: false,
23 },
24 }
25 }
26
27 pub const fn from_config_struct(config: MD010Config) -> Self {
28 Self { config }
29 }
30
31 fn count_leading_tabs(line: &str) -> usize {
32 let mut count = 0;
33 for c in line.chars() {
34 if c == '\t' {
35 count += 1;
36 } else {
37 break;
38 }
39 }
40 count
41 }
42
43 fn find_and_group_tabs(line: &str) -> Vec<(usize, usize)> {
44 let mut groups = Vec::new();
45 let mut current_group_start: Option<usize> = None;
46 let mut last_tab_pos = 0;
47
48 for (i, c) in line.chars().enumerate() {
49 if c == '\t' {
50 if let Some(start) = current_group_start {
51 if i == last_tab_pos + 1 {
53 last_tab_pos = i;
55 } else {
56 groups.push((start, last_tab_pos + 1));
58 current_group_start = Some(i);
59 last_tab_pos = i;
60 }
61 } else {
62 current_group_start = Some(i);
64 last_tab_pos = i;
65 }
66 }
67 }
68
69 if let Some(start) = current_group_start {
71 groups.push((start, last_tab_pos + 1));
72 }
73
74 groups
75 }
76}
77
78impl Rule for MD010NoHardTabs {
79 fn name(&self) -> &'static str {
80 "MD010"
81 }
82
83 fn description(&self) -> &'static str {
84 "No tabs"
85 }
86
87 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
88 let line_index = &ctx.line_index;
89
90 let mut warnings = Vec::new();
91
92 let mut filtered = ctx
93 .filtered_lines()
94 .skip_front_matter()
95 .skip_html_comments()
96 .skip_mdx_comments()
97 .skip_html_blocks()
98 .skip_pymdown_blocks()
99 .skip_mkdocstrings()
100 .skip_esm_blocks();
101
102 if !self.config.code_blocks {
103 filtered = filtered.skip_code_blocks();
104 }
105
106 for filtered_line in filtered {
107 let line_num = filtered_line.line_num - 1;
108 let line = filtered_line.content;
109
110 let tab_groups = Self::find_and_group_tabs(line);
112 if tab_groups.is_empty() {
113 continue;
114 }
115
116 let leading_tabs = Self::count_leading_tabs(line);
117
118 for (start_pos, end_pos) in tab_groups {
120 let tab_count = end_pos - start_pos;
121 let is_leading = start_pos < leading_tabs;
122
123 let (start_line, start_col, end_line, end_col) =
125 calculate_match_range(line_num + 1, line, start_pos, tab_count);
126
127 let message = if line.trim().is_empty() {
128 if tab_count == 1 {
129 "Empty line contains tab".to_string()
130 } else {
131 format!("Empty line contains {tab_count} tabs")
132 }
133 } else if is_leading {
134 if tab_count == 1 {
135 format!(
136 "Found leading tab, use {} spaces instead",
137 self.config.spaces_per_tab.get()
138 )
139 } else {
140 format!(
141 "Found {} leading tabs, use {} spaces instead",
142 tab_count,
143 tab_count * self.config.spaces_per_tab.get()
144 )
145 }
146 } else if tab_count == 1 {
147 "Found tab for alignment, use spaces instead".to_string()
148 } else {
149 format!("Found {tab_count} tabs for alignment, use spaces instead")
150 };
151
152 warnings.push(LintWarning {
153 rule_name: Some(self.name().to_string()),
154 line: start_line,
155 column: start_col,
156 end_line,
157 end_column: end_col,
158 message,
159 severity: Severity::Warning,
160 fix: Some(Fix::new(
161 line_index.line_col_to_byte_range_with_length(line_num + 1, start_pos + 1, tab_count),
162 " ".repeat(tab_count * self.config.spaces_per_tab.get()),
163 )),
164 });
165 }
166 }
167
168 Ok(warnings)
169 }
170
171 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
172 if self.should_skip(ctx) {
173 return Ok(ctx.content.to_string());
174 }
175 let warnings = self.check(ctx)?;
176 if warnings.is_empty() {
177 return Ok(ctx.content.to_string());
178 }
179 let warnings =
180 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
181 crate::utils::fix_utils::apply_warning_fixes(ctx.content, &warnings)
182 .map_err(crate::rule::LintError::InvalidInput)
183 }
184
185 fn as_any(&self) -> &dyn std::any::Any {
186 self
187 }
188
189 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
190 ctx.content.is_empty() || !ctx.has_char('\t')
192 }
193
194 fn category(&self) -> RuleCategory {
195 RuleCategory::Whitespace
196 }
197
198 crate::impl_rule_config_methods!(MD010Config);
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::lint_context::LintContext;
205 use crate::rule::Rule;
206
207 #[test]
208 fn test_no_tabs() {
209 let rule = MD010NoHardTabs::default();
210 let content = "This is a line\nAnother line\nNo tabs here";
211 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
212 let result = rule.check(&ctx).unwrap();
213 assert!(result.is_empty());
214 }
215
216 #[test]
217 fn test_single_tab() {
218 let rule = MD010NoHardTabs::default();
219 let content = "Line with\ttab";
220 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
221 let result = rule.check(&ctx).unwrap();
222 assert_eq!(result.len(), 1);
223 assert_eq!(result[0].line, 1);
224 assert_eq!(result[0].column, 10);
225 assert_eq!(result[0].message, "Found tab for alignment, use spaces instead");
226 }
227
228 #[test]
229 fn test_leading_tabs_skipped_in_indented_code_by_default() {
230 let content = "\tIndented line\n\t\tDouble indented";
233 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
234
235 let rule_off = MD010NoHardTabs::default();
236 let result_off = rule_off.check(&ctx).unwrap();
237 assert!(
238 result_off.is_empty(),
239 "indented code block skipped by default, got {result_off:?}"
240 );
241 assert_eq!(
242 rule_off.fix(&ctx).unwrap(),
243 "\tIndented line\n\t\tDouble indented",
244 "fix must preserve indented code block content"
245 );
246
247 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
249 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
250 code_blocks: true,
251 });
252 let result_on = rule_on.check(&ctx).unwrap();
253 assert_eq!(result_on.len(), 2, "got {result_on:?}");
254 assert_eq!(result_on[0].line, 1);
255 assert_eq!(result_on[0].message, "Found leading tab, use 4 spaces instead");
256 assert_eq!(result_on[1].line, 2);
257 assert_eq!(result_on[1].message, "Found 2 leading tabs, use 8 spaces instead");
258 assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented line\n Double indented");
259 }
260
261 #[test]
262 fn test_fix_tabs() {
263 let content = "\tIndented\nNormal\tline\nNo tabs";
266 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
267
268 let rule_off = MD010NoHardTabs::default();
269 let warnings_off = rule_off.check(&ctx).unwrap();
270 assert_eq!(warnings_off.len(), 1, "got {warnings_off:?}");
271 assert_eq!(warnings_off[0].line, 2);
272 assert_eq!(warnings_off[0].message, "Found tab for alignment, use spaces instead");
273 assert_eq!(
274 rule_off.fix(&ctx).unwrap(),
275 "\tIndented\nNormal line\nNo tabs",
276 "indented code block line preserved; alignment tab fixed"
277 );
278
279 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
281 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
282 code_blocks: true,
283 });
284 let warnings_on = rule_on.check(&ctx).unwrap();
285 assert_eq!(warnings_on.len(), 2, "got {warnings_on:?}");
286 assert_eq!(warnings_on[0].line, 1);
287 assert_eq!(warnings_on[1].line, 2);
288 assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented\nNormal line\nNo tabs");
289 }
290
291 #[test]
292 fn test_custom_spaces_per_tab() {
293 let content = "\tIndented";
295 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
296
297 let rule_off = MD010NoHardTabs::new(4);
298 assert!(
299 rule_off.check(&ctx).unwrap().is_empty(),
300 "indented code block skipped by default"
301 );
302 assert_eq!(
303 rule_off.fix(&ctx).unwrap(),
304 "\tIndented",
305 "indented code block preserved by default"
306 );
307
308 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
310 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
311 code_blocks: true,
312 });
313 assert_eq!(rule_on.check(&ctx).unwrap().len(), 1);
314 assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented");
315 }
316
317 #[test]
318 fn test_fenced_code_block_tabs_skipped_by_default() {
319 let rule = MD010NoHardTabs::default();
320 let content = "Normal\tline\n```\nCode\twith\ttab\n```\nAnother\tline";
321 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
322 let result = rule.check(&ctx).unwrap();
323 assert_eq!(result.len(), 2);
325 assert_eq!(result[0].line, 1);
326 assert_eq!(result[1].line, 5);
327
328 let fixed = rule.fix(&ctx).unwrap();
329 assert_eq!(fixed, "Normal line\n```\nCode\twith\ttab\n```\nAnother line");
330 }
331
332 #[test]
333 fn test_fenced_only_content_skipped_by_default() {
334 let rule = MD010NoHardTabs::default();
335 let content = "```\nCode\twith\ttab\n```";
336 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
337 let result = rule.check(&ctx).unwrap();
338 assert_eq!(result.len(), 0);
341 }
342
343 #[test]
344 fn test_html_comments_ignored() {
345 let rule = MD010NoHardTabs::default();
346 let content = "Normal\tline\n<!-- HTML\twith\ttab -->\nAnother\tline";
347 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
348 let result = rule.check(&ctx).unwrap();
349 assert_eq!(result.len(), 2);
351 assert_eq!(result[0].line, 1);
352 assert_eq!(result[1].line, 3);
353 }
354
355 #[test]
356 fn test_multiline_html_comments() {
357 let rule = MD010NoHardTabs::default();
358 let content = "Before\n<!--\nMultiline\twith\ttabs\ncomment\t-->\nAfter\ttab";
359 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
360 let result = rule.check(&ctx).unwrap();
361 assert_eq!(result.len(), 1);
363 assert_eq!(result[0].line, 5);
364 }
365
366 #[test]
367 fn test_empty_lines_with_tabs() {
368 let rule = MD010NoHardTabs::default();
369 let content = "Normal line\n\t\t\n\t\nAnother line";
370 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
371 let result = rule.check(&ctx).unwrap();
372 assert_eq!(result.len(), 2);
373 assert_eq!(result[0].message, "Empty line contains 2 tabs");
374 assert_eq!(result[1].message, "Empty line contains tab");
375 }
376
377 #[test]
378 fn test_mixed_tabs_and_spaces() {
379 let content = " \tMixed indentation\n\t Mixed again";
383 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
384
385 let rule_off = MD010NoHardTabs::default();
386 let result_off = rule_off.check(&ctx).unwrap();
387 assert!(
388 result_off.is_empty(),
389 "indented code block lines skipped, got {result_off:?}"
390 );
391 assert_eq!(
392 rule_off.fix(&ctx).unwrap(),
393 " \tMixed indentation\n\t Mixed again",
394 "content preserved unchanged"
395 );
396
397 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
399 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
400 code_blocks: true,
401 });
402 let result_on = rule_on.check(&ctx).unwrap();
403 assert_eq!(result_on.len(), 2, "got {result_on:?}");
404 assert_eq!(rule_on.fix(&ctx).unwrap(), " Mixed indentation\n Mixed again");
405 }
406
407 #[test]
408 fn test_consecutive_tabs() {
409 let rule = MD010NoHardTabs::default();
410 let content = "Text\t\t\tthree tabs\tand\tanother";
411 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
412 let result = rule.check(&ctx).unwrap();
413 assert_eq!(result.len(), 3);
415 assert_eq!(result[0].message, "Found 3 tabs for alignment, use spaces instead");
416 }
417
418 #[test]
419 fn test_find_and_group_tabs() {
420 let groups = MD010NoHardTabs::find_and_group_tabs("a\tb\tc");
422 assert_eq!(groups, vec![(1, 2), (3, 4)]);
423
424 let groups = MD010NoHardTabs::find_and_group_tabs("\t\tabc");
425 assert_eq!(groups, vec![(0, 2)]);
426
427 let groups = MD010NoHardTabs::find_and_group_tabs("no tabs");
428 assert!(groups.is_empty());
429
430 let groups = MD010NoHardTabs::find_and_group_tabs("\t\t\ta\t\tb");
432 assert_eq!(groups, vec![(0, 3), (4, 6)]);
433
434 let groups = MD010NoHardTabs::find_and_group_tabs("\ta\tb\tc");
435 assert_eq!(groups, vec![(0, 1), (2, 3), (4, 5)]);
436 }
437
438 #[test]
439 fn test_count_leading_tabs() {
440 assert_eq!(MD010NoHardTabs::count_leading_tabs("\t\tcode"), 2);
441 assert_eq!(MD010NoHardTabs::count_leading_tabs(" \tcode"), 0);
442 assert_eq!(MD010NoHardTabs::count_leading_tabs("no tabs"), 0);
443 assert_eq!(MD010NoHardTabs::count_leading_tabs("\t"), 1);
444 }
445
446 #[test]
447 fn test_default_config() {
448 let rule = MD010NoHardTabs::default();
449 let config = rule.default_config_section();
450 assert!(config.is_some());
451 let (name, _value) = config.unwrap();
452 assert_eq!(name, "MD010");
453 }
454
455 #[test]
456 fn test_from_config() {
457 let content_plain = "\tTab";
459 let ctx_plain = LintContext::new(content_plain, crate::config::MarkdownFlavor::Standard, None);
460 let rule_8_off = MD010NoHardTabs::new(8); assert!(
462 rule_8_off.check(&ctx_plain).unwrap().is_empty(),
463 "indented code block skipped"
464 );
465 assert_eq!(
466 rule_8_off.fix(&ctx_plain).unwrap(),
467 "\tTab",
468 "content preserved unchanged"
469 );
470
471 let rule_8_on = MD010NoHardTabs::from_config_struct(MD010Config {
473 spaces_per_tab: crate::types::PositiveUsize::from_const(8),
474 code_blocks: true,
475 });
476 assert_eq!(rule_8_on.check(&ctx_plain).unwrap().len(), 1);
477 assert_eq!(rule_8_on.fix(&ctx_plain).unwrap(), " Tab");
478
479 let content_fenced = "```\n\tTab in code\n```";
481 let ctx_fenced = LintContext::new(content_fenced, crate::config::MarkdownFlavor::Standard, None);
482 assert!(
483 rule_8_off.check(&ctx_fenced).unwrap().is_empty(),
484 "fenced code block skipped"
485 );
486 assert_eq!(rule_8_off.fix(&ctx_fenced).unwrap(), "```\n\tTab in code\n```");
487
488 let result_on = rule_8_on.check(&ctx_fenced).unwrap();
490 assert_eq!(result_on.len(), 1, "got {result_on:?}");
491 assert_eq!(result_on[0].line, 2);
492 assert_eq!(rule_8_on.fix(&ctx_fenced).unwrap(), "```\n Tab in code\n```");
493 }
494
495 #[test]
496 fn test_performance_large_document() {
497 let rule = MD010NoHardTabs::default();
498 let mut content = String::new();
499 for i in 0..1000 {
500 content.push_str(&format!("Line {i}\twith\ttabs\n"));
501 }
502 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
503 let result = rule.check(&ctx).unwrap();
504 assert_eq!(result.len(), 2000);
505 }
506
507 #[test]
508 fn test_preserve_content() {
509 let rule = MD010NoHardTabs::default();
510 let content = "**Bold**\ttext\n*Italic*\ttext\n[Link](url)\ttab";
511 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
512 let fixed = rule.fix(&ctx).unwrap();
513 assert_eq!(fixed, "**Bold** text\n*Italic* text\n[Link](url) tab");
514 }
515
516 #[test]
517 fn test_edge_cases() {
518 let rule = MD010NoHardTabs::default();
519
520 let content = "Text\t";
522 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
523 let result = rule.check(&ctx).unwrap();
524 assert_eq!(result.len(), 1);
525
526 let content = "\t\t\t";
528 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
529 let result = rule.check(&ctx).unwrap();
530 assert_eq!(result.len(), 1);
531 assert_eq!(result[0].message, "Empty line contains 3 tabs");
532 }
533
534 #[test]
535 fn test_fenced_code_block_tabs_preserved_in_fix_by_default() {
536 let rule = MD010NoHardTabs::default();
537
538 let content = "Text\twith\ttab\n```makefile\ntarget:\n\tcommand\n\tanother\n```\nMore\ttabs";
539 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
540 let fixed = rule.fix(&ctx).unwrap();
541
542 let expected = "Text with tab\n```makefile\ntarget:\n\tcommand\n\tanother\n```\nMore tabs";
545 assert_eq!(fixed, expected);
546 }
547
548 #[test]
549 fn test_tilde_fence_longer_than_3() {
550 let rule = MD010NoHardTabs::default();
551 let content = "~~~~~\ncode\twith\ttab\n~~~~~\ntext\twith\ttab";
553 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
554 let result = rule.check(&ctx).unwrap();
555 assert_eq!(
557 result.len(),
558 2,
559 "Expected 2 warnings but got {}: {:?}",
560 result.len(),
561 result
562 );
563 assert_eq!(result[0].line, 4);
564 assert_eq!(result[1].line, 4);
565 }
566
567 #[test]
568 fn test_backtick_fence_longer_than_3() {
569 let rule = MD010NoHardTabs::default();
570 let content = "`````\ncode\twith\ttab\n`````\ntext\twith\ttab";
572 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
573 let result = rule.check(&ctx).unwrap();
574 assert_eq!(
575 result.len(),
576 2,
577 "Expected 2 warnings but got {}: {:?}",
578 result.len(),
579 result
580 );
581 assert_eq!(result[0].line, 4);
582 assert_eq!(result[1].line, 4);
583 }
584
585 #[test]
586 fn test_indented_code_block_tabs_skipped_by_default() {
587 let content = " code\twith\ttab\n\nNormal\ttext";
590 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
591
592 let rule_off = MD010NoHardTabs::default();
593 let result_off = rule_off.check(&ctx).unwrap();
594 assert_eq!(
595 result_off.len(),
596 1,
597 "expected 1 warning (only normal-text tab), got {}: {:?}",
598 result_off.len(),
599 result_off
600 );
601 assert_eq!(result_off[0].line, 3);
602 assert_eq!(result_off[0].message, "Found tab for alignment, use spaces instead");
603
604 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
606 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
607 code_blocks: true,
608 });
609 let result_on = rule_on.check(&ctx).unwrap();
610 assert_eq!(
611 result_on.len(),
612 3,
613 "expected 3 warnings with code_blocks=true, got {}: {:?}",
614 result_on.len(),
615 result_on
616 );
617 assert_eq!(result_on[0].line, 1);
618 assert_eq!(result_on[1].line, 1);
619 assert_eq!(result_on[2].line, 3);
620 }
621
622 #[test]
623 fn test_html_comment_end_then_start_same_line() {
624 let rule = MD010NoHardTabs::default();
625 let content =
627 "<!-- first comment\nend --> text <!-- second comment\n\ttabbed content inside second comment\n-->";
628 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
629 let result = rule.check(&ctx).unwrap();
630 assert!(
631 result.is_empty(),
632 "Expected 0 warnings but got {}: {:?}",
633 result.len(),
634 result
635 );
636 }
637
638 #[test]
639 fn test_fix_tilde_fence_longer_than_3() {
640 let rule = MD010NoHardTabs::default();
641 let content = "~~~~~\ncode\twith\ttab\n~~~~~\ntext\twith\ttab";
642 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
643 let fixed = rule.fix(&ctx).unwrap();
644 assert_eq!(fixed, "~~~~~\ncode\twith\ttab\n~~~~~\ntext with tab");
646 }
647
648 #[test]
649 fn test_fix_indented_code_block_tabs_replaced() {
650 let content = " code\twith\ttab\n\nNormal\ttext";
652 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
653
654 let rule_off = MD010NoHardTabs::default();
655 assert_eq!(
656 rule_off.fix(&ctx).unwrap(),
657 " code\twith\ttab\n\nNormal text",
658 "indented code block preserved; only normal-text tab fixed"
659 );
660
661 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
663 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
664 code_blocks: true,
665 });
666 assert_eq!(
667 rule_on.fix(&ctx).unwrap(),
668 " code with tab\n\nNormal text",
669 "all tabs replaced with code_blocks=true"
670 );
671 }
672
673 #[test]
674 fn test_issue_630_default_skips_both_code_blocks() {
675 let rule = MD010NoHardTabs::default();
677 let content = "Foo bar\n\n for range 100 {\n \tfoo()\n }\n\nThis is a fenced\n\n```\nfor range 100 {\n\tfoo()\n}\n```\n";
678 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
679 let result = rule.check(&ctx).unwrap();
680 assert!(result.is_empty(), "both code blocks skipped, got {result:?}");
681 }
682
683 #[test]
684 fn test_issue_630_code_blocks_true_flags_both() {
685 let rule = MD010NoHardTabs::from_config_struct(MD010Config {
687 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
688 code_blocks: true,
689 });
690 let content = "Foo bar\n\n for range 100 {\n \tfoo()\n }\n\nThis is a fenced\n\n```\nfor range 100 {\n\tfoo()\n}\n```\n";
691 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
692 let result = rule.check(&ctx).unwrap();
693 assert_eq!(result.len(), 2, "got {result:?}");
696 assert_eq!(result[0].line, 4);
697 assert_eq!(result[1].line, 11);
698 }
699
700 #[test]
701 fn test_code_blocks_toggle_fenced() {
702 let content = "Normal\tline\n```\nCode\twith\ttab\n```\nAnother\tline";
703
704 let off = MD010NoHardTabs::default();
706 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
707 let r_off = off.check(&ctx).unwrap();
708 assert_eq!(r_off.len(), 2, "got {r_off:?}");
709 assert_eq!(r_off[0].line, 1);
710 assert_eq!(r_off[1].line, 5);
711 assert_eq!(
712 off.fix(&ctx).unwrap(),
713 "Normal line\n```\nCode\twith\ttab\n```\nAnother line"
714 );
715
716 let on = MD010NoHardTabs::from_config_struct(MD010Config {
718 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
719 code_blocks: true,
720 });
721 let r_on = on.check(&ctx).unwrap();
722 assert_eq!(r_on.len(), 4, "got {r_on:?}");
723 assert_eq!(r_on[0].line, 1);
724 assert_eq!(r_on[1].line, 3);
725 assert_eq!(r_on[2].line, 3);
726 assert_eq!(r_on[3].line, 5);
727 assert_eq!(
728 on.fix(&ctx).unwrap(),
729 "Normal line\n```\nCode with tab\n```\nAnother line"
730 );
731 }
732
733 #[test]
734 fn test_code_blocks_toggle_makefile_fence_preserved_by_default() {
735 let content = "Text\twith\ttab\n```makefile\ntarget:\n\tcommand\n```\nMore\ttabs";
736 let off = MD010NoHardTabs::default();
737 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
738 assert_eq!(
740 off.fix(&ctx).unwrap(),
741 "Text with tab\n```makefile\ntarget:\n\tcommand\n```\nMore tabs"
742 );
743 }
744
745 #[test]
746 fn test_tabs_in_front_matter_are_not_flagged() {
747 let rule = MD010NoHardTabs::default();
750 let content = "---\ntitle:\t\"Tabbed value\"\n---\n\n# Heading\n\nBody text.\n";
751 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
752 let result = rule.check(&ctx).unwrap();
753 assert!(
754 result.is_empty(),
755 "tabs inside front matter must not be flagged, got: {result:?}"
756 );
757 }
758}