Skip to content

Commit d30814e

Browse files
authored
Merge branch 'master' into support-deref-in-func
2 parents 87ad32c + cae6003 commit d30814e

File tree

15 files changed

+214
-164
lines changed

15 files changed

+214
-164
lines changed

.github/workflows/build.yml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: build
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
go-versions: [ '1.18', '1.22' ]
15+
go-arch: [ '386' ]
16+
steps:
17+
- uses: actions/checkout@v3
18+
- name: Setup Go ${{ matrix.go-version }}
19+
uses: actions/setup-go@v4
20+
with:
21+
go-version: ${{ matrix.go-version }}
22+
- name: Build
23+
run: GOARCH=${{ matrix.go-arch }} go build

compiler/compiler.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,7 @@ func (c *compiler) IntegerNode(node *ast.IntegerNode) {
345345
}
346346
c.emitPush(int32(node.Value))
347347
case reflect.Int64:
348-
if node.Value > math.MaxInt64 || node.Value < math.MinInt64 {
349-
panic(fmt.Sprintf("constant %d overflows int64", node.Value))
350-
}
348+
panic(fmt.Sprintf("constant %d overflows int64", node.Value))
351349
c.emitPush(int64(node.Value))
352350
case reflect.Uint:
353351
if node.Value < 0 {
@@ -365,7 +363,7 @@ func (c *compiler) IntegerNode(node *ast.IntegerNode) {
365363
}
366364
c.emitPush(uint16(node.Value))
367365
case reflect.Uint32:
368-
if node.Value > math.MaxUint32 || node.Value < 0 {
366+
if node.Value < 0 {
369367
panic(fmt.Sprintf("constant %d overflows uint32", node.Value))
370368
}
371369
c.emitPush(uint32(node.Value))

expr_test.go

+46-1
Original file line numberDiff line numberDiff line change
@@ -1623,7 +1623,10 @@ func TestCompile_exposed_error(t *testing.T) {
16231623

16241624
b, err := json.Marshal(err)
16251625
require.NoError(t, err)
1626-
require.Equal(t, `{"Line":1,"Column":2,"Message":"invalid operation: == (mismatched types int and bool)","Snippet":"\n | 1 == true\n | ..^","Prev":null}`, string(b))
1626+
require.Equal(t,
1627+
`{"from":2,"to":4,"line":1,"column":2,"message":"invalid operation: == (mismatched types int and bool)","snippet":"\n | 1 == true\n | ..^","prev":null}`,
1628+
string(b),
1629+
)
16271630
}
16281631

16291632
func TestAsBool_exposed_error(t *testing.T) {
@@ -2667,3 +2670,45 @@ func TestIssue_integer_truncated_by_compiler(t *testing.T) {
26672670
_, err = expr.Compile("fn(256)", expr.Env(env))
26682671
require.Error(t, err)
26692672
}
2673+
2674+
func TestExpr_crash(t *testing.T) {
2675+
content, err := os.ReadFile("testdata/crash.txt")
2676+
require.NoError(t, err)
2677+
2678+
_, err = expr.Compile(string(content))
2679+
require.Error(t, err)
2680+
}
2681+
2682+
func TestExpr_nil_op_str(t *testing.T) {
2683+
// Let's test operators, which do `.(string)` in VM, also check for nil.
2684+
2685+
var str *string = nil
2686+
env := map[string]any{
2687+
"nilString": str,
2688+
}
2689+
2690+
tests := []struct{ code string }{
2691+
{`nilString == "str"`},
2692+
{`nilString contains "str"`},
2693+
{`nilString matches "str"`},
2694+
{`nilString startsWith "str"`},
2695+
{`nilString endsWith "str"`},
2696+
2697+
{`"str" == nilString`},
2698+
{`"str" contains nilString`},
2699+
{`"str" matches nilString`},
2700+
{`"str" startsWith nilString`},
2701+
{`"str" endsWith nilString`},
2702+
}
2703+
2704+
for _, tt := range tests {
2705+
t.Run(tt.code, func(t *testing.T) {
2706+
program, err := expr.Compile(tt.code)
2707+
require.NoError(t, err)
2708+
2709+
output, err := expr.Run(program, env)
2710+
require.NoError(t, err)
2711+
require.Equal(t, false, output)
2712+
})
2713+
}
2714+
}

file/error.go

+21-7
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,36 @@ import (
88

99
type Error struct {
1010
Location
11-
Message string
12-
Snippet string
13-
Prev error
11+
Line int `json:"line"`
12+
Column int `json:"column"`
13+
Message string `json:"message"`
14+
Snippet string `json:"snippet"`
15+
Prev error `json:"prev"`
1416
}
1517

1618
func (e *Error) Error() string {
1719
return e.format()
1820
}
1921

20-
func (e *Error) Bind(source *Source) *Error {
21-
if snippet, found := source.Snippet(e.Location.Line); found {
22+
func (e *Error) Bind(source Source) *Error {
23+
e.Line = 1
24+
for i, r := range source {
25+
if i == e.From {
26+
break
27+
}
28+
if r == '\n' {
29+
e.Line++
30+
e.Column = 0
31+
} else {
32+
e.Column++
33+
}
34+
}
35+
if snippet, found := source.Snippet(e.Line); found {
2236
snippet := strings.Replace(snippet, "\t", " ", -1)
2337
srcLine := "\n | " + snippet
2438
var bytes = []byte(snippet)
2539
var indLine = "\n | "
26-
for i := 0; i < e.Location.Column && len(bytes) > 0; i++ {
40+
for i := 0; i < e.Column && len(bytes) > 0; i++ {
2741
_, sz := utf8.DecodeRune(bytes)
2842
bytes = bytes[sz:]
2943
if sz > 1 {
@@ -54,7 +68,7 @@ func (e *Error) Wrap(err error) {
5468
}
5569

5670
func (e *Error) format() string {
57-
if e.Location.Empty() {
71+
if e.Snippet == "" {
5872
return e.Message
5973
}
6074
return fmt.Sprintf(

file/location.go

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
package file
22

33
type Location struct {
4-
Line int // The 1-based line of the location.
5-
Column int // The 0-based column number of the location.
6-
}
7-
8-
func (l Location) Empty() bool {
9-
return l.Column == 0 && l.Line == 0
4+
From int `json:"from"`
5+
To int `json:"to"`
106
}

file/source.go

+21-52
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,47 @@
11
package file
22

33
import (
4-
"encoding/json"
54
"strings"
65
"unicode/utf8"
76
)
87

9-
type Source struct {
10-
contents []rune
11-
lineOffsets []int32
12-
}
13-
14-
func NewSource(contents string) *Source {
15-
s := &Source{
16-
contents: []rune(contents),
17-
}
18-
s.updateOffsets()
19-
return s
20-
}
21-
22-
func (s *Source) MarshalJSON() ([]byte, error) {
23-
return json.Marshal(s.contents)
24-
}
25-
26-
func (s *Source) UnmarshalJSON(b []byte) error {
27-
contents := make([]rune, 0)
28-
err := json.Unmarshal(b, &contents)
29-
if err != nil {
30-
return err
31-
}
8+
type Source []rune
329

33-
s.contents = contents
34-
s.updateOffsets()
35-
return nil
10+
func NewSource(contents string) Source {
11+
return []rune(contents)
3612
}
3713

38-
func (s *Source) Content() string {
39-
return string(s.contents)
14+
func (s Source) String() string {
15+
return string(s)
4016
}
4117

42-
func (s *Source) Snippet(line int) (string, bool) {
18+
func (s Source) Snippet(line int) (string, bool) {
4319
if s == nil {
4420
return "", false
4521
}
46-
charStart, found := s.findLineOffset(line)
47-
if !found || len(s.contents) == 0 {
22+
lines := strings.Split(string(s), "\n")
23+
lineOffsets := make([]int, len(lines))
24+
var offset int
25+
for i, line := range lines {
26+
offset = offset + utf8.RuneCountInString(line) + 1
27+
lineOffsets[i] = offset
28+
}
29+
charStart, found := getLineOffset(lineOffsets, line)
30+
if !found || len(s) == 0 {
4831
return "", false
4932
}
50-
charEnd, found := s.findLineOffset(line + 1)
33+
charEnd, found := getLineOffset(lineOffsets, line+1)
5134
if found {
52-
return string(s.contents[charStart : charEnd-1]), true
53-
}
54-
return string(s.contents[charStart:]), true
55-
}
56-
57-
// updateOffsets compute line offsets up front as they are referred to frequently.
58-
func (s *Source) updateOffsets() {
59-
lines := strings.Split(string(s.contents), "\n")
60-
offsets := make([]int32, len(lines))
61-
var offset int32
62-
for i, line := range lines {
63-
offset = offset + int32(utf8.RuneCountInString(line)) + 1
64-
offsets[int32(i)] = offset
35+
return string(s[charStart : charEnd-1]), true
6536
}
66-
s.lineOffsets = offsets
37+
return string(s[charStart:]), true
6738
}
6839

69-
// findLineOffset returns the offset where the (1-indexed) line begins,
70-
// or false if line doesn't exist.
71-
func (s *Source) findLineOffset(line int) (int32, bool) {
40+
func getLineOffset(lineOffsets []int, line int) (int, bool) {
7241
if line == 1 {
7342
return 0, true
74-
} else if line > 1 && line <= len(s.lineOffsets) {
75-
offset := s.lineOffsets[line-2]
43+
} else if line > 1 && line <= len(lineOffsets) {
44+
offset := lineOffsets[line-2]
7645
return offset, true
7746
}
7847
return -1, false

file/source_test.go

-15
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package file
22

33
import (
4-
"encoding/json"
54
"testing"
6-
7-
"github.com/expr-lang/expr/internal/testify/assert"
85
)
96

107
const (
@@ -55,15 +52,3 @@ func TestStringSource_SnippetSingleLine(t *testing.T) {
5552
t.Errorf(unexpectedSnippet, t.Name(), str2, "")
5653
}
5754
}
58-
59-
func TestStringSource_MarshalJSON(t *testing.T) {
60-
source := NewSource("hello, world")
61-
encoded, err := json.Marshal(source)
62-
assert.NoError(t, err)
63-
assert.Equal(t, `[104,101,108,108,111,44,32,119,111,114,108,100]`, string(encoded))
64-
65-
decoded := &Source{}
66-
err = json.Unmarshal(encoded, decoded)
67-
assert.NoError(t, err)
68-
assert.Equal(t, source.Content(), decoded.Content())
69-
}

0 commit comments

Comments
 (0)