mdlint/lint/rules/
md040.rs1use 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 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.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); }
118}