routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+378
@@ -0,0 +1,378 @@
|
||||
import {
|
||||
isOpeningBracketToken,
|
||||
isClosingBracketToken,
|
||||
getStaticValue,
|
||||
} from '@eslint-community/eslint-utils';
|
||||
import {
|
||||
isParenthesized,
|
||||
getParenthesizedRange,
|
||||
getParenthesizedText,
|
||||
isNodeMatchesNameOrPath,
|
||||
needsSemicolon,
|
||||
shouldAddParenthesesToMemberExpressionObject,
|
||||
isLeftHandSide,
|
||||
} from './utils/index.js';
|
||||
import {
|
||||
getNegativeIndexLengthNode,
|
||||
removeLengthNode,
|
||||
} from './shared/negative-index.js';
|
||||
import {removeMemberExpressionProperty, removeMethodCall} from './fix/index.js';
|
||||
import {
|
||||
isLiteral,
|
||||
isCallExpression,
|
||||
isMethodCall,
|
||||
isMemberExpression,
|
||||
} from './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 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 =>
|
||||
isMemberExpression(node.parent, {
|
||||
optional: false,
|
||||
computed: true,
|
||||
})
|
||||
&& node.parent.object === node
|
||||
&& isLiteral(node.parent.property, 0);
|
||||
|
||||
const isArrayPopOrShiftCall = (node, method) =>
|
||||
isMethodCall(node.parent.parent, {
|
||||
method,
|
||||
argumentsLength: 0,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
&& node.parent.object === node;
|
||||
|
||||
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 (
|
||||
startIndexNode.argument.value === 1
|
||||
&& (
|
||||
firstElementGetMethod === 'zero-index'
|
||||
|| firstElementGetMethod === 'shift'
|
||||
|| ((firstElementGetMethod === 'pop') && (startIndex === -1))
|
||||
)
|
||||
) {
|
||||
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,
|
||||
} = context.options[0];
|
||||
const getLastFunctions = [...getLastElementFunctions, ...lodashLastFunctions];
|
||||
const {sourceCode} = context;
|
||||
|
||||
// Index access
|
||||
context.on('MemberExpression', node => {
|
||||
if (
|
||||
!node.computed
|
||||
|| isLeftHandSide(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexNode = node.property;
|
||||
const lengthNode = getNegativeIndexLengthNode(indexNode, node.object);
|
||||
|
||||
if (!lengthNode) {
|
||||
if (!checkAllIndexAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only if we are sure it's a positive integer
|
||||
const staticValue = getStaticValue(indexNode, sourceCode.getScope(indexNode));
|
||||
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, context);
|
||||
}
|
||||
|
||||
// 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(sourceCode.getRange(tokenBefore)[1], sourceCode.getRange(numberNode)[0]))
|
||||
) {
|
||||
yield fixer.removeRange([sourceCode.getRange(tokenBefore)[1], sourceCode.getRange(numberNode)[0]]);
|
||||
}
|
||||
}
|
||||
|
||||
const isOptional = node.optional;
|
||||
const openingBracketToken = sourceCode.getTokenBefore(indexNode, isOpeningBracketToken);
|
||||
yield fixer.replaceText(openingBracketToken, `${isOptional ? '' : '.'}at(`);
|
||||
|
||||
const closingBracketToken = sourceCode.getTokenAfter(indexNode, isClosingBracketToken);
|
||||
yield fixer.replaceText(closingBracketToken, ')');
|
||||
};
|
||||
|
||||
return problem;
|
||||
});
|
||||
|
||||
// `string.charAt`
|
||||
context.on('CallExpression', node => {
|
||||
if (!isMethodCall(node, {
|
||||
method: 'charAt',
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
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, context);
|
||||
}
|
||||
|
||||
yield fixer.replaceText(node.callee.property, 'at');
|
||||
},
|
||||
}],
|
||||
};
|
||||
});
|
||||
|
||||
// `.slice()`
|
||||
context.on('CallExpression', sliceCall => {
|
||||
if (!isMethodCall(sliceCall, {
|
||||
method: 'slice',
|
||||
minimumArguments: 1,
|
||||
maximumArguments: 2,
|
||||
optionalCall: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
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], context);
|
||||
const [end] = sourceCode.getRange(sourceCode.getLastToken(sliceCall));
|
||||
yield fixer.removeRange([start, end]);
|
||||
}
|
||||
|
||||
yield (
|
||||
// Remove `[0]`, `.shift()`, or `.pop()`
|
||||
firstElementGetMethod === 'zero-index'
|
||||
? removeMemberExpressionProperty(fixer, sliceCall.parent, context)
|
||||
: removeMethodCall(fixer, sliceCall.parent.parent, context)
|
||||
);
|
||||
}
|
||||
|
||||
const problem = {
|
||||
node: sliceCall.callee.property,
|
||||
messageId: MESSAGE_ID_SLICE,
|
||||
};
|
||||
|
||||
if (safeToFix) {
|
||||
problem.fix = fix;
|
||||
} else {
|
||||
problem.suggest = [{messageId: SUGGESTION_ID, fix}];
|
||||
}
|
||||
|
||||
return problem;
|
||||
});
|
||||
|
||||
context.on('CallExpression', node => {
|
||||
if (!isCallExpression(node, {argumentsLength: 1, optional: false})) {
|
||||
return;
|
||||
}
|
||||
|
||||
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, context);
|
||||
|
||||
if (
|
||||
!isParenthesized(array, sourceCode)
|
||||
&& shouldAddParenthesesToMemberExpressionObject(array, context)
|
||||
) {
|
||||
fixed = `(${fixed})`;
|
||||
}
|
||||
|
||||
fixed = `${fixed}.at(-1)`;
|
||||
|
||||
const tokenBefore = sourceCode.getTokenBefore(node);
|
||||
if (needsSemicolon(tokenBefore, context, fixed)) {
|
||||
fixed = `;${fixed}`;
|
||||
}
|
||||
|
||||
return fixer.replaceText(node, fixed);
|
||||
};
|
||||
|
||||
return problem;
|
||||
});
|
||||
}
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
getLastElementFunctions: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
description: 'Additional functions that return the last element.',
|
||||
},
|
||||
checkAllIndexAccess: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to also check positive integer index access.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Prefer `.at()` method for index access and `String#charAt()`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
schema,
|
||||
defaultOptions: [{getLastElementFunctions: [], checkAllIndexAccess: false}],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user