Skip to content

Commit b2f6fb8

Browse files
authored
Allow to override builtins (#522)
* Allow to override builtins * Add :: syntax to access builtin in case of override
1 parent db94b96 commit b2f6fb8

File tree

7 files changed

+130
-57
lines changed

7 files changed

+130
-57
lines changed

builtin/builtin_test.go

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -284,37 +284,71 @@ func TestBuiltin_memory_limits(t *testing.T) {
284284
}
285285
}
286286

287-
func TestBuiltin_disallow_builtins_override(t *testing.T) {
288-
t.Run("via env", func(t *testing.T) {
289-
env := map[string]any{
290-
"len": func() int { return 42 },
291-
"repeat": func(a string) string {
292-
return a
293-
},
287+
func TestBuiltin_allow_builtins_override(t *testing.T) {
288+
t.Run("via env var", func(t *testing.T) {
289+
for _, name := range builtin.Names {
290+
t.Run(name, func(t *testing.T) {
291+
env := map[string]any{
292+
name: "hello world",
293+
}
294+
program, err := expr.Compile(name, expr.Env(env))
295+
require.NoError(t, err)
296+
297+
out, err := expr.Run(program, env)
298+
require.NoError(t, err)
299+
assert.Equal(t, "hello world", out)
300+
})
301+
}
302+
})
303+
t.Run("via env func", func(t *testing.T) {
304+
for _, name := range builtin.Names {
305+
t.Run(name, func(t *testing.T) {
306+
env := map[string]any{
307+
name: func() int { return 1 },
308+
}
309+
program, err := expr.Compile(fmt.Sprintf("%s()", name), expr.Env(env))
310+
require.NoError(t, err)
311+
312+
out, err := expr.Run(program, env)
313+
require.NoError(t, err)
314+
assert.Equal(t, 1, out)
315+
})
294316
}
295-
assert.Panics(t, func() {
296-
_, _ = expr.Compile(`string(len("foo")) + repeat("0", 2)`, expr.Env(env))
297-
})
298317
})
299318
t.Run("via expr.Function", func(t *testing.T) {
300-
length := expr.Function("len",
301-
func(params ...any) (any, error) {
302-
return 42, nil
303-
},
304-
new(func() int),
305-
)
306-
repeat := expr.Function("repeat",
307-
func(params ...any) (any, error) {
308-
return params[0], nil
309-
},
310-
new(func(string) string),
311-
)
312-
assert.Panics(t, func() {
313-
_, _ = expr.Compile(`string(len("foo")) + repeat("0", 2)`, length, repeat)
314-
})
319+
for _, name := range builtin.Names {
320+
t.Run(name, func(t *testing.T) {
321+
fn := expr.Function(name,
322+
func(params ...any) (any, error) {
323+
return 42, nil
324+
},
325+
new(func() int),
326+
)
327+
program, err := expr.Compile(fmt.Sprintf("%s()", name), fn)
328+
require.NoError(t, err)
329+
330+
out, err := expr.Run(program, nil)
331+
require.NoError(t, err)
332+
assert.Equal(t, 42, out)
333+
})
334+
}
315335
})
316336
}
317337

338+
func TestBuiltin_override_and_still_accessible(t *testing.T) {
339+
env := map[string]any{
340+
"len": func() int { return 42 },
341+
"all": []int{1, 2, 3},
342+
}
343+
344+
program, err := expr.Compile(`::all(all, #>0) && len() == 42 && ::len(all) == 3`, expr.Env(env))
345+
require.NoError(t, err)
346+
347+
out, err := expr.Run(program, env)
348+
require.NoError(t, err)
349+
assert.Equal(t, true, out)
350+
}
351+
318352
func TestBuiltin_DisableBuiltin(t *testing.T) {
319353
t.Run("via env", func(t *testing.T) {
320354
for _, b := range builtin.Builtins {

checker/checker.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -156,24 +156,25 @@ func (v *checker) IdentifierNode(node *ast.IdentifierNode) (reflect.Type, info)
156156
if node.Value == "$env" {
157157
return mapType, info{}
158158
}
159-
if fn, ok := v.config.Builtins[node.Value]; ok {
160-
return functionType, info{fn: fn}
161-
}
162-
if fn, ok := v.config.Functions[node.Value]; ok {
163-
return functionType, info{fn: fn}
164-
}
165-
return v.env(node, node.Value, true)
159+
return v.ident(node, node.Value, true, true)
166160
}
167161

168-
// env method returns type of environment variable. env only lookups for
169-
// environment variables, no builtins, no custom functions.
170-
func (v *checker) env(node ast.Node, name string, strict bool) (reflect.Type, info) {
162+
// ident method returns type of environment variable, builtin or function.
163+
func (v *checker) ident(node ast.Node, name string, strict, builtins bool) (reflect.Type, info) {
171164
if t, ok := v.config.Types[name]; ok {
172165
if t.Ambiguous {
173166
return v.error(node, "ambiguous identifier %v", name)
174167
}
175168
return t.Type, info{method: t.Method}
176169
}
170+
if builtins {
171+
if fn, ok := v.config.Functions[name]; ok {
172+
return functionType, info{fn: fn}
173+
}
174+
if fn, ok := v.config.Builtins[name]; ok {
175+
return functionType, info{fn: fn}
176+
}
177+
}
177178
if v.config.Strict && strict {
178179
return v.error(node, "unknown name %v", name)
179180
}
@@ -433,6 +434,7 @@ func (v *checker) MemberNode(node *ast.MemberNode) (reflect.Type, info) {
433434
base, _ := v.visit(node.Node)
434435
prop, _ := v.visit(node.Property)
435436

437+
// $env variable
436438
if an, ok := node.Node.(*ast.IdentifierNode); ok && an.Value == "$env" {
437439
if name, ok := node.Property.(*ast.StringNode); ok {
438440
strict := v.config.Strict
@@ -443,7 +445,7 @@ func (v *checker) MemberNode(node *ast.MemberNode) (reflect.Type, info) {
443445
// should throw error if field is not found & v.config.Strict.
444446
strict = false
445447
}
446-
return v.env(node, name.Value, strict)
448+
return v.ident(node, name.Value, strict, false /* no builtins and no functions */)
447449
}
448450
return anyType, info{}
449451
}

conf/config.go

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,20 +98,14 @@ func (c *Config) Check() {
9898
}
9999
}
100100
}
101-
for fnName, t := range c.Types {
102-
if kind(t.Type) == reflect.Func {
103-
for _, b := range c.Builtins {
104-
if b.Name == fnName {
105-
panic(fmt.Errorf(`cannot override builtin %s(): use expr.DisableBuiltin("%s") to override`, b.Name, b.Name))
106-
}
107-
}
108-
}
101+
}
102+
103+
func (c *Config) IsOverridden(name string) bool {
104+
if _, ok := c.Functions[name]; ok {
105+
return true
109106
}
110-
for _, f := range c.Functions {
111-
for _, b := range c.Builtins {
112-
if b.Name == f.Name {
113-
panic(fmt.Errorf(`cannot override builtin %s(); use expr.DisableBuiltin("%s") to override`, f.Name, f.Name))
114-
}
115-
}
107+
if _, ok := c.Types[name]; ok {
108+
return true
116109
}
110+
return false
117111
}

parser/lexer/lexer_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,14 @@ func TestLex(t *testing.T) {
225225
{Kind: EOF},
226226
},
227227
},
228+
{
229+
`: ::`,
230+
[]Token{
231+
{Kind: Operator, Value: ":"},
232+
{Kind: Operator, Value: "::"},
233+
{Kind: EOF},
234+
},
235+
},
228236
}
229237

230238
for _, test := range tests {

parser/lexer/state.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ func root(l *lexer) stateFn {
3737
case r == '|':
3838
l.accept("|")
3939
l.emit(Operator)
40+
case r == ':':
41+
l.accept(":")
42+
l.emit(Operator)
4043
case strings.ContainsRune("([{", r):
4144
l.emit(Bracket)
4245
case strings.ContainsRune(")]}", r):
4346
l.emit(Bracket)
44-
case strings.ContainsRune(",:;%+-^", r): // single rune operator
47+
case strings.ContainsRune(",;%+-^", r): // single rune operator
4548
l.emit(Operator)
4649
case strings.ContainsRune("&!=*<>", r): // possible double rune operator
4750
l.accept("&=*")

parser/parser.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,13 @@ func (p *parser) parsePrimary() Node {
275275
}
276276
}
277277

278+
if token.Is(Operator, "::") {
279+
p.next()
280+
token = p.current
281+
p.expect(Identifier)
282+
return p.parsePostfixExpression(p.parseCall(token, false))
283+
}
284+
278285
return p.parseSecondary()
279286
}
280287

@@ -300,7 +307,7 @@ func (p *parser) parseSecondary() Node {
300307
node.SetLocation(token.Location)
301308
return node
302309
default:
303-
node = p.parseCall(token)
310+
node = p.parseCall(token, true)
304311
}
305312

306313
case Number:
@@ -379,15 +386,17 @@ func (p *parser) toFloatNode(number float64) Node {
379386
return &FloatNode{Value: number}
380387
}
381388

382-
func (p *parser) parseCall(token Token) Node {
389+
func (p *parser) parseCall(token Token, checkOverrides bool) Node {
383390
var node Node
384391
if p.current.Is(Bracket, "(") {
385392
var arguments []Node
386393

387-
if b, ok := predicates[token.Value]; ok {
388-
p.expect(Bracket, "(")
394+
isOverridden := p.config.IsOverridden(token.Value)
395+
isOverridden = isOverridden && checkOverrides
389396

390-
// TODO: Refactor parser to use builtin.Builtins instead of predicates map.
397+
// TODO: Refactor parser to use builtin.Builtins instead of predicates map.
398+
if b, ok := predicates[token.Value]; ok && !isOverridden {
399+
p.expect(Bracket, "(")
391400

392401
if b.arity == 1 {
393402
arguments = make([]Node, 1)
@@ -417,7 +426,7 @@ func (p *parser) parseCall(token Token) Node {
417426
Arguments: arguments,
418427
}
419428
node.SetLocation(token.Location)
420-
} else if _, ok := builtin.Index[token.Value]; ok && !p.config.Disabled[token.Value] {
429+
} else if _, ok := builtin.Index[token.Value]; ok && !p.config.Disabled[token.Value] && !isOverridden {
421430
node = &BuiltinNode{
422431
Name: token.Value,
423432
Arguments: p.parseArguments(),

parser/parser_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,29 @@ world`},
498498
},
499499
},
500500
},
501+
{
502+
`::split("a,b,c", ",")`,
503+
&BuiltinNode{
504+
Name: "split",
505+
Arguments: []Node{
506+
&StringNode{Value: "a,b,c"},
507+
&StringNode{Value: ","},
508+
},
509+
},
510+
},
511+
{
512+
`::split("a,b,c", ",")[0]`,
513+
&MemberNode{
514+
Node: &BuiltinNode{
515+
Name: "split",
516+
Arguments: []Node{
517+
&StringNode{Value: "a,b,c"},
518+
&StringNode{Value: ","},
519+
},
520+
},
521+
Property: &IntegerNode{Value: 0},
522+
},
523+
},
501524
}
502525
for _, test := range tests {
503526
t.Run(test.input, func(t *testing.T) {

0 commit comments

Comments
 (0)