mdbook_lint_core/rules/standard/
md010.rs

1//! MD010: Hard tabs
2//!
3//! This rule checks for hard tab characters in the document.
4
5use crate::error::Result;
6use crate::rule::{Rule, RuleCategory, RuleMetadata};
7use crate::{
8    Document,
9    violation::{Severity, Violation},
10};
11
12/// Rule to check for hard tab characters
13pub struct MD010 {
14    /// Number of spaces that a tab character is equivalent to (for reporting)
15    spaces_per_tab: usize,
16}
17
18impl MD010 {
19    /// Create a new MD010 rule with default settings
20    pub fn new() -> Self {
21        Self { spaces_per_tab: 4 }
22    }
23
24    /// Create a new MD010 rule with custom tab size
25    #[allow(dead_code)]
26    pub fn with_spaces_per_tab(spaces_per_tab: usize) -> Self {
27        Self { spaces_per_tab }
28    }
29}
30
31impl Default for MD010 {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl Rule for MD010 {
38    fn id(&self) -> &'static str {
39        "MD010"
40    }
41
42    fn name(&self) -> &'static str {
43        "no-hard-tabs"
44    }
45
46    fn description(&self) -> &'static str {
47        "Hard tabs are not allowed"
48    }
49
50    fn metadata(&self) -> RuleMetadata {
51        RuleMetadata::stable(RuleCategory::Formatting).introduced_in("markdownlint v0.1.0")
52    }
53
54    fn check_with_ast<'a>(
55        &self,
56        document: &Document,
57        _ast: Option<&'a comrak::nodes::AstNode<'a>>,
58    ) -> Result<Vec<Violation>> {
59        let mut violations = Vec::new();
60
61        for (line_number, line) in document.lines.iter().enumerate() {
62            let line_num = line_number + 1; // Convert to 1-based line numbers
63
64            // Check for tab characters
65            if let Some(tab_pos) = line.find('\t') {
66                let column = tab_pos + 1; // Convert to 1-based column
67
68                violations.push(self.create_violation(
69                    format!(
70                        "Hard tab character found (consider using {} spaces)",
71                        self.spaces_per_tab
72                    ),
73                    line_num,
74                    column,
75                    Severity::Warning,
76                ));
77            }
78        }
79
80        Ok(violations)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::rule::Rule;
88    use std::path::PathBuf;
89
90    fn create_test_document(content: &str) -> Document {
91        Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
92    }
93
94    #[test]
95    fn test_md010_no_tabs() {
96        let content = "# Heading\n\nNo tabs here.\nJust spaces.";
97        let document = create_test_document(content);
98        let rule = MD010::new();
99        let violations = rule.check(&document).unwrap();
100
101        assert_eq!(violations.len(), 0);
102    }
103
104    #[test]
105    fn test_md010_single_tab() {
106        let content = "# Heading\n\nLine with\ttab.\nClean line.";
107        let document = create_test_document(content);
108        let rule = MD010::new();
109        let violations = rule.check(&document).unwrap();
110
111        assert_eq!(violations.len(), 1);
112        assert_eq!(violations[0].rule_id, "MD010");
113        assert_eq!(violations[0].line, 3);
114        assert_eq!(violations[0].column, 10);
115        assert!(violations[0].message.contains("Hard tab character"));
116        assert!(violations[0].message.contains("4 spaces"));
117    }
118
119    #[test]
120    fn test_md010_multiple_tabs() {
121        let content = "# Heading\n\nLine\twith\ttabs.\nAnother\ttab line.";
122        let document = create_test_document(content);
123        let rule = MD010::new();
124        let violations = rule.check(&document).unwrap();
125
126        assert_eq!(violations.len(), 2);
127        assert_eq!(violations[0].line, 3);
128        assert_eq!(violations[0].column, 5); // First tab position
129        assert_eq!(violations[1].line, 4);
130        assert_eq!(violations[1].column, 8); // First tab in second line
131    }
132
133    #[test]
134    fn test_md010_custom_spaces_per_tab() {
135        let content = "Line with\ttab.";
136        let document = create_test_document(content);
137        let rule = MD010::with_spaces_per_tab(2);
138        let violations = rule.check(&document).unwrap();
139
140        assert_eq!(violations.len(), 1);
141        assert!(violations[0].message.contains("2 spaces"));
142    }
143
144    #[test]
145    fn test_md010_tab_at_beginning() {
146        let content = "\tIndented with tab";
147        let document = create_test_document(content);
148        let rule = MD010::new();
149        let violations = rule.check(&document).unwrap();
150
151        assert_eq!(violations.len(), 1);
152        assert_eq!(violations[0].column, 1);
153    }
154
155    #[test]
156    fn test_md010_only_first_tab_reported() {
157        let content = "Line\twith\tmultiple\ttabs";
158        let document = create_test_document(content);
159        let rule = MD010::new();
160        let violations = rule.check(&document).unwrap();
161
162        // Should only report the first tab per line
163        assert_eq!(violations.len(), 1);
164        assert_eq!(violations[0].column, 5);
165    }
166}