From a777ada6a1aa504374f53c565df8a871e426d999 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 16 Jun 2026 17:21:45 -0400 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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())