diff --git a/src/parsers/toml.rs b/src/parsers/toml.rs index 94872ac93e43..47532f34f660 100644 --- a/src/parsers/toml.rs +++ b/src/parsers/toml.rs @@ -268,6 +268,7 @@ impl<'a> TOML<'a> { } pub fn parse_assignment(&mut self, obj: &mut E::Object, bump: &'a Bump) -> crate::Result<()> { + let outer_allow_double_bracket = self.lexer.allow_double_bracket; self.lexer.allow_double_bracket = false; let rope = self.parse_key(bump)?; let rope_end = self.lexer.start; @@ -278,6 +279,7 @@ impl<'a> TOML<'a> { } self.lexer.expect_assignment()?; + self.lexer.allow_double_bracket = outer_allow_double_bracket; if !is_array { let value = self.parse_value()?; match obj.set_rope(rope, self.bump, value) { @@ -306,7 +308,7 @@ impl<'a> TOML<'a> { } } } - self.lexer.allow_double_bracket = true; + self.lexer.allow_double_bracket = outer_allow_double_bracket; Ok(()) } @@ -324,7 +326,10 @@ impl<'a> TOML<'a> { fn parse_value_inner(&mut self) -> crate::Result { let loc = self.lexer.loc(); - self.lexer.allow_double_bracket = true; + // `[[`/`]]` merge into header tokens only at top level. Inside a value + // the flag is held false; the caller's value is restored before lexing + // the token after this value so a following `[[header]]` is recognised. + let outer_allow_double_bracket = self.lexer.allow_double_bracket; match self.lexer.token { T::t_false => { @@ -367,6 +372,7 @@ impl<'a> TOML<'a> { Ok(self.e(E::Number::new(value), loc)) } T::t_open_brace => { + self.lexer.allow_double_bracket = false; self.lexer.next()?; let mut is_single_line = !self.lexer.has_newline_before; let key_allocator = self.bump; @@ -396,23 +402,22 @@ impl<'a> TOML<'a> { unsafe { self.parse_assignment(&mut *obj, key_allocator)?; } - self.lexer.allow_double_bracket = false; } if self.lexer.has_newline_before { is_single_line = false; } let _ = is_single_line; - self.lexer.allow_double_bracket = true; + self.lexer.allow_double_bracket = outer_allow_double_bracket; self.lexer.expect(T::t_close_brace)?; Ok(expr) } T::t_empty_array => { self.lexer.next()?; - self.lexer.allow_double_bracket = true; Ok(self.e(E::Array::default(), loc)) } T::t_open_bracket => { + self.lexer.allow_double_bracket = false; self.lexer.next()?; let mut is_single_line = !self.lexer.has_newline_before; let array_ = self.e(E::Array::default(), loc); @@ -424,7 +429,6 @@ impl<'a> TOML<'a> { // SAFETY: `array` aliases into `array_.data`; the raw pointer // sidesteps overlapping &mut on `array_`. let bump = self.bump; - self.lexer.allow_double_bracket = false; while self.lexer.token != T::t_close_bracket { // SAFETY: `array` points into the AST store and is live here. @@ -453,7 +457,7 @@ impl<'a> TOML<'a> { is_single_line = false; } let _ = is_single_line; - self.lexer.allow_double_bracket = true; + self.lexer.allow_double_bracket = outer_allow_double_bracket; self.lexer.expect(T::t_close_bracket)?; Ok(array_) } diff --git a/test/js/bun/resolve/toml/toml-parse.test.ts b/test/js/bun/resolve/toml/toml-parse.test.ts index 8ebf533e488b..6f45e28651cb 100644 --- a/test/js/bun/resolve/toml/toml-parse.test.ts +++ b/test/js/bun/resolve/toml/toml-parse.test.ts @@ -117,3 +117,46 @@ test("Bun.TOML.parse rejects array values without comma separators (#31252)", () // Trailing comma is legal TOML. expect(Bun.TOML.parse("a = [1, 2,]")).toEqual({ a: [1, 2] }); }); + +// The TOML lexer has an `allow_double_bracket` mode flag so `[[` / `]]` are +// merged into a single array-of-tables header token. `parse_value_inner` set +// the flag to `true` unconditionally on entry and again after every compound +// value closed, so inside `a = [[1]]` the adjacent `]]` (and at depth >=3 the +// adjacent `[[`) were merged into header tokens and the parse failed. The same +// document with spaces between the brackets (`a = [ [1] ]`) parsed fine. The +// fix saves the caller's flag on entry, keeps it `false` while inside an +// array or inline table, and restores the saved value only for the token +// lexed after the outermost closing delimiter. +test("Bun.TOML.parse accepts nested array literals with adjacent brackets", () => { + expect(Bun.TOML.parse("a = [[1]]")).toEqual({ a: [[1]] }); + expect(Bun.TOML.parse("a = [[1], [2]]")).toEqual({ a: [[1], [2]] }); + expect(Bun.TOML.parse('a = [["x"], ["y"]]')).toEqual({ a: [["x"], ["y"]] }); + expect(Bun.TOML.parse("a = [[[1]]]")).toEqual({ a: [[[1]]] }); + expect(Bun.TOML.parse("a = [[1, 2], [3, 4]]")).toEqual({ + a: [ + [1, 2], + [3, 4], + ], + }); + expect(Bun.TOML.parse("a = [[]]")).toEqual({ a: [[]] }); + expect(Bun.TOML.parse("a = [[], []]")).toEqual({ a: [[], []] }); + expect(Bun.TOML.parse("a = [[[1], [2]], [[3]]]")).toEqual({ a: [[[1], [2]], [[3]]] }); + expect(Bun.TOML.parse("t = {a = [[1]]}")).toEqual({ t: { a: [[1]] } }); + expect(Bun.TOML.parse("a = [{b = [[1]]}]")).toEqual({ a: [{ b: [[1]] }] }); + // Whitespace-separated forms already parsed; they must keep working. + expect(Bun.TOML.parse("a = [ [1] ]")).toEqual({ a: [[1]] }); + expect(Bun.TOML.parse("a = [1, [2], 3]")).toEqual({ a: [1, [2], 3] }); +}); + +test("Bun.TOML.parse still merges [[ / ]] as array-of-tables headers after a value", () => { + // The flag restore must re-enable header merging once the top-level value is + // consumed; otherwise `[[t]]` on the next line would be lexed as two `[` and + // misparsed as a `[table]` header. Every value shape exercises a different + // restore point. + expect(Bun.TOML.parse("a = 1\n[[t]]\nb = 2\n")).toEqual({ a: 1, t: [{ b: 2 }] }); + expect(Bun.TOML.parse("a = [1]\n[[t]]\nb = 2\n")).toEqual({ a: [1], t: [{ b: 2 }] }); + expect(Bun.TOML.parse("a = []\n[[t]]\nb = 2\n")).toEqual({ a: [], t: [{ b: 2 }] }); + expect(Bun.TOML.parse("a = {b = 1}\n[[t]]\nc = 2\n")).toEqual({ a: { b: 1 }, t: [{ c: 2 }] }); + expect(Bun.TOML.parse("a = [[1]]\n[[t]]\nb = 2\n")).toEqual({ a: [[1]], t: [{ b: 2 }] }); + expect(Bun.TOML.parse("[[t]]\na = [[1]]\n[[t]]\nb = 2\n")).toEqual({ t: [{ a: [[1]] }, { b: 2 }] }); +});