hcl/hclsyntax: More accurate type information for splat expressions
Previously we were returning tuple-typed values in all cases, which meant we had to return no type information at all if the source was an unknown list since the length of that list is not predictable. Instead we'll now return a list if the source is a list or set and a tuple if the source is a tuple, allowing us to return exact type information when the source value is unknown. This is important for catching type errors early when accessing attributes across many objects using splat syntax, since before the presence of a splat operator effectively caused a total loss of downstream type checking for unknown values in expressions.
This commit is contained in:
parent
cd67ba1b25
commit
df9794be1f
@ -1226,16 +1226,55 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
||||
})
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
if !sourceVal.IsKnown() {
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
sourceTy := sourceVal.Type()
|
||||
|
||||
// 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-value list. We'll
|
||||
// the value will be treated as an implicit single-value tuple. We'll
|
||||
// deal with that here first.
|
||||
if !(sourceVal.Type().IsTupleType() || sourceVal.Type().IsListType() || sourceVal.Type().IsSetType()) {
|
||||
sourceVal = cty.ListVal([]cty.Value{sourceVal})
|
||||
if !(sourceTy.IsTupleType() || sourceTy.IsListType() || sourceTy.IsSetType()) {
|
||||
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())
|
||||
@ -1259,10 +1298,23 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
||||
e.Item.clearValue(ctx) // clean up our temporary value
|
||||
|
||||
if !isKnown {
|
||||
return cty.DynamicVal, diags
|
||||
// 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
|
||||
}
|
||||
|
||||
return cty.TupleVal(vals), 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) {
|
||||
|
@ -744,11 +744,66 @@ upper(
|
||||
}),
|
||||
},
|
||||
},
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.StringVal("Steve"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`unkstr.*.name`,
|
||||
&hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"unkstr": cty.UnknownVal(cty.String),
|
||||
},
|
||||
},
|
||||
cty.UnknownVal(cty.Tuple([]cty.Type{cty.DynamicPseudoType})),
|
||||
1, // a string has no attribute "name"
|
||||
},
|
||||
{
|
||||
`unkobj.*.name`,
|
||||
&hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"unkobj": cty.UnknownVal(cty.Object(map[string]cty.Type{
|
||||
"name": cty.String,
|
||||
})),
|
||||
},
|
||||
},
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.UnknownVal(cty.String),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`unklistobj.*.name`,
|
||||
&hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"unklistobj": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
|
||||
"name": cty.String,
|
||||
}))),
|
||||
},
|
||||
},
|
||||
cty.UnknownVal(cty.List(cty.String)),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`unktupleobj.*.name`,
|
||||
&hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"unktupleobj": cty.UnknownVal(
|
||||
cty.Tuple([]cty.Type{
|
||||
cty.Object(map[string]cty.Type{
|
||||
"name": cty.String,
|
||||
}),
|
||||
cty.Object(map[string]cty.Type{
|
||||
"name": cty.Bool,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Bool})),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`["hello", "goodbye"].*`,
|
||||
nil,
|
||||
|
Loading…
Reference in New Issue
Block a user