Repository: cloud-custodian/cel-python

Issue:
Getting a TypeError instead of evaluation error 
Getting the wrong exception type when expression uses _all_
Expect to get an CELEvalError but instead of a TypeError


```
import celpy
from celpy import CELEvalError

env = celpy.Environment()

# Press the green button in the gutter to run the script.
if __name__ == '__main__':
    all_positive = "data.all(x, x>0)"
    breaks = {'data_': [1, 2, 3] }

    ast = env.compile(all_positive)
    prgm = env.program(ast)

    try:
        prgm.evaluate(breaks)
    except CELEvalError as ex:
        print('Got an evaluation error')
    except TypeError as ex:
        print('Got a type error')
```


Happy to try and fix with some guidance

Discussion:
@hardbyte: I'm curious why your context has `data_` where your expression uses `data` - just to expose the issue right?

For reference, I see an evaluation error using:

```python
    all_positive = "data.all(x, x > 0)"
    breaks = {'data': [1, 2, '3']}
```

But a type error using:

```python
    all_positive = "true || data.all(x, x > 0)"
    breaks = {'data_': [1, 2, 3]}
```@slott56: I'm not sure what the baseline Golang CEL implementation does. If anyone has an example from the Go implementation, that would be helpful.

I am confident it does not expose a Python exception; it seems more likely that it returns a CEL error code.@slott56: ```
  File "/Users/slott/github/local/cel-python/src/celpy/evaluation.py", line 1952, in member_dot_arg
    reduction = reduce(and_oper, map(sub_expr, member_list), celpy.celtypes.BoolType(True))
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: 'CELEvalError' object is not iterable
```

This is because `data_` is undefined. I think the solution is the propogate the `CELEvalError` as the result.

What does the Go-language CEL do in this case?

Solution patch:
diff --git a/src/celpy/celparser.py b/src/celpy/celparser.py
index 95279ca..365388a 100644
--- a/src/celpy/celparser.py
+++ b/src/celpy/celparser.py
@@ -266,11 +266,8 @@ class DumpAST(lark.visitors.Visitor_Recursive):
         else:
             exprlist = ""
         right = cast(lark.Token, tree.children[1]).value
-        if self.stack:
-            left = self.stack.pop()
-            self.stack.append(f"{left}.{right}({exprlist})")
-        else:
-            self.stack.append(f".{right}({exprlist})")
+        left = self.stack.pop()
+        self.stack.append(f"{left}.{right}({exprlist})")
 
     def member_index(self, tree: lark.Tree) -> None:
         right = self.stack.pop()
diff --git a/src/celpy/evaluation.py b/src/celpy/evaluation.py
index a0af546..afe461a 100644
--- a/src/celpy/evaluation.py
+++ b/src/celpy/evaluation.py
@@ -2030,71 +2030,71 @@ class Evaluator(lark.visitors.Interpreter[Result]):
             Tuple[lark.Tree, lark.Token], tree.children[:2]
         )
 
-        if method_name_token.value == "map":
+        if method_name_token.value in {"map", "filter", "all", "exists", "exists_one", "reduce", "min"}:
             member_list = cast(celpy.celtypes.ListType, self.visit(member_tree))
-            sub_expr = self.build_macro_eval(tree)
-            mapping = cast(Iterable[celpy.celtypes.Value], map(sub_expr, member_list))
-            result = celpy.celtypes.ListType(mapping)
-            return result
-
-        elif method_name_token.value == "filter":
-            member_list = cast(celpy.celtypes.ListType, self.visit(member_tree))
-            sub_expr = self.build_macro_eval(tree)
-            result = celpy.celtypes.ListType(filter(sub_expr, member_list))
-            return result
 
-        elif method_name_token.value == "all":
-            member_list = cast(celpy.celtypes.ListType, self.visit(member_tree))
-            and_oper = cast(
-                CELBoolFunction,
-                eval_error("no such overload", TypeError)(celpy.celtypes.logical_and),
-            )
-            sub_expr = self.build_ss_macro_eval(tree)
-            reduction = reduce(
-                and_oper, map(sub_expr, member_list), celpy.celtypes.BoolType(True)
-            )
-            return reduction
-
-        elif method_name_token.value == "exists":
-            member_list = cast(celpy.celtypes.ListType, self.visit(member_tree))
-            or_oper = cast(
-                CELBoolFunction,
-                eval_error("no such overload", TypeError)(celpy.celtypes.logical_or),
-            )
-            sub_expr = self.build_ss_macro_eval(tree)
-            reduction = reduce(
-                or_oper, map(sub_expr, member_list), celpy.celtypes.BoolType(False)
-            )
-            return reduction
+            if isinstance(member_list, CELEvalError):
+                return member_list
+
+            if method_name_token.value == "map":
+                sub_expr = self.build_macro_eval(tree)
+                mapping = cast(Iterable[celpy.celtypes.Value], map(sub_expr, member_list))
+                result = celpy.celtypes.ListType(mapping)
+                return result
+
+            elif method_name_token.value == "filter":
+                sub_expr = self.build_macro_eval(tree)
+                result = celpy.celtypes.ListType(filter(sub_expr, member_list))
+                return result
+
+            elif method_name_token.value == "all":
+                sub_expr = self.build_ss_macro_eval(tree)
+                and_oper = cast(
+                    CELBoolFunction,
+                    eval_error("no such overload", TypeError)(
+                        celpy.celtypes.logical_and)
+                )
+                reduction = reduce(and_oper, map(sub_expr, member_list), celpy.celtypes.BoolType(True))
+                return reduction
+
+            elif method_name_token.value == "exists":
+                sub_expr = self.build_ss_macro_eval(tree)
+                or_oper = cast(
+                    CELBoolFunction,
+                    eval_error("no such overload", TypeError)(
+                        celpy.celtypes.logical_or)
+                )
+                reduction = reduce(or_oper, map(sub_expr, member_list), celpy.celtypes.BoolType(False))
+                return reduction
+
+            elif method_name_token.value == "exists_one":
+                # Is there exactly 1?
+                sub_expr = self.build_macro_eval(tree)
+                count = sum(1 for value in member_list if bool(sub_expr(value)))
+                return celpy.celtypes.BoolType(count == 1)
+
+            elif method_name_token.value == "reduce":
+                # Apply a function to reduce the list to a single value.
+                # The `tree` is a `member_dot_arg` construct with (member, method_name, args)
+                # The args have two variables and two expressions.
+                reduce_expr, init_expr_tree = self.build_reduce_macro_eval(tree)
+                initial_value = self.visit(init_expr_tree)
+                reduction = reduce(reduce_expr, member_list, initial_value)
+                return reduction
+
+            elif method_name_token.value == "min":
+                # Special case of "reduce()"
+                # with <member>.min() -> <member>.reduce(r, i, int_max, r < i ? r : i)
+                try:
+                    # Note. The Result type includes None, which will raise an exception.
+                    reduction = min(member_list)  # type: ignore [type-var]
+                except ValueError as ex:
+                    err = "Attempt to reduce an empty sequence or a sequence with a None value"
+                    reduction = CELEvalError(err, ex.__class__, ex.args, tree=tree)
+                return reduction
 
-        elif method_name_token.value == "exists_one":
-            # Is there exactly 1?
-            member_list = cast(celpy.celtypes.ListType, self.visit(member_tree))
-            sub_expr = self.build_macro_eval(tree)
-            count = sum(1 for value in member_list if bool(sub_expr(value)))
-            return celpy.celtypes.BoolType(count == 1)
-
-        elif method_name_token.value == "reduce":
-            # Apply a function to reduce the list to a single value.
-            # The `tree` is a `member_dot_arg` construct with (member, method_name, args)
-            # The args have two variables and two expressions.
-            member_list = cast(celpy.celtypes.ListType, self.visit(member_tree))
-            reduce_expr, init_expr_tree = self.build_reduce_macro_eval(tree)
-            initial_value = self.visit(init_expr_tree)
-            reduction = reduce(reduce_expr, member_list, initial_value)
-            return reduction
-
-        elif method_name_token.value == "min":
-            # Special case of "reduce()"
-            # with <member>.min() -> <member>.reduce(r, i, int_max, r < i ? r : i)
-            member_list = cast(celpy.celtypes.ListType, self.visit(member_tree))
-            try:
-                # Note. The Result type includes None, which will raise an exception.
-                reduction = min(member_list)  # type: ignore [type-var]
-            except ValueError as ex:
-                err = "Attempt to reduce an empty sequence or a sequence with a None value"
-                reduction = CELEvalError(err, ex.__class__, ex.args, tree=tree)
-            return reduction
+            else:
+                raise RuntimeError("Internal Design Error")  # pragma: no cover
 
         else:
             # Not a macro: a method evaluation.
diff --git a/type_check/lineprecision.txt b/type_check/lineprecision.txt
index b53a576..5e06b3b 100644
--- a/type_check/lineprecision.txt
+++ b/type_check/lineprecision.txt
@@ -1,11 +1,11 @@
 Name              Lines  Precise  Imprecise  Any  Empty  Unanalyzed
 -------------------------------------------------------------------
-celpy               306       76          0    4    226           0
-celpy.__main__      508      204          8   42    254           0
-celpy.adapter       141       34          3    9     89           6
-celpy.c7nlib       1663      351         14  149   1149           0
-celpy.celparser     389      199          3   23    164           0
-celpy.celtypes     1504      417         14  224    812          37
-celpy.evaluation   2578      866         38  176   1481          17
+celpy               293       76          0    4    213           0
+celpy.__main__      465      172          7   42    244           0
+celpy.adapter       137       35          3    9     85           5
+celpy.c7nlib       1584      340         15  154   1075           0
+celpy.celparser     401      207          2   22    170           0
+celpy.celtypes     1503      438         14  221    791          39
+celpy.evaluation   2472      835         33  177   1411          16
 xlate                 0        0          0    0      0           0
-xlate.c7n_to_cel   1755      385        103  146   1115           6
+xlate.c7n_to_cel   1730      377        102  145   1101           5


Test patch:
diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py
index 80bd8e7..c5d8bab 100644
--- a/tests/test_evaluation.py
+++ b/tests/test_evaluation.py
@@ -1631,6 +1631,15 @@ def test_member_dot_arg_all(monkeypatch):
     )
 
 
+def test_member_dot_arg_all_issue_41(monkeypatch):
+    """The filter macro CelEvalError().all(x, x) == CelEvalError()"""
+    tree = macro_member_tree("all")
+    the_error = CELEvalError()
+    eval_0 = Evaluator(tree, activation=Mock(resolve_variable=Mock(return_value=the_error)))
+    assert eval_0.member_dot_arg(tree.children[0]) is the_error
+
+
+
 def test_member_dot_arg_exists(monkeypatch):
     """The filter macro [true, false].exists(x, x) == [true]"""
     visit = Mock(
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 63e321c..3accfde 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -448,3 +448,4 @@ def test_dump_issue_35():
 def test_tree_dump(parser):
     ast = parser.parse("-(3*4+5-1/2%3==1)?name[index]:f(1,2)||false&&true")
     assert tree_dump(ast) == '- (3 *  4 +  5 -  1 /  2 %  3 ==  1) ? name[index] : f(1, 2) || false && true'
+