diff --git a/src/parsers/yaml.rs b/src/parsers/yaml.rs index 5851f585546..1b7c197840f 100644 --- a/src/parsers/yaml.rs +++ b/src/parsers/yaml.rs @@ -463,6 +463,9 @@ pub trait Encoding: Copy + 'static { /// buffer (see `EncLit` doc for the const-generics rationale). fn literal(s: &'static [u8]) -> EncLit; + /// 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 +526,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 +558,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 +604,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 +1495,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 +1654,23 @@ impl<'i, Enc: Encoding> ScalarResolverCtx<'i, Enc> { return Ok(()); } + let lexed = parser!().slice(start, end); let mut scalar: NodeScalar = 'scalar: { if x || o || hex { - let unsigned = match parse_unsigned_radix0::(parser!().slice(start, end)) { + let unsigned = match parse_unsigned_radix0::(lexed) { Ok(v) => v, Err(_) => return Ok(()), }; break 'scalar NodeScalar::Number(unsigned as f64); } - let float = match parse_double_generic::(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::(lexed, first_char) { + return Ok(()); + } + let float = match parse_double_generic::(lexed) { Ok(v) => v, Err(_) => return Ok(()), }; @@ -1664,6 +1696,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(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 +2444,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, line_indent: Indent::NONE, tab_after_indent: false, line: Line::from(1), @@ -3467,7 +3554,7 @@ impl NodeProperties { pub fn set_anchor(&mut self, anchor_token: Token) -> 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 +4064,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 +4120,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 +4212,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 +4336,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; break 'node mapping; } @@ -4325,6 +4426,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); + } + 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 +4686,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 +5970,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() { diff --git a/src/runtime/api/YAMLObject.rs b/src/runtime/api/YAMLObject.rs index 7b2b6422395..8cdf6649719 100644 --- a/src/runtime/api/YAMLObject.rs +++ b/src/runtime/api/YAMLObject.rs @@ -900,16 +900,14 @@ fn string_needs_quotes(str: &BunString) -> bool { /// Returns true when `str` would be parsed back as a number by `YAML.parse`. /// -/// This mirrors the rules in `src/interchange/yaml.zig`'s `tryResolveNumber`: -/// - Optional leading sign, optionally followed by `.inf`/`.Inf`/`.INF` for signed infinity. -/// - Otherwise a numeric mantissa: digits/`.`/`e`/`E`/hex letters, plus additional `+`/`-` -/// (the parser accepts any number of `+` after the leading sign as long as no `x` was -/// seen, and at most one additional `-`). -/// - `0x` / `0X` → hex digits; `0o` / `0O` → octal digits. -/// - Additionally, `wtf.parseDouble` is a prefix parser, so a leading numeric prefix is -/// enough for `YAML.parse` to resolve a number — e.g. `"1+5"` round-trips to `1`. -/// We err on the side of quoting when the parser's scanner would accept the full token -/// as `valid`. +/// This mirrors the rules in `src/parsers/yaml.rs`'s `try_resolve_number` / +/// `is_core_schema_number`: +/// - Optional leading sign, optionally followed by `.inf`/`.Inf`/`.INF`. +/// - Otherwise either an integer (`[0-9]+` / `0x…` / `0o…`) or a float +/// matching §10.2.1.4 `[-+]? ( . [0-9]+ | [0-9]+ ( . [0-9]* )? ) ([eE][-+]?[0-9]+)?`. +/// The parser-side gate now rejects non-conforming float-like tokens +/// (e.g. `"1+5"`, `"1e"`, `"."`) so this mirror should err on the side of +/// *quoting* whenever a token *might* parse as a number. fn string_is_number(str: &BunString) -> bool { let len = str.length(); if len == 0 { diff --git a/test/js/bun/yaml/yaml.test.ts b/test/js/bun/yaml/yaml.test.ts index 7769f01a86e..7dda7b23392 100644 --- a/test/js/bun/yaml/yaml.test.ts +++ b/test/js/bun/yaml/yaml.test.ts @@ -1214,12 +1214,9 @@ folded: > expect(() => YAML.parse("!!str\n!!map\n: x\n")).toThrow("Multiple tags"); }); - test.todo("two anchors before e-node `:` (outer=mapping, inner=key) — pre-existing over-reject", () => { + test("two anchors before e-node `:` (outer=mapping, inner=key)", () => { // Valid per [200]/[193]: outer anchors the collection, inner the - // e-node key. The Scalar-key analogue (`&outer\n&inner b: x`) is - // accepted because that arm `return Ok(mapping)`, bypassing the - // post-loop has_mapping_anchor guard; the e-node arm reaches it. - // Pre-existing on main. + // e-node key. expect(YAML.parse("&outer\n&inner : x\n")).toEqual({ null: "x" }); }); @@ -1357,6 +1354,18 @@ folded: > expect(() => YAML.parse("a: &x\n &y\n c: 1\n")).toThrow("Multiple anchors"); // Both props more-indented; inner same-line as key — valid. expect(YAML.parse("a:\n &x\n &y c: 1\n")).toEqual({ a: { c: 1 } }); + // 3rd anchor on a 3rd line — set_anchor's overflow guard catches it. + expect(() => YAML.parse("&a\n&b\n&c d: 1\n")).toThrow("Multiple anchors"); + }); + + test.todo("two tags before e-node `:` — [200]/[193] line split (tag analogue)", () => { + // The MappingValue arm clears has_anchor/has_mapping_anchor but not + // has_mapping_tag, so the post-loop guard over-rejects the valid + // 2-tag case. set_tag also lacks the has_mapping_tag.is_some() + // overflow guard that set_anchor has, so a 3rd tag on a 3rd line + // silently overwrites instead of erroring. + expect(YAML.parse("!!map\n!!str : x\n")).toEqual({ "": "x" }); + expect(() => YAML.parse("!!a\n!!b\n!!c d: 1\n")).toThrow("Multiple tags"); }); }); @@ -1501,12 +1510,11 @@ folded: > expect(() => YAML.parse("{? a: b: c}\n")).toThrow("Unexpected token"); }); - test.todo("flow-map value with multiline c-ns-properties before nested pair", () => { - // The cmi mechanism for [147] uses the first token's indent; when an - // anchor/tag is on a different line than the scalar, indents diverge - // and the cmi check is bypassed. Pre-existing. - expect(() => YAML.parse("{a: &x\n b: c}\n")).toThrow(); - expect(() => YAML.parse("{a: !!str\n b: c}\n")).toThrow(); + test("flow-map value with multiline c-ns-properties before nested pair", () => { + // [147] flow-map value is ns-flow-node; the Scalar arm returns the + // bare scalar in FlowIn when !flow_pair_allowed, regardless of cmi. + expect(() => YAML.parse("{a: &x\n b: c}\n")).toThrow("Unexpected token"); + expect(() => YAML.parse("{a: !!str\n b: c}\n")).toThrow("Unexpected token"); }); test("flow-seq pair value on next line at column ≤ key indent", () => { @@ -1516,6 +1524,12 @@ folded: > expect(YAML.parse("[a: \nb]\n")).toEqual([{ a: "b" }]); }); + test("flow-map [147] value guard mirrored to Alias/SequenceStart/MappingStart arms", () => { + expect(() => YAML.parse("{a: &x\n [b]: c}\n")).toThrow("Unexpected token"); + expect(() => YAML.parse("{a: &x\n {b}: c}\n")).toThrow("Unexpected token"); + expect(() => YAML.parse("{x: &y 1, a: &z\n *y : c}\n")).toThrow("Unexpected token"); + }); + test("multiline JSON-style key check ordering", () => { // §7.4.2 prose says implicit keys are "restricted to a single // line", but the official yaml-test-suite (4MUZ/*, 5MUD, 9SA2, @@ -1727,13 +1741,185 @@ folded: > expect(() => YAML.parse("a\n...\n... b\n")).toThrow("Unexpected token"); }); - test.todo("BOM-prefixed `---` recognized as doc marker", () => { - // [202] l-document-prefix admits c-byte-order-mark before - // c-directives-end. is_at_line_start() is false after the BOM - // (pos != line_start_pos). Pre-existing differently-wrong (was a - // spurious BOM-only first doc); proper fix is BOM strip in - // l-document-prefix, not special-casing here. + test("BOM-prefixed `---` recognized as doc marker", () => { + // [206] l-document-prefix ::= c-byte-order-mark? l-comment* expect(YAML.parse("\uFEFF---\na: 1\n")).toEqual({ a: 1 }); + expect(YAML.parse("\uFEFFa: 1\n")).toEqual({ a: 1 }); + expect(YAML.parse("\uFEFF# comment\na: 1\n")).toEqual({ a: 1 }); + expect(YAML.parse("\uFEFF")).toEqual(null); + // BOM mid-stream is not stripped (only [206] document-prefix). + expect(YAML.parse("a: \uFEFFx\n")).toEqual({ a: "\uFEFFx" }); + }); + + test("UTF-8 BOM on byte input is stripped", () => { + expect(YAML.parse(Buffer.from("a: 1\n"))).toEqual({ a: 1 }); + expect(YAML.parse(Buffer.from([0xef, 0xbb, 0xbf, ...Buffer.from("a: 1\n")]))).toEqual({ a: 1 }); + }); + }); + + describe("[10.2.1.4] Core schema number resolution", () => { + // The lexer loop accepted `+`/`-`/`e`/`.` at any position and + // wtf::parse_double prefix-parses, so `1+1` resolved as 1. Validation + // now requires the consumed slice to match the Core schema regex. + test.each([ + ["1+1", "1+1"], + ["1-1", "1-1"], + ["++1", "++1"], + ["--1", "--1"], + ["+-1", "+-1"], + ["1e", "1e"], + ["1e+", "1e+"], + ["1e-", "1e-"], + ["1E", "1E"], + ["1e1.5", "1e1.5"], + ["1e.5", "1e.5"], + ["1.2.3", "1.2.3"], + [".", "."], + [".e5", ".e5"], + ["-.e5", "-.e5"], + ["e5", "e5"], + ] as const)("%s resolves as string", (input, expected) => { + expect(YAML.parse(input)).toBe(expected); + }); + test.each([ + ["1", 1], + ["-1", -1], + ["+1", 1], + ["0", 0], + ["-0", -0], + ["1.5", 1.5], + [".5", 0.5], + ["-.5", -0.5], + ["+.5", 0.5], + ["1.", 1], + ["1e5", 1e5], + ["1e+5", 1e5], + ["1e-5", 1e-5], + ["1.5e10", 1.5e10], + [".5e2", 50], + ["-.5e2", -50], + ["0x1f", 31], + ["0o17", 15], + ] as const)("%s resolves as number %p", (input, expected) => { + expect(YAML.parse(input)).toBe(expected); + }); + test.todo.each(["-0x1f", "+0x1f", "-0o17", "+0o17"])( + "signed hex/octal %s resolves as string (§10.2.1.2)", + input => { + // Core schema int regex is `0x [0-9a-fA-F]+` — no sign. js-yaml, + // PyYAML, ruamel agree. Pre-existing on the int path (not gated + // by is_core_schema_number, which only validates the float path). + expect(YAML.parse(input)).toBe(input); + }, + ); + test.each([ + [".inf", Infinity], + ["+.inf", Infinity], + [".Inf", Infinity], + [".INF", Infinity], + ["-.inf", -Infinity], + ["-.Inf", -Infinity], + ["-.INF", -Infinity], + ] as const)("%s resolves as %p", (input, expected) => { + expect(YAML.parse(input)).toBe(expected); + }); + test(".nan resolves as NaN", () => { + expect(YAML.parse(".nan")).toBeNaN(); + expect(YAML.parse(".NaN")).toBeNaN(); + expect(YAML.parse(".NAN")).toBeNaN(); + }); + }); + + // Bugs surfaced by the multi-modal bughunt (12 finder lenses × 3 rounds). + // Each todo asserts the spec-correct result. + describe("bughunt findings", () => { + test.todo("NUL byte (U+0000) is not c-printable — should error, not truncate", () => { + // [1] c-printable excludes NUL. Currently NUL is the EOF sentinel, so + // input is silently truncated. Data loss / security-adjacent. + expect(() => YAML.parse("a: 1\x00b: 2")).toThrow(); + expect(() => YAML.parse("key: foo\x00bar")).toThrow(); + }); + + test.todo("C0/C1/DEL control characters are not c-printable — should error", () => { + // [1] c-printable: x09, x0A, x0D, x20-x7E, x85, xA0-D7FF, E000-FFFD, + // 10000-10FFFF. Currently x01-x08/x0B/x0C/x0E-x1F/x7F/x80-x84/x86-x9F + // are accepted as scalar content. + expect(() => YAML.parse("a\x01b")).toThrow(); + expect(() => YAML.parse("a\x7Fb")).toThrow(); + expect(() => YAML.parse("a\x80b")).toThrow(); + }); + + test.todo("CRLF in quoted scalars folds as one line break (→ space)", () => { + // [73] b-l-folded: a single break folds to a space. Currently `\r\n` + // in quoted scalars produces `\n` instead. + expect(YAML.parse('"a\r\nb"')).toBe("a b"); + expect(YAML.parse("'a\r\nb'")).toBe("a b"); + }); + + test.todo("verbatim/named-handle tags resolve as Core-schema types", () => { + // [10.2] tag:yaml.org,2002:int via `!<...>` or `%TAG !y! ...` should + // resolve identically to `!!int`. Currently only the `!!` shorthand + // resolves. + expect(YAML.parse("! 42")).toBe(42); + expect(YAML.parse("%TAG !y! tag:yaml.org,2002:\n---\n!y!int 42")).toBe(42); + }); + + test.todo("explicit tag on quoted scalar coerces ([10.1.1.8] resolve via tag)", () => { + expect(YAML.parse("!!bool 'true'")).toBe(true); + expect(YAML.parse('!!int "42"')).toBe(42); + }); + + test.todo("`!!int`/`!!float` validate their content", () => { + // [10.2.1.2]/[10.2.1.4] — the tag's regex must match. + expect(() => YAML.parse("!!int 1.5")).toThrow(); + expect(() => YAML.parse("!!float 0x1f")).toThrow(); + }); + + test.todo("`\\uXXXX` surrogate pairs combine ([57] ns-esc-16-bit)", () => { + // js-yaml/eemeli combine surrogate halves to the supplementary code + // point. Currently rejected. + expect(YAML.parse('"\\uD834\\uDD1E"')).toBe("𝄞"); + }); + + test.todo("s-separate required after tag ([97] c-ns-tag-property)", () => { + // No whitespace between tag and content. + expect(() => YAML.parse("!b")).toThrow(); + expect(() => YAML.parse("!tag,x a")).toThrow(); + }); + + test.todo("`%YAML`/`%TAG` directive validation", () => { + // [86]/[88] require arguments; [87] requires major version 1; + // [89] forbids duplicate handle in same document. + expect(() => YAML.parse("%YAML\n---\nfoo")).toThrow(); + expect(() => YAML.parse("%YAML 2.0\n---\nfoo")).toThrow(); + expect(() => YAML.parse("%TAG !e! tag:a:\n%TAG !e! tag:b:\n---\nfoo")).toThrow(); + }); + + test.todo("§7.4.2 1024-char implicit-key limit enforced", () => { + const long = Buffer.alloc(1025, "a").toString(); + expect(() => YAML.parse(`${long}: v`)).toThrow(); + expect(() => YAML.parse(`[${long}: v]`)).toThrow(); + }); + + test.todo("error message includes line:col position", () => { + // ParseResultError carries Pos; the JS binding discards it. + expect(() => YAML.parse("a: 1\nb:\n\tc: 2")).toThrow(/line\s*\d|:\d+:\d+/); + }); + + test.todo("`<<:` merge preserves source property order", () => { + const r: any = YAML.parse("x: &x\n a: 1\n b: 2\n c: 3\ny:\n <<: *x"); + expect(Object.keys(r.y)).toEqual(["a", "b", "c"]); + }); + + test.todo("alias with property on prior line is rejected ([104] c-ns-alias-node)", () => { + // An alias node has no properties; a tag/anchor on a prior line + // belongs to a different node. + expect(() => YAML.parse("- &a 1\n- !!str\n *a")).toThrow(); + }); + + test.todo("block-scalar header rejects whitespace before chomp/indent indicator", () => { + // [162] c-b-block-header: no s-separate between `|`/`>` and indicators. + expect(() => YAML.parse("| 1\n text")).toThrow(); }); });