1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rule_config_serde::RuleConfig;
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 let lines = ctx.raw_lines();
92
93 let skip_code_blocks = !self.config.code_blocks;
96
97 for (line_num, &line) in lines.iter().enumerate() {
98 if skip_code_blocks && ctx.line_info(line_num + 1).is_some_and(|info| info.in_code_block) {
99 continue;
100 }
101
102 if ctx.line_info(line_num + 1).is_some_and(|info| {
104 info.in_html_comment
105 || info.in_mdx_comment
106 || info.in_html_block
107 || info.in_pymdown_block
108 || info.in_mkdocstrings
109 || info.in_esm_block
110 }) {
111 continue;
112 }
113
114 let tab_groups = Self::find_and_group_tabs(line);
116 if tab_groups.is_empty() {
117 continue;
118 }
119
120 let leading_tabs = Self::count_leading_tabs(line);
121
122 for (start_pos, end_pos) in tab_groups {
124 let tab_count = end_pos - start_pos;
125 let is_leading = start_pos < leading_tabs;
126
127 let (start_line, start_col, end_line, end_col) =
129 calculate_match_range(line_num + 1, line, start_pos, tab_count);
130
131 let message = if line.trim().is_empty() {
132 if tab_count == 1 {
133 "Empty line contains tab".to_string()
134 } else {
135 format!("Empty line contains {tab_count} tabs")
136 }
137 } else if is_leading {
138 if tab_count == 1 {
139 format!(
140 "Found leading tab, use {} spaces instead",
141 self.config.spaces_per_tab.get()
142 )
143 } else {
144 format!(
145 "Found {} leading tabs, use {} spaces instead",
146 tab_count,
147 tab_count * self.config.spaces_per_tab.get()
148 )
149 }
150 } else if tab_count == 1 {
151 "Found tab for alignment, use spaces instead".to_string()
152 } else {
153 format!("Found {tab_count} tabs for alignment, use spaces instead")
154 };
155
156 warnings.push(LintWarning {
157 rule_name: Some(self.name().to_string()),
158 line: start_line,
159 column: start_col,
160 end_line,
161 end_column: end_col,
162 message,
163 severity: Severity::Warning,
164 fix: Some(Fix::new(
165 line_index.line_col_to_byte_range_with_length(line_num + 1, start_pos + 1, tab_count),
166 " ".repeat(tab_count * self.config.spaces_per_tab.get()),
167 )),
168 });
169 }
170 }
171
172 Ok(warnings)
173 }
174
175 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
176 if self.should_skip(ctx) {
177 return Ok(ctx.content.to_string());
178 }
179 let warnings = self.check(ctx)?;
180 if warnings.is_empty() {
181 return Ok(ctx.content.to_string());
182 }
183 let warnings =
184 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
185 crate::utils::fix_utils::apply_warning_fixes(ctx.content, &warnings)
186 .map_err(crate::rule::LintError::InvalidInput)
187 }
188
189 fn as_any(&self) -> &dyn std::any::Any {
190 self
191 }
192
193 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
194 ctx.content.is_empty() || !ctx.has_char('\t')
196 }
197
198 fn category(&self) -> RuleCategory {
199 RuleCategory::Whitespace
200 }
201
202 fn default_config_section(&self) -> Option<(String, toml::Value)> {
203 let default_config = MD010Config::default();
204 let json_value = serde_json::to_value(&default_config).ok()?;
205 let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
206
207 if let toml::Value::Table(table) = toml_value {
208 if !table.is_empty() {
209 Some((MD010Config::RULE_NAME.to_string(), toml::Value::Table(table)))
210 } else {
211 None
212 }
213 } else {
214 None
215 }
216 }
217
218 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
219 where
220 Self: Sized,
221 {
222 let rule_config = crate::rule_config_serde::load_rule_config::<MD010Config>(config);
223 Box::new(Self::from_config_struct(rule_config))
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::lint_context::LintContext;
231 use crate::rule::Rule;
232
233 #[test]
234 fn test_no_tabs() {
235 let rule = MD010NoHardTabs::default();
236 let content = "This is a line\nAnother line\nNo tabs here";
237 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
238 let result = rule.check(&ctx).unwrap();
239 assert!(result.is_empty());
240 }
241
242 #[test]
243 fn test_single_tab() {
244 let rule = MD010NoHardTabs::default();
245 let content = "Line with\ttab";
246 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
247 let result = rule.check(&ctx).unwrap();
248 assert_eq!(result.len(), 1);
249 assert_eq!(result[0].line, 1);
250 assert_eq!(result[0].column, 10);
251 assert_eq!(result[0].message, "Found tab for alignment, use spaces instead");
252 }
253
254 #[test]
255 fn test_leading_tabs_skipped_in_indented_code_by_default() {
256 let content = "\tIndented line\n\t\tDouble indented";
259 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
260
261 let rule_off = MD010NoHardTabs::default();
262 let result_off = rule_off.check(&ctx).unwrap();
263 assert!(
264 result_off.is_empty(),
265 "indented code block skipped by default, got {result_off:?}"
266 );
267 assert_eq!(
268 rule_off.fix(&ctx).unwrap(),
269 "\tIndented line\n\t\tDouble indented",
270 "fix must preserve indented code block content"
271 );
272
273 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
275 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
276 code_blocks: true,
277 });
278 let result_on = rule_on.check(&ctx).unwrap();
279 assert_eq!(result_on.len(), 2, "got {result_on:?}");
280 assert_eq!(result_on[0].line, 1);
281 assert_eq!(result_on[0].message, "Found leading tab, use 4 spaces instead");
282 assert_eq!(result_on[1].line, 2);
283 assert_eq!(result_on[1].message, "Found 2 leading tabs, use 8 spaces instead");
284 assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented line\n Double indented");
285 }
286
287 #[test]
288 fn test_fix_tabs() {
289 let content = "\tIndented\nNormal\tline\nNo tabs";
292 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
293
294 let rule_off = MD010NoHardTabs::default();
295 let warnings_off = rule_off.check(&ctx).unwrap();
296 assert_eq!(warnings_off.len(), 1, "got {warnings_off:?}");
297 assert_eq!(warnings_off[0].line, 2);
298 assert_eq!(warnings_off[0].message, "Found tab for alignment, use spaces instead");
299 assert_eq!(
300 rule_off.fix(&ctx).unwrap(),
301 "\tIndented\nNormal line\nNo tabs",
302 "indented code block line preserved; alignment tab fixed"
303 );
304
305 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
307 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
308 code_blocks: true,
309 });
310 let warnings_on = rule_on.check(&ctx).unwrap();
311 assert_eq!(warnings_on.len(), 2, "got {warnings_on:?}");
312 assert_eq!(warnings_on[0].line, 1);
313 assert_eq!(warnings_on[1].line, 2);
314 assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented\nNormal line\nNo tabs");
315 }
316
317 #[test]
318 fn test_custom_spaces_per_tab() {
319 let content = "\tIndented";
321 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
322
323 let rule_off = MD010NoHardTabs::new(4);
324 assert!(
325 rule_off.check(&ctx).unwrap().is_empty(),
326 "indented code block skipped by default"
327 );
328 assert_eq!(
329 rule_off.fix(&ctx).unwrap(),
330 "\tIndented",
331 "indented code block preserved by default"
332 );
333
334 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
336 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
337 code_blocks: true,
338 });
339 assert_eq!(rule_on.check(&ctx).unwrap().len(), 1);
340 assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented");
341 }
342
343 #[test]
344 fn test_fenced_code_block_tabs_skipped_by_default() {
345 let rule = MD010NoHardTabs::default();
346 let content = "Normal\tline\n```\nCode\twith\ttab\n```\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, 5);
353
354 let fixed = rule.fix(&ctx).unwrap();
355 assert_eq!(fixed, "Normal line\n```\nCode\twith\ttab\n```\nAnother line");
356 }
357
358 #[test]
359 fn test_fenced_only_content_skipped_by_default() {
360 let rule = MD010NoHardTabs::default();
361 let content = "```\nCode\twith\ttab\n```";
362 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
363 let result = rule.check(&ctx).unwrap();
364 assert_eq!(result.len(), 0);
367 }
368
369 #[test]
370 fn test_html_comments_ignored() {
371 let rule = MD010NoHardTabs::default();
372 let content = "Normal\tline\n<!-- HTML\twith\ttab -->\nAnother\tline";
373 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
374 let result = rule.check(&ctx).unwrap();
375 assert_eq!(result.len(), 2);
377 assert_eq!(result[0].line, 1);
378 assert_eq!(result[1].line, 3);
379 }
380
381 #[test]
382 fn test_multiline_html_comments() {
383 let rule = MD010NoHardTabs::default();
384 let content = "Before\n<!--\nMultiline\twith\ttabs\ncomment\t-->\nAfter\ttab";
385 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
386 let result = rule.check(&ctx).unwrap();
387 assert_eq!(result.len(), 1);
389 assert_eq!(result[0].line, 5);
390 }
391
392 #[test]
393 fn test_empty_lines_with_tabs() {
394 let rule = MD010NoHardTabs::default();
395 let content = "Normal line\n\t\t\n\t\nAnother line";
396 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
397 let result = rule.check(&ctx).unwrap();
398 assert_eq!(result.len(), 2);
399 assert_eq!(result[0].message, "Empty line contains 2 tabs");
400 assert_eq!(result[1].message, "Empty line contains tab");
401 }
402
403 #[test]
404 fn test_mixed_tabs_and_spaces() {
405 let content = " \tMixed indentation\n\t Mixed again";
409 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
410
411 let rule_off = MD010NoHardTabs::default();
412 let result_off = rule_off.check(&ctx).unwrap();
413 assert!(
414 result_off.is_empty(),
415 "indented code block lines skipped, got {result_off:?}"
416 );
417 assert_eq!(
418 rule_off.fix(&ctx).unwrap(),
419 " \tMixed indentation\n\t Mixed again",
420 "content preserved unchanged"
421 );
422
423 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
425 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
426 code_blocks: true,
427 });
428 let result_on = rule_on.check(&ctx).unwrap();
429 assert_eq!(result_on.len(), 2, "got {result_on:?}");
430 assert_eq!(rule_on.fix(&ctx).unwrap(), " Mixed indentation\n Mixed again");
431 }
432
433 #[test]
434 fn test_consecutive_tabs() {
435 let rule = MD010NoHardTabs::default();
436 let content = "Text\t\t\tthree tabs\tand\tanother";
437 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
438 let result = rule.check(&ctx).unwrap();
439 assert_eq!(result.len(), 3);
441 assert_eq!(result[0].message, "Found 3 tabs for alignment, use spaces instead");
442 }
443
444 #[test]
445 fn test_find_and_group_tabs() {
446 let groups = MD010NoHardTabs::find_and_group_tabs("a\tb\tc");
448 assert_eq!(groups, vec![(1, 2), (3, 4)]);
449
450 let groups = MD010NoHardTabs::find_and_group_tabs("\t\tabc");
451 assert_eq!(groups, vec![(0, 2)]);
452
453 let groups = MD010NoHardTabs::find_and_group_tabs("no tabs");
454 assert!(groups.is_empty());
455
456 let groups = MD010NoHardTabs::find_and_group_tabs("\t\t\ta\t\tb");
458 assert_eq!(groups, vec![(0, 3), (4, 6)]);
459
460 let groups = MD010NoHardTabs::find_and_group_tabs("\ta\tb\tc");
461 assert_eq!(groups, vec![(0, 1), (2, 3), (4, 5)]);
462 }
463
464 #[test]
465 fn test_count_leading_tabs() {
466 assert_eq!(MD010NoHardTabs::count_leading_tabs("\t\tcode"), 2);
467 assert_eq!(MD010NoHardTabs::count_leading_tabs(" \tcode"), 0);
468 assert_eq!(MD010NoHardTabs::count_leading_tabs("no tabs"), 0);
469 assert_eq!(MD010NoHardTabs::count_leading_tabs("\t"), 1);
470 }
471
472 #[test]
473 fn test_default_config() {
474 let rule = MD010NoHardTabs::default();
475 let config = rule.default_config_section();
476 assert!(config.is_some());
477 let (name, _value) = config.unwrap();
478 assert_eq!(name, "MD010");
479 }
480
481 #[test]
482 fn test_from_config() {
483 let content_plain = "\tTab";
485 let ctx_plain = LintContext::new(content_plain, crate::config::MarkdownFlavor::Standard, None);
486 let rule_8_off = MD010NoHardTabs::new(8); assert!(
488 rule_8_off.check(&ctx_plain).unwrap().is_empty(),
489 "indented code block skipped"
490 );
491 assert_eq!(
492 rule_8_off.fix(&ctx_plain).unwrap(),
493 "\tTab",
494 "content preserved unchanged"
495 );
496
497 let rule_8_on = MD010NoHardTabs::from_config_struct(MD010Config {
499 spaces_per_tab: crate::types::PositiveUsize::from_const(8),
500 code_blocks: true,
501 });
502 assert_eq!(rule_8_on.check(&ctx_plain).unwrap().len(), 1);
503 assert_eq!(rule_8_on.fix(&ctx_plain).unwrap(), " Tab");
504
505 let content_fenced = "```\n\tTab in code\n```";
507 let ctx_fenced = LintContext::new(content_fenced, crate::config::MarkdownFlavor::Standard, None);
508 assert!(
509 rule_8_off.check(&ctx_fenced).unwrap().is_empty(),
510 "fenced code block skipped"
511 );
512 assert_eq!(rule_8_off.fix(&ctx_fenced).unwrap(), "```\n\tTab in code\n```");
513
514 let result_on = rule_8_on.check(&ctx_fenced).unwrap();
516 assert_eq!(result_on.len(), 1, "got {result_on:?}");
517 assert_eq!(result_on[0].line, 2);
518 assert_eq!(rule_8_on.fix(&ctx_fenced).unwrap(), "```\n Tab in code\n```");
519 }
520
521 #[test]
522 fn test_performance_large_document() {
523 let rule = MD010NoHardTabs::default();
524 let mut content = String::new();
525 for i in 0..1000 {
526 content.push_str(&format!("Line {i}\twith\ttabs\n"));
527 }
528 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
529 let result = rule.check(&ctx).unwrap();
530 assert_eq!(result.len(), 2000);
531 }
532
533 #[test]
534 fn test_preserve_content() {
535 let rule = MD010NoHardTabs::default();
536 let content = "**Bold**\ttext\n*Italic*\ttext\n[Link](url)\ttab";
537 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
538 let fixed = rule.fix(&ctx).unwrap();
539 assert_eq!(fixed, "**Bold** text\n*Italic* text\n[Link](url) tab");
540 }
541
542 #[test]
543 fn test_edge_cases() {
544 let rule = MD010NoHardTabs::default();
545
546 let content = "Text\t";
548 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
549 let result = rule.check(&ctx).unwrap();
550 assert_eq!(result.len(), 1);
551
552 let content = "\t\t\t";
554 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
555 let result = rule.check(&ctx).unwrap();
556 assert_eq!(result.len(), 1);
557 assert_eq!(result[0].message, "Empty line contains 3 tabs");
558 }
559
560 #[test]
561 fn test_fenced_code_block_tabs_preserved_in_fix_by_default() {
562 let rule = MD010NoHardTabs::default();
563
564 let content = "Text\twith\ttab\n```makefile\ntarget:\n\tcommand\n\tanother\n```\nMore\ttabs";
565 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
566 let fixed = rule.fix(&ctx).unwrap();
567
568 let expected = "Text with tab\n```makefile\ntarget:\n\tcommand\n\tanother\n```\nMore tabs";
571 assert_eq!(fixed, expected);
572 }
573
574 #[test]
575 fn test_tilde_fence_longer_than_3() {
576 let rule = MD010NoHardTabs::default();
577 let content = "~~~~~\ncode\twith\ttab\n~~~~~\ntext\twith\ttab";
579 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
580 let result = rule.check(&ctx).unwrap();
581 assert_eq!(
583 result.len(),
584 2,
585 "Expected 2 warnings but got {}: {:?}",
586 result.len(),
587 result
588 );
589 assert_eq!(result[0].line, 4);
590 assert_eq!(result[1].line, 4);
591 }
592
593 #[test]
594 fn test_backtick_fence_longer_than_3() {
595 let rule = MD010NoHardTabs::default();
596 let content = "`````\ncode\twith\ttab\n`````\ntext\twith\ttab";
598 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
599 let result = rule.check(&ctx).unwrap();
600 assert_eq!(
601 result.len(),
602 2,
603 "Expected 2 warnings but got {}: {:?}",
604 result.len(),
605 result
606 );
607 assert_eq!(result[0].line, 4);
608 assert_eq!(result[1].line, 4);
609 }
610
611 #[test]
612 fn test_indented_code_block_tabs_skipped_by_default() {
613 let content = " code\twith\ttab\n\nNormal\ttext";
616 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
617
618 let rule_off = MD010NoHardTabs::default();
619 let result_off = rule_off.check(&ctx).unwrap();
620 assert_eq!(
621 result_off.len(),
622 1,
623 "expected 1 warning (only normal-text tab), got {}: {:?}",
624 result_off.len(),
625 result_off
626 );
627 assert_eq!(result_off[0].line, 3);
628 assert_eq!(result_off[0].message, "Found tab for alignment, use spaces instead");
629
630 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
632 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
633 code_blocks: true,
634 });
635 let result_on = rule_on.check(&ctx).unwrap();
636 assert_eq!(
637 result_on.len(),
638 3,
639 "expected 3 warnings with code_blocks=true, got {}: {:?}",
640 result_on.len(),
641 result_on
642 );
643 assert_eq!(result_on[0].line, 1);
644 assert_eq!(result_on[1].line, 1);
645 assert_eq!(result_on[2].line, 3);
646 }
647
648 #[test]
649 fn test_html_comment_end_then_start_same_line() {
650 let rule = MD010NoHardTabs::default();
651 let content =
653 "<!-- first comment\nend --> text <!-- second comment\n\ttabbed content inside second comment\n-->";
654 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
655 let result = rule.check(&ctx).unwrap();
656 assert!(
657 result.is_empty(),
658 "Expected 0 warnings but got {}: {:?}",
659 result.len(),
660 result
661 );
662 }
663
664 #[test]
665 fn test_fix_tilde_fence_longer_than_3() {
666 let rule = MD010NoHardTabs::default();
667 let content = "~~~~~\ncode\twith\ttab\n~~~~~\ntext\twith\ttab";
668 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
669 let fixed = rule.fix(&ctx).unwrap();
670 assert_eq!(fixed, "~~~~~\ncode\twith\ttab\n~~~~~\ntext with tab");
672 }
673
674 #[test]
675 fn test_fix_indented_code_block_tabs_replaced() {
676 let content = " code\twith\ttab\n\nNormal\ttext";
678 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
679
680 let rule_off = MD010NoHardTabs::default();
681 assert_eq!(
682 rule_off.fix(&ctx).unwrap(),
683 " code\twith\ttab\n\nNormal text",
684 "indented code block preserved; only normal-text tab fixed"
685 );
686
687 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
689 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
690 code_blocks: true,
691 });
692 assert_eq!(
693 rule_on.fix(&ctx).unwrap(),
694 " code with tab\n\nNormal text",
695 "all tabs replaced with code_blocks=true"
696 );
697 }
698
699 #[test]
700 fn test_issue_630_default_skips_both_code_blocks() {
701 let rule = MD010NoHardTabs::default();
703 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";
704 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
705 let result = rule.check(&ctx).unwrap();
706 assert!(result.is_empty(), "both code blocks skipped, got {result:?}");
707 }
708
709 #[test]
710 fn test_issue_630_code_blocks_true_flags_both() {
711 let rule = MD010NoHardTabs::from_config_struct(MD010Config {
713 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
714 code_blocks: true,
715 });
716 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";
717 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
718 let result = rule.check(&ctx).unwrap();
719 assert_eq!(result.len(), 2, "got {result:?}");
722 assert_eq!(result[0].line, 4);
723 assert_eq!(result[1].line, 11);
724 }
725
726 #[test]
727 fn test_code_blocks_toggle_fenced() {
728 let content = "Normal\tline\n```\nCode\twith\ttab\n```\nAnother\tline";
729
730 let off = MD010NoHardTabs::default();
732 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
733 let r_off = off.check(&ctx).unwrap();
734 assert_eq!(r_off.len(), 2, "got {r_off:?}");
735 assert_eq!(r_off[0].line, 1);
736 assert_eq!(r_off[1].line, 5);
737 assert_eq!(
738 off.fix(&ctx).unwrap(),
739 "Normal line\n```\nCode\twith\ttab\n```\nAnother line"
740 );
741
742 let on = MD010NoHardTabs::from_config_struct(MD010Config {
744 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
745 code_blocks: true,
746 });
747 let r_on = on.check(&ctx).unwrap();
748 assert_eq!(r_on.len(), 4, "got {r_on:?}");
749 assert_eq!(r_on[0].line, 1);
750 assert_eq!(r_on[1].line, 3);
751 assert_eq!(r_on[2].line, 3);
752 assert_eq!(r_on[3].line, 5);
753 assert_eq!(
754 on.fix(&ctx).unwrap(),
755 "Normal line\n```\nCode with tab\n```\nAnother line"
756 );
757 }
758
759 #[test]
760 fn test_code_blocks_toggle_makefile_fence_preserved_by_default() {
761 let content = "Text\twith\ttab\n```makefile\ntarget:\n\tcommand\n```\nMore\ttabs";
762 let off = MD010NoHardTabs::default();
763 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
764 assert_eq!(
766 off.fix(&ctx).unwrap(),
767 "Text with tab\n```makefile\ntarget:\n\tcommand\n```\nMore tabs"
768 );
769 }
770}