-
Notifications
You must be signed in to change notification settings - Fork 4.8k
yaml: clear remaining 3 non-cyclic test.todo entries (BOM strip, 2-anchor e-node key, flow cmi gap)
#31569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
yaml: clear remaining 3 non-cyclic test.todo entries (BOM strip, 2-anchor e-node key, flow cmi gap)
#31569
Changes from 12 commits
6f088e2
8687f17
544b779
6187d11
6885852
86ec4aa
e341f05
755f18a
938fb32
fc12ed7
89c9b9b
fcd70c0
12c16e5
efcfffe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,32 @@ use bun_core::{self, StackCheck}; | |
|
|
||
| pub struct YAML; | ||
|
|
||
| /// Spec §5.2 byte-order-mark / null-byte encoding detection. | ||
| #[derive(Clone, Copy, PartialEq, Eq)] | ||
| enum DetectedEncoding { | ||
| Utf8, | ||
| Utf16Le, | ||
| Utf16Be, | ||
| Utf32Le, | ||
| Utf32Be, | ||
| } | ||
|
|
||
| fn detect_encoding(bytes: &[u8]) -> DetectedEncoding { | ||
| use DetectedEncoding::*; | ||
| let b = |i: usize| bytes.get(i).copied().unwrap_or(0xFF); | ||
| match (b(0), b(1), b(2), b(3)) { | ||
| (0x00, 0x00, 0xFE, 0xFF) => Utf32Be, | ||
| (0xFF, 0xFE, 0x00, 0x00) => Utf32Le, | ||
| (0x00, 0x00, 0x00, _) => Utf32Be, | ||
| (_, 0x00, 0x00, 0x00) => Utf32Le, | ||
| (0xFE, 0xFF, _, _) => Utf16Be, | ||
| (0xFF, 0xFE, _, _) => Utf16Le, | ||
| (0x00, ..) => Utf16Be, | ||
| (_, 0x00, _, _) => Utf16Le, | ||
| _ => Utf8, | ||
| } | ||
| } | ||
|
|
||
| impl YAML { | ||
| pub fn parse( | ||
| source: &bun_ast::Source, | ||
|
|
@@ -34,12 +60,59 @@ impl YAML { | |
| // Zig: `bun.analytics.Features.yaml_parse += 1;` | ||
| bun_core::analytics::Features::yaml_parse_inc(); | ||
|
|
||
| let mut parser: Parser<Utf8> = Parser::init(bump, source.contents()); | ||
| let bytes = source.contents(); | ||
| match detect_encoding(bytes) { | ||
| DetectedEncoding::Utf8 => Self::parse_with::<Utf8>(source, bytes, log, bump), | ||
| enc @ (DetectedEncoding::Utf16Le | DetectedEncoding::Utf16Be) => { | ||
| let be = matches!(enc, DetectedEncoding::Utf16Be); | ||
| if !bytes.len().is_multiple_of(2) { | ||
| log.add_error( | ||
| Some(source), | ||
| bun_ast::Loc::EMPTY, | ||
| b"UTF-16 input has odd byte length", | ||
| ); | ||
| return Err(YamlParseError::SyntaxError); | ||
| } | ||
| // The Expr tree may hold slices into the input; allocate the | ||
| // transcoded buffer in the same arena so it shares its lifetime. | ||
| // TODO: Parser<Utf16> tracks `pos` as a u16 index, but | ||
| // `Pos::loc()` is interpreted as a byte offset into | ||
| // `source.contents` — error positions for UTF-16 byte input are | ||
| // off. Fixing requires mapping u16 index → byte offset before | ||
| // constructing `Loc`. | ||
| let units: &mut [u16] = bump.alloc_slice_fill_default(bytes.len() / 2); | ||
| for (i, c) in bytes.chunks_exact(2).enumerate() { | ||
| units[i] = if be { | ||
| u16::from_be_bytes([c[0], c[1]]) | ||
| } else { | ||
| u16::from_le_bytes([c[0], c[1]]) | ||
| }; | ||
| } | ||
| Self::parse_with::<Utf16>(source, units, log, bump) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
| DetectedEncoding::Utf32Le | DetectedEncoding::Utf32Be => { | ||
| log.add_error( | ||
| Some(source), | ||
| bun_ast::Loc::EMPTY, | ||
| b"UTF-32 input is not supported", | ||
| ); | ||
| Err(YamlParseError::SyntaxError) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn parse_with<Enc: Encoding>( | ||
| source: &bun_ast::Source, | ||
| input: &[Enc::Unit], | ||
| log: &mut bun_ast::Log, | ||
| bump: &bun_alloc::Arena, | ||
| ) -> Result<Expr, YamlParseError> { | ||
| let mut parser: Parser<Enc> = Parser::init(bump, input); | ||
|
|
||
| let stream = match parser.parse() { | ||
| Ok(s) => s, | ||
| Err(e) => { | ||
| let err = ParseResult::<Utf8>::fail(e, &parser); | ||
| let err = ParseResult::<Enc>::fail(e, &parser); | ||
| if let ParseResult::Err(err) = err { | ||
| err.add_to_log(source, log)?; | ||
| } | ||
|
|
@@ -463,6 +536,9 @@ pub trait Encoding: Copy + 'static { | |
| /// buffer (see `EncLit` doc for the const-generics rationale). | ||
| fn literal(s: &'static [u8]) -> EncLit<Self::Unit>; | ||
|
|
||
| /// Number of leading units to skip if `input` starts with [3] c-byte-order-mark. | ||
| fn bom_len(input: &[Self::Unit]) -> usize; | ||
|
|
||
| /// Reinterpret a `&[Unit]` slice as `&[u8]` for `StringHashMap` keying | ||
| /// (`anchors` / `tag_handles`). Zig's `bun.StringHashMap` is keyed by | ||
| /// `[]const u8`; calls like `tag_handles.put(handle.slice(self.input), {})` | ||
|
|
@@ -523,6 +599,10 @@ impl Encoding for Latin1 { | |
| // Only reachable from `EncodingKind::Utf16`-gated arms. | ||
| unreachable!("unit_from_u16 on Latin1") | ||
| } | ||
| #[inline] | ||
| fn bom_len(_input: &[u8]) -> usize { | ||
| 0 | ||
| } | ||
| } | ||
|
|
||
| impl Encoding for Utf8 { | ||
|
|
@@ -551,6 +631,14 @@ impl Encoding for Utf8 { | |
| // Only reachable from `EncodingKind::Utf16`-gated arms. | ||
| unreachable!("unit_from_u16 on Utf8") | ||
| } | ||
| #[inline] | ||
| fn bom_len(input: &[u8]) -> usize { | ||
| if input.len() >= 3 && input[0] == 0xEF && input[1] == 0xBB && input[2] == 0xBF { | ||
| 3 | ||
| } else { | ||
| 0 | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Encoding for Utf16 { | ||
|
|
@@ -589,6 +677,10 @@ impl Encoding for Utf16 { | |
| u | ||
| } | ||
| #[inline] | ||
| fn bom_len(input: &[u16]) -> usize { | ||
| if input.first() == Some(&0xFEFF) { 1 } else { 0 } | ||
| } | ||
| #[inline] | ||
| fn as_u16_slice(s: &[u16]) -> &[u16] { | ||
| s | ||
| } | ||
|
|
@@ -1476,7 +1568,12 @@ impl<'i, Enc: Encoding> ScalarResolverCtx<'i, Enc> { | |
| let mut minus = false; | ||
| let mut hex = false; | ||
|
|
||
| if first_char != FirstChar::Negative && first_char != FirstChar::Positive { | ||
| // For Negative/Positive the sign was consumed by the caller; the first | ||
| // body char (digit or `.`) is at `pos`. For Other/Dot the caller left | ||
| // `pos` at the first body char too. Either way, advance past it so the | ||
| // loop starts at the second body char with the `decimal`/digit flags | ||
| // already reflecting the first. | ||
| if !matches!(first_char, FirstChar::Negative | FirstChar::Positive) || decimal { | ||
| parser!().inc(1); | ||
| } | ||
|
|
||
|
|
@@ -1630,15 +1727,23 @@ impl<'i, Enc: Encoding> ScalarResolverCtx<'i, Enc> { | |
| return Ok(()); | ||
| } | ||
|
|
||
| let lexed = parser!().slice(start, end); | ||
| let mut scalar: NodeScalar<Enc> = 'scalar: { | ||
| if x || o || hex { | ||
| let unsigned = match parse_unsigned_radix0::<Enc>(parser!().slice(start, end)) { | ||
| let unsigned = match parse_unsigned_radix0::<Enc>(lexed) { | ||
| Ok(v) => v, | ||
| Err(_) => return Ok(()), | ||
| }; | ||
| break 'scalar NodeScalar::Number(unsigned as f64); | ||
| } | ||
| let float = match parse_double_generic::<Enc>(parser!().slice(start, end)) { | ||
| // [10.2.1.4] Core schema float/int regex. The lexer loop above is | ||
| // permissive (accepts `+`/`-`/`e`/`.` at any position) and | ||
| // `wtf::parse_double` prefix-parses, so `1+1` would resolve as 1. | ||
| // Validate the consumed slice matches the schema before parsing. | ||
| if !is_core_schema_number::<Enc>(lexed, first_char) { | ||
| return Ok(()); | ||
| } | ||
|
claude[bot] marked this conversation as resolved.
|
||
| let float = match parse_double_generic::<Enc>(lexed) { | ||
| Ok(v) => v, | ||
| Err(_) => return Ok(()), | ||
| }; | ||
|
|
@@ -1664,6 +1769,59 @@ impl<'i, Enc: Encoding> ScalarResolverCtx<'i, Enc> { | |
| } | ||
| } | ||
|
|
||
| /// [10.2.1.4] Core schema int/float pattern. The slice may already have had a | ||
| /// leading `.` or `+`/`-` consumed by the caller before `start` was captured; | ||
| /// `first_char` carries that. | ||
| /// `[-+]? ( \. [0-9]+ | [0-9]+ ( \. [0-9]* )? ) ( [eE] [-+]? [0-9]+ )?` | ||
| fn is_core_schema_number<Enc: Encoding>(s: &[Enc::Unit], first_char: FirstChar) -> bool { | ||
| let mut i = 0usize; | ||
| let len = s.len(); | ||
| let at = |j: usize| Enc::wide(s[j]); | ||
| let is_digit = |c: u32| (0x30..=0x39).contains(&c); | ||
|
|
||
| // Mantissa: \. [0-9]+ | [0-9]+ ( \. [0-9]* )? | ||
| let saw_leading_dot = first_char == FirstChar::Dot | ||
| || (i < len && at(i) == 0x2E && { | ||
| i += 1; | ||
| true | ||
| }); | ||
| if saw_leading_dot { | ||
| if i >= len || !is_digit(at(i)) { | ||
| return false; | ||
| } | ||
| while i < len && is_digit(at(i)) { | ||
| i += 1; | ||
| } | ||
| } else { | ||
| if i >= len || !is_digit(at(i)) { | ||
| return false; | ||
| } | ||
| while i < len && is_digit(at(i)) { | ||
| i += 1; | ||
| } | ||
| if i < len && at(i) == 0x2E { | ||
| i += 1; | ||
| while i < len && is_digit(at(i)) { | ||
| i += 1; | ||
| } | ||
| } | ||
| } | ||
| // Optional exponent: [eE] [-+]? [0-9]+ | ||
| if i < len && matches!(at(i), 0x65 | 0x45) { | ||
| i += 1; | ||
| if i < len && matches!(at(i), 0x2B | 0x2D) { | ||
| i += 1; | ||
| } | ||
| if i >= len || !is_digit(at(i)) { | ||
| return false; | ||
| } | ||
| while i < len && is_digit(at(i)) { | ||
| i += 1; | ||
| } | ||
| } | ||
| i == len | ||
| } | ||
|
|
||
| /// Port of `bun.jsc.wtf.parseDouble(slice)` over an encoding-generic slice. | ||
| /// `bun_core::wtf::parse_double` takes `&[u8]`; for `Utf8`/`Latin1` we narrow | ||
| /// via `Enc::key_bytes` (identity). For `Utf16` the lexer guarantees the | ||
|
|
@@ -2359,11 +2517,13 @@ impl<'i, Enc: Encoding> Parser<'i, Enc> { | |
| pub const MAX_ALIAS_EXPANSION: usize = 16 * 1024 * 1024; | ||
|
|
||
| pub fn init(bump: &'i bun_alloc::Arena, input: &'i [Enc::Unit]) -> Self { | ||
| // [206] l-document-prefix ::= c-byte-order-mark? l-comment* | ||
| let start = Pos::from(Enc::bom_len(input)); | ||
| Self { | ||
| input, | ||
| bump, | ||
| pos: Pos::from(0), | ||
| line_start_pos: Pos::from(0), | ||
| pos: start, | ||
| line_start_pos: start, | ||
|
coderabbitai[bot] marked this conversation as resolved.
claude[bot] marked this conversation as resolved.
Comment on lines
+2447
to
+2453
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟣 🟣 Pre-existing, non-blocking: this strips the BOM at stream-start only, but [211] Extended reasoning...What
so Code path
Step-by-step proofInput (UTF-8):
This is the realistic shape of Why nothing else catches it
ImpactObscure (multi-document streams with per-document BOM, e.g. concatenated UTF-8-with-BOM files), and strictly improved vs FixWhen the document loop sees |
||
| line_indent: Indent::NONE, | ||
| tab_after_indent: false, | ||
| line: Line::from(1), | ||
|
|
@@ -3467,7 +3627,7 @@ impl<Enc: Encoding> NodeProperties<Enc> { | |
|
|
||
| pub fn set_anchor(&mut self, anchor_token: Token<Enc>) -> Result<(), ParseError> { | ||
| if let Some(previous_anchor) = &self.has_anchor { | ||
| if previous_anchor.line == anchor_token.line { | ||
| if previous_anchor.line == anchor_token.line || self.has_mapping_anchor.is_some() { | ||
| return Err(ParseError::MultipleAnchors); | ||
| } | ||
| self.has_mapping_anchor = Some(previous_anchor.clone()); | ||
|
|
@@ -3977,6 +4137,11 @@ impl<'i, Enc: Encoding> Parser<'i, Enc> { | |
| } | ||
| } | ||
|
|
||
| // [147] flow-map value is ns-flow-node, not a pair. | ||
| if self.context.get() == Context::FlowIn && !opts.flow_pair_allowed { | ||
| return Ok(copy); | ||
| } | ||
|
|
||
| let map = self.parse_block_mapping( | ||
| copy, | ||
| alias_start, | ||
|
|
@@ -4028,6 +4193,11 @@ impl<'i, Enc: Encoding> Parser<'i, Enc> { | |
| } | ||
| } | ||
|
|
||
| // [147] flow-map value is ns-flow-node, not a pair. | ||
| if self.context.get() == Context::FlowIn && !opts.flow_pair_allowed { | ||
| break 'node seq; | ||
| } | ||
|
|
||
| let implicit_key_anchors = | ||
| node_props.implicit_key_anchors(sequence_line)?; | ||
|
|
||
|
|
@@ -4115,6 +4285,11 @@ impl<'i, Enc: Encoding> Parser<'i, Enc> { | |
| } | ||
| } | ||
|
|
||
| // [147] flow-map value is ns-flow-node, not a pair. | ||
| if self.context.get() == Context::FlowIn && !opts.flow_pair_allowed { | ||
| break 'node map; | ||
| } | ||
|
|
||
| let implicit_key_anchors = node_props.implicit_key_anchors(mapping_line)?; | ||
|
|
||
| if let Some(key_anchor) = implicit_key_anchors.key_anchor { | ||
|
|
@@ -4234,14 +4409,13 @@ impl<'i, Enc: Encoding> Parser<'i, Enc> { | |
| self.anchors | ||
| .put(Enc::key_bytes(mapping_anchor.slice(self.input)), mapping)?; | ||
| } | ||
| // Clear has_anchor so the post-loop fallback doesn't | ||
| // re-register the inner anchor on the mapping. The | ||
| // remaining fields reach the post-loop guards: this | ||
| // matches main's behavior (rejects `!!a\n!!b\n: x` and | ||
| // `&a\n&b\n&c : x`), but also over-rejects the valid | ||
| // `&outer\n&inner : x` — pre-existing; the Scalar arm | ||
| // avoids it via `return Ok`. | ||
| // Anchors are fully consumed by implicit_key_anchors; | ||
| // clear both so the post-loop fallback doesn't re-register | ||
| // (or over-reject `&outer\n&inner : x` via the | ||
| // has_mapping_anchor guard). Tag fields stay so the | ||
| // has_mapping_tag guard still catches `!!a\n!!b\n: x`. | ||
| node_props.has_anchor = None; | ||
| node_props.has_mapping_anchor = None; | ||
|
claude[bot] marked this conversation as resolved.
|
||
| break 'node mapping; | ||
| } | ||
|
|
||
|
|
@@ -4325,6 +4499,16 @@ impl<'i, Enc: Encoding> Parser<'i, Enc> { | |
| } | ||
| } | ||
|
|
||
| // [147] flow-map value is ns-flow-node, not a pair. | ||
| // Return the bare scalar; the leftover `:` reaches the | ||
| // caller's [140] check. The cmi==scalar_indent path | ||
| // above already handles the same-line case; this | ||
| // covers the multiline-property case where they | ||
| // diverge. | ||
| if self.context.get() == Context::FlowIn && !opts.flow_pair_allowed { | ||
| break 'node scalar.data.to_expr(scalar_start, self.input, self.bump); | ||
| } | ||
|
|
||
|
claude[bot] marked this conversation as resolved.
|
||
| let implicit_key = scalar.data.to_expr(scalar_start, self.input, self.bump); | ||
|
|
||
| let implicit_key_anchors = node_props.implicit_key_anchors(scalar_line)?; | ||
|
|
@@ -4575,9 +4759,11 @@ impl<'i, Enc: Encoding> Parser<'i, Enc> { | |
| } | ||
|
|
||
| 0x23 /* '#' */ => { | ||
| let prev = parser!().input[parser!().pos.sub(1).cast()]; | ||
| if parser!().pos == Pos::ZERO | ||
| || matches!(Enc::wide(prev), 0x20 | 0x09 | 0x0D | 0x0A) | ||
| if parser!().is_at_line_start() | ||
| || matches!( | ||
| Enc::wide(parser!().input[parser!().pos.sub(1).cast()]), | ||
| 0x20 | 0x09 | 0x0D | 0x0A | ||
| ) | ||
| { | ||
| return Ok(ctx.done()); | ||
| } | ||
|
|
@@ -5857,13 +6043,14 @@ impl<'i, Enc: Encoding> Parser<'i, Enc> { | |
| } | ||
| 0x23 /* '#' */ => { | ||
| let start = self.pos; | ||
| let prev = if start == Pos::ZERO { 0 } else { Enc::wide(self.input[start.cast() - 1]) }; | ||
| match prev { | ||
| 0 | 0x20 | 0x09 | 0x0A | 0x0D => {} | ||
| _ => { | ||
| // TODO: prove this is unreachable | ||
| return Err(ParseError::UnexpectedCharacter); | ||
| } | ||
| if !self.is_at_line_start() | ||
| && !matches!( | ||
| Enc::wide(self.input[start.cast() - 1]), | ||
| 0x20 | 0x09 | 0x0A | 0x0D | ||
| ) | ||
| { | ||
| // TODO: prove this is unreachable | ||
| return Err(ParseError::UnexpectedCharacter); | ||
| } | ||
| self.inc(1); | ||
| while !self.is_b_char_or_eof() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should go in a bun_core::bom module
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed — though
detect_encodingwas just removed in 12c16e5 (Dylan flagged thatParser<Utf16>has never been exercised, so shipping it or erroring on detected UTF-16 byte input both trade one untested behavior for another). Only the UTF-8 BOM strip inParser::initremains.When §5.2 detection ships properly (after a full
Parser<Utf16>test sweep),bun_core::bomis the right home so JSON5/TOML/JSONC can share it.