UNPKG

tenko

Version:

A "pixel perfect" 100% spec compliant ES2021 JavaScript parser written in JS.

255 lines (215 loc) 9.4 kB
// Auto test case reducer // Given some input that throws, generate the smallest input that throws the same error import fs from 'fs'; import { dumpFuzzOutput, warnOsd, } from './fuzz/fuzzutils.mjs' const BOLD = '\x1b[;1;1m'; const DIM = '\x1b[30;1m'; const BLINK = '\x1b[;5;1m'; const RED = '\x1b[31m'; const GREEN = '\x1b[32m'; const RESET = '\x1b[0m'; function reduceAndExit( input/*: string*/, checker/*: (input: string) => bool|string*/, // false: discard input, string is error message cliCommandPrefix/*?: string*/, // This should be a `./t --module --annexb` composition of how to repro the end result file/*?: string*/ ) { reduceErrorInput(input, checker, cliCommandPrefix, file); console.log('exit...'); process.exit(); } function tokenToStringPart(str) { // Change `{# NUMBER_DEC : nl=N ws=N pos=11:13 loc=2:2 curc=46 `.2`#}` to `.2` // This prevents the location data from preventing test case reduction while avoiding false positives return str.replace(/\{#(.*?)#\}/g, (_, m) => m.replace(/[\s\S]*(`[\s\S]*`)[\s\S]*/, (m, g) => g)); } function reduceErrorInput( input/*: string*/, checker/*: (input: string) => bool|string*/, // false: discard input, string is error message cliCommandPrefix/*?: string*/, // This should be a `./t --module --annexb` composition of how to repro the end result file/*?: string*/, trimCache/*?: Map<string, string>*/ = new Map, verbose/*?: boolean*/ = true ) { if (verbose) console.log(BOLD + '<reduce>' + RESET); let org = input; let asserts = new Set; let inputError = checker(input, true); // First run console.log('Input error:', [inputError]); if (inputError !== false && typeof inputError !== 'string') { dumpFuzzOutput(input, input, 'The checker function should return false (reject) or the error message (string) and nothing else', 'test case reducer'); process.exit(); } // inputError = tokenToStringPart(inputError); if (inputError && inputError.toLowerCase().includes('assert')) asserts.add(inputError); let same = (code, nocache) => { if (!code) return false; if (!nocache && trimCache.has(code)) { let err = trimCache.get(code); if (verbose) console.log('CACHED!', code.replace(/\n/g, '\\n').replace(/\s/g, ' '), err === inputError, (err || '<no error>').trim()); return err === inputError; } let err = checker(code); if (err === false) return false; if (err && err.toLowerCase().includes('assert')) asserts.add(err); if (verbose) console.log('Tested!' + (nocache?' (nocache)':''), code.replace(/\n/g, '\\n').replace(/\s/g, ' '), 'the error:', err === inputError ? '<same as input error>' : GREEN + (err || '<no error>').trim() + RESET); trimCache.set(code, err); return err === inputError; }; if (verbose) console.log('<test case reducer>'); if (verbose) console.log('Input error:', BOLD + inputError + RESET); trimCache.set(input, inputError); if (verbose) console.log('Normalizing:', input.replace(/[\n\r]/g, '\\n')); // Normalize newlines if (same(input.replace(/\r/g, '\n'))) input = input.replace(/\r/g, '\n'); // Replace newlines with semis if (same(input.replace(/\n/g, ';'))) input = input.replace(/\n/g, ';'); // Trim multiple whitespaces if (same(input.replace(/[\t ]+/g, ' '))) input = input.replace(/[\t ]+/g, ' '); if (verbose) console.log('Trimming'); let lastInput = ''; while (lastInput !== input) { if (verbose) console.log('Outer repeat!'); lastInput = input; // Slice out single chars for (let i=input.length; i>=0; --i) { let testingInput = input.slice(0, i) + input.slice(i + 1); if (same(testingInput)) { input = testingInput; } } // Slice out char pairs for (let i=input.length-1; i>=0; --i) { if (input[i] !== 'x' && input[i] !== ' ') { let testingInput = input.slice(0, i) + input.slice(i + 2); if (same(testingInput)) { input = testingInput; } } } // Slice out char triples for (let i=input.length-2; i>=0; --i) { if (input[i] !== 'x' && input[i] !== ' ') { let testingInput = input.slice(0, i) + ' x ' + input.slice(i + 2); if (same(testingInput)) { input = testingInput; } } } // Slice out char quads for (let i=input.length-3; i>=0; --i) { if (input[i] !== 'x' && input[i] !== ' ') { let testingInput = input.slice(0, i) + input.slice(i + 3); if (same(testingInput)) { input = testingInput; } } } // Slice out char quads and replace them with ` y ` for (let i=input.length-3; i>=0; --i) { if (input[i] !== 'x' && input[i] !== ' ') { let testingInput = input.slice(0, i) + ' y ' + input.slice(i + 3); if (same(testingInput)) { input = testingInput; } } } // Drop simplified "dud" structures, like `class x{}` or `try{}finally{}`, which we'll often see from fuzzers // In some cases replace it with a semi, in others with nothing, for some cases try both input = trimPatten(same, input, /try\{\}(?:catch(?:\(\w\))?\{\})?(?:finally\{\})?/g, ';') input = trimPatten(same, input, /for\([\w\d] (?:of|in) [\w\d]\)/g, ';'); input = trimPatten(same, input, /for\(;;\)/g, ';'); input = trimPatten(same, input, /class(?: +[\w\d$_]*)?(?: extends [\w\d$_]*)?\s*\{\s*\}/g, ' x\n '); input = trimPatten(same, input, /while\([\w\d]\)/g, ';'); input = trimPatten(same, input, /do(?:\s\w\s|;)while\([\w\d]\)/g, ';'); input = trimPatten(same, input, /with\([\w\d]\)/g, ';'); input = trimPatten(same, input, /if\(\w\)/g, ';'); input = trimPatten(same, input, /else(?: \w)?/g, ';'); input = trimPatten(same, input, /default:?/g, ''); input = trimPatten(same, input, /case [\w\d$_]+:/g, ''); input = trimPatten(same, input, /function(?: \w)?\(\)\{\s*\w?\s*\}/g, ''); input = trimPatten(same, input, /function(?: \w)?\(\)\{\s*\w?\s*\}/g, ' x\n '); input = trimPatten(same, input, /switch\([\w\d]\)\{(?:case [\w\d]:)*\}/g, ';'); input = trimPatten(same, input, /[\w\d]\?[\w\d]:[\w\d]/g, ' x '); input = trimPatten(same, input, /\w in(?:stanceof)? \w/g, ' x '); input = trimPatten(same, input, /\d+:[\w\d]+/g, ''); input = trimPatten(same, input, /[\w\d$_]*\([\w\d$_]*\)\{\}[;,]?/g, ''); input = trimPatten(same, input, /['"]use strict['"];/g, ''); // Known bad patterns input = trimPatten(same, input, /\b0\d+[eE]?\d+/g, '1'); if (lastInput === input) { // This is a RC. Now check for wrappers, `try{x}finally{}` -> `x` let t = input.replace(/try\{([\s\S]+)\}(?:finally|catch(?:\(\w\))?)\{\}?/, '$1'); if (t !== input && same(t)) input = t; t = input.replace(/try\{\}(?:finally|catch(?:\(\w\))?)\{([\s\S]+)\}?/, '$1'); if (t !== input && same(t)) input = t; t = input.replace(/switch\([\w\d]\){(?:case\s+)?[\w\d]:([\s\S]+)}?/, '$1'); if (t !== input && same(t)) input = t; t = input.replace(/\((.*)\)/, '$1'); if (t !== input && same(t)) input = t; t = input.replace(/for\((.*);;\)/, '$1;'); if (t !== input && same(t)) input = t; t = input.replace(/for\(;(.*);\)/, '$1;'); if (t !== input && same(t)) input = t; t = input.replace(/for\(;;(.*)\)/, '$1;'); if (t !== input && same(t)) input = t; // console.debug('testing:', [input]) t = input.replace(/try\{\}catch(?:\([\w\d]+\))?\{\}finally\s*\{(.*)\}?/, '$1'); // console.debug(' -> :', [t]) if (t !== input && same(t)) input = t; t = input.replace(/function\s+[\w\d_$]+\([\w\d_$]*\)\{.*\}?/, '$1'); if (t !== input && same(t)) input = t; t = input.replace(/[\w]+\s*=/, ''); if (t !== input && same(t)) input = t; t = input.replace(/extends\s+[\d\w$_]/, ' '); if (t !== input && same(t)) input = t; } } if (verbose) console.log('</trim>'); if (verbose) console.log('Reduced input:'); same(input, true); asserts.delete(inputError); if (asserts.length) { if (verbose) console.log(BLINK + 'THERE WERE ASSERTS' + RESET); if (verbose) console.log(asserts.forEach(s => ' - ' + s + '\n')); } if (process.argv.includes('--write') && file) { if (verbose) console.log('Writing new test case to', file + '.min'); fs.writeFileSync(file + '.min', '@Minified from ' + file + '\n###\n' + input); } if (verbose) { console.log(BOLD + '</reduce>' + RESET); console.log(`Input:`); console.log('```'); console.log(input); console.log('```'); console.log(''); console.log(cliCommandPrefix + ' i \'' + input.replace(/'/g, '\\\'') + '\'\n'); } return input; } function trimPatten(same, str, pattern, repl) { let lastOffset = -1; let found = true; let currentStr = str; while (found) { found = false; currentStr = currentStr.replace(pattern, (match, offset) => { if (offset <= lastOffset) return match; if (found) return match; found = true; lastOffset = offset; let lastEffort = currentStr.slice(0, offset) + repl + str.slice(offset + match.length); if (lastEffort !== currentStr && same(lastEffort)) { currentStr = lastEffort; return repl; } return match; }); } return currentStr; } export {reduceAndExit, reduceErrorInput, trimPatten};