Skip to content

Commit

Permalink
String.prototype.matchAll implementation (dop251#248)
Browse files Browse the repository at this point in the history
* Basic String.prototype.matchAll and friends implementation

* add one more test

* add one more test 2

* fix bugs

* fix RegExpStringIteratorPrototype ancestry

* Fix test/built-ins/RegExpStringIteratorPrototype/next/custom-regexpexec-not-callable.js

* Apply suggestions from code review

Co-authored-by: Dmitry Panov <[email protected]>

* Rename classRegExpStringIteratorPrototype to classRegExpStringIterator

* Return *regexpObject instead of *Object for *newRegExp* methods

* Update builtin_string.go

Co-authored-by: Dmitry Panov <[email protected]>

* better way to check the original regex doesn't change

* Remove a check from the go code that we do in the JS one

* fixup! Remove a check from the go code that we do in the JS one

Co-authored-by: Dmitry Panov <[email protected]>
  • Loading branch information
mstoykov and dop251 authored Jan 26, 2021
1 parent 084ecb4 commit f588426
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 23 deletions.
108 changes: 102 additions & 6 deletions builtin_regexp.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ func (r *Runtime) newRegexpObject(proto *Object) *regexpObject {
return o
}

func (r *Runtime) newRegExpp(pattern *regexpPattern, patternStr valueString, proto *Object) *Object {
func (r *Runtime) newRegExpp(pattern *regexpPattern, patternStr valueString, proto *Object) *regexpObject {
o := r.newRegexpObject(proto)

o.pattern = pattern
o.source = patternStr

return o.val
return o
}

func decodeHex(s string) (int, bool) {
Expand Down Expand Up @@ -276,7 +276,7 @@ func compileRegexp(patternStr, flags string) (p *regexpPattern, err error) {
return
}

func (r *Runtime) _newRegExp(patternStr valueString, flags string, proto *Object) *Object {
func (r *Runtime) _newRegExp(patternStr valueString, flags string, proto *Object) *regexpObject {
pattern, err := compileRegexpFromValueString(patternStr, flags)
if err != nil {
panic(r.newSyntaxError(err.Error(), -1))
Expand All @@ -292,10 +292,10 @@ func (r *Runtime) builtin_newRegExp(args []Value, proto *Object) *Object {
if len(args) > 1 {
flagsVal = args[1]
}
return r.newRegExp(patternVal, flagsVal, proto)
return r.newRegExp(patternVal, flagsVal, proto).val
}

func (r *Runtime) newRegExp(patternVal, flagsVal Value, proto *Object) *Object {
func (r *Runtime) newRegExp(patternVal, flagsVal Value, proto *Object) *regexpObject {
var pattern valueString
var flags string
if isRegexp(patternVal) { // this may have side effects so need to call it anyway
Expand Down Expand Up @@ -344,7 +344,7 @@ func (r *Runtime) builtin_RegExp(call FunctionCall) Value {
}
}
}
return r.newRegExp(pattern, flags, r.global.RegExpPrototype)
return r.newRegExp(pattern, flags, r.global.RegExpPrototype).val
}

func (r *Runtime) regexpproto_compile(call FunctionCall) Value {
Expand Down Expand Up @@ -759,6 +759,82 @@ func (r *Runtime) regexpproto_stdSearchGeneric(rxObj *Object, arg valueString) V
return r.toObject(result).self.getStr("index", nil)
}

func (r *Runtime) regexpproto_stdMatcherAll(call FunctionCall) Value {
thisObj := r.toObject(call.This)
s := call.Argument(0).toString()
flags := nilSafe(thisObj.self.getStr("flags", nil)).toString()
c := r.speciesConstructorObj(call.This.(*Object), r.global.RegExp)
matcher := r.toConstructor(c)([]Value{call.This, flags}, nil)
matcher.self.setOwnStr("lastIndex", valueInt(toLength(thisObj.Get("lastIndex"))), true)
flagsStr := flags.String()
global := strings.Contains(flagsStr, "g")
fullUnicode := strings.Contains(flagsStr, "u")
return r.createRegExpStringIterator(matcher, s, global, fullUnicode)
}

func (r *Runtime) createRegExpStringIterator(matcher *Object, s valueString, global, fullUnicode bool) Value {
o := &Object{runtime: r}

ri := &regExpStringIterObject{
matcher: matcher,
s: s,
global: global,
fullUnicode: fullUnicode,
}
ri.class = classRegExpStringIterator
ri.val = o
ri.extensible = true
o.self = ri
ri.prototype = r.global.RegExpStringIteratorPrototype
ri.init()

return o
}

type regExpStringIterObject struct {
baseObject
matcher *Object
s valueString
global, fullUnicode, done bool
}

// RegExpExec as defined in 21.2.5.2.1
func regExpExec(r *Object, s valueString) Value {
exec := r.Get("exec")
if execObject, ok := exec.(*Object); ok {
if execFn, ok := execObject.self.assertCallable(); ok {
return r.runtime.regExpExec(execFn, r, s)
}
}
if rx, ok := r.self.(*regexpObject); ok {
return rx.exec(s)
}
panic(r.runtime.NewTypeError("no RegExpMatcher internal slot"))
}

func (ri *regExpStringIterObject) next() (v Value) {
if ri.done {
return ri.val.runtime.createIterResultObject(_undefined, true)
}

match := regExpExec(ri.matcher, ri.s)
if IsNull(match) {
ri.done = true
return ri.val.runtime.createIterResultObject(_undefined, true)
}
if !ri.global {
ri.done = true
return ri.val.runtime.createIterResultObject(match, false)
}

matchStr := nilSafe(ri.val.runtime.toObject(match).self.getIdx(valueInt(0), nil)).toString()
if matchStr.length() == 0 {
thisIndex := toLength(ri.matcher.self.getStr("lastIndex", nil))
ri.matcher.self.setOwnStr("lastIndex", valueInt(advanceStringIndex64(ri.s, thisIndex, ri.fullUnicode)), true)
}
return ri.val.runtime.createIterResultObject(match, false)
}

func (r *Runtime) regexpproto_stdSearch(call FunctionCall) Value {
thisObj := r.toObject(call.This)
s := call.Argument(0).toString()
Expand Down Expand Up @@ -1119,10 +1195,29 @@ func (r *Runtime) regexpproto_stdReplacer(call FunctionCall) Value {
return stringReplace(s, found, replaceStr, rcall)
}

func (r *Runtime) regExpStringIteratorProto_next(call FunctionCall) Value {
thisObj := r.toObject(call.This)
if iter, ok := thisObj.self.(*regExpStringIterObject); ok {
return iter.next()
}
panic(r.NewTypeError("Method RegExp String Iterator.prototype.next called on incompatible receiver %s", thisObj.String()))
}

func (r *Runtime) createRegExpStringIteratorPrototype(val *Object) objectImpl {
o := newBaseObjectObj(val, r.global.IteratorPrototype, classObject)

o._putProp("next", r.newNativeFunc(r.regExpStringIteratorProto_next, nil, "next", nil, 0), true, false, true)
o._putSym(SymToStringTag, valueProp(asciiString(classRegExpStringIterator), false, false, true))

return o
}

func (r *Runtime) initRegExp() {
o := r.newGuardedObject(r.global.ObjectPrototype, classObject)
r.global.RegExpPrototype = o.val
r.global.stdRegexpProto = o
r.global.RegExpStringIteratorPrototype = r.newLazyObject(r.createRegExpStringIteratorPrototype)

o._putProp("compile", r.newNativeFunc(r.regexpproto_compile, nil, "compile", nil, 2), true, false, true)
o._putProp("exec", r.newNativeFunc(r.regexpproto_exec, nil, "exec", nil, 1), true, false, true)
o._putProp("test", r.newNativeFunc(r.regexpproto_test, nil, "test", nil, 1), true, false, true)
Expand Down Expand Up @@ -1164,6 +1259,7 @@ func (r *Runtime) initRegExp() {
}, false)

o._putSym(SymMatch, valueProp(r.newNativeFunc(r.regexpproto_stdMatcher, nil, "[Symbol.match]", nil, 1), true, false, true))
o._putSym(SymMatchAll, valueProp(r.newNativeFunc(r.regexpproto_stdMatcherAll, nil, "[Symbol.matchAll]", nil, 1), true, false, true))
o._putSym(SymSearch, valueProp(r.newNativeFunc(r.regexpproto_stdSearch, nil, "[Symbol.search]", nil, 1), true, false, true))
o._putSym(SymSplit, valueProp(r.newNativeFunc(r.regexpproto_stdSplitter, nil, "[Symbol.split]", nil, 2), true, false, true))
o._putSym(SymReplace, valueProp(r.newNativeFunc(r.regexpproto_stdReplacer, nil, "[Symbol.replace]", nil, 2), true, false, true))
Expand Down
38 changes: 36 additions & 2 deletions builtin_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ func (r *Runtime) stringproto_match(call FunctionCall) Value {
}

if rx == nil {
rx = r.newRegExp(regexp, nil, r.global.RegExpPrototype).self.(*regexpObject)
rx = r.newRegExp(regexp, nil, r.global.RegExpPrototype)
}

if matcher, ok := r.toObject(rx.getSym(SymMatch, nil)).self.assertCallable(); ok {
Expand All @@ -371,6 +371,39 @@ func (r *Runtime) stringproto_match(call FunctionCall) Value {
panic(r.NewTypeError("RegExp matcher is not a function"))
}

func (r *Runtime) stringproto_matchAll(call FunctionCall) Value {
r.checkObjectCoercible(call.This)
regexp := call.Argument(0)
if regexp != _undefined && regexp != _null {
if isRegexp(regexp) {
if o, ok := regexp.(*Object); ok {
flags := o.Get("flags")
r.checkObjectCoercible(flags)
if !strings.Contains(flags.toString().String(), "g") {
panic(r.NewTypeError("RegExp doesn't have global flag set"))
}
}
}
if matcher := toMethod(r.getV(regexp, SymMatchAll)); matcher != nil {
return matcher(FunctionCall{
This: regexp,
Arguments: []Value{call.This},
})
}
}

rx := r.newRegExp(regexp, asciiString("g"), r.global.RegExpPrototype)

if matcher, ok := r.toObject(rx.getSym(SymMatchAll, nil)).self.assertCallable(); ok {
return matcher(FunctionCall{
This: rx.val,
Arguments: []Value{call.This.toString()},
})
}

panic(r.NewTypeError("RegExp matcher is not a function"))
}

func (r *Runtime) stringproto_normalize(call FunctionCall) Value {
r.checkObjectCoercible(call.This)
s := call.This.toString()
Expand Down Expand Up @@ -662,7 +695,7 @@ func (r *Runtime) stringproto_search(call FunctionCall) Value {
}

if rx == nil {
rx = r.newRegExp(regexp, nil, r.global.RegExpPrototype).self.(*regexpObject)
rx = r.newRegExp(regexp, nil, r.global.RegExpPrototype)
}

if searcher, ok := r.toObject(rx.getSym(SymSearch, nil)).self.assertCallable(); ok {
Expand Down Expand Up @@ -924,6 +957,7 @@ func (r *Runtime) initString() {
o._putProp("lastIndexOf", r.newNativeFunc(r.stringproto_lastIndexOf, nil, "lastIndexOf", nil, 1), true, false, true)
o._putProp("localeCompare", r.newNativeFunc(r.stringproto_localeCompare, nil, "localeCompare", nil, 1), true, false, true)
o._putProp("match", r.newNativeFunc(r.stringproto_match, nil, "match", nil, 1), true, false, true)
o._putProp("matchAll", r.newNativeFunc(r.stringproto_matchAll, nil, "matchAll", nil, 1), true, false, true)
o._putProp("normalize", r.newNativeFunc(r.stringproto_normalize, nil, "normalize", nil, 0), true, false, true)
o._putProp("padEnd", r.newNativeFunc(r.stringproto_padEnd, nil, "padEnd", nil, 1), true, false, true)
o._putProp("padStart", r.newNativeFunc(r.stringproto_padStart, nil, "padStart", nil, 1), true, false, true)
Expand Down
20 changes: 19 additions & 1 deletion builtin_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,25 @@ var prefix2 = new Prefix("def");
testScript1(SCRIPT, valueTrue, t)
}

func TestStringMatchAllSym(t *testing.T) {
const SCRIPT = `
function Prefix(p) {
this.p = p;
}
Prefix.prototype[Symbol.matchAll] = function(s) {
return s.substring(0, this.p.length) === this.p;
}
var prefix1 = new Prefix("abc");
var prefix2 = new Prefix("def");
"abc123".matchAll(prefix1) === true && "abc123".matchAll(prefix2) === false &&
"def123".matchAll(prefix1) === false && "def123".matchAll(prefix2) === true;
`
testScript1(SCRIPT, valueTrue, t)
}

func TestGenericSplitter(t *testing.T) {
const SCRIPT = `
function MyRegexp(pattern, flags) {
Expand Down Expand Up @@ -223,5 +242,4 @@ func TestValueStringBuilder(t *testing.T) {
t.Fatal(res)
}
})

}
2 changes: 2 additions & 0 deletions builtin_symbol.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var (
SymIsConcatSpreadable = newSymbol(asciiString("Symbol.isConcatSpreadable"))
SymIterator = newSymbol(asciiString("Symbol.iterator"))
SymMatch = newSymbol(asciiString("Symbol.match"))
SymMatchAll = newSymbol(asciiString("Symbol.matchAll"))
SymReplace = newSymbol(asciiString("Symbol.replace"))
SymSearch = newSymbol(asciiString("Symbol.search"))
SymSpecies = newSymbol(asciiString("Symbol.species"))
Expand Down Expand Up @@ -117,6 +118,7 @@ func (r *Runtime) createSymbol(val *Object) objectImpl {
SymIsConcatSpreadable,
SymIterator,
SymMatch,
SymMatchAll,
SymReplace,
SymSearch,
SymSpecies,
Expand Down
9 changes: 5 additions & 4 deletions object.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ const (
classJSON = "JSON"
classGlobal = "global"

classArrayIterator = "Array Iterator"
classMapIterator = "Map Iterator"
classSetIterator = "Set Iterator"
classStringIterator = "String Iterator"
classArrayIterator = "Array Iterator"
classMapIterator = "Map Iterator"
classSetIterator = "Set Iterator"
classStringIterator = "String Iterator"
classRegExpStringIterator = "RegExp String Iterator"
)

var (
Expand Down
8 changes: 4 additions & 4 deletions regexp.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,12 +578,12 @@ func (r *regexpObject) test(target valueString) bool {
return match
}

func (r *regexpObject) clone() *Object {
func (r *regexpObject) clone() *regexpObject {
r1 := r.val.runtime.newRegexpObject(r.prototype)
r1.source = r.source
r1.pattern = r.pattern

return r1.val
return r1
}

func (r *regexpObject) init() {
Expand Down Expand Up @@ -612,7 +612,7 @@ func (r *regexpObject) defineOwnPropertySym(name *Symbol, desc PropertyDescripto
res := r.baseObject.defineOwnPropertySym(name, desc, throw)
if res && r.standard {
switch name {
case SymMatch, SymSearch, SymSplit, SymReplace:
case SymMatch, SymMatchAll, SymSearch, SymSplit, SymReplace:
r.standard = false
}
}
Expand All @@ -639,7 +639,7 @@ func (r *regexpObject) setOwnSym(name *Symbol, value Value, throw bool) bool {
res := r.baseObject.setOwnSym(name, value, throw)
if res && r.standard {
switch name {
case SymMatch, SymSearch, SymSplit, SymReplace:
case SymMatch, SymMatchAll, SymSearch, SymSplit, SymReplace:
r.standard = false
}
}
Expand Down
Loading

0 comments on commit f588426

Please sign in to comment.