diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f21c46..0dfd367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 1.14.5 (Unreleased) +* `function/stdlib`: The `element` function now accepts negative indices, extending the illusion of an infinitely-long list into the negative direction too. # 1.14.4 (March 20, 2024) diff --git a/cty/function/stdlib/collection.go b/cty/function/stdlib/collection.go index 25df19b..d1df73d 100644 --- a/cty/function/stdlib/collection.go +++ b/cty/function/stdlib/collection.go @@ -147,12 +147,6 @@ var ElementFunc = function.New(&function.Spec{ }, Type: func(args []cty.Value) (cty.Type, error) { list := args[0] - index := args[1] - if index.IsKnown() { - if index.LessThan(cty.NumberIntVal(0)).True() { - return cty.DynamicPseudoType, fmt.Errorf("cannot use element function with a negative index") - } - } listTy := list.Type() switch { @@ -189,10 +183,6 @@ var ElementFunc = function.New(&function.Spec{ return cty.DynamicVal, fmt.Errorf("invalid index: %s", err) } - if args[1].LessThan(cty.NumberIntVal(0)).True() { - return cty.DynamicVal, fmt.Errorf("cannot use element function with a negative index") - } - input, marks := args[0].Unmark() if !input.IsKnown() { return cty.UnknownVal(retType), nil @@ -203,6 +193,9 @@ var ElementFunc = function.New(&function.Spec{ return cty.DynamicVal, errors.New("cannot use element function with an empty list") } index = index % l + if index < 0 { + index += l + } // We did all the necessary type checks in the type function above, // so this is guaranteed not to fail. diff --git a/cty/function/stdlib/collection_test.go b/cty/function/stdlib/collection_test.go index b6b9b6b..f4d0675 100644 --- a/cty/function/stdlib/collection_test.go +++ b/cty/function/stdlib/collection_test.go @@ -1157,6 +1157,30 @@ func TestElement(t *testing.T) { cty.StringVal("quick"), false, }, + { // negative index counts from the end of the list + listOfStrings, + cty.NumberIntVal(-1), + cty.StringVal("fox"), + false, + }, + { // negative index can be out of bounds too + listOfStrings, + cty.NumberIntVal(-6), + cty.StringVal("brown"), + false, + }, + { // minimum valid index + listOfStrings, + cty.NumberIntVal(-9223372036854775808), + cty.StringVal("the"), + false, + }, + { // maximum valid index + listOfStrings, + cty.NumberIntVal(9223372036854775807), + cty.StringVal("fox"), + false, + }, { // list of lists cty.ListVal([]cty.Value{listOfStrings, listOfStrings}), cty.NumberIntVal(0), @@ -1207,16 +1231,28 @@ func TestElement(t *testing.T) { }, { listOfStrings, - cty.NumberIntVal(-1), + cty.StringVal("brown"), // definitely not an index cty.DynamicVal, - true, // index cannot be a negative number + true, }, { listOfStrings, - cty.StringVal("brown"), // definitely not an index + cty.NumberFloatVal(0.5), cty.DynamicVal, true, }, + { // index out of bounds of int64 + listOfStrings, + cty.MustParseNumberVal("-9223372036854775809"), + cty.StringVal("the"), + true, + }, + { // index out of bounds of int64 + listOfStrings, + cty.MustParseNumberVal("9223372036854775808"), + cty.StringVal("fox"), + true, + }, } for _, test := range tests {