routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
||||
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 String.raw`\u{${codePoint.toString(16)}}`;
|
||||
}
|
||||
|
||||
if (kind === 'octal') {
|
||||
return String.raw`\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 => {
|
||||
context.on('CallExpression', node => {
|
||||
if (!isMethodCall(node, {
|
||||
methods: ['replace', 'replaceAll'],
|
||||
argumentsLength: 2,
|
||||
optionalCall: 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: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user