This commit is contained in:
Jacob Heider 2026-07-04 19:17:48 -04:00 committed by GitHub
commit 9cf3f90788
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 114 additions and 13 deletions

View File

@ -157,9 +157,25 @@ func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNod
}, nil }, nil
} }
// commentIsOnNewLine reports whether comment begins on a line after the end of
// prev, by scanning the raw input bytes between them for a newline. When the
// range cannot be determined it returns true, preserving the historic behaviour
// of treating in-array comments as head comments for the next element.
func (dec *tomlDecoder) commentIsOnNewLine(prev, comment *toml.Node) bool {
prevEnd := prev.Raw.Offset + prev.Raw.Length
commentStart := comment.Raw.Offset
if commentStart < prevEnd {
return true
}
between := dec.parser.Raw(toml.Range{Offset: prevEnd, Length: commentStart - prevEnd})
return bytes.IndexByte(between, '\n') >= 0
}
func (dec *tomlDecoder) createArray(tomlNode *toml.Node) (*CandidateNode, error) { func (dec *tomlDecoder) createArray(tomlNode *toml.Node) (*CandidateNode, error) {
content := make([]*CandidateNode, 0) content := make([]*CandidateNode, 0)
var pendingArrayComments []string var pendingArrayComments []string
var prevElem *toml.Node
var prevYamlNode *CandidateNode
iterator := tomlNode.Children() iterator := tomlNode.Children()
for iterator.Next() { for iterator.Next() {
@ -167,8 +183,19 @@ func (dec *tomlDecoder) createArray(tomlNode *toml.Node) (*CandidateNode, error)
// Handle comments within arrays // Handle comments within arrays
if child.Kind == toml.Comment { if child.Kind == toml.Comment {
// A comment on the same line as the preceding element is a trailing
// (inline) comment on that element; a comment on its own line is a
// head comment for the next element.
if prevYamlNode != nil && !dec.commentIsOnNewLine(prevElem, child) {
if prevYamlNode.LineComment == "" {
prevYamlNode.LineComment = string(child.Data)
} else {
prevYamlNode.LineComment = prevYamlNode.LineComment + "\n" + string(child.Data)
}
} else {
// Collect comments to attach to the next array element // Collect comments to attach to the next array element
pendingArrayComments = append(pendingArrayComments, string(child.Data)) pendingArrayComments = append(pendingArrayComments, string(child.Data))
}
continue continue
} }
@ -184,6 +211,8 @@ func (dec *tomlDecoder) createArray(tomlNode *toml.Node) (*CandidateNode, error)
} }
content = append(content, yamlNode) content = append(content, yamlNode)
prevElem = child
prevYamlNode = yamlNode
} }
return &CandidateNode{ return &CandidateNode{

View File

@ -188,6 +188,10 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
te.wroteRootAttr = false te.wroteRootAttr = false
} }
for _, it := range node.Content { for _, it := range node.Content {
// Write head comment before the array-of-tables header
if err := te.writeComment(w, it.HeadComment); err != nil {
return err
}
if _, err := w.Write([]byte("[[" + quotedKey + "]]\n")); err != nil { if _, err := w.Write([]byte("[[" + quotedKey + "]]\n")); err != nil {
return err return err
} }
@ -283,10 +287,10 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
return err return err
} }
// Check if any array elements have head comments - if so, use multiline format // Check if any array elements have head or line comments - if so, use multiline format
hasElementComments := false hasElementComments := false
for _, it := range seq.Content { for _, it := range seq.Content {
if it.HeadComment != "" { if it.HeadComment != "" || it.LineComment != "" {
hasElementComments = true hasElementComments = true
break break
} }
@ -299,6 +303,13 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
} }
for i, it := range seq.Content { for i, it := range seq.Content {
// Separate a head-commented element from the previous one with a blank line
if i > 0 && it.HeadComment != "" {
if _, err := w.Write([]byte("\n")); err != nil {
return err
}
}
// Write head comment for this element // Write head comment for this element
if it.HeadComment != "" { if it.HeadComment != "" {
commentLines := strings.Split(it.HeadComment, "\n") commentLines := strings.Split(it.HeadComment, "\n")
@ -340,17 +351,19 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
// Always add trailing comma in multiline arrays // Always add trailing comma in multiline arrays
itemStr += "," itemStr += ","
if _, err := w.Write([]byte(" " + itemStr + "\n")); err != nil { // Append trailing (inline) comment on the same line, if present
return err if it.LineComment != "" {
lineComment := strings.TrimSpace(it.LineComment)
if !strings.HasPrefix(lineComment, "#") {
lineComment = "# " + lineComment
}
itemStr += " " + lineComment
} }
// Add blank line between elements (except after the last one) if _, err := w.Write([]byte(" " + itemStr + "\n")); err != nil {
if i < len(seq.Content)-1 {
if _, err := w.Write([]byte("\n")); err != nil {
return err return err
} }
} }
}
if _, err := w.Write([]byte("]\n")); err != nil { if _, err := w.Write([]byte("]\n")); err != nil {
return err return err
@ -546,6 +559,10 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
te.wroteRootAttr = false te.wroteRootAttr = false
} }
for _, it := range v.Content { for _, it := range v.Content {
// Write head comment before the array-of-tables header
if err := te.writeComment(w, it.HeadComment); err != nil {
return err
}
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil { if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
return err return err
} }
@ -603,6 +620,10 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
if isTomlArrayOfTables(v) { if isTomlArrayOfTables(v) {
dotted := tomlDottedKey(append(append([]string{}, path...), k)) dotted := tomlDottedKey(append(append([]string{}, path...), k))
for _, it := range v.Content { for _, it := range v.Content {
// Write head comment before the array-of-tables header
if err := te.writeComment(w, it.HeadComment); err != nil {
return err
}
if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil { if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil {
return err return err
} }

View File

@ -325,6 +325,33 @@ the_array = [
] ]
` `
// Bug: a head comment directly above an [[array-of-tables]] header is dropped
// on round-trip when other content precedes the array-of-tables.
var rtArrayTableHeadComment = `[package]
name = "x"
# the binary
[[bin]]
name = "y"
`
// Same bug via a nested array-of-tables ([[a.b]]) under a table with attributes.
var rtNestedArrayTableHeadComment = `[fruit]
name = "apple"
# a variety
[[fruit.variety]]
name = "red delicious"
`
// Bug: an inline (trailing) comment on an array item detaches from its item and
// floats onto its own line as a head comment on the next item, with a spurious
// blank line.
var rtArrayItemInlineComment = `default = [
"a", # why a
"b",
]
`
var expectedSubArrays = `array: var expectedSubArrays = `array:
- subarray: - subarray:
- subsubarray: - subsubarray:
@ -369,7 +396,7 @@ var tomlScenarios = []formatScenario{
skipDoc: true, skipDoc: true,
description: "blank", description: "blank",
input: `A = "hello`, input: `A = "hello`,
expectedError: `bad file 'sample.yml': basic string not terminated by "`, expectedError: `bad file 'sample.yml': unterminated basic string`,
scenarioType: "decode-error", scenarioType: "decode-error",
}, },
{ {
@ -700,6 +727,30 @@ var tomlScenarios = []formatScenario{
expected: tomlTableWithComments, expected: tomlTableWithComments,
scenarioType: "roundtrip", scenarioType: "roundtrip",
}, },
{
skipDoc: true,
description: "Roundtrip: head comment above array-of-tables",
input: rtArrayTableHeadComment,
expression: ".",
expected: rtArrayTableHeadComment,
scenarioType: "roundtrip",
},
{
skipDoc: true,
description: "Roundtrip: head comment above nested array-of-tables",
input: rtNestedArrayTableHeadComment,
expression: ".",
expected: rtNestedArrayTableHeadComment,
scenarioType: "roundtrip",
},
{
skipDoc: true,
description: "Roundtrip: inline comment on array item",
input: rtArrayItemInlineComment,
expression: ".",
expected: rtArrayItemInlineComment,
scenarioType: "roundtrip",
},
// Encode (YAML → TOML) scenarios - verify readable table sections are produced // Encode (YAML → TOML) scenarios - verify readable table sections are produced
{ {
description: "Encode: Simple mapping produces table section", description: "Encode: Simple mapping produces table section",