mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter_yaml): never start col-0 line with document marker or BOM (#27818)
- Bump `oxc-yaml-parser` to 0.0.7 - handles NUL as content, BOM only outside a document, markers only at column 0 - Keep `---`/`...` content on the `---` line, keep `?` on a root key starting with a marker, and indent a flow scalar continuation line starting with a marker - Add `---` before a document whose first text is U+FEFF
This commit is contained in:
1 parent
9261df700b
commit
52ee29d1be
17 files changed
+355
-12
No files matched your search
Generated
+2
-2
@@ -1734,9 +1734,9 @@ checksum = "f321c22ccee109a6fe0aef08d1521af693e4b178dcb7259137ff08f8ce5388d8"
|
||||
|
||||
[[package]]
|
||||
name = "oxc-yaml-parser"
|
||||
version = "0.0.6"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "097919ec3fb73bb7ec22ceea30171c0af1f14fa67d03bd410a15dc3b6b8eff1b"
|
||||
checksum = "41ca5cfdf8b83491f53dd1fe583f43923100eacf76d3b8e6bce9d24dec93356c"
|
||||
dependencies = [
|
||||
"oxc_allocator",
|
||||
]
|
||||
|
||||
+1
-1
@@ -244,7 +244,7 @@ oxc-css-parser = "0.0.17" # CSS/SCSS/Less parser (raffia 0.12.3 fork: adds `temp
|
||||
oxc-graphql-parser = "0.0.9" # GraphQL parser (direct AST; apollo-parser 0.8.6 fork aligned with graphql-js 17 syntax)
|
||||
oxc-markdown-parser = "0.0.3" # CommonMark + GFM parser (span-faithful, style-preserving typed AST)
|
||||
oxc-toml = "0.15.1" # TOML parser and formatter
|
||||
oxc-yaml-parser = "0.0.6" # YAML 1.2 parser (comment-preserving typed AST, node naming from `yaml-unist-parser`)
|
||||
oxc-yaml-parser = "0.0.7" # YAML 1.2 parser (comment-preserving typed AST, node naming from `yaml-unist-parser`)
|
||||
papaya = "0.2.4" # Concurrent hash map
|
||||
percent-encoding = "2.3.2" # URL encoding
|
||||
petgraph = { version = "0.8.3", default-features = false } # Graph data structures
|
||||
|
||||
@@ -36,6 +36,23 @@ The printer re-emits the configured `end_of_line` at the final stage.
|
||||
|
||||
A leading BOM is stripped before parsing and re-emitted by `format()`.
|
||||
|
||||
### Line starts
|
||||
|
||||
A line the printer starts at column 0 must not read back as stream-level syntax (see the parser README "Input acceptance"):
|
||||
|
||||
- `---`/`...` followed by a blank or the end is a document marker
|
||||
- U+FEFF outside a document is a BOM
|
||||
|
||||
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 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
|
||||
|
||||
Elsewhere lines start at column 2 or more and Prettier's layout is matched.
|
||||
See DIVERGENCES.md#document-marker-content and DIVERGENCES.md#bom-content.
|
||||
|
||||
### Comments
|
||||
|
||||
Positional cursor (`Comments` in `src/comments.rs`), same approach as graphql/json, yaml-unist-parser's attach algorithm is NOT ported.
|
||||
|
||||
@@ -293,3 +293,54 @@ run:
|
||||
A same-line trailing comment never counts toward the `fits` measurement, the treatment Prettier itself gives JS/JSON line comments and YAML flow collections, but not the block scalar header.
|
||||
Same rule as CSS `trailing-line-comment-print-width`.
|
||||
The KEY does count: a long key overflowing on `key: |` alone breaks the pair exactly like Prettier.
|
||||
|
||||
## 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`
|
||||
|
||||
```yaml
|
||||
# input
|
||||
--- ---
|
||||
? --- x
|
||||
: m
|
||||
# (proseWrap: always, printWidth: 10)
|
||||
aaaa bbbb --- cccc
|
||||
|
||||
# ours: no line starts with the marker at column 0
|
||||
--- ---
|
||||
? --- x
|
||||
: m
|
||||
aaaa bbbb
|
||||
--- cccc
|
||||
|
||||
# prettier: the content becomes a marker
|
||||
---
|
||||
---
|
||||
--- 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).
|
||||
The guarded sites are listed in AGENTS.md "Line starts".
|
||||
|
||||
## bom-content
|
||||
|
||||
- Why: semantics
|
||||
- Pin: `tests/fixtures/yaml/bom-content.yaml`
|
||||
|
||||
```yaml
|
||||
# input ("␢" marks U+FEFF)
|
||||
? ␢a
|
||||
: m
|
||||
|
||||
# ours: a `---` keeps the key inside a document
|
||||
---
|
||||
␢a: m
|
||||
|
||||
# prettier: the key leads the stream, read back as a BOM, the key becomes "a"
|
||||
␢a: m
|
||||
```
|
||||
|
||||
A U+FEFF never starts the text of a document without `---`, where it is read as a BOM: the document gets a `---` (AGENTS.md "Line starts").
|
||||
@@ -2,7 +2,7 @@ use oxc_formatter_core::{
|
||||
builders::{hard_line_break, space, text},
|
||||
write,
|
||||
};
|
||||
use oxc_yaml_parser::ast::{Directive, Document, Root};
|
||||
use oxc_yaml_parser::ast::{Content, Directive, Document, Node, Root};
|
||||
|
||||
use crate::{
|
||||
comments::{
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
print::{
|
||||
YamlFormatter,
|
||||
block::{ends_with_keep_chomped_block, item_gap_anchor, last_descendant_block_scalar},
|
||||
to_span, write_node_or_suppressed,
|
||||
source_starts_with_document_marker, to_span, write_node_or_suppressed,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -137,6 +137,20 @@ 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}') {
|
||||
// Content starting with U+FEFF needs a `---`: outside a document it would be read as a BOM
|
||||
write!(f, "---");
|
||||
needs_line = true;
|
||||
}
|
||||
if needs_line {
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
@@ -156,6 +170,16 @@ fn write_document<'a>(document: &'a Document<'a>, f: &mut YamlFormatter<'_, 'a>)
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a block mapping's first key starts: a one-line explicit key loses its `?` (`print/mapping_item.rs`),
|
||||
/// so the key text may lead the output.
|
||||
fn first_key_start(node: &Node<'_>) -> u32 {
|
||||
let key = match &node.content {
|
||||
Content::Mapping(mapping) => mapping.children.first().and_then(|item| item.key_content()),
|
||||
_ => None,
|
||||
};
|
||||
key.map_or(node.span.start, |key| key.span.start)
|
||||
}
|
||||
|
||||
/// Re-joins directive words with single spaces (whitespace runs between them are normalized).
|
||||
fn write_directive<'a>(directive: &'a Directive<'a>, f: &mut YamlFormatter<'_, 'a>) {
|
||||
write!(f, "%");
|
||||
|
||||
@@ -14,7 +14,10 @@ use crate::{
|
||||
},
|
||||
context::YamlFormatContext,
|
||||
options::ProseWrap,
|
||||
print::{YamlFormatter, column_of, format_with, to_span, write_node, write_node_or_suppressed},
|
||||
print::{
|
||||
YamlFormatter, column_of, format_with, source_starts_with_document_marker, to_span,
|
||||
write_node, write_node_or_suppressed,
|
||||
},
|
||||
};
|
||||
|
||||
/// Where a mapping item lives; decides the empty-value layout
|
||||
@@ -61,6 +64,10 @@ pub fn write_mapping_item<'a>(
|
||||
}
|
||||
|
||||
let space_before_colon = key_content.is_some_and(|k| matches!(k.content, Content::Alias(_)));
|
||||
// A root mapping key starting with a document marker keeps its `?`: without it the key starts a line at column 0
|
||||
let key_starts_with_marker = in_flow == FlowParent::No
|
||||
&& f.context().collection_depth().get() == 1
|
||||
&& key_content.is_some_and(|k| source_starts_with_document_marker(k.span.start, f));
|
||||
|
||||
if is_empty_value {
|
||||
if in_flow == FlowParent::Mapping {
|
||||
@@ -79,6 +86,7 @@ pub fn write_mapping_item<'a>(
|
||||
&& is_absolutely_single_line(key_content, f)
|
||||
&& !parent_tag_is_set
|
||||
&& !key_content_trailing_comment
|
||||
&& !key_starts_with_marker
|
||||
{
|
||||
write_key(item, f);
|
||||
if space_before_colon {
|
||||
@@ -106,12 +114,14 @@ pub fn write_mapping_item<'a>(
|
||||
let value_start = value_node.span.start;
|
||||
|
||||
// Force explicit key:
|
||||
// the key isn't an inline node, or the source was already explicit with a comment between `?` and the key,
|
||||
// the key isn't an inline node or starts with a document marker,
|
||||
// or the source was already explicit with a comment between `?` and the key,
|
||||
// or between the key and `:` (an implicit `key:` with a comment above the value keeps the implicit form,
|
||||
// the comment becomes the value's leading comment instead).
|
||||
let explicit_comment_before_key = key.explicit
|
||||
&& f.context().comments().peek().is_some_and(|c| c.span.end <= key_node.span.start);
|
||||
if !is_inline(key_content)
|
||||
|| key_starts_with_marker
|
||||
|| explicit_comment_before_key
|
||||
|| (key.explicit && has_own_line_comment_before_value(value_node, f))
|
||||
{
|
||||
|
||||
@@ -59,6 +59,18 @@ pub fn column_of(source: &str, offset: u32) -> u32 {
|
||||
u32::try_from(count).unwrap_or(u32::MAX)
|
||||
}
|
||||
|
||||
/// Whether `text` starts with a document marker: `---`/`...` followed by a blank or the end.
|
||||
/// At column 0 it would be read as one, see AGENTS.md "Line starts".
|
||||
pub fn starts_with_document_marker(text: &str) -> bool {
|
||||
(text.starts_with("---") || text.starts_with("..."))
|
||||
&& text.as_bytes().get(3).is_none_or(|b| matches!(b, b' ' | b'\t' | b'\n'))
|
||||
}
|
||||
|
||||
/// [starts_with_document_marker] for the source text from `offset`.
|
||||
pub fn source_starts_with_document_marker(offset: u32, f: &YamlFormatter<'_, '_>) -> bool {
|
||||
starts_with_document_marker(&f.context().source_text().as_str()[offset as usize..])
|
||||
}
|
||||
|
||||
/// Emits a node's properties (anchor/tag) in their source order.
|
||||
/// NOTE: Prettier 3.9.5 normalizes to `tag anchor` order; we keep the source order instead.
|
||||
///
|
||||
|
||||
@@ -4,12 +4,15 @@ use cow_utils::CowUtils;
|
||||
|
||||
use oxc_formatter_core::{
|
||||
arena_cow_str,
|
||||
builders::{exact_line_breaks, hard_line_break, soft_line_break_or_space, text},
|
||||
builders::{Line, exact_line_breaks, hard_line_break, indent, soft_line_break_or_space, text},
|
||||
write,
|
||||
};
|
||||
use oxc_span::Span;
|
||||
|
||||
use crate::{options::ProseWrap, print::YamlFormatter};
|
||||
use crate::{
|
||||
options::ProseWrap,
|
||||
print::{YamlFormatter, format_with, starts_with_document_marker},
|
||||
};
|
||||
|
||||
/// Which flow scalar shape is being folded (affects word-merge rules and quoting).
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
@@ -117,6 +120,18 @@ pub fn write_flow_scalar_content<'a>(
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Outside block collections, continuation lines start at column 0:
|
||||
// a line starting with a document marker is indented instead (see AGENTS.md "Line starts")
|
||||
let guards_line_start = f.context().collection_depth().get() == 0;
|
||||
let line_before = |line: Line, word: &'a str| {
|
||||
format_with(move |f| {
|
||||
if guards_line_start && starts_with_document_marker(word) {
|
||||
write!(f, indent(&line));
|
||||
} else {
|
||||
write!(f, line);
|
||||
}
|
||||
})
|
||||
};
|
||||
let mut blanks = 0usize;
|
||||
let mut wrote_any = false;
|
||||
for words in &line_groups {
|
||||
@@ -127,15 +142,15 @@ pub fn write_flow_scalar_content<'a>(
|
||||
if blanks > 0 {
|
||||
// N blank groups = N separators before this group
|
||||
// (plus the one separating it from the previous non-empty group, if any).
|
||||
write!(f, exact_line_breaks(blanks + usize::from(wrote_any)));
|
||||
write!(f, line_before(exact_line_breaks(blanks + usize::from(wrote_any)), words[0]));
|
||||
} else if wrote_any {
|
||||
write!(f, hard_line_break());
|
||||
write!(f, line_before(hard_line_break(), words[0]));
|
||||
}
|
||||
blanks = 0;
|
||||
wrote_any = true;
|
||||
let mut fill = f.fill();
|
||||
for &word in words {
|
||||
fill.entry(&soft_line_break_or_space(), &text(word));
|
||||
fill.entry(&line_before(soft_line_break_or_space(), word), &text(word));
|
||||
}
|
||||
fill.finish();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
? a
|
||||
: m
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/oxc_formatter_yaml/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
? a
|
||||
: m
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
---
|
||||
a: m
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
---
|
||||
a: m
|
||||
|
||||
===================== End =====================
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
--- ---
|
||||
--- ... x
|
||||
--- --- a
|
||||
b
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
---
|
||||
source: crates/oxc_formatter_yaml/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
--- ---
|
||||
--- ... x
|
||||
--- --- a
|
||||
b
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
--- ---
|
||||
--- ... x
|
||||
--- --- a
|
||||
b
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
--- ---
|
||||
--- ... x
|
||||
--- --- a
|
||||
b
|
||||
|
||||
---------------------------------------
|
||||
{ printWidth: 10, proseWrap: "always" }
|
||||
---------------------------------------
|
||||
--- ---
|
||||
--- ... x
|
||||
--- --- a
|
||||
b
|
||||
|
||||
---------------------------------------
|
||||
{ printWidth: 80, proseWrap: "always" }
|
||||
---------------------------------------
|
||||
--- ---
|
||||
--- ... x
|
||||
--- --- a b
|
||||
|
||||
----------------------------------------
|
||||
{ printWidth: 100, proseWrap: "always" }
|
||||
----------------------------------------
|
||||
--- ---
|
||||
--- ... x
|
||||
--- --- a b
|
||||
|
||||
===================== End =====================
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
a: 1
|
||||
? --- x
|
||||
: m
|
||||
? ... y
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
source: crates/oxc_formatter_yaml/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
a: 1
|
||||
? --- x
|
||||
: m
|
||||
? ... y
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
a: 1
|
||||
? --- x
|
||||
: m
|
||||
? ... y
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
a: 1
|
||||
? --- x
|
||||
: m
|
||||
? ... y
|
||||
|
||||
---------------------------------------
|
||||
{ printWidth: 10, proseWrap: "always" }
|
||||
---------------------------------------
|
||||
a: 1
|
||||
? --- x
|
||||
: m
|
||||
? ... y
|
||||
|
||||
---------------------------------------
|
||||
{ printWidth: 80, proseWrap: "always" }
|
||||
---------------------------------------
|
||||
a: 1
|
||||
? --- x
|
||||
: m
|
||||
? ... y
|
||||
|
||||
----------------------------------------
|
||||
{ printWidth: 100, proseWrap: "always" }
|
||||
----------------------------------------
|
||||
a: 1
|
||||
? --- x
|
||||
: m
|
||||
? ... y
|
||||
|
||||
===================== End =====================
|
||||
+1
@@ -0,0 +1 @@
|
||||
[{}, { "proseWrap": "always", "printWidth": 10 }]
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
aaaa bbbb --- cccc
|
||||
---
|
||||
"aaaa
|
||||
--- x"
|
||||
---
|
||||
aaaa
|
||||
|
||||
... x
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
source: crates/oxc_formatter_yaml/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
aaaa bbbb --- cccc
|
||||
---
|
||||
"aaaa
|
||||
--- x"
|
||||
---
|
||||
aaaa
|
||||
|
||||
... x
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
aaaa bbbb --- cccc
|
||||
---
|
||||
"aaaa
|
||||
--- x"
|
||||
---
|
||||
aaaa
|
||||
|
||||
... x
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
aaaa bbbb --- cccc
|
||||
---
|
||||
"aaaa
|
||||
--- x"
|
||||
---
|
||||
aaaa
|
||||
|
||||
... x
|
||||
|
||||
---------------------------------------
|
||||
{ printWidth: 10, proseWrap: "always" }
|
||||
---------------------------------------
|
||||
aaaa bbbb
|
||||
--- cccc
|
||||
---
|
||||
"aaaa ---
|
||||
x"
|
||||
---
|
||||
aaaa
|
||||
|
||||
... x
|
||||
|
||||
---------------------------------------
|
||||
{ printWidth: 80, proseWrap: "always" }
|
||||
---------------------------------------
|
||||
aaaa bbbb --- cccc
|
||||
---
|
||||
"aaaa --- x"
|
||||
---
|
||||
aaaa
|
||||
|
||||
... x
|
||||
|
||||
----------------------------------------
|
||||
{ printWidth: 100, proseWrap: "always" }
|
||||
----------------------------------------
|
||||
aaaa bbbb --- cccc
|
||||
---
|
||||
"aaaa --- x"
|
||||
---
|
||||
aaaa
|
||||
|
||||
... x
|
||||
|
||||
===================== End =====================
|
||||
Reference in new issue
Block a user