mdbook_lint_core/rules/
mdbook001.rs

1use crate::rule::{AstRule, RuleCategory, RuleMetadata};
2use crate::{
3    Document,
4    violation::{Severity, Violation},
5};
6use comrak::nodes::{AstNode, NodeValue};
7
8/// MDBOOK001: Code blocks should have language tags
9///
10/// This rule is triggered when code blocks don't have language tags for syntax highlighting.
11/// Proper language tags help with documentation clarity and proper rendering in mdBook.
12pub struct MDBOOK001;
13
14impl AstRule for MDBOOK001 {
15    fn id(&self) -> &'static str {
16        "MDBOOK001"
17    }
18
19    fn name(&self) -> &'static str {
20        "code-block-language"
21    }
22
23    fn description(&self) -> &'static str {
24        "Code blocks should have language tags for proper syntax highlighting"
25    }
26
27    fn metadata(&self) -> RuleMetadata {
28        RuleMetadata::stable(RuleCategory::MdBook).introduced_in("mdbook-lint v0.1.0")
29    }
30
31    fn check_ast<'a>(
32        &self,
33        document: &Document,
34        ast: &'a AstNode<'a>,
35    ) -> crate::error::Result<Vec<Violation>> {
36        let mut violations = Vec::new();
37        let code_blocks = document.code_blocks(ast);
38
39        for code_block in code_blocks {
40            if let NodeValue::CodeBlock(code_block_data) = &code_block.data.borrow().value {
41                // Only check fenced code blocks (skip indented code blocks)
42                if code_block_data.fenced {
43                    let info = code_block_data.info.trim();
44
45                    // Check if the info string is empty or just whitespace
46                    if info.is_empty() {
47                        let (line, column) = document.node_position(code_block).unwrap_or((1, 1));
48
49                        let message = "Code block is missing language tag for syntax highlighting"
50                            .to_string();
51
52                        violations.push(self.create_violation(
53                            message,
54                            line,
55                            column,
56                            Severity::Warning,
57                        ));
58                    }
59                }
60            }
61        }
62
63        Ok(violations)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::rule::Rule;
71    use std::path::PathBuf;
72
73    #[test]
74    fn test_mdbook001_valid_fenced_code_blocks() {
75        let content = r#"# Test
76
77```rust
78fn main() {
79    println!("Hello, world!");
80}
81```
82
83```bash
84echo "Hello from bash"
85```
86
87```json
88{"key": "value"}
89```
90"#;
91        let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
92        let rule = MDBOOK001;
93        let violations = rule.check(&document).unwrap();
94
95        assert_eq!(violations.len(), 0);
96    }
97
98    #[test]
99    fn test_mdbook001_missing_language_tags() {
100        let content = r#"# Test
101
102```
103fn main() {
104    println!("No language tag");
105}
106```
107
108Some text.
109
110```
111echo "Another block without language"
112```
113"#;
114        let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
115        let rule = MDBOOK001;
116        let violations = rule.check(&document).unwrap();
117
118        assert_eq!(violations.len(), 2);
119
120        assert_eq!(violations[0].rule_id, "MDBOOK001");
121        assert_eq!(violations[0].line, 3);
122        assert_eq!(violations[0].severity, Severity::Warning);
123        assert!(violations[0].message.contains("missing language tag"));
124
125        assert_eq!(violations[1].rule_id, "MDBOOK001");
126        assert_eq!(violations[1].line, 11);
127        assert_eq!(violations[1].severity, Severity::Warning);
128        assert!(violations[1].message.contains("missing language tag"));
129    }
130
131    #[test]
132    fn test_mdbook001_indented_code_blocks_ignored() {
133        let content = r#"# Test
134
135This is normal text.
136
137    // This is an indented code block
138    fn main() {
139        println!("This should be ignored");
140    }
141
142And some more text.
143"#;
144        let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
145        let rule = MDBOOK001;
146        let violations = rule.check(&document).unwrap();
147
148        // Indented code blocks should be ignored
149        assert_eq!(violations.len(), 0);
150    }
151
152    #[test]
153    fn test_mdbook001_mixed_code_blocks() {
154        let content = r#"# Test
155
156```rust
157// Good: has language tag
158fn main() {}
159```
160
161```
162// Bad: missing language tag
163fn bad() {}
164```
165
166    // Indented: should be ignored
167    fn indented() {}
168
169```bash
170# Good: has language tag
171echo "hello"
172```
173"#;
174        let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
175        let rule = MDBOOK001;
176        let violations = rule.check(&document).unwrap();
177
178        assert_eq!(violations.len(), 1);
179        assert_eq!(violations[0].line, 8);
180        assert!(violations[0].message.contains("missing language tag"));
181    }
182
183    #[test]
184    fn test_mdbook001_whitespace_only_info() {
185        let content = r#"```
186// Code block with whitespace-only info string
187fn test() {}
188```"#;
189        let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
190        let rule = MDBOOK001;
191        let violations = rule.check(&document).unwrap();
192
193        assert_eq!(violations.len(), 1);
194        assert!(violations[0].message.contains("missing language tag"));
195    }
196
197    #[test]
198    fn test_mdbook001_no_code_blocks() {
199        let content = r#"# Test Document
200
201This is just regular text with no code blocks.
202
203## Another Section
204
205Still no code blocks here.
206"#;
207        let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
208        let rule = MDBOOK001;
209        let violations = rule.check(&document).unwrap();
210
211        assert_eq!(violations.len(), 0);
212    }
213}