mdbook_lint_core/rules/
mdbook001.rs1use crate::rule::{AstRule, RuleCategory, RuleMetadata};
2use crate::{
3 Document,
4 violation::{Severity, Violation},
5};
6use comrak::nodes::{AstNode, NodeValue};
7
8pub 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 if code_block_data.fenced {
43 let info = code_block_data.info.trim();
44
45 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 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}