Skip to content

Commit

Permalink
Switch to babel-eslint (but test both espree and babel-eslint)
Browse files Browse the repository at this point in the history
  • Loading branch information
futpib committed Jul 3, 2020
1 parent aa0f37f commit a607fcd
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 111 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Type: `object`

##### `parserOptions`

Options for the template parser. Passed down to [`espree`](https://github.com/eslint/espree#usage).
Options for the template parser. Passed down to [`babel-eslint`](https://github.com/babel/babel-eslint#additional-parser-configuration).

Example:

Expand Down
16 changes: 11 additions & 5 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

const Multimap = require('multimap');

const espree = require('espree');
const { parse } = require('babel-eslint');
const { query } = require('esquery');

const { getMatchKeys } = require('./match-keys');
Expand All @@ -10,8 +10,11 @@ const { nodesPropertiesEqual } = require('./nodes-properties-equal');
const gensym = () => 'gensym' + Math.random().toString(16).slice(2);

const nodeEquals = (a, b) => {
if (a === null || b === null) {
return a === b;
if (!a || !b) {
return (
(a === null || a === undefined)
&& (b === null || b === undefined)
);
}

const { visitorKeys, equalityKeys } = getMatchKeys(a);
Expand Down Expand Up @@ -91,7 +94,7 @@ class Template {
ecmaVersion: 2018,
};

const { body } = espree.parse(ast, parserOptions);
const { body } = parse(ast, parserOptions);

if (body.length !== 1) {
throw new Error('Template must contain a single top-level AST node.');
Expand Down Expand Up @@ -171,7 +174,10 @@ class TemplateManager {

_nodeMatches(templateNode, node, context) {
if (!templateNode || !node) {
return templateNode === node;
return (
(templateNode === null || templateNode === undefined)
&& (node === null || node === undefined)
);
}

const variable = this._getNodeVariable(templateNode);
Expand Down
113 changes: 81 additions & 32 deletions lib/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ const sinon = require('sinon');

const { omit, times } = require('ramda');

const espree = require('espree');

const { default: fuzzProgram, FuzzerState } = require('shift-fuzzer');
const { default: shiftCodegen, FormattedCodeGen } = require('shift-codegen');

const { parse: babelEslintParse } = require('babel-eslint');

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-workarounds');
const { parse, PARSER } = require('../test/_parser');

const recurse = require('./recurse');

Expand Down Expand Up @@ -42,7 +44,7 @@ test('mixing templates into a visitor', t => {
const a = templates.variable();
const template = templates.template`${a}.parentNode.removeChild(${a})`;

const ast = espree.parse(`
const ast = parse(`
foo.parentNode.removeChild(foo);
foo.parentNode.removeChild(bar);
`);
Expand Down Expand Up @@ -89,10 +91,10 @@ test('variable matching', t => {
[template]: sinon.spy(),
};

recurse.visit(espree.parse('foo.bar()'), templates.visitor(visitor));
recurse.visit(parse('foo.bar()'), templates.visitor(visitor));
t.false(visitor[template].called);

recurse.visit(espree.parse('bar.foo()'), templates.visitor(visitor));
recurse.visit(parse('bar.foo()'), templates.visitor(visitor));
t.true(visitor[template].called);
});

Expand All @@ -104,7 +106,7 @@ const templateFoundInMacro = (t, templateSource, source, expectedToMatch = true)
[template]: sinon.spy(),
};

recurse.visit(espree.parse(source, parserOptions), templates.visitor(visitor));
recurse.visit(parse(source, parserOptions), templates.visitor(visitor));
t.is(visitor[template].called, expectedToMatch);
};

Expand All @@ -124,6 +126,16 @@ templateMatchesMacro.title = (_, templateSource, source, expectedToMatch = true)
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]');

Expand Down Expand Up @@ -189,11 +201,11 @@ test('variable values', t => {
};

// Should match
recurse.visit(espree.parse('bar.foo()'), templates.visitor(visitor));
recurse.visit(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));
recurse.visit(parse('bar.foo(argument)'), templates.visitor(visitor));
recurse.visit(parse('bar.foo(...arguments)', parserOptions), templates.visitor(visitor));
});

test('`spreadVariable` matching arguments', t => {
Expand All @@ -215,22 +227,22 @@ test('`spreadVariable` matching arguments', t => {
},
};

recurse.visit(espree.parse('receiver.method()'), templates.visitor(visitor));
recurse.visit(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));
recurse.visit(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));
recurse.visit(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));
recurse.visit(parse('receiver.method(...arguments)', parserOptions), templates.visitor(visitor));

t.is(recordedArguments.length, 4);
t.is(recordedArguments[3].length, 1);
Expand All @@ -257,17 +269,17 @@ test('`spreadVariable` matching statements', t => {
},
};

recurse.visit(espree.parse('() => {}', parserOptions), templates.visitor(visitor));
recurse.visit(parse('() => {}', parserOptions), templates.visitor(visitor));

t.is(recordedStatements.length, 1);
t.deepEqual(recordedStatements[0], []);

recurse.visit(espree.parse('() => { onlyStatement; }', parserOptions), templates.visitor(visitor));
recurse.visit(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));
recurse.visit(parse('() => { statement1; statement2 }', parserOptions), templates.visitor(visitor));

t.is(recordedStatements.length, 3);
t.is(recordedStatements[2].length, 2);
Expand Down Expand Up @@ -295,25 +307,25 @@ test('`variableDeclarationVariable` matching variable declarations', t => {
},
};

recurse.visit(espree.parse('() => {}', parserOptions), templates.visitor(visitor));
recurse.visit(parse('() => {}', parserOptions), templates.visitor(visitor));

t.deepEqual(recordedVariableDeclarations, []);

recurse.visit(espree.parse('() => { x = y; }', parserOptions), templates.visitor(visitor));
recurse.visit(parse('() => { x = y; }', parserOptions), templates.visitor(visitor));

t.deepEqual(recordedVariableDeclarations, []);

recurse.visit(espree.parse('() => { const x = y; }', parserOptions), templates.visitor(visitor));
recurse.visit(parse('() => { const x = y; }', parserOptions), templates.visitor(visitor));

t.is(recordedVariableDeclarations.length, 1);
t.is(recordedVariableDeclarations[0].kind, 'const');

recurse.visit(espree.parse('() => { let x = y; }', parserOptions), templates.visitor(visitor));
recurse.visit(parse('() => { let x = y; }', parserOptions), templates.visitor(visitor));

t.is(recordedVariableDeclarations.length, 2);
t.is(recordedVariableDeclarations[1].kind, 'let');

recurse.visit(espree.parse('() => { var x = y; }', parserOptions), templates.visitor(visitor));
recurse.visit(parse('() => { var x = y; }', parserOptions), templates.visitor(visitor));

t.is(recordedVariableDeclarations.length, 3);
t.is(recordedVariableDeclarations[2].kind, 'var');
Expand Down Expand Up @@ -344,13 +356,18 @@ test('`variableDeclarationVariable` unification', t => {
},
};

recurse.visit(espree.parse('() => { var x; var x; }', parserOptions), templates.visitor(visitor));
recurse.visit(parse('() => { var x; var x; }', parserOptions), templates.visitor(visitor));

t.is(recordedVariableDeclarations.length, 1);
t.is(recordedVariableDeclarations[0].kind, 'var');
});

const omitLocation = omit([ 'start', 'end' ]);
const omitLocation = omit([
'start',
'end',
'loc',
'range',
]);

test('variable unification', t => {
t.plan(6);
Expand Down Expand Up @@ -379,11 +396,11 @@ test('variable unification', t => {
};

// Should match
recurse.visit(espree.parse('foo + foo'), templates.visitor(visitor));
recurse.visit(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));
recurse.visit(parse('foo + bar'), templates.visitor(visitor));
recurse.visit(parse('bar + foo'), templates.visitor(visitor));
});

test('throws on multiple top-level nodes in a template', t => {
Expand All @@ -407,11 +424,29 @@ test('narrow to a statement with await', t => {
[template]: sinon.spy(),
};

recurse.visit(espree.parse(source, parserOptions), templates.visitor(visitor));
recurse.visit(parse(source, parserOptions), templates.visitor(visitor));
t.true(visitor[template].called);
t.is(visitor[template].callCount, 1);
});

if (PARSER === 'babel-eslint') {
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;

Expand All @@ -425,12 +460,24 @@ test('fuzzing', t => {
const randomEspreeSafeShiftAST = shiftToEspreeSafe(dropExtraTopLevelNodes(randomShiftAST));
const randomJS = shiftCodegen(randomEspreeSafeShiftAST, new FormattedCodeGen()) || '"empty program";';

try {
const { shouldSkip } = babelEslintWorkarounds(babelEslintParse(randomJS));

if (shouldSkip) {
console.warn('Ignored a random script due to a `babel-eslint` bug (this is fine):', randomJS);
skippedTests += 1;
return;
}
} catch {
/* Pass */
}

let randomTemplate;
let randomAST;

try {
randomTemplate = templates.template(randomJS);
randomAST = espree.parse(randomJS, parserOptions);
randomAST = parse(randomJS, parserOptions);
} catch (error) {
if (error.name === 'SyntaxError') {
// TODO: `shiftToEspreeSafe` or `fuzzProgram` should do a better job ensuring program is valid
Expand All @@ -450,15 +497,17 @@ test('fuzzing', t => {

const { called } = visitor[randomTemplate];

t.true(called);

if (!called) {
console.info(JSON.stringify({
console.dir({
randomJS,
randomEspreeSafeShiftAST,
randomAST,
}, null, 2));
}
}, { depth: null });

t.true(called);
throw new Error('Fuzzing test failed. This error is thrown just to stop this long test early.');
}
}, totalTests);

console.log({
Expand Down
12 changes: 12 additions & 0 deletions lib/match-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ const { KEYS, getKeys } = require('eslint-visitor-keys');
const ignoredKeys = new Set([
'start',
'end',
'loc',
'range',
'extra',
'_babelType',

// TODO: probably should not ignore (not sure yet)
'typeArguments',
'exportKind',
'importKind',

// TODO: should not ignore
'optional',
]);

const ignoredLiteralKeys = new Set([
Expand Down
4 changes: 2 additions & 2 deletions lib/recurse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
const test = require('ava');
const sinon = require('sinon');

const espree = require('espree');
const { parse } = require('babel-eslint');

const recurse = require('./recurse');

test('recurse.visit', t => {
const ast = espree.parse(`
const ast = parse(`
foo.parentNode.removeChild(foo);
foo.parentNode.removeChild(bar);
`);
Expand Down
Loading

0 comments on commit a607fcd

Please sign in to comment.