Skip to content

Commit 0fdcf0e

Browse files
bbasataapparentlymartmildwonkey
authored
Forward to v1.6.0 (#15)
* Prepare for a possible future 1.5.2 release * function/stdlib: Check for negative indices in ElementFunc Previously a negative index would lead to a panic. * Update CHANGELOG.md * convert: Experimental optional object attributes Previously we required that when converting to an object type the input must have at least the attributes from the target type, and optionally additional attributes that would then be discarded. We'll now allow creating an object type with one or more optional attributes. These are optional only for conversion purposes: converting to an object type with optional arguments will substitute a null value for any attribute that doesn't exist in the source object type. This being handled only under conversion is consistent with some other similar cases: an object type is not conforming to a type constraint if it has attributes that are not in the type constraint, even though a conversion to that type constraint would be accepted. Likewise, a number doesn't conform to the string type even though a conversion is available. This distinction is so that cty applications can potentially bring their own separate conversion rules if they wish, ignoring the convert package in this repository. For now this new capability is explicitly experimental and so not subject to our typical backward-compatibility promises. The behavior of any function producing or working with object types that have optional attributes is subject to change even in future minor versions of this package. * Update CHANGELOG.md * dependencies: use major version 4 of the underlying msgpack library This is just in the interests of keeping things up-to-date and doesn't really confer any specific benefit. The underlying library now defaults to always using the full-length encoding for integers provided as int64, so this change also includes an explicit opt-in to the old behavior of selecting the most compact integer representation possible, because for cty the different numeric encodings are just a size optimization and have no semantic meaning. * cty: Fix various quirks of handling sets with unknown values Early in the implementation of cty I made an oversight which has, unfortunately, played out as a variety of incorrect behaviors throughout the cty functions: when a set contains unknown values, those unknown values can potentially be standing in for values that are equal to others in the set, and thus the effective length of the set can't be predicted. Previously the Length function would return, in effect, the maximum possible size of the set, which would result if all of the values represented by the unknowns are non-equal. The caller might then get a different known result from a call with unknowns than from a subsequent call with those unknowns replaced with known values, which violates the guarantees that cty intends to make when handling unknown values. This changeset therefore starts by addressing that root bug: Length will now return an unknown number if called on a set containing unknown values, correctly representing that the final length is unpredictable. This in turn had some downstream consequences for other functionality. In particular, it should not have been possible to convert a set whose length is unknown to a list, because it's not possible for a list to have an unknown length. Therefore this changeset also includes a fix to the convert package so that an unknown-length set converts to an unknown list, which also addresses the related problem that a conversion from a set to list can't predict where the unknown values will appear in the sort order and thus can't predict the list indices even for the known elements. The rest of the changes here are similar adjustments to account for the possibility of an unknown set length, in most cases similarly returning an unknown result. This changeset causes a difference in observable behavior from the previous version, but it's not considered to be a breaking change because the previous behavior was defective. With that said, callers that were inadvertently depending on the defective behavior will find that their calling application now behaves slightly differently, and so may wish to make adjustments if the defective prior behavior was actually desirable for some unusual situation. As a pragmatic accommodation for existing callers, the Value.LengthInt function is now defined as returning the maximum possible length in situations where the length is unknown. This aligns with the number of iterations a caller would encounter using an ElementIterator on such a value, and also reflects a suitable capacity to use when allocating a Go slice to append iterated elements into. * Update CHANGELOG.md * v1.6.0 * Update CHANGELOG.md --------- Co-authored-by: Martin Atkins <[email protected]> Co-authored-by: Kristin Laemmert <[email protected]>
1 parent 8d5ce63 commit 0fdcf0e

27 files changed

+785
-57
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
# 1.6.0 (Unreleased)
2+
3+
* Fixed various defects in the handling of sets containing unknown values. This will cause unknown values to now be returned in more situations, whereas before `cty` would often return incorrect results when working with sets containing unknown values. The list of defects fixed in this release includes:
4+
- `cty`: The length of a set containing unknown values, as defined by `Value.Length`, is itself unknown, reflecting the fact that unknown values may be placeholders for values that are equal to other values in the set, which would thus coalesce into a single value.
5+
- `cty:` Converting a set with unknown values to a list produces an unknown value, because type conversion can't predict which indices each element of the set should take (the unknown elements could appear anywhere in the sort order) or the length of the resulting list.
6+
- `function/stdlib`: the `LengthFunc` and `ToList` functions wrap the behaviors described in the previous two items and are therefore also fixed in the same way.
7+
- `function/stclib`: `FormatListFunc` depends on knowing the length of all of its sequence arguments (which includes support for sets), so it will return an unknown result if given a set with an unknown length.
8+
- `function/stdlib`: The various set operation functions were previously producing incorrect results if one of their given sets contained unknown values, because they didn't consider that unknown values on one set may be placeholders for values that are equal to elements of the other set. For example, `SetSubtractFunc` now produces a wholly-unknown result if either of its arguments contains an unknown element, because it can't predict whether that unknown element represents a value equal to an element in the other set.
9+
- `cty`: The `Value.Equal` function would previously incorrectly return a known `cty.False` if one of the given sets contained an unknown value. It will now return `cty.UnknownVal(cty.Bool)` in that case, reflecting that the result could be either `cty.True` or `cty.False` were the unknown values to be replaced with known values.
10+
- `cty`: The `Value.LengthInt` function was also returning incorrect results for sets containing unknown elements. However, given that it is commonly used in conjunction with `ElementIterator` to determine the capacity for a slice to append elements to, it is not fixed and is instead redefined to return the _maximum possible length_, which would result if all of the unknown values represent values that are not equal to any other set element. Applications that use `Value.LengthInt` to determine lengths to return to users who are working in the space of `cty` values should switch to using `Value.Length` instead and handle the possibility of the length being unknown, to avoid returning incorrect results for sets with unknown values.
11+
12+
These are not classified as breaking changes because the previous behavior was defective per the design goals for unknown values. However, callers may notice their application behavior changes along with these fixes when upgrading. The new behaviors should all be more correct than the old; if you observe a change in behavior where there is now an _incorrect_ result for sets containing unknown values (that is, where `cty` claims it knows an answer that it should not actually know), please report that in a GitHub issue.
13+
14+
We advise callers which work with sets that may potentially contain unknown values to review their own set-handling functions to check if they too might be handling sets with unknown values incorrectly, particularly if they work with sets using [integration methods rather than operation methods](./docs/types.md#common-operations-and-integration-methods) (for example, using `Value.ValueList` or `Value.ValueSet` to extract elements directly). It seems that incorrect handling of sets with unknown values has been a common hazard, particularly in codepaths that aim to treat lists and sets as being interchangable.
15+
* `function/stdlib`: The `element` function will no longer panic if given a negative index. Instead, it will return a proper error. ([#62](https://github.com/zclconf/go-cty/pull/62))
16+
* `convert`: **Experimental** support for annotating one or more attributes of an object type as "optional", which the `convert` package can then use to suppress the error that would normally be returned if the source type has no corresponding attribute, and can substitute a correctly-typed null value instead. This new behavior is subject to change even in minor release of `cty`, until it has been tested in experimental releases of downstream applications and potentially modified in response.
17+
118
# 1.5.1 (Unreleased)
219

320
* `function/stdlib`: The `merge` function will no longer panic if all given maps are empty. ([#58](https://github.com/zclconf/go-cty/pull/58))

cty/convert/conversion_collection.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ import (
1313
// if we're converting from a set into a list of the same element type.)
1414
func conversionCollectionToList(ety cty.Type, conv conversion) conversion {
1515
return func(val cty.Value, path cty.Path) (cty.Value, error) {
16+
if !val.Length().IsKnown() {
17+
// If the input collection has an unknown length (which is true
18+
// for a set containing unknown values) then our result must be
19+
// an unknown list, because we can't predict how many elements
20+
// the resulting list should have.
21+
return cty.UnknownVal(cty.List(val.Type().ElementType())), nil
22+
}
23+
1624
elems := make([]cty.Value, 0, val.LengthInt())
1725
i := int64(0)
1826
elemPath := append(path.Copy(), nil)
@@ -458,6 +466,16 @@ func conversionMapToObject(mapType cty.Type, objType cty.Type, unsafe bool) conv
458466
elems[name.AsString()] = val
459467
}
460468

469+
for name, aty := range objectAtys {
470+
if _, exists := elems[name]; !exists {
471+
if optional := objType.AttributeOptional(name); optional {
472+
elems[name] = cty.NullVal(aty)
473+
} else {
474+
return cty.NilVal, path.NewErrorf("map has no element for required attribute %q", name)
475+
}
476+
}
477+
}
478+
461479
return cty.ObjectVal(elems), nil
462480
}
463481
}

cty/convert/conversion_object.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,29 @@ import (
1111
// type, meaning that each attribute of the output type has a corresponding
1212
// attribute in the input type where a recursive conversion is available.
1313
//
14+
// If the "out" type has any optional attributes, those attributes may be
15+
// absent in the "in" type, in which case null values will be used in their
16+
// place in the result.
17+
//
1418
// Shallow object conversions work the same for both safe and unsafe modes,
1519
// but the safety flag is passed on to recursive conversions and may thus
1620
// limit the above definition of "subset".
1721
func conversionObjectToObject(in, out cty.Type, unsafe bool) conversion {
1822
inAtys := in.AttributeTypes()
1923
outAtys := out.AttributeTypes()
24+
outOptionals := out.OptionalAttributes()
2025
attrConvs := make(map[string]conversion)
2126

2227
for name, outAty := range outAtys {
2328
inAty, exists := inAtys[name]
2429
if !exists {
30+
if _, optional := outOptionals[name]; optional {
31+
// If it's optional then we'll skip inserting an
32+
// attribute conversion and then deal with inserting
33+
// the default value in our overall conversion logic
34+
// later.
35+
continue
36+
}
2537
// No conversion is available, then.
2638
return nil
2739
}
@@ -71,6 +83,13 @@ func conversionObjectToObject(in, out cty.Type, unsafe bool) conversion {
7183
attrVals[name] = val
7284
}
7385

86+
for name := range outOptionals {
87+
if _, exists := attrVals[name]; !exists {
88+
wantTy := outAtys[name]
89+
attrVals[name] = cty.NullVal(wantTy)
90+
}
91+
}
92+
7493
return cty.ObjectVal(attrVals), nil
7594
}
7695
}

cty/convert/mismatch_msg.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ func mismatchMessageObjects(got, want cty.Type) string {
7878
for name, wantAty := range wantAtys {
7979
gotAty, exists := gotAtys[name]
8080
if !exists {
81-
missingAttrs = append(missingAttrs, name)
81+
if !want.AttributeOptional(name) {
82+
missingAttrs = append(missingAttrs, name)
83+
}
8284
continue
8385
}
8486

cty/convert/public_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ func TestConvert(t *testing.T) {
143143
Type: cty.List(cty.DynamicPseudoType),
144144
WantError: true, // there is no type that both tuple elements can unify to for conversion to list
145145
},
146+
{
147+
Value: cty.SetVal([]cty.Value{
148+
cty.StringVal("5"),
149+
cty.UnknownVal(cty.String),
150+
}),
151+
Type: cty.Set(cty.Number),
152+
Want: cty.SetVal([]cty.Value{cty.NumberIntVal(5), cty.UnknownVal(cty.Number)}),
153+
},
146154
{
147155
Value: cty.SetVal([]cty.Value{
148156
cty.StringVal("5"),
@@ -182,6 +190,27 @@ func TestConvert(t *testing.T) {
182190
cty.StringVal("10"),
183191
}),
184192
},
193+
{
194+
Value: cty.SetVal([]cty.Value{
195+
cty.StringVal("5"),
196+
cty.UnknownVal(cty.String),
197+
}),
198+
Type: cty.List(cty.String),
199+
Want: cty.UnknownVal(cty.List(cty.String)),
200+
},
201+
{
202+
Value: cty.SetVal([]cty.Value{
203+
cty.UnknownVal(cty.String),
204+
}),
205+
Type: cty.List(cty.String),
206+
// We get a known list value this time because even though we
207+
// don't know the single value that's in the list, we _do_ know
208+
// that there are no other values in the set for it to coalesce
209+
// with.
210+
Want: cty.ListVal([]cty.Value{
211+
cty.UnknownVal(cty.String),
212+
}),
213+
},
185214
{
186215
Value: cty.ListVal([]cty.Value{
187216
cty.NumberIntVal(5),
@@ -381,6 +410,32 @@ func TestConvert(t *testing.T) {
381410
"name": cty.StringVal("John"),
382411
}),
383412
},
413+
{
414+
Value: cty.MapVal(map[string]cty.Value{
415+
"name": cty.StringVal("John"),
416+
}),
417+
Type: cty.Object(map[string]cty.Type{
418+
"name": cty.String,
419+
"greeting": cty.String,
420+
}),
421+
WantError: true, // map has no element for required attribute "greeting"
422+
},
423+
{
424+
Value: cty.MapVal(map[string]cty.Value{
425+
"name": cty.StringVal("John"),
426+
}),
427+
Type: cty.ObjectWithOptionalAttrs(
428+
map[string]cty.Type{
429+
"name": cty.String,
430+
"greeting": cty.String,
431+
},
432+
[]string{"greeting"},
433+
),
434+
Want: cty.ObjectVal(map[string]cty.Value{
435+
"greeting": cty.NullVal(cty.String),
436+
"name": cty.StringVal("John"),
437+
}),
438+
},
384439
{
385440
Value: cty.MapVal(map[string]cty.Value{
386441
"a": cty.NumberIntVal(2),
@@ -476,6 +531,50 @@ func TestConvert(t *testing.T) {
476531
}),
477532
WantError: true, // given value must have superset object type
478533
},
534+
{
535+
Value: cty.ObjectVal(map[string]cty.Value{
536+
"bar": cty.StringVal("bar value"),
537+
}),
538+
Type: cty.ObjectWithOptionalAttrs(
539+
map[string]cty.Type{
540+
"foo": cty.String,
541+
"bar": cty.String,
542+
},
543+
[]string{"foo"},
544+
),
545+
Want: cty.ObjectVal(map[string]cty.Value{
546+
"foo": cty.NullVal(cty.String),
547+
"bar": cty.StringVal("bar value"),
548+
}),
549+
},
550+
{
551+
Value: cty.ObjectVal(map[string]cty.Value{
552+
"foo": cty.StringVal("foo value"),
553+
"bar": cty.StringVal("bar value"),
554+
}),
555+
Type: cty.ObjectWithOptionalAttrs(
556+
map[string]cty.Type{
557+
"foo": cty.String,
558+
"bar": cty.String,
559+
},
560+
[]string{"foo"},
561+
),
562+
Want: cty.ObjectVal(map[string]cty.Value{
563+
"foo": cty.StringVal("foo value"),
564+
"bar": cty.StringVal("bar value"),
565+
}),
566+
},
567+
{
568+
Value: cty.EmptyObjectVal,
569+
Type: cty.ObjectWithOptionalAttrs(
570+
map[string]cty.Type{
571+
"foo": cty.String,
572+
"bar": cty.String,
573+
},
574+
[]string{"foo"},
575+
),
576+
WantError: true, // Attribute "bar" is required
577+
},
479578
{
480579
Value: cty.ObjectVal(map[string]cty.Value{
481580
"foo": cty.True,

cty/function/stdlib/collection.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ var ElementFunc = function.New(&function.Spec{
138138
},
139139
Type: func(args []cty.Value) (cty.Type, error) {
140140
list := args[0]
141+
index := args[1]
142+
if index.IsKnown() {
143+
if index.LessThan(cty.NumberIntVal(0)).True() {
144+
return cty.DynamicPseudoType, fmt.Errorf("cannot use element function with a negative index")
145+
}
146+
}
147+
141148
listTy := list.Type()
142149
switch {
143150
case listTy.IsListType():
@@ -173,6 +180,10 @@ var ElementFunc = function.New(&function.Spec{
173180
return cty.DynamicVal, fmt.Errorf("invalid index: %s", err)
174181
}
175182

183+
if args[1].LessThan(cty.NumberIntVal(0)).True() {
184+
return cty.DynamicVal, fmt.Errorf("cannot use element function with a negative index")
185+
}
186+
176187
if !args[0].IsKnown() {
177188
return cty.UnknownVal(retType), nil
178189
}
@@ -492,6 +503,11 @@ var FlattenFunc = function.New(&function.Spec{
492503
// We can flatten lists with unknown values, as long as they are not
493504
// lists themselves.
494505
func flattener(flattenList cty.Value) ([]cty.Value, bool) {
506+
if !flattenList.Length().IsKnown() {
507+
// If we don't know the length of what we're flattening then we can't
508+
// predict the length of our result yet either.
509+
return nil, false
510+
}
495511
out := make([]cty.Value, 0)
496512
for it := flattenList.ElementIterator(); it.Next(); {
497513
_, val := it.Element()
@@ -872,6 +888,10 @@ var SetProductFunc = function.New(&function.Spec{
872888

873889
total := 1
874890
for _, arg := range args {
891+
if !arg.Length().IsKnown() {
892+
return cty.UnknownVal(retType), nil
893+
}
894+
875895
// Because of our type checking function, we are guaranteed that
876896
// all of the arguments are known, non-null values of types that
877897
// support LengthInt.
@@ -1019,7 +1039,8 @@ func sliceIndexes(args []cty.Value) (int, int, bool, error) {
10191039
var startIndex, endIndex, length int
10201040
var startKnown, endKnown, lengthKnown bool
10211041

1022-
if args[0].Type().IsTupleType() || args[0].IsKnown() { // if it's a tuple then we always know the length by the type, but lists must be known
1042+
// If it's a tuple then we always know the length by the type, but collections might be unknown or have unknown length
1043+
if args[0].Type().IsTupleType() || args[0].Length().IsKnown() {
10231044
length = args[0].LengthInt()
10241045
lengthKnown = true
10251046
}

0 commit comments

Comments
 (0)