Fix TOML roundtrip: use TomlInline flag instead of FlowStyle to preserve inline tables

FlowStyle affected YAML decode output (causing inline tables to appear as
YAML flow mappings). Replace it with a new TOML-specific TomlInline bool
on CandidateNode that:
- Is set by the TOML decoder for inline tables (not FlowStyle)
- Is copied by UpdateAttributesFrom so it survives DeeplyAssign merges
- Is checked by the TOML encoder alongside FlowStyle (for YAML flow maps)
- Has no effect on the YAML encoder, preserving existing TOML→YAML output

TOML roundtrip tests are restored to their original expected values (inline
tables stay inline, table sections stay as sections).

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/f59bdf62-6d16-4664-991b-38eb87c9d81c

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-08 07:06:48 +00:00 committed by GitHub
parent b99f4174ec
commit d1ffec12e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 21 additions and 54 deletions

View File

@ -100,6 +100,9 @@ type CandidateNode struct {
// For formats like HCL and TOML: indicates that child entries should be emitted as separate blocks/tables // For formats like HCL and TOML: indicates that child entries should be emitted as separate blocks/tables
// rather than consolidated into nested mappings (default behaviour) // rather than consolidated into nested mappings (default behaviour)
EncodeSeparate bool EncodeSeparate bool
// For TOML: indicates that this mapping was originally a TOML inline table and should be
// re-encoded as an inline table rather than a separate table section.
TomlInline bool
} }
func (n *CandidateNode) CreateChild() *CandidateNode { func (n *CandidateNode) CreateChild() *CandidateNode {
@ -412,6 +415,7 @@ func (n *CandidateNode) doCopy(cloneContent bool) *CandidateNode {
IsMapKey: n.IsMapKey, IsMapKey: n.IsMapKey,
EncodeSeparate: n.EncodeSeparate, EncodeSeparate: n.EncodeSeparate,
TomlInline: n.TomlInline,
} }
if cloneContent { if cloneContent {
@ -467,6 +471,8 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP
// Preserve EncodeSeparate flag for format-specific encoding hints // Preserve EncodeSeparate flag for format-specific encoding hints
n.EncodeSeparate = other.EncodeSeparate n.EncodeSeparate = other.EncodeSeparate
// Preserve TomlInline flag for TOML inline table round-trips
n.TomlInline = other.TomlInline
// merge will pickup the style of the new thing // merge will pickup the style of the new thing
// when autocreating nodes // when autocreating nodes

View File

@ -150,10 +150,10 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod
} }
return &CandidateNode{ return &CandidateNode{
Kind: MappingNode, Kind: MappingNode,
Tag: "!!map", Tag: "!!map",
Style: FlowStyle, TomlInline: true,
Content: content, Content: content,
}, nil }, nil
} }

View File

@ -153,9 +153,7 @@ yq '.' sample.toml
``` ```
will output will output
```yaml ```yaml
[name] name = { first = "Tom", last = "Preston-Werner" }
first = "Tom"
last = "Preston-Werner"
``` ```
## Roundtrip: table section ## Roundtrip: table section
@ -374,10 +372,7 @@ dob = 1979-05-27T07:32:00-08:00
enabled = true enabled = true
ports = [8000, 8001, 8002] ports = [8000, 8001, 8002]
data = [["delta", "phi"], [3.14]] data = [["delta", "phi"], [3.14]]
temp_targets = { cpu = 79.5, case = 72.0 }
[database.temp_targets]
cpu = 79.5
case = 72.0
# [servers] yq can't do this one yet # [servers] yq can't do this one yet
[servers.alpha] [servers.alpha]

View File

@ -162,10 +162,9 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
// Regular array attribute // Regular array attribute
return te.writeArrayAttribute(w, path[len(path)-1], node) return te.writeArrayAttribute(w, path[len(path)-1], node)
case MappingNode: case MappingNode:
// Use inline table syntax only for nodes explicitly marked with flow/inline style // Use inline table syntax for nodes explicitly marked as TOML inline tables
// (e.g. TOML inline tables or YAML flow mappings). All other mappings become // or YAML flow mappings. All other mappings become readable TOML table sections.
// readable TOML table sections. if node.TomlInline || node.Style&FlowStyle != 0 {
if node.Style&FlowStyle != 0 {
return te.writeInlineTableAttribute(w, path[len(path)-1], node) return te.writeInlineTableAttribute(w, path[len(path)-1], node)
} }
return te.encodeSeparateMapping(w, path, node) return te.encodeSeparateMapping(w, path, node)
@ -428,7 +427,7 @@ func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *Candidate
// It emits the table header for this mapping if it has any content, then processes children. // It emits the table header for this mapping if it has any content, then processes children.
func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *CandidateNode) error { func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *CandidateNode) error {
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes). // Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes).
// Flow-style (inline) mapping children also count as attributes since they render as key = { ... }. // TomlInline mapping children also count as attributes since they render as key = { ... }.
hasAttrs := false hasAttrs := false
for i := 0; i < len(m.Content); i += 2 { for i := 0; i < len(m.Content); i += 2 {
v := m.Content[i+1] v := m.Content[i+1]
@ -436,7 +435,7 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
hasAttrs = true hasAttrs = true
break break
} }
if v.Kind == MappingNode && v.Style&FlowStyle != 0 { if v.Kind == MappingNode && (v.TomlInline || v.Style&FlowStyle != 0) {
hasAttrs = true hasAttrs = true
break break
} }
@ -598,13 +597,13 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
} }
} }
// Finally, child mappings: flow-style (inline) ones become inline table attributes, // Finally, child mappings: TomlInline or flow-style ones become inline table attributes,
// while all others are emitted as separate sub-table sections. // while all others are emitted as separate sub-table sections.
for i := 0; i < len(m.Content); i += 2 { for i := 0; i < len(m.Content); i += 2 {
k := m.Content[i].Value k := m.Content[i].Value
v := m.Content[i+1] v := m.Content[i+1]
if v.Kind == MappingNode { if v.Kind == MappingNode {
if v.Style&FlowStyle != 0 { if v.TomlInline || v.Style&FlowStyle != 0 {
if err := te.writeInlineTableAttribute(w, k, v); err != nil { if err := te.writeInlineTableAttribute(w, k, v); err != nil {
return err return err
} }

View File

@ -182,12 +182,6 @@ var expectedSampleWithHeader = `servers:
var rtInlineTableAttr = `name = { first = "Tom", last = "Preston-Werner" } var rtInlineTableAttr = `name = { first = "Tom", last = "Preston-Werner" }
` `
// Inline tables are converted to readable table sections on encode
var rtInlineTableAttrEncoded = `[name]
first = "Tom"
last = "Preston-Werner"
`
var rtTableSection = `[owner.contact] var rtTableSection = `[owner.contact]
name = "Tom" name = "Tom"
age = 36 age = 36
@ -269,33 +263,6 @@ ip = "10.0.0.2"
role = "backend" role = "backend"
` `
// Inline table temp_targets is expanded to a readable sub-table when re-encoding
var sampleFromWebEncoded = `# This is a TOML document
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
enabled = true
ports = [8000, 8001, 8002]
data = [["delta", "phi"], [3.14]]
[database.temp_targets]
cpu = 79.5
case = 72.0
# [servers] yq can't do this one yet
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
[servers.beta]
ip = "10.0.0.2"
role = "backend"
`
var subArrays = ` var subArrays = `
[[array]] [[array]]
@ -539,7 +506,7 @@ var tomlScenarios = []formatScenario{
description: "Roundtrip: inline table attribute", description: "Roundtrip: inline table attribute",
input: rtInlineTableAttr, input: rtInlineTableAttr,
expression: ".", expression: ".",
expected: rtInlineTableAttrEncoded, expected: rtInlineTableAttr,
scenarioType: "roundtrip", scenarioType: "roundtrip",
}, },
{ {
@ -638,7 +605,7 @@ var tomlScenarios = []formatScenario{
description: "Roundtrip: sample from web", description: "Roundtrip: sample from web",
input: sampleFromWeb, input: sampleFromWeb,
expression: ".", expression: ".",
expected: sampleFromWebEncoded, expected: sampleFromWeb,
scenarioType: "roundtrip", scenarioType: "roundtrip",
}, },
{ {