123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- 'use strict';
- const {isOpeningBracketToken, isClosingBracketToken, getStaticValue} = require('@eslint-community/eslint-utils');
- const {
- isParenthesized,
- getParenthesizedRange,
- getParenthesizedText,
- } = require('./utils/parentheses.js');
- const {isNodeMatchesNameOrPath} = require('./utils/is-node-matches.js');
- const needsSemicolon = require('./utils/needs-semicolon.js');
- const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js');
- const isLeftHandSide = require('./utils/is-left-hand-side.js');
- const {
- getNegativeIndexLengthNode,
- removeLengthNode,
- } = require('./shared/negative-index.js');
- const {methodCallSelector, callExpressionSelector, notLeftHandSideSelector} = require('./selectors/index.js');
- const {removeMemberExpressionProperty, removeMethodCall} = require('./fix/index.js');
- const {isLiteral} = require('./ast/index.js');
- const MESSAGE_ID_NEGATIVE_INDEX = 'negative-index';
- const MESSAGE_ID_INDEX = 'index';
- const MESSAGE_ID_STRING_CHAR_AT_NEGATIVE = 'string-char-at-negative';
- const MESSAGE_ID_STRING_CHAR_AT = 'string-char-at';
- const MESSAGE_ID_SLICE = 'slice';
- const MESSAGE_ID_GET_LAST_FUNCTION = 'get-last-function';
- const SUGGESTION_ID = 'use-at';
- const messages = {
- [MESSAGE_ID_NEGATIVE_INDEX]: 'Prefer `.at(…)` over `[….length - index]`.',
- [MESSAGE_ID_INDEX]: 'Prefer `.at(…)` over index access.',
- [MESSAGE_ID_STRING_CHAR_AT_NEGATIVE]: 'Prefer `String#at(…)` over `String#charAt(….length - index)`.',
- [MESSAGE_ID_STRING_CHAR_AT]: 'Prefer `String#at(…)` over `String#charAt(…)`.',
- [MESSAGE_ID_SLICE]: 'Prefer `.at(…)` over the first element from `.slice(…)`.',
- [MESSAGE_ID_GET_LAST_FUNCTION]: 'Prefer `.at(-1)` over `{{description}}(…)` to get the last element.',
- [SUGGESTION_ID]: 'Use `.at(…)`.',
- };
- const indexAccess = [
- 'MemberExpression',
- '[optional!=true]',
- '[computed!=false]',
- notLeftHandSideSelector(),
- ].join('');
- const sliceCall = methodCallSelector({method: 'slice', minimumArguments: 1, maximumArguments: 2});
- const stringCharAt = methodCallSelector({method: 'charAt', argumentsLength: 1});
- const isArguments = node => node.type === 'Identifier' && node.name === 'arguments';
- const isLiteralNegativeInteger = node =>
- node.type === 'UnaryExpression'
- && node.prefix
- && node.operator === '-'
- && node.argument.type === 'Literal'
- && Number.isInteger(node.argument.value)
- && node.argument.value > 0;
- const isZeroIndexAccess = node => {
- const {parent} = node;
- return parent.type === 'MemberExpression'
- && !parent.optional
- && parent.computed
- && parent.object === node
- && isLiteral(parent.property, 0);
- };
- const isArrayPopOrShiftCall = (node, method) => {
- const {parent} = node;
- return parent.type === 'MemberExpression'
- && !parent.optional
- && !parent.computed
- && parent.object === node
- && parent.property.type === 'Identifier'
- && parent.property.name === method
- && parent.parent.type === 'CallExpression'
- && parent.parent.callee === parent
- && !parent.parent.optional
- && parent.parent.arguments.length === 0;
- };
- const isArrayPopCall = node => isArrayPopOrShiftCall(node, 'pop');
- const isArrayShiftCall = node => isArrayPopOrShiftCall(node, 'shift');
- function checkSliceCall(node) {
- const sliceArgumentsLength = node.arguments.length;
- const [startIndexNode, endIndexNode] = node.arguments;
- if (!isLiteralNegativeInteger(startIndexNode)) {
- return;
- }
- let firstElementGetMethod = '';
- if (isZeroIndexAccess(node)) {
- if (isLeftHandSide(node.parent)) {
- return;
- }
- firstElementGetMethod = 'zero-index';
- } else if (isArrayShiftCall(node)) {
- firstElementGetMethod = 'shift';
- } else if (isArrayPopCall(node)) {
- firstElementGetMethod = 'pop';
- }
- if (!firstElementGetMethod) {
- return;
- }
- const startIndex = -startIndexNode.argument.value;
- if (sliceArgumentsLength === 1) {
- if (
- firstElementGetMethod === 'zero-index'
- || firstElementGetMethod === 'shift'
- || (startIndex === -1 && firstElementGetMethod === 'pop')
- ) {
- return {safeToFix: true, firstElementGetMethod};
- }
- return;
- }
- if (
- isLiteralNegativeInteger(endIndexNode)
- && -endIndexNode.argument.value === startIndex + 1
- ) {
- return {safeToFix: true, firstElementGetMethod};
- }
- if (firstElementGetMethod === 'pop') {
- return;
- }
- return {safeToFix: false, firstElementGetMethod};
- }
- const lodashLastFunctions = [
- '_.last',
- 'lodash.last',
- 'underscore.last',
- ];
- /** @param {import('eslint').Rule.RuleContext} context */
- function create(context) {
- const {
- getLastElementFunctions,
- checkAllIndexAccess,
- } = {
- getLastElementFunctions: [],
- checkAllIndexAccess: false,
- ...context.options[0],
- };
- const getLastFunctions = [...getLastElementFunctions, ...lodashLastFunctions];
- const sourceCode = context.getSourceCode();
- return {
- [indexAccess](node) {
- const indexNode = node.property;
- const lengthNode = getNegativeIndexLengthNode(indexNode, node.object);
- if (!lengthNode) {
- if (!checkAllIndexAccess) {
- return;
- }
- // Only if we are sure it's an positive integer
- const staticValue = getStaticValue(indexNode, context.getScope());
- if (!staticValue || !Number.isInteger(staticValue.value) || staticValue.value < 0) {
- return;
- }
- }
- const problem = {
- node: indexNode,
- messageId: lengthNode ? MESSAGE_ID_NEGATIVE_INDEX : MESSAGE_ID_INDEX,
- };
- if (isArguments(node.object)) {
- return problem;
- }
- problem.fix = function * (fixer) {
- if (lengthNode) {
- yield removeLengthNode(lengthNode, fixer, sourceCode);
- }
- // Only remove space for `foo[foo.length - 1]`
- if (
- indexNode.type === 'BinaryExpression'
- && indexNode.operator === '-'
- && indexNode.left === lengthNode
- && indexNode.right.type === 'Literal'
- && /^\d+$/.test(indexNode.right.raw)
- ) {
- const numberNode = indexNode.right;
- const tokenBefore = sourceCode.getTokenBefore(numberNode);
- if (
- tokenBefore.type === 'Punctuator'
- && tokenBefore.value === '-'
- && /^\s+$/.test(sourceCode.text.slice(tokenBefore.range[1], numberNode.range[0]))
- ) {
- yield fixer.removeRange([tokenBefore.range[1], numberNode.range[0]]);
- }
- }
- const openingBracketToken = sourceCode.getTokenBefore(indexNode, isOpeningBracketToken);
- yield fixer.replaceText(openingBracketToken, '.at(');
- const closingBracketToken = sourceCode.getTokenAfter(indexNode, isClosingBracketToken);
- yield fixer.replaceText(closingBracketToken, ')');
- };
- return problem;
- },
- [stringCharAt](node) {
- const [indexNode] = node.arguments;
- const lengthNode = getNegativeIndexLengthNode(indexNode, node.callee.object);
- // `String#charAt` don't care about index value, we assume it's always number
- if (!lengthNode && !checkAllIndexAccess) {
- return;
- }
- return {
- node: indexNode,
- messageId: lengthNode ? MESSAGE_ID_STRING_CHAR_AT_NEGATIVE : MESSAGE_ID_STRING_CHAR_AT,
- suggest: [{
- messageId: SUGGESTION_ID,
- * fix(fixer) {
- if (lengthNode) {
- yield removeLengthNode(lengthNode, fixer, sourceCode);
- }
- yield fixer.replaceText(node.callee.property, 'at');
- },
- }],
- };
- },
- [sliceCall](sliceCall) {
- const result = checkSliceCall(sliceCall);
- if (!result) {
- return;
- }
- const {safeToFix, firstElementGetMethod} = result;
- /** @param {import('eslint').Rule.RuleFixer} fixer */
- function * fix(fixer) {
- // `.slice` to `.at`
- yield fixer.replaceText(sliceCall.callee.property, 'at');
- // Remove extra arguments
- if (sliceCall.arguments.length !== 1) {
- const [, start] = getParenthesizedRange(sliceCall.arguments[0], sourceCode);
- const [end] = sourceCode.getLastToken(sliceCall).range;
- yield fixer.removeRange([start, end]);
- }
- // Remove `[0]`, `.shift()`, or `.pop()`
- if (firstElementGetMethod === 'zero-index') {
- yield removeMemberExpressionProperty(fixer, sliceCall.parent, sourceCode);
- } else {
- yield * removeMethodCall(fixer, sliceCall.parent.parent, sourceCode);
- }
- }
- const problem = {
- node: sliceCall.callee.property,
- messageId: MESSAGE_ID_SLICE,
- };
- if (safeToFix) {
- problem.fix = fix;
- } else {
- problem.suggest = [{messageId: SUGGESTION_ID, fix}];
- }
- return problem;
- },
- [callExpressionSelector({argumentsLength: 1})](node) {
- const matchedFunction = getLastFunctions.find(nameOrPath => isNodeMatchesNameOrPath(node.callee, nameOrPath));
- if (!matchedFunction) {
- return;
- }
- const problem = {
- node: node.callee,
- messageId: MESSAGE_ID_GET_LAST_FUNCTION,
- data: {description: matchedFunction.trim()},
- };
- const [array] = node.arguments;
- if (isArguments(array)) {
- return problem;
- }
- problem.fix = function (fixer) {
- let fixed = getParenthesizedText(array, sourceCode);
- if (
- !isParenthesized(array, sourceCode)
- && shouldAddParenthesesToMemberExpressionObject(array, sourceCode)
- ) {
- fixed = `(${fixed})`;
- }
- fixed = `${fixed}.at(-1)`;
- const tokenBefore = sourceCode.getTokenBefore(node);
- if (needsSemicolon(tokenBefore, sourceCode, fixed)) {
- fixed = `;${fixed}`;
- }
- return fixer.replaceText(node, fixed);
- };
- return problem;
- },
- };
- }
- const schema = [
- {
- type: 'object',
- additionalProperties: false,
- properties: {
- getLastElementFunctions: {
- type: 'array',
- uniqueItems: true,
- },
- checkAllIndexAccess: {
- type: 'boolean',
- default: false,
- },
- },
- },
- ];
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'suggestion',
- docs: {
- description: 'Prefer `.at()` method for index access and `String#charAt()`.',
- },
- fixable: 'code',
- hasSuggestions: true,
- schema,
- messages,
- },
- };
|