fix(yaml): remove YAML 1.1 legacy boolean values for YAML 1.2 compliance (#25537)

## 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 <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
robobun
2025-12-16 14:29:39 -08:00
committed by GitHub
parent a1dd26d7db
commit b135c207ed
2 changed files with 184 additions and 92 deletions

View File

@@ -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);

View File

@@ -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);
});
});
});