From 1852073f29a6bdfa5a4176694589263c595b0190 Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Sun, 7 Dec 2025 14:52:30 +1100 Subject: [PATCH] hcl - sorted decoding --- pkg/yqlib/decoder_hcl.go | 38 +++++++++++++++++++++++++++++------- pkg/yqlib/format.go | 6 ++++++ pkg/yqlib/hcl_test.go | 42 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/pkg/yqlib/decoder_hcl.go b/pkg/yqlib/decoder_hcl.go index fa6d3010..d9213e20 100644 --- a/pkg/yqlib/decoder_hcl.go +++ b/pkg/yqlib/decoder_hcl.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "math/big" + "sort" "strconv" "strings" @@ -23,6 +24,23 @@ func NewHclDecoder() Decoder { return &hclDecoder{} } +// sortedAttributes returns attributes in declaration order by source position +func sortedAttributes(attrs hclsyntax.Attributes) []*attributeWithName { + var sorted []*attributeWithName + for name, attr := range attrs { + sorted = append(sorted, &attributeWithName{Name: name, Attr: attr}) + } + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].Attr.Range().Start.Byte < sorted[j].Attr.Range().Start.Byte + }) + return sorted +} + +type attributeWithName struct { + Name string + Attr *hclsyntax.Attribute +} + func (dec *hclDecoder) Init(reader io.Reader) error { data, err := io.ReadAll(reader) if err != nil { @@ -51,11 +69,11 @@ func (dec *hclDecoder) Decode() (*CandidateNode, error) { root := &CandidateNode{Kind: MappingNode} - // process attributes + // process attributes in declaration order body := dec.file.Body.(*hclsyntax.Body) - for name, attr := range body.Attributes { - keyNode := createStringScalarNode(name) - valNode := convertHclExprToNode(attr.Expr, dec.fileBytes) + for _, attrWithName := range sortedAttributes(body.Attributes) { + keyNode := createStringScalarNode(attrWithName.Name) + valNode := convertHclExprToNode(attrWithName.Attr.Expr, dec.fileBytes) root.AddKeyValueChild(keyNode, valNode) } @@ -78,9 +96,9 @@ func (dec *hclDecoder) Decode() (*CandidateNode, error) { func hclBodyToNode(body *hclsyntax.Body, src []byte) *CandidateNode { node := &CandidateNode{Kind: MappingNode} - for name, attr := range body.Attributes { - key := createStringScalarNode(name) - val := convertHclExprToNode(attr.Expr, src) + for _, attrWithName := range sortedAttributes(body.Attributes) { + key := createStringScalarNode(attrWithName.Name) + val := convertHclExprToNode(attrWithName.Attr.Expr, src) node.AddKeyValueChild(key, val) } for _, block := range body.Blocks { @@ -209,6 +227,12 @@ func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode combined := strings.Join(parts, "") return createScalarNode(combined, combined) default: + // try to evaluate the expression (handles unary, binary ops, etc.) + val, diags := expr.Value(nil) + if diags == nil || !diags.HasErrors() { + // successfully evaluated, convert cty.Value to node + return convertCtyValueToNode(val) + } // fallback: extract source text for the expression r := expr.Range() start := r.Start.Byte diff --git a/pkg/yqlib/format.go b/pkg/yqlib/format.go index 67cc1e22..030ba48d 100644 --- a/pkg/yqlib/format.go +++ b/pkg/yqlib/format.go @@ -67,6 +67,11 @@ var TomlFormat = &Format{"toml", []string{}, func() Decoder { return NewTomlDecoder() }, } +var HclFormat = &Format{"hcl", []string{"h", "hcl"}, + nil, + func() Decoder { return NewHclDecoder() }, +} + var ShellVariablesFormat = &Format{"shell", []string{"s", "sh"}, func() Encoder { return NewShellVariablesEncoder() }, nil, @@ -93,6 +98,7 @@ var Formats = []*Format{ UriFormat, ShFormat, TomlFormat, + HclFormat, ShellVariablesFormat, LuaFormat, INIFormat, diff --git a/pkg/yqlib/hcl_test.go b/pkg/yqlib/hcl_test.go index 35db2f4c..ad7fa5ba 100644 --- a/pkg/yqlib/hcl_test.go +++ b/pkg/yqlib/hcl_test.go @@ -49,6 +49,48 @@ var hclFormatScenarios = []formatScenario{ expected: "server:\n port: 8080\n", scenarioType: "decode", }, + { + description: "multiple attributes", + input: "name = \"app\"\nversion = 1\nenabled = true", + expected: "name: app\nversion: 1\nenabled: true\n", + scenarioType: "decode", + }, + { + description: "binary expression", + input: `count = 0 - 42`, + expected: "count: -42\n", + scenarioType: "decode", + }, + { + description: "negative number", + input: `count = -42`, + expected: "count: -42\n", + scenarioType: "decode", + }, + { + description: "scientific notation", + input: `value = 1e-3`, + expected: "value: 0.001\n", + scenarioType: "decode", + }, + { + description: "nested object", + input: `config = { db = { host = "localhost", port = 5432 } }`, + expected: "config:\n db:\n host: localhost\n port: 5432\n", + scenarioType: "decode", + }, + { + description: "mixed list", + input: `values = [1, "two", true]`, + expected: "values:\n - 1\n - two\n - true\n", + scenarioType: "decode", + }, + { + description: "block with labels", + input: `resource "aws_instance" "example" { ami = "ami-12345" }`, + expected: "resource aws_instance example:\n ami: ami-12345\n", + scenarioType: "decode", + }, } func testHclScenario(t *testing.T, s formatScenario) {