From a777ada6a1aa504374f53c565df8a871e426d999 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 16 Jun 2026 17:21:45 -0400 Subject: [PATCH 01/13] hclwrite: parse object-construct expressions --- hclwrite/ast_expression.go | 12 ++ hclwrite/ast_object_cons_expr.go | 76 +++++++++ hclwrite/parser.go | 70 ++++++++ hclwrite/parser_test.go | 274 +++++++++++++++++++++++++++++++ 4 files changed, 432 insertions(+) create mode 100644 hclwrite/ast_object_cons_expr.go diff --git a/hclwrite/ast_expression.go b/hclwrite/ast_expression.go index ffe78752b..9e8354bad 100644 --- a/hclwrite/ast_expression.go +++ b/hclwrite/ast_expression.go @@ -187,6 +187,18 @@ Traversals: } } +func (e *Expression) AsObjectConsExpr() *ObjectConsExpr { + var found *ObjectConsExpr + e.walkChildNodes(func(n *node) { + if o, ok := n.content.(*ObjectConsExpr); ok { + found = o + return + } + }) + + return found +} + // Traversal represents a sequence of variable, attribute, and/or index // operations. type Traversal struct { diff --git a/hclwrite/ast_object_cons_expr.go b/hclwrite/ast_object_cons_expr.go new file mode 100644 index 000000000..adef7e474 --- /dev/null +++ b/hclwrite/ast_object_cons_expr.go @@ -0,0 +1,76 @@ +package hclwrite + +type ObjectConsExpr struct { + inTree +} + +func newObjectConsExpr() *ObjectConsExpr { + return &ObjectConsExpr{ + inTree: newInTree(), + } +} + +func (o *ObjectConsExpr) ValueFor(key string) *ObjectConsValue { + var found *ObjectConsValue + o.walkChildNodes(func(n *node) { + if item, ok := n.content.(*ObjectConsItem); ok { + k := item.key.content.(*ObjectConsKey) + name := k.name.content.(*identifier) + if k.literal && string(name.token.Bytes) == key { + found = item.value.content.(*ObjectConsValue) + return + } + } + }) + return found +} + +type ObjectConsItem struct { + inTree + key *node + value *node +} + +func newObjectConsItem() *ObjectConsItem { + item := &ObjectConsItem{ + inTree: newInTree(), + key: newNode(newObjectConsKey()), + value: newNode(newObjectConsValue()), + } + item.children.AppendNode(item.key) + item.children.AppendNode(item.value) + + return item +} + +func (item *ObjectConsItem) kv() (*ObjectConsKey, *ObjectConsValue) { + return item.key.content.(*ObjectConsKey), item.value.content.(*ObjectConsValue) +} + +type ObjectConsKey struct { + inTree + + literal bool + name *node +} + +func newObjectConsKey() *ObjectConsKey { + return &ObjectConsKey{ + inTree: newInTree(), + } +} + +type ObjectConsValue struct { + inTree + expr *node +} + +func newObjectConsValue() *ObjectConsValue { + return &ObjectConsValue{ + inTree: newInTree(), + } +} + +func (o *ObjectConsValue) Expr() *Expression { + return o.expr.content.(*Expression) +} diff --git a/hclwrite/parser.go b/hclwrite/parser.go index db7115cf6..b5430db78 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -376,6 +376,76 @@ func parseBlockLabels(nativeBlock *hclsyntax.Block, from inputTokens) (inputToke } func parseExpression(nativeExpr hclsyntax.Expression, from inputTokens) *node { + switch tNativeExpr := nativeExpr.(type) { + + // Object-construct expression + case *hclsyntax.ObjectConsExpr: + return parseObjectConsExpr(tNativeExpr, from) + + default: + return parseAnyExpression(nativeExpr, from) + } +} + +// parseObjectConsExpr parses an object-construct expression, defined as: +// +// object = "{" ( +// (objectelem (( "," | Newline) objectelem)* ","?)? +// ) "}"; +// objectelem = (Identifier | Expression) ("=" | ":") Expression; +// +// It leaves any lead comments and line comments as unstructed tokens. +// +// It returns *Expression. The returned *Expression has a child *ObjectConsExpr +// that can be unwrapped via Expression.AsObjectConsExpr(). +func parseObjectConsExpr(nativeExpr *hclsyntax.ObjectConsExpr, from inputTokens) *node { + expr := newObjectConsExpr() + children := expr.children + + var before, open, keyTokens, valueTokens inputTokens + + before, open, from = from.Partition(nativeExpr.StartRange()) + children.AppendUnstructuredTokens(before.writerTokens) + children.AppendUnstructuredTokens(open.writerTokens) + + for _, nativeItem := range nativeExpr.Items { + item := newObjectConsItem() + key, value := item.kv() + + nativeKeyExpr := nativeItem.KeyExpr.(*hclsyntax.ObjectConsKeyExpr) + key.literal = !nativeKeyExpr.ForceNonLiteral + + if key.literal { + var keyToken *Token + before, keyToken, from = from.PartitionTypeSingle(hclsyntax.TokenIdent) + + key.children.AppendUnstructuredTokens(before.writerTokens) + key.name = key.children.Append(newIdentifier(keyToken)) + } else { + before, keyTokens, from = from.Partition(nativeKeyExpr.Range()) + + key.children.AppendUnstructuredTokens(before.writerTokens) + key.children.AppendNode(parseExpression(nativeKeyExpr, keyTokens)) + } + + before, valueTokens, from = from.Partition(nativeItem.ValueExpr.Range()) + value.children.AppendUnstructuredTokens(before.writerTokens) + value.expr = parseExpression(nativeItem.ValueExpr, valueTokens) + value.children.AppendNode(value.expr) + + children.Append(item) + } + + _, from, _ = from.Partition(nativeExpr.Range()) + children.AppendUnstructuredTokens(from.writerTokens) + + // Wrap in an Expression + wrapExpr := newExpression() + wrapExpr.children.Append(expr) + return newNode(wrapExpr) +} + +func parseAnyExpression(nativeExpr hclsyntax.Expression, from inputTokens) *node { expr := newExpression() children := expr.children diff --git a/hclwrite/parser_test.go b/hclwrite/parser_test.go index 3fce9a08a..eb06b146f 100644 --- a/hclwrite/parser_test.go +++ b/hclwrite/parser_test.go @@ -1200,6 +1200,280 @@ func TestParse(t *testing.T) { }, }, }, + { + `a = { + hat = "derby", (cat) = "calico" }`, + TestTreeNode{ + Type: "Body", + Children: []TestTreeNode{ + { + Type: "Attribute", + Children: []TestTreeNode{ + { + Type: "comments", + }, + { + Type: "identifier", + Val: "a", + }, + { + Type: "Tokens", + Val: " =", + }, + { + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "ObjectConsExpr", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " {", + }, + { + Type: "ObjectConsItem", + Children: []TestTreeNode{ + { + Type: "ObjectConsKey", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: "\n", + }, + { + Type: "identifier", + Val: " hat", + }, + }, + }, + { + Type: "ObjectConsValue", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " =", + }, + { + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: ` "derby"`, + }, + }, + }, + }, + }, + }, + }, + { + Type: "ObjectConsItem", + Children: []TestTreeNode{ + { + Type: "ObjectConsKey", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: ",", + }, + { + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " (", + }, + { + Type: "Traversal", + Children: []TestTreeNode{ + { + Type: "TraverseName", + Children: []TestTreeNode{{Type: "identifier", Val: "cat"}}, + }, + }, + }, + { + Type: "Tokens", + Val: ")", + }, + }, + }, + }, + }, + { + Type: "ObjectConsValue", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " =", + }, + { + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: ` "calico"`, + }, + }, + }, + }, + }, + }, + }, + { + Type: "Tokens", + Val: " }", + }, + }, + }, + }, + }, + { + Type: "comments", + }, + }, + }, + }, + }, + }, + { + `a = { + hat = "derby" // a fancy hat + (cat) = "calico" + }`, + TestTreeNode{ + Type: "Body", + Children: []TestTreeNode{ + { + Type: "Attribute", + Children: []TestTreeNode{ + { + Type: "comments", + }, + { + Type: "identifier", + Val: "a", + }, + { + Type: "Tokens", + Val: " =", + }, + { + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "ObjectConsExpr", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " {", + }, + { + Type: "ObjectConsItem", + Children: []TestTreeNode{ + { + Type: "ObjectConsKey", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: "\n", + }, + { + Type: "identifier", + Val: " hat", + }, + }, + }, + { + Type: "ObjectConsValue", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " =", + }, + { + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: ` "derby"`, + }, + }, + }, + }, + }, + }, + }, + { + Type: "ObjectConsItem", + Children: []TestTreeNode{ + { + Type: "ObjectConsKey", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " // a fancy hat\n", + }, + { + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " (", + }, + { + Type: "Traversal", + Children: []TestTreeNode{ + { + Type: "TraverseName", + Children: []TestTreeNode{{Type: "identifier", Val: "cat"}}, + }, + }, + }, + { + Type: "Tokens", + Val: ")", + }, + }, + }, + }, + }, + { + Type: "ObjectConsValue", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " =", + }, + { + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: ` "calico"`, + }, + }, + }, + }, + }, + }, + }, + { + Type: "Tokens", + Val: "\n }", + }, + }, + }, + }, + }, + { + Type: "comments", + }, + }, + }, + }, + }, + }, } for _, test := range tests { From 1386ddfd58b80ae3e6234fc84e319b908790e943 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Mon, 15 Jun 2026 18:01:40 -0400 Subject: [PATCH 02/13] Update copyright year --- hclwrite/ast_expression.go | 2 +- hclwrite/ast_object_cons_expr.go | 3 +++ hclwrite/parser.go | 2 +- hclwrite/parser_test.go | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/hclwrite/ast_expression.go b/hclwrite/ast_expression.go index 9e8354bad..c3b02a80b 100644 --- a/hclwrite/ast_expression.go +++ b/hclwrite/ast_expression.go @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2014, 2025 +// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: MPL-2.0 package hclwrite diff --git a/hclwrite/ast_object_cons_expr.go b/hclwrite/ast_object_cons_expr.go index adef7e474..a670fffd1 100644 --- a/hclwrite/ast_object_cons_expr.go +++ b/hclwrite/ast_object_cons_expr.go @@ -1,3 +1,6 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + package hclwrite type ObjectConsExpr struct { diff --git a/hclwrite/parser.go b/hclwrite/parser.go index b5430db78..e8bf50194 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2014, 2025 +// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: MPL-2.0 package hclwrite diff --git a/hclwrite/parser_test.go b/hclwrite/parser_test.go index eb06b146f..76f76d43f 100644 --- a/hclwrite/parser_test.go +++ b/hclwrite/parser_test.go @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2014, 2025 +// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: MPL-2.0 package hclwrite From 6e0ffff503d37c98917edb3d2eb6e20aa9066d21 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Wed, 17 Jun 2026 22:49:30 -0400 Subject: [PATCH 03/13] fix: panic on incorrect assumption of parsing an identifier --- hclwrite/ast_object_cons_expr.go | 12 ++++++++++-- hclwrite/parser.go | 16 ++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/hclwrite/ast_object_cons_expr.go b/hclwrite/ast_object_cons_expr.go index a670fffd1..123e8c4ef 100644 --- a/hclwrite/ast_object_cons_expr.go +++ b/hclwrite/ast_object_cons_expr.go @@ -18,8 +18,16 @@ func (o *ObjectConsExpr) ValueFor(key string) *ObjectConsValue { o.walkChildNodes(func(n *node) { if item, ok := n.content.(*ObjectConsItem); ok { k := item.key.content.(*ObjectConsKey) - name := k.name.content.(*identifier) - if k.literal && string(name.token.Bytes) == key { + name := k.name.content.(*Expression) + + // A temporary workaround for parsing expressions as names at ... + // parse-time + maybeKey := "" + for _, token := range name.BuildTokens(nil) { + maybeKey += string(token.Bytes) // no spaces before + } + + if k.literal && (maybeKey == key || maybeKey == `"`+key+`"`) { found = item.value.content.(*ObjectConsValue) return } diff --git a/hclwrite/parser.go b/hclwrite/parser.go index e8bf50194..498a1d47b 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -415,18 +415,10 @@ func parseObjectConsExpr(nativeExpr *hclsyntax.ObjectConsExpr, from inputTokens) nativeKeyExpr := nativeItem.KeyExpr.(*hclsyntax.ObjectConsKeyExpr) key.literal = !nativeKeyExpr.ForceNonLiteral - if key.literal { - var keyToken *Token - before, keyToken, from = from.PartitionTypeSingle(hclsyntax.TokenIdent) - - key.children.AppendUnstructuredTokens(before.writerTokens) - key.name = key.children.Append(newIdentifier(keyToken)) - } else { - before, keyTokens, from = from.Partition(nativeKeyExpr.Range()) - - key.children.AppendUnstructuredTokens(before.writerTokens) - key.children.AppendNode(parseExpression(nativeKeyExpr, keyTokens)) - } + before, keyTokens, from = from.Partition(nativeKeyExpr.Range()) + key.children.AppendUnstructuredTokens(before.writerTokens) + key.name = parseExpression(nativeKeyExpr, keyTokens) + key.children.AppendNode(key.name) before, valueTokens, from = from.Partition(nativeItem.ValueExpr.Range()) value.children.AppendUnstructuredTokens(before.writerTokens) From ad77ad6e730763bb8d7163573306ad7ace003e32 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Wed, 17 Jun 2026 22:56:38 -0400 Subject: [PATCH 04/13] Update test --- hclwrite/parser_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/hclwrite/parser_test.go b/hclwrite/parser_test.go index 76f76d43f..95baee620 100644 --- a/hclwrite/parser_test.go +++ b/hclwrite/parser_test.go @@ -1241,8 +1241,10 @@ func TestParse(t *testing.T) { Val: "\n", }, { - Type: "identifier", - Val: " hat", + Type: "Expression", + Children: []TestTreeNode{{ + Type: "Tokens", Val: " hat", + }}, }, }, }, @@ -1379,8 +1381,10 @@ func TestParse(t *testing.T) { Val: "\n", }, { - Type: "identifier", - Val: " hat", + Type: "Expression", + Children: []TestTreeNode{{ + Type: "Tokens", Val: " hat", + }}, }, }, }, From 613f994ecbf5cda1769a4185b050ef4c5bcb335d Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Wed, 17 Jun 2026 23:07:38 -0400 Subject: [PATCH 05/13] A nicer tree --- hclwrite/ast_object_cons_expr.go | 8 +--- hclwrite/parser.go | 8 ++-- hclwrite/parser_test.go | 64 ++++++++++++++++---------------- 3 files changed, 38 insertions(+), 42 deletions(-) diff --git a/hclwrite/ast_object_cons_expr.go b/hclwrite/ast_object_cons_expr.go index 123e8c4ef..d622b3803 100644 --- a/hclwrite/ast_object_cons_expr.go +++ b/hclwrite/ast_object_cons_expr.go @@ -43,15 +43,9 @@ type ObjectConsItem struct { } func newObjectConsItem() *ObjectConsItem { - item := &ObjectConsItem{ + return &ObjectConsItem{ inTree: newInTree(), - key: newNode(newObjectConsKey()), - value: newNode(newObjectConsValue()), } - item.children.AppendNode(item.key) - item.children.AppendNode(item.value) - - return item } func (item *ObjectConsItem) kv() (*ObjectConsKey, *ObjectConsValue) { diff --git a/hclwrite/parser.go b/hclwrite/parser.go index 498a1d47b..d4fe89c0b 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -410,20 +410,22 @@ func parseObjectConsExpr(nativeExpr *hclsyntax.ObjectConsExpr, from inputTokens) for _, nativeItem := range nativeExpr.Items { item := newObjectConsItem() - key, value := item.kv() + key, value := newObjectConsKey(), newObjectConsValue() nativeKeyExpr := nativeItem.KeyExpr.(*hclsyntax.ObjectConsKeyExpr) key.literal = !nativeKeyExpr.ForceNonLiteral before, keyTokens, from = from.Partition(nativeKeyExpr.Range()) - key.children.AppendUnstructuredTokens(before.writerTokens) + item.children.AppendUnstructuredTokens(before.writerTokens) key.name = parseExpression(nativeKeyExpr, keyTokens) key.children.AppendNode(key.name) + item.key = item.children.Append(key) before, valueTokens, from = from.Partition(nativeItem.ValueExpr.Range()) - value.children.AppendUnstructuredTokens(before.writerTokens) + item.children.AppendUnstructuredTokens(before.writerTokens) value.expr = parseExpression(nativeItem.ValueExpr, valueTokens) value.children.AppendNode(value.expr) + item.value = item.children.Append(value) children.Append(item) } diff --git a/hclwrite/parser_test.go b/hclwrite/parser_test.go index 95baee620..9aefd5684 100644 --- a/hclwrite/parser_test.go +++ b/hclwrite/parser_test.go @@ -1233,13 +1233,13 @@ func TestParse(t *testing.T) { { Type: "ObjectConsItem", Children: []TestTreeNode{ + { + Type: "Tokens", + Val: "\n", + }, { Type: "ObjectConsKey", Children: []TestTreeNode{ - { - Type: "Tokens", - Val: "\n", - }, { Type: "Expression", Children: []TestTreeNode{{ @@ -1248,13 +1248,13 @@ func TestParse(t *testing.T) { }, }, }, + { + Type: "Tokens", + Val: " =", + }, { Type: "ObjectConsValue", Children: []TestTreeNode{ - { - Type: "Tokens", - Val: " =", - }, { Type: "Expression", Children: []TestTreeNode{ @@ -1271,13 +1271,13 @@ func TestParse(t *testing.T) { { Type: "ObjectConsItem", Children: []TestTreeNode{ + { + Type: "Tokens", + Val: ",", + }, { Type: "ObjectConsKey", Children: []TestTreeNode{ - { - Type: "Tokens", - Val: ",", - }, { Type: "Expression", Children: []TestTreeNode{ @@ -1302,13 +1302,13 @@ func TestParse(t *testing.T) { }, }, }, + { + Type: "Tokens", + Val: " =", + }, { Type: "ObjectConsValue", Children: []TestTreeNode{ - { - Type: "Tokens", - Val: " =", - }, { Type: "Expression", Children: []TestTreeNode{ @@ -1373,13 +1373,13 @@ func TestParse(t *testing.T) { { Type: "ObjectConsItem", Children: []TestTreeNode{ + { + Type: "Tokens", + Val: "\n", + }, { Type: "ObjectConsKey", Children: []TestTreeNode{ - { - Type: "Tokens", - Val: "\n", - }, { Type: "Expression", Children: []TestTreeNode{{ @@ -1388,13 +1388,13 @@ func TestParse(t *testing.T) { }, }, }, + { + Type: "Tokens", + Val: " =", + }, { Type: "ObjectConsValue", Children: []TestTreeNode{ - { - Type: "Tokens", - Val: " =", - }, { Type: "Expression", Children: []TestTreeNode{ @@ -1411,13 +1411,13 @@ func TestParse(t *testing.T) { { Type: "ObjectConsItem", Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " // a fancy hat\n", + }, { Type: "ObjectConsKey", Children: []TestTreeNode{ - { - Type: "Tokens", - Val: " // a fancy hat\n", - }, { Type: "Expression", Children: []TestTreeNode{ @@ -1442,13 +1442,13 @@ func TestParse(t *testing.T) { }, }, }, + { + Type: "Tokens", + Val: " =", + }, { Type: "ObjectConsValue", Children: []TestTreeNode{ - { - Type: "Tokens", - Val: " =", - }, { Type: "Expression", Children: []TestTreeNode{ From 9f55a18d1964980cd66e2ff507e941fb5293a8f2 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Thu, 18 Jun 2026 07:28:40 -0400 Subject: [PATCH 06/13] tidy --- hclwrite/parser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hclwrite/parser.go b/hclwrite/parser.go index d4fe89c0b..8b84b74d8 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -427,7 +427,7 @@ func parseObjectConsExpr(nativeExpr *hclsyntax.ObjectConsExpr, from inputTokens) value.children.AppendNode(value.expr) item.value = item.children.Append(value) - children.Append(item) + expr.items.Add(children.Append(item)) } _, from, _ = from.Partition(nativeExpr.Range()) From 0d0583d584f04c0db4e754ccbb4c7a5699c73704 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Thu, 18 Jun 2026 14:33:55 -0400 Subject: [PATCH 07/13] fixit --- hclwrite/ast_object_cons_expr.go | 4 ---- hclwrite/parser.go | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/hclwrite/ast_object_cons_expr.go b/hclwrite/ast_object_cons_expr.go index d622b3803..19caeb532 100644 --- a/hclwrite/ast_object_cons_expr.go +++ b/hclwrite/ast_object_cons_expr.go @@ -48,10 +48,6 @@ func newObjectConsItem() *ObjectConsItem { } } -func (item *ObjectConsItem) kv() (*ObjectConsKey, *ObjectConsValue) { - return item.key.content.(*ObjectConsKey), item.value.content.(*ObjectConsValue) -} - type ObjectConsKey struct { inTree diff --git a/hclwrite/parser.go b/hclwrite/parser.go index 8b84b74d8..61444de6a 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -427,7 +427,7 @@ func parseObjectConsExpr(nativeExpr *hclsyntax.ObjectConsExpr, from inputTokens) value.children.AppendNode(value.expr) item.value = item.children.Append(value) - expr.items.Add(children.Append(item)) + expr.children.Append(item) } _, from, _ = from.Partition(nativeExpr.Range()) From f7c0634835ab020590f37ff9b58aa99c832ad1a4 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Thu, 18 Jun 2026 15:35:48 -0400 Subject: [PATCH 08/13] wip --- hclwrite/ast_object_cons_expr.go | 1 + hclwrite/parser.go | 43 ++++++++++++-- hclwrite/parser_test.go | 99 ++++++++++++++++++++++++++++---- 3 files changed, 126 insertions(+), 17 deletions(-) diff --git a/hclwrite/ast_object_cons_expr.go b/hclwrite/ast_object_cons_expr.go index 19caeb532..74f1b8c89 100644 --- a/hclwrite/ast_object_cons_expr.go +++ b/hclwrite/ast_object_cons_expr.go @@ -53,6 +53,7 @@ type ObjectConsKey struct { literal bool name *node + expr *node } func newObjectConsKey() *ObjectConsKey { diff --git a/hclwrite/parser.go b/hclwrite/parser.go index 61444de6a..0543487a8 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -382,11 +382,46 @@ func parseExpression(nativeExpr hclsyntax.Expression, from inputTokens) *node { case *hclsyntax.ObjectConsExpr: return parseObjectConsExpr(tNativeExpr, from) + case *hclsyntax.ObjectConsKeyExpr: + return parseObjectConsKeyExpr(tNativeExpr, from) + + case *hclsyntax.TemplateExpr: + if tNativeExpr.IsStringLiteral() { + quoted := newQuoted(from.writerTokens) + + // Wrap in an Expression + wrapExpr := newExpression() + wrapExpr.children.Append(quoted) + return newNode(wrapExpr) + } else { + return parseAnyExpression(nativeExpr, from) + } + default: return parseAnyExpression(nativeExpr, from) } } +// parseObjectConsExpr parses an object-construct key expression +func parseObjectConsKeyExpr(nativeExpr *hclsyntax.ObjectConsKeyExpr, from inputTokens) *node { + wrapExpr := newObjectConsKey() + + if nativeExpr.ForceNonLiteral { + expr := parseExpression(nativeExpr.Wrapped, from) + + wrapExpr.expr = expr + wrapExpr.children.AppendNode(expr) + + } else { + quoted := newQuoted(from.writerTokens) + + wrapExpr.literal = true + wrapExpr.name = wrapExpr.children.Append(quoted) + } + + return newNode(wrapExpr) +} + // parseObjectConsExpr parses an object-construct expression, defined as: // // object = "{" ( @@ -410,16 +445,14 @@ func parseObjectConsExpr(nativeExpr *hclsyntax.ObjectConsExpr, from inputTokens) for _, nativeItem := range nativeExpr.Items { item := newObjectConsItem() - key, value := newObjectConsKey(), newObjectConsValue() + value := newObjectConsValue() nativeKeyExpr := nativeItem.KeyExpr.(*hclsyntax.ObjectConsKeyExpr) - key.literal = !nativeKeyExpr.ForceNonLiteral before, keyTokens, from = from.Partition(nativeKeyExpr.Range()) item.children.AppendUnstructuredTokens(before.writerTokens) - key.name = parseExpression(nativeKeyExpr, keyTokens) - key.children.AppendNode(key.name) - item.key = item.children.Append(key) + item.key = parseObjectConsKeyExpr(nativeKeyExpr, keyTokens) + item.children.AppendNode(item.key) before, valueTokens, from = from.Partition(nativeItem.ValueExpr.Range()) item.children.AppendUnstructuredTokens(before.writerTokens) diff --git a/hclwrite/parser_test.go b/hclwrite/parser_test.go index 9aefd5684..84fd20945 100644 --- a/hclwrite/parser_test.go +++ b/hclwrite/parser_test.go @@ -1241,10 +1241,7 @@ func TestParse(t *testing.T) { Type: "ObjectConsKey", Children: []TestTreeNode{ { - Type: "Expression", - Children: []TestTreeNode{{ - Type: "Tokens", Val: " hat", - }}, + Type: "quoted", Val: " hat", }, }, }, @@ -1259,7 +1256,7 @@ func TestParse(t *testing.T) { Type: "Expression", Children: []TestTreeNode{ { - Type: "Tokens", + Type: "quoted", Val: ` "derby"`, }, }, @@ -1313,7 +1310,7 @@ func TestParse(t *testing.T) { Type: "Expression", Children: []TestTreeNode{ { - Type: "Tokens", + Type: "quoted", Val: ` "calico"`, }, }, @@ -1381,10 +1378,7 @@ func TestParse(t *testing.T) { Type: "ObjectConsKey", Children: []TestTreeNode{ { - Type: "Expression", - Children: []TestTreeNode{{ - Type: "Tokens", Val: " hat", - }}, + Type: "quoted", Val: " hat", }, }, }, @@ -1399,7 +1393,7 @@ func TestParse(t *testing.T) { Type: "Expression", Children: []TestTreeNode{ { - Type: "Tokens", + Type: "quoted", Val: ` "derby"`, }, }, @@ -1453,7 +1447,7 @@ func TestParse(t *testing.T) { Type: "Expression", Children: []TestTreeNode{ { - Type: "Tokens", + Type: "quoted", Val: ` "calico"`, }, }, @@ -1478,6 +1472,87 @@ func TestParse(t *testing.T) { }, }, }, + { + `zzz = { + "hat" = "derby" }`, + TestTreeNode{ + Type: "Body", + Children: []TestTreeNode{ + { + Type: "Attribute", + Children: []TestTreeNode{ + { + Type: "comments", + }, + { + Type: "identifier", + Val: "zzz", + }, + { + Type: "Tokens", + Val: " =", + }, + { + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "ObjectConsExpr", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: " {", + }, + { + Type: "ObjectConsItem", + Children: []TestTreeNode{ + { + Type: "Tokens", + Val: "\n", + }, + { + Type: "ObjectConsKey", + Children: []TestTreeNode{ + { + Type: "quoted", Val: ` "hat"`, + }, + }, + }, + { + Type: "Tokens", + Val: " =", + }, + { + Type: "ObjectConsValue", + Children: []TestTreeNode{ + { + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "quoted", + Val: ` "derby"`, + }, + }, + }, + }, + }, + }, + }, + { + Type: "Tokens", + Val: " }", + }, + }, + }, + }, + }, + { + Type: "comments", + }, + }, + }, + }, + }, + }, } for _, test := range tests { From 7940e82e5cbc6fc8051ea580c4c0c9fdf0303be4 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Thu, 18 Jun 2026 17:54:05 -0400 Subject: [PATCH 09/13] wip --- hclwrite/ast_object_cons_expr.go | 21 ++++++------------ hclwrite/parser.go | 25 +++++++++++++++++---- hclwrite/parser_test.go | 38 ++++++++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/hclwrite/ast_object_cons_expr.go b/hclwrite/ast_object_cons_expr.go index 74f1b8c89..4c83fe485 100644 --- a/hclwrite/ast_object_cons_expr.go +++ b/hclwrite/ast_object_cons_expr.go @@ -18,16 +18,9 @@ func (o *ObjectConsExpr) ValueFor(key string) *ObjectConsValue { o.walkChildNodes(func(n *node) { if item, ok := n.content.(*ObjectConsItem); ok { k := item.key.content.(*ObjectConsKey) - name := k.name.content.(*Expression) - // A temporary workaround for parsing expressions as names at ... - // parse-time - maybeKey := "" - for _, token := range name.BuildTokens(nil) { - maybeKey += string(token.Bytes) // no spaces before - } - - if k.literal && (maybeKey == key || maybeKey == `"`+key+`"`) { + maybeKey := k.literalName + if maybeKey == key || maybeKey == `"`+key+`"` { found = item.value.content.(*ObjectConsValue) return } @@ -38,8 +31,9 @@ func (o *ObjectConsExpr) ValueFor(key string) *ObjectConsValue { type ObjectConsItem struct { inTree - key *node - value *node + key *node + value *node + literalKey string } func newObjectConsItem() *ObjectConsItem { @@ -51,9 +45,8 @@ func newObjectConsItem() *ObjectConsItem { type ObjectConsKey struct { inTree - literal bool - name *node - expr *node + literalName string + expr *node } func newObjectConsKey() *ObjectConsKey { diff --git a/hclwrite/parser.go b/hclwrite/parser.go index 0543487a8..665e854ac 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -6,6 +6,7 @@ package hclwrite import ( "fmt" "sort" + "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -402,7 +403,9 @@ func parseExpression(nativeExpr hclsyntax.Expression, from inputTokens) *node { } } -// parseObjectConsExpr parses an object-construct key expression +// parseObjectConsExpr parses an object-construct key expression. +// An object key is an Identifier or an Expression. In this context, a quoted +// literal is functionally equivalent to an Identifier. func parseObjectConsKeyExpr(nativeExpr *hclsyntax.ObjectConsKeyExpr, from inputTokens) *node { wrapExpr := newObjectConsKey() @@ -413,10 +416,23 @@ func parseObjectConsKeyExpr(nativeExpr *hclsyntax.ObjectConsKeyExpr, from inputT wrapExpr.children.AppendNode(expr) } else { - quoted := newQuoted(from.writerTokens) + if templateExpr, ok := nativeExpr.Wrapped.(*hclsyntax.TemplateExpr); ok && templateExpr.IsStringLiteral() { + literalValueExpr := templateExpr.Parts[0].(*hclsyntax.LiteralValueExpr) - wrapExpr.literal = true - wrapExpr.name = wrapExpr.children.Append(quoted) + quoted := newQuoted(from.writerTokens) + _, literal, _ := from.Partition(literalValueExpr.Range()) + + var b strings.Builder + literal.writerTokens.WriteTo(&b) + wrapExpr.literalName = b.String() + wrapExpr.children.Append(quoted) + } else if scopeTraversalExpr, ok := nativeExpr.Wrapped.(*hclsyntax.ScopeTraversalExpr); ok { + traversal := scopeTraversalExpr.AsTraversal() + expr := NewExpressionAbsTraversal(traversal) + + wrapExpr.literalName = traversal.RootName() + wrapExpr.children.Append(expr) + } } return newNode(wrapExpr) @@ -459,6 +475,7 @@ func parseObjectConsExpr(nativeExpr *hclsyntax.ObjectConsExpr, from inputTokens) value.expr = parseExpression(nativeItem.ValueExpr, valueTokens) value.children.AppendNode(value.expr) item.value = item.children.Append(value) + item.literalKey = item.key.content.(*ObjectConsKey).literalName expr.children.Append(item) } diff --git a/hclwrite/parser_test.go b/hclwrite/parser_test.go index 84fd20945..344090d6d 100644 --- a/hclwrite/parser_test.go +++ b/hclwrite/parser_test.go @@ -1241,7 +1241,24 @@ func TestParse(t *testing.T) { Type: "ObjectConsKey", Children: []TestTreeNode{ { - Type: "quoted", Val: " hat", + Type: "Expression", + Children: []TestTreeNode{ + + { + Type: "Traversal", + Children: []TestTreeNode{ + { + Type: "TraverseName", + Children: []TestTreeNode{ + { + Type: "identifier", + Val: "hat", + }, + }, + }, + }, + }, + }, }, }, }, @@ -1378,7 +1395,24 @@ func TestParse(t *testing.T) { Type: "ObjectConsKey", Children: []TestTreeNode{ { - Type: "quoted", Val: " hat", + Type: "Expression", + Children: []TestTreeNode{ + + { + Type: "Traversal", + Children: []TestTreeNode{ + { + Type: "TraverseName", + Children: []TestTreeNode{ + { + Type: "identifier", + Val: "hat", + }, + }, + }, + }, + }, + }, }, }, }, From 2e6390ec188a95ea75bab47cfc606e950f444b10 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Fri, 19 Jun 2026 00:04:03 -0400 Subject: [PATCH 10/13] wip --- hclwrite/ast_expression.go | 46 +++++++++++++--- hclwrite/ast_object_cons_expr.go | 57 +++++++++++++------ hclwrite/parser.go | 95 ++++++++++++-------------------- hclwrite/parser_test.go | 31 +++++++---- 4 files changed, 135 insertions(+), 94 deletions(-) diff --git a/hclwrite/ast_expression.go b/hclwrite/ast_expression.go index c3b02a80b..e653c6e56 100644 --- a/hclwrite/ast_expression.go +++ b/hclwrite/ast_expression.go @@ -15,6 +15,7 @@ type Expression struct { inTree absTraversals nodeSet + wrapped *node } func newExpression() *Expression { @@ -24,6 +25,15 @@ func newExpression() *Expression { } } +func (e *Expression) wrap(c nodeContent) { + if e.wrapped != nil { + panic("this is a problem") + } + + e.wrapped = newNode(c) + e.children.AppendNode(e.wrapped) +} + // NewExpressionRaw constructs an expression containing the given raw tokens. // // There is no automatic validation that the given tokens produce a valid @@ -188,15 +198,35 @@ Traversals: } func (e *Expression) AsObjectConsExpr() *ObjectConsExpr { - var found *ObjectConsExpr - e.walkChildNodes(func(n *node) { - if o, ok := n.content.(*ObjectConsExpr); ok { - found = o - return - } - }) + if e.wrapped == nil { + return nil + } + + if object, ok := e.wrapped.content.(*ObjectConsExpr); ok { + return object + } - return found + return nil +} + +func (e *Expression) asQuoted() *quoted { + if e.wrapped == nil { + return nil + } + + if quoted, ok := e.wrapped.content.(*quoted); ok { + return quoted + } + + return nil +} + +func (e *Expression) AsQuotedLiteral() Tokens { + if quoted := e.asQuoted(); quoted == nil { + return Tokens{} + } else { + return quoted.tokens + } } // Traversal represents a sequence of variable, attribute, and/or index diff --git a/hclwrite/ast_object_cons_expr.go b/hclwrite/ast_object_cons_expr.go index 4c83fe485..58ed5c376 100644 --- a/hclwrite/ast_object_cons_expr.go +++ b/hclwrite/ast_object_cons_expr.go @@ -3,37 +3,40 @@ package hclwrite +import "strings" + type ObjectConsExpr struct { inTree + + items nodeSet } func newObjectConsExpr() *ObjectConsExpr { return &ObjectConsExpr{ inTree: newInTree(), + items: newNodeSet(), } } func (o *ObjectConsExpr) ValueFor(key string) *ObjectConsValue { - var found *ObjectConsValue - o.walkChildNodes(func(n *node) { + for _, n := range o.items.List() { if item, ok := n.content.(*ObjectConsItem); ok { - k := item.key.content.(*ObjectConsKey) + k := item.key.content.(*ObjectConsKeyExpr) - maybeKey := k.literalName - if maybeKey == key || maybeKey == `"`+key+`"` { - found = item.value.content.(*ObjectConsValue) - return + maybeKey := k.String() + if maybeKey == key { + return item.value.content.(*ObjectConsValue) } } - }) - return found + } + + return nil } type ObjectConsItem struct { inTree - key *node - value *node - literalKey string + key *node + value *node } func newObjectConsItem() *ObjectConsItem { @@ -42,17 +45,37 @@ func newObjectConsItem() *ObjectConsItem { } } -type ObjectConsKey struct { +type ObjectConsKeyExpr struct { inTree literalName string - expr *node + wrapped *node } -func newObjectConsKey() *ObjectConsKey { - return &ObjectConsKey{ - inTree: newInTree(), +func newObjectConsKeyExpr(wrapped *node) *ObjectConsKeyExpr { + expr := &ObjectConsKeyExpr{ + inTree: newInTree(), + wrapped: wrapped, } + expr.children.AppendNode(wrapped) + + return expr +} + +func (k *ObjectConsKeyExpr) String() string { + if k.wrapped == nil { + return "" + } + + if t, ok := k.wrapped.content.(*Traversal); ok && len(t.steps.List()) > 0 { + var b strings.Builder + tok := t.steps.List()[0].BuildTokens(nil) + tok.WriteTo(&b) + + return b.String() + } + + return "" } type ObjectConsValue struct { diff --git a/hclwrite/parser.go b/hclwrite/parser.go index 665e854ac..ab10514c4 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -6,7 +6,6 @@ package hclwrite import ( "fmt" "sort" - "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -387,55 +386,32 @@ func parseExpression(nativeExpr hclsyntax.Expression, from inputTokens) *node { return parseObjectConsKeyExpr(tNativeExpr, from) case *hclsyntax.TemplateExpr: - if tNativeExpr.IsStringLiteral() { - quoted := newQuoted(from.writerTokens) - - // Wrap in an Expression - wrapExpr := newExpression() - wrapExpr.children.Append(quoted) - return newNode(wrapExpr) - } else { - return parseAnyExpression(nativeExpr, from) - } + return parseTemplateExpr(tNativeExpr, from) default: return parseAnyExpression(nativeExpr, from) } } -// parseObjectConsExpr parses an object-construct key expression. -// An object key is an Identifier or an Expression. In this context, a quoted -// literal is functionally equivalent to an Identifier. -func parseObjectConsKeyExpr(nativeExpr *hclsyntax.ObjectConsKeyExpr, from inputTokens) *node { - wrapExpr := newObjectConsKey() - - if nativeExpr.ForceNonLiteral { - expr := parseExpression(nativeExpr.Wrapped, from) +func parseAnyExpression(nativeExpr hclsyntax.Expression, from inputTokens) *node { + expr := newExpression() + children := expr.children - wrapExpr.expr = expr - wrapExpr.children.AppendNode(expr) + nativeVars := nativeExpr.Variables() - } else { - if templateExpr, ok := nativeExpr.Wrapped.(*hclsyntax.TemplateExpr); ok && templateExpr.IsStringLiteral() { - literalValueExpr := templateExpr.Parts[0].(*hclsyntax.LiteralValueExpr) - - quoted := newQuoted(from.writerTokens) - _, literal, _ := from.Partition(literalValueExpr.Range()) - - var b strings.Builder - literal.writerTokens.WriteTo(&b) - wrapExpr.literalName = b.String() - wrapExpr.children.Append(quoted) - } else if scopeTraversalExpr, ok := nativeExpr.Wrapped.(*hclsyntax.ScopeTraversalExpr); ok { - traversal := scopeTraversalExpr.AsTraversal() - expr := NewExpressionAbsTraversal(traversal) - - wrapExpr.literalName = traversal.RootName() - wrapExpr.children.Append(expr) - } + for _, nativeTraversal := range nativeVars { + before, traversal, after := parseTraversal(nativeTraversal, from) + children.AppendUnstructuredTokens(before.Tokens()) + children.AppendNode(traversal) + expr.absTraversals.Add(traversal) + from = after } + // Attach any stragglers that don't belong to a traversal to the expression + // itself. In an expression with no traversals at all, this is just the + // entirety of "from". + children.AppendUnstructuredTokens(from.Tokens()) - return newNode(wrapExpr) + return newNode(expr) } // parseObjectConsExpr parses an object-construct expression, defined as: @@ -475,9 +451,9 @@ func parseObjectConsExpr(nativeExpr *hclsyntax.ObjectConsExpr, from inputTokens) value.expr = parseExpression(nativeItem.ValueExpr, valueTokens) value.children.AppendNode(value.expr) item.value = item.children.Append(value) - item.literalKey = item.key.content.(*ObjectConsKey).literalName - expr.children.Append(item) + node := expr.children.Append(item) + expr.items.Add(node) } _, from, _ = from.Partition(nativeExpr.Range()) @@ -485,29 +461,30 @@ func parseObjectConsExpr(nativeExpr *hclsyntax.ObjectConsExpr, from inputTokens) // Wrap in an Expression wrapExpr := newExpression() - wrapExpr.children.Append(expr) + wrapExpr.wrap(expr) return newNode(wrapExpr) } -func parseAnyExpression(nativeExpr hclsyntax.Expression, from inputTokens) *node { - expr := newExpression() - children := expr.children +// parseObjectConsExpr parses an object-construct key expression. +// An object key is an Identifier or an Expression. In this context, a quoted +// literal is functionally equivalent to an Identifier. +func parseObjectConsKeyExpr(nativeExpr *hclsyntax.ObjectConsKeyExpr, from inputTokens) *node { + expr := parseExpression(nativeExpr.Wrapped, from) + wrapExpr := newObjectConsKeyExpr(expr) - nativeVars := nativeExpr.Variables() + return newNode(wrapExpr) +} - for _, nativeTraversal := range nativeVars { - before, traversal, after := parseTraversal(nativeTraversal, from) - children.AppendUnstructuredTokens(before.Tokens()) - children.AppendNode(traversal) - expr.absTraversals.Add(traversal) - from = after - } - // Attach any stragglers that don't belong to a traversal to the expression - // itself. In an expression with no traversals at all, this is just the - // entirety of "from". - children.AppendUnstructuredTokens(from.Tokens()) +func parseTemplateExpr(nativeExpr *hclsyntax.TemplateExpr, from inputTokens) *node { + if nativeExpr.IsStringLiteral() { + quoted := newQuoted(from.writerTokens) - return newNode(expr) + expr := newExpression() + expr.wrapped = expr.children.Append(quoted) + return newNode(expr) + } else { + return parseAnyExpression(nativeExpr, from) + } } func parseTraversal(nativeTraversal hcl.Traversal, from inputTokens) (before inputTokens, n *node, after inputTokens) { diff --git a/hclwrite/parser_test.go b/hclwrite/parser_test.go index 344090d6d..3e89352ca 100644 --- a/hclwrite/parser_test.go +++ b/hclwrite/parser_test.go @@ -1238,7 +1238,7 @@ func TestParse(t *testing.T) { Val: "\n", }, { - Type: "ObjectConsKey", + Type: "ObjectConsKeyExpr", Children: []TestTreeNode{ { Type: "Expression", @@ -1252,7 +1252,7 @@ func TestParse(t *testing.T) { Children: []TestTreeNode{ { Type: "identifier", - Val: "hat", + Val: " hat", }, }, }, @@ -1290,7 +1290,7 @@ func TestParse(t *testing.T) { Val: ",", }, { - Type: "ObjectConsKey", + Type: "ObjectConsKeyExpr", Children: []TestTreeNode{ { Type: "Expression", @@ -1303,8 +1303,13 @@ func TestParse(t *testing.T) { Type: "Traversal", Children: []TestTreeNode{ { - Type: "TraverseName", - Children: []TestTreeNode{{Type: "identifier", Val: "cat"}}, + Type: "TraverseName", + Children: []TestTreeNode{ + { + Type: "identifier", + Val: "cat", + }, + }, }, }, }, @@ -1392,7 +1397,7 @@ func TestParse(t *testing.T) { Val: "\n", }, { - Type: "ObjectConsKey", + Type: "ObjectConsKeyExpr", Children: []TestTreeNode{ { Type: "Expression", @@ -1406,7 +1411,7 @@ func TestParse(t *testing.T) { Children: []TestTreeNode{ { Type: "identifier", - Val: "hat", + Val: " hat", }, }, }, @@ -1444,7 +1449,7 @@ func TestParse(t *testing.T) { Val: " // a fancy hat\n", }, { - Type: "ObjectConsKey", + Type: "ObjectConsKeyExpr", Children: []TestTreeNode{ { Type: "Expression", @@ -1544,10 +1549,16 @@ func TestParse(t *testing.T) { Val: "\n", }, { - Type: "ObjectConsKey", + Type: "ObjectConsKeyExpr", Children: []TestTreeNode{ { - Type: "quoted", Val: ` "hat"`, + Type: "Expression", + Children: []TestTreeNode{ + { + Type: "quoted", + Val: ` "hat"`, + }, + }, }, }, }, From 91e9a7694f72a8974fbcca8bf46c587b2c155f14 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Fri, 19 Jun 2026 00:05:34 -0400 Subject: [PATCH 11/13] wip --- hclwrite/parser.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hclwrite/parser.go b/hclwrite/parser.go index ab10514c4..c7f911108 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -475,6 +475,8 @@ func parseObjectConsKeyExpr(nativeExpr *hclsyntax.ObjectConsKeyExpr, from inputT return newNode(wrapExpr) } +// parseObjectConsExpr is specifically interested in string literal expressions +// at this time. func parseTemplateExpr(nativeExpr *hclsyntax.TemplateExpr, from inputTokens) *node { if nativeExpr.IsStringLiteral() { quoted := newQuoted(from.writerTokens) From 88a6bc43ebe01df7dea58d7e362b6ae947944088 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Wed, 17 Jun 2026 16:10:06 -0400 Subject: [PATCH 12/13] hclwrite: add ObjectConsExpr.SetItemRaw() and ObjectConsExpr.ItemFor() --- hclwrite/ast_expression.go | 4 + hclwrite/ast_object_cons_expr.go | 69 +++++++++- hclwrite/ast_object_cons_expr_test.go | 189 ++++++++++++++++++++++++++ hclwrite/parser.go | 33 ++++- hclwrite/parser_test.go | 40 +----- hclwrite/unwrap.go | 25 ++++ 6 files changed, 314 insertions(+), 46 deletions(-) create mode 100644 hclwrite/ast_object_cons_expr_test.go create mode 100644 hclwrite/unwrap.go diff --git a/hclwrite/ast_expression.go b/hclwrite/ast_expression.go index e653c6e56..3f4f69206 100644 --- a/hclwrite/ast_expression.go +++ b/hclwrite/ast_expression.go @@ -18,6 +18,10 @@ type Expression struct { wrapped *node } +func (e *Expression) unwrap() *node { + return e.wrapped +} + func newExpression() *Expression { return &Expression{ inTree: newInTree(), diff --git a/hclwrite/ast_object_cons_expr.go b/hclwrite/ast_object_cons_expr.go index 58ed5c376..181b4b42e 100644 --- a/hclwrite/ast_object_cons_expr.go +++ b/hclwrite/ast_object_cons_expr.go @@ -3,7 +3,10 @@ package hclwrite -import "strings" +import ( + "reflect" + "strings" +) type ObjectConsExpr struct { inTree @@ -18,14 +21,14 @@ func newObjectConsExpr() *ObjectConsExpr { } } -func (o *ObjectConsExpr) ValueFor(key string) *ObjectConsValue { +func (o *ObjectConsExpr) ItemFor(key string) *ObjectConsItem { for _, n := range o.items.List() { if item, ok := n.content.(*ObjectConsItem); ok { k := item.key.content.(*ObjectConsKeyExpr) maybeKey := k.String() if maybeKey == key { - return item.value.content.(*ObjectConsValue) + return item } } } @@ -33,6 +36,49 @@ func (o *ObjectConsExpr) ValueFor(key string) *ObjectConsValue { return nil } +func (o *ObjectConsExpr) ValueFor(key string) *ObjectConsValue { + if item := o.ItemFor(key); item == nil { + return nil + } else { + return item.value.content.(*ObjectConsValue) + } +} + +// SetItemRaw either replaces the expression of an existing item of the given +// name or adds a new item definition to the end of the object, using the given +// tokens verbatim as the expression. +// +// The same caveats apply to this function as for NewExpressionRaw on which it +// is based. If possible, prefer to use SetItemValue or SetItemTraversal. +func (o *ObjectConsExpr) SetItemRaw(key string, tokens Tokens) (k *ObjectConsKeyExpr, v *ObjectConsValue) { + item := o.ItemFor(key) + expr := NewExpressionRaw(tokens) + if item != nil { + k = item.key.content.(*ObjectConsKeyExpr) + v = item.value.content.(*ObjectConsValue) + + v.expr.list.Clear() + v.expr = v.expr.ReplaceWith(expr) + v.children.AppendNode(v.expr) + + } else { + item = newObjectConsItem() + + ident := newIdentifier(TokensForIdentifier(key)[0]) + k = newObjectConsKeyExpr(newNode(ident)) + item.key = item.children.Append(k) + + v = newObjectConsValue() // TODO: expr in constructor + v.expr = v.children.Append(expr) + item.value = item.children.Append(v) + + node := newNode(item) + o.children.AppendNode(node) + o.items.Add(node) + } + return +} + type ObjectConsItem struct { inTree key *node @@ -48,8 +94,11 @@ func newObjectConsItem() *ObjectConsItem { type ObjectConsKeyExpr struct { inTree - literalName string - wrapped *node + wrapped *node +} + +func (k *ObjectConsKeyExpr) unwrap() *node { + return k.wrapped } func newObjectConsKeyExpr(wrapped *node) *ObjectConsKeyExpr { @@ -67,9 +116,15 @@ func (k *ObjectConsKeyExpr) String() string { return "" } - if t, ok := k.wrapped.content.(*Traversal); ok && len(t.steps.List()) > 0 { + unwrapped := unwrapUntilType(k.wrapped, reflect.TypeFor[*identifier]()) + if unwrapped == nil { + return "" + } + + if ident, ok := unwrapped.content.(*identifier); ok { var b strings.Builder - tok := t.steps.List()[0].BuildTokens(nil) + tok := ident.BuildTokens(nil) + format(tok) // removes any SpacesBefore tok.WriteTo(&b) return b.String() diff --git a/hclwrite/ast_object_cons_expr_test.go b/hclwrite/ast_object_cons_expr_test.go new file mode 100644 index 000000000..02989e610 --- /dev/null +++ b/hclwrite/ast_object_cons_expr_test.go @@ -0,0 +1,189 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package hclwrite + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + +func TestObjectConsExprValueFor(t *testing.T) { + ident := newIdentifier(TokensForIdentifier("hat")[0]) + key := newObjectConsKeyExpr(newNode(ident)) + + valueExpr := NewExpressionRaw(TokensForValue(cty.StringVal("fez"))) + value := newObjectConsValue() + value.expr = value.children.Append(valueExpr) + + item := newObjectConsItem() + item.key = item.children.Append(key) + item.value = item.children.Append(value) + + object := newObjectConsExpr() + object.items.Add(object.children.Append(item)) + + var actual strings.Builder + object.ValueFor("hat").expr.BuildTokens(nil).WriteTo(&actual) + expected := `"fez"` + if diff := cmp.Diff(actual.String(), expected); len(diff) > 0 { + t.Error(diff) + } +} + +func TestObjectConsExprSetItemRaw(t *testing.T) { + tests := []struct { + src string + attrName string + key string + tokens Tokens + want Tokens + }{ + { + `a = { + hat = "derby", (cat) = "calico" }` + "\n", + "a", + "hat", + Tokens{ + { + Type: hclsyntax.TokenOQuote, + Bytes: []byte(`"`), + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenQuotedLit, + Bytes: []byte(`bowler`), + }, + { + Type: hclsyntax.TokenCQuote, + Bytes: []byte(`"`), + }, + }, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte{'a'}, + }, + { + Type: hclsyntax.TokenEqual, + Bytes: []byte{'='}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte("\n"), + }, + { + Type: hclsyntax.TokenIdent, + Bytes: []byte("hat"), + }, + { + Type: hclsyntax.TokenEqual, + Bytes: []byte{'='}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenOQuote, + Bytes: []byte{'"'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenQuotedLit, + Bytes: []byte(`bowler`), + }, + { + Type: hclsyntax.TokenCQuote, + Bytes: []byte{'"'}, + }, + { + Type: hclsyntax.TokenComma, + Bytes: []byte{','}, + }, + { + Type: hclsyntax.TokenOParen, + Bytes: []byte{'('}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenIdent, + Bytes: []byte("cat"), + }, + { + Type: hclsyntax.TokenCParen, + Bytes: []byte{')'}, + }, + { + Type: hclsyntax.TokenEqual, + Bytes: []byte{'='}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenOQuote, + Bytes: []byte{'"'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenQuotedLit, + Bytes: []byte("calico"), + }, + { + Type: hclsyntax.TokenCQuote, + Bytes: []byte{'"'}, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte("\n"), + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s = %s in %s", test.attrName, test.tokens.Bytes(), test.src), func(t *testing.T) { + f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1}) + if len(diags) != 0 { + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + t.Fatalf("unexpected diagnostics") + } + + attr := f.Body().GetAttribute(test.attrName) + if attr == nil { + t.Fatal("attr nil") + } + expr := attr.Expr().AsObjectConsExpr() + expr.SetItemRaw(test.key, test.tokens) + + got := f.BuildTokens(nil) + format(got) + if !reflect.DeepEqual(got, test.want) { + diff := cmp.Diff(test.want, got) + t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff) + } + }) + } +} diff --git a/hclwrite/parser.go b/hclwrite/parser.go index c7f911108..5cc61e6ac 100644 --- a/hclwrite/parser.go +++ b/hclwrite/parser.go @@ -469,10 +469,37 @@ func parseObjectConsExpr(nativeExpr *hclsyntax.ObjectConsExpr, from inputTokens) // An object key is an Identifier or an Expression. In this context, a quoted // literal is functionally equivalent to an Identifier. func parseObjectConsKeyExpr(nativeExpr *hclsyntax.ObjectConsKeyExpr, from inputTokens) *node { - expr := parseExpression(nativeExpr.Wrapped, from) - wrapExpr := newObjectConsKeyExpr(expr) + var objectConsKeyExpr *ObjectConsKeyExpr + + switch wrapped := nativeExpr.Wrapped.(type) { + + // a = { + // hat = "derby" } + // appends ObjectConsKeyExpr => identifier + case *hclsyntax.ScopeTraversalExpr: + expr := newNode(newIdentifier(from.writerTokens[0])) + objectConsKeyExpr = newObjectConsKeyExpr(expr) + + // a = { + // (var.hat) = "derby" } + // => appends ObjectConsKeyExpr => Expression + case *hclsyntax.ParenthesesExpr: + expr := parseAnyExpression(wrapped, from) + objectConsKeyExpr = newObjectConsKeyExpr(expr) + + // a = { + // "a hat" = "derby" } + // => appends ObjectConsKeyExpr => Expression => quoted + case *hclsyntax.TemplateExpr: + expr := parseTemplateExpr(wrapped, from) + objectConsKeyExpr = newObjectConsKeyExpr(expr) - return newNode(wrapExpr) + default: + expr := parseAnyExpression(wrapped, from) + objectConsKeyExpr = newObjectConsKeyExpr(expr) + } + + return newNode(objectConsKeyExpr) } // parseObjectConsExpr is specifically interested in string literal expressions diff --git a/hclwrite/parser_test.go b/hclwrite/parser_test.go index 3e89352ca..2bd5d7cf3 100644 --- a/hclwrite/parser_test.go +++ b/hclwrite/parser_test.go @@ -1241,24 +1241,8 @@ func TestParse(t *testing.T) { Type: "ObjectConsKeyExpr", Children: []TestTreeNode{ { - Type: "Expression", - Children: []TestTreeNode{ - - { - Type: "Traversal", - Children: []TestTreeNode{ - { - Type: "TraverseName", - Children: []TestTreeNode{ - { - Type: "identifier", - Val: " hat", - }, - }, - }, - }, - }, - }, + Type: "identifier", + Val: " hat", }, }, }, @@ -1400,24 +1384,8 @@ func TestParse(t *testing.T) { Type: "ObjectConsKeyExpr", Children: []TestTreeNode{ { - Type: "Expression", - Children: []TestTreeNode{ - - { - Type: "Traversal", - Children: []TestTreeNode{ - { - Type: "TraverseName", - Children: []TestTreeNode{ - { - Type: "identifier", - Val: " hat", - }, - }, - }, - }, - }, - }, + Type: "identifier", + Val: " hat", }, }, }, diff --git a/hclwrite/unwrap.go b/hclwrite/unwrap.go new file mode 100644 index 000000000..fe228fcaa --- /dev/null +++ b/hclwrite/unwrap.go @@ -0,0 +1,25 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package hclwrite + +import "reflect" + +type unwrapNode interface { + unwrap() *node +} + +// cross-reference: hcl.UnwrapExpressionUntil +func unwrapUntilType(n *node, typ reflect.Type) *node { +TEST: + if n == nil || reflect.TypeOf(n.content) == typ { + return n + } + + if un, ok := n.content.(unwrapNode); ok { + n = un.unwrap() + goto TEST + } + + return nil +} From 5999eae672bbc2873e4510975d8dc7000d13ce34 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Wed, 24 Jun 2026 23:54:55 -0400 Subject: [PATCH 13/13] add ObjectConsExpr.Items() --- hclwrite/ast_object_cons_expr.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/hclwrite/ast_object_cons_expr.go b/hclwrite/ast_object_cons_expr.go index 181b4b42e..7f252ef4e 100644 --- a/hclwrite/ast_object_cons_expr.go +++ b/hclwrite/ast_object_cons_expr.go @@ -21,6 +21,17 @@ func newObjectConsExpr() *ObjectConsExpr { } } +func (o *ObjectConsExpr) Items() []*ObjectConsItem { + list := o.items.List() + items := make([]*ObjectConsItem, 0, len(list)) + + for _, n := range list { + items = append(items, n.content.(*ObjectConsItem)) + } + + return items +} + func (o *ObjectConsExpr) ItemFor(key string) *ObjectConsItem { for _, n := range o.items.List() { if item, ok := n.content.(*ObjectConsItem); ok {