This commit is contained in:
2022-07-21 03:28:35 +00:00
parent d7c883d6df
commit 51b34b0e1d
30103 changed files with 4152204 additions and 23 deletions
+12
View File
@@ -0,0 +1,12 @@
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,*.yml}]
indent_style = space
indent_size = 2
+8
View File
@@ -0,0 +1,8 @@
language: node_js
node_js:
- 'node'
- '12'
- '10'
- '8'
after_success:
- './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls'
+192
View File
@@ -0,0 +1,192 @@
# ESLint Template Visitor
[![Build Status](https://travis-ci.org/futpib/eslint-template-visitor.svg?branch=master)](https://travis-ci.org/futpib/eslint-template-visitor) [![Coverage Status](https://coveralls.io/repos/github/futpib/eslint-template-visitor/badge.svg?branch=master)](https://coveralls.io/github/futpib/eslint-template-visitor?branch=master)
Simplify eslint rules by visiting templates
## Install
```
npm install eslint-template-visitor
# or
yarn add eslint-template-visitor
```
## Showcase
```diff
+const eslintTemplateVisitor = require('eslint-template-visitor');
+
+const templates = eslintTemplateVisitor();
+
+const objectVariable = templates.variable();
+const argumentsVariable = templates.spreadVariable();
+
+const substrCallTemplate = templates.template`${objectVariable}.substr(${argumentsVariable})`;
const create = context => {
const sourceCode = context.getSourceCode();
- return {
- CallExpression(node) {
- if (node.callee.type !== 'MemberExpression'
- || node.callee.property.type !== 'Identifier'
- || node.callee.property.name !== 'substr'
- ) {
- return;
- }
-
- const objectNode = node.callee.object;
+ return templates.visitor({
+ [substrCallTemplate](node) {
+ const objectNode = substrCallTemplate.context.getMatch(objectVariable);
+ const argumentNodes = substrCallTemplate.context.getMatch(argumentsVariable);
const problem = {
node,
message: 'Prefer `String#slice()` over `String#substr()`.',
};
- const canFix = node.arguments.length === 0;
+ const canFix = argumentNodes.length === 0;
if (canFix) {
problem.fix = fixer => fixer.replaceText(node, sourceCode.getText(objectNode) + '.slice()');
}
context.report(problem);
},
- };
+ });
};
```
See [examples](https://github.com/futpib/eslint-template-visitor/tree/master/examples) for more.
## API
### `eslintTemplateVisitor(options?)`
Craete a template visitor.
Example:
```js
const eslintTemplateVisitor = require('eslint-template-visitor');
const templates = eslintTemplateVisitor();
```
#### `options`
Type: `object`
##### `parserOptions`
Options for the template parser. Passed down to [`espree`](https://github.com/eslint/espree#usage).
Example:
```js
const templates = eslintTemplateVisitor({
parserOptions: {
ecmaVersion: 2018,
},
});
```
### `templates.variable()`
Create a variable to be used in a template. Such a variable can match exactly one AST node.
### `templates.spreadVariable()`
Create a spread variable. Spread variable can match an array of AST nodes.
This is useful for matching a number of arguments in a call or a number of statements in a block.
### `templates.template` tag
Creates a template possibly containing variables.
Example:
```js
const objectVariable = templates.variable();
const argumentsVariable = templates.spreadVariable();
const substrCallTemplate = templates.template`${objectVariable}.substr(${argumentsVariable})`;
const create = () => templates.visitor({
[substrCallTemplate](node) {
// `node` here is the matching `.substr` call (i.e. `CallExpression`)
}
});
```
### `templates.visitor({ /* visitors */ })`
Used to merge template visitors with [common ESLint visitors](https://eslint.org/docs/developer-guide/selectors#listening-for-selectors-in-rules).
Example:
```js
const create = () => templates.visitor({
[substrCallTemplate](node) {
// Template visitor
},
FunctionDeclaration(node) {
// Simple node type visitor
},
'IfStatement > BlockStatement'(node) {
// ESLint selector visitor
},
});
```
### `template.context`
A template match context. This property is defined only within a visitor call (in other words, only when working on a matching node).
Example:
```js
const create = () => templates.visitor({
[substrCallTemplate](node) {
// `substrCallTemplate.context` can be used here
},
FunctionDeclaration(node) {
// `substrCallTemplate.context` is not defined here, and it does not make sense to use it here,
// since we `substrCallTemplate` did not match an AST node.
},
});
```
#### `template.context.getMatch(variable)`
Used to get a match for a variable.
Example:
```js
const objectVariable = templates.variable();
const argumentsVariable = templates.spreadVariable();
const substrCallTemplate = templates.template`${objectVariable}.substr(${argumentsVariable})`;
const create = () => templates.visitor({
[substrCallTemplate](node) {
const objectNode = substrCallTemplate.context.getMatch(objectVariable);
// For example, let's check if `objectNode` is an `Identifier`: `objectNode.type === 'Identifier'`
const argumentNodes = substrCallTemplate.context.getMatch(argumentsVariable);
// `Array.isArray(argumentNodes) === true`
},
});
```
@@ -0,0 +1,43 @@
const errors = [ {
ruleId: 'prefer-string-slice',
} ];
module.exports = (ruleTester, rule) => ruleTester.run('prefer-string-slice', rule, {
valid: [
'foo.slice()',
'foo.slice(0)',
'foo.slice(1, 2)',
'foo.slice(-3, -2)',
],
invalid: [
{
code: 'foo.substr()',
output: 'foo.slice()',
errors,
},
{
code: '"foo".substr()',
output: '"foo".slice()',
errors,
},
{
code: 'foo.substr(start)',
errors,
},
{
code: '"foo".substr(1)',
errors,
},
{
code: 'foo.substr(start, length)',
errors,
},
{
code: '"foo".substr(1, 3)',
errors,
},
],
});
@@ -0,0 +1,41 @@
'use strict';
const eslintTemplateVisitor = require('../..');
const templates = eslintTemplateVisitor();
const objectVariable = templates.variable();
const argumentsVariable = templates.spreadVariable();
const substrCallTemplate = templates.template`${objectVariable}.substr(${argumentsVariable})`;
const create = context => {
const sourceCode = context.getSourceCode();
return templates.visitor({
[substrCallTemplate](node) {
const objectNode = substrCallTemplate.context.getMatch(objectVariable);
const argumentNodes = substrCallTemplate.context.getMatch(argumentsVariable);
const problem = {
node,
message: 'Prefer `String#slice()` over `String#substr()`.',
};
const canFix = argumentNodes.length === 0;
if (canFix) {
problem.fix = fixer => fixer.replaceText(node, sourceCode.getText(objectNode) + '.slice()');
}
context.report(problem);
},
});
};
module.exports = {
create,
meta: {
type: 'suggestion',
fixable: 'code',
},
};
@@ -0,0 +1,14 @@
import test from 'ava';
import avaRuleTester from 'eslint-ava-rule-tester';
import run from './_common';
import rule from './after';
const ruleTester = avaRuleTester(test, {
env: {
es6: true,
},
});
run(ruleTester, rule);
@@ -0,0 +1,39 @@
'use strict';
const create = context => {
const sourceCode = context.getSourceCode();
return {
CallExpression(node) {
if (node.callee.type !== 'MemberExpression'
|| node.callee.property.type !== 'Identifier'
|| node.callee.property.name !== 'substr'
) {
return;
}
const objectNode = node.callee.object;
const problem = {
node,
message: 'Prefer `String#slice()` over `String#substr()`.',
};
const canFix = node.arguments.length === 0;
if (canFix) {
problem.fix = fixer => fixer.replaceText(node, sourceCode.getText(objectNode) + '.slice()');
}
context.report(problem);
},
};
};
module.exports = {
create,
meta: {
type: 'suggestion',
fixable: 'code',
},
};
@@ -0,0 +1,14 @@
import test from 'ava';
import avaRuleTester from 'eslint-ava-rule-tester';
import run from './_common';
import rule from './before';
const ruleTester = avaRuleTester(test, {
env: {
es6: true,
},
});
run(ruleTester, rule);
+1
View File
@@ -0,0 +1 @@
module.exports = require('./lib');
+236
View File
@@ -0,0 +1,236 @@
const Multimap = require('multimap');
const espree = require('espree');
const { getMatchKeys } = require('./match-keys');
const { nodesPropertiesEqual } = require('./nodes-properties-equal');
const gensym = () => 'gensym' + Math.random().toString(16).slice(2);
class Variable {
constructor() {
this._id = gensym();
}
toString() {
return this._id;
}
}
class SpreadVariable extends Variable {}
class TemplateContext {
constructor() {
this._matches = new Multimap();
}
_pushVariableMatch(variableId, node) {
this._matches.set(variableId, node);
}
getMatches(variable) {
return this._matches.get(variable._id) || [];
}
getMatch(variable) {
return this.getMatches(variable)[0];
}
}
class Template {
constructor(source, options) {
const parserOptions = options.parserOptions || {
ecmaVersion: 2018,
};
this._id = gensym();
const { body: [ firstNode ] } = espree.parse(source, parserOptions);
this._ast = firstNode.type === 'ExpressionStatement' ? firstNode.expression : firstNode;
}
toString() {
return this._id;
}
}
class TemplateManager {
constructor(options = {}) {
this._options = options;
this._variables = new Map();
this._templates = new Map();
}
_matchTemplate(handler, template, node, ...rest) {
template.context = new TemplateContext();
if (this._nodeMatches(template._ast, node, template.context)) {
return handler(node, ...rest);
}
template.context = null;
}
_isNodeVariable(templateNode) {
return templateNode.type === 'Identifier'
&& this._variables.has(templateNode.name);
}
_getSpreadVariableNode(templateNode) {
if (templateNode.type === 'ExpressionStatement') {
templateNode = templateNode.expression;
}
if (!this._isNodeVariable(templateNode)) {
return undefined;
}
const variable = this._variables.get(templateNode.name);
return variable instanceof SpreadVariable
? templateNode
: undefined;
}
_nodeEquals(a, b) {
const { visitorKeys, equalityKeys } = getMatchKeys(a);
return visitorKeys.every(key => {
return this._nodePropertyEquals(key, a, b);
}) && equalityKeys.every(key => {
return nodesPropertiesEqual(a, b, key);
});
}
_everyNodeEquals(as, bs) {
return as.length === bs.length
&& as.every((a, index) => {
const b = bs[index];
return this._nodeEquals(a, b);
});
}
_nodePropertyEquals(key, a, b) {
if (Array.isArray(a[key])) {
return a[key].length === b[key].length
&& a[key].every((x, i) => this._nodeEquals(x, b[key][i]));
}
return this._nodeEquals(a[key], b[key]);
}
_nodeMatches(templateNode, node, context) {
if (!templateNode || !node) {
return templateNode === node;
}
if (this._isNodeVariable(templateNode)) {
const variable = this._variables.get(templateNode.name);
const previousMatches = context.getMatches(variable);
if (previousMatches.every(previousMatchNode => this._nodeEquals(previousMatchNode, node))) {
context._pushVariableMatch(templateNode.name, node);
return true;
}
return false;
}
const { visitorKeys, equalityKeys } = getMatchKeys(templateNode);
const matches = visitorKeys.every(key => {
return this._nodePropertyMatches(key, templateNode, node, context);
}) && equalityKeys.every(key => {
return nodesPropertiesEqual(templateNode, node, key);
});
return matches;
}
_spreadVariableMatches(templateNode, nodes, context) {
const variable = this._variables.get(templateNode.name);
const previousMatches = context.getMatches(variable);
if (previousMatches.every(previousMatchNodes => this._everyNodeEquals(previousMatchNodes, nodes))) {
context._pushVariableMatch(templateNode.name, nodes);
return true;
}
return false;
}
_nodePropertyMatches(key, templateNode, node, context) {
if (Array.isArray(templateNode[key])) {
if (!node[key]) {
return false;
}
if (templateNode[key].length === 1) {
const spreadVariableNode = this._getSpreadVariableNode(templateNode[key][0]);
if (spreadVariableNode) {
return this._spreadVariableMatches(spreadVariableNode, node[key], context);
}
}
return templateNode[key].length === node[key].length
&& templateNode[key].every((x, i) => this._nodeMatches(x, node[key][i], context));
}
return this._nodeMatches(templateNode[key], node[key], context);
}
variable() {
const variable = new Variable();
this._variables.set(variable._id, variable);
return variable;
}
spreadVariable() {
const variable = new SpreadVariable();
this._variables.set(variable._id, variable);
return variable;
}
template(strings, ...vars) {
const source = typeof strings === 'string'
? strings
: strings.map((string, i) => string + (vars[i] || '')).join('');
const template = new Template(source, this._options);
this._templates.set(template._id, template);
return template;
}
visitor(visitor) {
const newVisitor = {};
for (const key of Object.keys(visitor)) {
const value = visitor[key];
const template = this._templates.get(key);
const newKey = template ? template._ast.type : key;
const newValue = template ? (...args) => {
return this._matchTemplate(value, template, ...args);
} : value;
newVisitor[newKey] = newVisitor[newKey] || [];
newVisitor[newKey].push(newValue);
}
for (const newKey of Object.keys(newVisitor)) {
const newValue = newVisitor[newKey];
newVisitor[newKey] = newValue.length === 1 ? newValue[0] : (...args) => {
newValue.forEach(handler => handler(...args));
};
}
return newVisitor;
}
}
const eslintTemplateVisitor = options => new TemplateManager(options);
module.exports = eslintTemplateVisitor;
+364
View File
@@ -0,0 +1,364 @@
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,
});
});
+30
View File
@@ -0,0 +1,30 @@
const { KEYS, getKeys } = require('eslint-visitor-keys');
const ignoredKeys = new Set([
'start',
'end',
]);
const ignoredLiteralKeys = new Set([
'value',
'regex',
]);
const getMatchKeys = node => {
const keys = getKeys(node).filter(key => !ignoredKeys.has(key));
const visitorKeys = KEYS[node.type];
let equalityKeys = keys.filter(key => !visitorKeys.includes(key));
if (node.type === 'Literal') {
equalityKeys = equalityKeys.filter(key => !ignoredLiteralKeys.has(key));
}
return {
visitorKeys,
equalityKeys,
};
};
module.exports = {
getMatchKeys,
};
+12
View File
@@ -0,0 +1,12 @@
const nodesPropertiesEqual = (nodeA, nodeB, key) => {
if (nodeA.type === 'TemplateElement' && key === 'value') {
return nodeA.value.raw === nodeB.value.raw;
}
return nodeA[key] === nodeB[key];
};
module.exports = {
nodesPropertiesEqual,
};
+20
View File
@@ -0,0 +1,20 @@
const esrecurse = require('esrecurse');
const visit = (ast, visitor) => {
const newVisitor = {};
for (const key of Object.keys(visitor)) {
const value = visitor[key];
newVisitor[key] = function (node, ...rest) {
value.call(this, node, ...rest);
this.visitChildren(node);
};
}
esrecurse.visit(ast, newVisitor);
};
module.exports = {
visit,
};
+22
View File
@@ -0,0 +1,22 @@
import test from 'ava';
import sinon from 'sinon';
import espree from 'espree';
import recurse from './recurse';
test('recurse.visit', t => {
const ast = espree.parse(`
foo.parentNode.removeChild(foo);
foo.parentNode.removeChild(bar);
`);
const spy = sinon.spy();
recurse.visit(ast, {
MemberExpression: spy,
});
t.is(spy.callCount, 4);
});
+71
View File
@@ -0,0 +1,71 @@
{
"_args": [
[
"eslint-template-visitor@1.1.0",
"/home/node/nuxt"
]
],
"_development": true,
"_from": "eslint-template-visitor@1.1.0",
"_id": "eslint-template-visitor@1.1.0",
"_inBundle": false,
"_integrity": "sha512-Lmy6QVlmFiIGl5fPi+8ACnov3sare+0Ouf7deJAGGhmUfeWJ5fVarELUxZRpsZ9sHejiJUq8626d0dn9uvcZTw==",
"_location": "/eslint-template-visitor",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "eslint-template-visitor@1.1.0",
"name": "eslint-template-visitor",
"escapedName": "eslint-template-visitor",
"rawSpec": "1.1.0",
"saveSpec": null,
"fetchSpec": "1.1.0"
},
"_requiredBy": [
"/eslint-plugin-unicorn"
],
"_resolved": "https://registry.npmjs.org/eslint-template-visitor/-/eslint-template-visitor-1.1.0.tgz",
"_spec": "1.1.0",
"_where": "/home/node/nuxt",
"dependencies": {
"eslint-visitor-keys": "^1.1.0",
"espree": "^6.1.1",
"multimap": "^1.0.2"
},
"description": "[![Build Status](https://travis-ci.org/futpib/eslint-template-visitor.svg?branch=master)](https://travis-ci.org/futpib/eslint-template-visitor) [![Coverage Status](https://coveralls.io/repos/github/futpib/eslint-template-visitor/badge.svg?branch=master)](https://coveralls.io/github/futpib/eslint-template-visitor?branch=master)",
"devDependencies": {
"ava": "^2.4.0",
"coveralls": "^3.0.6",
"eslint": "^6.4.0",
"eslint-ava-rule-tester": "^3.0.0",
"eslint-config-xo-overrides": "^1.4.0",
"esrecurse": "^4.2.1",
"nyc": "^14.1.1",
"ramda": "^0.26.1",
"reserved-words": "^0.1.2",
"seedrandom": "^3.0.5",
"shift-codegen": "^6.0.0",
"shift-fuzzer": "^1.0.2",
"shift-parser": "^7.0.0",
"shift-reducer": "^6.0.0",
"shift-scope": "^4.0.0",
"sinon": "^7.5.0",
"xo": "^0.24.0"
},
"license": "GPL-3.0-or-later OR MIT",
"main": "index.js",
"name": "eslint-template-visitor",
"peerDependencies": {
"eslint": "^6.4.0"
},
"scripts": {
"test": "xo && nyc ava --verbose"
},
"version": "1.1.0",
"xo": {
"extends": [
"eslint-config-xo-overrides"
]
}
}
+283
View File
@@ -0,0 +1,283 @@
import reduceUsing, { CloneReducer } from 'shift-reducer';
import analyzeScope, { ScopeLookup } from 'shift-scope';
import { check as isReservedWord } from 'reserved-words';
class RegexReducer extends CloneReducer {
reduceLiteralRegExpExpression(node) {
return {
...node,
unicode: false,
};
}
}
class NoReservedKeywordsReducer extends CloneReducer {
isSafeName(name) {
return !isReservedWord(name, 3)
&& !isReservedWord(name, 6, true)
&& name !== 'arguments'
&& name !== 'eval';
}
reduceLabeledStatement(node, rest) {
if (this.isSafeName(node.label)) {
return super.reduceLabeledStatement(node, rest);
}
return {
...node,
...rest,
label: node.label + '_avoid_reserved_word',
};
}
reduceBindingIdentifier(node) {
if (this.isSafeName(node.name)) {
return node;
}
return {
...node,
name: node.name + '_avoid_reserved_word',
};
}
reduceAssignmentTargetIdentifier(...args) {
return this.reduceBindingIdentifier(...args);
}
reduceIdentifierExpression(...args) {
return this.reduceBindingIdentifier(...args);
}
}
class NoIdentifierDuplicatingAnImportReducer extends CloneReducer {
reduceImportNamespace(node, { namespaceBinding, ...rest }) {
return {
...node,
...rest,
namespaceBinding: namespaceBinding && {
...namespaceBinding,
name: namespaceBinding.name + '_avoid_capture',
},
};
}
}
class ScopeLookupCloneReducer extends CloneReducer {
constructor(scopeLookup) {
super();
this.scopeLookup = scopeLookup;
}
}
class NoUndeclaredExportReducer extends ScopeLookupCloneReducer {
reduceExportLocals(node, { namedExports, ...rest }) {
return {
...node,
...rest,
namedExports: namedExports.filter(export_ => !export_._isUndeclaredVariable),
};
}
reduceExportLocalSpecifier(node, { name: { _isUndeclaredVariable, ...name }, ...rest }) {
return {
...node,
...rest,
name,
_isUndeclaredVariable,
};
}
reduceIdentifierExpression(node) {
const [ variable ] = this.scopeLookup.lookup(node);
return {
...node,
_isUndeclaredVariable: variable && variable.declarations.length === 0,
};
}
}
const removingHOR = Reducer => class extends Reducer {
constructor(...args) {
super(...args);
this.nodesToRemove = new Set();
this.shouldKeep = statement => !this.nodesToRemove.has(statement);
}
markForRemoval(node) {
this.nodesToRemove.add(node);
}
propagateRemoval(from, to) {
if (this.nodesToRemove.has(from)) {
this.nodesToRemove.add(to);
}
}
reduceSwitchCase(node, { consequent, ...rest }) {
return {
...node,
...rest,
consequent: consequent.filter(this.shouldKeep),
};
}
reduceSwitchDefault(node, { consequent, ...rest }) {
return {
...node,
...rest,
consequent: consequent.filter(this.shouldKeep),
};
}
reduceScript(node, { statements, ...rest }) {
return {
...node,
...rest,
statements: statements.filter(this.shouldKeep),
};
}
reduceFunctionBody(node, { statements, ...rest }) {
return {
...node,
...rest,
statements: statements.filter(this.shouldKeep),
};
}
reduceBlock(node, { statements, ...rest }) {
return {
...node,
...rest,
statements: statements.filter(this.shouldKeep),
};
}
reduceVariableDeclaration(node, { declarators, ...rest }) {
declarators = declarators.filter(this.shouldKeep);
node = {
...node,
...rest,
declarators,
};
if (declarators.length === 0) {
this.markForRemoval(node);
}
return node;
}
reduceVariableDeclarationStatement(node, { declaration }) {
this.propagateRemoval(declaration, node);
return node;
}
reduceForStatement(node, { body }) {
this.propagateRemoval(body, node);
return node;
}
reduceWhileStatement(node, { body }) {
this.propagateRemoval(body, node);
return node;
}
reduceLabeledStatement(node, { body }) {
this.propagateRemoval(body, node);
return node;
}
reduceForInStatement(node, { body }) {
this.propagateRemoval(body, node);
return node;
}
reduceIfStatement(node, { consequent, alternate }) {
this.propagateRemoval(consequent, node);
this.propagateRemoval(alternate, node);
return node;
}
reduceDoWhileStatement(node, { body }) {
this.propagateRemoval(body, node);
return node;
}
};
class NoNonStrictFeaturesReducer extends removingHOR(CloneReducer) {
reduceWithStatement(node) {
this.markForRemoval(node);
return node;
}
reduceUnaryExpression(node) {
if (node.operator === 'delete' && node.operand.type === 'IdentifierExpression') {
return node.operand;
}
return node;
}
}
class NoLabeledFunctionDeclarationReducer extends CloneReducer {
reduceLabeledStatement(node, { body }) {
if (body.type === 'FunctionDeclaration') {
return body;
}
return node;
}
}
class NoLabeledBreakContinueReducer extends CloneReducer {
reduceBreakStatement(node) {
return {
...node,
label: null,
};
}
}
class UniqueBindingIdentifiersReducer extends CloneReducer {
constructor() {
super();
this.boundNames = new Set();
}
reduceBindingIdentifier(node) {
if (!this.boundNames.has(node.name)) {
this.boundNames.add(node.name);
return node;
}
return {
...node,
name: node.name + Math.random().toString(16).slice(2),
};
}
}
export default shiftAST => {
[
NoNonStrictFeaturesReducer,
RegexReducer,
NoReservedKeywordsReducer,
NoIdentifierDuplicatingAnImportReducer,
NoUndeclaredExportReducer,
NoLabeledFunctionDeclarationReducer,
UniqueBindingIdentifiersReducer,
NoLabeledBreakContinueReducer,
].forEach(Reducer => {
const scope = analyzeScope(shiftAST);
const scopeLookup = new ScopeLookup(scope);
shiftAST = reduceUsing(new Reducer(scopeLookup), shiftAST);
});
return shiftAST;
};