Preserve empty TOML arrays in tables (#2686)

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
This commit is contained in:
Rayan Salhab 2026-04-27 12:12:30 +03:00 committed by GitHub
parent 1a433d1035
commit cfe2eee7e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 74 additions and 33 deletions

View File

@ -195,6 +195,18 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
}
}
func isTomlArrayOfTables(seq *CandidateNode) bool {
if len(seq.Content) == 0 {
return false
}
for _, it := range seq.Content {
if it.Kind != MappingNode {
return false
}
}
return true
}
func (te *tomlEncoder) writeAttribute(w io.Writer, key string, value *CandidateNode) error {
te.wroteRootAttr = true // Mark that we wrote a root attribute
@ -462,15 +474,7 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
break
}
if v.Kind == SequenceNode {
// Check if it's NOT an array of tables
allMaps := true
for _, it := range v.Content {
if it.Kind != MappingNode {
allMaps = false
break
}
}
if !allMaps {
if !isTomlArrayOfTables(v) {
hasAttrs = true
break
}
@ -500,14 +504,7 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
}
case SequenceNode:
// If sequence of maps, emit [[path.k]] per element
allMaps := true
for _, it := range v.Content {
if it.Kind != MappingNode {
allMaps = false
break
}
}
if allMaps {
if isTomlArrayOfTables(v) {
key := tomlDottedKey(append(append([]string{}, path...), k))
for _, it := range v.Content {
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
@ -545,14 +542,7 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
return err
}
case SequenceNode:
allMaps := true
for _, it := range v.Content {
if it.Kind != MappingNode {
allMaps = false
break
}
}
if !allMaps {
if !isTomlArrayOfTables(v) {
if err := te.writeArrayAttribute(w, k, v); err != nil {
return err
}
@ -565,14 +555,7 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
k := m.Content[i].Value
v := m.Content[i+1]
if v.Kind == SequenceNode {
allMaps := true
for _, it := range v.Content {
if it.Kind != MappingNode {
allMaps = false
break
}
}
if allMaps {
if isTomlArrayOfTables(v) {
dotted := tomlDottedKey(append(append([]string{}, path...), k))
for _, it := range v.Content {
if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil {

View File

@ -209,6 +209,34 @@ address = "12 cat st"
var rtEmptyArray = `A = []
`
var rtEmptyArrayInTable = `[features]
my-feature = []
`
var rtMixedEmptyArraysInTable = `[features]
my-other-feature = []
my-feature = ["my-other-feature"]
`
var yamlEmptyArrayInTable = `features:
my-feature: []
`
var expectedTomlEmptyArrayInTable = `[features]
my-feature = []
`
var yamlMixedEmptyArraysInTable = `features:
my-other-feature: []
my-feature:
- my-other-feature
`
var expectedTomlMixedEmptyArraysInTable = `[features]
my-other-feature = []
my-feature = ["my-other-feature"]
`
var rtSampleTable = `var = "x"
[owner.contact]
@ -563,6 +591,36 @@ var tomlScenarios = []formatScenario{
expected: rtEmptyArray,
scenarioType: "roundtrip",
},
{
skipDoc: true,
description: "Issue #2674: roundtrip empty array in table",
input: rtEmptyArrayInTable,
expression: ".",
expected: rtEmptyArrayInTable,
scenarioType: "roundtrip",
},
{
skipDoc: true,
description: "Issue #2674: roundtrip mixed empty and non-empty arrays in table",
input: rtMixedEmptyArraysInTable,
expression: ".",
expected: rtMixedEmptyArraysInTable,
scenarioType: "roundtrip",
},
{
skipDoc: true,
description: "Issue #2674: encode empty array in table",
input: yamlEmptyArrayInTable,
expected: expectedTomlEmptyArrayInTable,
scenarioType: "encode",
},
{
skipDoc: true,
description: "Issue #2674: encode mixed empty and non-empty arrays in table",
input: yamlMixedEmptyArraysInTable,
expected: expectedTomlMixedEmptyArraysInTable,
scenarioType: "encode",
},
{
description: "Roundtrip: sample table",
input: rtSampleTable,