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:
Martin Atkins 2018-12-05 16:59:33 -08:00
parent cd67ba1b25
commit df9794be1f
2 changed files with 116 additions and 9 deletions

View File

@ -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) {

View File

@ -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,