From b135c207ed0a7d599f458b40be307e7d7d6bc26c Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 16 Dec 2025 14:29:39 -0800 Subject: [PATCH] fix(yaml): remove YAML 1.1 legacy boolean values for YAML 1.2 compliance (#25537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Remove YAML 1.1 legacy boolean values (`yes/no/on/off/y/Y`) that are not part of the YAML 1.2 Core Schema - Keep YAML 1.2 Core Schema compliant values: `true/True/TRUE`, `false/False/FALSE`, `null/Null/NULL`, `0x` hex, `0o` octal - Add comprehensive roundtrip tests for YAML 1.2 compliance **Removed (now parsed as strings):** - `yes`, `Yes`, `YES` (were `true`) - `no`, `No`, `NO` (were `false`) - `on`, `On`, `ON` (were `true`) - `off`, `Off`, `OFF` (were `false`) - `y`, `Y` (were `true`) This fixes a common pain point where GitHub Actions workflow files with `on:` keys would have the key parsed as boolean `true` instead of the string `"on"`. ## YAML 1.2 Core Schema Specification From [YAML 1.2.2 Section 10.3.2 Tag Resolution](https://yaml.org/spec/1.2.2/#1032-tag-resolution): | Regular expression | Resolved to tag | |-------------------|-----------------| | `null \| Null \| NULL \| ~` | tag:yaml.org,2002:null | | `/* Empty */` | tag:yaml.org,2002:null | | `true \| True \| TRUE \| false \| False \| FALSE` | tag:yaml.org,2002:bool | | `[-+]? [0-9]+` | tag:yaml.org,2002:int (Base 10) | | `0o [0-7]+` | tag:yaml.org,2002:int (Base 8) | | `0x [0-9a-fA-F]+` | tag:yaml.org,2002:int (Base 16) | | `[-+]? ( \. [0-9]+ \| [0-9]+ ( \. [0-9]* )? ) ( [eE] [-+]? [0-9]+ )?` | tag:yaml.org,2002:float | | `[-+]? ( \.inf \| \.Inf \| \.INF )` | tag:yaml.org,2002:float (Infinity) | | `\.nan \| \.NaN \| \.NAN` | tag:yaml.org,2002:float (Not a number) | Note: `yes`, `no`, `on`, `off`, `y`, `n` are **not** in the YAML 1.2 Core Schema boolean list. These were removed from YAML 1.1 as noted in [YAML 1.2 Section 1.2](https://yaml.org/spec/1.2.2/#12-yaml-history): > The YAML 1.2 specification was published in 2009. Its primary focus was making YAML a strict superset of JSON. **It also removed many of the problematic implicit typing recommendations.** ## Test plan - [x] Updated existing YAML tests to reflect YAML 1.2 Core Schema behavior - [x] Added roundtrip tests (stringify → parse) for YAML 1.2 compliance - [x] Verified tests fail with system Bun (YAML 1.1 behavior) and pass with debug build (YAML 1.2) - [x] Run `bun bd test test/js/bun/yaml/yaml.test.ts` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude --- src/interchange/yaml.zig | 84 --------------- test/js/bun/yaml/yaml.test.ts | 192 ++++++++++++++++++++++++++++++++-- 2 files changed, 184 insertions(+), 92 deletions(-) diff --git a/src/interchange/yaml.zig b/src/interchange/yaml.zig index 048fc86d53..9f38ec1050 100644 --- a/src/interchange/yaml.zig +++ b/src/interchange/yaml.zig @@ -2471,11 +2471,6 @@ pub fn Parser(comptime enc: Encoding) type { self.inc(3); continue :next self.next(); } - if (self.remainStartsWithChar('o')) { - try ctx.resolve(.{ .boolean = false }, n_start, "no"); - self.inc(1); - continue :next self.next(); - } try ctx.appendSource(c, n_start); continue :next self.next(); }, @@ -2492,16 +2487,6 @@ pub fn Parser(comptime enc: Encoding) type { self.inc(3); continue :next self.next(); } - if (self.remainStartsWithChar('o')) { - try ctx.resolve(.{ .boolean = false }, n_start, "No"); - self.inc(1); - continue :next self.next(); - } - if (self.remainStartsWithChar('O')) { - try ctx.resolve(.{ .boolean = false }, n_start, "NO"); - self.inc(1); - continue :next self.next(); - } try ctx.appendSource(c, n_start); continue :next self.next(); }, @@ -2538,75 +2523,6 @@ pub fn Parser(comptime enc: Encoding) type { try ctx.appendSource(c, t_start); continue :next self.next(); }, - 'y' => { - const y_start = self.pos; - self.inc(1); - if (self.remainStartsWith("es")) { - try ctx.resolve(.{ .boolean = true }, y_start, "yes"); - self.inc(2); - continue :next self.next(); - } - try ctx.appendSource(c, y_start); - continue :next self.next(); - }, - 'Y' => { - const y_start = self.pos; - self.inc(1); - if (self.remainStartsWith("es")) { - try ctx.resolve(.{ .boolean = true }, y_start, "Yes"); - self.inc(2); - continue :next self.next(); - } - if (self.remainStartsWith("ES")) { - try ctx.resolve(.{ .boolean = true }, y_start, "YES"); - self.inc(2); - continue :next self.next(); - } - try ctx.appendSource(c, y_start); - continue :next self.next(); - }, - 'o' => { - const o_start = self.pos; - self.inc(1); - if (self.remainStartsWithChar('n')) { - try ctx.resolve(.{ .boolean = true }, o_start, "on"); - self.inc(1); - continue :next self.next(); - } - if (self.remainStartsWith("ff")) { - try ctx.resolve(.{ .boolean = false }, o_start, "off"); - self.inc(2); - continue :next self.next(); - } - try ctx.appendSource(c, o_start); - continue :next self.next(); - }, - 'O' => { - const o_start = self.pos; - self.inc(1); - if (self.remainStartsWithChar('n')) { - try ctx.resolve(.{ .boolean = true }, o_start, "On"); - self.inc(1); - continue :next self.next(); - } - if (self.remainStartsWithChar('N')) { - try ctx.resolve(.{ .boolean = true }, o_start, "ON"); - self.inc(1); - continue :next self.next(); - } - if (self.remainStartsWith("ff")) { - try ctx.resolve(.{ .boolean = false }, o_start, "Off"); - self.inc(2); - continue :next self.next(); - } - if (self.remainStartsWith("FF")) { - try ctx.resolve(.{ .boolean = false }, o_start, "OFF"); - self.inc(2); - continue :next self.next(); - } - try ctx.appendSource(c, o_start); - continue :next self.next(); - }, 'f' => { const f_start = self.pos; self.inc(1); diff --git a/test/js/bun/yaml/yaml.test.ts b/test/js/bun/yaml/yaml.test.ts index 3e56fac8c4..29813c003d 100644 --- a/test/js/bun/yaml/yaml.test.ts +++ b/test/js/bun/yaml/yaml.test.ts @@ -361,22 +361,41 @@ services: }); }); - test("parses null values", () => { + test("parses null values (YAML 1.2 Core Schema)", () => { + // YAML 1.2 Core Schema: null, Null, NULL, ~ and empty are null expect(YAML.parse("null")).toBe(null); + expect(YAML.parse("Null")).toBe(null); + expect(YAML.parse("NULL")).toBe(null); expect(YAML.parse("~")).toBe(null); expect(YAML.parse("")).toBe(null); }); - test("parses boolean values", () => { + test("parses boolean values (YAML 1.2 Core Schema)", () => { + // YAML 1.2 Core Schema: true, True, TRUE, false, False, FALSE are booleans expect(YAML.parse("true")).toBe(true); + expect(YAML.parse("True")).toBe(true); + expect(YAML.parse("TRUE")).toBe(true); expect(YAML.parse("false")).toBe(false); - expect(YAML.parse("yes")).toBe(true); - expect(YAML.parse("no")).toBe(false); - expect(YAML.parse("on")).toBe(true); - expect(YAML.parse("off")).toBe(false); + expect(YAML.parse("False")).toBe(false); + expect(YAML.parse("FALSE")).toBe(false); + // YAML 1.2: these YAML 1.1 legacy values are strings, not booleans + expect(YAML.parse("yes")).toBe("yes"); + expect(YAML.parse("no")).toBe("no"); + expect(YAML.parse("on")).toBe("on"); + expect(YAML.parse("off")).toBe("off"); + expect(YAML.parse("Yes")).toBe("Yes"); + expect(YAML.parse("No")).toBe("No"); + expect(YAML.parse("YES")).toBe("YES"); + expect(YAML.parse("NO")).toBe("NO"); + expect(YAML.parse("On")).toBe("On"); + expect(YAML.parse("Off")).toBe("Off"); + expect(YAML.parse("ON")).toBe("ON"); + expect(YAML.parse("OFF")).toBe("OFF"); + expect(YAML.parse("y")).toBe("y"); + expect(YAML.parse("n")).toBe("n"); }); - test("parses number values", () => { + test("parses number values (YAML 1.2 Core Schema)", () => { expect(YAML.parse("42")).toBe(42); expect(YAML.parse("3.14")).toBe(3.14); expect(YAML.parse("-17")).toBe(-17); @@ -384,6 +403,12 @@ services: expect(YAML.parse(".inf")).toBe(Infinity); expect(YAML.parse("-.inf")).toBe(-Infinity); expect(YAML.parse(".nan")).toBeNaN(); + // YAML 1.2 Core Schema: octal (0o) and hex (0x) are supported + expect(YAML.parse("0o777")).toBe(511); + expect(YAML.parse("0o10")).toBe(8); + expect(YAML.parse("0xFF")).toBe(255); + expect(YAML.parse("0x10")).toBe(16); + expect(YAML.parse("0xDEADBEEF")).toBe(0xdeadbeef); }); test("parses string values", () => { @@ -681,7 +706,7 @@ unicode: "\\u0041\\u0042\\u0043" }); }); - test("handles large numbers", () => { + test("handles large numbers (YAML 1.2 Core Schema)", () => { const yaml = ` int: 9007199254740991 float: 1.7976931348623157e+308 @@ -692,6 +717,7 @@ binary: 0b1010 const result = YAML.parse(yaml); expect(result.int).toBe(9007199254740991); expect(result.float).toBe(1.7976931348623157e308); + // YAML 1.2 Core Schema: hex (0x) is supported, binary (0b) is NOT expect(result.hex).toBe(255); expect(result.octal).toBe(511); expect(result.binary).toBe("0b1010"); @@ -2546,4 +2572,154 @@ refs: }); }); }); + + describe("roundtrip (stringify -> parse)", () => { + // Test that stringify -> parse produces deep equality for YAML 1.2 compliant values + + test("roundtrips booleans", () => { + expect(YAML.parse(YAML.stringify(true))).toBe(true); + expect(YAML.parse(YAML.stringify(false))).toBe(false); + }); + + test("roundtrips null", () => { + expect(YAML.parse(YAML.stringify(null))).toBe(null); + }); + + test("roundtrips numbers", () => { + const numbers = [0, 1, -1, 42, 3.14, -17.5, 1e10, 1.5e-10, Infinity, -Infinity]; + for (const n of numbers) { + expect(YAML.parse(YAML.stringify(n))).toBe(n); + } + expect(YAML.parse(YAML.stringify(NaN))).toBeNaN(); + }); + + test("roundtrips strings", () => { + const strings = [ + "hello", + "hello world", + "with\nnewline", + "with\ttab", + 'with "quotes"', + "with 'single quotes'", + // YAML 1.2: these YAML 1.1 legacy values are strings, should roundtrip + "yes", + "no", + "on", + "off", + "Yes", + "No", + "YES", + "NO", + "On", + "Off", + "ON", + "OFF", + "y", + "n", + // YAML 1.2 Core Schema: True/TRUE/False/FALSE/Null/NULL are special values, + // but when passed as strings to stringify, they should be quoted and roundtrip + "True", + "False", + "TRUE", + "FALSE", + "Null", + "NULL", + ]; + for (const s of strings) { + const roundtripped = YAML.parse(YAML.stringify(s)); + expect(roundtripped).toBe(s); + } + }); + + test("roundtrips arrays", () => { + const arrays = [ + [], + [1, 2, 3], + ["a", "b", "c"], + [true, false, null], + [1, "two", true, null], + [ + [1, 2], + [3, 4], + ], + // YAML 1.2: these YAML 1.1 legacy strings should survive roundtrip + ["yes", "no", "on", "off"], + // YAML 1.2: these are booleans/null when parsed, but stringify should quote string values + ["True", "False", "NULL"], + ]; + for (const arr of arrays) { + expect(YAML.parse(YAML.stringify(arr))).toEqual(arr); + } + }); + + test("roundtrips objects", () => { + const objects = [ + {}, + { a: 1, b: 2 }, + { name: "test", count: 42 }, + { nested: { deep: { value: true } } }, + { arr: [1, 2, 3], obj: { key: "value" } }, + // YAML 1.2: these YAML 1.1 legacy strings as values should survive roundtrip + { yes: "yes", no: "no", on: "on", off: "off" }, + // YAML 1.2: these are special values but stringify should quote string values + { True: "True", False: "False", NULL: "NULL" }, + ]; + for (const obj of objects) { + expect(YAML.parse(YAML.stringify(obj))).toEqual(obj); + } + }); + + test("roundtrips complex nested structures", () => { + const complex = { + users: [ + { name: "Alice", active: true, score: 100 }, + { name: "Bob", active: false, score: null }, + ], + settings: { + enabled: true, + count: 42, + values: [1, 2, 3], + }, + // YAML 1.2: these YAML 1.1 legacy strings should survive roundtrip + yaml11strings: { + yes: "yes", + no: "no", + on: "on", + off: "off", + // These are YAML 1.1 legacy - strings in YAML 1.2 + yes_variants: ["Yes", "YES"], + no_variants: ["No", "NO"], + on_variants: ["On", "ON"], + off_variants: ["Off", "OFF"], + // These are special in YAML 1.2 Core Schema but stringify quotes them + true_variants: ["True", "TRUE"], + false_variants: ["False", "FALSE"], + null_variants: ["Null", "NULL"], + }, + }; + expect(YAML.parse(YAML.stringify(complex))).toEqual(complex); + }); + + test("roundtrips GitHub Actions workflow keys", () => { + // This was a common pain point with YAML 1.1 - 'on' being parsed as true + const workflow = { + name: "CI", + on: { + push: { + branches: ["main"], + }, + pull_request: { + branches: ["main"], + }, + }, + jobs: { + build: { + "runs-on": "ubuntu-latest", + steps: [{ uses: "actions/checkout@v4" }, { run: "npm test" }], + }, + }, + }; + expect(YAML.parse(YAML.stringify(workflow))).toEqual(workflow); + }); + }); });