mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-03 19:05:38 +00:00
HCL improvements
This commit is contained in:
parent
f4fd8c585a
commit
d2d657eacc
@ -19,6 +19,11 @@ type hclEncoder struct {
|
|||||||
prefs HclPreferences
|
prefs HclPreferences
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commentPathSep is used to join path segments when collecting comments.
|
||||||
|
// It uses a rarely used ASCII control character to avoid collisions with
|
||||||
|
// normal key names (including dots).
|
||||||
|
const commentPathSep = "\x1e"
|
||||||
|
|
||||||
// NewHclEncoder creates a new HCL encoder
|
// NewHclEncoder creates a new HCL encoder
|
||||||
func NewHclEncoder(prefs HclPreferences) Encoder {
|
func NewHclEncoder(prefs HclPreferences) Encoder {
|
||||||
return &hclEncoder{prefs: prefs}
|
return &hclEncoder{prefs: prefs}
|
||||||
@ -84,7 +89,7 @@ func (he *hclEncoder) collectComments(node *CandidateNode, prefix string, commen
|
|||||||
if node.Kind == MappingNode {
|
if node.Kind == MappingNode {
|
||||||
// Collect root-level head comment if at root (prefix is empty)
|
// Collect root-level head comment if at root (prefix is empty)
|
||||||
if prefix == "" && node.HeadComment != "" {
|
if prefix == "" && node.HeadComment != "" {
|
||||||
commentMap[".head"] = node.HeadComment
|
commentMap[joinCommentPath("__root__", "head")] = node.HeadComment
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(node.Content); i += 2 {
|
for i := 0; i < len(node.Content); i += 2 {
|
||||||
@ -93,21 +98,18 @@ func (he *hclEncoder) collectComments(node *CandidateNode, prefix string, commen
|
|||||||
key := keyNode.Value
|
key := keyNode.Value
|
||||||
|
|
||||||
// Create a path for this key
|
// Create a path for this key
|
||||||
path := key
|
path := joinCommentPath(prefix, key)
|
||||||
if prefix != "" {
|
|
||||||
path = prefix + "." + key
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store comments from the key (head comments appear before the attribute)
|
// Store comments from the key (head comments appear before the attribute)
|
||||||
if keyNode.HeadComment != "" {
|
if keyNode.HeadComment != "" {
|
||||||
commentMap[path+".head"] = keyNode.HeadComment
|
commentMap[joinCommentPath(path, "head")] = keyNode.HeadComment
|
||||||
}
|
}
|
||||||
// Store comments from the value (line comments appear after the value)
|
// Store comments from the value (line comments appear after the value)
|
||||||
if valueNode.LineComment != "" {
|
if valueNode.LineComment != "" {
|
||||||
commentMap[path+".line"] = valueNode.LineComment
|
commentMap[joinCommentPath(path, "line")] = valueNode.LineComment
|
||||||
}
|
}
|
||||||
if valueNode.FootComment != "" {
|
if valueNode.FootComment != "" {
|
||||||
commentMap[path+".foot"] = valueNode.FootComment
|
commentMap[joinCommentPath(path, "foot")] = valueNode.FootComment
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recurse into nested mappings
|
// Recurse into nested mappings
|
||||||
@ -118,14 +120,22 @@ func (he *hclEncoder) collectComments(node *CandidateNode, prefix string, commen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// joinCommentPath concatenates path segments using commentPathSep, safely handling empty prefixes.
|
||||||
|
func joinCommentPath(prefix, segment string) string {
|
||||||
|
if prefix == "" {
|
||||||
|
return segment
|
||||||
|
}
|
||||||
|
return prefix + commentPathSep + segment
|
||||||
|
}
|
||||||
|
|
||||||
// injectComments adds collected comments back into the HCL output
|
// injectComments adds collected comments back into the HCL output
|
||||||
func (he *hclEncoder) injectComments(output []byte, commentMap map[string]string) []byte {
|
func (he *hclEncoder) injectComments(output []byte, commentMap map[string]string) []byte {
|
||||||
// Convert output to string for easier manipulation
|
// Convert output to string for easier manipulation
|
||||||
result := string(output)
|
result := string(output)
|
||||||
|
|
||||||
// Root-level head comment (stored as ".head")
|
// Root-level head comment (stored on the synthetic __root__/head path)
|
||||||
for path, comment := range commentMap {
|
for path, comment := range commentMap {
|
||||||
if path == ".head" {
|
if path == joinCommentPath("__root__", "head") {
|
||||||
trimmed := strings.TrimSpace(comment)
|
trimmed := strings.TrimSpace(comment)
|
||||||
if trimmed != "" && !strings.HasPrefix(result, trimmed) {
|
if trimmed != "" && !strings.HasPrefix(result, trimmed) {
|
||||||
result = trimmed + "\n" + result
|
result = trimmed + "\n" + result
|
||||||
@ -135,13 +145,13 @@ func (he *hclEncoder) injectComments(output []byte, commentMap map[string]string
|
|||||||
|
|
||||||
// Attribute head comments: insert above matching assignment
|
// Attribute head comments: insert above matching assignment
|
||||||
for path, comment := range commentMap {
|
for path, comment := range commentMap {
|
||||||
parts := strings.Split(path, ".")
|
parts := strings.Split(path, commentPathSep)
|
||||||
if len(parts) != 2 {
|
if len(parts) < 2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
key := parts[0]
|
commentType := parts[len(parts)-1]
|
||||||
commentType := parts[1]
|
key := parts[len(parts)-2]
|
||||||
if commentType != "head" || key == "" {
|
if commentType != "head" || key == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -271,7 +281,6 @@ func isHCLIdentifierPart(r rune) bool {
|
|||||||
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-'
|
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValidHCLIdentifier checks if a string is a valid HCL identifier (unquoted)
|
|
||||||
func isValidHCLIdentifier(s string) bool {
|
func isValidHCLIdentifier(s string) bool {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -407,6 +407,62 @@ var hclFormatScenarios = []formatScenario{
|
|||||||
expected: "# Main config\nenabled = true\nport = 8080\n",
|
expected: "# Main config\nenabled = true\nport = 8080\n",
|
||||||
scenarioType: "roundtrip",
|
scenarioType: "roundtrip",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Multiple attributes with comments (comment safety with safe path separator)",
|
||||||
|
skipDoc: true,
|
||||||
|
input: "# Database config\ndb_host = \"localhost\"\n# Connection pool\ndb_pool = 10",
|
||||||
|
expected: "# Database config\ndb_host = \"localhost\"\n# Connection pool\ndb_pool = 10\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Nested blocks with head comments",
|
||||||
|
skipDoc: true,
|
||||||
|
input: "service \"api\" {\n # Listen address\n listen = \"0.0.0.0:8080\"\n # TLS enabled\n tls = true\n}",
|
||||||
|
expected: "service \"api\" {\n # Listen address\n listen = \"0.0.0.0:8080\"\n # TLS enabled\n tls = true\n}\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Multiple blocks with EncodeSeparate preservation",
|
||||||
|
skipDoc: true,
|
||||||
|
input: "resource \"aws_s3_bucket\" \"bucket1\" {\n bucket = \"my-bucket-1\"\n}\nresource \"aws_s3_bucket\" \"bucket2\" {\n bucket = \"my-bucket-2\"\n}",
|
||||||
|
expected: "resource \"aws_s3_bucket\" \"bucket1\" {\n bucket = \"my-bucket-1\"\n}\nresource \"aws_s3_bucket\" \"bucket2\" {\n bucket = \"my-bucket-2\"\n}\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Blocks with same name handled separately",
|
||||||
|
skipDoc: true,
|
||||||
|
input: "server \"primary\" { port = 8080 }\nserver \"backup\" { port = 8081 }",
|
||||||
|
expected: "server \"primary\" {\n port = 8080\n}\nserver \"backup\" {\n port = 8081\n}\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Block label with dot roundtrip (commentPathSep)",
|
||||||
|
skipDoc: true,
|
||||||
|
input: "service \"api.service\" {\n port = 8080\n}",
|
||||||
|
expected: "service \"api.service\" {\n port = 8080\n}\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Nested template expression",
|
||||||
|
skipDoc: true,
|
||||||
|
input: `message = "User: ${username}, Role: ${user_role}"`,
|
||||||
|
expected: "message = \"User: ${username}, Role: ${user_role}\"\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Empty object roundtrip",
|
||||||
|
skipDoc: true,
|
||||||
|
input: `obj = {}`,
|
||||||
|
expected: "obj = {}\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Null value in block",
|
||||||
|
skipDoc: true,
|
||||||
|
input: `service { optional_field = null }`,
|
||||||
|
expected: "service {\n optional_field = null\n}\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHclScenario(t *testing.T, s formatScenario) {
|
func testHclScenario(t *testing.T, s formatScenario) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user