Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/parsers/toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -306,7 +308,7 @@ impl<'a> TOML<'a> {
}
}
}
self.lexer.allow_double_bracket = true;
self.lexer.allow_double_bracket = outer_allow_double_bracket;
Ok(())
}

Expand All @@ -324,7 +326,10 @@ impl<'a> TOML<'a> {
fn parse_value_inner(&mut self) -> crate::Result<Expr> {
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 => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand Down Expand Up @@ -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_)
}
Expand Down
43 changes: 43 additions & 0 deletions test/js/bun/resolve/toml/toml-parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }] });
});
Loading