diff --git a/hclsyntax/expression.go b/hclsyntax/expression.go index 0ee8de46..5df423fe 100644 --- a/hclsyntax/expression.go +++ b/hclsyntax/expression.go @@ -1684,11 +1684,15 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { // example, it is valid to use a splat on a single object to retrieve a // list of a single attribute, but we still need to check if that // attribute actually exists. - upgradedUnknown = !sourceVal.IsKnown() + if !sourceVal.IsKnown() { + sourceRng := sourceVal.Range() + if sourceRng.CouldBeNull() { + upgradedUnknown = true + } + } sourceVal = cty.TupleVal([]cty.Value{sourceVal}) sourceTy = sourceVal.Type() - } // We'll compute our result type lazily if we need it. In the normal case @@ -1727,7 +1731,20 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { // checking to proceed. ty, tyDiags := resultTy() diags = append(diags, tyDiags...) - return cty.UnknownVal(ty), diags + ret := cty.UnknownVal(ty) + if ty != cty.DynamicPseudoType { + ret = ret.RefineNotNull() + } + if ty.IsListType() && sourceVal.Type().IsCollectionType() { + // We can refine the length of an unknown list result based on + // the source collection's own length. + sourceRng := sourceVal.Range() + ret = ret.Refine(). + CollectionLengthLowerBound(sourceRng.LengthLowerBound()). + CollectionLengthUpperBound(sourceRng.LengthUpperBound()). + NewValue() + } + return ret.WithSameMarks(sourceVal), diags } // Unmark the collection, and save the marks to apply to the returned diff --git a/hclsyntax/expression_test.go b/hclsyntax/expression_test.go index 57fcc54e..5a6fedbc 100644 --- a/hclsyntax/expression_test.go +++ b/hclsyntax/expression_test.go @@ -1150,6 +1150,20 @@ upper( cty.DynamicVal, 0, }, + { + `unkstr[*]`, + &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "unkstr": cty.UnknownVal(cty.String).RefineNotNull(), + }, + }, + // If the unknown string is definitely not null then we already + // know that the result will be a single-element tuple. + cty.TupleVal([]cty.Value{ + cty.UnknownVal(cty.String).RefineNotNull(), + }), + 0, + }, { `unkstr.*.name`, &hcl.EvalContext{ @@ -1182,6 +1196,20 @@ upper( cty.DynamicVal, 0, }, + { + `unkobj.*.name`, + &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "unkobj": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + })).RefineNotNull(), + }, + }, + cty.TupleVal([]cty.Value{ + cty.UnknownVal(cty.String), + }), + 0, + }, { `unkobj.*.names`, &hcl.EvalContext{ @@ -1203,7 +1231,24 @@ upper( }))), }, }, - cty.UnknownVal(cty.List(cty.String)), + cty.UnknownVal(cty.List(cty.String)).RefineNotNull(), + 0, + }, + { + `unklistobj.*.name`, + &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "unklistobj": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ + "name": cty.String, + }))).Refine(). + CollectionLengthUpperBound(5). + NewValue(), + }, + }, + cty.UnknownVal(cty.List(cty.String)).Refine(). + NotNull(). + CollectionLengthUpperBound(5). + NewValue(), 0, }, { @@ -1222,7 +1267,7 @@ upper( ), }, }, - cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Bool})), + cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Bool})).RefineNotNull(), 0, }, {