fix: Fix error messages when a comprehension iterates over a non-array.

Problems:
 - When iterating over an empty string in a list comprehension, the
   result is an empty string. This is a bug, it should be an error.
 - When iterating over a non-empty string in a list comprehension, the
   expected and unexpected types in the error message are swapped.
 - Error messages mention "std.flatMap" when object/list comprehensions
   would iterate over a value that is neither array nor string.

```
$ jsonnet --version
Jsonnet commandline interpreter (Go implementation) v0.21.0-rc2
$ jsonnet -e '[a for a in ""]'
""
$ jsonnet -e '[a for a in "b"]'
RUNTIME ERROR: Unexpected type array, expected string
	<cmdline>:1:1-17
	During evaluation
$ jsonnet -e '{[a]: 1 for a in 2}'
RUNTIME ERROR: std.flatMap second param must be array / string, got number
	<cmdline>:1:1-20
	<cmdline>:1:1-20
	During evaluation
$ jsonnet -e '[a for a in 1]'
RUNTIME ERROR: std.flatMap second param must be array / string, got number
	<cmdline>:1:1-15
	During evaluation
```

FWIW, the C++ implementation does not have any of these problems. It
gives:
```
RUNTIME ERROR: In comprehension, can only iterate over array.
```

In the Go implementation comprehensions are desugared to a call to
std.flatMap which does accept a string in the "arr" parameter.

The fix: Desugar comprehensions to a call to a new hidden builtin which
only accepts arrays.
This commit is contained in:
Rudo Thomas 2025-03-14 12:00:52 +01:00 committed by John Bartholomew
parent 1add1e1b24
commit 1c79f577ba
16 changed files with 71 additions and 2 deletions

View File

@ -345,6 +345,19 @@ func builtinFlatMap(i *interpreter, funcv, arrv value) (value, error) {
}
}
// builtinFlatMapArray is like builtinFlatMap, but only accepts array as the
// arrv value. Desugared comprehensions contain a call to this function, rather
// than builtinFlatMap, so that a better error message is printed when the
// comprehension would iterate over a non-array.
func builtinFlatMapArray(i *interpreter, funcv, arrv value) (value, error) {
switch arrv := arrv.(type) {
case *valueArray:
return builtinFlatMap(i, funcv, arrv)
default:
return nil, i.typeErrorSpecific(arrv, &valueArray{})
}
}
func joinArrays(i *interpreter, sep *valueArray, arr *valueArray) (value, error) {
result := make([]*cachedThunk, 0, arr.length())
first := true
@ -2880,4 +2893,5 @@ var funcBuiltins = buildBuiltinMap([]builtin{
// internal
&unaryBuiltin{name: "$objectFlatMerge", function: builtinUglyObjectFlatMerge, params: ast.Identifiers{"x"}},
&binaryBuiltin{name: "$flatMapArray", function: builtinFlatMapArray, params: ast.Identifiers{"func", "arr"}},
})

View File

@ -184,7 +184,7 @@ func desugarForSpec(inside ast.Node, loc ast.LocationRange, forSpec *ast.ForSpec
if err != nil {
return nil, err
}
current := buildStdCall("flatMap", loc, function, forSpec.Expr)
current := buildStdCall("$flatMapArray", loc, function, forSpec.Expr)
if forSpec.Outer == nil {
return current, nil
}

View File

@ -199,6 +199,7 @@ func prepareStdlib(g *typeGraph) {
"mod": g.newSimpleFuncType(stringOrNumber, "a", "b"),
"native": g.newSimpleFuncType(anyFunctionType, "x"),
"$objectFlatMerge": g.newSimpleFuncType(anyObjectType, "x"),
"$flatMapArray": g.newSimpleFuncType(anyArrayType, "func", "arr"),
// Boolean

View File

@ -0,0 +1,10 @@
RUNTIME ERROR: Unexpected type string, expected array
-------------------------------------------------
testdata/array_comp_try_iterate_over_empty_string:1:1-16
[a for a in '']
-------------------------------------------------
During evaluation

View File

@ -0,0 +1 @@
[a for a in '']

View File

@ -1,4 +1,4 @@
RUNTIME ERROR: std.flatMap second param must be array / string, got object
RUNTIME ERROR: Unexpected type object, expected array
-------------------------------------------------
testdata/array_comp_try_iterate_over_obj:1:1-16

View File

@ -0,0 +1,10 @@
RUNTIME ERROR: Unexpected type string, expected array
-------------------------------------------------
testdata/array_comp_try_iterate_over_string:1:1-17
[a for a in 'b']
-------------------------------------------------
During evaluation

View File

@ -0,0 +1 @@
[a for a in 'b']

View File

@ -0,0 +1,15 @@
RUNTIME ERROR: Unexpected type object, expected array
-------------------------------------------------
testdata/object_comp_try_iterate_over_obj:1:1-23
{ [a]: 0 for a in {} }
-------------------------------------------------
testdata/object_comp_try_iterate_over_obj:1:1-23
{ [a]: 0 for a in {} }
-------------------------------------------------
During evaluation

View File

@ -0,0 +1 @@
{ [a]: 0 for a in {} }

View File

@ -0,0 +1,15 @@
RUNTIME ERROR: Unexpected type string, expected array
-------------------------------------------------
testdata/object_comp_try_iterate_over_string:1:1-24
{ [a]: 0 for a in 'b' }
-------------------------------------------------
testdata/object_comp_try_iterate_over_string:1:1-24
{ [a]: 0 for a in 'b' }
-------------------------------------------------
During evaluation

View File

@ -0,0 +1 @@
{ [a]: 0 for a in 'b' }