Skip to content

Commit fc20f24

Browse files
authored
feat: add suggestions for redundant wrapping in prefer-regex-literals (#16658)
* feat: add suggestions in prefer-regex-literals * add tests & refactor * change test case * apply reviews * apply review and refactor * refactor * apply review * Apply reviews * add data
1 parent 762a872 commit fc20f24

2 files changed

Lines changed: 485 additions & 34 deletions

File tree

lib/rules/prefer-regex-literals.js

Lines changed: 144 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ module.exports = {
146146
messages: {
147147
unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.",
148148
replaceWithLiteral: "Replace with an equivalent regular expression literal.",
149+
replaceWithLiteralAndFlags: "Replace with an equivalent regular expression literal with flags '{{ flags }}'.",
150+
replaceWithIntendedLiteralAndFlags: "Replace with a regular expression literal with flags '{{ flags }}'.",
149151
unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
150152
unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor."
151153
}
@@ -258,6 +260,8 @@ module.exports = {
258260
return Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION);
259261
}
260262

263+
const regexppEcmaVersion = getRegexppEcmaVersion(context.languageOptions.ecmaVersion);
264+
261265
/**
262266
* Makes a character escaped or else returns null.
263267
* @param {string} character The character to escape.
@@ -293,6 +297,83 @@ module.exports = {
293297
}
294298
}
295299

300+
/**
301+
* Checks whether the given regex and flags are valid for the ecma version or not.
302+
* @param {string} pattern The regex pattern to check.
303+
* @param {string | undefined} flags The regex flags to check.
304+
* @returns {boolean} True if the given regex pattern and flags are valid for the ecma version.
305+
*/
306+
function isValidRegexForEcmaVersion(pattern, flags) {
307+
const validator = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
308+
309+
try {
310+
validator.validatePattern(pattern, 0, pattern.length, flags ? flags.includes("u") : false);
311+
if (flags) {
312+
validator.validateFlags(flags);
313+
}
314+
return true;
315+
} catch {
316+
return false;
317+
}
318+
}
319+
320+
/**
321+
* Checks whether two given regex flags contain the same flags or not.
322+
* @param {string} flagsA The regex flags.
323+
* @param {string} flagsB The regex flags.
324+
* @returns {boolean} True if two regex flags contain same flags.
325+
*/
326+
function areFlagsEqual(flagsA, flagsB) {
327+
return [...flagsA].sort().join("") === [...flagsB].sort().join("");
328+
}
329+
330+
331+
/**
332+
* Merges two regex flags.
333+
* @param {string} flagsA The regex flags.
334+
* @param {string} flagsB The regex flags.
335+
* @returns {string} The merged regex flags.
336+
*/
337+
function mergeRegexFlags(flagsA, flagsB) {
338+
const flagsSet = new Set([
339+
...flagsA,
340+
...flagsB
341+
]);
342+
343+
return [...flagsSet].join("");
344+
}
345+
346+
/**
347+
* Checks whether a give node can be fixed to the given regex pattern and flags.
348+
* @param {ASTNode} node The node to check.
349+
* @param {string} pattern The regex pattern to check.
350+
* @param {string} flags The regex flags
351+
* @returns {boolean} True if a node can be fixed to the given regex pattern and flags.
352+
*/
353+
function canFixTo(node, pattern, flags) {
354+
const tokenBefore = sourceCode.getTokenBefore(node);
355+
356+
return sourceCode.getCommentsInside(node).length === 0 &&
357+
(!tokenBefore || validPrecedingTokens.has(tokenBefore.value)) &&
358+
isValidRegexForEcmaVersion(pattern, flags);
359+
}
360+
361+
/**
362+
* Returns a safe output code considering the before and after tokens.
363+
* @param {ASTNode} node The regex node.
364+
* @param {string} newRegExpValue The new regex expression value.
365+
* @returns {string} The output code.
366+
*/
367+
function getSafeOutput(node, newRegExpValue) {
368+
const tokenBefore = sourceCode.getTokenBefore(node);
369+
const tokenAfter = sourceCode.getTokenAfter(node);
370+
371+
return (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
372+
newRegExpValue +
373+
(tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "");
374+
375+
}
376+
296377
return {
297378
Program() {
298379
const scope = context.getScope();
@@ -306,10 +387,69 @@ module.exports = {
306387

307388
for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
308389
if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) {
390+
const regexNode = node.arguments[0];
391+
309392
if (node.arguments.length === 2) {
310-
context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" });
393+
const suggests = [];
394+
395+
const argFlags = getStringValue(node.arguments[1]) || "";
396+
397+
if (canFixTo(node, regexNode.regex.pattern, argFlags)) {
398+
suggests.push({
399+
messageId: "replaceWithLiteralAndFlags",
400+
pattern: regexNode.regex.pattern,
401+
flags: argFlags
402+
});
403+
}
404+
405+
const literalFlags = regexNode.regex.flags || "";
406+
const mergedFlags = mergeRegexFlags(literalFlags, argFlags);
407+
408+
if (
409+
!areFlagsEqual(mergedFlags, argFlags) &&
410+
canFixTo(node, regexNode.regex.pattern, mergedFlags)
411+
) {
412+
suggests.push({
413+
messageId: "replaceWithIntendedLiteralAndFlags",
414+
pattern: regexNode.regex.pattern,
415+
flags: mergedFlags
416+
});
417+
}
418+
419+
context.report({
420+
node,
421+
messageId: "unexpectedRedundantRegExpWithFlags",
422+
suggest: suggests.map(({ flags, pattern, messageId }) => ({
423+
messageId,
424+
data: {
425+
flags
426+
},
427+
fix(fixer) {
428+
return fixer.replaceText(node, getSafeOutput(node, `/${pattern}/${flags}`));
429+
}
430+
}))
431+
});
311432
} else {
312-
context.report({ node, messageId: "unexpectedRedundantRegExp" });
433+
const outputs = [];
434+
435+
if (canFixTo(node, regexNode.regex.pattern, regexNode.regex.flags)) {
436+
outputs.push(sourceCode.getText(regexNode));
437+
}
438+
439+
440+
context.report({
441+
node,
442+
messageId: "unexpectedRedundantRegExp",
443+
suggest: outputs.map(output => ({
444+
messageId: "replaceWithLiteral",
445+
fix(fixer) {
446+
return fixer.replaceText(
447+
node,
448+
getSafeOutput(node, output)
449+
);
450+
}
451+
}))
452+
});
313453
}
314454
} else if (hasOnlyStaticStringArguments(node)) {
315455
let regexContent = getStringValue(node.arguments[0]);
@@ -320,32 +460,14 @@ module.exports = {
320460
flags = getStringValue(node.arguments[1]);
321461
}
322462

323-
const regexppEcmaVersion = getRegexppEcmaVersion(context.languageOptions.ecmaVersion);
324-
const RegExpValidatorInstance = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
325-
326-
try {
327-
RegExpValidatorInstance.validatePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
328-
if (flags) {
329-
RegExpValidatorInstance.validateFlags(flags);
330-
}
331-
} catch {
332-
noFix = true;
333-
}
334-
335-
const tokenBefore = sourceCode.getTokenBefore(node);
336-
337-
if (tokenBefore && !validPrecedingTokens.has(tokenBefore.value)) {
463+
if (!canFixTo(node, regexContent, flags)) {
338464
noFix = true;
339465
}
340466

341467
if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
342468
noFix = true;
343469
}
344470

345-
if (sourceCode.getCommentsInside(node).length > 0) {
346-
noFix = true;
347-
}
348-
349471
if (regexContent && !noFix) {
350472
let charIncrease = 0;
351473

@@ -377,14 +499,7 @@ module.exports = {
377499
suggest: noFix ? [] : [{
378500
messageId: "replaceWithLiteral",
379501
fix(fixer) {
380-
const tokenAfter = sourceCode.getTokenAfter(node);
381-
382-
return fixer.replaceText(
383-
node,
384-
(tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
385-
newRegExpValue +
386-
(tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "")
387-
);
502+
return fixer.replaceText(node, getSafeOutput(node, newRegExpValue));
388503
}
389504
}]
390505
});

0 commit comments

Comments
 (0)