Skip to main content

mdlint/lint/rules/
md040.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use pulldown_cmark::{CodeBlockKind, Event, Tag};
5use serde_json::Value;
6
7pub struct MD040;
8
9impl Rule for MD040 {
10    fn name(&self) -> &str {
11        "MD040"
12    }
13
14    fn description(&self) -> &str {
15        "Fenced code blocks should have a language specified"
16    }
17
18    fn tags(&self) -> &[&str] {
19        &["code", "language"]
20    }
21
22    fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
23        let allowed_languages: Option<Vec<String>> = config
24            .and_then(|c| c.get("allowed_languages"))
25            .and_then(|v| v.as_array())
26            .map(|arr| {
27                arr.iter()
28                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
29                    .collect()
30            });
31
32        let mut violations = Vec::new();
33
34        for (event, range) in parser.parse_with_offsets() {
35            if let Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) = event {
36                let lang_str = lang.to_string();
37                let line = parser.offset_to_line(range.start);
38
39                if lang_str.is_empty() {
40                    // Always report code blocks without a language
41                    violations.push(Violation {
42                        line,
43                        column: Some(1),
44                        rule: self.name().to_string(),
45                        message: "Fenced code block should have a language specified".to_string(),
46                        fix: None,
47                    });
48                } else if let Some(ref allowed) = allowed_languages {
49                    // If allowed_languages is specified, check if lang is in the list
50                    if !allowed.contains(&lang_str.to_lowercase()) {
51                        violations.push(Violation {
52                            line,
53                            column: Some(1),
54                            rule: self.name().to_string(),
55                            message: format!("Language '{}' is not in the allowed list", lang_str),
56                            fix: None,
57                        });
58                    }
59                }
60            }
61        }
62
63        violations
64    }
65
66    fn fixable(&self) -> bool {
67        false
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_with_language() {
77        let content = "```rust\nlet x = 5;\n```";
78        let parser = MarkdownParser::new(content);
79        let rule = MD040;
80        let violations = rule.check(&parser, None);
81
82        assert_eq!(violations.len(), 0);
83    }
84
85    #[test]
86    fn test_without_language() {
87        let content = "```\ncode here\n```";
88        let parser = MarkdownParser::new(content);
89        let rule = MD040;
90        let config = serde_json::json!({ "allowed_languages": ["rust", "python"] });
91        let violations = rule.check(&parser, Some(&config));
92
93        assert_eq!(violations.len(), 1);
94        assert!(violations[0].message.contains("should have a language"));
95    }
96
97    #[test]
98    fn test_allowed_languages() {
99        let content = "```javascript\ncode here\n```";
100        let parser = MarkdownParser::new(content);
101        let rule = MD040;
102        let config = serde_json::json!({ "allowed_languages": ["rust", "python"] });
103        let violations = rule.check(&parser, Some(&config));
104
105        assert_eq!(violations.len(), 1);
106        assert!(violations[0].message.contains("not in the allowed list"));
107    }
108
109    #[test]
110    fn test_indented_code_block() {
111        let content = "    indented code\n    more code";
112        let parser = MarkdownParser::new(content);
113        let rule = MD040;
114        let violations = rule.check(&parser, None);
115
116        assert_eq!(violations.len(), 0); // Indented blocks are ignored
117    }
118}