| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 | 'use strict';const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils');const {methodCallSelector} = require('./selectors/index.js');const escapeString = require('./utils/escape-string.js');const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js');const shouldAddParenthesesToLogicalExpressionChild = require('./utils/should-add-parentheses-to-logical-expression-child.js');const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js');const MESSAGE_STARTS_WITH = 'prefer-starts-with';const MESSAGE_ENDS_WITH = 'prefer-ends-with';const FIX_TYPE_STRING_CASTING = 'useStringCasting';const FIX_TYPE_OPTIONAL_CHAINING = 'useOptionalChaining';const FIX_TYPE_NULLISH_COALESCING = 'useNullishCoalescing';const messages = {	[MESSAGE_STARTS_WITH]: 'Prefer `String#startsWith()` over a regex with `^`.',	[MESSAGE_ENDS_WITH]: 'Prefer `String#endsWith()` over a regex with `$`.',	[FIX_TYPE_STRING_CASTING]: 'Convert to string `String(…).{{method}}()`.',	[FIX_TYPE_OPTIONAL_CHAINING]: 'Use optional chaining `…?.{{method}}()`.',	[FIX_TYPE_NULLISH_COALESCING]: 'Use nullish coalescing `(… ?? \'\').{{method}}()`.',};const doesNotContain = (string, characters) => characters.every(character => !string.includes(character));const isSimpleString = string => doesNotContain(	string,	['^', '$', '+', '[', '{', '(', '\\', '.', '?', '*', '|'],);const addParentheses = text => `(${text})`;const regexTestSelector = [	methodCallSelector({method: 'test', argumentsLength: 1}),	'[callee.object.regex]',].join('');const checkRegex = ({pattern, flags}) => {	if (flags.includes('i') || flags.includes('m')) {		return;	}	if (pattern.startsWith('^')) {		const string = pattern.slice(1);		if (isSimpleString(string)) {			return {				messageId: MESSAGE_STARTS_WITH,				string,			};		}	}	if (pattern.endsWith('$')) {		const string = pattern.slice(0, -1);		if (isSimpleString(string)) {			return {				messageId: MESSAGE_ENDS_WITH,				string,			};		}	}};/** @param {import('eslint').Rule.RuleContext} context */const create = context => {	const sourceCode = context.getSourceCode();	return {		[regexTestSelector](node) {			const regexNode = node.callee.object;			const {regex} = regexNode;			const result = checkRegex(regex);			if (!result) {				return;			}			const [target] = node.arguments;			const method = result.messageId === MESSAGE_STARTS_WITH ? 'startsWith' : 'endsWith';			let isString = target.type === 'TemplateLiteral'				|| (					target.type === 'CallExpression'					&& target.callee.type === 'Identifier'					&& target.callee.name === 'String'				);			let isNonString = false;			if (!isString) {				const staticValue = getStaticValue(target, context.getScope());				if (staticValue) {					isString = typeof staticValue.value === 'string';					isNonString = !isString;				}			}			const problem = {				node,				messageId: result.messageId,			};			function * fix(fixer, fixType) {				let targetText = getParenthesizedText(target, sourceCode);				const isRegexParenthesized = isParenthesized(regexNode, sourceCode);				const isTargetParenthesized = isParenthesized(target, sourceCode);				switch (fixType) {					// Goal: `(target ?? '').startsWith(pattern)`					case FIX_TYPE_NULLISH_COALESCING: {						if (							!isTargetParenthesized							&& shouldAddParenthesesToLogicalExpressionChild(target, {operator: '??', property: 'left'})						) {							targetText = addParentheses(targetText);						}						targetText += ' ?? \'\'';						// `LogicalExpression` need add parentheses to call `.startsWith()`,						// but if regex is parenthesized, we can reuse it						if (!isRegexParenthesized) {							targetText = addParentheses(targetText);						}						break;					}					// Goal: `String(target).startsWith(pattern)`					case FIX_TYPE_STRING_CASTING: {						// `target` was a call argument, don't need check parentheses						targetText = `String(${targetText})`;						// `CallExpression` don't need add parentheses to call `.startsWith()`						break;					}					// Goal: `target.startsWith(pattern)` or `target?.startsWith(pattern)`					case FIX_TYPE_OPTIONAL_CHAINING: {						// Optional chaining: `target.startsWith` => `target?.startsWith`						yield fixer.replaceText(sourceCode.getTokenBefore(node.callee.property), '?.');					}					// Fallthrough					default: {						if (							!isRegexParenthesized							&& !isTargetParenthesized							&& shouldAddParenthesesToMemberExpressionObject(target, sourceCode)						) {							targetText = addParentheses(targetText);						}					}				}				// The regex literal always starts with `/` or `(`, so we don't need check ASI				// Replace regex with string				yield fixer.replaceText(regexNode, targetText);				// `.test` => `.startsWith` / `.endsWith`				yield fixer.replaceText(node.callee.property, method);				// Replace argument with result.string				yield fixer.replaceTextRange(getParenthesizedRange(target, sourceCode), escapeString(result.string));			}			if (isString || !isNonString) {				problem.fix = fix;			}			if (!isString) {				problem.suggest = [					FIX_TYPE_STRING_CASTING,					FIX_TYPE_OPTIONAL_CHAINING,					FIX_TYPE_NULLISH_COALESCING,				].map(type => ({messageId: type, data: {method}, fix: fixer => fix(fixer, type)}));			}			return problem;		},	};};/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`.',		},		fixable: 'code',		hasSuggestions: true,		messages,	},};
 |