Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions ast/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,10 @@ type MemberNode struct {
// array[1:4]
type SliceNode struct {
base
Node Node // Node of the slice. Like "array" in "array[1:4]".
From Node // From an index of the array. Like "1" in "array[1:4]".
To Node // To an index of the array. Like "4" in "array[1:4]".
Node Node // Node of the slice. Like "array" in "array[1:4]".
From Node // From an index of the array. Like "1" in "array[1:4]".
To Node // To an index of the array. Like "4" in "array[1:4]".
Optional bool // If true then the slice access is optional. Like "foo?.[1:4]".
}

// CallNode represents a function or a method call.
Expand Down
12 changes: 8 additions & 4 deletions ast/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,20 @@ func (n *MemberNode) String() string {
}

func (n *SliceNode) String() string {
op := ""
if n.Optional {
op = "?."
}
if n.From == nil && n.To == nil {
return fmt.Sprintf("%s[:]", n.Node.String())
return fmt.Sprintf("%s%s[:]", n.Node.String(), op)
}
if n.From == nil {
return fmt.Sprintf("%s[:%s]", n.Node.String(), n.To.String())
return fmt.Sprintf("%s%s[:%s]", n.Node.String(), op, n.To.String())
}
if n.To == nil {
return fmt.Sprintf("%s[%s:]", n.Node.String(), n.From.String())
return fmt.Sprintf("%s%s[%s:]", n.Node.String(), op, n.From.String())
}
return fmt.Sprintf("%s[%s:%s]", n.Node.String(), n.From.String(), n.To.String())
return fmt.Sprintf("%s%s[%s:%s]", n.Node.String(), op, n.From.String(), n.To.String())
}

func (n *CallNode) String() string {
Expand Down
4 changes: 4 additions & 0 deletions ast/print_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ func TestPrint(t *testing.T) {
{`a[1:]`, `a[1:]`},
{`a[1:]`, `a[1:]`},
{`a[:]`, `a[:]`},
{`a?.[1:-1]`, `a?.[1:-1]`},
{`a?.[1:]`, `a?.[1:]`},
{`a?.[:1]`, `a?.[:1]`},
{`a?.[:]`, `a?.[:]`},
{`(nil ?? 1) > 0`, `(nil ?? 1) > 0`},
{`{("a" + "b"): 42}`, `{("a" + "b"): 42}`},
{`(One == 1 ? true : false) && Two == 2`, `(One == 1 ? true : false) && Two == 2`},
Expand Down
7 changes: 6 additions & 1 deletion checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,12 @@ func (v *Checker) sliceNode(node *ast.SliceNode) Nature {
case reflect.String, reflect.Array, reflect.Slice:
// ok
default:
return v.error(node, "cannot slice %s", nt.String())
// Optional chaining only guards against a nil value, so it must not
// hide the error for a type that can never be sliced.
if !node.Optional || !nt.Nil {
return v.error(node, "cannot slice %s", nt.String())
}
nt = Nature{}
}

if node.From != nil {
Expand Down
6 changes: 6 additions & 0 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,12 @@ func (c *compiler) MemberNode(node *ast.MemberNode) {

func (c *compiler) SliceNode(node *ast.SliceNode) {
c.compile(node.Node)
// If the slice is optional, we need to jump over the slice operation.
// If no ChainNode (none c.chains) is used, do not compile the optional slice.
if node.Optional && len(c.chains) > 0 {
ph := c.emit(OpJumpIfNil, placeholder)
c.chains[len(c.chains)-1] = append(c.chains[len(c.chains)-1], ph)
}
if node.To != nil {
c.compile(node.To)
c.derefInNeeded(node.To)
Expand Down
24 changes: 19 additions & 5 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -848,12 +848,19 @@ func (p *Parser) parsePostfixExpression(node Node) Node {
}

node = p.createNode(&SliceNode{
Node: node,
To: to,
Node: node,
To: to,
Optional: optional,
}, postfixToken.Location)
if node == nil {
return nil
}
if optional {
node = p.createNode(&ChainNode{Node: node}, postfixToken.Location)
if node == nil {
return nil
}
}
p.expect(Bracket, "]")

} else {
Expand All @@ -868,13 +875,20 @@ func (p *Parser) parsePostfixExpression(node Node) Node {
}

node = p.createNode(&SliceNode{
Node: node,
From: from,
To: to,
Node: node,
From: from,
To: to,
Optional: optional,
}, postfixToken.Location)
if node == nil {
return nil
}
if optional {
node = p.createNode(&ChainNode{Node: node}, postfixToken.Location)
if node == nil {
return nil
}
}
p.expect(Bracket, "]")

} else {
Expand Down
21 changes: 21 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,27 @@ func TestParse_optional_chaining(t *testing.T) {
},
},
},
{
"array?.[1:2]",
&ChainNode{
Node: &SliceNode{
Node: &IdentifierNode{Value: "array"},
From: &IntegerNode{Value: 1},
To: &IntegerNode{Value: 2},
Optional: true,
},
},
},
{
"array?.[:2]",
&ChainNode{
Node: &SliceNode{
Node: &IdentifierNode{Value: "array"},
To: &IntegerNode{Value: 2},
Optional: true,
},
},
},
{
"!foo?.bar.baz",
&UnaryNode{
Expand Down
81 changes: 81 additions & 0 deletions test/issues/822/issue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package issue_test

import (
"testing"

"github.com/expr-lang/expr"
"github.com/expr-lang/expr/internal/testify/require"
)

func TestIssue822(t *testing.T) {
var tests = []struct {
input string
want any
err string
}{
{
input: `let x = nil; x?.[0:1]`,
},
{
input: `let x = nil; x?.[:1]`,
},
{
input: `let x = nil; x?.[1:]`,
},
{
input: `let x = [1, 2, 3]; x?.[0:2]`,
want: []any{1, 2},
},
{
input: `let x = "test"; x?.[5:10]`,
want: "",
},
{
input: `let x = 1; x?.[0:1]`,
err: "cannot slice int",
},
{
input: `let x = 1.5; x?.[0:1]`,
err: "cannot slice float64",
},
{
input: `let x = true; x?.[0:1]`,
err: "cannot slice bool",
},
{
input: `let x = {a: 1}; x?.[0:1]`,
err: "cannot slice map[string]interface {}",
},
{
input: `let x = nil; x?.[true:false]`,
err: "non-integer slice index bool",
},
}

for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
program, err := expr.Compile(tt.input)

if tt.err != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.err)
} else {
require.NoError(t, err)
out, err := expr.Run(program, nil)
require.NoError(t, err)
require.Equal(t, tt.want, out)
}
})
}
}

func TestIssue822_nil_from_env(t *testing.T) {
env := map[string]any{"a": nil}

program, err := expr.Compile(`a?.[0:1]`, expr.Env(env))
require.NoError(t, err)

out, err := expr.Run(program, env)
require.NoError(t, err)
require.Nil(t, out)
}