eslint-plugin-unicorn-x
Version:
More than 100 powerful ESLint rules
194 lines (161 loc) • 4.08 kB
JavaScript
import {getStaticValue} from '@eslint-community/eslint-utils';
import regjsparser from 'regjsparser';
import {isRegexLiteral, isNewExpression, isMethodCall} from './ast/index.js';
const {parse: parseRegExp} = regjsparser;
const MESSAGE_ID_USE_REPLACE_ALL = 'method';
const MESSAGE_ID_USE_STRING = 'pattern';
const messages = {
[MESSAGE_ID_USE_REPLACE_ALL]:
'Prefer `String#replaceAll()` over `String#replace()`.',
[MESSAGE_ID_USE_STRING]: 'This pattern can be replaced with {{replacement}}.',
};
const QUOTE = "'";
function getPatternReplacement(node) {
if (!isRegexLiteral(node)) {
return;
}
const {pattern, flags} = node.regex;
if (flags.replace('u', '').replace('v', '') !== 'g') {
return;
}
let tree;
try {
tree = parseRegExp(pattern, flags, {
unicodePropertyEscape: flags.includes('u'),
unicodeSet: flags.includes('v'),
namedGroups: true,
lookbehind: true,
});
} catch {
return;
}
const parts = tree.type === 'alternative' ? tree.body : [tree];
if (parts.some((part) => part.type !== 'value')) {
return;
}
return (
QUOTE +
parts
.map((part) => {
const {kind, codePoint, raw} = part;
if (kind === 'controlLetter') {
if (codePoint === 13) {
return String.raw`\r`;
}
if (codePoint === 10) {
return String.raw`\n`;
}
if (codePoint === 9) {
return String.raw`\t`;
}
return `\\u{${codePoint.toString(16)}}`;
}
if (kind === 'octal') {
return `\\u{${codePoint.toString(16)}}`;
}
let character = raw;
if (kind === 'identifier') {
character = character.slice(1);
}
if (character === QUOTE || character === '\\') {
return `\\${character}`;
}
return character;
})
.join('') +
QUOTE
);
}
const isRegExpWithGlobalFlag = (node, scope) => {
if (isRegexLiteral(node)) {
return node.regex.flags.includes('g');
}
if (
isNewExpression(node, {name: 'RegExp'}) &&
node.arguments[0]?.type !== 'SpreadElement' &&
node.arguments[1]?.type === 'Literal' &&
typeof node.arguments[1].value === 'string'
) {
return node.arguments[1].value.includes('g');
}
const staticResult = getStaticValue(node, scope);
// Don't know if there is `g` flag
if (!staticResult) {
return false;
}
const {value} = staticResult;
return (
Object.prototype.toString.call(value) === '[object RegExp]' && value.global
);
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = (context) => ({
CallExpression(node) {
if (
!isMethodCall(node, {
methods: ['replace', 'replaceAll'],
argumentsLength: 2,
optionalCall: false,
optionalMember: false,
})
) {
return;
}
const {
arguments: [pattern],
callee: {property},
} = node;
if (
!isRegExpWithGlobalFlag(pattern, context.sourceCode.getScope(pattern))
) {
return;
}
const methodName = property.name;
const patternReplacement = getPatternReplacement(pattern);
if (methodName === 'replaceAll') {
if (!patternReplacement) {
return;
}
return {
node: pattern,
messageId: MESSAGE_ID_USE_STRING,
data: {
// Show `This pattern can be replaced with a string literal.` for long strings
replacement:
patternReplacement.length < 20
? patternReplacement
: 'a string literal',
},
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix: (fixer) => fixer.replaceText(pattern, patternReplacement),
};
}
return {
node: property,
messageId: MESSAGE_ID_USE_REPLACE_ALL,
/** @param {import('eslint').Rule.RuleFixer} fixer */
*fix(fixer) {
yield fixer.insertTextAfter(property, 'All');
if (!patternReplacement) {
return;
}
yield fixer.replaceText(pattern, patternReplacement);
},
};
},
});
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description:
'Prefer `String#replaceAll()` over regex searches with the global flag.',
recommended: true,
},
fixable: 'code',
messages,
},
};
export default config;