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)
}
func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_Linebreak1(t *testing.T) {
input := "# No newlines are allowed between the curly braces unless they are valid within\n# a value.\nsimple = { a = 1 \n}\n"
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)
}
// TestTOMLTest_Invalid_Tests_Invalid_InlineTable_Linebreak1 through Linebreak4
// are removed because TOML v1.1.0 allows newlines in inline tables.
func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_NoClose1(t *testing.T) {
input := "a={\n"
@@ -833,10 +816,8 @@ func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_Overwrite10(t *testing.T) {
testgenInvalid(t, input)
}
func TestTOMLTest_Invalid_Tests_Invalid_InlineTable_TrailingComma(t *testing.T) {
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"
testgenInvalid(t, input)
}
// TestTOMLTest_Invalid_Tests_Invalid_InlineTable_TrailingComma is removed
// because TOML v1.1.0 allows trailing commas in inline tables.
func TestTOMLTest_Invalid_Tests_Invalid_Integer_CapitalBin(t *testing.T) {
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",
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
}
//nolint:funlen,cyclop
func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
// inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
// inline-table-open = %x7B ws ; {
// inline-table-close = ws %x7D ; }
// inline-table-sep = ws %x2C ws ; , Comma
// inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
tableStart := b
parent := p.builder.Push(Node{
Kind: InlineTable,
Raw: p.rangeOfToken(b[:1], b[1:]),
@@ -473,45 +475,77 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
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:]
var err error
for len(b) > 0 {
previousB := b
b = p.parseWhitespace(b)
var cref reference
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return parent, nil, err
}
if cref != invalidReference {
addChild(cref)
}
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] == '}' {
break
}
if !first {
b, err = expect(',', b)
if b[0] == ',' {
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 {
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
kv, b, err = p.parseKeyval(b)
if err != nil {
return parent, nil, err
}
if first {
p.builder.AttachChild(parent, kv)
} else {
p.builder.Chain(child, kv)
addChild(kv)
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return parent, nil, err
}
if cref != invalidReference {
addChild(cref)
}
child = kv
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 {