Skip to main content

mdlint/lint/rules/
md055.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use serde_json::Value;
5
6pub struct MD055;
7
8impl Rule for MD055 {
9    fn name(&self) -> &str {
10        "MD055"
11    }
12
13    fn description(&self) -> &str {
14        "Table pipe style"
15    }
16
17    fn tags(&self) -> &[&str] {
18        &["table"]
19    }
20
21    fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
22        let style = config
23            .and_then(|c| c.get("style"))
24            .and_then(|v| v.as_str())
25            .unwrap_or("leading_and_trailing");
26
27        let mut violations = Vec::new();
28        let mut first_style: Option<&str> = None;
29        let code_block_lines = parser.get_code_block_line_numbers();
30
31        for (line_num, line) in parser.lines().iter().enumerate() {
32            let line_number = line_num + 1;
33
34            if code_block_lines.contains(&line_number) {
35                continue;
36            }
37
38            // Check if line is a table row (contains pipes)
39            if !line.contains('|') {
40                continue;
41            }
42
43            let trimmed = line.trim();
44
45            // Determine the style of this line
46            let has_leading = trimmed.starts_with('|');
47            let has_trailing = trimmed.ends_with('|');
48
49            let current_style = match (has_leading, has_trailing) {
50                (true, true) => "leading_and_trailing",
51                (true, false) => "leading_only",
52                (false, true) => "trailing_only",
53                (false, false) => "no_leading_or_trailing",
54            };
55
56            if style == "consistent" {
57                if let Some(first) = first_style {
58                    if current_style != first {
59                        // Report separate violations for leading and trailing mismatches
60                        let (first_leading, first_trailing) = match first {
61                            "leading_and_trailing" => (true, true),
62                            "leading_only" => (true, false),
63                            "trailing_only" => (false, true),
64                            "no_leading_or_trailing" => (false, false),
65                            _ => (false, false),
66                        };
67
68                        // Check leading pipe
69                        if has_leading != first_leading {
70                            violations.push(Violation {
71                                line: line_number,
72                                column: Some(1),
73                                rule: self.name().to_string(),
74                                message: format!(
75                                    "Table pipe style should be consistent: expected {}, found {}",
76                                    if first_leading {
77                                        "leading pipe"
78                                    } else {
79                                        "no leading pipe"
80                                    },
81                                    if has_leading {
82                                        "leading pipe"
83                                    } else {
84                                        "no leading pipe"
85                                    }
86                                ),
87                                fix: None,
88                            });
89                        }
90
91                        // Check trailing pipe
92                        if has_trailing != first_trailing {
93                            violations.push(Violation {
94                                line: line_number,
95                                column: Some(1),
96                                rule: self.name().to_string(),
97                                message: format!(
98                                    "Table pipe style should be consistent: expected {}, found {}",
99                                    if first_trailing {
100                                        "trailing pipe"
101                                    } else {
102                                        "no trailing pipe"
103                                    },
104                                    if has_trailing {
105                                        "trailing pipe"
106                                    } else {
107                                        "no trailing pipe"
108                                    }
109                                ),
110                                fix: None,
111                            });
112                        }
113                    }
114                } else {
115                    first_style = Some(current_style);
116                }
117            } else if style == "leading_and_trailing" && current_style != "leading_and_trailing" {
118                // Report separate violations for missing leading/trailing
119                if !has_leading {
120                    violations.push(Violation {
121                        line: line_number,
122                        column: Some(1),
123                        rule: self.name().to_string(),
124                        message: "Table should have leading pipe".to_string(),
125                        fix: None,
126                    });
127                }
128                if !has_trailing {
129                    violations.push(Violation {
130                        line: line_number,
131                        column: Some(1),
132                        rule: self.name().to_string(),
133                        message: "Table should have trailing pipe".to_string(),
134                        fix: None,
135                    });
136                }
137            } else if style == "no_leading_or_trailing" && (has_leading || has_trailing) {
138                // Report separate violations for unwanted leading/trailing
139                if has_leading {
140                    violations.push(Violation {
141                        line: line_number,
142                        column: Some(1),
143                        rule: self.name().to_string(),
144                        message: "Table should not have leading pipe".to_string(),
145                        fix: None,
146                    });
147                }
148                if has_trailing {
149                    violations.push(Violation {
150                        line: line_number,
151                        column: Some(1),
152                        rule: self.name().to_string(),
153                        message: "Table should not have trailing pipe".to_string(),
154                        fix: None,
155                    });
156                }
157            }
158        }
159
160        violations
161    }
162
163    fn fixable(&self) -> bool {
164        false
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_consistent_with_pipes() {
174        let content = "| Col1 | Col2 |\n|------|------|\n| A    | B    |";
175        let parser = MarkdownParser::new(content);
176        let rule = MD055;
177        let violations = rule.check(&parser, None);
178
179        assert_eq!(violations.len(), 0);
180    }
181
182    #[test]
183    fn test_consistent_without_pipes() {
184        let content = "Col1 | Col2\n-----|-----\nA    | B";
185        let parser = MarkdownParser::new(content);
186        let rule = MD055;
187        let config = serde_json::json!({ "style": "consistent" });
188        let violations = rule.check(&parser, Some(&config));
189
190        assert_eq!(violations.len(), 0);
191    }
192
193    #[test]
194    fn test_inconsistent_pipes() {
195        let content = "| Col1 | Col2 |\n|------|------|\nA    | B";
196        let parser = MarkdownParser::new(content);
197        let rule = MD055;
198        let violations = rule.check(&parser, None);
199
200        // Last row is inconsistent: reports 2 violations (missing leading and trailing)
201        assert_eq!(violations.len(), 2);
202    }
203
204    #[test]
205    fn test_enforced_leading_and_trailing() {
206        let content = "Col1 | Col2\n-----|-----\nA | B";
207        let parser = MarkdownParser::new(content);
208        let rule = MD055;
209        let config = serde_json::json!({ "style": "leading_and_trailing" });
210        let violations = rule.check(&parser, Some(&config));
211
212        // 3 rows (header, separator, data) × 2 violations each (missing leading and trailing)
213        assert_eq!(violations.len(), 6);
214    }
215
216    #[test]
217    fn test_simple_table() {
218        let content = "| Header |\n| ------ |\n| Cell   |";
219        let parser = MarkdownParser::new(content);
220        let rule = MD055;
221        let violations = rule.check(&parser, None);
222
223        assert_eq!(violations.len(), 0);
224    }
225}