Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 1 addition & 111 deletions internal/addrs/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,117 +382,7 @@ func ParseAbsActionInstanceStr(str string) (AbsActionInstance, tfdiags.Diagnosti
// If error diagnostics are returned then the AbsResource value is invalid and
// must not be used.
func ParseAbsActionInstance(traversal hcl.Traversal) (AbsActionInstance, tfdiags.Diagnostics) {
moduleAddr, remain, diags := parseModuleInstancePrefix(traversal, false)
if diags.HasErrors() {
return AbsActionInstance{}, diags
}

if remain.IsRelative() {
// (relative means that there's either nothing left or what's next isn't an identifier)
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid action address",
Detail: "Module path must be followed by an action instance address.",
Subject: remain.SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}

if remain.RootName() != "action" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action address must start with \"action.\".",
Subject: remain[0].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
remain = remain[1:]

if len(remain) < 2 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action specification must include an action type and name.",
Subject: remain.SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}

var actionType, name string
switch tt := remain[0].(type) {
case hcl.TraverseRoot:
actionType = tt.Name
case hcl.TraverseAttr:
actionType = tt.Name
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid action address",
Detail: "An action name is required.",
Subject: remain[0].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}

switch tt := remain[1].(type) {
case hcl.TraverseAttr:
name = tt.Name
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "An action name is required.",
Subject: remain[1].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}

remain = remain[2:]
switch len(remain) {
case 0:
return moduleAddr.ActionInstance(actionType, name, NoKey), diags
case 1:
switch tt := remain[0].(type) {
case hcl.TraverseIndex:
key, err := ParseInstanceKey(tt.Key)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: fmt.Sprintf("Invalid resource instance key: %s.", err),
Subject: remain[0].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
return moduleAddr.ActionInstance(actionType, name, key), diags
case hcl.TraverseSplat:
// Not yet supported!
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action instance key must be given in square brackets.",
Subject: remain[0].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action instance key must be given in square brackets.",
Subject: remain[0].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Unexpected extra operators after address.",
Subject: remain[1].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
return parseTargetAction(traversal, knownInstanceKeys)
}

// ParseAbsAction attempts to interpret the given traversal as an absolute
Expand Down
100 changes: 100 additions & 0 deletions internal/addrs/address_expr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package addrs

import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)

// AddressExpr is an expression referring to an address, in which each
// instance key can be given by an arbitrary expression rather than only by a
// constant, such as aws_instance.example[each.key].
//
// An AddressExpr has the same steps as the traversal for the address, except
// that each instance key given by an expression is kept as that expression
// for the caller to evaluate.
type AddressExpr []AddressExprStep

// AddressExprStep is a single step of an AddressExpr, which is either a
// static traversal step or an instance key given by an expression.
type AddressExprStep struct {
// Step is the static traversal step, when Key is nil.
Step hcl.Traverser

// Key is the expression for an instance key which isn't a constant.
Key hcl.Expression
}

// ParseAddressExpr returns the AddressExpr for the given expression.
//
// Expressions which aren't from the HCL native syntax must be static
// traversals, without any instance keys given by expressions.
func ParseAddressExpr(expr hcl.Expression) (AddressExpr, hcl.Diagnostics) {
switch e := expr.(type) {
case *hclsyntax.RelativeTraversalExpr:
ret, diags := ParseAddressExpr(e.Source)
if diags.HasErrors() {
return nil, diags
}
return ret.appendTraversal(e.Traversal), diags

case *hclsyntax.ScopeTraversalExpr:
return AddressExpr(nil).appendTraversal(e.Traversal), nil

case *hclsyntax.IndexExpr:
ret, diags := ParseAddressExpr(e.Collection)
if diags.HasErrors() {
return nil, diags
}
return append(ret, AddressExprStep{Key: e.Key}), diags

default:
traversal, diags := hcl.AbsTraversalForExpr(expr)
if diags.HasErrors() {
return nil, diags
}
return AddressExpr(nil).appendTraversal(traversal), diags
}
}

func (e AddressExpr) appendTraversal(traversal hcl.Traversal) AddressExpr {
for _, step := range traversal {
e = append(e, AddressExprStep{Step: step})
}
return e
}

// Traversal returns the traversal for the address, where each instance key
// given by an expression is replaced with the value returned by evalKey. If
// that value is unknown, the key is a wildcard when the traversal is parsed
// as a pattern or partial-expanded address.
//
// If evalKey is nil, each instance key given by an expression is instead a
// wildcard, as if written as [*].
func (e AddressExpr) Traversal(evalKey func(hcl.Expression) (cty.Value, hcl.Diagnostics)) (hcl.Traversal, hcl.Diagnostics) {
var diags hcl.Diagnostics
ret := make(hcl.Traversal, 0, len(e))
for _, step := range e {
if step.Key == nil {
ret = append(ret, step.Step)
continue
}

rng := step.Key.Range()
if evalKey == nil {
ret = append(ret, hcl.TraverseSplat{SrcRange: rng})
continue
}

key, moreDiags := evalKey(step.Key)
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
return nil, diags
}
ret = append(ret, hcl.TraverseIndex{Key: key, SrcRange: rng})
}
return ret, diags
}
93 changes: 93 additions & 0 deletions internal/addrs/address_expr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package addrs

import (
"testing"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)

func TestAddressExpr(t *testing.T) {
// evalKey evaluates each key with a fixed value for each.key.
evalKey := func(eachKey cty.Value) func(hcl.Expression) (cty.Value, hcl.Diagnostics) {
return func(expr hcl.Expression) (cty.Value, hcl.Diagnostics) {
return expr.Value(&hcl.EvalContext{
Variables: map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{"key": eachKey}),
},
})
}
}

for name, tc := range map[string]struct {
expr string
evalKey func(hcl.Expression) (cty.Value, hcl.Diagnostics)
want string
wantErr string
}{
"static address": {
expr: `module.m[0].test_instance.foo["a"]`,
want: `module.m[0].test_instance.foo["a"]`,
},
"evaluated keys": {
expr: `module.m[each.key].test_instance.foo[each.key]`,
evalKey: evalKey(cty.StringVal("a")),
want: `module.m["a"].test_instance.foo["a"]`,
},
"unknown keys": {
expr: `module.m[each.key].test_instance.foo`,
evalKey: evalKey(cty.UnknownVal(cty.String)),
want: `module.m[*].test_instance.foo`,
},
"keys without an evaluator": {
expr: `module.m[each.key].module.n[0].test_instance.foo[each.key]`,
want: `module.m[*].module.n[0].test_instance.foo[*]`,
},
"key evaluation error": {
expr: `test_instance.foo[local.key]`,
evalKey: evalKey(cty.StringVal("a")),
wantErr: "Unknown variable",
},
"not an address": {
expr: `upper(test_instance.foo)`,
wantErr: "Invalid expression",
},
} {
t.Run(name, func(t *testing.T) {
expr, hclDiags := hclsyntax.ParseExpression([]byte(tc.expr), "", hcl.InitialPos)
if hclDiags.HasErrors() {
t.Fatal(hclDiags.Error())
}

addrExpr, diags := ParseAddressExpr(expr)
var traversal hcl.Traversal
if !diags.HasErrors() {
traversal, diags = addrExpr.Traversal(tc.evalKey)
}
if tc.wantErr != "" {
if !diags.HasErrors() {
t.Fatalf("unexpected success\nwant error: %s", tc.wantErr)
}
if got := diags[0].Summary; got != tc.wantErr {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, tc.wantErr)
}
return
}
if diags.HasErrors() {
t.Fatal(diags.Error())
}

target, targetDiags := ParseTarget(traversal)
if targetDiags.HasErrors() {
t.Fatal(targetDiags.Err())
}
if got := target.String(); got != tc.want {
t.Errorf("wrong address\ngot: %s\nwant: %s", got, tc.want)
}
})
}
}
4 changes: 2 additions & 2 deletions internal/addrs/checkable.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func ParseCheckableStr(kind CheckableKind, src string) (Checkable, tfdiags.Diagn
return nil, diags
}

path, remain, diags := parseModuleInstancePrefix(traversal, false)
path, remain, diags := parseModuleInstancePrefix(traversal, knownInstanceKeys)
if diags.HasErrors() {
return nil, diags
}
Expand Down Expand Up @@ -158,7 +158,7 @@ func ParseCheckableStr(kind CheckableKind, src string) (Checkable, tfdiags.Diagn
// might be a resource whose type is literally "output".
switch kind {
case CheckableResource:
riAddr, moreDiags := parseResourceInstanceUnderModule(path, false, remain)
riAddr, moreDiags := parseResourceInstanceUnderModule(path, knownInstanceKeys, remain)
diags = diags.Append(moreDiags)
if diags.HasErrors() {
return nil, diags
Expand Down
Loading
Loading