Skip to content

Commit

Permalink
cty: Value.HasWhollyKnownType
Browse files Browse the repository at this point in the history
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".
  • Loading branch information
jbardin authored Jun 5, 2020
1 parent f9ae910 commit 0f5a4b7
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 6 deletions.
2 changes: 1 addition & 1 deletion cty/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (t Type) HasDynamicTypes() bool {
case t.IsPrimitiveType():
return false
case t.IsCollectionType():
return false
return t.ElementType().HasDynamicTypes()
case t.IsObjectType():
attrTypes := t.AttributeTypes()
for _, at := range attrTypes {
Expand Down
56 changes: 56 additions & 0 deletions cty/type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package cty

import (
"fmt"
"testing"
)

func TestHasDynamicTypes(t *testing.T) {
tests := []struct {
ty Type
expected bool
}{
{
DynamicPseudoType,
true,
},
{
List(DynamicPseudoType),
true,
},
{
Tuple([]Type{String, DynamicPseudoType}),
true,
},
{
Object(map[string]Type{
"a": String,
"unknown": DynamicPseudoType,
}),
true,
},
{
List(Object(map[string]Type{
"a": String,
"unknown": DynamicPseudoType,
})),
true,
},
{
Tuple([]Type{Object(map[string]Type{
"a": String,
"unknown": DynamicPseudoType,
})}),
true,
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("%#v.HasDynamicTypes()", test.ty), func(t *testing.T) {
got := test.ty.HasDynamicTypes()
if got != test.expected {
t.Errorf("Equals returned %#v; want %#v", got, test.expected)
}
})
}
}
34 changes: 34 additions & 0 deletions cty/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,37 @@ func (val Value) IsWhollyKnown() bool {
return true
}
}

// HasWhollyKnownType checks if the value is dynamic, or contains any nested
// DynamicVal. This implies that both the value is not known, and the final
// type may change.
func (val Value) HasWhollyKnownType() bool {
// a null dynamic type is known
if val.IsNull() {
return true
}

// an unknown DynamicPseudoType is a DynamicVal, but we don't want to
// check that value for equality here, since this method is used within the
// equality check.
if !val.IsKnown() && val.ty == DynamicPseudoType {
return false
}

if val.CanIterateElements() {
// if the value is not known, then we can look directly at the internal
// types
if !val.IsKnown() {
return !val.ty.HasDynamicTypes()
}

for it := val.ElementIterator(); it.Next(); {
_, ev := it.Element()
if !ev.HasWhollyKnownType() {
return false
}
}
}

return true
}
18 changes: 13 additions & 5 deletions cty/value_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ func (val Value) Equals(other Value) Value {
case val.IsKnown() && !other.IsKnown():
switch {
case val.IsNull(), other.ty.HasDynamicTypes():
// If known is Null, we need to wait for the unkown value since
// If known is Null, we need to wait for the unknown value since
// nulls of any type are equal.
// An unkown with a dynamic type compares as unknown, which we need
// An unknown with a dynamic type compares as unknown, which we need
// to check before the type comparison below.
return UnknownVal(Bool)
case !val.ty.Equals(other.ty):
Expand All @@ -148,9 +148,9 @@ func (val Value) Equals(other Value) Value {
case other.IsKnown() && !val.IsKnown():
switch {
case other.IsNull(), val.ty.HasDynamicTypes():
// If known is Null, we need to wait for the unkown value since
// If known is Null, we need to wait for the unknown value since
// nulls of any type are equal.
// An unkown with a dynamic type compares as unknown, which we need
// An unknown with a dynamic type compares as unknown, which we need
// to check before the type comparison below.
return UnknownVal(Bool)
case !other.ty.Equals(val.ty):
Expand All @@ -171,7 +171,15 @@ func (val Value) Equals(other Value) Value {
return BoolVal(false)
}

if val.ty.HasDynamicTypes() || other.ty.HasDynamicTypes() {
// Check if there are any nested dynamic values making this comparison
// unknown.
if !val.HasWhollyKnownType() || !other.HasWhollyKnownType() {
// Even if we have dynamic values, we can still determine inequality if
// there is no way the types could later conform.
if val.ty.TestConformance(other.ty) != nil && other.ty.TestConformance(val.ty) != nil {
return BoolVal(false)
}

return UnknownVal(Bool)
}

Expand Down
149 changes: 149 additions & 0 deletions cty/value_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,80 @@ func TestValueEquals(t *testing.T) {
NumberIntVal(1),
False, // because no string value -- even null -- can be equal to a non-null number
},
{
ObjectVal(map[string]Value{
"a": StringVal("a"),
}),
// A null value is always known
ObjectVal(map[string]Value{
"a": NullVal(DynamicPseudoType),
}),
BoolVal(false),
},
{
ObjectVal(map[string]Value{
"a": NullVal(DynamicPseudoType),
}),
ObjectVal(map[string]Value{
"a": NullVal(DynamicPseudoType),
}),
BoolVal(true),
},
{
ObjectVal(map[string]Value{
"a": StringVal("a"),
"b": UnknownVal(Number),
}),
// While we have a dynamic type, the different object types should
// still compare false
ObjectVal(map[string]Value{
"a": NullVal(DynamicPseudoType),
"c": UnknownVal(Number),
}),
BoolVal(false),
},
{
ObjectVal(map[string]Value{
"a": StringVal("a"),
"b": UnknownVal(Number),
}),
// While we have a dynamic type, the different object types should
// still compare false
ObjectVal(map[string]Value{
"a": DynamicVal,
"c": UnknownVal(Number),
}),
BoolVal(false),
},
{
ObjectVal(map[string]Value{
"a": NullVal(DynamicPseudoType),
}),
ObjectVal(map[string]Value{
"a": DynamicVal,
}),
UnknownVal(Bool),
},
{
ObjectVal(map[string]Value{
"a": NullVal(List(String)),
}),
// While the unknown val does contain dynamic types, the overall
// container types can't conform.
ObjectVal(map[string]Value{
"a": UnknownVal(List(List(DynamicPseudoType))),
}),
BoolVal(false),
},
{
ObjectVal(map[string]Value{
"a": NullVal(List(List(String))),
}),
ObjectVal(map[string]Value{
"a": UnknownVal(List(List(DynamicPseudoType))),
}),
UnknownVal(Bool),
},

// Marks
{
Expand Down Expand Up @@ -2435,3 +2509,78 @@ func TestValueGoString(t *testing.T) {
})
}
}

func TestHasWhollyKnownType(t *testing.T) {
tests := []struct {
Value Value
Want bool
}{
{
Value: DynamicVal,
Want: false,
},
{
Value: ObjectVal(map[string]Value{
"dyn": DynamicVal,
}),
Want: false,
},
{
Value: NullVal(Object(map[string]Type{
"dyn": DynamicPseudoType,
})),
Want: true,
},
{
Value: TupleVal([]Value{
StringVal("a"),
NullVal(DynamicPseudoType),
}),
Want: true,
},
{
Value: ListVal([]Value{
ObjectVal(map[string]Value{
"null": NullVal(DynamicPseudoType),
}),
}),
Want: true,
},
{
Value: ListVal([]Value{
NullVal(Object(map[string]Type{
"dyn": DynamicPseudoType,
})),
}),
Want: true,
},
{
Value: ObjectVal(map[string]Value{
"tuple": TupleVal([]Value{
StringVal("a"),
NullVal(DynamicPseudoType),
}),
}),
Want: true,
},
{
Value: ObjectVal(map[string]Value{
"tuple": TupleVal([]Value{
ObjectVal(map[string]Value{
"dyn": DynamicVal,
}),
}),
}),
Want: false,
},
}
for _, test := range tests {
t.Run(test.Value.GoString(), func(t *testing.T) {
got := test.Value.HasWhollyKnownType()
want := test.Want
if got != want {
t.Errorf("wrong result\ngot: %v\nwant: %v", got, want)
}
})
}
}

0 comments on commit 0f5a4b7

Please sign in to comment.