| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 | '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,	},};
 |