Skip to content

Commit 08cd546

Browse files
bbasataapparentlymartalisdairjbardin
authored
Forward to 1.5.0 (#10)
* Changelog preparation for future 1.4.2 release * convert: Fix panic: heterogeneous tuple with null Tuples with elements of different types can be converted to homogeneous collections (sets or lists), so long as their elements are unifiable. For example: list("a", "b") // all elements have the same type list("a", 5) // "a" and 5 can be unified to string list("a", 5, null) // null is a valid value for string However, tuples with elements which are not unifiable cannot be converted to homogeneous collections: list(["a"], "b") // no common type for list(string) and string This commit fixes a panic for this failure case, when the tuple contains both non-unifiable types and a null value: list(["a"], "b", null) // should not panic The null value was causing the unification process to result in a list or set of dynamic type, which causes the conversion functions to pass through the original value. This meant that in the final conversion step, we would attempt to construct a list or set of different values, which panics. * Update CHANGELOG.md * cty: Value.HasWhollyKnownType This tests whether a value contains any unknown values of unknown type. This is different than just testing if any of the nested types are DynamicPseudoType, because a null value of DynamicPseudoType has a different meaning than an unknown value of DynamicPseudoType: the null value's type can't become any more "known". * Update CHANGELOG.md * v1.5.0 * Update CHANGELOG.md --------- Co-authored-by: Martin Atkins <[email protected]> Co-authored-by: Alisdair McDiarmid <[email protected]> Co-authored-by: James Bardin <[email protected]>
1 parent b56e5cf commit 08cd546

File tree

8 files changed

+376
-13
lines changed

8 files changed

+376
-13
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 1.5.0 (Unreleased)
2+
3+
* `cty`: New `Value.HasWhollyKnownType` method, for testing whether a value's type could potentially change if any unknown values it was constructed from were to become known. ([#55](https://github.com/zclconf/go-cty/pull/55))
4+
* `convert`: Fix incorrect panic when converting a tuple with a dynamic-typed null member into a list or set, due to overly-liberal type unification. ([#56](https://github.com/zclconf/go-cty/pull/56))
5+
16
# 1.4.2 (Unreleased)
27

38
* `function/stdlib`: The `jsonencode` function will now correctly accept a null as its argument, and produce the JSON representation `"null"` rather than returning an error. ([#54](https://github.com/zclconf/go-cty/pull/54))

cty/convert/conversion_collection.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,34 +156,45 @@ func conversionCollectionToMap(ety cty.Type, conv conversion) conversion {
156156
// given tuple type and return a set of the given element type.
157157
//
158158
// Will panic if the given tupleType isn't actually a tuple type.
159-
func conversionTupleToSet(tupleType cty.Type, listEty cty.Type, unsafe bool) conversion {
159+
func conversionTupleToSet(tupleType cty.Type, setEty cty.Type, unsafe bool) conversion {
160160
tupleEtys := tupleType.TupleElementTypes()
161161

162162
if len(tupleEtys) == 0 {
163163
// Empty tuple short-circuit
164164
return func(val cty.Value, path cty.Path) (cty.Value, error) {
165-
return cty.SetValEmpty(listEty), nil
165+
return cty.SetValEmpty(setEty), nil
166166
}
167167
}
168168

169-
if listEty == cty.DynamicPseudoType {
169+
if setEty == cty.DynamicPseudoType {
170170
// This is a special case where the caller wants us to find
171171
// a suitable single type that all elements can convert to, if
172172
// possible.
173-
listEty, _ = unify(tupleEtys, unsafe)
174-
if listEty == cty.NilType {
173+
setEty, _ = unify(tupleEtys, unsafe)
174+
if setEty == cty.NilType {
175175
return nil
176176
}
177+
178+
// If the set element type after unification is still the dynamic
179+
// type, the only way this can result in a valid set is if all values
180+
// are of dynamic type
181+
if setEty == cty.DynamicPseudoType {
182+
for _, tupleEty := range tupleEtys {
183+
if !tupleEty.Equals(cty.DynamicPseudoType) {
184+
return nil
185+
}
186+
}
187+
}
177188
}
178189

179190
elemConvs := make([]conversion, len(tupleEtys))
180191
for i, tupleEty := range tupleEtys {
181-
if tupleEty.Equals(listEty) {
192+
if tupleEty.Equals(setEty) {
182193
// no conversion required
183194
continue
184195
}
185196

186-
elemConvs[i] = getConversion(tupleEty, listEty, unsafe)
197+
elemConvs[i] = getConversion(tupleEty, setEty, unsafe)
187198
if elemConvs[i] == nil {
188199
// If any of our element conversions are impossible, then the our
189200
// whole conversion is impossible.
@@ -244,6 +255,17 @@ func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) co
244255
if listEty == cty.NilType {
245256
return nil
246257
}
258+
259+
// If the list element type after unification is still the dynamic
260+
// type, the only way this can result in a valid list is if all values
261+
// are of dynamic type
262+
if listEty == cty.DynamicPseudoType {
263+
for _, tupleEty := range tupleEtys {
264+
if !tupleEty.Equals(cty.DynamicPseudoType) {
265+
return nil
266+
}
267+
}
268+
}
247269
}
248270

249271
elemConvs := make([]conversion, len(tupleEtys))

cty/convert/public_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,95 @@ func TestConvert(t *testing.T) {
677677
}),
678678
WantError: false,
679679
},
680+
// https://github.com/hashicorp/terraform/issues/24377:
681+
{
682+
Value: cty.TupleVal([]cty.Value{
683+
cty.ListVal([]cty.Value{cty.StringVal("a")}),
684+
cty.StringVal("b"),
685+
cty.NullVal(cty.DynamicPseudoType),
686+
}),
687+
Type: cty.Set(cty.DynamicPseudoType),
688+
WantError: true,
689+
},
690+
{
691+
Value: cty.TupleVal([]cty.Value{
692+
cty.ListVal([]cty.Value{cty.StringVal("a")}),
693+
cty.StringVal("b"),
694+
cty.NullVal(cty.DynamicPseudoType),
695+
}),
696+
Type: cty.List(cty.DynamicPseudoType),
697+
WantError: true,
698+
},
699+
{
700+
Value: cty.TupleVal([]cty.Value{
701+
cty.ListVal([]cty.Value{cty.StringVal("a")}),
702+
cty.StringVal("b"),
703+
}),
704+
Type: cty.Set(cty.DynamicPseudoType),
705+
WantError: true,
706+
},
707+
{
708+
Value: cty.TupleVal([]cty.Value{
709+
cty.ListVal([]cty.Value{cty.StringVal("a")}),
710+
cty.StringVal("b"),
711+
}),
712+
Type: cty.List(cty.DynamicPseudoType),
713+
WantError: true,
714+
},
715+
{
716+
Value: cty.TupleVal([]cty.Value{
717+
cty.StringVal("a"),
718+
cty.NumberIntVal(9),
719+
cty.NullVal(cty.DynamicPseudoType),
720+
}),
721+
Type: cty.Set(cty.DynamicPseudoType),
722+
Want: cty.SetVal([]cty.Value{
723+
cty.StringVal("a"),
724+
cty.StringVal("9"),
725+
cty.NullVal(cty.DynamicPseudoType),
726+
}),
727+
WantError: false,
728+
},
729+
{
730+
Value: cty.TupleVal([]cty.Value{
731+
cty.StringVal("a"),
732+
cty.NumberIntVal(9),
733+
cty.NullVal(cty.DynamicPseudoType),
734+
}),
735+
Type: cty.List(cty.DynamicPseudoType),
736+
Want: cty.ListVal([]cty.Value{
737+
cty.StringVal("a"),
738+
cty.StringVal("9"),
739+
cty.NullVal(cty.DynamicPseudoType),
740+
}),
741+
WantError: false,
742+
},
743+
{
744+
Value: cty.TupleVal([]cty.Value{
745+
cty.NullVal(cty.DynamicPseudoType),
746+
cty.NullVal(cty.DynamicPseudoType),
747+
cty.NullVal(cty.DynamicPseudoType),
748+
}),
749+
Type: cty.Set(cty.DynamicPseudoType),
750+
Want: cty.SetVal([]cty.Value{
751+
cty.NullVal(cty.DynamicPseudoType),
752+
}),
753+
WantError: false,
754+
},
755+
{
756+
Value: cty.TupleVal([]cty.Value{
757+
cty.NullVal(cty.DynamicPseudoType),
758+
cty.NullVal(cty.DynamicPseudoType),
759+
cty.NullVal(cty.DynamicPseudoType),
760+
}),
761+
Type: cty.List(cty.DynamicPseudoType),
762+
Want: cty.ListVal([]cty.Value{
763+
cty.NullVal(cty.DynamicPseudoType),
764+
cty.NullVal(cty.DynamicPseudoType),
765+
cty.NullVal(cty.DynamicPseudoType),
766+
}),
767+
WantError: false,
768+
},
680769
}
681770

682771
for _, test := range tests {

cty/type.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func (t Type) HasDynamicTypes() bool {
8787
case t.IsPrimitiveType():
8888
return false
8989
case t.IsCollectionType():
90-
return false
90+
return t.ElementType().HasDynamicTypes()
9191
case t.IsObjectType():
9292
attrTypes := t.AttributeTypes()
9393
for _, at := range attrTypes {

cty/type_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cty
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func TestHasDynamicTypes(t *testing.T) {
9+
tests := []struct {
10+
ty Type
11+
expected bool
12+
}{
13+
{
14+
DynamicPseudoType,
15+
true,
16+
},
17+
{
18+
List(DynamicPseudoType),
19+
true,
20+
},
21+
{
22+
Tuple([]Type{String, DynamicPseudoType}),
23+
true,
24+
},
25+
{
26+
Object(map[string]Type{
27+
"a": String,
28+
"unknown": DynamicPseudoType,
29+
}),
30+
true,
31+
},
32+
{
33+
List(Object(map[string]Type{
34+
"a": String,
35+
"unknown": DynamicPseudoType,
36+
})),
37+
true,
38+
},
39+
{
40+
Tuple([]Type{Object(map[string]Type{
41+
"a": String,
42+
"unknown": DynamicPseudoType,
43+
})}),
44+
true,
45+
},
46+
}
47+
48+
for _, test := range tests {
49+
t.Run(fmt.Sprintf("%#v.HasDynamicTypes()", test.ty), func(t *testing.T) {
50+
got := test.ty.HasDynamicTypes()
51+
if got != test.expected {
52+
t.Errorf("Equals returned %#v; want %#v", got, test.expected)
53+
}
54+
})
55+
}
56+
}

cty/value.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,37 @@ func (val Value) IsWhollyKnown() bool {
106106
return true
107107
}
108108
}
109+
110+
// HasWhollyKnownType checks if the value is dynamic, or contains any nested
111+
// DynamicVal. This implies that both the value is not known, and the final
112+
// type may change.
113+
func (val Value) HasWhollyKnownType() bool {
114+
// a null dynamic type is known
115+
if val.IsNull() {
116+
return true
117+
}
118+
119+
// an unknown DynamicPseudoType is a DynamicVal, but we don't want to
120+
// check that value for equality here, since this method is used within the
121+
// equality check.
122+
if !val.IsKnown() && val.ty == DynamicPseudoType {
123+
return false
124+
}
125+
126+
if val.CanIterateElements() {
127+
// if the value is not known, then we can look directly at the internal
128+
// types
129+
if !val.IsKnown() {
130+
return !val.ty.HasDynamicTypes()
131+
}
132+
133+
for it := val.ElementIterator(); it.Next(); {
134+
_, ev := it.Element()
135+
if !ev.HasWhollyKnownType() {
136+
return false
137+
}
138+
}
139+
}
140+
141+
return true
142+
}

cty/value_ops.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ func (val Value) Equals(other Value) Value {
133133
case val.IsKnown() && !other.IsKnown():
134134
switch {
135135
case val.IsNull(), other.ty.HasDynamicTypes():
136-
// If known is Null, we need to wait for the unkown value since
136+
// If known is Null, we need to wait for the unknown value since
137137
// nulls of any type are equal.
138-
// An unkown with a dynamic type compares as unknown, which we need
138+
// An unknown with a dynamic type compares as unknown, which we need
139139
// to check before the type comparison below.
140140
return UnknownVal(Bool)
141141
case !val.ty.Equals(other.ty):
@@ -148,9 +148,9 @@ func (val Value) Equals(other Value) Value {
148148
case other.IsKnown() && !val.IsKnown():
149149
switch {
150150
case other.IsNull(), val.ty.HasDynamicTypes():
151-
// If known is Null, we need to wait for the unkown value since
151+
// If known is Null, we need to wait for the unknown value since
152152
// nulls of any type are equal.
153-
// An unkown with a dynamic type compares as unknown, which we need
153+
// An unknown with a dynamic type compares as unknown, which we need
154154
// to check before the type comparison below.
155155
return UnknownVal(Bool)
156156
case !other.ty.Equals(val.ty):
@@ -171,7 +171,15 @@ func (val Value) Equals(other Value) Value {
171171
return BoolVal(false)
172172
}
173173

174-
if val.ty.HasDynamicTypes() || other.ty.HasDynamicTypes() {
174+
// Check if there are any nested dynamic values making this comparison
175+
// unknown.
176+
if !val.HasWhollyKnownType() || !other.HasWhollyKnownType() {
177+
// Even if we have dynamic values, we can still determine inequality if
178+
// there is no way the types could later conform.
179+
if val.ty.TestConformance(other.ty) != nil && other.ty.TestConformance(val.ty) != nil {
180+
return BoolVal(false)
181+
}
182+
175183
return UnknownVal(Bool)
176184
}
177185

0 commit comments

Comments
 (0)