fix(formatter_yaml): keep a root plain scalar starting with a document marker verbatim (#27947)

Similar case with #27946, save more cases.
This commit is contained in:
leaysgur committed 2026-10-19 01:42:25 +00:00
1 parent 529b057811
commit 4d8195c183
5 files changed
+149 -12

No files matched your search

+1
View File
@@ -46,6 +46,7 @@ A line the printer starts at column 0 must not read back as stream-level syntax
Column 0 is reached at these sites, each guarded without changing the value:
- The document's first text (`print/document.rs`): content after `---` stays on its line; a document without `---` whose first text is U+FEFF gets one
- A plain scalar starting with a marker on a line of its own stays verbatim from its line start (indentation and continuation lines included)
- A root mapping key (`print/mapping_item.rs`): a key starting with a marker keeps its `?`
- A flow scalar's continuation line outside block collections (`print/scalar.rs`): a line starting with a marker is indented
- This also covers entries of a root flow collection, already indented, where the extra indent is harmless
+8 -1
View File
@@ -297,7 +297,7 @@ The KEY does count: a long key overflowing on `key: |` alone breaks the pair exa
## document-marker-content
- Why: semantics
- Pin: `tests/fixtures/yaml/document-marker-content/document.yaml`, `tests/fixtures/yaml/document-marker-content/key.yaml`, `tests/fixtures/yaml/document-marker-content/scalar.yaml`
- Pin: `tests/fixtures/yaml/document-marker-content/document.yaml`, `tests/fixtures/yaml/document-marker-content/indented.yaml`, `tests/fixtures/yaml/document-marker-content/key.yaml`, `tests/fixtures/yaml/document-marker-content/scalar.yaml`
```yaml
# input
@@ -306,6 +306,8 @@ The KEY does count: a long key overflowing on `key: |` alone breaks the pair exa
: m
# (proseWrap: always, printWidth: 10)
aaaa bbbb --- cccc
...
---
# ours: no line starts with the marker at column 0
--- ---
@@ -313,6 +315,8 @@ aaaa bbbb --- cccc
: m
aaaa bbbb
--- cccc
...
---
# prettier: the content becomes a marker
---
@@ -320,9 +324,12 @@ aaaa bbbb
--- x: m
aaaa bbbb
--- cccc
...
---
```
Text starting with `---` or `...` never starts a line at column 0, where it is read as a document marker (`"---"` becomes an empty document, the rest fails to parse).
A root plain scalar on a line of its own stays verbatim from its line start: a lenient parser (js-yaml) reads an indented marker as one, so re-indenting it would change the value there.
The guarded sites are listed in AGENTS.md "Line starts".
## bom-content
+24 -11
View File
@@ -2,6 +2,7 @@ use oxc_formatter_core::{
builders::{hard_line_break, space, text},
write,
};
use oxc_span::Span;
use oxc_yaml_parser::ast::{Content, Directive, Document, Node, Root};
use crate::{
@@ -13,7 +14,7 @@ use crate::{
print::{
YamlFormatter,
block::{ends_with_keep_chomped_block, item_gap_anchor, last_descendant_block_scalar},
source_starts_with_document_marker, to_span, write_node_or_suppressed,
column_of, source_starts_with_document_marker, to_span, write_node_or_suppressed,
},
};
@@ -138,15 +139,21 @@ fn write_document<'a>(document: &'a Document<'a>, f: &mut YamlFormatter<'_, 'a>)
if let Some(node) = &document.body.content {
let source = f.context().source_text();
if let Some(marker) = document.directives_end_marker {
// Content starting with `---`/`...` stays on the `---` line: on a line of its own it would become a marker
if source.all_bytes_match(marker.end, node.span.start, |b| matches!(b, b' ' | b'\t'))
&& source_starts_with_document_marker(node.span.start, f)
{
needs_line = false;
write!(f, space());
}
} else if source.as_str()[first_key_start(node) as usize..].starts_with('\u{FEFF}') {
// A plain scalar starting with `---`/`...` never starts a line at column 0: it would become a marker
// (a root mapping key keeps its `?` instead, `print/mapping_item.rs`)
let starts_with_marker = matches!(node.content, Content::Plain(_))
&& source_starts_with_document_marker(node.span.start, f);
let on_marker_line = starts_with_marker
&& document.directives_end_marker.is_some_and(|marker| {
source.all_bytes_match(marker.end, node.span.start, |b| matches!(b, b' ' | b'\t'))
});
if on_marker_line {
// On the `---` line, it stays there
needs_line = false;
write!(f, space());
} else if document.directives_end_marker.is_none()
&& source.as_str()[first_key_start(node) as usize..].starts_with('\u{FEFF}')
{
// Content starting with U+FEFF needs a `---`: outside a document it would be read as a BOM
write!(f, "---");
needs_line = true;
@@ -156,7 +163,13 @@ fn write_document<'a>(document: &'a Document<'a>, f: &mut YamlFormatter<'_, 'a>)
}
// Whole-body suppression needs the explicit head form (`# oxfmt-ignore` above `---`);
// an ignore right above the body is `write_node_or_suppressed`'s per-node rule.
let suppressed = if head_ignores_body {
let suppressed = if starts_with_marker && !on_marker_line {
// On a line of its own, it stays verbatim from its line start, indentation included:
// indented, a lenient parser (js-yaml) may still read it as a marker
let line_start = node.span.start - column_of(source.as_str(), node.span.start);
write_suppressed_node(Span::new(line_start, node.span.end), f);
false
} else if head_ignores_body {
write_suppressed_node(to_span(node.span), f);
true
} else {
@@ -0,0 +1,12 @@
---
---
... x
...
# c
--- a
b
...
--- y: m
...
# oxfmt-ignore
--- z
@@ -0,0 +1,104 @@
---
source: crates/oxc_formatter_yaml/tests/fixtures/mod.rs
---
==================== Input ====================
---
---
... x
...
# c
--- a
b
...
--- y: m
...
# oxfmt-ignore
--- z
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
---
---
... x
...
# c
--- a
b
...
? --- y
: m
...
# oxfmt-ignore
--- z
-------------------
{ printWidth: 100 }
-------------------
---
---
... x
...
# c
--- a
b
...
? --- y
: m
...
# oxfmt-ignore
--- z
---------------------------------------
{ printWidth: 10, proseWrap: "always" }
---------------------------------------
---
---
... x
...
# c
--- a
b
...
? --- y
: m
...
# oxfmt-ignore
--- z
---------------------------------------
{ printWidth: 80, proseWrap: "always" }
---------------------------------------
---
---
... x
...
# c
--- a
b
...
? --- y
: m
...
# oxfmt-ignore
--- z
----------------------------------------
{ printWidth: 100, proseWrap: "always" }
----------------------------------------
---
---
... x
...
# c
--- a
b
...
? --- y
: m
...
# oxfmt-ignore
--- z
===================== End =====================