186 lines
5.7 KiB
JavaScript
186 lines
5.7 KiB
JavaScript
|
'use strict';
|
||
|
const {getStaticValue} = require('@eslint-community/eslint-utils');
|
||
|
const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js');
|
||
|
const {methodCallSelector} = require('./selectors/index.js');
|
||
|
const isNumber = require('./utils/is-number.js');
|
||
|
const {replaceArgument} = require('./fix/index.js');
|
||
|
const {isNumberLiteral} = require('./ast/index.js');
|
||
|
|
||
|
const MESSAGE_ID_SUBSTR = 'substr';
|
||
|
const MESSAGE_ID_SUBSTRING = 'substring';
|
||
|
const messages = {
|
||
|
[MESSAGE_ID_SUBSTR]: 'Prefer `String#slice()` over `String#substr()`.',
|
||
|
[MESSAGE_ID_SUBSTRING]: 'Prefer `String#slice()` over `String#substring()`.',
|
||
|
};
|
||
|
|
||
|
const selector = methodCallSelector({
|
||
|
methods: ['substr', 'substring'],
|
||
|
includeOptionalMember: true,
|
||
|
includeOptionalCall: true,
|
||
|
});
|
||
|
|
||
|
const getNumericValue = node => {
|
||
|
if (isNumberLiteral(node)) {
|
||
|
return node.value;
|
||
|
}
|
||
|
|
||
|
if (node.type === 'UnaryExpression' && node.operator === '-') {
|
||
|
return -getNumericValue(node.argument);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// This handles cases where the argument is very likely to be a number, such as `.substring('foo'.length)`.
|
||
|
const isLengthProperty = node => (
|
||
|
node?.type === 'MemberExpression'
|
||
|
&& node.computed === false
|
||
|
&& node.property.type === 'Identifier'
|
||
|
&& node.property.name === 'length'
|
||
|
);
|
||
|
|
||
|
function * fixSubstrArguments({node, fixer, context, abort}) {
|
||
|
const argumentNodes = node.arguments;
|
||
|
const [firstArgument, secondArgument] = argumentNodes;
|
||
|
|
||
|
if (!secondArgument) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const scope = context.getScope();
|
||
|
const sourceCode = context.getSourceCode();
|
||
|
const firstArgumentStaticResult = getStaticValue(firstArgument, scope);
|
||
|
const secondArgumentRange = getParenthesizedRange(secondArgument, sourceCode);
|
||
|
const replaceSecondArgument = text => replaceArgument(fixer, secondArgument, text, sourceCode);
|
||
|
|
||
|
if (firstArgumentStaticResult?.value === 0) {
|
||
|
if (isNumberLiteral(secondArgument) || isLengthProperty(secondArgument)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (typeof getNumericValue(secondArgument) === 'number') {
|
||
|
yield replaceSecondArgument(Math.max(0, getNumericValue(secondArgument)));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
yield fixer.insertTextBeforeRange(secondArgumentRange, 'Math.max(0, ');
|
||
|
yield fixer.insertTextAfterRange(secondArgumentRange, ')');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (argumentNodes.every(node => isNumberLiteral(node))) {
|
||
|
yield replaceSecondArgument(firstArgument.value + secondArgument.value);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (argumentNodes.every(node => isNumber(node, context.getScope()))) {
|
||
|
const firstArgumentText = getParenthesizedText(firstArgument, sourceCode);
|
||
|
|
||
|
yield fixer.insertTextBeforeRange(secondArgumentRange, `${firstArgumentText} + `);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
return abort();
|
||
|
}
|
||
|
|
||
|
function * fixSubstringArguments({node, fixer, context, abort}) {
|
||
|
const sourceCode = context.getSourceCode();
|
||
|
const [firstArgument, secondArgument] = node.arguments;
|
||
|
|
||
|
const firstNumber = firstArgument ? getNumericValue(firstArgument) : undefined;
|
||
|
const firstArgumentText = getParenthesizedText(firstArgument, sourceCode);
|
||
|
const replaceFirstArgument = text => replaceArgument(fixer, firstArgument, text, sourceCode);
|
||
|
|
||
|
if (!secondArgument) {
|
||
|
if (isLengthProperty(firstArgument)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (firstNumber !== undefined) {
|
||
|
yield replaceFirstArgument(Math.max(0, firstNumber));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const firstArgumentRange = getParenthesizedRange(firstArgument, sourceCode);
|
||
|
yield fixer.insertTextBeforeRange(firstArgumentRange, 'Math.max(0, ');
|
||
|
yield fixer.insertTextAfterRange(firstArgumentRange, ')');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const secondNumber = getNumericValue(secondArgument);
|
||
|
const secondArgumentText = getParenthesizedText(secondArgument, sourceCode);
|
||
|
const replaceSecondArgument = text => replaceArgument(fixer, secondArgument, text, sourceCode);
|
||
|
|
||
|
if (firstNumber !== undefined && secondNumber !== undefined) {
|
||
|
const argumentsValue = [Math.max(0, firstNumber), Math.max(0, secondNumber)];
|
||
|
if (firstNumber > secondNumber) {
|
||
|
argumentsValue.reverse();
|
||
|
}
|
||
|
|
||
|
if (argumentsValue[0] !== firstNumber) {
|
||
|
yield replaceFirstArgument(argumentsValue[0]);
|
||
|
}
|
||
|
|
||
|
if (argumentsValue[1] !== secondNumber) {
|
||
|
yield replaceSecondArgument(argumentsValue[1]);
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (firstNumber === 0 || secondNumber === 0) {
|
||
|
yield replaceFirstArgument(0);
|
||
|
yield replaceSecondArgument(`Math.max(0, ${firstNumber === 0 ? secondArgumentText : firstArgumentText})`);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue:
|
||
|
// .substring(0, 2) and .substring(2, 0) returns the same result
|
||
|
// .slice(0, 2) and .slice(2, 0) doesn't return the same result
|
||
|
// There's also an issue with us now knowing whether the value will be negative or not, due to:
|
||
|
// .substring() treats a negative number the same as it treats a zero.
|
||
|
// The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice
|
||
|
|
||
|
return abort();
|
||
|
}
|
||
|
|
||
|
/** @param {import('eslint').Rule.RuleContext} context */
|
||
|
const create = context => ({
|
||
|
[selector](node) {
|
||
|
const method = node.callee.property.name;
|
||
|
|
||
|
return {
|
||
|
node,
|
||
|
messageId: method,
|
||
|
* fix(fixer, {abort}) {
|
||
|
yield fixer.replaceText(node.callee.property, 'slice');
|
||
|
|
||
|
if (node.arguments.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
node.arguments.length > 2
|
||
|
|| node.arguments.some(node => node.type === 'SpreadElement')
|
||
|
) {
|
||
|
return abort();
|
||
|
}
|
||
|
|
||
|
const fixArguments = method === 'substr' ? fixSubstrArguments : fixSubstringArguments;
|
||
|
yield * fixArguments({node, fixer, context, abort});
|
||
|
},
|
||
|
};
|
||
|
},
|
||
|
});
|
||
|
|
||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||
|
module.exports = {
|
||
|
create,
|
||
|
meta: {
|
||
|
type: 'suggestion',
|
||
|
docs: {
|
||
|
description: 'Prefer `String#slice()` over `String#substr()` and `String#substring()`.',
|
||
|
},
|
||
|
fixable: 'code',
|
||
|
messages,
|
||
|
},
|
||
|
};
|