tenko
Version:
A "pixel perfect" 100% spec compliant ES2021 JavaScript parser written in JS.
161 lines (140 loc) • 6.09 kB
JavaScript
import {reduceErrorInput} from "../test_case_reducer.mjs";
import {
dumpFuzzOutput,
warnOsd,
} from './fuzzutils.mjs'
const VERBOSE = false;
const buffer = [];
function ignoreKnownCases(input) {
// This is checked after every reduction step and in the end.
// Return true if the input will be ignored (the reducer will reject the attempt and try something else)
return (
false
// Catch `for(y()of y)` cases
|| /\(\)of/.test(input)
// Silly case that often gets reduced to
|| /\nlet in\n/.test(input)
// for(y()in y)y
|| /for\s*\(y\(\)\s*in/.test(input)
// y()=x (but not arrow)
|| /\(\)=[^>=]/.test(input)
// Class methods:
// Node is allowing `class x {y}` which leads to a bunch of false positives in the fuzzer. Same for `{set;` and `{x=`
// || /class(\s+[\d\w$_]+)?\s*\{[\d\w$_]+[\n:;}]/.test(input)
// || /class\s*[\w\d$_]*(?:\s*extends\s*[\w\d$_]*)?\s*\{\s*\[?[\w\d$_]*\]?\s*([;}=]|$)/.test(input)
// Ignore some `class x {async;` cases, or with newline
// || /class\s*[\w\d$_]*(?:\s*extends\s*[\w\d$_]*)?\s*\{async(\n|;)/.test(input)
// // This may be legacy but node/v8 will accept `y()=x` even though that's a parse error now (wasn't in es5...)
// || /\(\)(?:=[^>]|\s*of|\s*in)/.test(input)
// // Same for ++y() and other updates
// || /[-+]{2}\w+\(\)/.test(input)
// || /\w+\(\)[-+]{2}/.test(input)
);
}
function fuzzAgainstNode(input, tfailed, counts, injectionMode, parseTenko, cliCommandPrefix) {
let parsedInput = input;
let nodefailed = false;
let errorMessage = tfailed;
try {
Function(input);
if (!injectionMode) ++counts.nodePassedFuzz;
} catch (e) {
if (!tfailed) errorMessage = e.message;
nodefailed = e.message;
}
if (!nodefailed !== !tfailed) {
if (nodefailed && (undefined
|| nodefailed.includes('has already been declared') // `switch(y){case y:function*d(){}function*d(){}}`
|| nodefailed.includes('Unexpected eval or arguments in strict mode') // (eval = a => { "use strict"})
|| nodefailed.includes('Unexpected strict mode reserved word') // (interface = a => { "use strict"})
)) {
if (VERBOSE) buffer.push('Skipping case that is likely a false positive (error in node that was not an error in Tenko)');
}
else if (tfailed && (undefined
// Regexes
// v8 is lazy parsing regexes
|| tfailed.includes('Lexer error! Regex:')
// This case often leads to false positives for classes
|| tfailed.includes('Either the current modifier is unknown')
|| tfailed.includes('Async methods are a restricted production')
|| tfailed.includes('Expected to parse the modified key of a class method')
// let \n keyword cases
|| tfailed.includes('`let` must be a declaration in strict mode ')
// // Delete
// // v8 doesn't seem to statically detect the delete-on-ident case
// || tfailed.includes('Bad delete case')
//
// // Break Continue
// // No lexical validation for occurrence of `break` or `continue`
// || tfailed.includes('only `continue` inside a loop')
// || tfailed.includes('inside a `switch` or loop')
// Labels are not verified (?)
|| tfailed.includes('not defined in the current label set')
//
// Assignments to crap
// Getting too many false positives for something like `y()=x` so going to disable the whole range :(
|| tfailed.includes('Cannot assign to lhs (starting with `async`)')
// || tfailed.includes('because it is not a valid assignment target')
)) {
// ignore (based on original input)
if (VERBOSE) buffer.push('Skipping case that is likely a false positive (error in Tenko that was not an error in node)');
}
else {
// Node error wasn't black listed. Now test Tenko
let beforeLen = input.length;
// Checker must confirm that the crash state of both parsers remains the same,
// and that the error message of the one parser stays the same as well
let checker = modifiedInput => {
// Prevent certain known errors from hiding real problems
if (ignoreKnownCases(input)) return false;
// Parse with Tenko, see what happens
let {t, e} = parseTenko(modifiedInput, counts, false);
if (!e !== !tfailed) return false;
// Parse with node, see what happens
let n
try {
n = {e: undefined, n: Function(modifiedInput)};
} catch (e) {
n = {e, n: undefined};
}
if (!n.e !== !nodefailed) return false;
if (errorMessage !== (e || n.e)) return false;
return e || n.e;
};
if (VERBOSE) buffer.push(['Trimming input (len was ' + input.length +')']);
input = reduceErrorInput(input, checker, cliCommandPrefix, undefined, undefined, true);
++counts.reduced;
if (VERBOSE) buffer.push(['Finished trimming (len now ' + input.length +', down from ' + beforeLen + ')']);
if (ignoreKnownCases(input)) {
// ignore (based on trimmed input)
console.log('Ignoring post scrubbed result');
if (VERBOSE) buffer.push('Ignoring outcome after trimming because it probably is a false positive');
} else {
// We will exit in this branch
console.log('');
console.log('Repro prefix:', cliCommandPrefix);
if (nodefailed) {
console.log('Thrown by v8');
} else {
console.log('Not thrown by v8');
}
if (tfailed) {
console.log('Thrown by Tenko');
} else {
console.log('Not thrown by Tenko');
}
if (tfailed) {
dumpFuzzOutput(input, parsedInput, errorMessage, 'Tenko failed but node did not');
warnOsd('Tenko fuzzing found problem');
} else if (nodefailed) {
dumpFuzzOutput(input, parsedInput, errorMessage, 'node failed but Tenko did not');
warnOsd('Tenko fuzzing found problem');
}
process.exit();
}
}
}
}
export {
fuzzAgainstNode,
};