Skip to content

Commit

Permalink
native function validator for your consideration
Browse files Browse the repository at this point in the history
  • Loading branch information
devsnek committed Jul 26, 2020
1 parent 4e7f377 commit 405d896
Show file tree
Hide file tree
Showing 2 changed files with 238 additions and 48 deletions.
197 changes: 188 additions & 9 deletions harness/nativeFunctionMatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 + ")" : ""))
}
};
89 changes: 50 additions & 39 deletions test/harness/nativeFunctionMatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}
});

0 comments on commit 405d896

Please sign in to comment.