Skip to content

Commit e897556

Browse files
authored
fix(md012): report blank lines before a code block that ends the document (#791)
MD012 walks the document through `filtered_lines()` with `skip_code_blocks()`, so a blank run sitting immediately before a code block is only flushed by the skip detector, which fires on the next filtered line after the skipped region. When the code block runs to the end of the file there is no next filtered line, and the EOF handler is guarded by `last_line_is_blank`, false here because the last line is the closing fence, so the accumulated run is discarded. printf '# Install\n\nRun this:\n\n\n```bash\nnpm install\n```\n' > s.md rumdl check -e MD012 s.md # Success: No issues found Appending "\nDone.\n" to the same file reports the run at line 5. The blank run is identical; only whether the fenced block is the last block in the file differs. An indented code block at EOF behaves the same way, and the divergence is visible under the default rule set, not only with `-e MD012`. Flush the pending run after the loop when the last line is not blank, using the same effective maximum the in-loop skip handling uses, so a code block at EOF is treated like any other skipped region. Trailing-blank handling is untouched: the two branches are mutually exclusive on `last_line_is_blank`.
1 parent 6962184 commit e897556

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

‎src/rules/md012_no_multiple_blanks.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,26 @@ impl Rule for MD012NoMultipleBlanks {
315315
// which aren't truly "trailing blanks". We need to verify the actual last line is blank.
316316
let last_line_is_blank = lines.last().is_some_and(|l| l.trim().is_empty());
317317

318+
// Blanks left over before a skipped region that runs to EOF are mid-document
319+
// blanks, so they use the same limits as the skip handling inside the loop.
320+
if blank_count > 0 && !last_line_is_blank {
321+
let effective_max = if prev_content_line_num.is_some_and(|idx| is_heading_context(ctx, idx)) {
322+
self.effective_max_below()
323+
} else {
324+
self.config.maximum.get()
325+
};
326+
if blank_count > effective_max {
327+
warnings.extend(self.generate_excess_warnings(
328+
blank_start,
329+
blank_count,
330+
effective_max,
331+
lines,
332+
&lines_to_check,
333+
line_index,
334+
));
335+
}
336+
}
337+
318338
// Check for trailing blank lines
319339
// EOF semantics: ANY blank line at EOF should be flagged (stricter than mid-document)
320340
// Only fire if the actual last line(s) of the file are blank

‎tests/rules/md012_test.rs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,45 @@ fn test_md012_region_calculation() {
200200
// This should pass - there's only 1 blank line before and after the code block
201201
assert!(result.is_empty(), "Expected no warnings, got: {result:?}");
202202
}
203+
204+
#[test]
205+
fn test_md012_blanks_before_fenced_code_block_at_eof() {
206+
let rule = MD012NoMultipleBlanks::default();
207+
let content = "# Install\n\nRun this:\n\n\n```bash\nnpm install\n```\n";
208+
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
209+
let result = rule.check(&ctx).unwrap();
210+
assert_eq!(result.len(), 1, "Expected one warning, got: {result:?}");
211+
assert_eq!(result[0].line, 5);
212+
assert_eq!(result[0].message, "Multiple consecutive blank lines between content");
213+
}
214+
215+
#[test]
216+
fn test_md012_blanks_before_indented_code_block_at_eof() {
217+
let rule = MD012NoMultipleBlanks::default();
218+
let content = "Intro\n\n\n code\n";
219+
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
220+
let result = rule.check(&ctx).unwrap();
221+
assert_eq!(result.len(), 1, "Expected one warning, got: {result:?}");
222+
assert_eq!(result[0].line, 3);
223+
assert_eq!(result[0].message, "Multiple consecutive blank lines between content");
224+
}
225+
226+
#[test]
227+
fn test_md012_blanks_before_fenced_code_block_followed_by_content() {
228+
let rule = MD012NoMultipleBlanks::default();
229+
let content = "# Install\n\nRun this:\n\n\n```bash\nnpm install\n```\n\nDone.\n";
230+
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
231+
let result = rule.check(&ctx).unwrap();
232+
assert_eq!(result.len(), 1, "Expected one warning, got: {result:?}");
233+
assert_eq!(result[0].line, 5);
234+
assert_eq!(result[0].message, "Multiple consecutive blank lines between content");
235+
}
236+
237+
#[test]
238+
fn test_md012_fix_blanks_before_code_block_at_eof() {
239+
let rule = MD012NoMultipleBlanks::default();
240+
let content = "# Install\n\nRun this:\n\n\n```bash\nnpm install\n```\n";
241+
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
242+
let fixed = rule.fix(&ctx).unwrap();
243+
assert_eq!(fixed, "# Install\n\nRun this:\n\n```bash\nnpm install\n```\n");
244+
}

0 commit comments

Comments
 (0)