From d4a11521c3b167e94097294750840860a5ebd637 Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Sun, 7 Dec 2025 20:29:26 +1100 Subject: [PATCH] Fixed processing of block labels --- pkg/yqlib/encoder_hcl.go | 34 +++++++++++++++++++++++++++++++++- pkg/yqlib/hcl_test.go | 6 ++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pkg/yqlib/encoder_hcl.go b/pkg/yqlib/encoder_hcl.go index c94d7064..c367d37d 100644 --- a/pkg/yqlib/encoder_hcl.go +++ b/pkg/yqlib/encoder_hcl.go @@ -151,7 +151,15 @@ func (he *hclEncoder) encodeNode(body *hclwrite.Body, node *CandidateNode) error // Check if value is a mapping without FlowStyle -> render as block if valueNode.Kind == MappingNode && valueNode.Style != FlowStyle { - // Render as block: key { ... } + // Try to extract block labels from a single-entry mapping chain + if labels, bodyNode, ok := extractBlockLabels(valueNode); ok { + block := body.AppendNewBlock(key, labels) + if err := he.encodeNodeAttributes(block.Body(), bodyNode); err != nil { + return err + } + continue + } + // No labels detected, render as unlabeled block block := body.AppendNewBlock(key, nil) if err := he.encodeNodeAttributes(block.Body(), valueNode); err != nil { return err @@ -282,6 +290,30 @@ func (he *hclEncoder) encodeNodeAttributes(body *hclwrite.Body, node *CandidateN return nil } +// extractBlockLabels detects a chain of single-entry mappings that encode block labels. +// It returns the collected labels and the final mapping to be used as the block body. +// Pattern: {label1: {label2: { ... {bodyMap} }}} +func extractBlockLabels(node *CandidateNode) ([]string, *CandidateNode, bool) { + var labels []string + current := node + for current != nil && current.Kind == MappingNode && len(current.Content) == 2 { + keyNode := current.Content[0] + valNode := current.Content[1] + if valNode.Kind != MappingNode { + break + } + labels = append(labels, keyNode.Value) + // If the child is itself a single mapping entry with a mapping value, keep descending. + if len(valNode.Content) == 2 && valNode.Content[1].Kind == MappingNode { + current = valNode + continue + } + // Otherwise, we have reached the body mapping. + return labels, valNode, true + } + return nil, nil, false +} + // nodeToCtyValue converts a CandidateNode directly to cty.Value, preserving order func nodeToCtyValue(node *CandidateNode) (cty.Value, error) { switch node.Kind { diff --git a/pkg/yqlib/hcl_test.go b/pkg/yqlib/hcl_test.go index 6bf377e5..7bb302ff 100644 --- a/pkg/yqlib/hcl_test.go +++ b/pkg/yqlib/hcl_test.go @@ -139,6 +139,12 @@ var hclFormatScenarios = []formatScenario{ expected: "resource:\n aws_instance:\n example:\n ami: \"ami-12345\"\n", scenarioType: "decode", }, + { + description: "block with labels roundtrip", + input: `resource "aws_instance" "example" { ami = "ami-12345" }`, + expected: "resource \"aws_instance\" \"example\" {\n ami = \"ami-12345\"\n}\n", + scenarioType: "roundtrip", + }, { description: "roundtrip simple attribute", input: `io_mode = "async"`,