feat: allow newlines and trailing commas in inline tables

TOML v1.1.0 relaxes inline table syntax to allow newlines, comments,
and trailing commas, matching the existing behavior of arrays.
This commit is contained in:
João Fernandes
2026-02-11 11:10:20 +00:00
parent dd7970eb93
commit 517ceb4eb8
4 changed files with 213 additions and 36 deletions
+4 -23
View File
@@ -743,25 +743,8 @@ func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_Empty3(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_Linebreak1(t *testing.T) { // TestTOMLTest_Invalid_Tests_Invalid_InlineTable_Linebreak1 through Linebreak4
input := "# No newlines are allowed between the curly braces unless they are valid within\n# a value.\nsimple = { a = 1 \n}\n" // are removed because TOML v1.1.0 allows newlines in inline tables.
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_Linebreak2(t *testing.T) {
input := "t = {a=1,\nb=2}\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_Linebreak3(t *testing.T) {
input := "t = {a=1\n,b=2}\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_Linebreak4(t *testing.T) {
input := "json_like = {\n first = \"Tom\",\n last = \"Preston-Werner\"\n}\n"
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_NoClose1(t *testing.T) { func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_NoClose1(t *testing.T) {
input := "a={\n" input := "a={\n"
@@ -833,10 +816,8 @@ func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_Overwrite10(t *testing.T) {
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_TrailingComma(t *testing.T) { // TestTOMLTest_Invalid_Tests_Invalid_InlineTable_TrailingComma is removed
input := "# A terminating comma (also called trailing comma) is not permitted after the\n# last key/value pair in an inline table\nabc = { abc = 123, }\n" // because TOML v1.1.0 allows trailing commas in inline tables.
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Tests_Invalid_Integer_CapitalBin(t *testing.T) { func TestTOMLTest_Invalid_Tests_Invalid_Integer_CapitalBin(t *testing.T) {
input := "capital-bin = 0B0\n" input := "capital-bin = 0B0\n"
+81
View File
@@ -1094,6 +1094,87 @@ B = "data"`,
} }
}, },
}, },
{
desc: "multiline inline table",
input: "Name = {\n First = \"hello\",\n Last = \"world\"\n}",
gen: func() test {
type name struct {
First string
Last string
}
type doc struct {
Name name
}
return test{
target: &doc{},
expected: &doc{Name: name{
First: "hello",
Last: "world",
}},
}
},
},
{
desc: "inline table with trailing comma",
input: `Name = {First = "hello", Last = "world",}`,
gen: func() test {
type name struct {
First string
Last string
}
type doc struct {
Name name
}
return test{
target: &doc{},
expected: &doc{Name: name{
First: "hello",
Last: "world",
}},
}
},
},
{
desc: "multiline inline table with trailing comma and comments",
input: "Name = {\n # first name\n First = \"hello\",\n # last name\n Last = \"world\",\n}",
gen: func() test {
type name struct {
First string
Last string
}
type doc struct {
Name name
}
return test{
target: &doc{},
expected: &doc{Name: name{
First: "hello",
Last: "world",
}},
}
},
},
{
desc: "nested multiline inline tables",
input: "A = {\n B = {\n C = 1,\n },\n}",
gen: func() test {
var v map[string]interface{}
return test{
target: &v,
expected: &map[string]interface{}{
"A": map[string]interface{}{
"B": map[string]interface{}{
"C": int64(1),
},
},
},
}
},
},
{ {
desc: "inline table inside array", desc: "inline table inside array",
input: `Names = [{First = "hello", Last = "world"}, {First = "ab", Last = "cd"}]`, input: `Names = [{First = "hello", Last = "world"}, {First = "ab", Last = "cd"}]`,
+47 -13
View File
@@ -460,12 +460,14 @@ func (p *Parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) {
return v, v[1 : len(v)-1], rest, nil return v, v[1 : len(v)-1], rest, nil
} }
//nolint:funlen,cyclop
func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) { func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
// inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close // inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
// inline-table-open = %x7B ws ; { // inline-table-open = %x7B ws ; {
// inline-table-close = ws %x7D ; } // inline-table-close = ws %x7D ; }
// inline-table-sep = ws %x2C ws ; , Comma // inline-table-sep = ws %x2C ws ; , Comma
// inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ] // inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
tableStart := b
parent := p.builder.Push(Node{ parent := p.builder.Push(Node{
Kind: InlineTable, Kind: InlineTable,
Raw: p.rangeOfToken(b[:1], b[1:]), Raw: p.rangeOfToken(b[:1], b[1:]),
@@ -473,45 +475,77 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
first := true first := true
var child reference lastChild := invalidReference
addChild := func(ref reference) {
if lastChild == invalidReference {
p.builder.AttachChild(parent, ref)
} else {
p.builder.Chain(lastChild, ref)
}
lastChild = ref
}
b = b[1:] b = b[1:]
var err error var err error
for len(b) > 0 { for len(b) > 0 {
previousB := b var cref reference
b = p.parseWhitespace(b) cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return parent, nil, err
}
if cref != invalidReference {
addChild(cref)
}
if len(b) == 0 { if len(b) == 0 {
return parent, nil, NewParserError(previousB[:1], "inline table is incomplete") return parent, nil, NewParserError(tableStart[:1], "inline table is incomplete")
} }
if b[0] == '}' { if b[0] == '}' {
break break
} }
if !first { if b[0] == ',' {
b, err = expect(',', b) if first {
return parent, nil, NewParserError(b[0:1], "inline table cannot start with comma")
}
b = b[1:]
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil { if err != nil {
return parent, nil, err return parent, nil, err
} }
b = p.parseWhitespace(b) if cref != invalidReference {
addChild(cref)
}
} else if !first {
return parent, nil, NewParserError(b[0:1], "inline table entries must be separated by commas")
}
// trailing comma: if '}' follows, stop
if len(b) > 0 && b[0] == '}' {
break
} }
var kv reference var kv reference
kv, b, err = p.parseKeyval(b) kv, b, err = p.parseKeyval(b)
if err != nil { if err != nil {
return parent, nil, err return parent, nil, err
} }
if first { addChild(kv)
p.builder.AttachChild(parent, kv)
} else { cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
p.builder.Chain(child, kv) if err != nil {
return parent, nil, err
}
if cref != invalidReference {
addChild(cref)
} }
child = kv
first = false first = false
} }
+81
View File
@@ -331,6 +331,87 @@ func TestParser_AST(t *testing.T) {
}, },
}, },
}, },
{
desc: "multiline inline table",
input: "name = {\n first = \"Tom\",\n last = \"Preston-Werner\"\n}",
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: InlineTable,
Children: []astNode{
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Tom`)},
{Kind: Key, Data: []byte(`first`)},
},
},
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Preston-Werner`)},
{Kind: Key, Data: []byte(`last`)},
},
},
},
},
{
Kind: Key,
Data: []byte(`name`),
},
},
},
},
{
desc: "inline table with trailing comma",
input: `name = { first = "Tom", last = "Preston-Werner", }`,
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: InlineTable,
Children: []astNode{
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Tom`)},
{Kind: Key, Data: []byte(`first`)},
},
},
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Preston-Werner`)},
{Kind: Key, Data: []byte(`last`)},
},
},
},
},
{
Kind: Key,
Data: []byte(`name`),
},
},
},
},
{
desc: "empty inline table with newline",
input: "name = {\n}",
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: InlineTable,
Children: nil,
},
{
Kind: Key,
Data: []byte(`name`),
},
},
},
},
} }
for _, e := range examples { for _, e := range examples {