Skip to content

Commit a5e7c40

Browse files
committed
fix(md072): keep every trailing newline when sorting frontmatter keys
The fix rebuilds the document through `lines()` + `join("\n")`, which drops the empty element each trailing newline produces. Restoring a single newline covered a file ending in exactly one, but a file ending in blank lines came back one short, so sorting the keys silently deleted a blank line at the end of the file. Restore the original count instead. The test brackets 0 to 3 trailing newlines across YAML, TOML, JSON and a whole-file frontmatter, and asserts the keys were actually sorted: a fix that declined to run also leaves the newlines alone.
1 parent 2ed4238 commit a5e7c40

1 file changed

Lines changed: 44 additions & 5 deletions

File tree

src/rules/md072_frontmatter_key_sort.rs

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -608,12 +608,18 @@ impl Rule for MD072FrontmatterKeySort {
608608
}
609609

610610
impl MD072FrontmatterKeySort {
611-
/// Restore the original document's trailing newline. The fix functions
612-
/// rebuild content via `lines()` + `join("\n")`, which never re-emits a
613-
/// final newline, so without this a file ending in `\n` would lose it on
614-
/// every fix (a dirty, non-idempotent diff).
611+
/// Restore the original document's trailing newlines. The fix functions
612+
/// rebuild content via `lines()` + `join("\n")`, which drops the empty
613+
/// element every trailing newline produces, so without this a file ending
614+
/// in `\n` would lose it on every fix (a dirty, non-idempotent diff).
615+
///
616+
/// The count matters, not just the presence: a document ending in blank
617+
/// lines rebuilds one newline short, so restoring a single one still ate a
618+
/// blank line the rule was never asked to touch.
615619
fn preserve_trailing_newline(original: &str, mut result: String) -> String {
616-
if original.ends_with('\n') && !result.ends_with('\n') {
620+
let wanted = original.len() - original.trim_end_matches('\n').len();
621+
let have = result.len() - result.trim_end_matches('\n').len();
622+
for _ in have..wanted {
617623
result.push('\n');
618624
}
619625
result
@@ -978,6 +984,39 @@ mod tests {
978984
);
979985
}
980986

987+
#[test]
988+
fn test_fix_preserves_a_run_of_trailing_newlines() {
989+
// Rebuilding through `lines()` drops the empty element every trailing
990+
// newline produces, so a document ending in blank lines came back one
991+
// newline short: sorting the keys silently deleted a blank line at the
992+
// end of the file.
993+
let rule = create_enabled_rule();
994+
let cases = [
995+
("yaml", "---\ntitle: Test\nauthor: John\n---\n\n# Heading"),
996+
("toml", "+++\ntitle = \"Test\"\nauthor = \"John\"\n+++\n\n# Heading"),
997+
("json", "{\n\"title\": \"Test\",\n\"author\": \"John\"\n}\n\n# Heading"),
998+
("yaml whole file", "---\ntitle: Test\nauthor: John\n---"),
999+
];
1000+
for (label, body) in cases {
1001+
for trailing in 0..4 {
1002+
let content = format!("{body}{}", "\n".repeat(trailing));
1003+
let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
1004+
let fixed = rule.fix(&ctx).unwrap();
1005+
assert_eq!(
1006+
fixed.len() - fixed.trim_end_matches('\n').len(),
1007+
trailing,
1008+
"{label} with {trailing} trailing newline(s) must keep them, got {fixed:?}"
1009+
);
1010+
// Positive control: an unchanged document proves nothing about a
1011+
// fix that ran, so require the keys to have actually been sorted.
1012+
assert!(
1013+
fixed.find("author").unwrap() < fixed.find("title").unwrap(),
1014+
"{label} with {trailing} trailing newline(s) was not sorted, got {fixed:?}"
1015+
);
1016+
}
1017+
}
1018+
}
1019+
9811020
#[test]
9821021
fn test_yaml_quoted_keys_sort_by_content() {
9831022
let rule = create_enabled_rule();

0 commit comments

Comments
 (0)