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-utils");
14 const { RegExpValidator
, visitRegExpAST
, RegExpParser
} = require("regexpp");
15 const { canTokensBeAdjacent
} = require("./utils/ast-utils");
17 //------------------------------------------------------------------------------
19 //------------------------------------------------------------------------------
21 const REGEXPP_LATEST_ECMA_VERSION
= 2022;
24 * Determines whether the given node is a string literal.
25 * @param {ASTNode} node Node to check.
26 * @returns {boolean} True if the node is a string literal.
28 function isStringLiteral(node
) {
29 return node
.type
=== "Literal" && typeof node
.value
=== "string";
33 * Determines whether the given node is a regex literal.
34 * @param {ASTNode} node Node to check.
35 * @returns {boolean} True if the node is a regex literal.
37 function isRegexLiteral(node
) {
38 return node
.type
=== "Literal" && Object
.prototype.hasOwnProperty
.call(node
, "regex");
42 * Determines whether the given node is a template literal without expressions.
43 * @param {ASTNode} node Node to check.
44 * @returns {boolean} True if the node is a template literal without expressions.
46 function isStaticTemplateLiteral(node
) {
47 return node
.type
=== "TemplateLiteral" && node
.expressions
.length
=== 0;
50 const validPrecedingTokens
= new Set([
116 //------------------------------------------------------------------------------
118 //------------------------------------------------------------------------------
120 /** @type {import('../shared/types').Rule} */
126 description
: "Disallow use of the `RegExp` constructor in favor of regular expression literals",
128 url
: "https://eslint.org/docs/rules/prefer-regex-literals"
131 hasSuggestions
: true,
137 disallowRedundantWrapping
: {
142 additionalProperties
: false
147 unexpectedRegExp
: "Use a regular expression literal instead of the 'RegExp' constructor.",
148 replaceWithLiteral
: "Replace with an equivalent regular expression literal.",
149 unexpectedRedundantRegExp
: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
150 unexpectedRedundantRegExpWithFlags
: "Use regular expression literal with flags instead of the 'RegExp' constructor."
155 const [{ disallowRedundantWrapping
= false } = {}] = context
.options
;
156 const sourceCode
= context
.getSourceCode();
159 * Determines whether the given identifier node is a reference to a global variable.
160 * @param {ASTNode} node `Identifier` node to check.
161 * @returns {boolean} True if the identifier is a reference to a global variable.
163 function isGlobalReference(node
) {
164 const scope
= context
.getScope();
165 const variable
= findVariable(scope
, node
);
167 return variable
!== null && variable
.scope
.type
=== "global" && variable
.defs
.length
=== 0;
171 * Determines whether the given node is a String.raw`` tagged template expression
172 * with a static template literal.
173 * @param {ASTNode} node Node to check.
174 * @returns {boolean} True if the node is String.raw`` with a static template.
176 function isStringRawTaggedStaticTemplateLiteral(node
) {
177 return node
.type
=== "TaggedTemplateExpression" &&
178 astUtils
.isSpecificMemberAccess(node
.tag
, "String", "raw") &&
179 isGlobalReference(astUtils
.skipChainExpression(node
.tag
).object
) &&
180 isStaticTemplateLiteral(node
.quasi
);
184 * Gets the value of a string
185 * @param {ASTNode} node The node to get the string of.
186 * @returns {string|null} The value of the node.
188 function getStringValue(node
) {
189 if (isStringLiteral(node
)) {
193 if (isStaticTemplateLiteral(node
)) {
194 return node
.quasis
[0].value
.cooked
;
197 if (isStringRawTaggedStaticTemplateLiteral(node
)) {
198 return node
.quasi
.quasis
[0].value
.raw
;
205 * Determines whether the given node is considered to be a static string by the logic of this rule.
206 * @param {ASTNode} node Node to check.
207 * @returns {boolean} True if the node is a static string.
209 function isStaticString(node
) {
210 return isStringLiteral(node
) ||
211 isStaticTemplateLiteral(node
) ||
212 isStringRawTaggedStaticTemplateLiteral(node
);
216 * Determines whether the relevant arguments of the given are all static string literals.
217 * @param {ASTNode} node Node to check.
218 * @returns {boolean} True if all arguments are static strings.
220 function hasOnlyStaticStringArguments(node
) {
221 const args
= node
.arguments
;
223 if ((args
.length
=== 1 || args
.length
=== 2) && args
.every(isStaticString
)) {
231 * Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped.
232 * @param {ASTNode} node Node to check.
233 * @returns {boolean} True if the node already contains a regex literal argument.
235 function isUnnecessarilyWrappedRegexLiteral(node
) {
236 const args
= node
.arguments
;
238 if (args
.length
=== 1 && isRegexLiteral(args
[0])) {
242 if (args
.length
=== 2 && isRegexLiteral(args
[0]) && isStaticString(args
[1])) {
250 * Returns a ecmaVersion compatible for regexpp.
251 * @param {any} ecmaVersion The ecmaVersion to convert.
252 * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
254 function getRegexppEcmaVersion(ecmaVersion
) {
255 if (typeof ecmaVersion
!== "number" || ecmaVersion
<= 5) {
258 return Math
.min(ecmaVersion
+ 2009, REGEXPP_LATEST_ECMA_VERSION
);
262 * Makes a character escaped or else returns null.
263 * @param {string} character The character to escape.
264 * @returns {string} The resulting escaped character.
266 function resolveEscapes(character
) {
298 const scope
= context
.getScope();
299 const tracker
= new ReferenceTracker(scope
);
307 for (const { node
} of tracker
.iterateGlobalReferences(traceMap
)) {
308 if (disallowRedundantWrapping
&& isUnnecessarilyWrappedRegexLiteral(node
)) {
309 if (node
.arguments
.length
=== 2) {
310 context
.report({ node
, messageId
: "unexpectedRedundantRegExpWithFlags" });
312 context
.report({ node
, messageId
: "unexpectedRedundantRegExp" });
314 } else if (hasOnlyStaticStringArguments(node
)) {
315 let regexContent
= getStringValue(node
.arguments
[0]);
319 if (node
.arguments
[1]) {
320 flags
= getStringValue(node
.arguments
[1]);
323 const regexppEcmaVersion
= getRegexppEcmaVersion(context
.parserOptions
.ecmaVersion
);
324 const RegExpValidatorInstance
= new RegExpValidator({ ecmaVersion
: regexppEcmaVersion
});
327 RegExpValidatorInstance
.validatePattern(regexContent
, 0, regexContent
.length
, flags
? flags
.includes("u") : false);
329 RegExpValidatorInstance
.validateFlags(flags
);
335 const tokenBefore
= sourceCode
.getTokenBefore(node
);
337 if (tokenBefore
&& !validPrecedingTokens
.has(tokenBefore
.value
)) {
341 if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
345 if (sourceCode.getCommentsInside(node).length > 0) {
349 if (regexContent && !noFix) {
350 let charIncrease = 0;
352 const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
354 visitRegExpAST(ast, {
355 onCharacterEnter(characterNode) {
356 const escaped = resolveEscapes(characterNode.raw);
360 regexContent.slice(0, characterNode.start + charIncrease) +
362 regexContent.slice(characterNode.end + charIncrease);
364 if (characterNode.raw.length === 1) {
372 const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}
`;
376 messageId: "unexpectedRegExp",
377 suggest: noFix ? [] : [{
378 messageId: "replaceWithLiteral",
380 const tokenAfter = sourceCode.getTokenAfter(node);
382 return fixer.replaceText(
384 (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
386 (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "")