lang/funcs: Fix crash and improve precision of keys/values functions
The "values" function wasn't producing consistently-ordered keys in its result, leading to crashes. This fixes #19204. While working on these functions anyway, this also improves slightly their precision when working with object types, where we can produce a more complete result for unknown values because the attribute names are part of the type. We can also produce results for known maps that have unknown elements; these unknowns will also appear in the values(...) result, allowing them to propagate through expressions. Finally, this adds a few more test cases to try different permutations of empty and unknown values.
This commit is contained in:
parent
8c01cf7293
commit
ecc42b838c
|
@ -419,27 +419,69 @@ func flattener(finalList []cty.Value, flattenList cty.Value) []cty.Value {
|
|||
var KeysFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "inputMap",
|
||||
Type: cty.DynamicPseudoType,
|
||||
Name: "inputMap",
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowUnknown: true,
|
||||
},
|
||||
},
|
||||
Type: function.StaticReturnType(cty.List(cty.String)),
|
||||
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
|
||||
var keys []cty.Value
|
||||
Type: func(args []cty.Value) (cty.Type, error) {
|
||||
ty := args[0].Type()
|
||||
|
||||
if !ty.IsObjectType() && !ty.IsMapType() {
|
||||
return cty.NilVal, fmt.Errorf("keys() requires a map")
|
||||
}
|
||||
|
||||
for it := args[0].ElementIterator(); it.Next(); {
|
||||
k, _ := it.Element()
|
||||
keys = append(keys, k)
|
||||
if err != nil {
|
||||
return cty.ListValEmpty(cty.String), err
|
||||
switch {
|
||||
case ty.IsMapType():
|
||||
return cty.List(cty.String), nil
|
||||
case ty.IsObjectType():
|
||||
atys := ty.AttributeTypes()
|
||||
if len(atys) == 0 {
|
||||
return cty.EmptyTuple, nil
|
||||
}
|
||||
// All of our result elements will be strings, and atys just
|
||||
// decides how many there are.
|
||||
etys := make([]cty.Type, len(atys))
|
||||
for i := range etys {
|
||||
etys[i] = cty.String
|
||||
}
|
||||
return cty.Tuple(etys), nil
|
||||
default:
|
||||
return cty.DynamicPseudoType, function.NewArgErrorf(0, "must have map or object type")
|
||||
}
|
||||
},
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
m := args[0]
|
||||
var keys []cty.Value
|
||||
|
||||
switch {
|
||||
case m.Type().IsObjectType():
|
||||
// In this case we allow unknown values so we must work only with
|
||||
// the attribute _types_, not with the value itself.
|
||||
var names []string
|
||||
for name := range m.Type().AttributeTypes() {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names) // same ordering guaranteed by cty's ElementIterator
|
||||
if len(names) == 0 {
|
||||
return cty.EmptyTupleVal, nil
|
||||
}
|
||||
keys = make([]cty.Value, len(names))
|
||||
for i, name := range names {
|
||||
keys[i] = cty.StringVal(name)
|
||||
}
|
||||
return cty.TupleVal(keys), nil
|
||||
default:
|
||||
if !m.IsKnown() {
|
||||
return cty.UnknownVal(retType), nil
|
||||
}
|
||||
|
||||
// cty guarantees that ElementIterator will iterate in lexicographical
|
||||
// order by key.
|
||||
for it := args[0].ElementIterator(); it.Next(); {
|
||||
k, _ := it.Element()
|
||||
keys = append(keys, k)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return cty.ListValEmpty(cty.String), nil
|
||||
}
|
||||
return cty.ListVal(keys), nil
|
||||
}
|
||||
return cty.ListVal(keys), nil
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -891,9 +933,23 @@ var ValuesFunc = function.New(&function.Spec{
|
|||
if ty.IsMapType() {
|
||||
return cty.List(ty.ElementType()), nil
|
||||
} else if ty.IsObjectType() {
|
||||
var tys []cty.Type
|
||||
for _, v := range ty.AttributeTypes() {
|
||||
tys = append(tys, v)
|
||||
// The result is a tuple type with all of the same types as our
|
||||
// object type's attributes, sorted in lexicographical order by the
|
||||
// keys. (This matches the sort order guaranteed by ElementIterator
|
||||
// on a cty object value.)
|
||||
atys := ty.AttributeTypes()
|
||||
if len(atys) == 0 {
|
||||
return cty.EmptyTuple, nil
|
||||
}
|
||||
attrNames := make([]string, 0, len(atys))
|
||||
for name := range atys {
|
||||
attrNames = append(attrNames, name)
|
||||
}
|
||||
sort.Strings(attrNames)
|
||||
|
||||
tys := make([]cty.Type, len(attrNames))
|
||||
for i, name := range attrNames {
|
||||
tys[i] = atys[name]
|
||||
}
|
||||
return cty.Tuple(tys), nil
|
||||
}
|
||||
|
@ -902,33 +958,12 @@ var ValuesFunc = function.New(&function.Spec{
|
|||
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
|
||||
mapVar := args[0]
|
||||
|
||||
if !mapVar.IsWhollyKnown() {
|
||||
return cty.UnknownVal(retType), nil
|
||||
}
|
||||
|
||||
if mapVar.LengthInt() == 0 {
|
||||
return cty.ListValEmpty(retType.ElementType()), nil
|
||||
}
|
||||
|
||||
keys, err := Keys(mapVar)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
|
||||
// We can just iterate the map/object value here because cty guarantees
|
||||
// that these types always iterate in key lexicographical order.
|
||||
var values []cty.Value
|
||||
|
||||
for it := keys.ElementIterator(); it.Next(); {
|
||||
_, key := it.Element()
|
||||
k := key.AsString()
|
||||
if mapVar.Type().IsObjectType() {
|
||||
if mapVar.Type().HasAttribute(k) {
|
||||
value := mapVar.GetAttr(k)
|
||||
values = append(values, value)
|
||||
}
|
||||
} else {
|
||||
value := mapVar.Index(cty.StringVal(k))
|
||||
values = append(values, value)
|
||||
}
|
||||
for it := mapVar.ElementIterator(); it.Next(); {
|
||||
_, val := it.Element()
|
||||
values = append(values, val)
|
||||
}
|
||||
|
||||
if retType.IsTupleType() {
|
||||
|
|
|
@ -1018,21 +1018,55 @@ func TestKeys(t *testing.T) {
|
|||
"hello": cty.NumberIntVal(1),
|
||||
"goodbye": cty.StringVal("adieu"),
|
||||
}),
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.StringVal("goodbye"),
|
||||
cty.StringVal("hello"),
|
||||
}),
|
||||
false,
|
||||
},
|
||||
{ // Not a map
|
||||
{ // for an unknown object we can still return the keys, since they are part of the type
|
||||
cty.UnknownVal(cty.Object(map[string]cty.Type{
|
||||
"hello": cty.Number,
|
||||
"goodbye": cty.String,
|
||||
})),
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.StringVal("goodbye"),
|
||||
cty.StringVal("hello"),
|
||||
}),
|
||||
false,
|
||||
},
|
||||
{ // an empty object has no keys
|
||||
cty.EmptyObjectVal,
|
||||
cty.EmptyTupleVal,
|
||||
false,
|
||||
},
|
||||
{ // an empty map has no keys, but the result should still be properly typed
|
||||
cty.MapValEmpty(cty.Number),
|
||||
cty.ListValEmpty(cty.String),
|
||||
false,
|
||||
},
|
||||
{ // Unknown map has unknown keys
|
||||
cty.UnknownVal(cty.Map(cty.String)),
|
||||
cty.UnknownVal(cty.List(cty.String)),
|
||||
false,
|
||||
},
|
||||
{ // Not a map at all, so invalid
|
||||
cty.StringVal("foo"),
|
||||
cty.NilVal,
|
||||
true,
|
||||
},
|
||||
{ // Unknown map
|
||||
cty.UnknownVal(cty.Map(cty.String)),
|
||||
cty.UnknownVal(cty.List(cty.String)),
|
||||
false,
|
||||
{ // Can't get keys from a null object
|
||||
cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"hello": cty.Number,
|
||||
"goodbye": cty.String,
|
||||
})),
|
||||
cty.NilVal,
|
||||
true,
|
||||
},
|
||||
{ // Can't get keys from a null map
|
||||
cty.NullVal(cty.Map(cty.Number)),
|
||||
cty.NilVal,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -2015,8 +2049,8 @@ func TestValues(t *testing.T) {
|
|||
},
|
||||
{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"hello": cty.StringVal("world"),
|
||||
"what's": cty.StringVal("up"),
|
||||
"hello": cty.StringVal("world"),
|
||||
}),
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.StringVal("world"),
|
||||
|
@ -2024,6 +2058,22 @@ func TestValues(t *testing.T) {
|
|||
}),
|
||||
false,
|
||||
},
|
||||
{ // empty object
|
||||
cty.EmptyObjectVal,
|
||||
cty.EmptyTupleVal,
|
||||
false,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.Object(map[string]cty.Type{
|
||||
"what's": cty.String,
|
||||
"hello": cty.Bool,
|
||||
})),
|
||||
cty.UnknownVal(cty.Tuple([]cty.Type{
|
||||
cty.Bool,
|
||||
cty.String,
|
||||
})),
|
||||
false,
|
||||
},
|
||||
{ // note ordering: keys are sorted first
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"hello": cty.NumberIntVal(1),
|
||||
|
@ -2051,7 +2101,10 @@ func TestValues(t *testing.T) {
|
|||
"hello": cty.ListVal([]cty.Value{cty.StringVal("world")}),
|
||||
"what's": cty.UnknownVal(cty.List(cty.String)),
|
||||
}),
|
||||
cty.UnknownVal(cty.List(cty.List(cty.String))),
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.ListVal([]cty.Value{cty.StringVal("world")}),
|
||||
cty.UnknownVal(cty.List(cty.String)),
|
||||
}),
|
||||
false,
|
||||
},
|
||||
{ // empty m
|
||||
|
@ -2059,6 +2112,11 @@ func TestValues(t *testing.T) {
|
|||
cty.ListValEmpty(cty.DynamicPseudoType),
|
||||
false,
|
||||
},
|
||||
{ // unknown m
|
||||
cty.UnknownVal(cty.Map(cty.String)),
|
||||
cty.UnknownVal(cty.List(cty.String)),
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
|
Loading…
Reference in New Issue