From cfe2eee7e638f3247601a72452d64553a42e7b98 Mon Sep 17 00:00:00 2001 From: Rayan Salhab Date: Mon, 27 Apr 2026 12:12:30 +0300 Subject: [PATCH] Preserve empty TOML arrays in tables (#2686) Co-authored-by: cyphercodes --- pkg/yqlib/encoder_toml.go | 49 +++++++++++---------------------- pkg/yqlib/toml_test.go | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 33 deletions(-) diff --git a/pkg/yqlib/encoder_toml.go b/pkg/yqlib/encoder_toml.go index 8c418cc2..12a6d828 100644 --- a/pkg/yqlib/encoder_toml.go +++ b/pkg/yqlib/encoder_toml.go @@ -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 { diff --git a/pkg/yqlib/toml_test.go b/pkg/yqlib/toml_test.go index 4e75371c..2a54d331 100644 --- a/pkg/yqlib/toml_test.go +++ b/pkg/yqlib/toml_test.go @@ -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,