2 * @fileoverview Rule to disallow use of the `RegExp` constructor in favor of regular expression literals
3 * @author Milos Djermanovic
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils
= require("./utils/ast-utils");
13 const { CALL
, CONSTRUCT
, ReferenceTracker
, findVariable
} = require("@eslint-community/eslint-utils");
14 const { RegExpValidator
, visitRegExpAST
, RegExpParser
} = require("@eslint-community/regexpp");
15 const { canTokensBeAdjacent
} = require("./utils/ast-utils");
16 const { REGEXPP_LATEST_ECMA_VERSION
} = require("./utils/regular-expressions");
18 //------------------------------------------------------------------------------
20 //------------------------------------------------------------------------------
23 * Determines whether the given node is a string literal.
24 * @param {ASTNode} node Node to check.
25 * @returns {boolean} True if the node is a string literal.
27 function isStringLiteral(node
) {
28 return node
.type
=== "Literal" && typeof node
.value
=== "string";
32 * Determines whether the given node is a regex literal.
33 * @param {ASTNode} node Node to check.
34 * @returns {boolean} True if the node is a regex literal.
36 function isRegexLiteral(node
) {
37 return node
.type
=== "Literal" && Object
.prototype.hasOwnProperty
.call(node
, "regex");
41 * Determines whether the given node is a template literal without expressions.
42 * @param {ASTNode} node Node to check.
43 * @returns {boolean} True if the node is a template literal without expressions.
45 function isStaticTemplateLiteral(node
) {
46 return node
.type
=== "TemplateLiteral" && node
.expressions
.length
=== 0;
49 const validPrecedingTokens
= new Set([
115 //------------------------------------------------------------------------------
117 //------------------------------------------------------------------------------
119 /** @type {import('../shared/types').Rule} */
125 description
: "Disallow use of the `RegExp` constructor in favor of regular expression literals",
127 url
: "https://eslint.org/docs/latest/rules/prefer-regex-literals"
130 hasSuggestions
: true,
136 disallowRedundantWrapping
: {
141 additionalProperties
: false
146 unexpectedRegExp
: "Use a regular expression literal instead of the 'RegExp' constructor.",
147 replaceWithLiteral
: "Replace with an equivalent regular expression literal.",
148 replaceWithLiteralAndFlags
: "Replace with an equivalent regular expression literal with flags '{{ flags }}'.",
149 replaceWithIntendedLiteralAndFlags
: "Replace with a regular expression literal with flags '{{ flags }}'.",
150 unexpectedRedundantRegExp
: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
151 unexpectedRedundantRegExpWithFlags
: "Use regular expression literal with flags instead of the 'RegExp' constructor."
156 const [{ disallowRedundantWrapping
= false } = {}] = context
.options
;
157 const sourceCode
= context
.sourceCode
;
160 * Determines whether the given identifier node is a reference to a global variable.
161 * @param {ASTNode} node `Identifier` node to check.
162 * @returns {boolean} True if the identifier is a reference to a global variable.
164 function isGlobalReference(node
) {
165 const scope
= sourceCode
.getScope(node
);
166 const variable
= findVariable(scope
, node
);
168 return variable
!== null && variable
.scope
.type
=== "global" && variable
.defs
.length
=== 0;
172 * Determines whether the given node is a String.raw`` tagged template expression
173 * with a static template literal.
174 * @param {ASTNode} node Node to check.
175 * @returns {boolean} True if the node is String.raw`` with a static template.
177 function isStringRawTaggedStaticTemplateLiteral(node
) {
178 return node
.type
=== "TaggedTemplateExpression" &&
179 astUtils
.isSpecificMemberAccess(node
.tag
, "String", "raw") &&
180 isGlobalReference(astUtils
.skipChainExpression(node
.tag
).object
) &&
181 isStaticTemplateLiteral(node
.quasi
);
185 * Gets the value of a string
186 * @param {ASTNode} node The node to get the string of.
187 * @returns {string|null} The value of the node.
189 function getStringValue(node
) {
190 if (isStringLiteral(node
)) {
194 if (isStaticTemplateLiteral(node
)) {
195 return node
.quasis
[0].value
.cooked
;
198 if (isStringRawTaggedStaticTemplateLiteral(node
)) {
199 return node
.quasi
.quasis
[0].value
.raw
;
206 * Determines whether the given node is considered to be a static string by the logic of this rule.
207 * @param {ASTNode} node Node to check.
208 * @returns {boolean} True if the node is a static string.
210 function isStaticString(node
) {
211 return isStringLiteral(node
) ||
212 isStaticTemplateLiteral(node
) ||
213 isStringRawTaggedStaticTemplateLiteral(node
);
217 * Determines whether the relevant arguments of the given are all static string literals.
218 * @param {ASTNode} node Node to check.
219 * @returns {boolean} True if all arguments are static strings.
221 function hasOnlyStaticStringArguments(node
) {
222 const args
= node
.arguments
;
224 if ((args
.length
=== 1 || args
.length
=== 2) && args
.every(isStaticString
)) {
232 * Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped.
233 * @param {ASTNode} node Node to check.
234 * @returns {boolean} True if the node already contains a regex literal argument.
236 function isUnnecessarilyWrappedRegexLiteral(node
) {
237 const args
= node
.arguments
;
239 if (args
.length
=== 1 && isRegexLiteral(args
[0])) {
243 if (args
.length
=== 2 && isRegexLiteral(args
[0]) && isStaticString(args
[1])) {
251 * Returns a ecmaVersion compatible for regexpp.
252 * @param {number} ecmaVersion The ecmaVersion to convert.
253 * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
255 function getRegexppEcmaVersion(ecmaVersion
) {
256 if (ecmaVersion
<= 5) {
259 return Math
.min(ecmaVersion
, REGEXPP_LATEST_ECMA_VERSION
);
262 const regexppEcmaVersion
= getRegexppEcmaVersion(context
.languageOptions
.ecmaVersion
);
265 * Makes a character escaped or else returns null.
266 * @param {string} character The character to escape.
267 * @returns {string} The resulting escaped character.
269 function resolveEscapes(character
) {
300 * Checks whether the given regex and flags are valid for the ecma version or not.
301 * @param {string} pattern The regex pattern to check.
302 * @param {string | undefined} flags The regex flags to check.
303 * @returns {boolean} True if the given regex pattern and flags are valid for the ecma version.
305 function isValidRegexForEcmaVersion(pattern
, flags
) {
306 const validator
= new RegExpValidator({ ecmaVersion
: regexppEcmaVersion
});
309 validator
.validatePattern(pattern
, 0, pattern
.length
, flags
? flags
.includes("u") : false);
311 validator
.validateFlags(flags
);
320 * Checks whether two given regex flags contain the same flags or not.
321 * @param {string} flagsA The regex flags.
322 * @param {string} flagsB The regex flags.
323 * @returns {boolean} True if two regex flags contain same flags.
325 function areFlagsEqual(flagsA
, flagsB
) {
326 return [...flagsA
].sort().join("") === [...flagsB
].sort().join("");
331 * Merges two regex flags.
332 * @param {string} flagsA The regex flags.
333 * @param {string} flagsB The regex flags.
334 * @returns {string} The merged regex flags.
336 function mergeRegexFlags(flagsA
, flagsB
) {
337 const flagsSet
= new Set([
342 return [...flagsSet
].join("");
346 * Checks whether a give node can be fixed to the given regex pattern and flags.
347 * @param {ASTNode} node The node to check.
348 * @param {string} pattern The regex pattern to check.
349 * @param {string} flags The regex flags
350 * @returns {boolean} True if a node can be fixed to the given regex pattern and flags.
352 function canFixTo(node
, pattern
, flags
) {
353 const tokenBefore
= sourceCode
.getTokenBefore(node
);
355 return sourceCode
.getCommentsInside(node
).length
=== 0 &&
356 (!tokenBefore
|| validPrecedingTokens
.has(tokenBefore
.value
)) &&
357 isValidRegexForEcmaVersion(pattern
, flags
);
361 * Returns a safe output code considering the before and after tokens.
362 * @param {ASTNode} node The regex node.
363 * @param {string} newRegExpValue The new regex expression value.
364 * @returns {string} The output code.
366 function getSafeOutput(node
, newRegExpValue
) {
367 const tokenBefore
= sourceCode
.getTokenBefore(node
);
368 const tokenAfter
= sourceCode
.getTokenAfter(node
);
370 return (tokenBefore
&& !canTokensBeAdjacent(tokenBefore
, newRegExpValue
) && tokenBefore
.range
[1] === node
.range
[0] ? " " : "") +
372 (tokenAfter
&& !canTokensBeAdjacent(newRegExpValue
, tokenAfter
) && node
.range
[1] === tokenAfter
.range
[0] ? " " : "");
378 const scope
= sourceCode
.getScope(node
);
379 const tracker
= new ReferenceTracker(scope
);
387 for (const { node
: refNode
} of tracker
.iterateGlobalReferences(traceMap
)) {
388 if (disallowRedundantWrapping
&& isUnnecessarilyWrappedRegexLiteral(refNode
)) {
389 const regexNode
= refNode
.arguments
[0];
391 if (refNode
.arguments
.length
=== 2) {
394 const argFlags
= getStringValue(refNode
.arguments
[1]) || "";
396 if (canFixTo(refNode
, regexNode
.regex
.pattern
, argFlags
)) {
398 messageId
: "replaceWithLiteralAndFlags",
399 pattern
: regexNode
.regex
.pattern
,
404 const literalFlags
= regexNode
.regex
.flags
|| "";
405 const mergedFlags
= mergeRegexFlags(literalFlags
, argFlags
);
408 !areFlagsEqual(mergedFlags
, argFlags
) &&
409 canFixTo(refNode
, regexNode
.regex
.pattern
, mergedFlags
)
412 messageId
: "replaceWithIntendedLiteralAndFlags",
413 pattern
: regexNode
.regex
.pattern
,
420 messageId
: "unexpectedRedundantRegExpWithFlags",
421 suggest
: suggests
.map(({ flags
, pattern
, messageId
}) => ({
427 return fixer
.replaceText(refNode
, getSafeOutput(refNode
, `/${pattern}/${flags}`));
434 if (canFixTo(refNode
, regexNode
.regex
.pattern
, regexNode
.regex
.flags
)) {
435 outputs
.push(sourceCode
.getText(regexNode
));
441 messageId
: "unexpectedRedundantRegExp",
442 suggest
: outputs
.map(output
=> ({
443 messageId
: "replaceWithLiteral",
445 return fixer
.replaceText(
447 getSafeOutput(refNode
, output
)
453 } else if (hasOnlyStaticStringArguments(refNode
)) {
454 let regexContent
= getStringValue(refNode
.arguments
[0]);
458 if (refNode
.arguments
[1]) {
459 flags
= getStringValue(refNode
.arguments
[1]);
462 if (!canFixTo(refNode
, regexContent
, flags
)) {
466 if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
470 if (regexContent && !noFix) {
471 let charIncrease = 0;
473 const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
475 visitRegExpAST(ast, {
476 onCharacterEnter(characterNode) {
477 const escaped = resolveEscapes(characterNode.raw);
481 regexContent.slice(0, characterNode.start + charIncrease) +
483 regexContent.slice(characterNode.end + charIncrease);
485 if (characterNode.raw.length === 1) {
493 const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}
`;
497 messageId: "unexpectedRegExp",
498 suggest: noFix ? [] : [{
499 messageId: "replaceWithLiteral",
501 return fixer.replaceText(refNode, getSafeOutput(refNode, newRegExpValue));