diff --git a/cel/env_test.go b/cel/env_test.go index 0bfaf1b6..fa568257 100644 --- a/cel/env_test.go +++ b/cel/env_test.go @@ -579,6 +579,39 @@ func TestEnvFromConfig(t *testing.T) { }, }, }, + { + name: "custom env - variable to constant", + afterOpts: []EnvOption{ + Constant("foo.bar", types.IntType, types.Int(42)), + }, + conf: env.NewConfig("custom env - functions"). + AddVariables(env.NewVariable("foo.bar", env.NewTypeDesc("int"))), + exprs: []exprCase{ + { + name: "valid", + expr: "foo.bar == 42", + out: types.True, + }, + }, + }, + { + name: "custom env - variable to compatible constants", + beforeOpts: []EnvOption{ + Constant("foo.bar", types.IntType, types.Int(42)), + }, + afterOpts: []EnvOption{ + Constant("foo.bar", types.IntType, types.Int(42)), + }, + conf: env.NewConfig("custom env - functions"). + AddVariables(env.NewVariable("foo.bar", env.NewTypeDesc("int"))), + exprs: []exprCase{ + { + name: "valid", + expr: "foo.bar == 42", + out: types.True, + }, + }, + }, { name: "pure custom env", beforeOpts: []EnvOption{func(*Env) (*Env, error) { @@ -910,6 +943,73 @@ func TestEnvFromConfigErrors(t *testing.T) { } } +func TestEnvVariableValidation(t *testing.T) { + tcs := []struct { + name string + opts []EnvOption + wantErr string + }{ + { + name: "compatible_duplicate", + opts: []EnvOption{ + Variable("foo", IntType), + Variable("foo", IntType), + }, + }, + { + name: "variable_and_constant", + opts: []EnvOption{ + Variable("foo", IntType), + Constant("foo", IntType, types.Int(50)), + }, + }, + { + name: "compatible_constant", + opts: []EnvOption{ + Constant("foo", StringType, types.String("foo")), + Constant("foo", StringType, types.String("foo")), + }, + }, + { + name: "incompatible_variable", + opts: []EnvOption{ + Variable("foo", IntType), + Variable("foo", DoubleType), + }, + wantErr: "overlapping identifier for name", + }, + { + name: "incompatible_constant", + opts: []EnvOption{ + Constant("foo", IntType, types.Int(42)), + Constant("foo", IntType, types.Int(43)), + }, + wantErr: "conflicting constant definitions", + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(*testing.T) { + env, err := NewEnv(tc.opts...) + if err != nil { + t.Fatalf("NewEnv() returned error: %v", err) + } + // Variables are lazily validated. + _, iss := env.Compile("foo") + err = iss.Err() + if tc.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("Compile() returned error %v, wanted %s", err, tc.wantErr) + } + return + } + if err != nil { + t.Errorf("Compile() returned error %v wanted nil", err) + } + }) + } +} + func BenchmarkNewCustomEnvLazy(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { diff --git a/checker/env.go b/checker/env.go index 6d991eba..16c8ae60 100644 --- a/checker/env.go +++ b/checker/env.go @@ -273,12 +273,31 @@ func (e *Env) setFunction(fn *decls.FunctionDecl) []errorMsg { return errMsgs } +func maybeMergeConstant(a *decls.VariableDecl, b *decls.VariableDecl) (*decls.VariableDecl, errorMsg) { + if b.Value() != nil { + if a.Value() == nil { + return b, "" + } + eq, ok := a.Value().Equal(b.Value()).Value().(bool) + if ok && eq { + return a, "" + } + return nil, constantConflictError(b.Name()) + } + return a, "" +} + // addIdent adds the Decl to the declarations in the Env. // Returns a non-empty errorMsg if the identifier is already declared in the scope. func (e *Env) addIdent(decl *decls.VariableDecl) errorMsg { current := e.declarations.FindIdentInScope(decl.Name()) if current != nil { if current.DeclarationIsEquivalent(decl) { + decl, errMsg := maybeMergeConstant(current, decl) + if errMsg != "" { + return errMsg + } + e.declarations.AddIdent(decl) return "" } return overlappingIdentifierError(decl.Name()) @@ -325,6 +344,10 @@ func (e *Env) exitScope() *Env { // may be accumulated into an error at a later point in execution. type errorMsg string +func constantConflictError(name string) errorMsg { + return errorMsg(fmt.Sprintf("conflicting constant definitions for name '%s'", name)) +} + func overlappingIdentifierError(name string) errorMsg { return errorMsg(fmt.Sprintf("overlapping identifier for name '%s'", name)) }