lang/funcs: Remove the deprecated "list" and "map" functions

Prior to Terraform 0.12 these two functions were the only way to construct
literal lists and maps (respectively) in HIL expressions. Terraform 0.12,
by switching to HCL 2, introduced first-class syntax for constructing
tuple and object values, which can then be converted into list and map
values using the tolist and tomap type conversion functions.

We marked both of these functions as deprecated in the Terraform v0.12
release and have since then mentioned in the docs that they will be
removed in a future Terraform version. The "terraform 0.12upgrade" tool
from Terraform v0.12 also included a rule to automatically rewrite uses
of these functions into equivalent new syntax.

The main motivation for removing these now is just to get this change made
prior to Terraform 1.0. as we'll be doing with various other deprecations.
However, a specific reason for these two functions in particular is that
their existence is what caused us to invent the idea of a "type expression"
as a distinct kind of expression in Terraform v0.12, and so removing them
now would allow potentially  unifying type expressions with value
expressions in a future release.

We do not have any current specific plans to make that change, but one
potential motivation for doing so would be to take another attempt at a
generalized "convert" function which takes a type as one of its arguments.
Our previous attempt to implement such a function was foiled by the fact
that Terraform's expression validator doesn't have any way to know to
treat one argument of a particular function as special, and so it was
generating incorrect error messages. We won't necessarily do that, but
having these "list" and "map" functions out of the way leaves the option
open.
This commit is contained in:
Martin Atkins 2020-11-04 13:18:44 -08:00
parent c5b4ccfe8c
commit ae3c0c6a4a
7 changed files with 75 additions and 426 deletions

View File

@ -220,50 +220,6 @@ func flattener(flattenList cty.Value) ([]cty.Value, bool) {
return out, true
}
// ListFunc constructs a function that takes an arbitrary number of arguments
// and returns a list containing those values in the same order.
//
// This function is deprecated in Terraform v0.12
var ListFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
if len(args) == 0 {
return cty.NilType, errors.New("at least one argument is required")
}
argTypes := make([]cty.Type, len(args))
for i, arg := range args {
argTypes[i] = arg.Type()
}
retType, _ := convert.UnifyUnsafe(argTypes)
if retType == cty.NilType {
return cty.NilType, errors.New("all arguments must have the same type")
}
return cty.List(retType), nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
newList := make([]cty.Value, 0, len(args))
for _, arg := range args {
// We already know this will succeed because of the checks in our Type func above
arg, _ = convert.Convert(arg, retType.ElementType())
newList = append(newList, arg)
}
return cty.ListVal(newList), nil
},
})
// LookupFunc constructs a function that performs dynamic lookups of map types.
var LookupFunc = function.New(&function.Spec{
Params: []function.Parameter{
@ -354,81 +310,6 @@ var LookupFunc = function.New(&function.Spec{
},
})
// MapFunc constructs a function that takes an even number of arguments and
// returns a map whose elements are constructed from consecutive pairs of arguments.
//
// This function is deprecated in Terraform v0.12
var MapFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
if len(args) < 2 || len(args)%2 != 0 {
return cty.NilType, fmt.Errorf("map requires an even number of two or more arguments, got %d", len(args))
}
argTypes := make([]cty.Type, len(args)/2)
index := 0
for i := 0; i < len(args); i += 2 {
argTypes[index] = args[i+1].Type()
index++
}
valType, _ := convert.UnifyUnsafe(argTypes)
if valType == cty.NilType {
return cty.NilType, errors.New("all arguments must have the same type")
}
return cty.Map(valType), nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
for _, arg := range args {
if !arg.IsWhollyKnown() {
return cty.UnknownVal(retType), nil
}
}
outputMap := make(map[string]cty.Value)
for i := 0; i < len(args); i += 2 {
keyVal, err := convert.Convert(args[i], cty.String)
if err != nil {
return cty.NilVal, err
}
if keyVal.IsNull() {
return cty.NilVal, fmt.Errorf("argument %d is a null key", i+1)
}
key := keyVal.AsString()
val := args[i+1]
var variable cty.Value
err = gocty.FromCtyValue(val, &variable)
if err != nil {
return cty.NilVal, err
}
// We already know this will succeed because of the checks in our Type func above
variable, _ = convert.Convert(variable, retType.ElementType())
// Check for duplicate keys
if _, ok := outputMap[key]; ok {
return cty.NilVal, fmt.Errorf("argument %d is a duplicate key: %q", i+1, key)
}
outputMap[key] = variable
}
return cty.MapVal(outputMap), nil
},
})
// MatchkeysFunc constructs a function that constructs a new list by taking a
// subset of elements from one list whose indexes match the corresponding
// indexes of values in another list.
@ -614,6 +495,48 @@ var TransposeFunc = function.New(&function.Spec{
},
})
// ListFunc constructs a function that takes an arbitrary number of arguments
// and returns a list containing those values in the same order.
//
// This function is deprecated in Terraform v0.12
var ListFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
return cty.DynamicPseudoType, fmt.Errorf("the \"list\" function was deprecated in Terraform v0.12 and is no longer available; use tolist([ ... ]) syntax to write a literal list")
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return cty.DynamicVal, fmt.Errorf("the \"list\" function was deprecated in Terraform v0.12 and is no longer available; use tolist([ ... ]) syntax to write a literal list")
},
})
// MapFunc constructs a function that takes an even number of arguments and
// returns a map whose elements are constructed from consecutive pairs of arguments.
//
// This function is deprecated in Terraform v0.12
var MapFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
return cty.DynamicPseudoType, fmt.Errorf("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map")
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return cty.DynamicVal, fmt.Errorf("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map")
},
})
// helper function to add an element to a list, if it does not already exist
func appendIfMissing(slice []cty.Value, element cty.Value) ([]cty.Value, error) {
for _, ele := range slice {

View File

@ -5,7 +5,6 @@ import (
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
func TestLength(t *testing.T) {
@ -472,83 +471,6 @@ func TestIndex(t *testing.T) {
}
}
func TestList(t *testing.T) {
tests := []struct {
Values []cty.Value
Want cty.Value
Err bool
}{
{
[]cty.Value{
cty.NilVal,
},
cty.NilVal,
true,
},
{
[]cty.Value{
cty.StringVal("Hello"),
},
cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
}),
false,
},
{
[]cty.Value{
cty.StringVal("Hello"),
cty.StringVal("World"),
},
cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
cty.StringVal("World"),
}),
false,
},
{
[]cty.Value{
cty.StringVal("Hello"),
cty.NumberIntVal(42),
},
cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
cty.StringVal("42"),
}),
false,
},
{
[]cty.Value{
cty.StringVal("Hello"),
cty.UnknownVal(cty.String),
},
cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
cty.UnknownVal(cty.String),
}),
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("list(%#v)", test.Values), func(t *testing.T) {
got, err := List(test.Values...)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestLookup(t *testing.T) {
simpleMap := cty.MapVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
@ -802,169 +724,6 @@ func TestLookup(t *testing.T) {
}
}
func TestMap(t *testing.T) {
tests := []struct {
Values []cty.Value
Want cty.Value
Err bool
}{
{
[]cty.Value{
cty.StringVal("hello"),
cty.StringVal("world"),
},
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
false,
},
{
[]cty.Value{
cty.StringVal("hello"),
cty.UnknownVal(cty.String),
},
cty.UnknownVal(cty.Map(cty.String)),
false,
},
{
[]cty.Value{
cty.StringVal("hello"),
cty.StringVal("world"),
cty.StringVal("what's"),
cty.StringVal("up"),
},
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
"what's": cty.StringVal("up"),
}),
false,
},
{
[]cty.Value{
cty.StringVal("hello"),
cty.NumberIntVal(1),
cty.StringVal("goodbye"),
cty.NumberIntVal(42),
},
cty.MapVal(map[string]cty.Value{
"hello": cty.NumberIntVal(1),
"goodbye": cty.NumberIntVal(42),
}),
false,
},
{ // convert numbers to strings
[]cty.Value{
cty.StringVal("hello"),
cty.NumberIntVal(1),
cty.StringVal("goodbye"),
cty.StringVal("42"),
},
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("1"),
"goodbye": cty.StringVal("42"),
}),
false,
},
{ // convert number keys to strings
[]cty.Value{
cty.NumberIntVal(1),
cty.StringVal("hello"),
cty.NumberIntVal(2),
cty.StringVal("goodbye"),
},
cty.MapVal(map[string]cty.Value{
"1": cty.StringVal("hello"),
"2": cty.StringVal("goodbye"),
}),
false,
},
{ // map of lists is okay
[]cty.Value{
cty.StringVal("hello"),
cty.ListVal([]cty.Value{
cty.StringVal("world"),
}),
cty.StringVal("what's"),
cty.ListVal([]cty.Value{
cty.StringVal("up"),
}),
},
cty.MapVal(map[string]cty.Value{
"hello": cty.ListVal([]cty.Value{cty.StringVal("world")}),
"what's": cty.ListVal([]cty.Value{cty.StringVal("up")}),
}),
false,
},
{ // map of maps is okay
[]cty.Value{
cty.StringVal("hello"),
cty.MapVal(map[string]cty.Value{
"there": cty.StringVal("world"),
}),
cty.StringVal("what's"),
cty.MapVal(map[string]cty.Value{
"really": cty.StringVal("up"),
}),
},
cty.MapVal(map[string]cty.Value{
"hello": cty.MapVal(map[string]cty.Value{
"there": cty.StringVal("world"),
}),
"what's": cty.MapVal(map[string]cty.Value{
"really": cty.StringVal("up"),
}),
}),
false,
},
{ // single argument returns an error
[]cty.Value{
cty.StringVal("hello"),
},
cty.NilVal,
true,
},
{ // duplicate keys returns an error
[]cty.Value{
cty.StringVal("hello"),
cty.StringVal("world"),
cty.StringVal("hello"),
cty.StringVal("universe"),
},
cty.NilVal,
true,
},
{ // null key returns an error
[]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
cty.NumberIntVal(5),
},
cty.NilVal,
true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("map(%#v)", test.Values), func(t *testing.T) {
got, err := Map(test.Values...)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
if _, ok := err.(function.PanicError); ok {
t.Fatalf("unexpected panic: %s", err)
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestMatchkeys(t *testing.T) {
tests := []struct {
Keys cty.Value

View File

@ -224,7 +224,7 @@ func TestFunctions(t *testing.T) {
"coalescelist": {
{
`coalescelist(list("a", "b"), list("c", "d"))`,
`coalescelist(tolist(["a", "b"]), tolist(["c", "d"]))`,
cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
@ -512,12 +512,8 @@ func TestFunctions(t *testing.T) {
},
"list": {
{
`list("hello")`,
cty.ListVal([]cty.Value{
cty.StringVal("hello"),
}),
},
// There are intentionally no test cases for "list" because
// it is a stub that always returns an error.
},
"log": {
@ -542,12 +538,8 @@ func TestFunctions(t *testing.T) {
},
"map": {
{
`map("hello", "world")`,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
},
// There are intentionally no test cases for "map" because
// it is a stub that always returns an error.
},
"matchkeys": {
@ -759,7 +751,7 @@ func TestFunctions(t *testing.T) {
"slice": {
{
// force a list type here for testing
`slice(list("a", "b", "c", "d"), 1, 3)`,
`slice(tolist(["a", "b", "c", "d"]), 1, 3)`,
cty.ListVal([]cty.Value{
cty.StringVal("b"), cty.StringVal("c"),
}),

View File

@ -5,7 +5,7 @@ resource "aws_instance" "one" {
}
locals {
one_id = element(concat(aws_instance.one.*.id, list("")), 0)
one_id = element(concat(aws_instance.one.*.id, [""]), 0)
}
resource "aws_instance" "two" {

View File

@ -13,7 +13,7 @@ resource "aws_instance" "bar" {
for_each = toset([])
}
resource "aws_instance" "bar2" {
for_each = toset(list("z", "y", "x"))
for_each = toset(["z", "y", "x"])
}
# an empty map should generate no resource

View File

@ -12,37 +12,22 @@ description: |-
earlier, see
[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html).
~> **This function is deprecated.** From Terraform v0.12, the Terraform
language has built-in syntax for creating lists using the `[` and `]`
delimiters. Use the built-in syntax instead. The `list` function will be
removed in a future version of Terraform.
The `list` function is no longer available. Prior to Terraform v0.12 it was
the only available syntax for writing a literal list inside an expression,
but Terraform v0.12 introduced a new first-class syntax.
`list` takes an arbitrary number of arguments and returns a list containing
those values in the same order.
## Examples
To update an expression like `list(a, b, c)`, write the following instead:
```
> list("a", "b", "c")
[
"a",
"b",
"c",
]
tolist([a, b, c])
```
Do not use the above form in Terraform v0.12 or above. Instead, use the
built-in list construction syntax, which achieves the same result:
```
> ["a", "b", "c"]
[
"a",
"b",
"c",
]
```
The `[ ... ]` brackets construct a tuple value, and then the `tolist` function
then converts it to a list. For more information on the value types in the
Terraform language, see [Type Constraints](../types.html).
## Related Functions
* [`tolist`](./tolist.html) converts a set value to a list.
* [`concat`](./concat.html) produces a new list by concatenating together the
elements from other lists.
* [`tolist`](./tolist.html) converts a set or tuple value to a list.

View File

@ -12,35 +12,25 @@ description: |-
earlier, see
[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html).
~> **This function is deprecated.** From Terraform v0.12, the Terraform
language has built-in syntax for creating maps using the `{` and `}`
delimiters. Use the built-in syntax instead. The `map` function will be
removed in a future version of Terraform.
The `map` function is no longer available. Prior to Terraform v0.12 it was
the only available syntax for writing a literal map inside an expression,
but Terraform v0.12 introduced a new first-class syntax.
`map` takes an even number of arguments and returns a map whose elements
are constructed from consecutive pairs of arguments.
## Examples
To update an expression like `map("a", "b", "c", "d")`, write the following instead:
```
> map("a", "b", "c", "d")
{
"a" = "b"
"c" = "d"
}
tomap({
a = "b"
c = "d"
})
```
Do not use the above form in Terraform v0.12 or above. Instead, use the
built-in map construction syntax, which achieves the same result:
```
> {"a" = "b", "c" = "d"}
{
"a" = "b"
"c" = "d"
}
```
The `{ ... }` braces construct an object value, and then the `tomap` function
then converts it to a map. For more information on the value types in the
Terraform language, see [Type Constraints](../types.html).
## Related Functions
* [`tomap`](./tomap.html) performs a type conversion to a map type.
* [`tomap`](./tomap.html) converts an object value to a map.
* [`zipmap`](./zipmap.html) constructs a map dynamically, by taking keys from
one list and values from another list.