A method that never read exec: RegExp.prototype.test in goja
RegExp.prototype.test in goja, a JavaScript engine in Go, never read exec. A plain object with exec threw, and a replaced exec was ignored. I fixed it in PR #806, merged about an hour after I opened it.
When it happens
RegExp.prototype.test.call({exec() { return null; }}, "a"); // TypeError, want false
var r = /a/; r.exec = () => null; r.test("a"); // true, want false
RegExp.prototype.exec = () => null; /a/.test("a"); // true, want false
How RegExp.prototype.test works
The spec (ECMA-262 22.2.6.16) calls RegExpExec (step 4 in the current draft), which reads exec and calls it if callable (22.2.7.1). Only otherwise does the built-in matcher run. goja’s regexpproto_test required a *regexpObject and always ran the built-in matcher.
Why the gap existed
regexpproto_test has had the same logic since November 2016. Symbol.match and Symbol.search got checkStdRegexp, which accepts only an unmodified regexp, and a path through exec in February 2020. test was not moved over.
The fix
The whole diff to builtin_regexp.go, with one line of context:
@@ -487,4 +487,6 @@ func (r *Runtime) regexpproto_exec(call FunctionCall) Value {
func (r *Runtime) regexpproto_test(call FunctionCall) Value {
- if this, ok := r.toObject(call.This).self.(*regexpObject); ok {
- if this.test(call.Argument(0).toString()) {
+ thisObj := r.toObject(call.This)
+ s := call.Argument(0).toString()
+ if this := r.checkStdRegexp(thisObj); this != nil {
+ if this.test(s) {
return valueTrue
@@ -493,5 +495,7 @@ func (r *Runtime) regexpproto_test(call FunctionCall) Value {
}
- } else {
- panic(r.NewTypeError("Method RegExp.prototype.test called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This})))
}
+ if regExpExec(thisObj, s) == _null {
+ return valueFalse
+ }
+ return valueTrue
}
If checkStdRegexp accepts the regexp, the built-in matcher runs and builds no match array. Otherwise the existing regExpExec calls exec: null is false, an object true. An object that is not a regexp and has no callable exec still throws a TypeError, now with the message no RegExpMatcher internal slot.
The test
TestRegExpTestCallsExec has 14 assertions. A plain object with exec is checked for its this, its argument, and null and object results. A result that is not an object or null, exec: 1, no exec and a primitive this must throw TypeError. Then a regexp with its own exec, a subclass that overrides it, and a patched RegExp.prototype.exec.
Without the fix, three of three runs failed with a TypeError on the first assertion. With it, three passed.
What the maintainer did
@dop251 merged it with no comments.
The lesson
- A fast path needs a row for each way an object can differ: an own
execfails a guard that accepts any regexp object, a subclass one that reads a single flag. - Test where the spec branches. A non-callable
execfalls back to the built-in matcher, sor.exec = 1; r.test("a")staystrue. A helper that throws fails that row.
If an engine of yours special-cases its own regexp type before calling exec, reply on X.