blob: d84f6ac1049f6a85b8d0165df8c00f544bcad837 [file] [log] [blame]
package convert
import (
"github.com/zclconf/go-cty/cty"
)
// compareTypes implements a preference order for unification.
//
// The result of this method is not useful for anything other than unification
// preferences, since it assumes that the caller will verify that any suggested
// conversion is actually possible and it is thus able to to make certain
// optimistic assumptions.
func compareTypes(a cty.Type, b cty.Type) int {
// DynamicPseudoType always has lowest preference, because anything can
// convert to it (it acts as a placeholder for "any type") and we want
// to optimistically assume that any dynamics will converge on matching
// their neighbors.
if a == cty.DynamicPseudoType || b == cty.DynamicPseudoType {
if a != cty.DynamicPseudoType {
return -1
}
if b != cty.DynamicPseudoType {
return 1
}
return 0
}
if a.IsPrimitiveType() && b.IsPrimitiveType() {
// String is a supertype of all primitive types, because we can
// represent all primitive values as specially-formatted strings.
if a == cty.String || b == cty.String {
if a != cty.String {
return 1
}
if b != cty.String {
return -1
}
return 0
}
}
if a.IsListType() && b.IsListType() {
return compareTypes(a.ElementType(), b.ElementType())
}
if a.IsSetType() && b.IsSetType() {
return compareTypes(a.ElementType(), b.ElementType())
}
if a.IsMapType() && b.IsMapType() {
return compareTypes(a.ElementType(), b.ElementType())
}
// From this point on we may have swapped the two items in order to
// simplify our cases. Therefore any non-zero return after this point
// must be multiplied by "swap" to potentially invert the return value
// if needed.
swap := 1
switch {
case a.IsTupleType() && b.IsListType():
fallthrough
case a.IsObjectType() && b.IsMapType():
fallthrough
case a.IsSetType() && b.IsTupleType():
fallthrough
case a.IsSetType() && b.IsListType():
a, b = b, a
swap = -1
}
if b.IsSetType() && (a.IsTupleType() || a.IsListType()) {
// We'll just optimistically assume that the element types are
// unifyable/convertible, and let a second recursive pass
// figure out how to make that so.
return -1 * swap
}
if a.IsListType() && b.IsTupleType() {
// We'll just optimistically assume that the tuple's element types
// can be unified into something compatible with the list's element
// type.
return -1 * swap
}
if a.IsMapType() && b.IsObjectType() {
// We'll just optimistically assume that the object's attribute types
// can be unified into something compatible with the map's element
// type.
return -1 * swap
}
// For object and tuple types, comparing two types doesn't really tell
// the whole story because it may be possible to construct a new type C
// that is the supertype of both A and B by unifying each attribute/element
// separately. That possibility is handled by Unify as a follow-up if
// type sorting is insufficient to produce a valid result.
//
// Here we will take care of the simple possibilities where no new type
// is needed.
if a.IsObjectType() && b.IsObjectType() {
atysA := a.AttributeTypes()
atysB := b.AttributeTypes()
if len(atysA) != len(atysB) {
return 0
}
hasASuper := false
hasBSuper := false
for k := range atysA {
if _, has := atysB[k]; !has {
return 0
}
cmp := compareTypes(atysA[k], atysB[k])
if cmp < 0 {
hasASuper = true
} else if cmp > 0 {
hasBSuper = true
}
}
switch {
case hasASuper && hasBSuper:
return 0
case hasASuper:
return -1 * swap
case hasBSuper:
return 1 * swap
default:
return 0
}
}
if a.IsTupleType() && b.IsTupleType() {
etysA := a.TupleElementTypes()
etysB := b.TupleElementTypes()
if len(etysA) != len(etysB) {
return 0
}
hasASuper := false
hasBSuper := false
for i := range etysA {
cmp := compareTypes(etysA[i], etysB[i])
if cmp < 0 {
hasASuper = true
} else if cmp > 0 {
hasBSuper = true
}
}
switch {
case hasASuper && hasBSuper:
return 0
case hasASuper:
return -1 * swap
case hasBSuper:
return 1 * swap
default:
return 0
}
}
return 0
}