Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 1.17] fix: List operation returns null if the value is not a list #907

Merged
merged 3 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -172,48 +172,59 @@ class FeelInterpreter {

// list
case SomeItem(iterators, condition) =>
withCartesianProduct(
iterators,
p =>
atLeastOneValue(p.map(vars => () => eval(condition)(context.addAll(vars))), ValBoolean)
withValOrNull(
withCartesianProduct(
iterators,
p =>
atLeastOneValue(
p.map(vars => () => eval(condition)(context.addAll(vars))),
ValBoolean
)
)
)
case EveryItem(iterators, condition) =>
withCartesianProduct(
iterators,
p => allValues(p.map(vars => () => eval(condition)(context.addAll(vars))), ValBoolean)
withValOrNull(
withCartesianProduct(
iterators,
p => allValues(p.map(vars => () => eval(condition)(context.addAll(vars))), ValBoolean)
)
)
case For(iterators, exp) =>
withCartesianProduct(
iterators,
p =>
ValList((List[Val]() /: p) {
case (partial, vars) => {
val iterationContext = context.addAll(vars).add("partial" -> ValList(partial))
val value = eval(exp)(iterationContext)
partial ++ (value :: Nil)
}
})
withValOrNull(
withCartesianProduct(
iterators,
p =>
ValList((List[Val]() /: p) {
case (partial, vars) => {
val iterationContext = context.addAll(vars).add("partial" -> ValList(partial))
val value = eval(exp)(iterationContext)
partial ++ (value :: Nil)
}
})
)
)
case Filter(list, filter) =>
withList(
eval(list),
l => {
val evalFilterWithItem =
(item: Val) => eval(filter)(filterContext(item))

filter match {
case ConstNumber(index) => filterList(l.items, index)
case ArithmeticNegation(ConstNumber(index)) =>
filterList(l.items, -index)
case _: Comparison | _: FunctionInvocation | _: QualifiedFunctionInvocation =>
filterList(l.items, evalFilterWithItem)
case _ =>
eval(filter) match {
case ValNumber(index) => filterList(l.items, index)
case _ => filterList(l.items, evalFilterWithItem)
}
withValOrNull(
withList(
eval(list),
l => {
val evalFilterWithItem =
(item: Val) => eval(filter)(filterContext(item))

filter match {
case ConstNumber(index) => filterList(l.items, index)
case ArithmeticNegation(ConstNumber(index)) =>
filterList(l.items, -index)
case _: Comparison | _: FunctionInvocation | _: QualifiedFunctionInvocation =>
filterList(l.items, evalFilterWithItem)
case _ =>
eval(filter) match {
case ValNumber(index) => filterList(l.items, index)
case _ => filterList(l.items, evalFilterWithItem)
}
}
}
}
)
)
case IterationContext(start, end) =>
withNumbers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.camunda.feel.impl.interpreter

import org.camunda.feel.api.EvaluationFailureType
import org.camunda.feel.api.EvaluationFailureType.INVALID_TYPE
import org.camunda.feel.context.{CustomContext, VariableProvider}
import org.camunda.feel.impl.{EvaluationResultMatchers, FeelEngineTest}
import org.camunda.feel.syntaxtree._
Expand All @@ -34,59 +35,155 @@ class InterpreterListExpressionTest
with FeelEngineTest
with EvaluationResultMatchers {

"A list" should "be checked with 'some'" in {
"A list" should "contain null if a variable doesn't exist" in {
evaluateExpression("[1, x]") should returnResult(List(1, null))
}

it should "be compared with '='" in {
evaluateExpression("[] = []") should returnResult(true)
evaluateExpression("[1] = [1]") should returnResult(true)
evaluateExpression("[[1]] = [[1]]") should returnResult(true)
evaluateExpression("[{x:1}] = [{x:1}]") should returnResult(true)

evaluateExpression("[] = [1]") should returnResult(false)
evaluateExpression("[1] = []") should returnResult(false)
evaluateExpression("[1] = [2]") should returnResult(false)
evaluateExpression("[[1]] = [[2]]") should returnResult(false)
evaluateExpression("[{x:1}] = [{x:2}]") should returnResult(false)

evaluateExpression("[1] = [true]") should returnResult(false)
}

it should "be compared with '!='" in {
evaluateExpression("[] != []") should returnResult(false)
evaluateExpression("[1] != [1]") should returnResult(false)
evaluateExpression("[[1]] != [[1]]") should returnResult(false)
evaluateExpression("[{x:1}] != [{x:1}]") should returnResult(false)

evaluateExpression("[] != [1]") should returnResult(true)
evaluateExpression("[1] != []") should returnResult(true)
evaluateExpression("[1] != [2]") should returnResult(true)
evaluateExpression("[[1]] != [[2]]") should returnResult(true)
evaluateExpression("[{x:1}] != [{x:2}]") should returnResult(true)

evaluateExpression("[1] != [true]") should returnResult(true)
}

it should "be accessed and compared" in {
evaluateExpression("[1][1] = 1") should returnResult(true)
}

it should "return null if compare to not a list" in {
evaluateExpression("[] = 1") should (returnNull() and reportFailure(
failureType = EvaluationFailureType.NOT_COMPARABLE,
failureMessage = "Can't compare '[]' with '1'"
))
}

"A some-expression" should "return true if one item satisfies the condition" in {

evaluateExpression("some x in [1,2,3] satisfies x > 2") should returnResult(true)
evaluateExpression("some x in [1,2,3] satisfies x > 3") should returnResult(false)

evaluateExpression(
expression = "some x in xs satisfies x > 2",
expression = "some x in xs satisfies x > 1",
variables = Map("xs" -> List(1, 2, 3))
) should returnResult(true)

evaluateExpression(
expression = "some x in xs satisfies x > 2",
variables = Map("xs" -> List(1, 2))
) should returnResult(false)
expression = "some x in xs satisfies count(xs) > 2",
variables = Map("xs" -> List(1, 2, 3))
) should returnResult(true)

evaluateExpression("some x in [1,2], y in [2,3] satisfies x < y") should returnResult(true)
}

it should "return false if no item satisfies the condition" in {

evaluateExpression("some x in [1,2,3] satisfies x > 3") should returnResult(false)

evaluateExpression(
expression = "some x in xs satisfies count(xs) > 2",
expression = "some x in xs satisfies x > 2",
variables = Map("xs" -> List(1, 2))
) should returnResult(false)

evaluateExpression("some x in [1,2], y in [2,3] satisfies x < y") should returnResult(true)
evaluateExpression("some x in [1,2], y in [1,1] satisfies x < y") should returnResult(false)
}

it should "be checked with 'some' (range)" in {
it should "return true if the range satisfies the condition" in {
evaluateExpression("some x in 1..5 satisfies x > 3") should returnResult(true)
}

it should "be checked with 'every'" in {
it should "return false if the range doesn't satisfy the condition" in {
evaluateExpression("some x in 1..5 satisfies x > 10") should returnResult(false)
}

it should "return null if the value is not a list" in {
evaluateExpression(
expression = "some item in x satisfies x > 10"
) should (returnNull() and reportFailure(
INVALID_TYPE,
"Expected list but found 'null'"
))

evaluateExpression(
expression = "some item in x satisfies x > 10",
variables = Map("x" -> 2)
) should (returnNull() and reportFailure(
INVALID_TYPE,
"Expected list but found '2'"
))
}

"An every-expression" should "return true if all items satisfy the condition" in {

evaluateExpression("every x in [1,2,3] satisfies x >= 1") should returnResult(true)
evaluateExpression("every x in [1,2,3] satisfies x >= 2") should returnResult(false)

evaluateExpression(
expression = "every x in xs satisfies x >= 1",
variables = Map("xs" -> List(1, 2, 3))
) should returnResult(true)

evaluateExpression("every x in [1,2], y in [3,4] satisfies x < y") should returnResult(true)
}

it should "return false if one item doesn't satisfy the condition" in {

evaluateExpression("every x in [1,2,3] satisfies x >= 2") should returnResult(false)

evaluateExpression(
expression = "every x in xs satisfies x >= 1",
variables = Map("xs" -> List(0, 1, 2, 3))
) should returnResult(false)

evaluateExpression("every x in [1,2], y in [3,4] satisfies x < y") should returnResult(true)
evaluateExpression("every x in [1,2], y in [2,3] satisfies x < y") should returnResult(false)
}

it should "be checked with 'every' (range)" in {
it should "return true if the range satisfies the condition" in {
evaluateExpression("every x in 1..5 satisfies x < 10") should returnResult(true)
}

it should "be processed in a for-expression" in {
it should "return false if the range doesn't satisfy the condition" in {
evaluateExpression("every x in 1..10 satisfies x < 5") should returnResult(false)
}

it should "return null if the value is not a list" in {
evaluateExpression(
expression = "every item in x satisfies x > 10"
) should (returnNull() and reportFailure(
INVALID_TYPE,
"Expected list but found 'null'"
))

evaluateExpression(
expression = "every item in x satisfies x > 10",
variables = Map("x" -> 2)
) should (returnNull() and reportFailure(
INVALID_TYPE,
"Expected list but found '2'"
))
}

"A for-expression" should "iterate over a list" in {
evaluateExpression("for x in [1,2] return x * 2") should returnResult(
List(2, 4)
)
Expand All @@ -106,58 +203,7 @@ class InterpreterListExpressionTest
) should returnResult(List(List.empty, List(1)))
}

it should "be processed in a for-expression (range)" in {
evaluateExpression("for x in 1..3 return x") should returnResult(
List(1, 2, 3)
)
}

it should "contain null if a variable doesn't exist" in {
evaluateExpression("[1, x]") should returnResult(List(1, null))
}

it should "be compared with '='" in {
evaluateExpression("[] = []") should returnResult(true)
evaluateExpression("[1] = [1]") should returnResult(true)
evaluateExpression("[[1]] = [[1]]") should returnResult(true)
evaluateExpression("[{x:1}] = [{x:1}]") should returnResult(true)

evaluateExpression("[] = [1]") should returnResult(false)
evaluateExpression("[1] = []") should returnResult(false)
evaluateExpression("[1] = [2]") should returnResult(false)
evaluateExpression("[[1]] = [[2]]") should returnResult(false)
evaluateExpression("[{x:1}] = [{x:2}]") should returnResult(false)

evaluateExpression("[1] = [true]") should returnResult(false)
}

it should "be compared with '!='" in {
evaluateExpression("[] != []") should returnResult(false)
evaluateExpression("[1] != [1]") should returnResult(false)
evaluateExpression("[[1]] != [[1]]") should returnResult(false)
evaluateExpression("[{x:1}] != [{x:1}]") should returnResult(false)

evaluateExpression("[] != [1]") should returnResult(true)
evaluateExpression("[1] != []") should returnResult(true)
evaluateExpression("[1] != [2]") should returnResult(true)
evaluateExpression("[[1]] != [[2]]") should returnResult(true)
evaluateExpression("[{x:1}] != [{x:2}]") should returnResult(true)

evaluateExpression("[1] != [true]") should returnResult(true)
}

it should "be accessed and compared" in {
evaluateExpression("[1][1] = 1") should returnResult(true)
}

it should "return null if compare to not a list" in {
evaluateExpression("[] = 1") should (returnNull() and reportFailure(
failureType = EvaluationFailureType.NOT_COMPARABLE,
failureMessage = "Can't compare '[]' with '1'"
))
}

"A for-expression" should "iterate over a range" in {
it should "iterate over a range" in {
evaluateExpression("for x in 1..3 return x * 2") should returnResult(List(2, 4, 6))

evaluateExpression("for x in 1..n return x * 2", Map("n" -> 3)) should returnResult(
Expand Down Expand Up @@ -185,6 +231,23 @@ class InterpreterListExpressionTest
) should returnResult(List(1, 1, 2, 3, 5, 8, 13, 21))
}

it should "return null if the value is not a list" in {
evaluateExpression(
expression = "for item in x return item * 2"
) should (returnNull() and reportFailure(
INVALID_TYPE,
"Expected list but found 'null'"
))

evaluateExpression(
expression = "for item in x return item * 2",
variables = Map("x" -> 2)
) should (returnNull() and reportFailure(
INVALID_TYPE,
"Expected list but found '2'"
))
}

private val hugeList: List[Int] = (1 to 10000).toList

"A huge list" should "be defined as range" in {
Expand Down Expand Up @@ -423,4 +486,28 @@ class InterpreterListExpressionTest
) should returnResult(20)
}

it should "return null if the value is not a list" in {
evaluateExpression(
expression = "x[even(item)]"
) should (returnNull() and reportFailure(
INVALID_TYPE,
"Expected list but found 'null'"
))

evaluateExpression(
expression = "x[even(item)]",
variables = Map("x" -> 2)
) should (returnNull() and reportFailure(
INVALID_TYPE,
"Expected list but found '2'"
))

evaluateExpression(
expression = "x[1]"
) should (returnNull() and reportFailure(
INVALID_TYPE,
"Expected list but found 'null'"
))
}

}
Loading