diff --git a/.travis.yml b/.travis.yml index 1a1eb03..6d10700 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: go go: - - 1.13.x + - 1.15.x + - 1.16.x - tip before_install: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9667554..b989242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# 1.8.0 (Unreleased) + +* `cty`: When running on Go 1.16 or later, the `cty.String` type will now normalize incoming string values using the Unicode 13 normalization rules. +* `function/stdlib`: The various string functions which split strings into individual characters as part of their work will now use the Unicode 13 version of the text segmentation algorithm to do so. + +# 1.7.2 (Unreleased) + +* `cty`: The `Type.GoString` implementation for object types with optional attributes was previously producing incorrect results due to an implementation bug. ([#86](https://github.com/zclconf/go-cty/pull/86)) + +# 1.7.1 (Unreleased) + +* `cty`: The `Value.Multiply` and `Value.Modulo` functions now correctly propagate the floating point precision of the arguments, which avoids generating incorrect results for large integer operands. ([#75](https://github.com/zclconf/go-cty/pull/75)) +* `convert`: The `convert.MismatchMessage` function will now correctly identify mismatching attributes in objects, rather than misreporting attributes that are actually present and correct. ([#78](https://github.com/zclconf/go-cty/pull/78)) +* `function/stdlib`: The `merge` function now returns an empty object if all of its arguments are `null`, rather than returning `null` as before. That's more consistent with its usual behavior of ignoring `null` arguments when there is at least one non-null argument. ([#82](https://github.com/zclconf/go-cty/pull/82)) +* `function/stdlib`: The `coalescelist` function now ignores any arguments that are null, rather than panicking as before.. ([#81](https://github.com/zclconf/go-cty/pull/81)) + # 1.7.0 (Unreleased) * `cty`: `Value.UnmarkDeepWithPaths` and `Value.MarkWithPaths` are like `Value.UnmarkDeep` and `Value.Mark` but they retain path information for each marked value, so that marks can be re-applied later without all the loss of detail that results from `Value.UnmarkDeep` aggregating together all of the nested marks. diff --git a/cty/convert/mismatch_msg.go b/cty/convert/mismatch_msg.go index 9873621..5dad886 100644 --- a/cty/convert/mismatch_msg.go +++ b/cty/convert/mismatch_msg.go @@ -84,6 +84,10 @@ func mismatchMessageObjects(got, want cty.Type) string { continue } + if gotAty.Equals(wantAty) { + continue // exact match, so no problem + } + // We'll now try to convert these attributes in isolation and // see if we have a nested conversion error to report. // We'll try an unsafe conversion first, and then fall back on diff --git a/cty/convert/mismatch_msg_test.go b/cty/convert/mismatch_msg_test.go index 0aa3285..66643da 100644 --- a/cty/convert/mismatch_msg_test.go +++ b/cty/convert/mismatch_msg_test.go @@ -104,6 +104,24 @@ func TestMismatchMessage(t *testing.T) { cty.List(cty.DynamicPseudoType), `all list elements must have the same type`, }, + { + cty.Object(map[string]cty.Type{ + "foo": cty.Bool, + "bar": cty.String, + "baz": cty.Object(map[string]cty.Type{ + "boop": cty.Number, + }), + }), + cty.Object(map[string]cty.Type{ + "foo": cty.Bool, + "bar": cty.String, + "baz": cty.Object(map[string]cty.Type{ + "boop": cty.Number, + "beep": cty.Bool, + }), + }), + `attribute "baz": attribute "beep" is required`, + }, } for _, test := range tests { diff --git a/cty/function/stdlib/collection.go b/cty/function/stdlib/collection.go index 688781b..c5ed4de 100644 --- a/cty/function/stdlib/collection.go +++ b/cty/function/stdlib/collection.go @@ -251,6 +251,10 @@ var CoalesceListFunc = function.New(&function.Spec{ return cty.UnknownVal(retType), nil } + if arg.IsNull() { + continue + } + if arg.LengthInt() > 0 { return arg, nil } @@ -758,16 +762,10 @@ var MergeFunc = function.New(&function.Spec{ Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { outputMap := make(map[string]cty.Value) - // if all inputs are null, return a null value rather than an object - // with null attributes - allNull := true for _, arg := range args { if arg.IsNull() { continue - } else { - allNull = false } - for it := arg.ElementIterator(); it.Next(); { k, v := it.Element() outputMap[k.AsString()] = v @@ -775,8 +773,6 @@ var MergeFunc = function.New(&function.Spec{ } switch { - case allNull: - return cty.NullVal(retType), nil case retType.IsMapType(): if len(outputMap) == 0 { return cty.MapValEmpty(retType.ElementType()), nil diff --git a/cty/function/stdlib/collection_test.go b/cty/function/stdlib/collection_test.go index 1120e21..a718e36 100644 --- a/cty/function/stdlib/collection_test.go +++ b/cty/function/stdlib/collection_test.go @@ -291,14 +291,21 @@ func TestMerge(t *testing.T) { }), false, }, - { // handle null map + { // all inputs are null []cty.Value{ cty.NullVal(cty.Map(cty.String)), cty.NullVal(cty.Object(map[string]cty.Type{ "a": cty.List(cty.String), })), }, - cty.NullVal(cty.EmptyObject), + cty.EmptyObjectVal, + false, + }, + { // single null input + []cty.Value{ + cty.MapValEmpty(cty.String), + }, + cty.MapValEmpty(cty.String), false, }, { // handle null object @@ -843,3 +850,136 @@ func TestElement(t *testing.T) { }) } } + +func TestCoalesceList(t *testing.T) { + tests := map[string]struct { + Values []cty.Value + Want cty.Value + Err bool + }{ + "returns first list if non-empty": { + []cty.Value{ + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + }), + cty.ListVal([]cty.Value{ + cty.StringVal("c"), + cty.StringVal("d"), + }), + }, + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + }), + false, + }, + "returns second list if first is empty": { + []cty.Value{ + cty.ListValEmpty(cty.String), + cty.ListVal([]cty.Value{ + cty.StringVal("c"), + cty.StringVal("d"), + }), + }, + cty.ListVal([]cty.Value{ + cty.StringVal("c"), + cty.StringVal("d"), + }), + false, + }, + "return type is dynamic, not unified": { + []cty.Value{ + cty.ListValEmpty(cty.String), + cty.ListVal([]cty.Value{ + cty.NumberIntVal(3), + cty.NumberIntVal(4), + }), + }, + cty.ListVal([]cty.Value{ + cty.NumberIntVal(3), + cty.NumberIntVal(4), + }), + false, + }, + "works with tuples": { + []cty.Value{ + cty.EmptyTupleVal, + cty.TupleVal([]cty.Value{ + cty.StringVal("c"), + cty.StringVal("d"), + }), + }, + cty.TupleVal([]cty.Value{ + cty.StringVal("c"), + cty.StringVal("d"), + }), + false, + }, + "unknown arguments": { + []cty.Value{ + cty.UnknownVal(cty.List(cty.String)), + cty.ListVal([]cty.Value{ + cty.StringVal("c"), + cty.StringVal("d"), + }), + }, + cty.DynamicVal, + false, + }, + "null arguments": { + []cty.Value{ + cty.NullVal(cty.List(cty.String)), + cty.ListVal([]cty.Value{ + cty.StringVal("c"), + cty.StringVal("d"), + }), + }, + cty.ListVal([]cty.Value{ + cty.StringVal("c"), + cty.StringVal("d"), + }), + false, + }, + "all null arguments": { + []cty.Value{ + cty.NullVal(cty.List(cty.String)), + cty.NullVal(cty.List(cty.String)), + }, + cty.NilVal, + true, + }, + "invalid arguments": { + []cty.Value{ + cty.MapVal(map[string]cty.Value{"a": cty.True}), + cty.ObjectVal(map[string]cty.Value{"b": cty.False}), + }, + cty.NilVal, + true, + }, + "no arguments": { + []cty.Value{}, + cty.NilVal, + true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got, err := CoalesceList(test.Values...) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} diff --git a/cty/function/stdlib/format.go b/cty/function/stdlib/format.go index 6ff3152..81be361 100644 --- a/cty/function/stdlib/format.go +++ b/cty/function/stdlib/format.go @@ -6,7 +6,7 @@ import ( "math/big" "strings" - "github.com/apparentlymart/go-textseg/v12/textseg" + "github.com/apparentlymart/go-textseg/v13/textseg" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/go-cty/cty/convert" diff --git a/cty/function/stdlib/string.go b/cty/function/stdlib/string.go index 60e0ab5..a9a0ccb 100644 --- a/cty/function/stdlib/string.go +++ b/cty/function/stdlib/string.go @@ -6,7 +6,7 @@ import ( "sort" "strings" - "github.com/apparentlymart/go-textseg/v12/textseg" + "github.com/apparentlymart/go-textseg/v13/textseg" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/go-cty/cty/function" diff --git a/cty/object_type.go b/cty/object_type.go index 0c67d4e..e6bbab4 100644 --- a/cty/object_type.go +++ b/cty/object_type.go @@ -112,7 +112,7 @@ func (t typeObject) GoString() string { return "cty.EmptyObject" } if len(t.AttrOptional) > 0 { - opt := make([]string, len(t.AttrOptional)) + var opt []string for k := range t.AttrOptional { opt = append(opt, k) } diff --git a/cty/type.go b/cty/type.go index 5f7813e..5c44575 100644 --- a/cty/type.go +++ b/cty/type.go @@ -36,6 +36,9 @@ func (t typeImplSigil) isTypeImpl() typeImplSigil { // Equals returns true if the other given Type exactly equals the receiver // type. func (t Type) Equals(other Type) bool { + if t == NilType || other == NilType { + return t == other + } return t.typeImpl.Equals(other) } diff --git a/cty/type_test.go b/cty/type_test.go index 7861542..c156837 100644 --- a/cty/type_test.go +++ b/cty/type_test.go @@ -54,3 +54,98 @@ func TestHasDynamicTypes(t *testing.T) { }) } } + +func TestNilTypeEquals(t *testing.T) { + var typ Type + if !typ.Equals(NilType) { + t.Fatal("expected NilTypes to equal") + } +} + +func TestTypeGoString(t *testing.T) { + tests := []struct { + Type Type + Want string + }{ + { + DynamicPseudoType, + `cty.DynamicPseudoType`, + }, + { + String, + `cty.String`, + }, + { + Tuple([]Type{String, Bool}), + `cty.Tuple([]cty.Type{cty.String, cty.Bool})`, + }, + + { + Number, + `cty.Number`, + }, + { + Bool, + `cty.Bool`, + }, + { + List(String), + `cty.List(cty.String)`, + }, + { + List(List(String)), + `cty.List(cty.List(cty.String))`, + }, + { + List(Bool), + `cty.List(cty.Bool)`, + }, + { + Set(String), + `cty.Set(cty.String)`, + }, + { + Set(Map(String)), + `cty.Set(cty.Map(cty.String))`, + }, + { + Set(Bool), + `cty.Set(cty.Bool)`, + }, + { + Tuple([]Type{Bool}), + `cty.Tuple([]cty.Type{cty.Bool})`, + }, + + { + Map(String), + `cty.Map(cty.String)`, + }, + { + Map(Set(String)), + `cty.Map(cty.Set(cty.String))`, + }, + { + Map(Bool), + `cty.Map(cty.Bool)`, + }, + { + Object(map[string]Type{"foo": Bool}), + `cty.Object(map[string]cty.Type{"foo":cty.Bool})`, + }, + { + ObjectWithOptionalAttrs(map[string]Type{"foo": Bool, "bar": String}, []string{"bar"}), + `cty.ObjectWithOptionalAttrs(map[string]cty.Type{"bar":cty.String, "foo":cty.Bool}, []string{"bar"})`, + }, + } + + for _, test := range tests { + t.Run(test.Type.GoString(), func(t *testing.T) { + got := test.Type.GoString() + want := test.Want + if got != want { + t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) + } + }) + } +} diff --git a/cty/value_ops.go b/cty/value_ops.go index 297119b..ec77b4a 100644 --- a/cty/value_ops.go +++ b/cty/value_ops.go @@ -596,8 +596,25 @@ func (val Value) Multiply(other Value) Value { return *shortCircuit } - ret := new(big.Float) + // find the larger precision of the arguments + resPrec := val.v.(*big.Float).Prec() + otherPrec := other.v.(*big.Float).Prec() + if otherPrec > resPrec { + resPrec = otherPrec + } + + // make sure we have enough precision for the product + ret := new(big.Float).SetPrec(512) ret.Mul(val.v.(*big.Float), other.v.(*big.Float)) + + // now reduce the precision back to the greater argument, or the minimum + // required by the product. + minPrec := ret.MinPrec() + if minPrec > resPrec { + resPrec = minPrec + } + ret.SetPrec(resPrec) + return NumberVal(ret) } @@ -669,11 +686,14 @@ func (val Value) Modulo(other Value) Value { // FIXME: This is a bit clumsy. Should come back later and see if there's a // more straightforward way to do this. rat := val.Divide(other) - ratFloorInt := &big.Int{} - rat.v.(*big.Float).Int(ratFloorInt) - work := (&big.Float{}).SetInt(ratFloorInt) + ratFloorInt, _ := rat.v.(*big.Float).Int(nil) + + // start with a copy of the original larger value so that we do not lose + // precision. + v := val.v.(*big.Float) + work := new(big.Float).Copy(v).SetInt(ratFloorInt) work.Mul(other.v.(*big.Float), work) - work.Sub(val.v.(*big.Float), work) + work.Sub(v, work) return NumberVal(work) } diff --git a/cty/value_ops_test.go b/cty/value_ops_test.go index c928055..6f49ac9 100644 --- a/cty/value_ops_test.go +++ b/cty/value_ops_test.go @@ -1777,6 +1777,17 @@ func TestValueMultiply(t *testing.T) { Zero.Mark(2), Zero.WithMarks(NewValueMarks(1, 2)), }, + { + MustParseNumberVal("967323432120515089486873574508975134568969931547"), + NumberFloatVal(12345), + MustParseNumberVal("11941607769527758779715454277313298036253933804947715"), + }, + // + { + NumberFloatVal(22337203685475.5), + NumberFloatVal(22337203685475.5), + MustParseNumberVal("498950668486420259929661100.25"), + }, } for _, test := range tests { @@ -1953,6 +1964,11 @@ func TestValueModulo(t *testing.T) { NumberIntVal(10).Mark(2), Zero.WithMarks(NewValueMarks(1, 2)), }, + { + MustParseNumberVal("967323432120515089486873574508975134568969931547"), + NumberIntVal(10), + NumberIntVal(7), + }, } for _, test := range tests { diff --git a/go.mod b/go.mod index e991685..44f5552 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/hashicorp/go-cty require ( - github.com/apparentlymart/go-textseg/v12 v12.0.0 + github.com/apparentlymart/go-textseg/v13 v13.0.0 github.com/google/go-cmp v0.3.1 github.com/vmihailenco/msgpack/v4 v4.3.12 golang.org/x/text v0.3.8 diff --git a/go.sum b/go.sum index ffeb16a..a827a67 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbjdL7GzRt3F8NvfJ0= -github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=