diff --git a/cty/type.go b/cty/type.go index 730cb986..5f7813e8 100644 --- a/cty/type.go +++ b/cty/type.go @@ -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 { diff --git a/cty/type_test.go b/cty/type_test.go new file mode 100644 index 00000000..7861542e --- /dev/null +++ b/cty/type_test.go @@ -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) + } + }) + } +} diff --git a/cty/value.go b/cty/value.go index 1025ba82..f6a25dde 100644 --- a/cty/value.go +++ b/cty/value.go @@ -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 +} diff --git a/cty/value_ops.go b/cty/value_ops.go index 35a644be..966b999c 100644 --- a/cty/value_ops.go +++ b/cty/value_ops.go @@ -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): @@ -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): @@ -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) } diff --git a/cty/value_ops_test.go b/cty/value_ops_test.go index 8e313fac..7392047a 100644 --- a/cty/value_ops_test.go +++ b/cty/value_ops_test.go @@ -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 { @@ -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) + } + }) + } +}