123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- const test = require('ava');
- const sinon = require('sinon');
- const { omit, times } = require('ramda');
- const { default: fuzzProgram, FuzzerState } = require('shift-fuzzer');
- const { default: shiftCodegen, FormattedCodeGen } = require('shift-codegen');
- const { parse: babelEslintParse } = require('@babel/eslint-parser');
- const seedrandom = require('seedrandom');
- const shiftToEspreeSafe = require('../test/_shift-to-espree-safe');
- const dropExtraTopLevelNodes = require('../test/_drop-extra-top-level-nodes');
- const babelEslintWorkarounds = require('../test/_babel-eslint-parser-workarounds');
- const { parse, PARSER } = require('../test/_parser');
- const recurse = require('./recurse');
- const eslintTemplateVisitor = require('.');
- 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)} npm test
- \`\`\`
- `);
- const parserOptions = {
- sourceType: 'module',
- ecmaVersion: 2018,
- requireConfigFile: false,
- };
- 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 = parse(`
- foo.parentNode.removeChild(foo);
- foo.parentNode.removeChild(bar);
- `, parserOptions);
- 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(parse('foo.bar()', parserOptions), templates.visitor(visitor));
- t.false(visitor[template].called);
- recurse.visit(parse('bar.foo()', parserOptions), templates.visitor(visitor));
- t.true(visitor[template].called);
- });
- const templateFoundInMacro = (t, templateSource, source, expectedToMatch = true) => {
- const templates = eslintTemplateVisitor({ parserOptions });
- const template = templates.template(templateSource);
- const visitor = {
- [template]: sinon.spy(),
- };
- recurse.visit(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(templateMatchesMacro, 'foo.bar', 'foo.bar');
- test(templateMatchesMacro, 'foo.bar', 'foo["bar"]', false);
- test(templateMatchesMacro, 'foo.bar', 'foo.quz', false);
- test(templateMatchesMacro, 'foo.bar', 'quz.bar', false);
- test(templateMatchesMacro, 'foo.bar()', 'foo.bar()');
- test(templateMatchesMacro, 'foo.bar()', 'foo["bar"]()', false);
- test(templateMatchesMacro, 'foo.bar()', 'foo.quz()', false);
- test(templateMatchesMacro, 'foo.bar()', 'quz.bar()', false);
- 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(parse('bar.foo()', parserOptions), templates.visitor(visitor));
- // Should not match
- recurse.visit(parse('bar.foo(argument)', parserOptions), templates.visitor(visitor));
- recurse.visit(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(parse('receiver.method()', parserOptions), templates.visitor(visitor));
- t.is(recordedArguments.length, 1);
- t.deepEqual(recordedArguments[0], []);
- recurse.visit(parse('receiver.method(onlyArgument)', parserOptions), templates.visitor(visitor));
- t.is(recordedArguments.length, 2);
- t.is(recordedArguments[1].length, 1);
- recurse.visit(parse('receiver.method(argument1, argument2)', parserOptions), templates.visitor(visitor));
- t.is(recordedArguments.length, 3);
- t.is(recordedArguments[2].length, 2);
- recurse.visit(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(parse('() => {}', parserOptions), templates.visitor(visitor));
- t.is(recordedStatements.length, 1);
- t.deepEqual(recordedStatements[0], []);
- recurse.visit(parse('() => { onlyStatement; }', parserOptions), templates.visitor(visitor));
- t.is(recordedStatements.length, 2);
- t.is(recordedStatements[1].length, 1);
- recurse.visit(parse('() => { statement1; statement2 }', parserOptions), templates.visitor(visitor));
- t.is(recordedStatements.length, 3);
- t.is(recordedStatements[2].length, 2);
- });
- test('`variableDeclarationVariable` matching variable declarations', t => {
- const templates = eslintTemplateVisitor({ parserOptions });
- const variableDeclarationVariable = templates.variableDeclarationVariable();
- const template = templates.template`() => {
- ${variableDeclarationVariable} x = y;
- }`;
- const recordedVariableDeclarations = [];
- const visitor = {
- [template](node) {
- const variableDeclarationNode = template.context.getMatch(variableDeclarationVariable);
- recordedVariableDeclarations.push(variableDeclarationNode);
- t.is(node.type, 'ArrowFunctionExpression');
- t.is(node.body.type, 'BlockStatement');
- t.deepEqual(node.body.body[0], variableDeclarationNode);
- },
- };
- recurse.visit(parse('() => {}', parserOptions), templates.visitor(visitor));
- t.deepEqual(recordedVariableDeclarations, []);
- recurse.visit(parse('() => { x = y; }', parserOptions), templates.visitor(visitor));
- t.deepEqual(recordedVariableDeclarations, []);
- recurse.visit(parse('() => { const x = y; }', parserOptions), templates.visitor(visitor));
- t.is(recordedVariableDeclarations.length, 1);
- t.is(recordedVariableDeclarations[0].kind, 'const');
- recurse.visit(parse('() => { let x = y; }', parserOptions), templates.visitor(visitor));
- t.is(recordedVariableDeclarations.length, 2);
- t.is(recordedVariableDeclarations[1].kind, 'let');
- recurse.visit(parse('() => { var x = y; }', parserOptions), templates.visitor(visitor));
- t.is(recordedVariableDeclarations.length, 3);
- t.is(recordedVariableDeclarations[2].kind, 'var');
- });
- test('`variableDeclarationVariable` unification', t => {
- const templates = eslintTemplateVisitor({ parserOptions });
- const variableDeclarationVariable = templates.variableDeclarationVariable();
- const variableVariable = templates.variable();
- const template = templates.template`{
- ${variableDeclarationVariable} ${variableVariable};
- ${variableDeclarationVariable} ${variableVariable};
- }`;
- const recordedVariableDeclarations = [];
- const recordedVariables = [];
- const visitor = {
- [template](node) {
- const variableDeclarationNode = template.context.getMatch(variableDeclarationVariable);
- const variableNode = template.context.getMatch(variableVariable);
- recordedVariableDeclarations.push(variableDeclarationNode);
- recordedVariables.push(variableNode);
- t.is(node.type, 'BlockStatement');
- },
- };
- recurse.visit(parse('() => { var x; var x; }', parserOptions), templates.visitor(visitor));
- t.is(recordedVariableDeclarations.length, 1);
- t.is(recordedVariableDeclarations[0].kind, 'var');
- t.is(recordedVariables.length, 1);
- t.is(recordedVariables[0].type, 'Identifier');
- t.is(recordedVariables[0].name, 'x');
- });
- const omitLocation = omit([
- 'start',
- 'end',
- 'loc',
- 'range',
- ]);
- 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(parse('foo + foo', parserOptions), templates.visitor(visitor));
- // Should not match
- recurse.visit(parse('foo + bar', parserOptions), templates.visitor(visitor));
- recurse.visit(parse('bar + foo', parserOptions), templates.visitor(visitor));
- });
- test('throws on multiple top-level nodes in a template', t => {
- const templates = eslintTemplateVisitor({ parserOptions });
- t.throws(() => {
- return templates.template`a; b;`;
- });
- });
- test('narrow to a statement with await', t => {
- const templates = eslintTemplateVisitor({ parserOptions });
- const template = templates.template`
- async () => { await 1; }
- `.narrow('BlockStatement > :has(AwaitExpression)');
- const source = 'async () => { await 0; await 1; await 2; }';
- const visitor = {
- [template]: sinon.spy(),
- };
- recurse.visit(parse(source, parserOptions), templates.visitor(visitor));
- t.true(visitor[template].called);
- t.is(visitor[template].callCount, 1);
- });
- if (PARSER === '@babel/eslint-parser') {
- test('dynamic import', t => {
- const templates = eslintTemplateVisitor({ parserOptions });
- const template = templates.template`import('foo')`;
- const source = 'async () => { await import(\'bar\'); await import(\'foo\'); await import(\'qux\'); }';
- const visitor = {
- [template]: sinon.spy(),
- };
- recurse.visit(parse(source, parserOptions), templates.visitor(visitor));
- t.true(visitor[template].called);
- t.is(visitor[template].callCount, 1);
- });
- }
- 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(dropExtraTopLevelNodes(randomShiftAST));
- const randomJS = shiftCodegen(randomEspreeSafeShiftAST, new FormattedCodeGen()) || '"empty program";';
- try {
- const { shouldSkip } = babelEslintWorkarounds(babelEslintParse(randomJS, parserOptions));
- if (shouldSkip) {
- console.warn('Ignored a random script due to a `@babel/eslint-parser` bug (this is fine):', randomJS);
- skippedTests += 1;
- return;
- }
- } catch (error) {
- if (error.name === 'SyntaxError') {
- // TODO: `shiftToEspreeSafe` or `fuzzProgram` should do a better job ensuring program is valid
- console.warn('Ignored error (this is fine):', error.name + ':', error.message);
- skippedTests += 1;
- return;
- }
- throw error;
- }
- const randomTemplate = templates.template(randomJS);
- const randomAST = parse(randomJS, parserOptions);
- const visitor = {
- [randomTemplate]: sinon.spy(),
- };
- recurse.visit(randomAST, templates.visitor(visitor));
- const { called } = visitor[randomTemplate];
- t.true(called);
- if (!called) {
- console.dir({
- randomJS,
- randomEspreeSafeShiftAST,
- randomAST,
- }, { depth: null });
- throw new Error('Fuzzing test failed. This error is thrown just to stop this long test early.');
- }
- }, totalTests);
- console.log({
- skippedTests,
- totalTests,
- });
- });
|