150 lines
4.0 KiB
JavaScript
150 lines
4.0 KiB
JavaScript
|
'use strict';
|
||
|
const {methodCallSelector, matches, memberExpressionSelector} = require('./selectors/index.js');
|
||
|
const isSameReference = require('./utils/is-same-reference.js');
|
||
|
const {getParenthesizedRange} = require('./utils/parentheses.js');
|
||
|
|
||
|
const messages = {
|
||
|
'non-zero': 'The non-empty check is useless as `Array#some()` returns `false` for an empty array.',
|
||
|
zero: 'The empty check is useless as `Array#every()` returns `true` for an empty array.',
|
||
|
};
|
||
|
|
||
|
const logicalExpressionSelector = [
|
||
|
'LogicalExpression',
|
||
|
matches(['[operator="||"]', '[operator="&&"]']),
|
||
|
].join('');
|
||
|
// We assume the user already follows `unicorn/explicit-length-check`. These are allowed in that rule.
|
||
|
const lengthCompareZeroSelector = [
|
||
|
logicalExpressionSelector,
|
||
|
' > ',
|
||
|
'BinaryExpression',
|
||
|
memberExpressionSelector({path: 'left', property: 'length'}),
|
||
|
'[right.type="Literal"]',
|
||
|
'[right.raw="0"]',
|
||
|
].join('');
|
||
|
const zeroLengthCheckSelector = [
|
||
|
lengthCompareZeroSelector,
|
||
|
'[operator="==="]',
|
||
|
].join('');
|
||
|
const nonZeroLengthCheckSelector = [
|
||
|
lengthCompareZeroSelector,
|
||
|
matches(['[operator=">"]', '[operator="!=="]']),
|
||
|
].join('');
|
||
|
const arraySomeCallSelector = methodCallSelector('some');
|
||
|
const arrayEveryCallSelector = methodCallSelector('every');
|
||
|
|
||
|
function flatLogicalExpression(node) {
|
||
|
return [node.left, node.right].flatMap(child =>
|
||
|
child.type === 'LogicalExpression' && child.operator === node.operator
|
||
|
? flatLogicalExpression(child)
|
||
|
: [child],
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/** @param {import('eslint').Rule.RuleContext} context */
|
||
|
const create = context => {
|
||
|
const logicalExpressions = [];
|
||
|
const zeroLengthChecks = new Set();
|
||
|
const nonZeroLengthChecks = new Set();
|
||
|
const arraySomeCalls = new Set();
|
||
|
const arrayEveryCalls = new Set();
|
||
|
|
||
|
function isUselessLengthCheckNode({node, operator, siblings}) {
|
||
|
return (
|
||
|
(
|
||
|
operator === '||'
|
||
|
&& zeroLengthChecks.has(node)
|
||
|
&& siblings.some(condition =>
|
||
|
arrayEveryCalls.has(condition)
|
||
|
&& isSameReference(node.left.object, condition.callee.object),
|
||
|
)
|
||
|
)
|
||
|
|| (
|
||
|
operator === '&&'
|
||
|
&& nonZeroLengthChecks.has(node)
|
||
|
&& siblings.some(condition =>
|
||
|
arraySomeCalls.has(condition)
|
||
|
&& isSameReference(node.left.object, condition.callee.object),
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function getUselessLengthCheckNode(logicalExpression) {
|
||
|
const {operator} = logicalExpression;
|
||
|
return flatLogicalExpression(logicalExpression)
|
||
|
.filter((node, index, conditions) => isUselessLengthCheckNode({
|
||
|
node,
|
||
|
operator,
|
||
|
siblings: [
|
||
|
conditions[index - 1],
|
||
|
conditions[index + 1],
|
||
|
].filter(Boolean),
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
[zeroLengthCheckSelector](node) {
|
||
|
zeroLengthChecks.add(node);
|
||
|
},
|
||
|
[nonZeroLengthCheckSelector](node) {
|
||
|
nonZeroLengthChecks.add(node);
|
||
|
},
|
||
|
[arraySomeCallSelector](node) {
|
||
|
arraySomeCalls.add(node);
|
||
|
},
|
||
|
[arrayEveryCallSelector](node) {
|
||
|
arrayEveryCalls.add(node);
|
||
|
},
|
||
|
[logicalExpressionSelector](node) {
|
||
|
logicalExpressions.push(node);
|
||
|
},
|
||
|
* 'Program:exit'() {
|
||
|
const nodes = new Set(
|
||
|
logicalExpressions.flatMap(logicalExpression =>
|
||
|
getUselessLengthCheckNode(logicalExpression),
|
||
|
),
|
||
|
);
|
||
|
|
||
|
for (const node of nodes) {
|
||
|
yield {
|
||
|
loc: {
|
||
|
start: node.left.property.loc.start,
|
||
|
end: node.loc.end,
|
||
|
},
|
||
|
messageId: zeroLengthChecks.has(node) ? 'zero' : 'non-zero',
|
||
|
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
||
|
fix(fixer) {
|
||
|
const sourceCode = context.getSourceCode();
|
||
|
const {left, right} = node.parent;
|
||
|
const leftRange = getParenthesizedRange(left, sourceCode);
|
||
|
const rightRange = getParenthesizedRange(right, sourceCode);
|
||
|
const range = [];
|
||
|
if (left === node) {
|
||
|
range[0] = leftRange[0];
|
||
|
range[1] = rightRange[0];
|
||
|
} else {
|
||
|
range[0] = leftRange[1];
|
||
|
range[1] = rightRange[1];
|
||
|
}
|
||
|
|
||
|
return fixer.removeRange(range);
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||
|
module.exports = {
|
||
|
create,
|
||
|
meta: {
|
||
|
type: 'suggestion',
|
||
|
docs: {
|
||
|
description: 'Disallow useless array length check.',
|
||
|
},
|
||
|
fixable: 'code',
|
||
|
messages,
|
||
|
},
|
||
|
};
|