Skip to content

Commit cf006b6

Browse files
committed
fix(checker): support optional chaining on slice operations
Allow nil?.[from:to] to return nil instead of erroring with "cannot slice unknown". Add Optional field to SliceNode, propagate it through parser, checker, and compiler using the same pattern as MemberNode optional chaining. Fixes #822
1 parent b90e77c commit cf006b6

6 files changed

Lines changed: 84 additions & 12 deletions

File tree

ast/node.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,10 @@ type MemberNode struct {
168168
// array[1:4]
169169
type SliceNode struct {
170170
base
171-
Node Node // Node of the slice. Like "array" in "array[1:4]".
172-
From Node // From an index of the array. Like "1" in "array[1:4]".
173-
To Node // To an index of the array. Like "4" in "array[1:4]".
171+
Node Node // Node of the slice. Like "array" in "array[1:4]".
172+
From Node // From an index of the array. Like "1" in "array[1:4]".
173+
To Node // To an index of the array. Like "4" in "array[1:4]".
174+
Optional bool // If true then the slice access is optional. Like "foo?.[1:4]".
174175
}
175176

176177
// CallNode represents a function or a method call.

ast/print.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,16 +162,20 @@ func (n *MemberNode) String() string {
162162
}
163163

164164
func (n *SliceNode) String() string {
165+
op := ""
166+
if n.Optional {
167+
op = "?."
168+
}
165169
if n.From == nil && n.To == nil {
166-
return fmt.Sprintf("%s[:]", n.Node.String())
170+
return fmt.Sprintf("%s%s[:]", n.Node.String(), op)
167171
}
168172
if n.From == nil {
169-
return fmt.Sprintf("%s[:%s]", n.Node.String(), n.To.String())
173+
return fmt.Sprintf("%s%s[:%s]", n.Node.String(), op, n.To.String())
170174
}
171175
if n.To == nil {
172-
return fmt.Sprintf("%s[%s:]", n.Node.String(), n.From.String())
176+
return fmt.Sprintf("%s%s[%s:]", n.Node.String(), op, n.From.String())
173177
}
174-
return fmt.Sprintf("%s[%s:%s]", n.Node.String(), n.From.String(), n.To.String())
178+
return fmt.Sprintf("%s%s[%s:%s]", n.Node.String(), op, n.From.String(), n.To.String())
175179
}
176180

177181
func (n *CallNode) String() string {

checker/checker.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,9 @@ func (v *Checker) sliceNode(node *ast.SliceNode) Nature {
613613
case reflect.String, reflect.Array, reflect.Slice:
614614
// ok
615615
default:
616+
if node.Optional {
617+
return Nature{}
618+
}
616619
return v.error(node, "cannot slice %s", nt.String())
617620
}
618621

compiler/compiler.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,10 @@ func (c *compiler) MemberNode(node *ast.MemberNode) {
760760

761761
func (c *compiler) SliceNode(node *ast.SliceNode) {
762762
c.compile(node.Node)
763+
if node.Optional && len(c.chains) > 0 {
764+
ph := c.emit(OpJumpIfNil, placeholder)
765+
c.chains[len(c.chains)-1] = append(c.chains[len(c.chains)-1], ph)
766+
}
763767
if node.To != nil {
764768
c.compile(node.To)
765769
c.derefInNeeded(node.To)

parser/parser.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -848,12 +848,19 @@ func (p *Parser) parsePostfixExpression(node Node) Node {
848848
}
849849

850850
node = p.createNode(&SliceNode{
851-
Node: node,
852-
To: to,
851+
Node: node,
852+
To: to,
853+
Optional: optional,
853854
}, postfixToken.Location)
854855
if node == nil {
855856
return nil
856857
}
858+
if optional {
859+
node = p.createNode(&ChainNode{Node: node}, postfixToken.Location)
860+
if node == nil {
861+
return nil
862+
}
863+
}
857864
p.expect(Bracket, "]")
858865

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

870877
node = p.createNode(&SliceNode{
871-
Node: node,
872-
From: from,
873-
To: to,
878+
Node: node,
879+
From: from,
880+
To: to,
881+
Optional: optional,
874882
}, postfixToken.Location)
875883
if node == nil {
876884
return nil
877885
}
886+
if optional {
887+
node = p.createNode(&ChainNode{Node: node}, postfixToken.Location)
888+
if node == nil {
889+
return nil
890+
}
891+
}
878892
p.expect(Bracket, "]")
879893

880894
} else {

test/issues/822/issue_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package issue_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/expr-lang/expr"
7+
"github.com/expr-lang/expr/internal/testify/require"
8+
)
9+
10+
func TestIssue822(t *testing.T) {
11+
t.Run("nil optional slice returns nil", func(t *testing.T) {
12+
program, err := expr.Compile(`let x = nil; x?.[0:1]`)
13+
require.NoError(t, err)
14+
15+
out, err := expr.Run(program, nil)
16+
require.NoError(t, err)
17+
require.Nil(t, out)
18+
})
19+
20+
t.Run("non-nil optional slice works normally", func(t *testing.T) {
21+
program, err := expr.Compile(`let x = [1, 2, 3]; x?.[0:2]`)
22+
require.NoError(t, err)
23+
24+
out, err := expr.Run(program, nil)
25+
require.NoError(t, err)
26+
require.Equal(t, []interface{}{1, 2}, out)
27+
})
28+
29+
t.Run("nil optional slice without from", func(t *testing.T) {
30+
program, err := expr.Compile(`let x = nil; x?.[:1]`)
31+
require.NoError(t, err)
32+
33+
out, err := expr.Run(program, nil)
34+
require.NoError(t, err)
35+
require.Nil(t, out)
36+
})
37+
38+
t.Run("nil optional slice without to", func(t *testing.T) {
39+
program, err := expr.Compile(`let x = nil; x?.[1:]`)
40+
require.NoError(t, err)
41+
42+
out, err := expr.Run(program, nil)
43+
require.NoError(t, err)
44+
require.Nil(t, out)
45+
})
46+
}

0 commit comments

Comments
 (0)