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) - } - }) - } -}