Made indexing a little more strict
All checks were successful
Build / build (push) Successful in 2m24s

Also added support for var setting with or without the dollar sign.
This commit is contained in:
Leon Mika 2025-06-12 13:28:44 +02:00
parent 3a88c0c777
commit eda791d714
6 changed files with 73 additions and 30 deletions

View file

@ -98,6 +98,7 @@ type astDotSuffix struct {
}
type astDot struct {
Pos lexer.Position
Arg astCmdArg `parser:"@@"`
DotSuffix []astDotSuffix `parser:"( DOT @@ )*"`
}

View file

@ -6,6 +6,8 @@ import (
"fmt"
"strconv"
"strings"
"github.com/alecthomas/participle/v2/lexer"
)
func echoBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
@ -509,7 +511,10 @@ func lenBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return IntObject(0), nil
}
func indexLookup(ctx context.Context, obj, elem Object) (Object, error) {
func indexLookup(ctx context.Context, obj, elem Object, pos lexer.Position) (Object, error) {
if obj == nil {
return nil, nil
}
switch v := obj.(type) {
case Listable:
intIdx, ok := elem.(IntObject)
@ -525,9 +530,11 @@ func indexLookup(ctx context.Context, obj, elem Object) (Object, error) {
case Hashable:
strIdx, ok := elem.(StringObject)
if !ok {
return nil, errors.New("expected string for Hashable")
return nil, nil
}
return v.Value(string(strIdx)), nil
default:
return nil, notIndexableError(pos)
}
return nil, nil
}
@ -539,7 +546,7 @@ func indexBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
val := args.args[0]
for _, idx := range args.args[1:] {
newVal, err := indexLookup(ctx, val, idx)
newVal, err := indexLookup(ctx, val, idx, lexer.Position{})
if err != nil {
return nil, err
}

View file

@ -5,9 +5,10 @@ import (
"context"
"errors"
"fmt"
"github.com/stretchr/testify/assert"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
type testIterator struct {
@ -57,6 +58,19 @@ func WithTestBuiltin() InstOption {
return &a, nil
}))
i.rootEC.addCmd("rearrange", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) {
var as ListObject = make([]Object, 0)
for _, a := range args.args {
vs, ok := args.kwargs[a.String()]
if ok {
as = append(as, vs.Index(0))
}
}
return &as, nil
}))
i.rootEC.addCmd("error", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) == 0 {
return nil, errors.New("an error occurred")
@ -131,10 +145,10 @@ func TestBuiltins_Echo(t *testing.T) {
echo "world" # command after this
;
`, want: "Hello\nworld\n"},
{desc: "interpolated string 1", expr: `$what = "world" ; echo "Hello, $what"`, want: "Hello, world\n"},
{desc: "interpolated string 2", expr: `$what = "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"},
{desc: "interpolated string 1", expr: `what = "world" ; echo "Hello, $what"`, want: "Hello, world\n"},
{desc: "interpolated string 2", expr: `what = "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"},
{desc: "interpolated string 3", expr: `echo "separate\nlines\n\tand tabs"`, want: "separate\nlines\n\tand tabs\n"},
{desc: "interpolated string 4", expr: `$what = "Hello" ; $where = "world" ; echo "$what, $where"`, want: "Hello, world\n"},
{desc: "interpolated string 4", expr: `what = "Hello" ; where = "world" ; echo "$what, $where"`, want: "Hello, world\n"},
{desc: "interpolated string 5", expr: `
for [123 "foo" true ()] { |x|
echo "[[$x]]"
@ -167,19 +181,19 @@ func TestBuiltins_If(t *testing.T) {
want string
}{
{desc: "single then", expr: `
$x = "Hello"
x = "Hello"
if $x {
echo "true"
}`, want: "true\n(nil)\n"},
{desc: "single then and else", expr: `
$x = "Hello"
x = "Hello"
if $x {
echo "true"
} else {
echo "false"
}`, want: "true\n(nil)\n"},
{desc: "single then, elif and else", expr: `
$x = "Hello"
x = "Hello"
if $y {
echo "y is true"
} elif $x {
@ -188,14 +202,14 @@ func TestBuiltins_If(t *testing.T) {
echo "nothings x"
}`, want: "x is true\n(nil)\n"},
{desc: "single then and elif, no else", expr: `
$x = "Hello"
x = "Hello"
if $y {
echo "y is true"
} elif $x {
echo "x is true"
}`, want: "x is true\n(nil)\n"},
{desc: "single then, two elif, and else", expr: `
$x = "Hello"
x = "Hello"
if $z {
echo "z is true"
} elif $y {
@ -213,15 +227,15 @@ func TestBuiltins_If(t *testing.T) {
} else {
echo "none is true"
}`, want: "none is true\n(nil)\n"},
{desc: "compressed then", expr: `$x = "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"},
{desc: "compressed then", expr: `x = "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"},
{desc: "compressed else", expr: `if $x { echo "true" } else { echo "false" }`, want: "false\n(nil)\n"},
{desc: "compressed if", expr: `if $x { echo "x" } elif $y { echo "y" } else { echo "false" }`, want: "false\n(nil)\n"},
{desc: "if of itr 1", expr: `$i = itr ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 2", expr: `$i = itr ; for (seq 1) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 3", expr: `$i = itr ; for (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"},
{desc: "if of itr 4", expr: `$i = (itr | map { |x| add 2 $x }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 5", expr: `$i = (itr | filter { |x| () }) ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"},
{desc: "if of itr 6", expr: `$i = (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 1", expr: `i = itr ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 2", expr: `i = itr ; for (seq 1) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 3", expr: `i = itr ; for (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"},
{desc: "if of itr 4", expr: `i = (itr | map { |x| add 2 $x }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
{desc: "if of itr 5", expr: `i = (itr | filter { |x| () }) ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"},
{desc: "if of itr 6", expr: `i = (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
}
for _, tt := range tests {
@ -508,6 +522,9 @@ func TestBuiltins_Procs(t *testing.T) {
echo (call $er ["xxx"])
echo (call $er ["yyy"])
`, want: "Xxxx\nXxxxyyy\n(nil)\n"},
{desc: "calling with kwargs", expr: `
echo (call rearrange [b a] [a:"ey" b:"bee"])
`, want: "[bee ey]\n(nil)\n"},
}
for _, tt := range tests {

View file

@ -3,6 +3,7 @@ package ucl
import (
"errors"
"fmt"
"github.com/alecthomas/participle/v2/lexer"
)
@ -12,6 +13,7 @@ var (
var (
tooManyFinallyBlocksError = newBadUsage("try needs at most 1 finally")
notIndexableError = newBadUsage("index only support on lists and hashes")
)
type errorWithPos struct {

View file

@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"strings"
"github.com/alecthomas/participle/v2/lexer"
)
type evaluator struct {
@ -205,7 +207,7 @@ func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (Object,
}
}
res, err = indexLookup(ctx, res, idx)
res, err = indexLookup(ctx, res, idx, n.Pos)
if err != nil {
return nil, err
}
@ -258,9 +260,11 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec
func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVal Object) (Object, error) {
switch {
case n.Ident != nil:
ec.setOrDefineVar(n.Ident.String(), toVal)
return toVal, nil
case n.Literal != nil:
// We may use this for variable setting?
return nil, errors.New("cannot assign to a literal")
return nil, errors.New("cannot assign to a literal value")
case n.Var != nil:
ec.setOrDefineVar(*n.Var, toVal)
return toVal, nil
@ -428,7 +432,7 @@ func (e evaluator) interpolateLongIdent(ctx context.Context, ec *evalCtx, n *ast
}
}
res, err = indexLookup(ctx, res, idx)
res, err = indexLookup(ctx, res, idx, lexer.Position{})
if err != nil {
return "", err
}

View file

@ -4,10 +4,12 @@ import (
"bytes"
"context"
"strings"
"ucl.lmika.dev/ucl"
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/assert"
)
func TestInst_Eval(t *testing.T) {
@ -16,6 +18,7 @@ func TestInst_Eval(t *testing.T) {
expr string
want any
wantObj bool
wantAnErr bool
wantErr error
}{
{desc: "simple string", expr: `firstarg "hello"`, want: "hello"},
@ -100,7 +103,14 @@ func TestInst_Eval(t *testing.T) {
{desc: "dot idents 6", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil},
{desc: "dot idents 7", expr: `$x = [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"},
{desc: "dot idents 8", expr: `$x = [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"},
{desc: "dot idents 9", expr: `$x = [MORE:"stuff"] ; x.y`, want: nil},
{desc: "dot idents 9", expr: `$x = [MORE:"stuff"] ; $x.y`, want: nil},
{desc: "dot err 1", expr: `$x = [1 2 3] ; $x.Hello`, want: nil},
{desc: "dot err 2", expr: `$x = [1 2 3] ; $x.("world")`, want: nil},
{desc: "dot err 4", expr: `$x = [a:1 b:2] ; $x.(5)`, want: nil},
{desc: "dot err 3", expr: `$x = [a:1 b:2] ; $x.(0)`, want: nil},
{desc: "dot err 5", expr: `$x = 123 ; $x.(5)`, wantAnErr: true},
{desc: "dot err 6", expr: `$x = 123 ; $x.Five`, wantAnErr: true},
{desc: "parse comments 1", expr: parseComments1, wantObj: true, wantErr: nil},
{desc: "parse comments 2", expr: parseComments2, wantObj: true, wantErr: nil},
@ -118,6 +128,8 @@ func TestInst_Eval(t *testing.T) {
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
} else if tt.wantAnErr {
assert.Error(t, err)
} else if tt.wantObj {
assert.NoError(t, err)
_, isObj := res.(ucl.Object)