blob: 11e8848be7ca7db41f1031477c786888b6115bb8 [file]
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"errors"
"strings"
"testing"
"github.com/google/cel-go/common"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/operators"
"google.golang.org/protobuf/proto"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
func TestUnparse(t *testing.T) {
tests := []struct {
name string
in string
out interface{}
requiresMacroCalls bool
unparserOptions []UnparserOption
}{
{name: "call_add", in: `a + b - c`},
{name: "call_and", in: `a && b && c && d && e`},
{name: "call_and_or", in: `a || b && (c || d) && e`},
{name: "call_cond", in: `a ? b : c`},
{name: "call_cond_nested_inner", in: `a ? (c ? d : e) : b`},
{name: "call_cond_nested_outer", in: `a ? b : c ? d : e`, out: `a ? b : (c ? d : e)`},
{name: "call_index", in: `a[1]["b"]`},
{name: "call_index_eq", in: `x["a"].single_int32 == 23`},
{name: "call_mul", in: `a * (b / c) % 0`},
{name: "call_mul_add", in: `a + b * c`},
{name: "call_mul_add_nested", in: `(a + b) * c / (d - e)`},
{name: "call_mul_nested", in: `a * b / c % 0`},
{name: "call_not", in: `!true`},
{name: "call_neg", in: `-num`},
{name: "call_or", in: `a || b || c || d || e`},
{name: "call_neg_mult", in: `-(1 * 2)`},
{name: "call_neg_add", in: `-(1 + 2)`},
{name: "call_operator_precedence", in: `1 - (2 == -1)`},
{name: "calc_distr_paren", in: `(1 + 2) * 3`},
{name: "calc_distr_noparen", in: `1 + 2 * 3`},
{name: "cond_tern_simple", in: `(x > 5) ? (x - 5) : 0`},
{name: "cond_tern_neg_expr", in: `-((x > 5) ? (x - 5) : 0)`},
{name: "cond_tern_neg_term", in: `-x ? (x - 5) : 0`},
{name: "func_global", in: `size(a ? (b ? c : d) : e)`},
{name: "func_member", in: `a.hello("world")`},
{name: "func_no_arg", in: `zero()`},
{name: "func_one_arg", in: `one("a")`},
{name: "func_two_args", in: `and(d, 32u)`},
{name: "func_var_args", in: `max(a, b, 100)`},
{name: "func_neq", in: `x != "a"`},
{name: "func_in", in: `a in b`},
{name: "list_empty", in: `[]`},
{name: "list_one", in: `[1]`},
{name: "list_ints", in: `[1, 2, 3]`},
{name: "list_doubles", in: `[1.0, 2.0, 3.0]`},
{name: "list_doubles", in: `[1.1, 2.1, 3.1]`},
{name: "list_uints", in: `[1u, 2u, 3u]`},
{name: "list_numeric", in: `[1, 2.0, 3u]`},
{name: "list_many", in: `["hello, world", "goodbye, world", "sure, why not?"]`},
{name: "lit_bytes", in: `b"\303\203\302\277"`},
{name: "lit_double", in: `-42.101`},
{name: "lit_false", in: `false`},
{name: "lit_int", in: `-405069`},
{name: "lit_null", in: `null`},
{name: "lit_string", in: `"hello:\t'world'"`},
{name: "lit_string_quote", in: `"hello:\"world\""`},
{name: "lit_true", in: `true`},
{name: "lit_uint", in: `42u`},
{name: "ident", in: `my_ident`},
{name: "macro_has", in: `has(hello.world)`},
{name: "map_empty", in: `{}`},
{name: "map_lit_key", in: `{"a": a.b.c, b"\142": bytes(a.b.c)}`},
{name: "map_expr_key", in: `{a: a, b: a.b, c: a.b.c, a ? b : c: false, a || b: true}`},
{name: "msg_empty", in: `v1alpha1.Expr{}`},
{name: "msg_fields", in: `v1alpha1.Expr{id: 1, call_expr: v1alpha1.Call_Expr{function: "name"}}`},
{name: "select", in: `a.b.c`},
{name: "idx_idx_sel", in: `a[b][c].name`},
{name: "sel_expr_target", in: `(a + b).name`},
{name: "sel_cond_target", in: `(a ? b : c).name`},
{name: "idx_cond_target", in: `(a ? b : c)[0]`},
{name: "cond_conj", in: `(a1 && a2) ? b : c`},
{name: "cond_disj_conj", in: `a ? (b1 || b2) : (c1 && c2)`},
{name: "call_cond_target", in: `(a ? b : c).method(d)`},
{name: "cond_flat", in: `false && !true || false`},
{name: "cond_paren", in: `false && (!true || false)`},
{name: "cond_cond", in: `(false && !true || false) ? 2 : 3`},
{name: "cond_binop", in: `(x < 5) ? x : 5`},
{name: "cond_binop_binop", in: `(x > 5) ? (x - 5) : 0`},
{name: "cond_cond_binop", in: `(x > 5) ? ((x > 10) ? (x - 10) : 5) : 0`},
{name: "select_opt", in: `a.?b`},
{name: "index_opt", in: `a[?b]`},
{name: "list_lit_opt", in: `[?a, ?b, c]`},
{name: "map_lit_opt", in: `{?a: b, c: d}`},
{name: "msg_fields_opt", in: `v1alpha1.Expr{?id: id, call_expr: v1alpha1.Call_Expr{function: "name"}}`},
{name: "select_quoted", in: "a.`b-c`"},
{name: "opt_select_quoted", in: "a.?`b.c`"},
{name: "message_create_quoted", in: "MyType{`in`: false}"},
// Equivalent expressions form unparse which do not match the originals.
{name: "call_add_equiv", in: `a+b-c`, out: `a + b - c`},
{name: "call_cond_equiv", in: `a ? b : c`, out: `a ? b : c`},
{name: "call_index_equiv", in: `a[ 1 ]["b"]`, out: `a[1]["b"]`},
{name: "call_or_and_equiv", in: `(false && !true) || false`, out: `false && !true || false`},
{name: "call_not_not_equiv", in: `!!true`, out: `true`},
{name: "call_cond_equiv", in: `(a || b ? c : d).e`, out: `((a || b) ? c : d).e`},
{name: "lit_quote_bytes_equiv", in: `b'aaa"bbb'`, out: `b"\141\141\141\042\142\142\142"`},
{name: "select_equiv", in: `a . b . c`, out: `a.b.c`},
// These expressions require macro call tracking to be enabled.
{
name: "comp_all",
in: `[1, 2, 3].all(x, x > 0)`,
requiresMacroCalls: true,
},
{
name: "comp_exists",
in: `[1, 2, 3].exists(x, x > 0)`,
requiresMacroCalls: true,
},
{
name: "comp_map",
in: `[1, 2, 3].map(x, x >= 2, x * 4)`,
requiresMacroCalls: true,
},
{
name: "comp_exists_one",
in: `[1, 2, 3].exists_one(x, x >= 2)`,
requiresMacroCalls: true,
},
{
name: "comp_nested",
in: `[[1], [2], [3]].map(x, x.filter(y, y > 1))`,
requiresMacroCalls: true,
},
{
name: "comp_chained",
in: `[1, 2, 3].map(x, x >= 2, x * 4).filter(x, x <= 10)`,
requiresMacroCalls: true,
},
{
name: "comp_chained_opt",
in: `[?a, b[?0], c].map(x, x >= 2, x * 4).filter(x, x <= 10)`,
requiresMacroCalls: true,
},
{
name: "comp_map_opt",
in: `{?a: b[?0]}.map(k, x >= 2, x * 4)`,
requiresMacroCalls: true,
},
{
name: "comp_map_opt",
in: `{a: has(b.c)}.exists(k, k != "")`,
requiresMacroCalls: true,
},
{
name: "comp_nested",
in: `{a: [1, 2].all(i > 0)}.exists(k, k != "")`,
requiresMacroCalls: true,
},
// These expressions will not be wrapped because they haven't met the
// conditions required by the provided unparser options
{
name: "call_no_wrap_no_operators",
in: "a + b + c + d",
out: "a + b + c + d",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
},
},
{
name: "call_no_wrap_column_limit_large_val",
in: "a + b + c + d",
out: "a + b + c + d",
unparserOptions: []UnparserOption{
WrapOnColumn(1000),
WrapOnOperators(operators.Add),
},
},
{
name: "call_no_wrap_column_limit_equal_length_to_input",
in: "a + b + c + d",
out: "a + b + c + d",
unparserOptions: []UnparserOption{
WrapOnColumn(13),
WrapOnOperators(operators.Add),
},
},
// These expressions will be formatted based on the unparser options provided
{
name: "call_wrap_add",
in: "a + b - d * e",
out: "a +\nb - d * e",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Add),
},
},
{
name: "call_wrap_add_subtract",
in: "a * b + c - d * e",
out: "a * b +\nc -\nd * e",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Add, operators.Subtract),
},
},
{
name: "call_wrap_add_subtract",
in: "a * b + c - d * e",
out: "a * b +\nc -\nd * e",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Add, operators.Subtract),
},
},
{
name: "call_wrap_logical_and",
in: "a && b && c && d && e",
out: "a &&\nb &&\nc &&\nd &&\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.LogicalAnd),
},
},
{
name: "call_wrap_logical_and_2",
in: "a && b",
out: "a &&\nb",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.LogicalAnd),
},
},
{
name: "call_wrap_conditional",
in: "a ? b : c ? d : e",
out: "a ?\nb : (c ?\nd : e)",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Conditional),
},
},
{
name: "call_wrap_or",
in: "a || b || c || d || e",
out: "a ||\nb ||\nc ||\nd ||\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.LogicalOr),
},
},
{
name: "call_wrap_equals",
in: "a == b == c == d == e",
out: "a ==\nb ==\nc ==\nd ==\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Equals),
},
},
{
name: "call_wrap_greater",
in: "a > b > c > d > e",
out: "a >\nb >\nc >\nd >\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Greater),
},
},
{
name: "call_wrap_greater_equals",
in: "a >= b >= c >= d >= e",
out: "a >=\nb >=\nc >=\nd >=\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.GreaterEquals),
},
},
{
name: "call_wrap_in",
in: "a in b in c in d in e",
out: "a in\nb in\nc in\nd in\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.In),
},
},
{
name: "call_wrap_less",
in: "a < b < c < d < e",
out: "a <\nb <\nc <\nd <\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Less),
},
},
{
name: "call_wrap_less_equals",
in: "a <= b <= c <= d <= e",
out: "a <=\nb <=\nc <=\nd <=\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.LessEquals),
},
},
{
name: "call_wrap_not_equals",
in: "a != b != c != d != e",
out: "a !=\nb !=\nc !=\nd !=\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.NotEquals),
},
},
{
name: "call_wrap_divide",
in: "a / b / c / d / e",
out: "a /\nb /\nc /\nd /\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Divide),
},
},
{
name: "call_wrap_modulo",
in: "a % b % c % d % e",
out: "a %\nb %\nc %\nd %\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Modulo),
},
},
{
name: "call_wrap_multiply",
in: "a * b * c * d * e",
out: "a *\nb *\nc *\nd *\ne",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Multiply),
},
},
{
name: "call_wrap_logical_and_long_variables",
in: "longVariableA && longVariableB && longVariableC",
out: "longVariableA &&\nlongVariableB &&\nlongVariableC",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.LogicalAnd),
},
},
{
name: "comp_chained_wrap_comparisons",
in: "[1, 2, 3].map(x, x >= 2, x * 4).filter(x, x <= 10)",
out: "[1, 2, 3].map(x, x >=\n2, x * 4).filter(x, x <=\n10)",
requiresMacroCalls: true,
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.GreaterEquals, operators.LessEquals),
},
},
{
name: "call_wrap_before_add",
in: "a + b - d * e",
out: "a\n+ b - d * e",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Add),
WrapAfterColumnLimit(false),
},
},
{
name: "call_wrap_before_add_subtract",
in: "a * b + c - d * e",
out: "a * b\n+ c\n- d * e",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.Add, operators.Subtract),
WrapAfterColumnLimit(false),
},
},
{
name: "call_wrap_logical_and_long_variables",
in: "longVariableA && longVariableB && longVariableC",
out: "longVariableA\n&& longVariableB\n&& longVariableC",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
WrapOnOperators(operators.LogicalAnd),
WrapAfterColumnLimit(false),
},
},
{
name: "call_wrap_logical_and_long_input",
in: `"my-principal-group" in request.auth.claims && request.auth.claims.iat > now - duration("5m")`,
out: `"my-principal-group" in request.auth.claims &&` + "\n" + `request.auth.claims.iat > now - duration("5m")`,
unparserOptions: []UnparserOption{
WrapOnColumn(40),
WrapOnOperators(operators.LogicalAnd),
},
},
{
name: "call_wrap_before_logical_and_long_input",
in: `"my-principal-group" in request.auth.claims && request.auth.claims.iat > now - duration("5m")`,
out: `"my-principal-group" in request.auth.claims` + "\n" + `&& request.auth.claims.iat > now - duration("5m")`,
unparserOptions: []UnparserOption{
WrapOnColumn(40),
WrapOnOperators(operators.LogicalAnd),
WrapAfterColumnLimit(false),
},
},
{
// By default:
// - Column limit is at 80
// - && and || are wrapped
// - Wrapping occurs after the symbol
name: "call_wrap_default",
in: `jwt.extra_claims.filter(c, c.startsWith("group")).all(c, jwt.extra_claims[c].all(g, g.endsWith("@acme.co"))) && jwt.extra_claims.exists(c, c.startsWith("group")) || request.auth.claims.group == "admin" || request.auth.principal == "user:me@acme.co"`,
out: `jwt.extra_claims.filter(c, c.startsWith("group")).all(c, jwt.extra_claims[c].all(g, g.endsWith("@acme.co"))) &&` +
"\n" +
`jwt.extra_claims.exists(c, c.startsWith("group")) || request.auth.claims.group == "admin" ||` +
"\n" +
`request.auth.principal == "user:me@acme.co"`,
requiresMacroCalls: true,
},
{
// && and || are wrapped by default if only the column limit is specified
name: "call_wrap_default_operators",
in: "longVariableA && longVariableB || longVariableC + longVariableD - longVariableE",
out: "longVariableA &&\nlongVariableB ||\nlongVariableC + longVariableD - longVariableE",
unparserOptions: []UnparserOption{
WrapOnColumn(3),
},
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.name, func(t *testing.T) {
prsr, err := NewParser(
Macros(AllMacros...),
PopulateMacroCalls(tc.requiresMacroCalls),
EnableOptionalSyntax(true),
EnableIdentEscapeSyntax(true),
)
if err != nil {
t.Fatalf("NewParser() failed: %v", err)
}
p, iss := prsr.Parse(common.NewTextSource(tc.in))
if len(iss.GetErrors()) > 0 {
t.Fatalf("parser.Parse(%s) failed: %v", tc.in, iss.ToDisplayString())
}
out, err := Unparse(p.Expr(), p.SourceInfo(), tc.unparserOptions...)
if err != nil {
t.Fatalf("Unparse(%s) failed: %v", tc.in, err)
}
var want interface{} = tc.in
if tc.out != nil {
want = tc.out
}
if out != want {
t.Errorf("Unparse() got '%s', wanted '%s'", out, want)
}
p2, iss := prsr.Parse(common.NewTextSource(out))
if len(iss.GetErrors()) > 0 {
t.Fatalf("parser.Parse(%s) roundtrip failed: %v", tc.in, iss.ToDisplayString())
}
before, err := ast.ExprToProto(p.Expr())
if err != nil {
t.Fatalf("ast.ExprToProto() failed: %v", err)
}
after, err := ast.ExprToProto(p2.Expr())
if err != nil {
t.Fatalf("ast.ExprToProto() failed: %v", err)
}
if !proto.Equal(before, after) {
t.Errorf("Roundtrip Parse() differs from original. Got '%v', wanted '%v'", before, after)
}
})
}
}
func TestUnparseErrors(t *testing.T) {
validConstantExpression := &exprpb.Expr{
ExprKind: &exprpb.Expr_ConstExpr{
ConstExpr: &exprpb.Constant{
ConstantKind: &exprpb.Constant_NullValue{},
},
},
}
tests := []struct {
name string
in *exprpb.Expr
err error
unparserOptions []UnparserOption
}{
{name: "empty_expr", in: &exprpb.Expr{}, err: errors.New("unsupported expression")},
{
name: "bad_args",
in: &exprpb.Expr{
ExprKind: &exprpb.Expr_CallExpr{
CallExpr: &exprpb.Expr_Call{
Function: "_&&_",
Args: []*exprpb.Expr{{}, {}},
},
},
},
err: errors.New("unsupported expression"),
},
{
name: "bad_struct",
in: &exprpb.Expr{
ExprKind: &exprpb.Expr_StructExpr{
StructExpr: &exprpb.Expr_CreateStruct{
MessageName: "Msg",
Entries: []*exprpb.Expr_CreateStruct_Entry{
{KeyKind: &exprpb.Expr_CreateStruct_Entry_FieldKey{FieldKey: "field"}},
},
},
},
},
err: errors.New("unsupported expression"),
},
{
name: "bad_map",
in: &exprpb.Expr{
ExprKind: &exprpb.Expr_StructExpr{
StructExpr: &exprpb.Expr_CreateStruct{
Entries: []*exprpb.Expr_CreateStruct_Entry{
{KeyKind: &exprpb.Expr_CreateStruct_Entry_FieldKey{FieldKey: "field"}},
},
},
},
},
err: errors.New("unsupported expression"),
},
{
name: "bad_index",
in: &exprpb.Expr{
ExprKind: &exprpb.Expr_CallExpr{
CallExpr: &exprpb.Expr_Call{
Function: "_[_]",
Args: []*exprpb.Expr{{}, {}},
},
},
},
err: errors.New("unsupported expression"),
},
{
name: "bad_unparser_option_wrap_column_zero",
in: validConstantExpression,
err: errors.New("Invalid unparser option. Wrap column value must be greater than or equal to 1. Got 0 instead"),
unparserOptions: []UnparserOption{
WrapOnColumn(0),
},
},
{
name: "bad_unparser_option_wrap_column_negative",
in: validConstantExpression,
err: errors.New("Invalid unparser option. Wrap column value must be greater than or equal to 1. Got -1 instead"),
unparserOptions: []UnparserOption{
WrapOnColumn(-1),
},
},
{
name: "bad_unparser_option_unsupported_operator",
in: validConstantExpression,
err: errors.New("Invalid unparser option. Unsupported operator: bogus"),
unparserOptions: []UnparserOption{
WrapOnOperators("bogus"),
},
},
{
name: "bad_unparser_option_unary_operator",
in: validConstantExpression,
err: errors.New("Invalid unparser option. Unary operators are unsupported: " + operators.Negate),
unparserOptions: []UnparserOption{
WrapOnOperators(operators.Negate),
},
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.name, func(t *testing.T) {
info := ast.NewSourceInfo(nil)
e, err := ast.ProtoToExpr(tc.in)
if err != nil {
t.Fatalf("ast.ProtoToExpr(%v) failed: %v", tc.in, err)
}
out, err := Unparse(e, info, tc.unparserOptions...)
if err == nil {
t.Fatalf("Unparse(%v) got %v, wanted error %v", tc.in, out, tc.err)
}
if !strings.Contains(err.Error(), tc.err.Error()) {
t.Errorf("Unparse(%v) got unexpected error: %v, wanted %v", tc.in, err, tc.err)
}
})
}
}