diff --git a/harness/nativeFunctionMatcher.js b/harness/nativeFunctionMatcher.js index b8ab9b25793..c0eeec465d1 100644 --- a/harness/nativeFunctionMatcher.js +++ b/harness/nativeFunctionMatcher.js @@ -3,20 +3,198 @@ /*--- description: Assert _NativeFunction_ Syntax info: | - This regex makes a best-effort determination that the tested string matches - the NativeFunction grammar production without requiring a correct tokeniser. - NativeFunction : function _NativeFunctionAccessor_ opt _IdentifierName_ opt ( _FormalParameters_ ) { [ native code ] } NativeFunctionAccessor : get set defines: - - NATIVE_FUNCTION_RE - assertToStringOrNativeFunction - assertNativeFunction + - validateNativeFunctionSource ---*/ -const NATIVE_FUNCTION_RE = /\bfunction\b((get|set)\b)?[\s\S]*\([\s\S]*\)[\s\S]*\{[\s\S]*\[[\s\S]*\bnative\b[\s\S]+\bcode\b[\s\S]*\][\s\S]*\}/; + +const validateNativeFunctionSource = function(source) { + const UnicodeIDStart = /\p{ID_Start}/u; + const UnicodeIDContinue = /\p{ID_Continue}/u; + const isNewline = (c) => /[\u000A\u000D\u2028\u2029]/u.test(c); + const isWhitespace = (c) => /[\u0009\u000B\u000C\u0020\u00A0\uFEFF]|\p{Space_Separator}/u.test(c); + + let i = 0; + + const eatWhitespace = () => { + while (i < source.length) { + const c = source[i]; + if (isWhitespace(c) || isNewline(c)) { + i += 1; + continue; + } + + if (c === '/') { + if (source[i + 1] === '/') { + while (i < source.length) { + if (isNewline(source[i])) { + break; + } + i += 1; + } + continue; + } + if (source[i + 1] === '*') { + const end = source.indexOf('*/', i); + if (end === -1) { + throw new SyntaxError(); + } + i = end + '*/'.length; + continue; + } + } + + break; + } + }; + + const getIdentifier = () => { + eatWhitespace(); + + const start = i; + let end = i; + switch (source[end]) { + case '_': + case '$': + end += 1; + break; + default: + if (UnicodeIDStart.test(source[end])) { + end += 1; + break; + } + return null; + } + while (end < source.length) { + const c = source[end]; + switch (c) { + case '_': + case '$': + end += 1; + break; + default: + if (UnicodeIDContinue.test(c)) { + end += 1; + break; + } + return source.slice(start, end); + } + } + return source.slice(start, end); + }; + + const test = (s) => { + eatWhitespace(); + + if (/\w/.test(s)) { + return getIdentifier() === s; + } + return source.slice(i, i + s.length) === s; + }; + + const eat = (s) => { + if (test(s)) { + i += s.length; + return true; + } + return false; + }; + + const eatIdentifier = () => { + const n = getIdentifier(); + if (n !== null) { + i += n.length; + return true; + } + return false; + }; + + const expect = (s) => { + if (!eat(s)) { + throw new SyntaxError(); + } + }; + + const eatString = () => { + if (source[i] === '\'' || source[i] === '"') { + const match = source[i]; + i += 1; + while (i < source.length) { + if (source[i] === match && source[i - 1] !== '\\') { + return; + } + if (isNewline(source[i])) { + throw new SyntaxError(); + } + i += 1; + } + throw new SyntaxError(); + } + }; + + // "Stumble" through source text until matching character is found. + // Assumes ECMAScript syntax keeps `[]` and `()` balanced. + const stumbleUntil = (c) => { + const match = { + ']': '[', + ')': '(', + }[c]; + let nesting = 1; + while (i < source.length) { + eatWhitespace(); + eatString(); // Strings may contain unbalanced characters. + if (source[i] === match) { + nesting += 1; + } else if (source[i] === c) { + nesting -= 1; + } + i += 1; + if (nesting === 0) { + return; + } + } + throw new SyntaxError(); + }; + + // function + expect('function'); + + // NativeFunctionAccessor + eat('get') || eat('set'); + + // PropertyName + if (eatIdentifier()) { + } else if (eat('[')) { + stumbleUntil(']'); + } + + // ( FormalParameters ) + expect('('); + stumbleUntil(')'); + + // { + expect('{'); + + // [native code] + expect('['); + expect('native'); + expect('code'); + expect(']'); + + // } + expect('}'); + + eatWhitespace(); + if (i !== source.length) { + throw new SyntaxError(); + } +}; const assertToStringOrNativeFunction = function(fn, expected) { const actual = "" + fn; @@ -29,8 +207,9 @@ const assertToStringOrNativeFunction = function(fn, expected) { const assertNativeFunction = function(fn, special) { const actual = "" + fn; - assert( - NATIVE_FUNCTION_RE.test(actual), - "Conforms to NativeFunction Syntax: '" + actual + "'." + (special ? "(" + special + ")" : "") - ); + try { + validateNativeFunctionSource(actual); + } catch (unused) { + $ERROR("Conforms to NativeFunction Syntax: '" + actual + "'." + (special ? "(" + special + ")" : "")) + } }; diff --git a/test/harness/nativeFunctionMatcher.js b/test/harness/nativeFunctionMatcher.js index bfe45b6f632..6b470850cc5 100644 --- a/test/harness/nativeFunctionMatcher.js +++ b/test/harness/nativeFunctionMatcher.js @@ -8,42 +8,53 @@ description: > includes: [nativeFunctionMatcher.js] ---*/ -if (!NATIVE_FUNCTION_RE.test('function(){[native code]}')) { - $ERROR('expected string to pass: "function(){[native code]}"'); -} - -if (!NATIVE_FUNCTION_RE.test('function(){ [native code] }')) { - $ERROR('expected string to pass: "function(){ [native code] }"'); -} - -if (!NATIVE_FUNCTION_RE.test('function ( ) { [ native code ] }')) { - $ERROR('expected string to pass: "function ( ) { [ native code ] }"'); -} - -if (!NATIVE_FUNCTION_RE.test('function a(){ [native code] }')) { - $ERROR('expected string to pass: "function a(){ [native code] }"'); -} - -if (!NATIVE_FUNCTION_RE.test('function a(){ /* } */ [native code] }')) { - $ERROR('expected string to pass: "function a(){ /* } */ [native code] }"'); -} - -if (NATIVE_FUNCTION_RE.test('')) { - $ERROR('expected string to fail: ""'); -} - -if (NATIVE_FUNCTION_RE.test('native code')) { - $ERROR('expected string to fail: "native code"'); -} - -if (NATIVE_FUNCTION_RE.test('function(){}')) { - $ERROR('expected string to fail: "function(){}"'); -} - -if (NATIVE_FUNCTION_RE.test('function(){ "native code" }')) { - $ERROR('expected string to fail: "function(){ "native code" }"'); -} - -if (NATIVE_FUNCTION_RE.test('function(){ [] native code }')) { - $ERROR('expected string to fail: "function(){ [] native code }"'); -} +[ + 'function(){[native code]}', + 'function(){ [native code] }', + 'function ( ) { [ native code ] }', + 'function a(){ [native code] }', + 'function a(){ /* } */ [native code] }', + `function a() { + // test + [native code] + /* test */ + }`, + 'function(a, b = function() { []; }) { [native code] }', + 'function [Symbol.xyz]() { [native code] }', + 'function [x[y][z[d]()]]() { [native code] }', + 'function ["]"] () { [native code] }', + 'function [\']\'] () { [native code] }', + '/* test */ function() { [native code] }', + 'function() { [native code] } /* test */', + 'function() { [native code] } // test', +].forEach((s) => { + try { + validateNativeFunctionSource(s); + } catch (unused) { + $ERROR(`"${s}" should pass`); + } +}); + +[ + 'native code', + 'function() {}', + 'function(){ "native code" }', + 'function(){ [] native code }', + 'function()) { [native code] }', + 'function(() { [native code] }', + 'function []] () { [native code] }', + 'function [[] () { [native code] }', + 'function ["]] () { [native code] }', + 'function [\']] () { [native code] }', + 'function() { [native code] /* }', + '// function() { [native code] }', +].forEach((s) => { + let fail = false; + try { + validateNativeFunctionSource(s); + fail = true; + } catch (unused) {} + if (fail) { + $ERROR(`"${s}" should fail`); + } +});