| // 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 | |
| } |