function generateTestCase(assign) { let sideEffectCount = 0 let patternCount = 0 let limit = 10 let depth = 0 function choice(n) { return Math.random() * n | 0 } function patternAndValue() { patternCount++ switch (choice(3)) { case 0: return array() case 1: return object() case 2: return [assign(), choice(10), choice(10)] } } function sideEffect(result) { return `s(${sideEffectCount++}${result ? `, ${result}` : ''})` } function indent(open, items, close) { let tab = ' ' items = items.map(i => `\n${tab.repeat(depth + 1)}${i}`).join(',') return `${open}${items}\n${tab.repeat(depth)}${close}` } function id() { return String.fromCharCode('a'.charCodeAt(0) + choice(3)) } function array() { let count = 1 + choice(2) let pattern = [] let value = [] depth++ for (let i = 0; i < count; i++) { if (patternCount > limit) break let [pat, val, defVal] = patternAndValue() switch (choice(3)) { case 0: pattern.push(pat) value.push(val) break case 1: pattern.push(`${pat} = ${sideEffect(defVal)}`) value.push(val) break case 2: pattern.push(`${pat} = ${sideEffect(defVal)}`) value.push(defVal) break } if (choice(10) < 8) value.push(val) } if (choice(2)) { pattern.push(`...${assign()}`) if (choice(10) < 8) value.push(choice(10)) } depth-- return [ indent('[', pattern, ']'), indent('[', value, ']'), '[]', ] } function object() { let count = 1 + choice(2) let pattern = [] let value = [] let valKeys = new Set() depth++ for (let i = 0; i < count; i++) { if (patternCount > limit) break let valKey = id() if (valKeys.has(valKey)) continue valKeys.add(valKey) let patKey = choice() ? valKey : `[${sideEffect(`'${valKey}'`)}]` let [pat, val, defVal] = patternAndValue() switch (choice(3)) { case 0: pattern.push(`${patKey}: ${pat}`) value.push(`${valKey}: ${val}`) break case 1: pattern.push(`${patKey}: ${pat} = ${sideEffect(defVal)}`) value.push(`${valKey}: ${val}`) break case 2: pattern.push(`${patKey}: ${pat} = ${sideEffect(defVal)}`) value.push(`${valKey}: ${defVal}`) break } } if (choice(2)) { pattern.push(`...${assign()}`) if (choice(10) < 8) value.push(`${id()}: ${choice(10)}`) } depth-- return [ indent('{', pattern, '}'), indent('{', value, '}'), '{}', ] } return choice(2) ? array() : object() } function evaluate(code) { let effectTrace = [] let assignTarget = {} let sideEffect = (id, value) => (effectTrace.push(id), value) new Function('a', 's', code)(assignTarget, sideEffect) return JSON.stringify({ assignTarget, effectTrace }) } function generateTestCases(trials) { let testCases = [] while (testCases.length < trials) { let ids = [] let assignCount = 0 let [pattern, value] = generateTestCase(() => { let id = `_${assignCount++}` ids.push(id) return id }) try { evaluate(`(${pattern.replace(/_/g, 'a._')} = ${value});`) testCases.push([pattern, value, ids]) } catch (e) { } } return testCases } function AssignmentOperator([pattern, value]) { let ts = `(${pattern.replace(/_/g, 'a._')} = ${value});` let js = ts return { js, ts } } function NamespaceExport([pattern, value]) { let ts = `namespace a { export const ${`${pattern} = ${value}`} }` let js = `(${pattern.replace(/_/g, 'a._')} = ${value});` return { js, ts } } function ConstDeclaration([pattern, value, ids]) { let ts = `const ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}` let js = ts return { js, ts } } function LetDeclaration([pattern, value, ids]) { let ts = `let ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}` let js = ts return { js, ts } } function VarDeclaration([pattern, value, ids]) { let ts = `var ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}` let js = ts return { js, ts } } function TryCatchBinding([pattern, value, ids]) { let ts = `try { throw ${value} } catch (${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` let js = ts return { js, ts } } function FunctionStatementArguments([pattern, value, ids]) { let ts = `function foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }\nfoo(${value});` let js = ts return { js, ts } } function FunctionExpressionArguments([pattern, value, ids]) { let ts = `(function(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} })(${value});` let js = ts return { js, ts } } function ArrowFunctionArguments([pattern, value, ids]) { let ts = `((${pattern}) => { ${ids.map(id => `a.${id} = ${id};`).join('\n')} })(${value});` let js = ts return { js, ts } } function ObjectMethodArguments([pattern, value, ids]) { let ts = `({ foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } }).foo(${value});` let js = ts return { js, ts } } function ClassStatementMethodArguments([pattern, value, ids]) { let ts = `class Foo { foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } }\nnew Foo().foo(${value});` let js = ts return { js, ts } } function ClassExpressionMethodArguments([pattern, value, ids]) { let ts = `(new (class { foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } })).foo(${value});` let js = ts return { js, ts } } function ForLoopConst([pattern, value, ids]) { let ts = `var i; for (const ${pattern} = ${value}; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` let js = ts return { js, ts } } function ForLoopLet([pattern, value, ids]) { let ts = `for (let ${pattern} = ${value}, i = 0; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` let js = ts return { js, ts } } function ForLoopVar([pattern, value, ids]) { let ts = `for (var ${pattern} = ${value}, i = 0; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` let js = ts return { js, ts } } function ForLoop([pattern, value]) { let ts = `for (${pattern.replace(/_/g, 'a._')} = ${value}; 0; ) ;` let js = ts return { js, ts } } function ForOfLoopConst([pattern, value, ids]) { let ts = `for (const ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` let js = ts return { js, ts } } function ForOfLoopLet([pattern, value, ids]) { let ts = `for (let ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` let js = ts return { js, ts } } function ForOfLoopVar([pattern, value, ids]) { let ts = `for (var ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }` let js = ts return { js, ts } } function ForOfLoop([pattern, value]) { let ts = `for (${pattern.replace(/_/g, 'a._')} of [${value}]) ;` let js = ts return { js, ts } } async function verify(test, transform, testCases) { let verbose = process.argv.indexOf('--verbose') >= 0 let indent = t => t.replace(/\n/g, '\n ') let newline = false console.log(`${test.name} (${transform.name}):`) await concurrentMap(testCases, 20, async (testCase) => { let { js, ts } = test(testCase) let expected try { expected = evaluate(js) } catch (e) { return } let transformed try { transformed = await transform(ts) } catch (e) { process.stdout.write('T') newline = true if (verbose) { console.log('\n' + '='.repeat(80)) console.log(indent(`Original code:\n${ts}`)) console.log(indent(`Transform error:\n${e}`)) newline = false } return } let actual try { actual = evaluate(transformed) } catch (e) { actual = e + '' } if (actual !== expected) { process.stdout.write(actual.indexOf('SyntaxError') >= 0 ? 'S' : 'X') newline = true if (verbose) { console.log('\n' + '='.repeat(80)) console.log(indent(`Original code:\n${ts}`)) console.log(indent(`Transformed code:\n${transformed}`)) console.log(indent(`Expected output:\n${expected}`)) console.log(indent(`Actual output:\n${actual}`)) newline = false } } else { process.stdout.write('-') newline = true } }) if (newline) process.stdout.write('\n') } function concurrentMap(items, batch, callback) { return new Promise((resolve, reject) => { let index = 0 let pending = 0 let next = () => { if (index === items.length && pending === 0) { resolve() } else if (index < items.length) { let item = items[index++] pending++ callback(item).then(() => { pending-- next() }, e => { items.length = 0 reject(e) }) } } for (let i = 0; i < batch; i++)next() }) } async function main() { let es = require('./esbuild').installForTests() let esbuild = async (x) => (await es.transform(x, { target: 'es6', loader: 'ts' })).code.trim() console.log(` Options: --verbose = Print details for failures Legend: - = The test passed X = The test failed T = The transform function itself failed S = The generated code has a syntax error`) let tests = [ // Bindings ConstDeclaration, LetDeclaration, VarDeclaration, TryCatchBinding, FunctionStatementArguments, FunctionExpressionArguments, ArrowFunctionArguments, ObjectMethodArguments, ClassStatementMethodArguments, ClassExpressionMethodArguments, ForLoopConst, ForLoopLet, ForLoopVar, ForOfLoopConst, ForOfLoopLet, ForOfLoopVar, // Destructuring AssignmentOperator, ForLoop, ForOfLoop, // TypeScript-specific NamespaceExport, ] let transforms = [ esbuild, ] let testCases = generateTestCases(100) for (let transform of transforms) { console.log() for (let test of tests) { await verify(test, transform, testCases) } } } main().catch(e => setTimeout(() => { throw e }))