SourceTermAnalysisSystem_vue/node_modules/eslint-plugin-unicorn/rules/prefer-array-some.js
2026-05-15 10:22:44 +08:00

216 lines
6.1 KiB
JavaScript

'use strict';
const {checkVueTemplate} = require('./utils/rule.js');
const {
isBooleanNode,
getParenthesizedRange,
isNodeValueNotFunction,
} = require('./utils/index.js');
const {removeMemberExpressionProperty} = require('./fix/index.js');
const {isLiteral, isUndefined, isMethodCall, isMemberExpression} = require('./ast/index.js');
const ERROR_ID_ARRAY_SOME = 'some';
const SUGGESTION_ID_ARRAY_SOME = 'some-suggestion';
const ERROR_ID_ARRAY_FILTER = 'filter';
const messages = {
[ERROR_ID_ARRAY_SOME]: 'Prefer `.some(…)` over `.{{method}}(…)`.',
[SUGGESTION_ID_ARRAY_SOME]: 'Replace `.{{method}}(…)` with `.some(…)`.',
[ERROR_ID_ARRAY_FILTER]: 'Prefer `.some(…)` over non-zero length check from `.filter(…)`.',
};
const isCheckingUndefined = node =>
node.parent.type === 'BinaryExpression'
// Not checking yoda expression `null != foo.find()` and `undefined !== foo.find()
&& node.parent.left === node
&& (
(
(
node.parent.operator === '!='
|| node.parent.operator === '=='
|| node.parent.operator === '==='
|| node.parent.operator === '!=='
)
&& isUndefined(node.parent.right)
)
|| (
(
node.parent.operator === '!='
|| node.parent.operator === '=='
)
// eslint-disable-next-line unicorn/no-null
&& isLiteral(node.parent.right, null)
)
);
const isNegativeOne = node => node.type === 'UnaryExpression' && node.operator === '-' && node.argument && node.argument.type === 'Literal' && node.argument.value === 1;
const isLiteralZero = node => isLiteral(node, 0);
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
// `.find(…)`
// `.findLast(…)`
context.on('CallExpression', callExpression => {
if (!isMethodCall(callExpression, {
methods: ['find', 'findLast'],
minimumArguments: 1,
maximumArguments: 2,
optionalCall: false,
optionalMember: false,
})) {
return;
}
const isCompare = isCheckingUndefined(callExpression);
if (!isCompare && !isBooleanNode(callExpression)) {
return;
}
const methodNode = callExpression.callee.property;
return {
node: methodNode,
messageId: ERROR_ID_ARRAY_SOME,
data: {method: methodNode.name},
suggest: [
{
messageId: SUGGESTION_ID_ARRAY_SOME,
* fix(fixer) {
yield fixer.replaceText(methodNode, 'some');
if (!isCompare) {
return;
}
const parenthesizedRange = getParenthesizedRange(callExpression, context.sourceCode);
yield fixer.replaceTextRange([parenthesizedRange[1], callExpression.parent.range[1]], '');
if (callExpression.parent.operator === '!=' || callExpression.parent.operator === '!==') {
return;
}
yield fixer.insertTextBeforeRange(parenthesizedRange, '!');
},
},
],
};
});
// These operators also used in `prefer-includes`, try to reuse the code in future
// `.{findIndex,findLastIndex}(…) !== -1`
// `.{findIndex,findLastIndex}(…) != -1`
// `.{findIndex,findLastIndex}(…) > -1`
// `.{findIndex,findLastIndex}(…) === -1`
// `.{findIndex,findLastIndex}(…) == -1`
// `.{findIndex,findLastIndex}(…) >= 0`
// `.{findIndex,findLastIndex}(…) < 0`
context.on('BinaryExpression', binaryExpression => {
const {left, right, operator} = binaryExpression;
if (!(
isMethodCall(left, {
methods: ['findIndex', 'findLastIndex'],
argumentsLength: 1,
optionalCall: false,
optionalMember: false,
})
&& (
(['!==', '!=', '>', '===', '=='].includes(operator) && isNegativeOne(right))
|| (['>=', '<'].includes(operator) && isLiteralZero(right))
)
)) {
return;
}
const methodNode = left.callee.property;
return {
node: methodNode,
messageId: ERROR_ID_ARRAY_SOME,
data: {method: methodNode.name},
* fix(fixer) {
if (['===', '==', '<'].includes(operator)) {
yield fixer.insertTextBefore(binaryExpression, '!');
}
yield fixer.replaceText(methodNode, 'some');
const operatorToken = context.sourceCode.getTokenAfter(
left,
token => token.type === 'Punctuator' && token.value === operator,
);
const [start] = operatorToken.range;
const [, end] = binaryExpression.range;
yield fixer.removeRange([start, end]);
},
};
});
// `.filter(…).length > 0`
// `.filter(…).length !== 0`
context.on('BinaryExpression', binaryExpression => {
if (!(
// We assume the user already follows `unicorn/explicit-length-check`. These are allowed in that rule.
(binaryExpression.operator === '>' || binaryExpression.operator === '!==')
&& binaryExpression.right.type === 'Literal'
&& binaryExpression.right.raw === '0'
&& isMemberExpression(binaryExpression.left, {property: 'length', optional: false})
&& isMethodCall(binaryExpression.left.object, {
method: 'filter',
optionalCall: false,
optionalMember: false,
})
)) {
return;
}
const filterCall = binaryExpression.left.object;
const [firstArgument] = filterCall.arguments;
if (!firstArgument || isNodeValueNotFunction(firstArgument)) {
return;
}
const filterProperty = filterCall.callee.property;
return {
node: filterProperty,
messageId: ERROR_ID_ARRAY_FILTER,
* fix(fixer) {
// `.filter` to `.some`
yield fixer.replaceText(filterProperty, 'some');
const {sourceCode} = context;
const lengthNode = binaryExpression.left;
/*
Remove `.length`
`(( (( array.filter() )).length )) > (( 0 ))`
------------------------^^^^^^^
*/
yield removeMemberExpressionProperty(fixer, lengthNode, sourceCode);
/*
Remove `> 0`
`(( (( array.filter() )).length )) > (( 0 ))`
----------------------------------^^^^^^^^^^
*/
yield fixer.removeRange([
getParenthesizedRange(lengthNode, sourceCode)[1],
binaryExpression.range[1],
]);
// The `BinaryExpression` always ends with a number or `)`, no need check for ASI
},
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create: checkVueTemplate(create),
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast,findIndex,findLastIndex}(…)`.',
recommended: true,
},
fixable: 'code',
messages,
hasSuggestions: true,
},
};