eslint-plugin-unicorn
Version:
Various awesome ESLint rules
317 lines (278 loc) • 9.2 kB
JavaScript
'use strict';
const {isParenthesized, findVariable} = require('eslint-utils');
const getDocumentationUrl = require('./utils/get-documentation-url');
const methodSelector = require('./utils/method-selector');
const getVariableIdentifiers = require('./utils/get-variable-identifiers');
const ERROR_ZERO_INDEX = 'error-zero-index';
const ERROR_SHIFT = 'error-shift';
const ERROR_DESTRUCTURING_DECLARATION = 'error-destructuring-declaration';
const ERROR_DESTRUCTURING_ASSIGNMENT = 'error-destructuring-assignment';
const ERROR_DECLARATION = 'error-variable';
const SUGGESTION_NULLISH_COALESCING_OPERATOR = 'suggest-nullish-coalescing-operator';
const SUGGESTION_LOGICAL_OR_OPERATOR = 'suggest-logical-or-operator';
const messages = {
[ERROR_DECLARATION]: 'Prefer `.find(…)` over `.filter(…)`.',
[ERROR_ZERO_INDEX]: 'Prefer `.find(…)` over `.filter(…)[0]`.',
[ERROR_SHIFT]: 'Prefer `.find(…)` over `.filter(…).shift()`.',
[ERROR_DESTRUCTURING_DECLARATION]: 'Prefer `.find(…)` over destructuring `.filter(…)`.',
// Same message as `ERROR_DESTRUCTURING_DECLARATION`, but different case
[ERROR_DESTRUCTURING_ASSIGNMENT]: 'Prefer `.find(…)` over destructuring `.filter(…)`.',
[SUGGESTION_NULLISH_COALESCING_OPERATOR]: 'Replace `.filter(…)` with `.find(…) ?? …`.',
[SUGGESTION_LOGICAL_OR_OPERATOR]: 'Replace `.filter(…)` with `.find(…) || …`.'
};
const filterMethodSelectorOptions = {
name: 'filter',
min: 1,
max: 2
};
const filterVariableSelector = [
'VariableDeclaration',
// Exclude `export const foo = [];`
`:not(${
[
'ExportNamedDeclaration',
'>',
'VariableDeclaration.declaration'
].join('')
})`,
'>',
'VariableDeclarator.declarations',
'[id.type="Identifier"]',
methodSelector({
...filterMethodSelectorOptions,
property: 'init'
})
].join('');
const zeroIndexSelector = [
'MemberExpression',
'[computed=true]',
'[property.type="Literal"]',
'[property.raw="0"]',
methodSelector({
...filterMethodSelectorOptions,
property: 'object'
})
].join('');
const shiftSelector = [
methodSelector({
name: 'shift',
length: 0
}),
methodSelector({
...filterMethodSelectorOptions,
property: 'callee.object'
})
].join('');
const destructuringDeclaratorSelector = [
'VariableDeclarator',
'[id.type="ArrayPattern"]',
'[id.elements.length=1]',
'[id.elements.0.type!="RestElement"]',
methodSelector({
...filterMethodSelectorOptions,
property: 'init'
})
].join('');
const destructuringAssignmentSelector = [
'AssignmentExpression',
'[left.type="ArrayPattern"]',
'[left.elements.length=1]',
'[left.elements.0.type!="RestElement"]',
methodSelector({
...filterMethodSelectorOptions,
property: 'right'
})
].join('');
// Need add `()` to the `AssignmentExpression`
// - `ObjectExpression`: `[{foo}] = array.filter(bar)` fix to `{foo} = array.find(bar)`
// - `ObjectPattern`: `[{foo = baz}] = array.filter(bar)`
const assignmentNeedParenthesize = (node, source) => {
const isAssign = node.type === 'AssignmentExpression';
if (!isAssign || isParenthesized(node, source)) {
return false;
}
const {left} = getDestructuringLeftAndRight(node);
const [element] = left.elements;
const {type} = element.type === 'AssignmentPattern' ? element.left : element;
return type === 'ObjectExpression' || type === 'ObjectPattern';
};
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
const hasLowerPrecedence = (node, operator) => (
(node.type === 'LogicalExpression' && (
node.operator === operator ||
// https://tc39.es/proposal-nullish-coalescing/ says
// `??` has lower precedence than `||`
// But MDN says
// `??` has higher precedence than `||`
(operator === '||' && node.operator === '??') ||
(operator === '??' && (node.operator === '||' || node.operator === '&&'))
)) ||
node.type === 'ConditionalExpression' ||
// Lower than `assignment`, should already parenthesized
/* istanbul ignore next */
node.type === 'AssignmentExpression' ||
node.type === 'YieldExpression' ||
node.type === 'SequenceExpression'
);
const getDestructuringLeftAndRight = node => {
/* istanbul ignore next */
if (!node) {
return {};
}
if (node.type === 'AssignmentExpression') {
return node;
}
if (node.type === 'VariableDeclarator') {
return {left: node.id, right: node.init};
}
return {};
};
function * fixDestructuring(node, source, fixer) {
const {left} = getDestructuringLeftAndRight(node);
const [element] = left.elements;
const leftText = source.getText(element.type === 'AssignmentPattern' ? element.left : element);
yield fixer.replaceText(left, leftText);
// `AssignmentExpression` always starts with `[` or `(`, so we don't need check ASI
if (assignmentNeedParenthesize(node, source)) {
yield fixer.insertTextBefore(node, '(');
yield fixer.insertTextAfter(node, ')');
}
}
const hasDefaultValue = node => getDestructuringLeftAndRight(node).left.elements[0].type === 'AssignmentPattern';
const fixDestructuringDefaultValue = (node, source, fixer, operator) => {
const {left, right} = getDestructuringLeftAndRight(node);
const [element] = left.elements;
const defaultValue = element.right;
let defaultValueText = source.getText(defaultValue);
if (isParenthesized(defaultValue, source) || hasLowerPrecedence(defaultValue, operator)) {
defaultValueText = `(${defaultValueText})`;
}
return fixer.insertTextAfter(right, ` ${operator} ${defaultValueText}`);
};
const fixDestructuringAndReplaceFilter = (source, node) => {
const {property} = getDestructuringLeftAndRight(node).right.callee;
let suggest;
let fix;
if (hasDefaultValue(node)) {
suggest = [
{operator: '??', messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR},
{operator: '||', messageId: SUGGESTION_LOGICAL_OR_OPERATOR}
].map(({messageId, operator}) => ({
messageId,
* fix(fixer) {
yield fixer.replaceText(property, 'find');
yield fixDestructuringDefaultValue(node, source, fixer, operator);
yield * fixDestructuring(node, source, fixer);
}
}));
} else {
fix = function * (fixer) {
yield fixer.replaceText(property, 'find');
yield * fixDestructuring(node, source, fixer);
};
}
return {fix, suggest};
};
const isAccessingZeroIndex = node =>
node.parent &&
node.parent.type === 'MemberExpression' &&
node.parent.computed === true &&
node.parent.object === node &&
node.parent.property &&
node.parent.property.type === 'Literal' &&
node.parent.property.raw === '0';
const isDestructuringFirstElement = node => {
const {left, right} = getDestructuringLeftAndRight(node.parent);
return left &&
right &&
right === node &&
left.type === 'ArrayPattern' &&
left.elements &&
left.elements.length === 1 &&
left.elements[0].type !== 'RestElement';
};
const create = context => {
const source = context.getSourceCode();
return {
[zeroIndexSelector](node) {
context.report({
node: node.object.callee.property,
messageId: ERROR_ZERO_INDEX,
fix: fixer => [
fixer.replaceText(node.object.callee.property, 'find'),
fixer.removeRange([node.object.range[1], node.range[1]])
]
});
},
[shiftSelector](node) {
context.report({
node: node.callee.object.callee.property,
messageId: ERROR_SHIFT,
fix: fixer => [
fixer.replaceText(node.callee.object.callee.property, 'find'),
fixer.removeRange([node.callee.object.range[1], node.range[1]])
]
});
},
[destructuringDeclaratorSelector](node) {
context.report({
node: node.init.callee.property,
messageId: ERROR_DESTRUCTURING_DECLARATION,
...fixDestructuringAndReplaceFilter(source, node)
});
},
[destructuringAssignmentSelector](node) {
context.report({
node: node.right.callee.property,
messageId: ERROR_DESTRUCTURING_ASSIGNMENT,
...fixDestructuringAndReplaceFilter(source, node)
});
},
[filterVariableSelector](node) {
const variable = findVariable(context.getScope(), node.id);
const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node.id);
if (identifiers.length === 0) {
return;
}
const zeroIndexNodes = [];
const destructuringNodes = [];
for (const identifier of identifiers) {
if (isAccessingZeroIndex(identifier)) {
zeroIndexNodes.push(identifier.parent);
} else if (isDestructuringFirstElement(identifier)) {
destructuringNodes.push(identifier.parent);
} else {
return;
}
}
const problem = {
node: node.init.callee.property,
messageId: ERROR_DECLARATION
};
// `const [foo = bar] = baz` is not fixable
if (!destructuringNodes.some(node => hasDefaultValue(node))) {
problem.fix = function * (fixer) {
yield fixer.replaceText(node.init.callee.property, 'find');
for (const node of zeroIndexNodes) {
yield fixer.removeRange([node.object.range[1], node.range[1]]);
}
for (const node of destructuringNodes) {
yield * fixDestructuring(node, source, fixer);
}
};
}
context.report(problem);
}
};
};
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code',
messages
}
};