Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: hashicorp/go-cty
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.4.1
Choose a base ref
...
head repository: hashicorp/go-cty
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.5.0
Choose a head ref
  • 5 commits
  • 13 files changed
  • 6 contributors

Commits on Mar 6, 2025

  1. Cherry-pick another upstream v1.4.1 change (#9)

    * Revert "Forward to v1.4.1 (#7)"
    
    This reverts commit 92d5fa8.
    
    * convert: fix panic when converting nested objects
    
    When converting a list of objects, it is necessary to unify the types of
    the child elements.
    
    * Update CHANGELOG.md
    
    * set: Add SameRules method to set.Rules
    
    Previously, we checked if two sets had the same rules using a simple
    equality check. This panics if the sets contain object types, as they
    cannot be compared using `==`.
    
    Adding a SameRules method to the Rules interface allows us to delegate
    to the Type.Equals method where necessary, fixing this problem.
    
    * stdlib: Fix set function crashes with empty sets
    
    If one or more arguments to the stdlib set functions was an empty set of
    dynamic pseudo type, the functions would panic due to incompatible set
    rules.
    
    We can special case empty dynamic pseudo type sets to be ignored through
    type unification, because they are always capable of being converted to
    any other type.
    
    * Update CHANGELOG.md
    
    * v1.4.1 release
    
    * Update CHANGELOG.md
    
    * Replace import in test
    
    ---------
    
    Co-authored-by: Kristin Laemmert <mildwonkey@users.noreply.github.com>
    Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
    Co-authored-by: Alisdair McDiarmid <alisdair@users.noreply.github.com>
    4 people authored Mar 6, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    soyuka Antoine Bluchet
    Copy the full SHA
    016615a View commit details
  2. Forward to v1.4.2 (#8)

    * Prepare CHANGELOG for a forthcoming 1.4.2 release
    
    * function/stdlib: jsonencode should produce a string representation of null
    
    * Update CHANGELOG.md
    
    * v1.4.2 release
    
    * Update CHANGELOG.md
    
    ---------
    
    Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
    Co-authored-by: Pam Selle <pamela.selle@gmail.com>
    3 people authored Mar 6, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    soyuka Antoine Bluchet
    Copy the full SHA
    b56e5cf View commit details
  3. Forward to 1.5.0 (#10)

    * Changelog preparation for future 1.4.2 release
    
    * convert: Fix panic: heterogeneous tuple with null
    
    Tuples with elements of different types can be converted to homogeneous
    collections (sets or lists), so long as their elements are unifiable.
    For example:
    
      list("a", "b")     // all elements have the same type
      list("a", 5)       // "a" and 5 can be unified to string
      list("a", 5, null) // null is a valid value for string
    
    However, tuples with elements which are not unifiable cannot be
    converted to homogeneous collections:
    
      list(["a"], "b")   // no common type for list(string) and string
    
    This commit fixes a panic for this failure case, when the tuple contains
    both non-unifiable types and a null value:
    
      list(["a"], "b", null) // should not panic
    
    The null value was causing the unification process to result in a list
    or set of dynamic type, which causes the conversion functions to pass
    through the original value. This meant that in the final conversion
    step, we would attempt to construct a list or set of different values,
    which panics.
    
    * Update CHANGELOG.md
    
    * cty: Value.HasWhollyKnownType
    
    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".
    
    * Update CHANGELOG.md
    
    * v1.5.0
    
    * Update CHANGELOG.md
    
    ---------
    
    Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
    Co-authored-by: Alisdair McDiarmid <alisdair@users.noreply.github.com>
    Co-authored-by: James Bardin <j.bardin@gmail.com>
    4 people authored Mar 6, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    soyuka Antoine Bluchet
    Copy the full SHA
    08cd546 View commit details

Commits on Mar 17, 2025

  1. Update golang.org/x/text (#11)

    * Update golang.org/x/text
    
    * fixup! Update golang.org/x/text
    bbasata authored Mar 17, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    soyuka Antoine Bluchet
    Copy the full SHA
    f3d5407 View commit details
  2. Changelog for v1.5.0 (#13)

    bbasata authored Mar 17, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    soyuka Antoine Bluchet
    Copy the full SHA
    f09c453 View commit details
Showing with 463 additions and 19 deletions.
  1. +10 −1 CHANGELOG.md
  2. +70 −7 cty/convert/conversion_collection.go
  3. +117 −0 cty/convert/public_test.go
  4. +5 −0 cty/function/stdlib/json.go
  5. +4 −0 cty/function/stdlib/json_test.go
  6. +1 −1 cty/set_internals_test.go
  7. +1 −1 cty/type.go
  8. +56 −0 cty/type_test.go
  9. +34 −0 cty/value.go
  10. +13 −5 cty/value_ops.go
  11. +149 −0 cty/value_ops_test.go
  12. +1 −1 go.mod
  13. +2 −3 go.sum
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
# 1.5.0 (March 17, 2025)

* `cty`: New `Value.HasWhollyKnownType` method, for testing whether a value's type could potentially change if any unknown values it was constructed from were to become known. ([#55](https://github.com/zclconf/go-cty/pull/55))
* `convert`: Fix incorrect panic when converting a tuple with a dynamic-typed null member into a list or set, due to overly-liberal type unification. ([#56](https://github.com/zclconf/go-cty/pull/56))

# 1.4.2 (Unreleased)

* `function/stdlib`: The `jsonencode` function will now correctly accept a null as its argument, and produce the JSON representation `"null"` rather than returning an error. ([#54](https://github.com/zclconf/go-cty/pull/54))
* `convert`: Don't panic when asked to convert a tuple of objects to a list type constraint containing a nested `cty.DynamicPseudoType`. ([#53](https://github.com/zclconf/go-cty/pull/53))

# 1.4.1 (March 5, 2025)

* `function/stdlib`: Fix various panics related to sets with unknown element types in the set-manipulation functions. ([#52](https://github.com/zclconf/go-cty/pull/52))
* `convert`: Don't panic when asked to convert a tuple of objects to a list type constraint containing a nested `cty.DynamicPseudoType`. ([#53](https://github.com/zclconf/go-cty/pull/53))
* `json`: Remove `json.UnmarshalDynamicWithImpliedType` function that was only available in hashicorp/go-cty 1.4.1 pseudo-versions. ([#6](https://github.com/hashicorp/go-cty/pull/6))

# 1.4.0 (April 7, 2020)
77 changes: 70 additions & 7 deletions cty/convert/conversion_collection.go
Original file line number Diff line number Diff line change
@@ -156,34 +156,45 @@ func conversionCollectionToMap(ety cty.Type, conv conversion) conversion {
// given tuple type and return a set of the given element type.
//
// Will panic if the given tupleType isn't actually a tuple type.
func conversionTupleToSet(tupleType cty.Type, listEty cty.Type, unsafe bool) conversion {
func conversionTupleToSet(tupleType cty.Type, setEty cty.Type, unsafe bool) conversion {
tupleEtys := tupleType.TupleElementTypes()

if len(tupleEtys) == 0 {
// Empty tuple short-circuit
return func(val cty.Value, path cty.Path) (cty.Value, error) {
return cty.SetValEmpty(listEty), nil
return cty.SetValEmpty(setEty), nil
}
}

if listEty == cty.DynamicPseudoType {
if setEty == cty.DynamicPseudoType {
// This is a special case where the caller wants us to find
// a suitable single type that all elements can convert to, if
// possible.
listEty, _ = unify(tupleEtys, unsafe)
if listEty == cty.NilType {
setEty, _ = unify(tupleEtys, unsafe)
if setEty == cty.NilType {
return nil
}

// If the set element type after unification is still the dynamic
// type, the only way this can result in a valid set is if all values
// are of dynamic type
if setEty == cty.DynamicPseudoType {
for _, tupleEty := range tupleEtys {
if !tupleEty.Equals(cty.DynamicPseudoType) {
return nil
}
}
}
}

elemConvs := make([]conversion, len(tupleEtys))
for i, tupleEty := range tupleEtys {
if tupleEty.Equals(listEty) {
if tupleEty.Equals(setEty) {
// no conversion required
continue
}

elemConvs[i] = getConversion(tupleEty, listEty, unsafe)
elemConvs[i] = getConversion(tupleEty, setEty, unsafe)
if elemConvs[i] == nil {
// If any of our element conversions are impossible, then the our
// whole conversion is impossible.
@@ -244,6 +255,17 @@ func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) co
if listEty == cty.NilType {
return nil
}

// If the list element type after unification is still the dynamic
// type, the only way this can result in a valid list is if all values
// are of dynamic type
if listEty == cty.DynamicPseudoType {
for _, tupleEty := range tupleEtys {
if !tupleEty.Equals(cty.DynamicPseudoType) {
return nil
}
}
}
}

elemConvs := make([]conversion, len(tupleEtys))
@@ -265,6 +287,7 @@ func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) co
// element conversions in elemConvs
return func(val cty.Value, path cty.Path) (cty.Value, error) {
elems := make([]cty.Value, 0, len(elemConvs))
elemTys := make([]cty.Type, 0, len(elems))
elemPath := append(path.Copy(), nil)
i := int64(0)
it := val.ElementIterator()
@@ -284,10 +307,15 @@ func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) co
}
}
elems = append(elems, val)
elemTys = append(elemTys, val.Type())

i++
}

elems, err := conversionUnifyListElements(elems, elemPath, unsafe)
if err != nil {
return cty.NilVal, err
}
return cty.ListVal(elems), nil
}
}
@@ -441,6 +469,7 @@ func conversionUnifyCollectionElements(elems map[string]cty.Value, path cty.Path
}
unifiedType, _ := unify(elemTypes, unsafe)
if unifiedType == cty.NilType {
return nil, path.NewErrorf("collection elements cannot be unified")
}

unifiedElems := make(map[string]cty.Value)
@@ -486,3 +515,37 @@ func conversionCheckMapElementTypes(elems map[string]cty.Value, path cty.Path) e

return nil
}

func conversionUnifyListElements(elems []cty.Value, path cty.Path, unsafe bool) ([]cty.Value, error) {
elemTypes := make([]cty.Type, len(elems))
for i, elem := range elems {
elemTypes[i] = elem.Type()
}
unifiedType, _ := unify(elemTypes, unsafe)
if unifiedType == cty.NilType {
return nil, path.NewErrorf("collection elements cannot be unified")
}

ret := make([]cty.Value, len(elems))
elemPath := append(path.Copy(), nil)

for i, elem := range elems {
if elem.Type().Equals(unifiedType) {
ret[i] = elem
continue
}
conv := getConversion(elem.Type(), unifiedType, unsafe)
if conv == nil {
}
elemPath[len(elemPath)-1] = cty.IndexStep{
Key: cty.NumberIntVal(int64(i)),
}
val, err := conv(elem, elemPath)
if err != nil {
return nil, err
}
ret[i] = val
}

return ret, nil
}
117 changes: 117 additions & 0 deletions cty/convert/public_test.go
Original file line number Diff line number Diff line change
@@ -649,6 +649,123 @@ func TestConvert(t *testing.T) {
"b": cty.MapValEmpty(cty.String),
}),
},
// https://github.com/hashicorp/terraform/issues/21588:
{
Value: cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"a": cty.EmptyObjectVal,
"b": cty.NumberIntVal(2),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{"var1": cty.StringVal("val1")}),
"b": cty.StringVal("2"),
}),
}),
Type: cty.List(cty.Object(map[string]cty.Type{
"a": cty.DynamicPseudoType,
"b": cty.String,
})),
Want: cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapValEmpty(cty.String),
"b": cty.StringVal("2"),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.MapVal(map[string]cty.Value{"var1": cty.StringVal("val1")}),
"b": cty.StringVal("2"),
}),
}),
WantError: false,
},
// https://github.com/hashicorp/terraform/issues/24377:
{
Value: cty.TupleVal([]cty.Value{
cty.ListVal([]cty.Value{cty.StringVal("a")}),
cty.StringVal("b"),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.Set(cty.DynamicPseudoType),
WantError: true,
},
{
Value: cty.TupleVal([]cty.Value{
cty.ListVal([]cty.Value{cty.StringVal("a")}),
cty.StringVal("b"),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.List(cty.DynamicPseudoType),
WantError: true,
},
{
Value: cty.TupleVal([]cty.Value{
cty.ListVal([]cty.Value{cty.StringVal("a")}),
cty.StringVal("b"),
}),
Type: cty.Set(cty.DynamicPseudoType),
WantError: true,
},
{
Value: cty.TupleVal([]cty.Value{
cty.ListVal([]cty.Value{cty.StringVal("a")}),
cty.StringVal("b"),
}),
Type: cty.List(cty.DynamicPseudoType),
WantError: true,
},
{
Value: cty.TupleVal([]cty.Value{
cty.StringVal("a"),
cty.NumberIntVal(9),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.Set(cty.DynamicPseudoType),
Want: cty.SetVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("9"),
cty.NullVal(cty.DynamicPseudoType),
}),
WantError: false,
},
{
Value: cty.TupleVal([]cty.Value{
cty.StringVal("a"),
cty.NumberIntVal(9),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.List(cty.DynamicPseudoType),
Want: cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("9"),
cty.NullVal(cty.DynamicPseudoType),
}),
WantError: false,
},
{
Value: cty.TupleVal([]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.Set(cty.DynamicPseudoType),
Want: cty.SetVal([]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
}),
WantError: false,
},
{
Value: cty.TupleVal([]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
}),
Type: cty.List(cty.DynamicPseudoType),
Want: cty.ListVal([]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
}),
WantError: false,
},
}

for _, test := range tests {
5 changes: 5 additions & 0 deletions cty/function/stdlib/json.go
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ var JSONEncodeFunc = function.New(&function.Spec{
Name: "val",
Type: cty.DynamicPseudoType,
AllowDynamicType: true,
AllowNull: true,
},
},
Type: function.StaticReturnType(cty.String),
@@ -24,6 +25,10 @@ var JSONEncodeFunc = function.New(&function.Spec{
return cty.UnknownVal(retType), nil
}

if val.IsNull() {
return cty.StringVal("null"), nil
}

buf, err := json.Marshal(val, val.Type())
if err != nil {
return cty.NilVal, err
4 changes: 4 additions & 0 deletions cty/function/stdlib/json_test.go
Original file line number Diff line number Diff line change
@@ -52,6 +52,10 @@ func TestJSONEncode(t *testing.T) {
cty.DynamicVal,
cty.UnknownVal(cty.String),
},
{
cty.NullVal(cty.String),
cty.StringVal("null"),
},
}

for _, test := range tests {
2 changes: 1 addition & 1 deletion cty/set_internals_test.go
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import (
"math/big"
"testing"

"github.com/zclconf/go-cty/cty/set"
"github.com/hashicorp/go-cty/cty/set"
)

func TestSetHashBytes(t *testing.T) {
2 changes: 1 addition & 1 deletion cty/type.go
Original file line number Diff line number Diff line change
@@ -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 {
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)
}
})
}
}
Loading