d8ae04bc78
Most of the time, the standard expression decoding built in to HCL is sufficient. Sometimes though, it's useful to be able to customize the decoding of certain arguments where the application intends to use them in a very specific way, such as in static analysis. This extension is an approximate analog of gohcl's support for decoding into an hcl.Expression, allowing hcldec-based applications and applications with custom functions to similarly capture and manipulate the physical expressions used in arguments, rather than their values. This includes one example use-case: the typeexpr extension now includes a cty.Function called ConvertFunc that takes a type expression as its second argument. A type expression is not evaluatable in the usual sense, but thanks to cty capsule types we _can_ produce a cty.Value from one and then make use of it inside the function implementation, without exposing this custom type to the broader language: convert(["foo"], set(string)) This mechanism is intentionally restricted only to "argument-like" locations where there is a specific type we are attempting to decode into. For now, that's hcldec AttrSpec/BlockAttrsSpec -- analogous to gohcl decoding into hcl.Expression -- and in arguments to functions.
1492 lines
40 KiB
Go
1492 lines
40 KiB
Go
package hclsyntax
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/ext/customdecode"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
)
|
|
|
|
// Expression is the abstract type for nodes that behave as HCL expressions.
|
|
type Expression interface {
|
|
Node
|
|
|
|
// The hcl.Expression methods are duplicated here, rather than simply
|
|
// embedded, because both Node and hcl.Expression have a Range method
|
|
// and so they conflict.
|
|
|
|
Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
|
|
Variables() []hcl.Traversal
|
|
StartRange() hcl.Range
|
|
}
|
|
|
|
// Assert that Expression implements hcl.Expression
|
|
var assertExprImplExpr hcl.Expression = Expression(nil)
|
|
|
|
// LiteralValueExpr is an expression that just always returns a given value.
|
|
type LiteralValueExpr struct {
|
|
Val cty.Value
|
|
SrcRange hcl.Range
|
|
}
|
|
|
|
func (e *LiteralValueExpr) walkChildNodes(w internalWalkFunc) {
|
|
// Literal values have no child nodes
|
|
}
|
|
|
|
func (e *LiteralValueExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
return e.Val, nil
|
|
}
|
|
|
|
func (e *LiteralValueExpr) Range() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
func (e *LiteralValueExpr) StartRange() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
// Implementation for hcl.AbsTraversalForExpr.
|
|
func (e *LiteralValueExpr) AsTraversal() hcl.Traversal {
|
|
// This one's a little weird: the contract for AsTraversal is to interpret
|
|
// an expression as if it were traversal syntax, and traversal syntax
|
|
// doesn't have the special keywords "null", "true", and "false" so these
|
|
// are expected to be treated like variables in that case.
|
|
// Since our parser already turned them into LiteralValueExpr by the time
|
|
// we get here, we need to undo this and infer the name that would've
|
|
// originally led to our value.
|
|
// We don't do anything for any other values, since they don't overlap
|
|
// with traversal roots.
|
|
|
|
if e.Val.IsNull() {
|
|
// In practice the parser only generates null values of the dynamic
|
|
// pseudo-type for literals, so we can safely assume that any null
|
|
// was orignally the keyword "null".
|
|
return hcl.Traversal{
|
|
hcl.TraverseRoot{
|
|
Name: "null",
|
|
SrcRange: e.SrcRange,
|
|
},
|
|
}
|
|
}
|
|
|
|
switch e.Val {
|
|
case cty.True:
|
|
return hcl.Traversal{
|
|
hcl.TraverseRoot{
|
|
Name: "true",
|
|
SrcRange: e.SrcRange,
|
|
},
|
|
}
|
|
case cty.False:
|
|
return hcl.Traversal{
|
|
hcl.TraverseRoot{
|
|
Name: "false",
|
|
SrcRange: e.SrcRange,
|
|
},
|
|
}
|
|
default:
|
|
// No traversal is possible for any other value.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ScopeTraversalExpr is an Expression that retrieves a value from the scope
|
|
// using a traversal.
|
|
type ScopeTraversalExpr struct {
|
|
Traversal hcl.Traversal
|
|
SrcRange hcl.Range
|
|
}
|
|
|
|
func (e *ScopeTraversalExpr) walkChildNodes(w internalWalkFunc) {
|
|
// Scope traversals have no child nodes
|
|
}
|
|
|
|
func (e *ScopeTraversalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
val, diags := e.Traversal.TraverseAbs(ctx)
|
|
setDiagEvalContext(diags, e, ctx)
|
|
return val, diags
|
|
}
|
|
|
|
func (e *ScopeTraversalExpr) Range() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
func (e *ScopeTraversalExpr) StartRange() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
// Implementation for hcl.AbsTraversalForExpr.
|
|
func (e *ScopeTraversalExpr) AsTraversal() hcl.Traversal {
|
|
return e.Traversal
|
|
}
|
|
|
|
// RelativeTraversalExpr is an Expression that retrieves a value from another
|
|
// value using a _relative_ traversal.
|
|
type RelativeTraversalExpr struct {
|
|
Source Expression
|
|
Traversal hcl.Traversal
|
|
SrcRange hcl.Range
|
|
}
|
|
|
|
func (e *RelativeTraversalExpr) walkChildNodes(w internalWalkFunc) {
|
|
w(e.Source)
|
|
}
|
|
|
|
func (e *RelativeTraversalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
src, diags := e.Source.Value(ctx)
|
|
ret, travDiags := e.Traversal.TraverseRel(src)
|
|
setDiagEvalContext(travDiags, e, ctx)
|
|
diags = append(diags, travDiags...)
|
|
return ret, diags
|
|
}
|
|
|
|
func (e *RelativeTraversalExpr) Range() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
func (e *RelativeTraversalExpr) StartRange() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
// Implementation for hcl.AbsTraversalForExpr.
|
|
func (e *RelativeTraversalExpr) AsTraversal() hcl.Traversal {
|
|
// We can produce a traversal only if our source can.
|
|
st, diags := hcl.AbsTraversalForExpr(e.Source)
|
|
if diags.HasErrors() {
|
|
return nil
|
|
}
|
|
|
|
ret := make(hcl.Traversal, len(st)+len(e.Traversal))
|
|
copy(ret, st)
|
|
copy(ret[len(st):], e.Traversal)
|
|
return ret
|
|
}
|
|
|
|
// FunctionCallExpr is an Expression that calls a function from the EvalContext
|
|
// and returns its result.
|
|
type FunctionCallExpr struct {
|
|
Name string
|
|
Args []Expression
|
|
|
|
// If true, the final argument should be a tuple, list or set which will
|
|
// expand to be one argument per element.
|
|
ExpandFinal bool
|
|
|
|
NameRange hcl.Range
|
|
OpenParenRange hcl.Range
|
|
CloseParenRange hcl.Range
|
|
}
|
|
|
|
func (e *FunctionCallExpr) walkChildNodes(w internalWalkFunc) {
|
|
for _, arg := range e.Args {
|
|
w(arg)
|
|
}
|
|
}
|
|
|
|
func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
var f function.Function
|
|
exists := false
|
|
hasNonNilMap := false
|
|
thisCtx := ctx
|
|
for thisCtx != nil {
|
|
if thisCtx.Functions == nil {
|
|
thisCtx = thisCtx.Parent()
|
|
continue
|
|
}
|
|
hasNonNilMap = true
|
|
f, exists = thisCtx.Functions[e.Name]
|
|
if exists {
|
|
break
|
|
}
|
|
thisCtx = thisCtx.Parent()
|
|
}
|
|
|
|
if !exists {
|
|
if !hasNonNilMap {
|
|
return cty.DynamicVal, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Function calls not allowed",
|
|
Detail: "Functions may not be called here.",
|
|
Subject: e.Range().Ptr(),
|
|
Expression: e,
|
|
EvalContext: ctx,
|
|
},
|
|
}
|
|
}
|
|
|
|
avail := make([]string, 0, len(ctx.Functions))
|
|
for name := range ctx.Functions {
|
|
avail = append(avail, name)
|
|
}
|
|
suggestion := nameSuggestion(e.Name, avail)
|
|
if suggestion != "" {
|
|
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
|
}
|
|
|
|
return cty.DynamicVal, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Call to unknown function",
|
|
Detail: fmt.Sprintf("There is no function named %q.%s", e.Name, suggestion),
|
|
Subject: &e.NameRange,
|
|
Context: e.Range().Ptr(),
|
|
Expression: e,
|
|
EvalContext: ctx,
|
|
},
|
|
}
|
|
}
|
|
|
|
params := f.Params()
|
|
varParam := f.VarParam()
|
|
|
|
args := e.Args
|
|
if e.ExpandFinal {
|
|
if len(args) < 1 {
|
|
// should never happen if the parser is behaving
|
|
panic("ExpandFinal set on function call with no arguments")
|
|
}
|
|
expandExpr := args[len(args)-1]
|
|
expandVal, expandDiags := expandExpr.Value(ctx)
|
|
diags = append(diags, expandDiags...)
|
|
if expandDiags.HasErrors() {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
switch {
|
|
case expandVal.Type().IsTupleType() || expandVal.Type().IsListType() || expandVal.Type().IsSetType():
|
|
if expandVal.IsNull() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid expanding argument value",
|
|
Detail: "The expanding argument (indicated by ...) must not be null.",
|
|
Subject: expandExpr.Range().Ptr(),
|
|
Context: e.Range().Ptr(),
|
|
Expression: expandExpr,
|
|
EvalContext: ctx,
|
|
})
|
|
return cty.DynamicVal, diags
|
|
}
|
|
if !expandVal.IsKnown() {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
newArgs := make([]Expression, 0, (len(args)-1)+expandVal.LengthInt())
|
|
newArgs = append(newArgs, args[:len(args)-1]...)
|
|
it := expandVal.ElementIterator()
|
|
for it.Next() {
|
|
_, val := it.Element()
|
|
newArgs = append(newArgs, &LiteralValueExpr{
|
|
Val: val,
|
|
SrcRange: expandExpr.Range(),
|
|
})
|
|
}
|
|
args = newArgs
|
|
default:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid expanding argument value",
|
|
Detail: "The expanding argument (indicated by ...) must be of a tuple, list, or set type.",
|
|
Subject: expandExpr.Range().Ptr(),
|
|
Context: e.Range().Ptr(),
|
|
Expression: expandExpr,
|
|
EvalContext: ctx,
|
|
})
|
|
return cty.DynamicVal, diags
|
|
}
|
|
}
|
|
|
|
if len(args) < len(params) {
|
|
missing := params[len(args)]
|
|
qual := ""
|
|
if varParam != nil {
|
|
qual = " at least"
|
|
}
|
|
return cty.DynamicVal, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Not enough function arguments",
|
|
Detail: fmt.Sprintf(
|
|
"Function %q expects%s %d argument(s). Missing value for %q.",
|
|
e.Name, qual, len(params), missing.Name,
|
|
),
|
|
Subject: &e.CloseParenRange,
|
|
Context: e.Range().Ptr(),
|
|
Expression: e,
|
|
EvalContext: ctx,
|
|
},
|
|
}
|
|
}
|
|
|
|
if varParam == nil && len(args) > len(params) {
|
|
return cty.DynamicVal, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Too many function arguments",
|
|
Detail: fmt.Sprintf(
|
|
"Function %q expects only %d argument(s).",
|
|
e.Name, len(params),
|
|
),
|
|
Subject: args[len(params)].StartRange().Ptr(),
|
|
Context: e.Range().Ptr(),
|
|
Expression: e,
|
|
EvalContext: ctx,
|
|
},
|
|
}
|
|
}
|
|
|
|
argVals := make([]cty.Value, len(args))
|
|
|
|
for i, argExpr := range args {
|
|
var param *function.Parameter
|
|
if i < len(params) {
|
|
param = ¶ms[i]
|
|
} else {
|
|
param = varParam
|
|
}
|
|
|
|
var val cty.Value
|
|
if decodeFn := customdecode.CustomExpressionDecoderForType(param.Type); decodeFn != nil {
|
|
var argDiags hcl.Diagnostics
|
|
val, argDiags = decodeFn(argExpr, ctx)
|
|
diags = append(diags, argDiags...)
|
|
if val == cty.NilVal {
|
|
val = cty.UnknownVal(param.Type)
|
|
}
|
|
} else {
|
|
var argDiags hcl.Diagnostics
|
|
val, argDiags = argExpr.Value(ctx)
|
|
if len(argDiags) > 0 {
|
|
diags = append(diags, argDiags...)
|
|
}
|
|
|
|
// Try to convert our value to the parameter type
|
|
var err error
|
|
val, err = convert.Convert(val, param.Type)
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid function argument",
|
|
Detail: fmt.Sprintf(
|
|
"Invalid value for %q parameter: %s.",
|
|
param.Name, err,
|
|
),
|
|
Subject: argExpr.StartRange().Ptr(),
|
|
Context: e.Range().Ptr(),
|
|
Expression: argExpr,
|
|
EvalContext: ctx,
|
|
})
|
|
}
|
|
}
|
|
|
|
argVals[i] = val
|
|
}
|
|
|
|
if diags.HasErrors() {
|
|
// Don't try to execute the function if we already have errors with
|
|
// the arguments, because the result will probably be a confusing
|
|
// error message.
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
resultVal, err := f.Call(argVals)
|
|
if err != nil {
|
|
switch terr := err.(type) {
|
|
case function.ArgError:
|
|
i := terr.Index
|
|
var param *function.Parameter
|
|
if i < len(params) {
|
|
param = ¶ms[i]
|
|
} else {
|
|
param = varParam
|
|
}
|
|
argExpr := e.Args[i]
|
|
|
|
// TODO: we should also unpick a PathError here and show the
|
|
// path to the deep value where the error was detected.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid function argument",
|
|
Detail: fmt.Sprintf(
|
|
"Invalid value for %q parameter: %s.",
|
|
param.Name, err,
|
|
),
|
|
Subject: argExpr.StartRange().Ptr(),
|
|
Context: e.Range().Ptr(),
|
|
Expression: argExpr,
|
|
EvalContext: ctx,
|
|
})
|
|
|
|
default:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Error in function call",
|
|
Detail: fmt.Sprintf(
|
|
"Call to function %q failed: %s.",
|
|
e.Name, err,
|
|
),
|
|
Subject: e.StartRange().Ptr(),
|
|
Context: e.Range().Ptr(),
|
|
Expression: e,
|
|
EvalContext: ctx,
|
|
})
|
|
}
|
|
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
return resultVal, diags
|
|
}
|
|
|
|
func (e *FunctionCallExpr) Range() hcl.Range {
|
|
return hcl.RangeBetween(e.NameRange, e.CloseParenRange)
|
|
}
|
|
|
|
func (e *FunctionCallExpr) StartRange() hcl.Range {
|
|
return hcl.RangeBetween(e.NameRange, e.OpenParenRange)
|
|
}
|
|
|
|
// Implementation for hcl.ExprCall.
|
|
func (e *FunctionCallExpr) ExprCall() *hcl.StaticCall {
|
|
ret := &hcl.StaticCall{
|
|
Name: e.Name,
|
|
NameRange: e.NameRange,
|
|
Arguments: make([]hcl.Expression, len(e.Args)),
|
|
ArgsRange: hcl.RangeBetween(e.OpenParenRange, e.CloseParenRange),
|
|
}
|
|
// Need to convert our own Expression objects into hcl.Expression.
|
|
for i, arg := range e.Args {
|
|
ret.Arguments[i] = arg
|
|
}
|
|
return ret
|
|
}
|
|
|
|
type ConditionalExpr struct {
|
|
Condition Expression
|
|
TrueResult Expression
|
|
FalseResult Expression
|
|
|
|
SrcRange hcl.Range
|
|
}
|
|
|
|
func (e *ConditionalExpr) walkChildNodes(w internalWalkFunc) {
|
|
w(e.Condition)
|
|
w(e.TrueResult)
|
|
w(e.FalseResult)
|
|
}
|
|
|
|
func (e *ConditionalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
trueResult, trueDiags := e.TrueResult.Value(ctx)
|
|
falseResult, falseDiags := e.FalseResult.Value(ctx)
|
|
var diags hcl.Diagnostics
|
|
|
|
resultType := cty.DynamicPseudoType
|
|
convs := make([]convert.Conversion, 2)
|
|
|
|
switch {
|
|
// If either case is a dynamic null value (which would result from a
|
|
// literal null in the config), we know that it can convert to the expected
|
|
// type of the opposite case, and we don't need to speculatively reduce the
|
|
// final result type to DynamicPseudoType.
|
|
|
|
// If we know that either Type is a DynamicPseudoType, we can be certain
|
|
// that the other value can convert since it's a pass-through, and we don't
|
|
// need to unify the types. If the final evaluation results in the dynamic
|
|
// value being returned, there's no conversion we can do, so we return the
|
|
// value directly.
|
|
case trueResult.RawEquals(cty.NullVal(cty.DynamicPseudoType)):
|
|
resultType = falseResult.Type()
|
|
convs[0] = convert.GetConversionUnsafe(cty.DynamicPseudoType, resultType)
|
|
case falseResult.RawEquals(cty.NullVal(cty.DynamicPseudoType)):
|
|
resultType = trueResult.Type()
|
|
convs[1] = convert.GetConversionUnsafe(cty.DynamicPseudoType, resultType)
|
|
case trueResult.Type() == cty.DynamicPseudoType, falseResult.Type() == cty.DynamicPseudoType:
|
|
// the final resultType type is still unknown
|
|
// we don't need to get the conversion, because both are a noop.
|
|
|
|
default:
|
|
// Try to find a type that both results can be converted to.
|
|
resultType, convs = convert.UnifyUnsafe([]cty.Type{trueResult.Type(), falseResult.Type()})
|
|
}
|
|
|
|
if resultType == cty.NilType {
|
|
return cty.DynamicVal, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Inconsistent conditional result types",
|
|
Detail: fmt.Sprintf(
|
|
// FIXME: Need a helper function for showing natural-language type diffs,
|
|
// since this will generate some useless messages in some cases, like
|
|
// "These expressions are object and object respectively" if the
|
|
// object types don't exactly match.
|
|
"The true and false result expressions must have consistent types. The given expressions are %s and %s, respectively.",
|
|
trueResult.Type().FriendlyName(), falseResult.Type().FriendlyName(),
|
|
),
|
|
Subject: hcl.RangeBetween(e.TrueResult.Range(), e.FalseResult.Range()).Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e,
|
|
EvalContext: ctx,
|
|
},
|
|
}
|
|
}
|
|
|
|
condResult, condDiags := e.Condition.Value(ctx)
|
|
diags = append(diags, condDiags...)
|
|
if condResult.IsNull() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Null condition",
|
|
Detail: "The condition value is null. Conditions must either be true or false.",
|
|
Subject: e.Condition.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.Condition,
|
|
EvalContext: ctx,
|
|
})
|
|
return cty.UnknownVal(resultType), diags
|
|
}
|
|
if !condResult.IsKnown() {
|
|
return cty.UnknownVal(resultType), diags
|
|
}
|
|
condResult, err := convert.Convert(condResult, cty.Bool)
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Incorrect condition type",
|
|
Detail: fmt.Sprintf("The condition expression must be of type bool."),
|
|
Subject: e.Condition.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.Condition,
|
|
EvalContext: ctx,
|
|
})
|
|
return cty.UnknownVal(resultType), diags
|
|
}
|
|
|
|
if condResult.True() {
|
|
diags = append(diags, trueDiags...)
|
|
if convs[0] != nil {
|
|
var err error
|
|
trueResult, err = convs[0](trueResult)
|
|
if err != nil {
|
|
// Unsafe conversion failed with the concrete result value
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Inconsistent conditional result types",
|
|
Detail: fmt.Sprintf(
|
|
"The true result value has the wrong type: %s.",
|
|
err.Error(),
|
|
),
|
|
Subject: e.TrueResult.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.TrueResult,
|
|
EvalContext: ctx,
|
|
})
|
|
trueResult = cty.UnknownVal(resultType)
|
|
}
|
|
}
|
|
return trueResult, diags
|
|
} else {
|
|
diags = append(diags, falseDiags...)
|
|
if convs[1] != nil {
|
|
var err error
|
|
falseResult, err = convs[1](falseResult)
|
|
if err != nil {
|
|
// Unsafe conversion failed with the concrete result value
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Inconsistent conditional result types",
|
|
Detail: fmt.Sprintf(
|
|
"The false result value has the wrong type: %s.",
|
|
err.Error(),
|
|
),
|
|
Subject: e.FalseResult.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.FalseResult,
|
|
EvalContext: ctx,
|
|
})
|
|
falseResult = cty.UnknownVal(resultType)
|
|
}
|
|
}
|
|
return falseResult, diags
|
|
}
|
|
}
|
|
|
|
func (e *ConditionalExpr) Range() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
func (e *ConditionalExpr) StartRange() hcl.Range {
|
|
return e.Condition.StartRange()
|
|
}
|
|
|
|
type IndexExpr struct {
|
|
Collection Expression
|
|
Key Expression
|
|
|
|
SrcRange hcl.Range
|
|
OpenRange hcl.Range
|
|
BracketRange hcl.Range
|
|
}
|
|
|
|
func (e *IndexExpr) walkChildNodes(w internalWalkFunc) {
|
|
w(e.Collection)
|
|
w(e.Key)
|
|
}
|
|
|
|
func (e *IndexExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
coll, collDiags := e.Collection.Value(ctx)
|
|
key, keyDiags := e.Key.Value(ctx)
|
|
diags = append(diags, collDiags...)
|
|
diags = append(diags, keyDiags...)
|
|
|
|
val, indexDiags := hcl.Index(coll, key, &e.BracketRange)
|
|
setDiagEvalContext(indexDiags, e, ctx)
|
|
diags = append(diags, indexDiags...)
|
|
return val, diags
|
|
}
|
|
|
|
func (e *IndexExpr) Range() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
func (e *IndexExpr) StartRange() hcl.Range {
|
|
return e.OpenRange
|
|
}
|
|
|
|
type TupleConsExpr struct {
|
|
Exprs []Expression
|
|
|
|
SrcRange hcl.Range
|
|
OpenRange hcl.Range
|
|
}
|
|
|
|
func (e *TupleConsExpr) walkChildNodes(w internalWalkFunc) {
|
|
for _, expr := range e.Exprs {
|
|
w(expr)
|
|
}
|
|
}
|
|
|
|
func (e *TupleConsExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
var vals []cty.Value
|
|
var diags hcl.Diagnostics
|
|
|
|
vals = make([]cty.Value, len(e.Exprs))
|
|
for i, expr := range e.Exprs {
|
|
val, valDiags := expr.Value(ctx)
|
|
vals[i] = val
|
|
diags = append(diags, valDiags...)
|
|
}
|
|
|
|
return cty.TupleVal(vals), diags
|
|
}
|
|
|
|
func (e *TupleConsExpr) Range() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
func (e *TupleConsExpr) StartRange() hcl.Range {
|
|
return e.OpenRange
|
|
}
|
|
|
|
// Implementation for hcl.ExprList
|
|
func (e *TupleConsExpr) ExprList() []hcl.Expression {
|
|
ret := make([]hcl.Expression, len(e.Exprs))
|
|
for i, expr := range e.Exprs {
|
|
ret[i] = expr
|
|
}
|
|
return ret
|
|
}
|
|
|
|
type ObjectConsExpr struct {
|
|
Items []ObjectConsItem
|
|
|
|
SrcRange hcl.Range
|
|
OpenRange hcl.Range
|
|
}
|
|
|
|
type ObjectConsItem struct {
|
|
KeyExpr Expression
|
|
ValueExpr Expression
|
|
}
|
|
|
|
func (e *ObjectConsExpr) walkChildNodes(w internalWalkFunc) {
|
|
for _, item := range e.Items {
|
|
w(item.KeyExpr)
|
|
w(item.ValueExpr)
|
|
}
|
|
}
|
|
|
|
func (e *ObjectConsExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
var vals map[string]cty.Value
|
|
var diags hcl.Diagnostics
|
|
|
|
// This will get set to true if we fail to produce any of our keys,
|
|
// either because they are actually unknown or if the evaluation produces
|
|
// errors. In all of these case we must return DynamicPseudoType because
|
|
// we're unable to know the full set of keys our object has, and thus
|
|
// we can't produce a complete value of the intended type.
|
|
//
|
|
// We still evaluate all of the item keys and values to make sure that we
|
|
// get as complete as possible a set of diagnostics.
|
|
known := true
|
|
|
|
vals = make(map[string]cty.Value, len(e.Items))
|
|
for _, item := range e.Items {
|
|
key, keyDiags := item.KeyExpr.Value(ctx)
|
|
diags = append(diags, keyDiags...)
|
|
|
|
val, valDiags := item.ValueExpr.Value(ctx)
|
|
diags = append(diags, valDiags...)
|
|
|
|
if keyDiags.HasErrors() {
|
|
known = false
|
|
continue
|
|
}
|
|
|
|
if key.IsNull() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Null value as key",
|
|
Detail: "Can't use a null value as a key.",
|
|
Subject: item.ValueExpr.Range().Ptr(),
|
|
Expression: item.KeyExpr,
|
|
EvalContext: ctx,
|
|
})
|
|
known = false
|
|
continue
|
|
}
|
|
|
|
var err error
|
|
key, err = convert.Convert(key, cty.String)
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Incorrect key type",
|
|
Detail: fmt.Sprintf("Can't use this value as a key: %s.", err.Error()),
|
|
Subject: item.KeyExpr.Range().Ptr(),
|
|
Expression: item.KeyExpr,
|
|
EvalContext: ctx,
|
|
})
|
|
known = false
|
|
continue
|
|
}
|
|
|
|
if !key.IsKnown() {
|
|
known = false
|
|
continue
|
|
}
|
|
|
|
keyStr := key.AsString()
|
|
|
|
vals[keyStr] = val
|
|
}
|
|
|
|
if !known {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
return cty.ObjectVal(vals), diags
|
|
}
|
|
|
|
func (e *ObjectConsExpr) Range() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
func (e *ObjectConsExpr) StartRange() hcl.Range {
|
|
return e.OpenRange
|
|
}
|
|
|
|
// Implementation for hcl.ExprMap
|
|
func (e *ObjectConsExpr) ExprMap() []hcl.KeyValuePair {
|
|
ret := make([]hcl.KeyValuePair, len(e.Items))
|
|
for i, item := range e.Items {
|
|
ret[i] = hcl.KeyValuePair{
|
|
Key: item.KeyExpr,
|
|
Value: item.ValueExpr,
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// ObjectConsKeyExpr is a special wrapper used only for ObjectConsExpr keys,
|
|
// which deals with the special case that a naked identifier in that position
|
|
// must be interpreted as a literal string rather than evaluated directly.
|
|
type ObjectConsKeyExpr struct {
|
|
Wrapped Expression
|
|
ForceNonLiteral bool
|
|
}
|
|
|
|
func (e *ObjectConsKeyExpr) literalName() string {
|
|
// This is our logic for deciding whether to behave like a literal string.
|
|
// We lean on our AbsTraversalForExpr implementation here, which already
|
|
// deals with some awkward cases like the expression being the result
|
|
// of the keywords "null", "true" and "false" which we'd want to interpret
|
|
// as keys here too.
|
|
return hcl.ExprAsKeyword(e.Wrapped)
|
|
}
|
|
|
|
func (e *ObjectConsKeyExpr) walkChildNodes(w internalWalkFunc) {
|
|
// We only treat our wrapped expression as a real expression if we're
|
|
// not going to interpret it as a literal.
|
|
if e.literalName() == "" {
|
|
w(e.Wrapped)
|
|
}
|
|
}
|
|
|
|
func (e *ObjectConsKeyExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
// Because we accept a naked identifier as a literal key rather than a
|
|
// reference, it's confusing to accept a traversal containing periods
|
|
// here since we can't tell if the user intends to create a key with
|
|
// periods or actually reference something. To avoid confusing downstream
|
|
// errors we'll just prohibit a naked multi-step traversal here and
|
|
// require the user to state their intent more clearly.
|
|
// (This is handled at evaluation time rather than parse time because
|
|
// an application using static analysis _can_ accept a naked multi-step
|
|
// traversal here, if desired.)
|
|
if !e.ForceNonLiteral {
|
|
if travExpr, isTraversal := e.Wrapped.(*ScopeTraversalExpr); isTraversal && len(travExpr.Traversal) > 1 {
|
|
var diags hcl.Diagnostics
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Ambiguous attribute key",
|
|
Detail: "If this expression is intended to be a reference, wrap it in parentheses. If it's instead intended as a literal name containing periods, wrap it in quotes to create a string literal.",
|
|
Subject: e.Range().Ptr(),
|
|
})
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
if ln := e.literalName(); ln != "" {
|
|
return cty.StringVal(ln), nil
|
|
}
|
|
}
|
|
return e.Wrapped.Value(ctx)
|
|
}
|
|
|
|
func (e *ObjectConsKeyExpr) Range() hcl.Range {
|
|
return e.Wrapped.Range()
|
|
}
|
|
|
|
func (e *ObjectConsKeyExpr) StartRange() hcl.Range {
|
|
return e.Wrapped.StartRange()
|
|
}
|
|
|
|
// Implementation for hcl.AbsTraversalForExpr.
|
|
func (e *ObjectConsKeyExpr) AsTraversal() hcl.Traversal {
|
|
// If we're forcing a non-literal then we can never be interpreted
|
|
// as a traversal.
|
|
if e.ForceNonLiteral {
|
|
return nil
|
|
}
|
|
|
|
// We can produce a traversal only if our wrappee can.
|
|
st, diags := hcl.AbsTraversalForExpr(e.Wrapped)
|
|
if diags.HasErrors() {
|
|
return nil
|
|
}
|
|
|
|
return st
|
|
}
|
|
|
|
func (e *ObjectConsKeyExpr) UnwrapExpression() Expression {
|
|
return e.Wrapped
|
|
}
|
|
|
|
// ForExpr represents iteration constructs:
|
|
//
|
|
// tuple = [for i, v in list: upper(v) if i > 2]
|
|
// object = {for k, v in map: k => upper(v)}
|
|
// object_of_tuples = {for v in list: v.key: v...}
|
|
type ForExpr struct {
|
|
KeyVar string // empty if ignoring the key
|
|
ValVar string
|
|
|
|
CollExpr Expression
|
|
|
|
KeyExpr Expression // nil when producing a tuple
|
|
ValExpr Expression
|
|
CondExpr Expression // null if no "if" clause is present
|
|
|
|
Group bool // set if the ellipsis is used on the value in an object for
|
|
|
|
SrcRange hcl.Range
|
|
OpenRange hcl.Range
|
|
CloseRange hcl.Range
|
|
}
|
|
|
|
func (e *ForExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
collVal, collDiags := e.CollExpr.Value(ctx)
|
|
diags = append(diags, collDiags...)
|
|
|
|
if collVal.IsNull() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Iteration over null value",
|
|
Detail: "A null value cannot be used as the collection in a 'for' expression.",
|
|
Subject: e.CollExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.CollExpr,
|
|
EvalContext: ctx,
|
|
})
|
|
return cty.DynamicVal, diags
|
|
}
|
|
if collVal.Type() == cty.DynamicPseudoType {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
if !collVal.CanIterateElements() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Iteration over non-iterable value",
|
|
Detail: fmt.Sprintf(
|
|
"A value of type %s cannot be used as the collection in a 'for' expression.",
|
|
collVal.Type().FriendlyName(),
|
|
),
|
|
Subject: e.CollExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.CollExpr,
|
|
EvalContext: ctx,
|
|
})
|
|
return cty.DynamicVal, diags
|
|
}
|
|
if !collVal.IsKnown() {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
// Before we start we'll do an early check to see if any CondExpr we've
|
|
// been given is of the wrong type. This isn't 100% reliable (it may
|
|
// be DynamicVal until real values are given) but it should catch some
|
|
// straightforward cases and prevent a barrage of repeated errors.
|
|
if e.CondExpr != nil {
|
|
childCtx := ctx.NewChild()
|
|
childCtx.Variables = map[string]cty.Value{}
|
|
if e.KeyVar != "" {
|
|
childCtx.Variables[e.KeyVar] = cty.DynamicVal
|
|
}
|
|
childCtx.Variables[e.ValVar] = cty.DynamicVal
|
|
|
|
result, condDiags := e.CondExpr.Value(childCtx)
|
|
diags = append(diags, condDiags...)
|
|
if result.IsNull() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Condition is null",
|
|
Detail: "The value of the 'if' clause must not be null.",
|
|
Subject: e.CondExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.CondExpr,
|
|
EvalContext: ctx,
|
|
})
|
|
return cty.DynamicVal, diags
|
|
}
|
|
_, err := convert.Convert(result, cty.Bool)
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' condition",
|
|
Detail: fmt.Sprintf("The 'if' clause value is invalid: %s.", err.Error()),
|
|
Subject: e.CondExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.CondExpr,
|
|
EvalContext: ctx,
|
|
})
|
|
return cty.DynamicVal, diags
|
|
}
|
|
if condDiags.HasErrors() {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
}
|
|
|
|
if e.KeyExpr != nil {
|
|
// Producing an object
|
|
var vals map[string]cty.Value
|
|
var groupVals map[string][]cty.Value
|
|
if e.Group {
|
|
groupVals = map[string][]cty.Value{}
|
|
} else {
|
|
vals = map[string]cty.Value{}
|
|
}
|
|
|
|
it := collVal.ElementIterator()
|
|
|
|
known := true
|
|
for it.Next() {
|
|
k, v := it.Element()
|
|
childCtx := ctx.NewChild()
|
|
childCtx.Variables = map[string]cty.Value{}
|
|
if e.KeyVar != "" {
|
|
childCtx.Variables[e.KeyVar] = k
|
|
}
|
|
childCtx.Variables[e.ValVar] = v
|
|
|
|
if e.CondExpr != nil {
|
|
includeRaw, condDiags := e.CondExpr.Value(childCtx)
|
|
diags = append(diags, condDiags...)
|
|
if includeRaw.IsNull() {
|
|
if known {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' condition",
|
|
Detail: "The value of the 'if' clause must not be null.",
|
|
Subject: e.CondExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.CondExpr,
|
|
EvalContext: childCtx,
|
|
})
|
|
}
|
|
known = false
|
|
continue
|
|
}
|
|
include, err := convert.Convert(includeRaw, cty.Bool)
|
|
if err != nil {
|
|
if known {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' condition",
|
|
Detail: fmt.Sprintf("The 'if' clause value is invalid: %s.", err.Error()),
|
|
Subject: e.CondExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.CondExpr,
|
|
EvalContext: childCtx,
|
|
})
|
|
}
|
|
known = false
|
|
continue
|
|
}
|
|
if !include.IsKnown() {
|
|
known = false
|
|
continue
|
|
}
|
|
|
|
if include.False() {
|
|
// Skip this element
|
|
continue
|
|
}
|
|
}
|
|
|
|
keyRaw, keyDiags := e.KeyExpr.Value(childCtx)
|
|
diags = append(diags, keyDiags...)
|
|
if keyRaw.IsNull() {
|
|
if known {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid object key",
|
|
Detail: "Key expression in 'for' expression must not produce a null value.",
|
|
Subject: e.KeyExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.KeyExpr,
|
|
EvalContext: childCtx,
|
|
})
|
|
}
|
|
known = false
|
|
continue
|
|
}
|
|
if !keyRaw.IsKnown() {
|
|
known = false
|
|
continue
|
|
}
|
|
|
|
key, err := convert.Convert(keyRaw, cty.String)
|
|
if err != nil {
|
|
if known {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid object key",
|
|
Detail: fmt.Sprintf("The key expression produced an invalid result: %s.", err.Error()),
|
|
Subject: e.KeyExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.KeyExpr,
|
|
EvalContext: childCtx,
|
|
})
|
|
}
|
|
known = false
|
|
continue
|
|
}
|
|
|
|
val, valDiags := e.ValExpr.Value(childCtx)
|
|
diags = append(diags, valDiags...)
|
|
|
|
if e.Group {
|
|
k := key.AsString()
|
|
groupVals[k] = append(groupVals[k], val)
|
|
} else {
|
|
k := key.AsString()
|
|
if _, exists := vals[k]; exists {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate object key",
|
|
Detail: fmt.Sprintf(
|
|
"Two different items produced the key %q in this 'for' expression. If duplicates are expected, use the ellipsis (...) after the value expression to enable grouping by key.",
|
|
k,
|
|
),
|
|
Subject: e.KeyExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.KeyExpr,
|
|
EvalContext: childCtx,
|
|
})
|
|
} else {
|
|
vals[key.AsString()] = val
|
|
}
|
|
}
|
|
}
|
|
|
|
if !known {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
if e.Group {
|
|
vals = map[string]cty.Value{}
|
|
for k, gvs := range groupVals {
|
|
vals[k] = cty.TupleVal(gvs)
|
|
}
|
|
}
|
|
|
|
return cty.ObjectVal(vals), diags
|
|
|
|
} else {
|
|
// Producing a tuple
|
|
vals := []cty.Value{}
|
|
|
|
it := collVal.ElementIterator()
|
|
|
|
known := true
|
|
for it.Next() {
|
|
k, v := it.Element()
|
|
childCtx := ctx.NewChild()
|
|
childCtx.Variables = map[string]cty.Value{}
|
|
if e.KeyVar != "" {
|
|
childCtx.Variables[e.KeyVar] = k
|
|
}
|
|
childCtx.Variables[e.ValVar] = v
|
|
|
|
if e.CondExpr != nil {
|
|
includeRaw, condDiags := e.CondExpr.Value(childCtx)
|
|
diags = append(diags, condDiags...)
|
|
if includeRaw.IsNull() {
|
|
if known {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' condition",
|
|
Detail: "The value of the 'if' clause must not be null.",
|
|
Subject: e.CondExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.CondExpr,
|
|
EvalContext: childCtx,
|
|
})
|
|
}
|
|
known = false
|
|
continue
|
|
}
|
|
if !includeRaw.IsKnown() {
|
|
// We will eventually return DynamicVal, but we'll continue
|
|
// iterating in case there are other diagnostics to gather
|
|
// for later elements.
|
|
known = false
|
|
continue
|
|
}
|
|
|
|
include, err := convert.Convert(includeRaw, cty.Bool)
|
|
if err != nil {
|
|
if known {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'for' condition",
|
|
Detail: fmt.Sprintf("The 'if' clause value is invalid: %s.", err.Error()),
|
|
Subject: e.CondExpr.Range().Ptr(),
|
|
Context: &e.SrcRange,
|
|
Expression: e.CondExpr,
|
|
EvalContext: childCtx,
|
|
})
|
|
}
|
|
known = false
|
|
continue
|
|
}
|
|
|
|
if include.False() {
|
|
// Skip this element
|
|
continue
|
|
}
|
|
}
|
|
|
|
val, valDiags := e.ValExpr.Value(childCtx)
|
|
diags = append(diags, valDiags...)
|
|
vals = append(vals, val)
|
|
}
|
|
|
|
if !known {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
return cty.TupleVal(vals), diags
|
|
}
|
|
}
|
|
|
|
func (e *ForExpr) walkChildNodes(w internalWalkFunc) {
|
|
w(e.CollExpr)
|
|
|
|
scopeNames := map[string]struct{}{}
|
|
if e.KeyVar != "" {
|
|
scopeNames[e.KeyVar] = struct{}{}
|
|
}
|
|
if e.ValVar != "" {
|
|
scopeNames[e.ValVar] = struct{}{}
|
|
}
|
|
|
|
if e.KeyExpr != nil {
|
|
w(ChildScope{
|
|
LocalNames: scopeNames,
|
|
Expr: e.KeyExpr,
|
|
})
|
|
}
|
|
w(ChildScope{
|
|
LocalNames: scopeNames,
|
|
Expr: e.ValExpr,
|
|
})
|
|
if e.CondExpr != nil {
|
|
w(ChildScope{
|
|
LocalNames: scopeNames,
|
|
Expr: e.CondExpr,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (e *ForExpr) Range() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
func (e *ForExpr) StartRange() hcl.Range {
|
|
return e.OpenRange
|
|
}
|
|
|
|
type SplatExpr struct {
|
|
Source Expression
|
|
Each Expression
|
|
Item *AnonSymbolExpr
|
|
|
|
SrcRange hcl.Range
|
|
MarkerRange hcl.Range
|
|
}
|
|
|
|
func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
sourceVal, diags := e.Source.Value(ctx)
|
|
if diags.HasErrors() {
|
|
// We'll evaluate our "Each" expression here just to see if it
|
|
// produces any more diagnostics we can report. Since we're not
|
|
// assigning a value to our AnonSymbolExpr here it will return
|
|
// DynamicVal, which should short-circuit any use of it.
|
|
_, itemDiags := e.Item.Value(ctx)
|
|
diags = append(diags, itemDiags...)
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
sourceTy := sourceVal.Type()
|
|
if sourceTy == cty.DynamicPseudoType {
|
|
// If we don't even know the _type_ of our source value yet then
|
|
// we'll need to defer all processing, since we can't decide our
|
|
// result type either.
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
// A "special power" of splat expressions is that they can be applied
|
|
// both to tuples/lists and to other values, and in the latter case
|
|
// the value will be treated as an implicit single-item tuple, or as
|
|
// an empty tuple if the value is null.
|
|
autoUpgrade := !(sourceTy.IsTupleType() || sourceTy.IsListType() || sourceTy.IsSetType())
|
|
|
|
if sourceVal.IsNull() {
|
|
if autoUpgrade {
|
|
return cty.EmptyTupleVal, diags
|
|
}
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Splat of null value",
|
|
Detail: "Splat expressions (with the * symbol) cannot be applied to null sequences.",
|
|
Subject: e.Source.Range().Ptr(),
|
|
Context: hcl.RangeBetween(e.Source.Range(), e.MarkerRange).Ptr(),
|
|
Expression: e.Source,
|
|
EvalContext: ctx,
|
|
})
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
if autoUpgrade {
|
|
sourceVal = cty.TupleVal([]cty.Value{sourceVal})
|
|
sourceTy = sourceVal.Type()
|
|
}
|
|
|
|
// We'll compute our result type lazily if we need it. In the normal case
|
|
// it's inferred automatically from the value we construct.
|
|
resultTy := func() (cty.Type, hcl.Diagnostics) {
|
|
chiCtx := ctx.NewChild()
|
|
var diags hcl.Diagnostics
|
|
switch {
|
|
case sourceTy.IsListType() || sourceTy.IsSetType():
|
|
ety := sourceTy.ElementType()
|
|
e.Item.setValue(chiCtx, cty.UnknownVal(ety))
|
|
val, itemDiags := e.Each.Value(chiCtx)
|
|
diags = append(diags, itemDiags...)
|
|
e.Item.clearValue(chiCtx) // clean up our temporary value
|
|
return cty.List(val.Type()), diags
|
|
case sourceTy.IsTupleType():
|
|
etys := sourceTy.TupleElementTypes()
|
|
resultTys := make([]cty.Type, 0, len(etys))
|
|
for _, ety := range etys {
|
|
e.Item.setValue(chiCtx, cty.UnknownVal(ety))
|
|
val, itemDiags := e.Each.Value(chiCtx)
|
|
diags = append(diags, itemDiags...)
|
|
e.Item.clearValue(chiCtx) // clean up our temporary value
|
|
resultTys = append(resultTys, val.Type())
|
|
}
|
|
return cty.Tuple(resultTys), diags
|
|
default:
|
|
// Should never happen because of our promotion to list above.
|
|
return cty.DynamicPseudoType, diags
|
|
}
|
|
}
|
|
|
|
if !sourceVal.IsKnown() {
|
|
// We can't produce a known result in this case, but we'll still
|
|
// indicate what the result type would be, allowing any downstream type
|
|
// checking to proceed.
|
|
ty, tyDiags := resultTy()
|
|
diags = append(diags, tyDiags...)
|
|
return cty.UnknownVal(ty), diags
|
|
}
|
|
|
|
vals := make([]cty.Value, 0, sourceVal.LengthInt())
|
|
it := sourceVal.ElementIterator()
|
|
if ctx == nil {
|
|
// we need a context to use our AnonSymbolExpr, so we'll just
|
|
// make an empty one here to use as a placeholder.
|
|
ctx = ctx.NewChild()
|
|
}
|
|
isKnown := true
|
|
for it.Next() {
|
|
_, sourceItem := it.Element()
|
|
e.Item.setValue(ctx, sourceItem)
|
|
newItem, itemDiags := e.Each.Value(ctx)
|
|
diags = append(diags, itemDiags...)
|
|
if itemDiags.HasErrors() {
|
|
isKnown = false
|
|
}
|
|
vals = append(vals, newItem)
|
|
}
|
|
e.Item.clearValue(ctx) // clean up our temporary value
|
|
|
|
if !isKnown {
|
|
// We'll ingore the resultTy diagnostics in this case since they
|
|
// will just be the same errors we saw while iterating above.
|
|
ty, _ := resultTy()
|
|
return cty.UnknownVal(ty), diags
|
|
}
|
|
|
|
switch {
|
|
case sourceTy.IsListType() || sourceTy.IsSetType():
|
|
if len(vals) == 0 {
|
|
ty, tyDiags := resultTy()
|
|
diags = append(diags, tyDiags...)
|
|
return cty.ListValEmpty(ty.ElementType()), diags
|
|
}
|
|
return cty.ListVal(vals), diags
|
|
default:
|
|
return cty.TupleVal(vals), diags
|
|
}
|
|
}
|
|
|
|
func (e *SplatExpr) walkChildNodes(w internalWalkFunc) {
|
|
w(e.Source)
|
|
w(e.Each)
|
|
}
|
|
|
|
func (e *SplatExpr) Range() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
func (e *SplatExpr) StartRange() hcl.Range {
|
|
return e.MarkerRange
|
|
}
|
|
|
|
// AnonSymbolExpr is used as a placeholder for a value in an expression that
|
|
// can be applied dynamically to any value at runtime.
|
|
//
|
|
// This is a rather odd, synthetic expression. It is used as part of the
|
|
// representation of splat expressions as a placeholder for the current item
|
|
// being visited in the splat evaluation.
|
|
//
|
|
// AnonSymbolExpr cannot be evaluated in isolation. If its Value is called
|
|
// directly then cty.DynamicVal will be returned. Instead, it is evaluated
|
|
// in terms of another node (i.e. a splat expression) which temporarily
|
|
// assigns it a value.
|
|
type AnonSymbolExpr struct {
|
|
SrcRange hcl.Range
|
|
|
|
// values and its associated lock are used to isolate concurrent
|
|
// evaluations of a symbol from one another. It is the calling application's
|
|
// responsibility to ensure that the same splat expression is not evalauted
|
|
// concurrently within the _same_ EvalContext, but it is fine and safe to
|
|
// do cuncurrent evaluations with distinct EvalContexts.
|
|
values map[*hcl.EvalContext]cty.Value
|
|
valuesLock sync.RWMutex
|
|
}
|
|
|
|
func (e *AnonSymbolExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
if ctx == nil {
|
|
return cty.DynamicVal, nil
|
|
}
|
|
|
|
e.valuesLock.RLock()
|
|
defer e.valuesLock.RUnlock()
|
|
|
|
val, exists := e.values[ctx]
|
|
if !exists {
|
|
return cty.DynamicVal, nil
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
// setValue sets a temporary local value for the expression when evaluated
|
|
// in the given context, which must be non-nil.
|
|
func (e *AnonSymbolExpr) setValue(ctx *hcl.EvalContext, val cty.Value) {
|
|
e.valuesLock.Lock()
|
|
defer e.valuesLock.Unlock()
|
|
|
|
if e.values == nil {
|
|
e.values = make(map[*hcl.EvalContext]cty.Value)
|
|
}
|
|
if ctx == nil {
|
|
panic("can't setValue for a nil EvalContext")
|
|
}
|
|
e.values[ctx] = val
|
|
}
|
|
|
|
func (e *AnonSymbolExpr) clearValue(ctx *hcl.EvalContext) {
|
|
e.valuesLock.Lock()
|
|
defer e.valuesLock.Unlock()
|
|
|
|
if e.values == nil {
|
|
return
|
|
}
|
|
if ctx == nil {
|
|
panic("can't clearValue for a nil EvalContext")
|
|
}
|
|
delete(e.values, ctx)
|
|
}
|
|
|
|
func (e *AnonSymbolExpr) walkChildNodes(w internalWalkFunc) {
|
|
// AnonSymbolExpr is a leaf node in the tree
|
|
}
|
|
|
|
func (e *AnonSymbolExpr) Range() hcl.Range {
|
|
return e.SrcRange
|
|
}
|
|
|
|
func (e *AnonSymbolExpr) StartRange() hcl.Range {
|
|
return e.SrcRange
|
|
}
|