1use serde::Deserialize;
2use std::rc::Rc;
3use tree_sitter::Node;
4
5use crate::linter::{CharPosition, Context, Range, RuleLinter, RuleViolation};
6
7use super::{Rule, RuleType};
8
9#[derive(Debug, PartialEq, Clone, Deserialize)]
11pub enum CodeBlockStyle {
12 #[serde(rename = "consistent")]
13 Consistent,
14 #[serde(rename = "fenced")]
15 Fenced,
16 #[serde(rename = "indented")]
17 Indented,
18}
19
20impl Default for CodeBlockStyle {
21 fn default() -> Self {
22 Self::Consistent
23 }
24}
25
26#[derive(Debug, PartialEq, Clone, Deserialize)]
27pub struct MD046CodeBlockStyleTable {
28 #[serde(default)]
29 pub style: CodeBlockStyle,
30}
31
32impl Default for MD046CodeBlockStyleTable {
33 fn default() -> Self {
34 Self {
35 style: CodeBlockStyle::Consistent,
36 }
37 }
38}
39
40const VIOLATION_MESSAGE: &str = "Code block style";
41
42pub(crate) struct MD046Linter {
43 context: Rc<Context>,
44 violations: Vec<RuleViolation>,
45 expected_style: Option<CodeBlockStyle>,
46}
47
48impl MD046Linter {
49 pub fn new(context: Rc<Context>) -> Self {
50 Self {
51 context,
52 violations: Vec::new(),
53 expected_style: None,
54 }
55 }
56
57 fn analyze_all_code_blocks(&mut self) {
58 let configured_style = self
59 .context
60 .config
61 .linters
62 .settings
63 .code_block_style
64 .style
65 .clone();
66
67 let all_code_blocks = {
68 let node_cache = self.context.node_cache.borrow();
69 let mut all_code_blocks = Vec::new();
70
71 if let Some(fenced_blocks) = node_cache.get("fenced_code_block") {
72 all_code_blocks.extend(
73 fenced_blocks
74 .iter()
75 .map(|n| (n.clone(), CodeBlockStyle::Fenced)),
76 );
77 }
78
79 if let Some(indented_blocks) = node_cache.get("indented_code_block") {
80 all_code_blocks.extend(
81 indented_blocks
82 .iter()
83 .map(|n| (n.clone(), CodeBlockStyle::Indented)),
84 );
85 }
86
87 all_code_blocks.sort_by_key(|(node_info, _)| node_info.line_start);
88 all_code_blocks
89 };
90
91 for (node_info, block_style) in all_code_blocks {
92 self.check_code_block(&node_info, block_style, &configured_style);
93 }
94 }
95
96 fn check_code_block(
97 &mut self,
98 node_info: &crate::linter::NodeInfo,
99 block_style: CodeBlockStyle,
100 configured_style: &CodeBlockStyle,
101 ) {
102 let expected_style = if *configured_style == CodeBlockStyle::Consistent {
103 if self.expected_style.is_none() {
104 self.expected_style = Some(block_style.clone());
105 }
106 self.expected_style.as_ref().unwrap()
107 } else {
108 configured_style
109 };
110
111 if block_style != *expected_style {
112 let range = Range {
113 start: CharPosition {
114 line: node_info.line_start,
115 character: 0,
116 },
117 end: CharPosition {
118 line: node_info.line_start,
119 character: 0, },
121 };
122
123 self.violations.push(RuleViolation::new(
124 &MD046,
125 VIOLATION_MESSAGE.to_string(),
126 self.context.file_path.clone(),
127 range,
128 ));
129 }
130 }
131}
132
133impl RuleLinter for MD046Linter {
134 fn feed(&mut self, _node: &Node) {
135 }
137
138 fn finalize(&mut self) -> Vec<RuleViolation> {
139 self.analyze_all_code_blocks();
140 std::mem::take(&mut self.violations)
141 }
142}
143
144pub const MD046: Rule = Rule {
145 id: "MD046",
146 alias: "code-block-style",
147 tags: &["code"],
148 description: "Code block style",
149 rule_type: RuleType::Document,
150 required_nodes: &["fenced_code_block", "indented_code_block"],
151 new_linter: |context| Box::new(MD046Linter::new(context)),
152};
153
154#[cfg(test)]
155mod test {
156 use std::path::PathBuf;
157
158 use crate::config::RuleSeverity;
159 use crate::linter::MultiRuleLinter;
160 use crate::test_utils::test_helpers::test_config_with_settings;
161
162 fn test_config() -> crate::config::QuickmarkConfig {
163 test_config_with_settings(
164 vec![
165 ("code-block-style", RuleSeverity::Error),
166 ("heading-style", RuleSeverity::Off),
167 ("heading-increment", RuleSeverity::Off),
168 ],
169 Default::default(),
170 )
171 }
172
173 #[test]
174 fn test_violation_consistent_style_mixed() {
175 let config = test_config();
176
177 let input = "Some text.
178
179 This is a
180 code block.
181
182And here is more text
183
184```text
185and here is a different
186code block
187```";
188 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
189 let violations = linter.analyze();
190 assert_eq!(1, violations.len());
191 assert!(violations[0].message().contains("Code block style"));
192 }
193
194 #[test]
195 fn test_no_violation_consistent_style_all_fenced() {
196 let config = test_config();
197
198 let input = "Some text.
199
200```text
201This is a fenced code block.
202```
203
204And here is more text
205
206```text
207and here is another fenced code block
208```";
209 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
210 let violations = linter.analyze();
211 assert_eq!(0, violations.len());
212 }
213
214 #[test]
215 fn test_no_violation_consistent_style_all_indented() {
216 let config = test_config();
217
218 let input = "Some text.
219
220 This is an indented
221 code block.
222
223And here is more text
224
225 And this is another
226 indented code block";
227 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
228 let violations = linter.analyze();
229 assert_eq!(0, violations.len());
230 }
231
232 #[test]
233 fn test_violation_fenced_style_with_indented() {
234 use crate::config::{CodeBlockStyle, MD046CodeBlockStyleTable};
235
236 let mut config = test_config();
237 config.linters.settings.code_block_style = MD046CodeBlockStyleTable {
238 style: CodeBlockStyle::Fenced,
239 };
240
241 let input = "Some text.
242
243 This is an indented
244 code block.
245
246And here is more text
247
248```text
249and here is a fenced code block
250```";
251 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
252 let violations = linter.analyze();
253 assert_eq!(1, violations.len());
254 assert!(violations[0].message().contains("Code block style"));
255 assert_eq!(violations[0].location().range.start.line, 2); }
257
258 #[test]
259 fn test_violation_indented_style_with_fenced() {
260 use crate::config::{CodeBlockStyle, MD046CodeBlockStyleTable};
261
262 let mut config = test_config();
263 config.linters.settings.code_block_style = MD046CodeBlockStyleTable {
264 style: CodeBlockStyle::Indented,
265 };
266
267 let input = "Some text.
268
269```text
270This is a fenced code block
271```
272
273And here is more text
274
275 This is an indented
276 code block";
277 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
278 let violations = linter.analyze();
279 assert_eq!(1, violations.len());
280 assert!(violations[0].message().contains("Code block style"));
281 assert_eq!(violations[0].location().range.start.line, 2); }
283
284 #[test]
285 fn test_no_violation_single_code_block() {
286 let config = test_config();
287
288 let input = "Some text.
289
290 This is an indented
291 code block.
292
293No other code blocks.";
294 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
295 let violations = linter.analyze();
296 assert_eq!(0, violations.len());
297 }
298
299 #[test]
300 fn test_no_violation_no_code_blocks() {
301 let config = test_config();
302
303 let input = "Some text.
304
305Just regular paragraphs.
306
307No code blocks at all.";
308 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
309 let violations = linter.analyze();
310 assert_eq!(0, violations.len());
311 }
312
313 #[test]
314 fn test_violation_multiple_inconsistent_blocks() {
315 let config = test_config();
316
317 let input = "Some text.
318
319 First indented block
320
321Text between
322
323```text
324First fenced block
325```
326
327More text
328
329 Second indented block
330
331```javascript
332Second fenced block
333```";
334 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
335 let violations = linter.analyze();
336 assert_eq!(2, violations.len());
338 assert_eq!(violations[0].location().range.start.line, 6); assert_eq!(violations[1].location().range.start.line, 14); }
342}