blob: e79e431c1fc7e98264103b2f2d034d96c560e2eb [file] [edit]
// 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) {
var calls []hcl.Traversal
if h.dynblock {
// When we're doing dynblock expansion we need to do a little
// more work to also detect functions 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.Functions directly.
calls = dynblock.FunctionsHCLDec(h.body, h.spec)
} else {
calls = hcldec.Functions(h.body, h.spec)
}
for _, traversal := range calls {
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] {
// 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) {
// We intentionally ignore diagnostics here since we're just making a
// best-effort static analysis in preparation for a later call to
// [hclBodyJustAttributes.Evaluate], which will then perform this
// same operation and return its diagnostics. Whatever subset of
// the attributes this function returns should all be valid enough
// for the subsequent analysis.
attrs, _ := h.body.JustAttributes()
for _, attr := range attrs {
withFuncs, ok := attr.Expr.(hcl.ExpressionWithFunctions)
if !ok {
continue
}
for _, traversal := range withFuncs.Functions() {
wantMore := yield(&hcl.StaticCall{
Name: traversal.RootName(),
NameRange: traversal.SourceRange(),
})
if !wantMore {
return
}
}
}
}
}
// 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
}