From 9d864c2430cf2fd49c5038af2b18fc56ef5c8e15 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Thu, 1 Sep 2022 15:59:28 -0400 Subject: [PATCH] Use upstreamed HCL typexpr package The 2.14.0 release of HCL includes the typeexpr changes we tested here, so now we can revert to using the HCL package and remove our fork. --- go.mod | 2 +- go.sum | 4 +- internal/configs/named_values.go | 2 +- internal/typeexpr/defaults.go | 157 ------ internal/typeexpr/defaults_test.go | 504 ------------------- internal/typeexpr/doc.go | 10 - internal/typeexpr/get_type.go | 345 ------------- internal/typeexpr/get_type_test.go | 669 -------------------------- internal/typeexpr/public.go | 143 ------ internal/typeexpr/type_string_test.go | 100 ---- internal/typeexpr/type_type.go | 119 ----- internal/typeexpr/type_type_test.go | 118 ----- 12 files changed, 4 insertions(+), 2169 deletions(-) delete mode 100644 internal/typeexpr/defaults.go delete mode 100644 internal/typeexpr/defaults_test.go delete mode 100644 internal/typeexpr/doc.go delete mode 100644 internal/typeexpr/get_type.go delete mode 100644 internal/typeexpr/get_type_test.go delete mode 100644 internal/typeexpr/public.go delete mode 100644 internal/typeexpr/type_string_test.go delete mode 100644 internal/typeexpr/type_type.go delete mode 100644 internal/typeexpr/type_type_test.go diff --git a/go.mod b/go.mod index e19740ee32..a4256fb732 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f - github.com/hashicorp/hcl/v2 v2.13.0 + github.com/hashicorp/hcl/v2 v2.14.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2 github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 diff --git a/go.sum b/go.sum index 8dfef2a875..05b473fce2 100644 --- a/go.sum +++ b/go.sum @@ -387,8 +387,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws= github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= -github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc= -github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= +github.com/hashicorp/hcl/v2 v2.14.0 h1:jX6+Q38Ly9zaAJlAjnFVyeNSNCKKW8D0wvyg7vij5Wc= +github.com/hashicorp/hcl/v2 v2.14.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d h1:9ARUJJ1VVynB176G1HCwleORqCaXm/Vx0uUi0dL26I0= github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= diff --git a/internal/configs/named_values.go b/internal/configs/named_values.go index bb0698071e..b12b212880 100644 --- a/internal/configs/named_values.go +++ b/internal/configs/named_values.go @@ -4,13 +4,13 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/typeexpr" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/typeexpr" ) // A consistent detail message for all "not a valid identifier" diagnostics. diff --git a/internal/typeexpr/defaults.go b/internal/typeexpr/defaults.go deleted file mode 100644 index 851c72fbfc..0000000000 --- a/internal/typeexpr/defaults.go +++ /dev/null @@ -1,157 +0,0 @@ -package typeexpr - -import ( - "github.com/zclconf/go-cty/cty" -) - -// Defaults represents a type tree which may contain default values for -// optional object attributes at any level. This is used to apply nested -// defaults to an input value before converting it to the concrete type. -type Defaults struct { - // Type of the node for which these defaults apply. This is necessary in - // order to determine how to inspect the Defaults and Children collections. - Type cty.Type - - // DefaultValues contains the default values for each object attribute, - // indexed by attribute name. - DefaultValues map[string]cty.Value - - // Children is a map of Defaults for elements contained in this type. This - // only applies to structural and collection types. - // - // The map is indexed by string instead of cty.Value because cty.Number - // instances are non-comparable, due to embedding a *big.Float. - // - // Collections have a single element type, which is stored at key "". - Children map[string]*Defaults -} - -// Apply walks the given value, applying specified defaults wherever optional -// attributes are missing. The input and output values may have different -// types, and the result may still require type conversion to the final desired -// type. -// -// This function is permissive and does not report errors, assuming that the -// caller will have better context to report useful type conversion failure -// diagnostics. -func (d *Defaults) Apply(val cty.Value) cty.Value { - val, err := cty.TransformWithTransformer(val, &defaultsTransformer{defaults: d}) - - // The transformer should never return an error. - if err != nil { - panic(err) - } - - return val -} - -// defaultsTransformer implements cty.Transformer, as a pre-order traversal, -// applying defaults as it goes. The pre-order traversal allows us to specify -// defaults more loosely for structural types, as the defaults for the types -// will be applied to the default value later in the walk. -type defaultsTransformer struct { - defaults *Defaults -} - -var _ cty.Transformer = (*defaultsTransformer)(nil) - -func (t *defaultsTransformer) Enter(p cty.Path, v cty.Value) (cty.Value, error) { - // Cannot apply defaults to an unknown value - if !v.IsKnown() { - return v, nil - } - - // Look up the defaults for this path. - defaults := t.defaults.traverse(p) - - // If we have no defaults, nothing to do. - if len(defaults) == 0 { - return v, nil - } - - // Ensure we are working with an object or map. - vt := v.Type() - if !vt.IsObjectType() && !vt.IsMapType() { - // Cannot apply defaults because the value type is incompatible. - // We'll ignore this and let the later conversion stage display a - // more useful diagnostic. - return v, nil - } - - // Unmark the value and reapply the marks later. - v, valMarks := v.Unmark() - - // Convert the given value into an attribute map (if it's non-null and - // non-empty). - attrs := make(map[string]cty.Value) - if !v.IsNull() && v.LengthInt() > 0 { - attrs = v.AsValueMap() - } - - // Apply defaults where attributes are missing, constructing a new - // value with the same marks. - for attr, defaultValue := range defaults { - if attrValue, ok := attrs[attr]; !ok || attrValue.IsNull() { - attrs[attr] = defaultValue - } - } - - // We construct an object even if the input value was a map, as the - // type of an attribute's default value may be incompatible with the - // map element type. - return cty.ObjectVal(attrs).WithMarks(valMarks), nil -} - -func (t *defaultsTransformer) Exit(p cty.Path, v cty.Value) (cty.Value, error) { - return v, nil -} - -// traverse walks the abstract defaults structure for a given path, returning -// a set of default values (if any are present) or nil (if not). This operation -// differs from applying a path to a value because we need to customize the -// traversal steps for collection types, where a single set of defaults can be -// applied to an arbitrary number of elements. -func (d *Defaults) traverse(path cty.Path) map[string]cty.Value { - if len(path) == 0 { - return d.DefaultValues - } - - switch s := path[0].(type) { - case cty.GetAttrStep: - if d.Type.IsObjectType() { - // Attribute path steps are normally applied to objects, where each - // attribute may have different defaults. - return d.traverseChild(s.Name, path) - } else if d.Type.IsMapType() { - // Literal values for maps can result in attribute path steps, in which - // case we need to disregard the attribute name, as maps can have only - // one child. - return d.traverseChild("", path) - } - - return nil - case cty.IndexStep: - if d.Type.IsTupleType() { - // Tuples can have different types for each element, so we look - // up the defaults based on the index key. - return d.traverseChild(s.Key.AsBigFloat().String(), path) - } else if d.Type.IsCollectionType() { - // Defaults for collection element types are stored with a blank - // key, so we disregard the index key. - return d.traverseChild("", path) - } - return nil - default: - // At time of writing there are no other path step types. - return nil - } -} - -// traverseChild continues the traversal for a given child key, and mutually -// recurses with traverse. -func (d *Defaults) traverseChild(name string, path cty.Path) map[string]cty.Value { - if child, ok := d.Children[name]; ok { - return child.traverse(path[1:]) - } - return nil -} diff --git a/internal/typeexpr/defaults_test.go b/internal/typeexpr/defaults_test.go deleted file mode 100644 index a4da6bb6b2..0000000000 --- a/internal/typeexpr/defaults_test.go +++ /dev/null @@ -1,504 +0,0 @@ -package typeexpr - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/zclconf/go-cty/cty" -) - -var ( - valueComparer = cmp.Comparer(cty.Value.RawEquals) -) - -func TestDefaults_Apply(t *testing.T) { - simpleObject := cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Bool, - }, []string{"b"}) - nestedObject := cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "c": simpleObject, - "d": cty.Number, - }, []string{"c"}) - - testCases := map[string]struct { - defaults *Defaults - value cty.Value - want cty.Value - }{ - // Nothing happens when there are no default values and no children. - "no defaults": { - defaults: &Defaults{ - Type: cty.Map(cty.String), - }, - value: cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.StringVal("bar"), - }), - want: cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.StringVal("bar"), - }), - }, - // Passing a map which does not include one of the attributes with a - // default results in the default being applied to the output. Output - // is always an object. - "simple object with defaults applied": { - defaults: &Defaults{ - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - value: cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - want: cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - }, - // Unknown values may be assigned to root modules during validation, - // and we cannot apply defaults at that time. - "simple object with defaults but unknown value": { - defaults: &Defaults{ - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - value: cty.UnknownVal(cty.Map(cty.String)), - want: cty.UnknownVal(cty.Map(cty.String)), - }, - // Defaults do not override attributes which are present in the given - // value. - "simple object with optional attributes specified": { - defaults: &Defaults{ - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - value: cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.StringVal("false"), - }), - want: cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.StringVal("false"), - }), - }, - // Defaults will replace explicit nulls. - "object with explicit null for attribute with default": { - defaults: &Defaults{ - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - value: cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.NullVal(cty.String), - }), - want: cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - }, - // Defaults can be specified at any level of depth and will be applied - // so long as there is a parent value to populate. - "nested object with defaults applied": { - defaults: &Defaults{ - Type: nestedObject, - Children: map[string]*Defaults{ - "c": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.False, - }, - }, - }, - }, - value: cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "d": cty.NumberIntVal(5), - }), - want: cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.False, - }), - "d": cty.NumberIntVal(5), - }), - }, - // Testing traversal of collections. - "map of objects with defaults applied": { - defaults: &Defaults{ - Type: cty.Map(simpleObject), - Children: map[string]*Defaults{ - "": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - value: cty.MapVal(map[string]cty.Value{ - "f": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - }), - }), - want: cty.MapVal(map[string]cty.Value{ - "f": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - "b": cty.True, - }), - }), - }, - // A map variable value specified in a tfvars file will be an object, - // in which case we must still traverse the defaults structure - // correctly. - "map of objects with defaults applied, given object instead of map": { - defaults: &Defaults{ - Type: cty.Map(simpleObject), - Children: map[string]*Defaults{ - "": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - value: cty.ObjectVal(map[string]cty.Value{ - "f": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - }), - }), - want: cty.ObjectVal(map[string]cty.Value{ - "f": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - "b": cty.True, - }), - }), - }, - // Another example of a collection type, this time exercising the code - // processing a tuple input. - "list of objects with defaults applied": { - defaults: &Defaults{ - Type: cty.List(simpleObject), - Children: map[string]*Defaults{ - "": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - "b": cty.True, - }), - }), - }, - // Unlike collections, tuple variable types can have defaults for - // multiple element types. - "tuple of objects with defaults applied": { - defaults: &Defaults{ - Type: cty.Tuple([]cty.Type{simpleObject, nestedObject}), - Children: map[string]*Defaults{ - "0": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.False, - }, - }, - "1": { - Type: nestedObject, - DefaultValues: map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("default"), - "b": cty.True, - }), - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - cty.ObjectVal(map[string]cty.Value{ - "d": cty.NumberIntVal(5), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.False, - }), - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("default"), - "b": cty.True, - }), - "d": cty.NumberIntVal(5), - }), - }), - }, - // More complex cases with deeply nested defaults, testing the "default - // within a default" edges. - "set of nested objects, no default sub-object": { - defaults: &Defaults{ - Type: cty.Set(nestedObject), - Children: map[string]*Defaults{ - "": { - Type: nestedObject, - Children: map[string]*Defaults{ - "c": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "d": cty.NumberIntVal(7), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - // No default value for "c" specified, so none applied. The - // convert stage will fill in a null. - "d": cty.NumberIntVal(7), - }), - }), - }, - "set of nested objects, empty default sub-object": { - defaults: &Defaults{ - Type: cty.Set(nestedObject), - Children: map[string]*Defaults{ - "": { - Type: nestedObject, - DefaultValues: map[string]cty.Value{ - // This is a convenient shorthand which causes a - // missing sub-object to be filled with an object - // with all of the default values specified in the - // sub-object's type. - "c": cty.EmptyObjectVal, - }, - Children: map[string]*Defaults{ - "c": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "d": cty.NumberIntVal(7), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - // Default value for "b" is applied to the empty object - // specified as the default for "c" - "b": cty.True, - }), - "d": cty.NumberIntVal(7), - }), - }), - }, - "set of nested objects, overriding default sub-object": { - defaults: &Defaults{ - Type: cty.Set(nestedObject), - Children: map[string]*Defaults{ - "": { - Type: nestedObject, - DefaultValues: map[string]cty.Value{ - // If no value is given for "c", we use this object - // of non-default values instead. These take - // precedence over the default values specified in - // the child type. - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("fallback"), - "b": cty.False, - }), - }, - Children: map[string]*Defaults{ - "c": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "d": cty.NumberIntVal(7), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - // The default value for "b" is not applied, as the - // default value for "c" includes a non-default value - // already. - "a": cty.StringVal("fallback"), - "b": cty.False, - }), - "d": cty.NumberIntVal(7), - }), - }), - }, - "set of nested objects, nulls in default sub-object overridden": { - defaults: &Defaults{ - Type: cty.Set(nestedObject), - Children: map[string]*Defaults{ - "": { - Type: nestedObject, - DefaultValues: map[string]cty.Value{ - // The default value for "c" is used to prepopulate - // the nested object's value if not specified, but - // the null default for its "b" attribute will be - // overridden by the default specified in the child - // type. - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("fallback"), - "b": cty.NullVal(cty.Bool), - }), - }, - Children: map[string]*Defaults{ - "c": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "d": cty.NumberIntVal(7), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - // The default value for "b" overrides the explicit - // null in the default value for "c". - "a": cty.StringVal("fallback"), - "b": cty.True, - }), - "d": cty.NumberIntVal(7), - }), - }), - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - got := tc.defaults.Apply(tc.value) - if !cmp.Equal(tc.want, got, valueComparer) { - t.Errorf("wrong result\n%s", cmp.Diff(tc.want, got, valueComparer)) - } - }) - } -} diff --git a/internal/typeexpr/doc.go b/internal/typeexpr/doc.go deleted file mode 100644 index 9a62984a35..0000000000 --- a/internal/typeexpr/doc.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package typeexpr is a fork of github.com/hashicorp/hcl/v2/ext/typeexpr -// which has additional experimental support for optional attributes. -// -// This is here as part of the module_variable_optional_attrs experiment. -// If that experiment is successful, the changes here may be upstreamed into -// HCL itself or, if we deem it to be Terraform-specific, we should at least -// update this documentation to reflect that this is now the primary -// Terraform-specific type expression implementation, separate from the -// upstream HCL one. -package typeexpr diff --git a/internal/typeexpr/get_type.go b/internal/typeexpr/get_type.go deleted file mode 100644 index 10ed611cb2..0000000000 --- a/internal/typeexpr/get_type.go +++ /dev/null @@ -1,345 +0,0 @@ -package typeexpr - -import ( - "fmt" - - "github.com/hashicorp/hcl/v2" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/convert" -) - -const invalidTypeSummary = "Invalid type specification" - -// getType is the internal implementation of Type, TypeConstraint, and -// TypeConstraintWithDefaults, using the passed flags to distinguish. When -// `constraint` is true, the "any" keyword can be used in place of a concrete -// type. When `withDefaults` is true, the "optional" call expression supports -// an additional argument describing a default value. -func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Defaults, hcl.Diagnostics) { - // First we'll try for one of our keywords - kw := hcl.ExprAsKeyword(expr) - switch kw { - case "bool": - return cty.Bool, nil, nil - case "string": - return cty.String, nil, nil - case "number": - return cty.Number, nil, nil - case "any": - if constraint { - return cty.DynamicPseudoType, nil, nil - } - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw), - Subject: expr.Range().Ptr(), - }} - case "list", "map", "set": - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw), - Subject: expr.Range().Ptr(), - }} - case "object": - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.", - Subject: expr.Range().Ptr(), - }} - case "tuple": - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "The tuple type constructor requires one argument specifying the element types as a list.", - Subject: expr.Range().Ptr(), - }} - case "": - // okay! we'll fall through and try processing as a call, then. - default: - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: fmt.Sprintf("The keyword %q is not a valid type specification.", kw), - Subject: expr.Range().Ptr(), - }} - } - - // If we get down here then our expression isn't just a keyword, so we'll - // try to process it as a call instead. - call, diags := hcl.ExprCall(expr) - if diags.HasErrors() { - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).", - Subject: expr.Range().Ptr(), - }} - } - - switch call.Name { - case "bool", "string", "number": - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name), - Subject: &call.ArgsRange, - }} - case "any": - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: fmt.Sprintf("Type constraint keyword %q does not expect arguments.", call.Name), - Subject: &call.ArgsRange, - }} - } - - if len(call.Arguments) != 1 { - contextRange := call.ArgsRange - subjectRange := call.ArgsRange - if len(call.Arguments) > 1 { - // If we have too many arguments (as opposed to too _few_) then - // we'll highlight the extraneous arguments as the diagnostic - // subject. - subjectRange = hcl.RangeBetween(call.Arguments[1].Range(), call.Arguments[len(call.Arguments)-1].Range()) - } - - switch call.Name { - case "list", "set", "map": - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name), - Subject: &subjectRange, - Context: &contextRange, - }} - case "object": - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.", - Subject: &subjectRange, - Context: &contextRange, - }} - case "tuple": - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "The tuple type constructor requires one argument specifying the element types as a list.", - Subject: &subjectRange, - Context: &contextRange, - }} - } - } - - switch call.Name { - - case "list": - ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults) - ty := cty.List(ety) - return ty, collectionDefaults(ty, defaults), diags - case "set": - ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults) - ty := cty.Set(ety) - return ty, collectionDefaults(ty, defaults), diags - case "map": - ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults) - ty := cty.Map(ety) - return ty, collectionDefaults(ty, defaults), diags - case "object": - attrDefs, diags := hcl.ExprMap(call.Arguments[0]) - if diags.HasErrors() { - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.", - Subject: call.Arguments[0].Range().Ptr(), - Context: expr.Range().Ptr(), - }} - } - - atys := make(map[string]cty.Type) - defaultValues := make(map[string]cty.Value) - children := make(map[string]*Defaults) - var optAttrs []string - for _, attrDef := range attrDefs { - attrName := hcl.ExprAsKeyword(attrDef.Key) - if attrName == "" { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "Object constructor map keys must be attribute names.", - Subject: attrDef.Key.Range().Ptr(), - Context: expr.Range().Ptr(), - }) - continue - } - atyExpr := attrDef.Value - - // the attribute type expression might be wrapped in the special - // modifier optional(...) to indicate an optional attribute. If - // so, we'll unwrap that first and make a note about it being - // optional for when we construct the type below. - var defaultExpr hcl.Expression - if call, callDiags := hcl.ExprCall(atyExpr); !callDiags.HasErrors() { - if call.Name == "optional" { - if len(call.Arguments) < 1 { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "Optional attribute modifier requires the attribute type as its argument.", - Subject: call.ArgsRange.Ptr(), - Context: atyExpr.Range().Ptr(), - }) - continue - } - if constraint { - if withDefaults { - switch len(call.Arguments) { - case 2: - defaultExpr = call.Arguments[1] - defaultVal, defaultDiags := defaultExpr.Value(nil) - diags = append(diags, defaultDiags...) - if !defaultDiags.HasErrors() { - optAttrs = append(optAttrs, attrName) - defaultValues[attrName] = defaultVal - } - case 1: - optAttrs = append(optAttrs, attrName) - default: - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "Optional attribute modifier expects at most two arguments: the attribute type, and a default value.", - Subject: call.ArgsRange.Ptr(), - Context: atyExpr.Range().Ptr(), - }) - } - } else { - if len(call.Arguments) == 1 { - optAttrs = append(optAttrs, attrName) - } else { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "Optional attribute modifier expects only one argument: the attribute type.", - Subject: call.ArgsRange.Ptr(), - Context: atyExpr.Range().Ptr(), - }) - } - } - } else { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "Optional attribute modifier is only for type constraints, not for exact types.", - Subject: call.NameRange.Ptr(), - Context: atyExpr.Range().Ptr(), - }) - } - atyExpr = call.Arguments[0] - } - } - - aty, aDefaults, attrDiags := getType(atyExpr, constraint, withDefaults) - diags = append(diags, attrDiags...) - - // If a default is set for an optional attribute, verify that it is - // convertible to the attribute type. - if defaultVal, ok := defaultValues[attrName]; ok { - _, err := convert.Convert(defaultVal, aty) - if err != nil { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid default value for optional attribute", - Detail: fmt.Sprintf("This default value is not compatible with the attribute's type constraint: %s.", err), - Subject: defaultExpr.Range().Ptr(), - }) - delete(defaultValues, attrName) - } - } - - atys[attrName] = aty - if aDefaults != nil { - children[attrName] = aDefaults - } - } - // NOTE: ObjectWithOptionalAttrs is experimental in cty at the - // time of writing, so this interface might change even in future - // minor versions of cty. We're accepting that because Terraform - // itself is considering optional attributes as experimental right now. - ty := cty.ObjectWithOptionalAttrs(atys, optAttrs) - return ty, structuredDefaults(ty, defaultValues, children), diags - case "tuple": - elemDefs, diags := hcl.ExprList(call.Arguments[0]) - if diags.HasErrors() { - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: "Tuple type constructor requires a list of element types.", - Subject: call.Arguments[0].Range().Ptr(), - Context: expr.Range().Ptr(), - }} - } - etys := make([]cty.Type, len(elemDefs)) - children := make(map[string]*Defaults, len(elemDefs)) - for i, defExpr := range elemDefs { - ety, elemDefaults, elemDiags := getType(defExpr, constraint, withDefaults) - diags = append(diags, elemDiags...) - etys[i] = ety - if elemDefaults != nil { - children[fmt.Sprintf("%d", i)] = elemDefaults - } - } - ty := cty.Tuple(etys) - return ty, structuredDefaults(ty, nil, children), diags - case "optional": - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: fmt.Sprintf("Keyword %q is valid only as a modifier for object type attributes.", call.Name), - Subject: call.NameRange.Ptr(), - }} - default: - // Can't access call.Arguments in this path because we've not validated - // that it contains exactly one expression here. - return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ - Severity: hcl.DiagError, - Summary: invalidTypeSummary, - Detail: fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name), - Subject: expr.Range().Ptr(), - }} - } -} - -func collectionDefaults(ty cty.Type, defaults *Defaults) *Defaults { - if defaults == nil { - return nil - } - return &Defaults{ - Type: ty, - Children: map[string]*Defaults{ - "": defaults, - }, - } -} - -func structuredDefaults(ty cty.Type, defaultValues map[string]cty.Value, children map[string]*Defaults) *Defaults { - if len(defaultValues) == 0 && len(children) == 0 { - return nil - } - - defaults := &Defaults{ - Type: ty, - } - if len(defaultValues) > 0 { - defaults.DefaultValues = defaultValues - } - if len(children) > 0 { - defaults.Children = children - } - - return defaults -} diff --git a/internal/typeexpr/get_type_test.go b/internal/typeexpr/get_type_test.go deleted file mode 100644 index 2dca23d27e..0000000000 --- a/internal/typeexpr/get_type_test.go +++ /dev/null @@ -1,669 +0,0 @@ -package typeexpr - -import ( - "fmt" - "testing" - - "github.com/hashicorp/hcl/v2/gohcl" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/hcl/v2/json" - "github.com/zclconf/go-cty/cty" -) - -var ( - typeComparer = cmp.Comparer(cty.Type.Equals) -) - -func TestGetType(t *testing.T) { - tests := []struct { - Source string - Constraint bool - Want cty.Type - WantError string - }{ - // keywords - { - `bool`, - false, - cty.Bool, - "", - }, - { - `number`, - false, - cty.Number, - "", - }, - { - `string`, - false, - cty.String, - "", - }, - { - `any`, - false, - cty.DynamicPseudoType, - `The keyword "any" cannot be used in this type specification: an exact type is required.`, - }, - { - `any`, - true, - cty.DynamicPseudoType, - "", - }, - { - `list`, - false, - cty.DynamicPseudoType, - "The list type constructor requires one argument specifying the element type.", - }, - { - `map`, - false, - cty.DynamicPseudoType, - "The map type constructor requires one argument specifying the element type.", - }, - { - `set`, - false, - cty.DynamicPseudoType, - "The set type constructor requires one argument specifying the element type.", - }, - { - `object`, - false, - cty.DynamicPseudoType, - "The object type constructor requires one argument specifying the attribute types and values as a map.", - }, - { - `tuple`, - false, - cty.DynamicPseudoType, - "The tuple type constructor requires one argument specifying the element types as a list.", - }, - - // constructors - { - `bool()`, - false, - cty.DynamicPseudoType, - `Primitive type keyword "bool" does not expect arguments.`, - }, - { - `number()`, - false, - cty.DynamicPseudoType, - `Primitive type keyword "number" does not expect arguments.`, - }, - { - `string()`, - false, - cty.DynamicPseudoType, - `Primitive type keyword "string" does not expect arguments.`, - }, - { - `any()`, - false, - cty.DynamicPseudoType, - `Type constraint keyword "any" does not expect arguments.`, - }, - { - `any()`, - true, - cty.DynamicPseudoType, - `Type constraint keyword "any" does not expect arguments.`, - }, - { - `list(string)`, - false, - cty.List(cty.String), - ``, - }, - { - `set(string)`, - false, - cty.Set(cty.String), - ``, - }, - { - `map(string)`, - false, - cty.Map(cty.String), - ``, - }, - { - `list()`, - false, - cty.DynamicPseudoType, - `The list type constructor requires one argument specifying the element type.`, - }, - { - `list(string, string)`, - false, - cty.DynamicPseudoType, - `The list type constructor requires one argument specifying the element type.`, - }, - { - `list(any)`, - false, - cty.List(cty.DynamicPseudoType), - `The keyword "any" cannot be used in this type specification: an exact type is required.`, - }, - { - `list(any)`, - true, - cty.List(cty.DynamicPseudoType), - ``, - }, - { - `object({})`, - false, - cty.EmptyObject, - ``, - }, - { - `object({name=string})`, - false, - cty.Object(map[string]cty.Type{"name": cty.String}), - ``, - }, - { - `object({"name"=string})`, - false, - cty.EmptyObject, - `Object constructor map keys must be attribute names.`, - }, - { - `object({name=nope})`, - false, - cty.Object(map[string]cty.Type{"name": cty.DynamicPseudoType}), - `The keyword "nope" is not a valid type specification.`, - }, - { - `object()`, - false, - cty.DynamicPseudoType, - `The object type constructor requires one argument specifying the attribute types and values as a map.`, - }, - { - `object(string)`, - false, - cty.DynamicPseudoType, - `Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.`, - }, - { - `tuple([])`, - false, - cty.EmptyTuple, - ``, - }, - { - `tuple([string, bool])`, - false, - cty.Tuple([]cty.Type{cty.String, cty.Bool}), - ``, - }, - { - `tuple([nope])`, - false, - cty.Tuple([]cty.Type{cty.DynamicPseudoType}), - `The keyword "nope" is not a valid type specification.`, - }, - { - `tuple()`, - false, - cty.DynamicPseudoType, - `The tuple type constructor requires one argument specifying the element types as a list.`, - }, - { - `tuple(string)`, - false, - cty.DynamicPseudoType, - `Tuple type constructor requires a list of element types.`, - }, - { - `shwoop(string)`, - false, - cty.DynamicPseudoType, - `Keyword "shwoop" is not a valid type constructor.`, - }, - { - `list("string")`, - false, - cty.List(cty.DynamicPseudoType), - `A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).`, - }, - - // More interesting combinations - { - `list(object({}))`, - false, - cty.List(cty.EmptyObject), - ``, - }, - { - `list(map(tuple([])))`, - false, - cty.List(cty.Map(cty.EmptyTuple)), - ``, - }, - - // Optional modifier - { - `object({name=string,age=optional(number)})`, - true, - cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "name": cty.String, - "age": cty.Number, - }, []string{"age"}), - ``, - }, - { - `object({name=string,meta=optional(any)})`, - true, - cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "name": cty.String, - "meta": cty.DynamicPseudoType, - }, []string{"meta"}), - ``, - }, - { - `object({name=string,age=optional(number)})`, - false, - cty.Object(map[string]cty.Type{ - "name": cty.String, - "age": cty.Number, - }), - `Optional attribute modifier is only for type constraints, not for exact types.`, - }, - { - `object({name=string,meta=optional(any)})`, - false, - cty.Object(map[string]cty.Type{ - "name": cty.String, - "meta": cty.DynamicPseudoType, - }), - `Optional attribute modifier is only for type constraints, not for exact types.`, - }, - { - `object({name=string,meta=optional()})`, - true, - cty.Object(map[string]cty.Type{ - "name": cty.String, - }), - `Optional attribute modifier requires the attribute type as its argument.`, - }, - { - `object({name=string,meta=optional(string, "hello")})`, - true, - cty.Object(map[string]cty.Type{ - "name": cty.String, - "meta": cty.String, - }), - `Optional attribute modifier expects only one argument: the attribute type.`, - }, - { - `optional(string)`, - false, - cty.DynamicPseudoType, - `Keyword "optional" is valid only as a modifier for object type attributes.`, - }, - { - `optional`, - false, - cty.DynamicPseudoType, - `The keyword "optional" is not a valid type specification.`, - }, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%s (constraint=%v)", test.Source, test.Constraint), func(t *testing.T) { - expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1}) - if diags.HasErrors() { - t.Fatalf("failed to parse: %s", diags) - } - - got, _, diags := getType(expr, test.Constraint, false) - if test.WantError == "" { - for _, diag := range diags { - t.Error(diag) - } - } else { - found := false - for _, diag := range diags { - t.Log(diag) - if diag.Severity == hcl.DiagError && diag.Detail == test.WantError { - found = true - } - } - if !found { - t.Errorf("missing expected error detail message: %s", test.WantError) - } - } - - if !got.Equals(test.Want) { - t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) - } - }) - } -} - -func TestGetTypeJSON(t *testing.T) { - // We have fewer test cases here because we're mainly exercising the - // extra indirection in the JSON syntax package, which ultimately calls - // into the native syntax parser (which we tested extensively in - // TestGetType). - tests := []struct { - Source string - Constraint bool - Want cty.Type - WantError string - }{ - { - `{"expr":"bool"}`, - false, - cty.Bool, - "", - }, - { - `{"expr":"list(bool)"}`, - false, - cty.List(cty.Bool), - "", - }, - { - `{"expr":"list"}`, - false, - cty.DynamicPseudoType, - "The list type constructor requires one argument specifying the element type.", - }, - } - - for _, test := range tests { - t.Run(test.Source, func(t *testing.T) { - file, diags := json.Parse([]byte(test.Source), "") - if diags.HasErrors() { - t.Fatalf("failed to parse: %s", diags) - } - - type TestContent struct { - Expr hcl.Expression `hcl:"expr"` - } - var content TestContent - diags = gohcl.DecodeBody(file.Body, nil, &content) - if diags.HasErrors() { - t.Fatalf("failed to decode: %s", diags) - } - - got, _, diags := getType(content.Expr, test.Constraint, false) - if test.WantError == "" { - for _, diag := range diags { - t.Error(diag) - } - } else { - found := false - for _, diag := range diags { - t.Log(diag) - if diag.Severity == hcl.DiagError && diag.Detail == test.WantError { - found = true - } - } - if !found { - t.Errorf("missing expected error detail message: %s", test.WantError) - } - } - - if !got.Equals(test.Want) { - t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) - } - }) - } -} - -func TestGetTypeDefaults(t *testing.T) { - tests := []struct { - Source string - Want *Defaults - WantError string - }{ - // primitive types have nil defaults - { - `bool`, - nil, - "", - }, - { - `number`, - nil, - "", - }, - { - `string`, - nil, - "", - }, - { - `any`, - nil, - "", - }, - - // complex structures with no defaults have nil defaults - { - `map(string)`, - nil, - "", - }, - { - `set(number)`, - nil, - "", - }, - { - `tuple([number, string])`, - nil, - "", - }, - { - `object({ a = string, b = number })`, - nil, - "", - }, - { - `map(list(object({ a = string, b = optional(number) })))`, - nil, - "", - }, - - // object optional attribute with defaults - { - `object({ a = string, b = optional(number, 5) })`, - &Defaults{ - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - "", - }, - - // nested defaults - { - `object({ a = optional(object({ b = optional(number, 5) }), {}) })`, - &Defaults{ - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "b": cty.Number, - }, []string{"b"}), - }, []string{"a"}), - DefaultValues: map[string]cty.Value{ - "a": cty.EmptyObjectVal, - }, - Children: map[string]*Defaults{ - "a": { - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - }, - }, - "", - }, - - // collections of objects with defaults - { - `map(object({ a = string, b = optional(number, 5) }))`, - &Defaults{ - Type: cty.Map(cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"})), - Children: map[string]*Defaults{ - "": { - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - }, - }, - "", - }, - { - `list(object({ a = string, b = optional(number, 5) }))`, - &Defaults{ - Type: cty.List(cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"})), - Children: map[string]*Defaults{ - "": { - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - }, - }, - "", - }, - { - `set(object({ a = string, b = optional(number, 5) }))`, - &Defaults{ - Type: cty.Set(cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"})), - Children: map[string]*Defaults{ - "": { - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - }, - }, - "", - }, - - // tuples containing objects with defaults work differently from - // collections - { - `tuple([string, bool, object({ a = string, b = optional(number, 5) })])`, - &Defaults{ - Type: cty.Tuple([]cty.Type{ - cty.String, - cty.Bool, - cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - }), - Children: map[string]*Defaults{ - "2": { - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - }, - }, - "", - }, - - // incompatible default value causes an error - { - `object({ a = optional(string, "hello"), b = optional(number, true) })`, - &Defaults{ - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"a", "b"}), - DefaultValues: map[string]cty.Value{ - "a": cty.StringVal("hello"), - }, - }, - "This default value is not compatible with the attribute's type constraint: number required.", - }, - - // Too many arguments - { - `object({name=string,meta=optional(string, "hello", "world")})`, - nil, - `Optional attribute modifier expects at most two arguments: the attribute type, and a default value.`, - }, - } - - for _, test := range tests { - t.Run(test.Source, func(t *testing.T) { - expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1}) - if diags.HasErrors() { - t.Fatalf("failed to parse: %s", diags) - } - - _, got, diags := getType(expr, true, true) - if test.WantError == "" { - for _, diag := range diags { - t.Error(diag) - } - } else { - found := false - for _, diag := range diags { - t.Log(diag) - if diag.Severity == hcl.DiagError && diag.Detail == test.WantError { - found = true - } - } - if !found { - t.Errorf("missing expected error detail message: %s", test.WantError) - } - } - - if !cmp.Equal(test.Want, got, valueComparer, typeComparer) { - t.Errorf("wrong result\n%s", cmp.Diff(test.Want, got, valueComparer, typeComparer)) - } - }) - } -} diff --git a/internal/typeexpr/public.go b/internal/typeexpr/public.go deleted file mode 100644 index 82f215c097..0000000000 --- a/internal/typeexpr/public.go +++ /dev/null @@ -1,143 +0,0 @@ -package typeexpr - -import ( - "bytes" - "fmt" - "sort" - - "github.com/hashicorp/hcl/v2/hclsyntax" - - "github.com/hashicorp/hcl/v2" - "github.com/zclconf/go-cty/cty" -) - -// Type attempts to process the given expression as a type expression and, if -// successful, returns the resulting type. If unsuccessful, error diagnostics -// are returned. -func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) { - ty, _, diags := getType(expr, false, false) - return ty, diags -} - -// TypeConstraint attempts to parse the given expression as a type constraint -// and, if successful, returns the resulting type. If unsuccessful, error -// diagnostics are returned. -// -// A type constraint has the same structure as a type, but it additionally -// allows the keyword "any" to represent cty.DynamicPseudoType, which is often -// used as a wildcard in type checking and type conversion operations. -func TypeConstraint(expr hcl.Expression) (cty.Type, hcl.Diagnostics) { - ty, _, diags := getType(expr, true, false) - return ty, diags -} - -// TypeConstraintWithDefaults attempts to parse the given expression as a type -// constraint which may include default values for object attributes. If -// successful both the resulting type and corresponding defaults are returned. -// If unsuccessful, error diagnostics are returned. -// -// When using this function, defaults should be applied to the input value -// before type conversion, to ensure that objects with missing attributes have -// default values populated. -func TypeConstraintWithDefaults(expr hcl.Expression) (cty.Type, *Defaults, hcl.Diagnostics) { - return getType(expr, true, true) -} - -// TypeString returns a string rendering of the given type as it would be -// expected to appear in the HCL native syntax. -// -// This is primarily intended for showing types to the user in an application -// that uses typexpr, where the user can be assumed to be familiar with the -// type expression syntax. In applications that do not use typeexpr these -// results may be confusing to the user and so type.FriendlyName may be -// preferable, even though it's less precise. -// -// TypeString produces reasonable results only for types like what would be -// produced by the Type and TypeConstraint functions. In particular, it cannot -// support capsule types. -func TypeString(ty cty.Type) string { - // Easy cases first - switch ty { - case cty.String: - return "string" - case cty.Bool: - return "bool" - case cty.Number: - return "number" - case cty.DynamicPseudoType: - return "any" - } - - if ty.IsCapsuleType() { - panic("TypeString does not support capsule types") - } - - if ty.IsCollectionType() { - ety := ty.ElementType() - etyString := TypeString(ety) - switch { - case ty.IsListType(): - return fmt.Sprintf("list(%s)", etyString) - case ty.IsSetType(): - return fmt.Sprintf("set(%s)", etyString) - case ty.IsMapType(): - return fmt.Sprintf("map(%s)", etyString) - default: - // Should never happen because the above is exhaustive - panic("unsupported collection type") - } - } - - if ty.IsObjectType() { - var buf bytes.Buffer - buf.WriteString("object({") - atys := ty.AttributeTypes() - names := make([]string, 0, len(atys)) - for name := range atys { - names = append(names, name) - } - sort.Strings(names) - first := true - for _, name := range names { - aty := atys[name] - if !first { - buf.WriteByte(',') - } - if !hclsyntax.ValidIdentifier(name) { - // Should never happen for any type produced by this package, - // but we'll do something reasonable here just so we don't - // produce garbage if someone gives us a hand-assembled object - // type that has weird attribute names. - // Using Go-style quoting here isn't perfect, since it doesn't - // exactly match HCL syntax, but it's fine for an edge-case. - buf.WriteString(fmt.Sprintf("%q", name)) - } else { - buf.WriteString(name) - } - buf.WriteByte('=') - buf.WriteString(TypeString(aty)) - first = false - } - buf.WriteString("})") - return buf.String() - } - - if ty.IsTupleType() { - var buf bytes.Buffer - buf.WriteString("tuple([") - etys := ty.TupleElementTypes() - first := true - for _, ety := range etys { - if !first { - buf.WriteByte(',') - } - buf.WriteString(TypeString(ety)) - first = false - } - buf.WriteString("])") - return buf.String() - } - - // Should never happen because we covered all cases above. - panic(fmt.Errorf("unsupported type %#v", ty)) -} diff --git a/internal/typeexpr/type_string_test.go b/internal/typeexpr/type_string_test.go deleted file mode 100644 index fbdf3f481d..0000000000 --- a/internal/typeexpr/type_string_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package typeexpr - -import ( - "testing" - - "github.com/zclconf/go-cty/cty" -) - -func TestTypeString(t *testing.T) { - tests := []struct { - Type cty.Type - Want string - }{ - { - cty.DynamicPseudoType, - "any", - }, - { - cty.String, - "string", - }, - { - cty.Number, - "number", - }, - { - cty.Bool, - "bool", - }, - { - cty.List(cty.Number), - "list(number)", - }, - { - cty.Set(cty.Bool), - "set(bool)", - }, - { - cty.Map(cty.String), - "map(string)", - }, - { - cty.EmptyObject, - "object({})", - }, - { - cty.Object(map[string]cty.Type{"foo": cty.Bool}), - "object({foo=bool})", - }, - { - cty.Object(map[string]cty.Type{"foo": cty.Bool, "bar": cty.String}), - "object({bar=string,foo=bool})", - }, - { - cty.EmptyTuple, - "tuple([])", - }, - { - cty.Tuple([]cty.Type{cty.Bool}), - "tuple([bool])", - }, - { - cty.Tuple([]cty.Type{cty.Bool, cty.String}), - "tuple([bool,string])", - }, - { - cty.List(cty.DynamicPseudoType), - "list(any)", - }, - { - cty.Tuple([]cty.Type{cty.DynamicPseudoType}), - "tuple([any])", - }, - { - cty.Object(map[string]cty.Type{"foo": cty.DynamicPseudoType}), - "object({foo=any})", - }, - { - // We don't expect to find attributes that aren't valid identifiers - // because we only promise to support types that this package - // would've created, but we allow this situation during rendering - // just because it's convenient for applications trying to produce - // error messages about mismatched types. Note that the quoted - // attribute name is not actually accepted by our Type and - // TypeConstraint functions, so this is one situation where the - // TypeString result cannot be re-parsed by those functions. - cty.Object(map[string]cty.Type{"foo bar baz": cty.String}), - `object({"foo bar baz"=string})`, - }, - } - - for _, test := range tests { - t.Run(test.Type.GoString(), func(t *testing.T) { - got := TypeString(test.Type) - if got != test.Want { - t.Errorf("wrong result\ntype: %#v\ngot: %s\nwant: %s", test.Type, got, test.Want) - } - }) - } -} diff --git a/internal/typeexpr/type_type.go b/internal/typeexpr/type_type.go deleted file mode 100644 index e72bf6beff..0000000000 --- a/internal/typeexpr/type_type.go +++ /dev/null @@ -1,119 +0,0 @@ -package typeexpr - -import ( - "fmt" - "reflect" - - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/ext/customdecode" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/convert" - "github.com/zclconf/go-cty/cty/function" -) - -// TypeConstraintType is a cty capsule type that allows cty type constraints to -// be used as values. -// -// If TypeConstraintType is used in a context supporting the -// customdecode.CustomExpressionDecoder extension then it will implement -// expression decoding using the TypeConstraint function, thus allowing -// type expressions to be used in contexts where value expressions might -// normally be expected, such as in arguments to function calls. -var TypeConstraintType cty.Type - -// TypeConstraintVal constructs a cty.Value whose type is -// TypeConstraintType. -func TypeConstraintVal(ty cty.Type) cty.Value { - return cty.CapsuleVal(TypeConstraintType, &ty) -} - -// TypeConstraintFromVal extracts the type from a cty.Value of -// TypeConstraintType that was previously constructed using TypeConstraintVal. -// -// If the given value isn't a known, non-null value of TypeConstraintType -// then this function will panic. -func TypeConstraintFromVal(v cty.Value) cty.Type { - if !v.Type().Equals(TypeConstraintType) { - panic("value is not of TypeConstraintType") - } - ptr := v.EncapsulatedValue().(*cty.Type) - return *ptr -} - -// ConvertFunc is a cty function that implements type conversions. -// -// Its signature is as follows: -// -// convert(value, type_constraint) -// -// ...where type_constraint is a type constraint expression as defined by -// typeexpr.TypeConstraint. -// -// It relies on HCL's customdecode extension and so it's not suitable for use -// in non-HCL contexts or if you are using a HCL syntax implementation that -// does not support customdecode for function arguments. However, it _is_ -// supported for function calls in the HCL native expression syntax. -var ConvertFunc function.Function - -func init() { - TypeConstraintType = cty.CapsuleWithOps("type constraint", reflect.TypeOf(cty.Type{}), &cty.CapsuleOps{ - ExtensionData: func(key interface{}) interface{} { - switch key { - case customdecode.CustomExpressionDecoder: - return customdecode.CustomExpressionDecoderFunc( - func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { - ty, diags := TypeConstraint(expr) - if diags.HasErrors() { - return cty.NilVal, diags - } - return TypeConstraintVal(ty), nil - }, - ) - default: - return nil - } - }, - TypeGoString: func(_ reflect.Type) string { - return "typeexpr.TypeConstraintType" - }, - GoString: func(raw interface{}) string { - tyPtr := raw.(*cty.Type) - return fmt.Sprintf("typeexpr.TypeConstraintVal(%#v)", *tyPtr) - }, - RawEquals: func(a, b interface{}) bool { - aPtr := a.(*cty.Type) - bPtr := b.(*cty.Type) - return (*aPtr).Equals(*bPtr) - }, - }) - - ConvertFunc = function.New(&function.Spec{ - Params: []function.Parameter{ - { - Name: "value", - Type: cty.DynamicPseudoType, - AllowNull: true, - AllowDynamicType: true, - }, - { - Name: "type", - Type: TypeConstraintType, - }, - }, - Type: func(args []cty.Value) (cty.Type, error) { - wantTypePtr := args[1].EncapsulatedValue().(*cty.Type) - got, err := convert.Convert(args[0], *wantTypePtr) - if err != nil { - return cty.NilType, function.NewArgError(0, err) - } - return got.Type(), nil - }, - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - v, err := convert.Convert(args[0], retType) - if err != nil { - return cty.NilVal, function.NewArgError(0, err) - } - return v, nil - }, - }) -} diff --git a/internal/typeexpr/type_type_test.go b/internal/typeexpr/type_type_test.go deleted file mode 100644 index 2286a2e1a5..0000000000 --- a/internal/typeexpr/type_type_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package typeexpr - -import ( - "fmt" - "testing" - - "github.com/zclconf/go-cty/cty" -) - -func TestTypeConstraintType(t *testing.T) { - tyVal1 := TypeConstraintVal(cty.String) - tyVal2 := TypeConstraintVal(cty.String) - tyVal3 := TypeConstraintVal(cty.Number) - - if !tyVal1.RawEquals(tyVal2) { - t.Errorf("tyVal1 not equal to tyVal2\ntyVal1: %#v\ntyVal2: %#v", tyVal1, tyVal2) - } - if tyVal1.RawEquals(tyVal3) { - t.Errorf("tyVal1 equal to tyVal2, but should not be\ntyVal1: %#v\ntyVal3: %#v", tyVal1, tyVal3) - } - - if got, want := TypeConstraintFromVal(tyVal1), cty.String; !got.Equals(want) { - t.Errorf("wrong type extracted from tyVal1\ngot: %#v\nwant: %#v", got, want) - } - if got, want := TypeConstraintFromVal(tyVal3), cty.Number; !got.Equals(want) { - t.Errorf("wrong type extracted from tyVal3\ngot: %#v\nwant: %#v", got, want) - } -} - -func TestConvertFunc(t *testing.T) { - // This is testing the convert function directly, skipping over the HCL - // parsing and evaluation steps that would normally lead there. There is - // another test in the "integrationtest" package called TestTypeConvertFunc - // that exercises the full path to this function via the hclsyntax parser. - - tests := []struct { - val, ty cty.Value - want cty.Value - wantErr string - }{ - // The goal here is not an exhaustive set of conversions, since that's - // already covered in cty/convert, but rather exercising different - // permutations of success and failure to make sure the function - // handles all of the results in a reasonable way. - { - cty.StringVal("hello"), - TypeConstraintVal(cty.String), - cty.StringVal("hello"), - ``, - }, - { - cty.True, - TypeConstraintVal(cty.String), - cty.StringVal("true"), - ``, - }, - { - cty.StringVal("hello"), - TypeConstraintVal(cty.Bool), - cty.NilVal, - `a bool is required`, - }, - { - cty.UnknownVal(cty.Bool), - TypeConstraintVal(cty.Bool), - cty.UnknownVal(cty.Bool), - ``, - }, - { - cty.DynamicVal, - TypeConstraintVal(cty.Bool), - cty.UnknownVal(cty.Bool), - ``, - }, - { - cty.NullVal(cty.Bool), - TypeConstraintVal(cty.Bool), - cty.NullVal(cty.Bool), - ``, - }, - { - cty.NullVal(cty.DynamicPseudoType), - TypeConstraintVal(cty.Bool), - cty.NullVal(cty.Bool), - ``, - }, - { - cty.StringVal("hello").Mark(1), - TypeConstraintVal(cty.String), - cty.StringVal("hello").Mark(1), - ``, - }, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%#v to %#v", test.val, test.ty), func(t *testing.T) { - got, err := ConvertFunc.Call([]cty.Value{test.val, test.ty}) - - if err != nil { - if test.wantErr != "" { - if got, want := err.Error(), test.wantErr; got != want { - t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) - } - } else { - t.Errorf("unexpected error\ngot: %s\nwant: ", err) - } - return - } - if test.wantErr != "" { - t.Errorf("wrong error\ngot: \nwant: %s", test.wantErr) - } - - if !test.want.RawEquals(got) { - t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) - } - }) - } -}