Files
opentf/internal/lang/exprs/evalable.go
Martin Atkins 639dff15c3 lang/eval: Clear some linter warnings
Based on discussion so far it seems that we have consensus about merging
the lang/eval and engine-related commits that precede this one as a
starting point for ongoing work in future (hopefully smaller) PRs.

This commit therefore mostly just acknowledges a small amount of unused
code in a way that it won't cause linter noise, since the future work is
likely to make these be used and so it'll be helpful to have them around as
examples to build on. (Which is, after all, the whole point of merging this
as dead code in the first place!)

This does _actually_ remove an old, now-redundant declaration of the
"compiled module instance" interface though, since that eventually ended
up in a different place and I just forgot to clean up this initial form
of it in later refactoring.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-27 10:15:41 -07:00

365 lines
14 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"
"slices"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/dynblock"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// Evalable is implemented by types that encapsulate expressions that can be
// evaluated in some evaluation scope decided by a caller.
//
// An Evalable implementation must include any supporting metadata needed to
// analyze and evaluate the expressions inside. For example, an [Evalable]
// representing a HCL body must also include the expected schema for that body.
type Evalable interface {
// References returns a sequence of references this Evalable makes to
// values in its containing scope.
References() iter.Seq[hcl.Traversal]
// FunctionCalls returns a sequence of all of the function calls that
// could be made if this were evaluated.
//
// TODO: Perhaps References and FunctionCalls should be combined together
// somehow to return a tree that shows when a reference appears as part
// of an argument to a function, to address the problem described in
// this issue:
// https://github.com/opentofu/opentofu/issues/2630
FunctionCalls() iter.Seq[*hcl.StaticCall]
// Evaluate performs the actual expression evaluation, using the given
// HCL evaluation context to satisfy any references.
//
// Callers must first use the References method to discover what the
// wrapped expressions refer to, and make sure that the given evaluation
// context contains at least the variables required to satisfy those
// references.
//
// This method takes a [context.Context] because some implementations
// may internally block on the completion of a potentially-time-consuming
// operation, in which case they should respond gracefully to the
// cancellation or deadline of the given context.
//
// If Evaluate returns diagnostics then it must also return a suitable
// placeholder value that could be use for downstream expression evaluation
// despite the error. Returning [cty.DynamicVal] is acceptable if all else
// fails, but returning an unknown value with a more specific type
// constraint can give more opportunities to proactively detect downstream
// errors in a single evaluation pass.
Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics)
// ResultTypeConstraint returns a type constrant that all possible results
// from method Evaluate would conform to.
//
// This is used for static type checking. Return [cty.DynamicPseudoType]
// if it's impossible to predict any single type constraint for the
// possible results.
//
// TODO: Some implementations of this would be able to do better if they
// knew the types of everything that'd be passed in hclCtx when calling
// Evaluate. Is there some way we can approximate that?
ResultTypeConstraint() cty.Type
// EvalableSourceRange returns a description of a source location that this
// Evalable was derived from.
EvalableSourceRange() tfdiags.SourceRange
}
// ForcedErrorEvalable returns an [Evalable] that always fails with
// [cty.DynamicVal] as its placeholder result and with the given diagnostics,
// which must include at least one error or this function will panic.
//
// This is primarily intended for unit testing purposes for creating
// placeholders for upstream objects that have failed, but might also be useful
// sometimes for handling early-detected error situations in "real" code.
func ForcedErrorEvalable(diags tfdiags.Diagnostics, sourceRange tfdiags.SourceRange) Evalable {
if !diags.HasErrors() {
panic("ForcedErrorEvalable without any error diagnostics")
}
// We reuse the same type as ForcedErrorValuer here because it can
// implement both interfaces just fine with the information available here.
return forcedErrorValuer{diags, &sourceRange}
}
func StaticCheckTraversal(traversal hcl.Traversal, evalable Evalable) tfdiags.Diagnostics {
return StaticCheckTraversalThroughType(traversal, evalable.ResultTypeConstraint())
}
func StaticCheckTraversalThroughType(traversal hcl.Traversal, typeConstraint cty.Type) tfdiags.Diagnostics {
// We perform a static check by attempting to apply the traversal to
// an unknown value of the given type constraint, which will fail if
// no possible value meeting that type constraint could possibly support
// the traversal.
var diags tfdiags.Diagnostics
placeholder := cty.UnknownVal(typeConstraint)
_, hclDiags := traversal.TraverseRel(placeholder)
diags = diags.Append(hclDiags)
return diags
}
// hclExpression implements [Evalable] for a standalone [hcl.Expression].
type hclExpression struct {
expr hcl.Expression
}
// EvalableHCLExpression returns an [Evalable] that is just a thin wrapper
// around the given HCL expression.
func EvalableHCLExpression(expr hcl.Expression) Evalable {
return hclExpression{expr}
}
// Evaluate implements Evalable.
func (h hclExpression) Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
v, hclDiags := h.expr.Value(hclCtx)
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
v = cty.UnknownVal(h.ResultTypeConstraint()).WithSameMarks(v)
}
return v, diags
}
// FunctionCalls implements Evalable.
func (h hclExpression) FunctionCalls() iter.Seq[*hcl.StaticCall] {
// FIXME: The underlying HCL API somewhat-misuses [hcl.Traversal] to
// enumerate the names of the functions without providing any
// information about their arguments. We can get away with leaving
// the args unpopulated for now since we're not actually relying on
// them but it would be nice to update HCL to return [hcl.StaticCall]
// for the function analysis instead, for consistency with how
// [hcl.ExprCall] works.
expr, ok := h.expr.(hcl.ExpressionWithFunctions)
if !ok {
return func(yield func(*hcl.StaticCall) bool) {}
}
return func(yield func(*hcl.StaticCall) bool) {
for _, traversal := range expr.Functions() {
wantMore := yield(&hcl.StaticCall{
Name: traversal.RootName(),
NameRange: traversal.SourceRange(),
})
if !wantMore {
return
}
}
}
}
// References implements Evalable.
func (h hclExpression) References() iter.Seq[hcl.Traversal] {
return slices.Values(h.expr.Variables())
}
// ResultTypeConstraint implements Evalable.
func (h hclExpression) ResultTypeConstraint() cty.Type {
// We can only predict the result type of the expression if it doesn't
// include any references or function calls.
v, hclDiags := h.expr.Value(nil)
if hclDiags.HasErrors() {
return cty.DynamicPseudoType
}
// For an expression that only uses constants, its type is guaranteed
// to always be the same.
return v.Type()
}
// SourceRange implements Evalable.
func (h hclExpression) EvalableSourceRange() tfdiags.SourceRange {
return tfdiags.SourceRangeFromHCL(h.expr.Range())
}
// hclBody implements [Evalable] for a [hcl.Body] and associated [hcldec.Spec].
type hclBody struct {
body hcl.Body
spec hcldec.Spec
dynblock bool
}
// EvalableHCLBody returns an [Evalable] that evaluates the given HCL body
// using the given [hcldec] specification.
func EvalableHCLBody(body hcl.Body, spec hcldec.Spec) Evalable {
return &hclBody{
body: body,
spec: spec,
dynblock: false,
}
}
// EvalableHCLBodyWithDynamicBlocks is a variant of [EvalableHCLBody] that
// calls [dynblock.Expand] before evaluating the body so that "dynamic" blocks
// would be supported and expanded to their equivalent static blocks.
func EvalableHCLBodyWithDynamicBlocks(body hcl.Body, spec hcldec.Spec) Evalable {
return &hclBody{
body: body,
spec: spec,
dynblock: true,
}
}
// Evaluate implements Evalable.
func (h *hclBody) Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
body := h.body
if h.dynblock {
// [dynblock.Expand] wraps our body so that hcldec.Decode below will
// indirectly cause the "dynamic" blocks to be expanded, using the
// same evaluation context for the for_each expressions.
body = dynblock.Expand(body, hclCtx)
}
v, hclDiags := hcldec.Decode(body, h.spec, hclCtx)
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
v = cty.UnknownVal(h.ResultTypeConstraint()).WithSameMarks(v)
}
return v, diags
}
// FunctionCalls implements Evalable.
func (h hclBody) FunctionCalls() iter.Seq[*hcl.StaticCall] {
// FIXME: The underlying HCL API somewhat-misuses [hcl.Traversal] to
// enumerate the names of the functions without providing any
// information about their arguments. We can get away with leaving
// the args unpopulated for now since we're not actually relying on
// them but it would be nice to update HCL to return [hcl.StaticCall]
// for the function analysis instead, for consistency with how
// [hcl.ExprCall] works.
return func(yield func(*hcl.StaticCall) bool) {
for _, traversal := range hcldec.Functions(h.body, h.spec) {
wantMore := yield(&hcl.StaticCall{
Name: traversal.RootName(),
NameRange: traversal.SourceRange(),
})
if !wantMore {
return
}
}
}
}
// References implements Evalable.
func (h *hclBody) References() iter.Seq[hcl.Traversal] {
if h.dynblock {
// When we're doing dynblock expansion we need to do a little
// more work to also detect references from the for_each
// arguments in the "dynamic" blocks. The dynblock package
// does this additional work for us as long as we ask its
// wrapper function instead of hcldec.Variables directly.
return slices.Values(dynblock.VariablesHCLDec(h.body, h.spec))
}
return slices.Values(hcldec.Variables(h.body, h.spec))
}
// ResultTypeConstraint implements Evalable.
func (h *hclBody) ResultTypeConstraint() cty.Type {
return hcldec.ImpliedType(h.spec)
}
// SourceRange implements Evalable.
func (h *hclBody) EvalableSourceRange() tfdiags.SourceRange {
// The "missing item range" is not necessarily a good range to use here,
// but is the best we can do. At least in HCL native syntax this tends
// to be in the header of the block that contained the body and so
// is _close_ to the body being described.
return tfdiags.SourceRangeFromHCL(h.body.MissingItemRange())
}
// hclBodyJustAttributes implements [Evalable] for a [hcl.Body] using HCL's
// "just attributes" evaluation mode.
type hclBodyJustAttributes struct {
body hcl.Body
}
// EvalableHCLBody returns an [Evalable] that evaluates the given HCL body
// in HCL's "just attributes" mode, and then returns an object value whose
// attribute names and values are derived from the result.
func EvalableHCLBodyJustAttributes(body hcl.Body) Evalable {
return &hclBodyJustAttributes{
body: body,
}
}
// EvalableSourceRange implements Evalable.
func (h *hclBodyJustAttributes) EvalableSourceRange() tfdiags.SourceRange {
return tfdiags.SourceRangeFromHCL(h.body.MissingItemRange())
}
// Evaluate implements Evalable.
func (h *hclBodyJustAttributes) Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
attrs, hclDiags := h.body.JustAttributes()
diags = diags.Append(hclDiags)
// If we have errors already then we might not have the full set of
// attributes that were declared, so we'll return cty.DynamicVal below
// to avoid overpromising that we know the result type. However,
// we'll still visit all of the attributes we _did_ find in case
// that allows us to collect up some more expression-errors to return so we
// can tell the module author about as much as possible at once.
typeKnown := !hclDiags.HasErrors()
retAttrs := make(map[string]cty.Value, len(attrs))
for name, attr := range attrs {
val, hclDiags := attr.Expr.Value(hclCtx)
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
val = AsEvalError(cty.DynamicVal)
}
retAttrs[name] = val
}
if !typeKnown {
return EvalResult(cty.DynamicVal, diags)
}
// We don't use a top-level EvalError here because we selectively
// marked individual attribute values above, and we're confident
// that the set of attribute names in this object is correct because
// the original JustAttributes call succeeded.
return cty.ObjectVal(retAttrs), diags
}
// FunctionCalls implements Evalable.
func (h *hclBodyJustAttributes) FunctionCalls() iter.Seq[*hcl.StaticCall] {
// For now this is not implemented because the underlying HCL API
// isn't the right shape to implement this method.
return func(yield func(*hcl.StaticCall) bool) {}
}
// References implements Evalable.
func (h *hclBodyJustAttributes) References() iter.Seq[hcl.Traversal] {
// This case is annoying because we need to perform the shallow
// JustAttributes call to get the expressions to analyze, but then
// we'll need to call it again in Evaluate once the scope has
// been built. But only if we find this being a performance problem
// should we consider trying to cache this result.
//
// We ignore the diagnostics here because we're just making a best
// effort to learn what traversals we might need and then we'll
// return the same set of diagnostics from Evaluate.
attrs, _ := h.body.JustAttributes()
return func(yield func(hcl.Traversal) bool) {
for _, attr := range attrs {
for _, traversal := range attr.Expr.Variables() {
if !yield(traversal) {
return
}
}
}
}
}
// ResultTypeConstraint implements Evalable.
func (h *hclBodyJustAttributes) ResultTypeConstraint() cty.Type {
// We cannot predict a result type in "just attributes" mode because
// the type depends on the results of the expressions in the body.
return cty.DynamicPseudoType
}