Made undefined variables return an error
All checks were successful
Build / build (push) Successful in 2m5s

Previously undefined variables would be set to nil, which introduced subtle errors.
Changed this so that referencing an undefined variable will produce an error.
This behaviour can changed as part of the instance configuration by declaring a missing-var handler.
This commit is contained in:
Leon Mika 2025-01-28 09:25:52 +11:00
parent 2d56517de9
commit c433e4bf53
5 changed files with 82 additions and 3 deletions

View file

@ -186,7 +186,16 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec
if v, ok := ec.getVar(*n.Var); ok {
return v, nil
}
return nil, nil
if e.inst.missingVarHandler != nil {
dv, err := e.inst.missingVarHandler(ctx, *n.Var)
if err != nil {
return nil, err
}
return fromGoValue(dv)
}
return nil, errors.New("undefined variable: " + *n.Var)
case n.MaybeSub != nil:
sub := n.MaybeSub.Sub
if sub == nil {

View file

@ -11,6 +11,7 @@ import (
type Inst struct {
out io.Writer
missingBuiltinHandler MissingBuiltinHandler
missingVarHandler MissingVarHandler
echoPrinter EchoPrinter
rootEC *evalCtx
@ -30,6 +31,12 @@ func WithMissingBuiltinHandler(handler MissingBuiltinHandler) InstOption {
}
}
func WithMissingVarHandler(handler MissingVarHandler) InstOption {
return func(i *Inst) {
i.missingVarHandler = handler
}
}
func WithModule(module Module) InstOption {
return func(i *Inst) {
for name, builtin := range module.Builtins {

View file

@ -119,6 +119,64 @@ func TestInst_Eval(t *testing.T) {
}
}
func TestInst_MissingVarHandler(t *testing.T) {
tests := []struct {
desc string
expr string
handler ucl.MissingVarHandler
want any
wantErr string
}{
{
desc: "default - error",
expr: `$bla`,
wantErr: "undefined variable: bla",
},
{
desc: "handler - set to nil",
handler: func(ctx context.Context, name string) (any, error) {
return nil, nil
},
expr: `$bla`,
want: nil,
},
{
desc: "handler - set to a string 1",
handler: func(ctx context.Context, name string) (any, error) {
return "I am " + name, nil
},
expr: `$bla`,
want: "I am bla",
},
{
desc: "handler - set to a string 2",
handler: func(ctx context.Context, name string) (any, error) {
return "I am " + name, nil
},
expr: `$something`,
want: "I am something",
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
ctx := context.Background()
outW := bytes.NewBuffer(nil)
inst := ucl.New(ucl.WithOut(outW), ucl.WithTestBuiltin(), ucl.WithMissingVarHandler(tt.handler))
res, err := inst.Eval(ctx, tt.expr)
if tt.wantErr != "" {
assert.Error(t, err)
assert.Equal(t, err.Error(), tt.wantErr)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
}
})
}
}
var parseComments1 = `
proc lookup { |file|
foreach { |toks|

View file

@ -197,7 +197,9 @@ func TestBuiltins_If(t *testing.T) {
ctx := context.Background()
outW := bytes.NewBuffer(nil)
inst := New(WithOut(outW), WithTestBuiltin())
inst := New(WithOut(outW), WithTestBuiltin(), WithMissingVarHandler(func(ctx context.Context, name string) (any, error) {
return nil, nil
}))
err := evalAndDisplay(ctx, inst, tt.expr)
assert.NoError(t, err)
@ -387,7 +389,9 @@ func TestBuiltins_Try(t *testing.T) {
ctx := context.Background()
outW := bytes.NewBuffer(nil)
inst := New(WithOut(outW), WithTestBuiltin())
inst := New(WithOut(outW), WithTestBuiltin(), WithMissingVarHandler(func(ctx context.Context, name string) (any, error) {
return nil, nil
}))
err := evalAndDisplay(ctx, inst, tt.expr)
if tt.wantErr != "" {

View file

@ -11,6 +11,7 @@ import (
type BuiltinHandler func(ctx context.Context, args CallArgs) (any, error)
type MissingBuiltinHandler func(ctx context.Context, name string, args CallArgs) (any, error)
type MissingVarHandler func(ctx context.Context, name string) (any, error)
type CallArgs struct {
args invocationArgs