eslint-plugin-unicorn
Version:
Various awesome ESLint rules
254 lines (230 loc) • 7.26 kB
JavaScript
'use strict';
const {
methodCallSelector,
arrayPrototypeMethodSelector,
emptyArraySelector,
callExpressionSelector
} = require('./selectors/index.js');
const needsSemicolon = require('./utils/needs-semicolon.js');
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js');
const {isNodeMatches, isNodeMatchesNameOrPath} = require('./utils/is-node-matches.js');
const {getParenthesizedText, isParenthesized} = require('./utils/parentheses.js');
const MESSAGE_ID = 'prefer-array-flat';
const messages = {
[MESSAGE_ID]: 'Prefer `Array#flat()` over `{{description}}` to flatten an array.'
};
// `array.flatMap(x => x)`
const arrayFlatMap = {
selector: [
methodCallSelector({
name: 'flatMap',
length: 1
}),
'[arguments.0.type="ArrowFunctionExpression"]',
'[arguments.0.async!=true]',
'[arguments.0.generator!=true]',
'[arguments.0.params.length=1]',
'[arguments.0.params.0.type="Identifier"]',
'[arguments.0.body.type="Identifier"]'
].join(''),
testFunction: node => node.arguments[0].params[0].name === node.arguments[0].body.name,
getArrayNode: node => node.callee.object,
description: 'Array#flatMap()'
};
// `array.reduce((a, b) => a.concat(b), [])`
const arrayReduce = {
selector: [
methodCallSelector({
name: 'reduce',
length: 2
}),
'[arguments.0.type="ArrowFunctionExpression"]',
'[arguments.0.async!=true]',
'[arguments.0.generator!=true]',
'[arguments.0.params.length=2]',
'[arguments.0.params.0.type="Identifier"]',
'[arguments.0.params.1.type="Identifier"]',
methodCallSelector({
name: 'concat',
length: 1,
path: 'arguments.0.body'
}),
'[arguments.0.body.callee.object.type="Identifier"]',
'[arguments.0.body.arguments.0.type="Identifier"]',
emptyArraySelector('arguments.1')
].join(''),
testFunction: node => node.arguments[0].params[0].name === node.arguments[0].body.callee.object.name &&
node.arguments[0].params[1].name === node.arguments[0].body.arguments[0].name,
getArrayNode: node => node.callee.object,
description: 'Array#reduce()'
};
// `array.reduce((a, b) => [...a, ...b], [])`
const arrayReduce2 = {
selector: [
methodCallSelector({
name: 'reduce',
length: 2
}),
'[arguments.0.type="ArrowFunctionExpression"]',
'[arguments.0.async!=true]',
'[arguments.0.generator!=true]',
'[arguments.0.params.length=2]',
'[arguments.0.params.0.type="Identifier"]',
'[arguments.0.params.1.type="Identifier"]',
'[arguments.0.body.type="ArrayExpression"]',
'[arguments.0.body.elements.length=2]',
'[arguments.0.body.elements.0.type="SpreadElement"]',
'[arguments.0.body.elements.0.argument.type="Identifier"]',
'[arguments.0.body.elements.1.type="SpreadElement"]',
'[arguments.0.body.elements.1.argument.type="Identifier"]',
emptyArraySelector('arguments.1')
].join(''),
testFunction: node => node.arguments[0].params[0].name === node.arguments[0].body.elements[0].argument.name &&
node.arguments[0].params[1].name === node.arguments[0].body.elements[1].argument.name,
getArrayNode: node => node.callee.object,
description: 'Array#reduce()'
};
// `[].concat(maybeArray)` and `[].concat(...array)`
const emptyArrayConcat = {
selector: [
methodCallSelector({
name: 'concat',
length: 1,
allowSpreadElement: true
}),
emptyArraySelector('callee.object')
].join(''),
getArrayNode: node => {
const argumentNode = node.arguments[0];
return argumentNode.type === 'SpreadElement' ? argumentNode.argument : argumentNode;
},
description: '[].concat()',
shouldSwitchToArray: node => node.arguments[0].type !== 'SpreadElement'
};
// - `[].concat.apply([], array)` and `Array.prototype.concat.apply([], array)`
// - `[].concat.call([], maybeArray)` and `Array.prototype.concat.call([], maybeArray)`
// - `[].concat.call([], ...array)` and `Array.prototype.concat.call([], ...array)`
const arrayPrototypeConcat = {
selector: [
methodCallSelector({
names: ['apply', 'call'],
length: 2,
allowSpreadElement: true
}),
emptyArraySelector('arguments.0'),
arrayPrototypeMethodSelector({
path: 'callee.object',
name: 'concat'
})
].join(''),
testFunction: node => node.arguments[1].type !== 'SpreadElement' || node.callee.property.name === 'call',
getArrayNode: node => {
const argumentNode = node.arguments[1];
return argumentNode.type === 'SpreadElement' ? argumentNode.argument : argumentNode;
},
description: 'Array.prototype.concat()',
shouldSwitchToArray: node => node.arguments[1].type !== 'SpreadElement' && node.callee.property.name === 'call'
};
const lodashFlattenFunctions = [
'_.flatten',
'lodash.flatten',
'underscore.flatten'
];
const anyCall = {
selector: callExpressionSelector({length: 1}),
getArrayNode: node => node.arguments[0]
};
function fix(node, array, sourceCode, shouldSwitchToArray) {
if (typeof shouldSwitchToArray === 'function') {
shouldSwitchToArray = shouldSwitchToArray(node);
}
return fixer => {
let fixed = getParenthesizedText(array, sourceCode);
if (shouldSwitchToArray) {
// `array` is an argument, when it changes to `array[]`, we don't need add extra parentheses
fixed = `[${fixed}]`;
// And we don't need to add parentheses to the new array to call `.flat()`
} else if (
!isParenthesized(array, sourceCode) &&
shouldAddParenthesesToMemberExpressionObject(array, sourceCode)
) {
fixed = `(${fixed})`;
}
fixed = `${fixed}.flat()`;
const tokenBefore = sourceCode.getTokenBefore(node);
if (needsSemicolon(tokenBefore, sourceCode, fixed)) {
fixed = `;${fixed}`;
}
return fixer.replaceText(node, fixed);
};
}
function create(context) {
const {functions: configFunctions} = {
functions: [],
...context.options[0]
};
const functions = [...configFunctions, ...lodashFlattenFunctions];
const sourceCode = context.getSourceCode();
const listeners = {};
const cases = [
arrayFlatMap,
arrayReduce,
arrayReduce2,
emptyArrayConcat,
arrayPrototypeConcat,
{
...anyCall,
testFunction: node => isNodeMatches(node.callee, functions),
description: node => `${functions.find(nameOrPath => isNodeMatchesNameOrPath(node.callee, nameOrPath)).trim()}()`
}
];
for (const {selector, testFunction, description, getArrayNode, shouldSwitchToArray} of cases) {
listeners[selector] = function (node) {
if (testFunction && !testFunction(node)) {
return;
}
const array = getArrayNode(node);
const data = {
description: typeof description === 'string' ? description : description(node)
};
const problem = {
node,
messageId: MESSAGE_ID,
data
};
// Don't fix if it has comments.
if (
sourceCode.getCommentsInside(node).length ===
sourceCode.getCommentsInside(array).length
) {
problem.fix = fix(node, array, sourceCode, shouldSwitchToArray);
}
return problem;
};
}
return listeners;
}
const schema = [
{
type: 'object',
properties: {
functions: {
type: 'array',
uniqueItems: true
}
},
additionalProperties: false
}
];
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `Array#flat()` over legacy techniques to flatten arrays.'
},
fixable: 'code',
schema,
messages
}
};