Files
opentf/internal/lang/exprs/evaluate.go
Martin Atkins aa4743f23f lang/eval+lang/exprs: More exprs.EvalError markings
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>
2025-10-27 10:15:41 -07:00

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
}