Skip to content

Commit e33f36c

Browse files
committed
feat: regexp engine configuration
Add support for ECMAScript compatible regular expression engine via regexp2 using functional options e.g. vm := New(RegExp(regexp2.Creator{})) This also adds function option support for the existing options: * Debugger - Deprecates: SetDebuggerHandler * RandomSource - Deprecates: SetRandomSource. * Stack - Deprecates: SetStackLimit. * StackTrace - Deprecates: SetStackTraceLimit.
1 parent f142365 commit e33f36c

15 files changed

Lines changed: 682 additions & 42 deletions

File tree

‎builtin_string.go‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package otto
22

33
import (
44
"bytes"
5-
"regexp"
5+
restd "regexp"
66
"strconv"
77
"strings"
88
"unicode/utf16"
99
"unicode/utf8"
10+
11+
"github.com/robertkrimen/otto/regexp"
1012
)
1113

1214
// String
@@ -170,7 +172,7 @@ func builtinStringMatch(call FunctionCall) Value {
170172
return objectValue(call.runtime.newArrayOf(valueArray))
171173
}
172174

173-
var builtinStringReplaceRegexp = regexp.MustCompile("\\$(?:[\\$\\&\\'\\`1-9]|0[1-9]|[1-9][0-9])")
175+
var builtinStringReplaceRegexp = restd.MustCompile("\\$(?:[\\$\\&\\'\\`1-9]|0[1-9]|[1-9][0-9])")
174176

175177
func builtinStringFindAndReplaceString(input []byte, lastIndex int, match []int, target []byte, replaceValue []byte) []byte {
176178
matchCount := len(match) / 2
@@ -215,7 +217,7 @@ func builtinStringReplace(call FunctionCall) Value {
215217
searchObject := searchValue.object()
216218

217219
// TODO If a capture is -1?
218-
var search *regexp.Regexp
220+
var search regexp.Finder
219221
global := false
220222
find := 1
221223
if searchValue.IsObject() && searchObject.class == classRegExpName {
@@ -226,7 +228,7 @@ func builtinStringReplace(call FunctionCall) Value {
226228
global = true
227229
}
228230
} else {
229-
search = regexp.MustCompile(regexp.QuoteMeta(searchValue.string()))
231+
search = call.runtime.regExp.MustCompile(restd.QuoteMeta(searchValue.string()))
230232
}
231233

232234
found := search.FindAllSubmatchIndex(target, find)

‎clone.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func (rt *runtime) clone() *runtime {
1717
defer rt.lck.Unlock()
1818

1919
out := &runtime{
20+
regExp: rt.regExp,
2021
debugger: rt.debugger,
2122
random: rt.random,
2223
stackLimit: rt.stackLimit,

‎go.mod‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/robertkrimen/otto
33
go 1.18
44

55
require (
6+
github.com/dlclark/regexp2 v1.7.0
67
github.com/stretchr/testify v1.8.1
78
golang.org/x/text v0.4.0
89
gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375

‎go.sum‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
55
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
66
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
77
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8+
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
9+
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
810
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
911
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1012
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

‎otto.go‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ import (
225225
"strings"
226226

227227
"github.com/robertkrimen/otto/file"
228+
"github.com/robertkrimen/otto/regexp"
228229
"github.com/robertkrimen/otto/registry"
229230
)
230231

@@ -237,11 +238,60 @@ type Otto struct {
237238
runtime *runtime
238239
}
239240

241+
// Option represents a Otto configuration option.
242+
type Option func(*Otto)
243+
244+
// Debugger is an option that sets the debugger handler to fn.
245+
func Debugger(fn func(vm *Otto)) Option {
246+
return func(o *Otto) {
247+
o.runtime.debugger = fn
248+
}
249+
}
250+
251+
// RandomSource is an option that sets the random source to fn.
252+
func RandomSource(fn func() float64) Option {
253+
return func(o *Otto) {
254+
o.runtime.random = fn
255+
}
256+
}
257+
258+
// Stack is an option that sets the stack depth to limit.
259+
// Default: 0 (unlimited).
260+
func Stack(limit int) Option {
261+
return func(o *Otto) {
262+
o.runtime.stackLimit = limit
263+
}
264+
}
265+
266+
// StackTrace is an option that sets the stack trace depth to limit.
267+
// Default: 10.
268+
func StackTrace(limit int) Option {
269+
return func(o *Otto) {
270+
o.runtime.traceLimit = limit
271+
}
272+
}
273+
274+
// RegExp is an option which sets the regular expression engine.
275+
// This can be used to enable ECMAScript compatible regular expression
276+
// processing using regexp2.Creator.
277+
// Default: standard regexp library using ECMA to RE2 transformation
278+
// which provides constant order matching but doesn't support backtracking.
279+
func RegExp(creator regexp.Creator) Option {
280+
return func(o *Otto) {
281+
o.runtime.regExp = creator
282+
}
283+
}
284+
240285
// New will allocate a new JavaScript runtime.
241-
func New() *Otto {
286+
func New(options ...Option) *Otto {
242287
o := &Otto{
243288
runtime: newContext(),
244289
}
290+
o.runtime.regExp = &regexpStd{}
291+
292+
for _, f := range options {
293+
f(o)
294+
}
245295
o.runtime.otto = o
246296
o.runtime.traceLimit = 10
247297
if err := o.Set("console", o.runtime.newConsole()); err != nil {
@@ -360,11 +410,13 @@ func (o Otto) setValue(name string, value Value) {
360410
}
361411

362412
// SetDebuggerHandler sets the debugger handler to fn.
413+
// Deprecated: Use Debugger option.
363414
func (o Otto) SetDebuggerHandler(fn func(vm *Otto)) {
364415
o.runtime.debugger = fn
365416
}
366417

367418
// SetRandomSource sets the random source to fn.
419+
// Deprecated: Use RandomSource option.
368420
func (o Otto) SetRandomSource(fn func() float64) {
369421
o.runtime.random = fn
370422
}
@@ -377,6 +429,7 @@ func (o Otto) SetRandomSource(fn func() float64) {
377429
// JavaScript makes a call to a Go function, otto won't keep track of what
378430
// happens outside the interpreter. So if your Go function is infinitely
379431
// recursive, you're still in trouble.
432+
// Deprecated: Use Stack option.
380433
func (o Otto) SetStackDepthLimit(limit int) {
381434
o.runtime.stackLimit = limit
382435
}
@@ -386,6 +439,7 @@ func (o Otto) SetStackDepthLimit(limit int) {
386439
// is 10. This is consistent with V8 and SpiderMonkey.
387440
//
388441
// TODO: expose via `Error.stackTraceLimit`.
442+
// Deprecated: Use StackTrace option.
389443
func (o Otto) SetStackTraceLimit(limit int) {
390444
o.runtime.traceLimit = limit
391445
}

‎parser/expression.go‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,21 +142,24 @@ func (p *_parser) parseRegExpLiteral() *ast.RegExpLiteral {
142142

143143
var value string
144144
// TODO 15.10
145-
// Test during parsing that this is a valid regular expression
146-
// Sorry, (?=) and (?!) are invalid (for now)
147-
pat, err := TransformRegExp(pattern)
148-
if err != nil {
149-
if pat == "" || p.mode&IgnoreRegExpErrors == 0 {
150-
p.error(idx, "Invalid regular expression: %s", err.Error())
151-
}
152-
} else {
153-
_, err = regexp.Compile(pat)
145+
if p.mode&NoRegExpTransform == 0 {
146+
// Test during parsing that this is a valid regular expression
147+
// Sorry, (?=) and (?!) are invalid (for now)
148+
pat, err := TransformRegExp(pattern)
154149
if err != nil {
155-
// We should not get here, ParseRegExp should catch any errors
156-
p.error(idx, "Invalid regular expression: %s", err.Error()[22:]) // Skip redundant "parse regexp error"
150+
if pat == "" || p.mode&IgnoreRegExpErrors == 0 {
151+
p.error(idx, "Invalid regular expression: %s", err.Error())
152+
}
157153
} else {
158-
value = pat
154+
if _, err = regexp.Compile(pat); err != nil {
155+
// We should not get here, ParseRegExp should catch any errors
156+
p.error(idx, "Invalid regular expression: %s", err.Error()[22:]) // Skip redundant "parse regexp error"
157+
} else {
158+
value = pat
159+
}
159160
}
161+
} else {
162+
value = pattern
160163
}
161164

162165
literal := p.str[offset:endOffset]

‎parser/parser.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ const (
5454

5555
// StoreComments stores the comments from source to the comments map.
5656
StoreComments
57+
58+
// NoRegExpTransform disables RegExp transformation.
59+
NoRegExpTransform
5760
)
5861

5962
type _parser struct { //nolint: maligned

‎parser/regexp.go‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ func TransformRegExp(pattern string) (string, error) {
3939
}
4040

4141
// TODO If without \, if without (?=, (?!, then another shortcut
42-
4342
parser := regExpParser{
4443
str: pattern,
4544
length: len(pattern),

‎regexp/interfaces.go‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Package regexp defines Regular Expression interfaces used for otto.
2+
package regexp
3+
4+
// Finder is implemented by types which can act as a regular expression finder.
5+
type Finder interface {
6+
// FindStringIndex returns a two-element slice of integers defining the location of the
7+
// leftmost match in s of the regular expression. The match itself is at s[loc[0]:loc[1]].
8+
// A return value of nil indicates no match.
9+
FindStringIndex(s string) []int
10+
11+
// FindAllStringIndex is the 'All' version of FindStringIndex; it returns a slice of all
12+
// successive matches of the expression, as defined by the 'All' description in the package
13+
// comment. A return value of nil indicates no match.
14+
FindAllStringIndex(s string, n int) [][]int
15+
16+
// FindStringSubmatchIndex returns a slice holding the index pairs identifying the leftmost
17+
// match of the regular expression in s and the matches, if any, of its sub expressions, as
18+
// defined by the 'Submatch' and 'Index' descriptions in the package comment. A return value
19+
// of nil indicates no match.
20+
FindStringSubmatchIndex(s string) []int
21+
22+
// FindAllStringSubmatchIndex is the 'All' version of FindStringSubmatchIndex; it returns a
23+
// slice of all successive matches of the expression, as defined by the 'All' description in
24+
// the package comment. A return value of nil indicates no match.
25+
FindAllStringSubmatchIndex(s string, n int) [][]int
26+
27+
// FindAllSubmatchIndex is the 'All' version of FindSubmatchIndex; it returns a slice of all
28+
// successive matches of the expression, as defined by the 'All' description in the package
29+
// comment. A return value of nil indicates no match.
30+
FindAllSubmatchIndex(b []byte, n int) [][]int
31+
}
32+
33+
// Creator is implemented by types which can operate a RegExp provider for otto.
34+
type Creator interface {
35+
// Compile parses a regular expression and returns, if successful, a Finder that
36+
// can be used to match against text.
37+
Compile(str string) (Finder, error)
38+
39+
// MustCompile is like Compile but panics if the expression cannot be parsed. It simplifies safe
40+
// initialization of global variables holding compiled regular expressions.
41+
MustCompile(str string) Finder
42+
43+
// NoTransform returns true if no RegExp transformation should be done.
44+
NoTransform() bool
45+
}

0 commit comments

Comments
 (0)