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 { type astDot struct {
Pos lexer.Position
Arg astCmdArg `parser:"@@"` Arg astCmdArg `parser:"@@"`
DotSuffix []astDotSuffix `parser:"( DOT @@ )*"` DotSuffix []astDotSuffix `parser:"( DOT @@ )*"`
} }

View file

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

View file

@ -5,9 +5,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/stretchr/testify/assert"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
type testIterator struct { type testIterator struct {
@ -57,6 +58,19 @@ func WithTestBuiltin() InstOption {
return &a, nil 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) { i.rootEC.addCmd("error", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) == 0 { if len(args.args) == 0 {
return nil, errors.New("an error occurred") return nil, errors.New("an error occurred")
@ -131,10 +145,10 @@ func TestBuiltins_Echo(t *testing.T) {
echo "world" # command after this echo "world" # command after this
; ;
`, want: "Hello\nworld\n"}, `, want: "Hello\nworld\n"},
{desc: "interpolated string 1", expr: `$what = "world" ; echo "Hello, $what"`, want: "Hello, world\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 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 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: ` {desc: "interpolated string 5", expr: `
for [123 "foo" true ()] { |x| for [123 "foo" true ()] { |x|
echo "[[$x]]" echo "[[$x]]"
@ -167,19 +181,19 @@ func TestBuiltins_If(t *testing.T) {
want string want string
}{ }{
{desc: "single then", expr: ` {desc: "single then", expr: `
$x = "Hello" x = "Hello"
if $x { if $x {
echo "true" echo "true"
}`, want: "true\n(nil)\n"}, }`, want: "true\n(nil)\n"},
{desc: "single then and else", expr: ` {desc: "single then and else", expr: `
$x = "Hello" x = "Hello"
if $x { if $x {
echo "true" echo "true"
} else { } else {
echo "false" echo "false"
}`, want: "true\n(nil)\n"}, }`, want: "true\n(nil)\n"},
{desc: "single then, elif and else", expr: ` {desc: "single then, elif and else", expr: `
$x = "Hello" x = "Hello"
if $y { if $y {
echo "y is true" echo "y is true"
} elif $x { } elif $x {
@ -188,14 +202,14 @@ func TestBuiltins_If(t *testing.T) {
echo "nothings x" echo "nothings x"
}`, want: "x is true\n(nil)\n"}, }`, want: "x is true\n(nil)\n"},
{desc: "single then and elif, no else", expr: ` {desc: "single then and elif, no else", expr: `
$x = "Hello" x = "Hello"
if $y { if $y {
echo "y is true" echo "y is true"
} elif $x { } elif $x {
echo "x is true" echo "x is true"
}`, want: "x is true\n(nil)\n"}, }`, want: "x is true\n(nil)\n"},
{desc: "single then, two elif, and else", expr: ` {desc: "single then, two elif, and else", expr: `
$x = "Hello" x = "Hello"
if $z { if $z {
echo "z is true" echo "z is true"
} elif $y { } elif $y {
@ -213,15 +227,15 @@ func TestBuiltins_If(t *testing.T) {
} else { } else {
echo "none is true" echo "none is true"
}`, want: "none is true\n(nil)\n"}, }`, 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 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: "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 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 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 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 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 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 6", expr: `i = (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"},
} }
for _, tt := range tests { for _, tt := range tests {
@ -508,6 +522,9 @@ func TestBuiltins_Procs(t *testing.T) {
echo (call $er ["xxx"]) echo (call $er ["xxx"])
echo (call $er ["yyy"]) echo (call $er ["yyy"])
`, want: "Xxxx\nXxxxyyy\n(nil)\n"}, `, 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 { for _, tt := range tests {

View file

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

View file

@ -5,6 +5,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"github.com/alecthomas/participle/v2/lexer"
) )
type evaluator struct { 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 { if err != nil {
return nil, err 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) { func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVal Object) (Object, error) {
switch { switch {
case n.Ident != nil:
ec.setOrDefineVar(n.Ident.String(), toVal)
return toVal, nil
case n.Literal != nil: case n.Literal != nil:
// We may use this for variable setting? return nil, errors.New("cannot assign to a literal value")
return nil, errors.New("cannot assign to a literal")
case n.Var != nil: case n.Var != nil:
ec.setOrDefineVar(*n.Var, toVal) ec.setOrDefineVar(*n.Var, toVal)
return toVal, nil 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 { if err != nil {
return "", err return "", err
} }

View file

@ -4,19 +4,22 @@ import (
"bytes" "bytes"
"context" "context"
"strings" "strings"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestInst_Eval(t *testing.T) { func TestInst_Eval(t *testing.T) {
tests := []struct { tests := []struct {
desc string desc string
expr string expr string
want any want any
wantObj bool wantObj bool
wantErr error wantAnErr bool
wantErr error
}{ }{
{desc: "simple string", expr: `firstarg "hello"`, want: "hello"}, {desc: "simple string", expr: `firstarg "hello"`, want: "hello"},
{desc: "simple int 1", expr: `firstarg 123`, want: 123}, {desc: "simple int 1", expr: `firstarg 123`, want: 123},
@ -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 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 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 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 1", expr: parseComments1, wantObj: true, wantErr: nil},
{desc: "parse comments 2", expr: parseComments2, 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 { if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr) assert.ErrorIs(t, err, tt.wantErr)
} else if tt.wantAnErr {
assert.Error(t, err)
} else if tt.wantObj { } else if tt.wantObj {
assert.NoError(t, err) assert.NoError(t, err)
_, isObj := res.(ucl.Object) _, isObj := res.(ucl.Object)