365 lines
9.8 KiB
JavaScript
365 lines
9.8 KiB
JavaScript
|
|
import test from 'ava';
|
|
import sinon from 'sinon';
|
|
|
|
import { omit, times } from 'ramda';
|
|
|
|
import espree from 'espree';
|
|
|
|
import fuzzProgram, { FuzzerState } from 'shift-fuzzer';
|
|
import shiftCodegen, { FormattedCodeGen } from 'shift-codegen';
|
|
|
|
import seedrandom from 'seedrandom';
|
|
|
|
import shiftToEspreeSafe from '../test/_shift-to-espree-safe';
|
|
|
|
import recurse from './recurse';
|
|
|
|
import eslintTemplateVisitor from '.';
|
|
|
|
const SEED = process.env.SEED || Math.random().toString(16).slice(2);
|
|
|
|
console.log(`
|
|
Reproduce the randomized fuzzing test by running:
|
|
\`\`\`bash
|
|
SEED=${JSON.stringify(SEED)} yarn ava
|
|
\`\`\`
|
|
`);
|
|
|
|
const parserOptions = {
|
|
sourceType: 'module',
|
|
ecmaVersion: 2018,
|
|
};
|
|
|
|
test.beforeEach(t => {
|
|
t.context.rng = seedrandom(SEED);
|
|
});
|
|
|
|
test('mixing templates into a visitor', t => {
|
|
const templates = eslintTemplateVisitor();
|
|
|
|
const a = templates.variable();
|
|
const template = templates.template`${a}.parentNode.removeChild(${a})`;
|
|
|
|
const ast = espree.parse(`
|
|
foo.parentNode.removeChild(foo);
|
|
foo.parentNode.removeChild(bar);
|
|
`);
|
|
|
|
const visitorA = {
|
|
[template]: sinon.spy(),
|
|
CallExpression: sinon.spy(),
|
|
MemberExpression: sinon.spy(),
|
|
};
|
|
|
|
const visitorB = {
|
|
[template]: sinon.spy(),
|
|
MemberExpression: sinon.spy(),
|
|
};
|
|
|
|
recurse.visit(ast, visitorA);
|
|
recurse.visit(ast, templates.visitor(visitorB));
|
|
|
|
t.false(visitorA[template].called);
|
|
t.true(visitorA.CallExpression.called);
|
|
t.true(visitorA.MemberExpression.called);
|
|
|
|
t.true(visitorB[template].called);
|
|
t.true(visitorB.MemberExpression.called);
|
|
|
|
t.deepEqual(
|
|
visitorA.MemberExpression.getCalls().map(call => call.args),
|
|
visitorB.MemberExpression.getCalls().map(call => call.args),
|
|
);
|
|
|
|
t.deepEqual(
|
|
visitorA.CallExpression.getCalls().map(call => call.args).slice(0, 1),
|
|
visitorB[template].getCalls().map(call => call.args),
|
|
);
|
|
});
|
|
|
|
test('variable matching', t => {
|
|
const templates = eslintTemplateVisitor();
|
|
|
|
const a = templates.variable();
|
|
const template = templates.template`${a}.foo()`;
|
|
|
|
const visitor = {
|
|
[template]: sinon.spy(),
|
|
};
|
|
|
|
recurse.visit(espree.parse('foo.bar()'), templates.visitor(visitor));
|
|
t.false(visitor[template].called);
|
|
|
|
recurse.visit(espree.parse('bar.foo()'), templates.visitor(visitor));
|
|
t.true(visitor[template].called);
|
|
});
|
|
|
|
const templateFoundInMacro = (t, templateSource, source, expectedToMatch = true) => {
|
|
const templates = eslintTemplateVisitor();
|
|
const template = templates.template(templateSource);
|
|
|
|
const visitor = {
|
|
[template]: sinon.spy(),
|
|
};
|
|
|
|
recurse.visit(espree.parse(source, parserOptions), templates.visitor(visitor));
|
|
t.is(visitor[template].called, expectedToMatch);
|
|
};
|
|
|
|
templateFoundInMacro.title = (_, templateSource, source, expectedToMatch = true) => {
|
|
return `\`${templateSource}\` ${expectedToMatch ? 'should be found in' : 'should not be found in'} \`${source}\``;
|
|
};
|
|
|
|
const templateMatchesMacro = (t, templateSource, source, expectedToMatch = true) => {
|
|
const wrap = s => `uniqueEnoughIdentifier((${s}))`;
|
|
templateFoundInMacro(t, wrap(templateSource), wrap(source), expectedToMatch);
|
|
};
|
|
|
|
templateMatchesMacro.title = (_, templateSource, source, expectedToMatch = true) => {
|
|
return `\`${templateSource}\` ${expectedToMatch ? 'should match' : 'should not match'} \`${source}\``;
|
|
};
|
|
|
|
test(templateMatchesMacro, 'foo', 'bar', false);
|
|
test(templateMatchesMacro, 'foo', 'foo');
|
|
|
|
test(templateFoundInMacro, 'x', '[a, b, c]', false);
|
|
test(templateFoundInMacro, 'b', '[a, b, c]');
|
|
|
|
test(templateMatchesMacro, '1', '2', false);
|
|
test(templateMatchesMacro, '1', '1');
|
|
|
|
test(templateFoundInMacro, '9', '[1, 2, 3]', false);
|
|
test(templateFoundInMacro, '2', '[1, 2, 3]');
|
|
|
|
test(templateFoundInMacro, '({})', '({a:[]})', false);
|
|
test(templateFoundInMacro, '({})', '[{}]');
|
|
|
|
test(templateMatchesMacro, '(() => {})', '(function() {})', false);
|
|
test(templateMatchesMacro, '(( ) => { })', '(()=>{})');
|
|
|
|
test(templateMatchesMacro, 'NaN', '-NaN', false);
|
|
test(templateMatchesMacro, 'NaN', 'NaN');
|
|
|
|
test(templateFoundInMacro, 'NaN', 'NaN');
|
|
test(templateFoundInMacro, 'NaN', '-NaN');
|
|
test(templateFoundInMacro, '-NaN', '+NaN', false);
|
|
test(templateFoundInMacro, '+NaN', '-NaN', false);
|
|
|
|
test(templateMatchesMacro, '/a/', '/a/g', false);
|
|
test(templateMatchesMacro, '/a/', '/a/');
|
|
|
|
test(templateFoundInMacro, '/x/', 'foo(/x/)');
|
|
test(templateFoundInMacro, '/x/', 'foo(/x/y)', false);
|
|
|
|
test(templateMatchesMacro, '0', '+0', false);
|
|
test(templateMatchesMacro, '0', '-0', false);
|
|
test(templateMatchesMacro, '0', '0');
|
|
|
|
test(templateFoundInMacro, '0', '+0');
|
|
test(templateFoundInMacro, '0', '-0');
|
|
test(templateFoundInMacro, '0', '0');
|
|
test(templateFoundInMacro, '-0', '0', false);
|
|
test(templateFoundInMacro, '+0', '0', false);
|
|
|
|
test('variable values', t => {
|
|
t.plan(6);
|
|
|
|
const templates = eslintTemplateVisitor();
|
|
|
|
const receiver = templates.variable();
|
|
const method = templates.variable();
|
|
const template = templates.template`${receiver}.${method}()`;
|
|
|
|
const visitor = {
|
|
[template](node) {
|
|
const receiverNode = template.context.getMatch(receiver);
|
|
const methodNode = template.context.getMatch(method);
|
|
|
|
t.is(node.type, 'CallExpression');
|
|
t.is(node.arguments.length, 0);
|
|
|
|
t.is(receiverNode.type, 'Identifier');
|
|
t.is(receiverNode.name, 'bar');
|
|
|
|
t.is(methodNode.type, 'Identifier');
|
|
t.is(methodNode.name, 'foo');
|
|
},
|
|
};
|
|
|
|
// Should match
|
|
recurse.visit(espree.parse('bar.foo()'), templates.visitor(visitor));
|
|
|
|
// Should not match
|
|
recurse.visit(espree.parse('bar.foo(argument)'), templates.visitor(visitor));
|
|
recurse.visit(espree.parse('bar.foo(...arguments)', parserOptions), templates.visitor(visitor));
|
|
});
|
|
|
|
test('`spreadVariable` matching arguments', t => {
|
|
const templates = eslintTemplateVisitor();
|
|
|
|
const argumentsVariable = templates.spreadVariable();
|
|
const template = templates.template`receiver.method(${argumentsVariable})`;
|
|
|
|
const recordedArguments = [];
|
|
|
|
const visitor = {
|
|
[template](node) {
|
|
const argumentNodes = template.context.getMatch(argumentsVariable);
|
|
|
|
recordedArguments.push(argumentNodes);
|
|
|
|
t.is(node.type, 'CallExpression');
|
|
t.is(node.arguments, argumentNodes);
|
|
},
|
|
};
|
|
|
|
recurse.visit(espree.parse('receiver.method()'), templates.visitor(visitor));
|
|
|
|
t.is(recordedArguments.length, 1);
|
|
t.deepEqual(recordedArguments[0], []);
|
|
|
|
recurse.visit(espree.parse('receiver.method(onlyArgument)'), templates.visitor(visitor));
|
|
|
|
t.is(recordedArguments.length, 2);
|
|
t.is(recordedArguments[1].length, 1);
|
|
|
|
recurse.visit(espree.parse('receiver.method(argument1, argument2)'), templates.visitor(visitor));
|
|
|
|
t.is(recordedArguments.length, 3);
|
|
t.is(recordedArguments[2].length, 2);
|
|
|
|
recurse.visit(espree.parse('receiver.method(...arguments)', parserOptions), templates.visitor(visitor));
|
|
|
|
t.is(recordedArguments.length, 4);
|
|
t.is(recordedArguments[3].length, 1);
|
|
t.is(recordedArguments[3][0].type, 'SpreadElement');
|
|
});
|
|
|
|
test('`spreadVariable` matching statements', t => {
|
|
const templates = eslintTemplateVisitor({ parserOptions });
|
|
|
|
const statementsVariable = templates.spreadVariable();
|
|
const template = templates.template`() => {${statementsVariable}}`;
|
|
|
|
const recordedStatements = [];
|
|
|
|
const visitor = {
|
|
[template](node) {
|
|
const statementNodes = template.context.getMatch(statementsVariable);
|
|
|
|
recordedStatements.push(statementNodes);
|
|
|
|
t.is(node.type, 'ArrowFunctionExpression');
|
|
t.is(node.body.type, 'BlockStatement');
|
|
t.is(node.body.body, statementNodes);
|
|
},
|
|
};
|
|
|
|
recurse.visit(espree.parse('() => {}', parserOptions), templates.visitor(visitor));
|
|
|
|
t.is(recordedStatements.length, 1);
|
|
t.deepEqual(recordedStatements[0], []);
|
|
|
|
recurse.visit(espree.parse('() => { onlyStatement; }', parserOptions), templates.visitor(visitor));
|
|
|
|
t.is(recordedStatements.length, 2);
|
|
t.is(recordedStatements[1].length, 1);
|
|
|
|
recurse.visit(espree.parse('() => { statement1; statement2 }', parserOptions), templates.visitor(visitor));
|
|
|
|
t.is(recordedStatements.length, 3);
|
|
t.is(recordedStatements[2].length, 2);
|
|
});
|
|
|
|
const omitLocation = omit([ 'start', 'end' ]);
|
|
|
|
test('variable unification', t => {
|
|
t.plan(6);
|
|
|
|
const templates = eslintTemplateVisitor();
|
|
|
|
const x = templates.variable();
|
|
const template = templates.template`${x} + ${x}`;
|
|
|
|
const visitor = {
|
|
[template](node) {
|
|
t.is(node.type, 'BinaryExpression');
|
|
|
|
const xNodes = template.context.getMatches(x);
|
|
|
|
t.is(xNodes.length, 2);
|
|
|
|
const [ x1, x2 ] = xNodes;
|
|
|
|
t.is(x1.type, 'Identifier');
|
|
t.is(x1.name, 'foo');
|
|
|
|
t.not(x1, x2);
|
|
t.deepEqual(omitLocation(x1), omitLocation(x2));
|
|
},
|
|
};
|
|
|
|
// Should match
|
|
recurse.visit(espree.parse('foo + foo'), templates.visitor(visitor));
|
|
|
|
// Should not match
|
|
recurse.visit(espree.parse('foo + bar'), templates.visitor(visitor));
|
|
recurse.visit(espree.parse('bar + foo'), templates.visitor(visitor));
|
|
});
|
|
|
|
test('fuzzing', t => {
|
|
const { rng } = t.context;
|
|
|
|
const templates = eslintTemplateVisitor({ parserOptions });
|
|
|
|
const totalTests = 2 ** 13;
|
|
let skippedTests = 0;
|
|
|
|
times(() => {
|
|
const randomShiftAST = fuzzProgram(new FuzzerState({ rng, maxDepth: 3 }));
|
|
const randomEspreeSafeShiftAST = shiftToEspreeSafe(randomShiftAST);
|
|
const randomJS = shiftCodegen(randomEspreeSafeShiftAST, new FormattedCodeGen()) || '"empty program";';
|
|
|
|
let randomTemplate;
|
|
let randomAST;
|
|
|
|
try {
|
|
randomTemplate = templates.template(randomJS);
|
|
randomAST = espree.parse(randomJS, parserOptions);
|
|
} catch (error) {
|
|
if (error.name === 'SyntaxError') {
|
|
// TODO: `shiftToEspreeSafe` or `fuzzProgram` should do a better job ensuring program is valid
|
|
console.warn('Ignored error:', error.name + ':', error.message);
|
|
skippedTests += 1;
|
|
return;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
const visitor = {
|
|
[randomTemplate]: sinon.spy(),
|
|
};
|
|
|
|
recurse.visit(randomAST, templates.visitor(visitor));
|
|
|
|
const { called } = visitor[randomTemplate];
|
|
|
|
if (!called) {
|
|
console.info(JSON.stringify({
|
|
randomJS,
|
|
randomEspreeSafeShiftAST,
|
|
randomAST,
|
|
}, null, 2));
|
|
}
|
|
|
|
t.true(called);
|
|
}, totalTests);
|
|
|
|
console.log({
|
|
skippedTests,
|
|
totalTests,
|
|
});
|
|
});
|