mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-21 10:47:34 -05:00
It doesn't hurt to be liberal in handling this everywhere that we know there's an error because applying the mark where it was already present is effectively a no-op, so we'll introduce more of these both for robustness and to help folks who are reading this code in future to learn the EvalError patterns by observation. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
200 lines
7.0 KiB
Go
200 lines
7.0 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package exprs
|
|
|
|
import (
|
|
"context"
|
|
"iter"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// Evaluate attempts to evaluate the given [Evaluable] in the given [Scope],
|
|
// either returning the resulting value or some error diagnostics describing
|
|
// problems that prevented successful evaluation.
|
|
//
|
|
// Some [Evaluable] implementations (or the symbols they refer to) can block
|
|
// on potentially-time-consuming operations, in which case they should respond
|
|
// gracefully to cancellation of the given context.
|
|
//
|
|
// It's valid to pass a nil Scope, representing that no symbols or functions
|
|
// are available at all. Note that HCL's JSON syntax treats that situation
|
|
// quite differently by taking JSON strings totally literally instead of
|
|
// trying to interpret them as HCL templates, and so switching to or from
|
|
// a nil scope is typically a breaking change for what's allowed in a
|
|
// particular position.
|
|
func Evaluate(ctx context.Context, what Evalable, scope Scope) (cty.Value, tfdiags.Diagnostics) {
|
|
hclCtx, diags := buildHCLEvalContext(ctx, what, scope)
|
|
if diags.HasErrors() {
|
|
return cty.DynamicVal.Mark(EvalError), diags
|
|
}
|
|
val, moreDiags := what.Evaluate(ctx, hclCtx)
|
|
diags = diags.Append(moreDiags)
|
|
return EvalResult(val, diags)
|
|
}
|
|
|
|
func buildHCLEvalContext(ctx context.Context, what Evalable, scope Scope) (*hcl.EvalContext, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
ret := &hcl.EvalContext{}
|
|
if scope == nil {
|
|
// A nil scope represents that nothing at all is available, which HCL
|
|
// represents as an EvalContext with nothing defined inside it.
|
|
// Note that this causes significantly different behavior for HCL's
|
|
// JSON syntax.
|
|
return ret, diags
|
|
}
|
|
|
|
symbols, moreDiags := buildSymbolTable(ctx, what.References(), scope)
|
|
ret.Variables = symbols
|
|
diags = diags.Append(moreDiags)
|
|
|
|
funcs, moreDiags := buildFunctionTable(ctx, what.FunctionCalls(), scope)
|
|
ret.Functions = funcs
|
|
diags = diags.Append(moreDiags)
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
func buildSymbolTable(ctx context.Context, refs iter.Seq[hcl.Traversal], scope Scope) (map[string]cty.Value, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// We'll first build an intermediate representation of the nested symbol
|
|
// tables, because a cty.ObjectVal result is immutable and so we need to
|
|
// collect up all of the attribute values in a mutable data structure
|
|
// and then freeze it into an immutable tree once complete.
|
|
nodes := make(map[string]*symbolTableTempNode)
|
|
References:
|
|
for traversal := range refs {
|
|
currentChildren := nodes
|
|
currentTable := SymbolTable(scope)
|
|
for i, step := range traversal {
|
|
// For our purposes here the distinction between TraverseAttr
|
|
// and TraverseRoot is unimportant, so we'll normalize to
|
|
// TraverseAttr.
|
|
if rootStep, ok := step.(hcl.TraverseRoot); ok {
|
|
step = hcl.TraverseAttr{
|
|
Name: rootStep.Name,
|
|
SrcRange: rootStep.SrcRange,
|
|
}
|
|
}
|
|
|
|
switch step := step.(type) {
|
|
case hcl.TraverseAttr:
|
|
attr, moreDiags := currentTable.ResolveAttr(step)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
continue
|
|
}
|
|
|
|
switch attr := attr.(type) {
|
|
case nestedSymbolTable:
|
|
if _, ok := currentChildren[step.Name]; !ok {
|
|
currentChildren[step.Name] = &symbolTableTempNode{
|
|
children: make(map[string]*symbolTableTempNode),
|
|
}
|
|
}
|
|
currentChildren = currentChildren[step.Name].children
|
|
currentTable = attr.SymbolTable
|
|
continue
|
|
case valueOf:
|
|
currentChildren[step.Name] = &symbolTableTempNode{
|
|
val: attr.Valuer,
|
|
remain: traversal[i+1:],
|
|
}
|
|
continue References // any remaining steps are dynamic steps through the final value, captured in "remain" above
|
|
}
|
|
default:
|
|
moreDiags := currentTable.HandleInvalidStep(tfdiags.SourceRangeFromHCL(step.SourceRange()))
|
|
diags = diags.Append(moreDiags)
|
|
continue References
|
|
}
|
|
}
|
|
// If we get here then we ran out of steps before we encountered a
|
|
// leaf Valuer, so this reference is incomplete and therefore invalid.
|
|
moreDiags := currentTable.HandleInvalidStep(tfdiags.SourceRangeFromHCL(traversal.SourceRange()))
|
|
diags = diags.Append(moreDiags)
|
|
}
|
|
|
|
ret, moreDiags := valuesForSymbolTableTempNodes(ctx, nodes)
|
|
diags = diags.Append(moreDiags)
|
|
return ret, diags
|
|
}
|
|
|
|
// symbolTableTempNode is an implementation detail of [buildSymbolTable] used
|
|
// as a mutable intermediate representation of the symbol table so that we
|
|
// can gradually assemble nested symbol tables and then turn them into
|
|
// immutable cty object values only at the end when the work is complete.
|
|
//
|
|
// A value of this type has EITHER val+remain or children alone populated.
|
|
type symbolTableTempNode struct {
|
|
val Valuer
|
|
remain hcl.Traversal
|
|
|
|
children map[string]*symbolTableTempNode
|
|
}
|
|
|
|
// valuesForSymbolTableTempNodes returns a map of [cty.Value] providing a
|
|
// frozen representation of the symbol tree rooted at the given map.
|
|
func valuesForSymbolTableTempNodes(ctx context.Context, symbols map[string]*symbolTableTempNode) (map[string]cty.Value, tfdiags.Diagnostics) {
|
|
if len(symbols) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var diags tfdiags.Diagnostics
|
|
ret := make(map[string]cty.Value, len(symbols))
|
|
for name, node := range symbols {
|
|
if node.val != nil {
|
|
// This is a leaf node, with a dynamic value associated with it.
|
|
moreDiags := node.val.StaticCheckTraversal(node.remain)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
ret[name] = AsEvalError(cty.DynamicVal)
|
|
continue
|
|
}
|
|
|
|
// When we take the value of the object being referred to we
|
|
// intentionally ignore any _indirect_ diagnostics that might
|
|
// cause because we only want to report diagnostics that are
|
|
// directly related to the expression we're currently
|
|
// evaluating. Whatever problems might exist in the definition
|
|
// of what we're referring to must be caught by visiting
|
|
// that thing and evaluating it directly. This is safe to do
|
|
// because the definition of Valuer requires that Value must
|
|
// always return some reasonable placeholder value to use even
|
|
// when an error occurs.
|
|
val, _ := node.val.Value(ctx)
|
|
ret[name] = val
|
|
continue
|
|
}
|
|
|
|
// This is a nested symbol table, so we'll analyze its content recursively.
|
|
childVals, moreDiags := valuesForSymbolTableTempNodes(ctx, node.children)
|
|
diags = diags.Append(moreDiags)
|
|
ret[name] = cty.ObjectVal(childVals)
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
func buildFunctionTable(_ context.Context, calls iter.Seq[*hcl.StaticCall], scope Scope) (map[string]function.Function, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
ret := make(map[string]function.Function)
|
|
for call := range calls {
|
|
funcName := call.Name
|
|
impl, moreDiags := scope.ResolveFunc(call)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
continue
|
|
}
|
|
ret[funcName] = impl
|
|
}
|
|
return ret, diags
|
|
}
|