hcl - sorted decoding

This commit is contained in:
Mike Farah 2025-12-07 14:52:30 +11:00
parent 7d2c774e8f
commit 1852073f29
3 changed files with 79 additions and 7 deletions

View File

@ -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

View File

@ -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,

View File

@ -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) {