]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/prefer-regex-literals.js
import 8.23.1 source
[pve-eslint.git] / eslint / lib / rules / prefer-regex-literals.js
1 /**
2 * @fileoverview Rule to disallow use of the `RegExp` constructor in favor of regular expression literals
3 * @author Milos Djermanovic
4 */
5
6 "use strict";
7
8 //------------------------------------------------------------------------------
9 // Requirements
10 //------------------------------------------------------------------------------
11
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");
16
17 //------------------------------------------------------------------------------
18 // Helpers
19 //------------------------------------------------------------------------------
20
21 const REGEXPP_LATEST_ECMA_VERSION = 2022;
22
23 /**
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.
27 */
28 function isStringLiteral(node) {
29 return node.type === "Literal" && typeof node.value === "string";
30 }
31
32 /**
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.
36 */
37 function isRegexLiteral(node) {
38 return node.type === "Literal" && Object.prototype.hasOwnProperty.call(node, "regex");
39 }
40
41 /**
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.
45 */
46 function isStaticTemplateLiteral(node) {
47 return node.type === "TemplateLiteral" && node.expressions.length === 0;
48 }
49
50 const validPrecedingTokens = new Set([
51 "(",
52 ";",
53 "[",
54 ",",
55 "=",
56 "+",
57 "*",
58 "-",
59 "?",
60 "~",
61 "%",
62 "**",
63 "!",
64 "typeof",
65 "instanceof",
66 "&&",
67 "||",
68 "??",
69 "return",
70 "...",
71 "delete",
72 "void",
73 "in",
74 "<",
75 ">",
76 "<=",
77 ">=",
78 "==",
79 "===",
80 "!=",
81 "!==",
82 "<<",
83 ">>",
84 ">>>",
85 "&",
86 "|",
87 "^",
88 ":",
89 "{",
90 "=>",
91 "*=",
92 "<<=",
93 ">>=",
94 ">>>=",
95 "^=",
96 "|=",
97 "&=",
98 "??=",
99 "||=",
100 "&&=",
101 "**=",
102 "+=",
103 "-=",
104 "/=",
105 "%=",
106 "/",
107 "do",
108 "break",
109 "continue",
110 "debugger",
111 "case",
112 "throw"
113 ]);
114
115
116 //------------------------------------------------------------------------------
117 // Rule Definition
118 //------------------------------------------------------------------------------
119
120 /** @type {import('../shared/types').Rule} */
121 module.exports = {
122 meta: {
123 type: "suggestion",
124
125 docs: {
126 description: "Disallow use of the `RegExp` constructor in favor of regular expression literals",
127 recommended: false,
128 url: "https://eslint.org/docs/rules/prefer-regex-literals"
129 },
130
131 hasSuggestions: true,
132
133 schema: [
134 {
135 type: "object",
136 properties: {
137 disallowRedundantWrapping: {
138 type: "boolean",
139 default: false
140 }
141 },
142 additionalProperties: false
143 }
144 ],
145
146 messages: {
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."
151 }
152 },
153
154 create(context) {
155 const [{ disallowRedundantWrapping = false } = {}] = context.options;
156 const sourceCode = context.getSourceCode();
157
158 /**
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.
162 */
163 function isGlobalReference(node) {
164 const scope = context.getScope();
165 const variable = findVariable(scope, node);
166
167 return variable !== null && variable.scope.type === "global" && variable.defs.length === 0;
168 }
169
170 /**
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.
175 */
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);
181 }
182
183 /**
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.
187 */
188 function getStringValue(node) {
189 if (isStringLiteral(node)) {
190 return node.value;
191 }
192
193 if (isStaticTemplateLiteral(node)) {
194 return node.quasis[0].value.cooked;
195 }
196
197 if (isStringRawTaggedStaticTemplateLiteral(node)) {
198 return node.quasi.quasis[0].value.raw;
199 }
200
201 return null;
202 }
203
204 /**
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.
208 */
209 function isStaticString(node) {
210 return isStringLiteral(node) ||
211 isStaticTemplateLiteral(node) ||
212 isStringRawTaggedStaticTemplateLiteral(node);
213 }
214
215 /**
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.
219 */
220 function hasOnlyStaticStringArguments(node) {
221 const args = node.arguments;
222
223 if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) {
224 return true;
225 }
226
227 return false;
228 }
229
230 /**
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.
234 */
235 function isUnnecessarilyWrappedRegexLiteral(node) {
236 const args = node.arguments;
237
238 if (args.length === 1 && isRegexLiteral(args[0])) {
239 return true;
240 }
241
242 if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) {
243 return true;
244 }
245
246 return false;
247 }
248
249 /**
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.
253 */
254 function getRegexppEcmaVersion(ecmaVersion) {
255 if (typeof ecmaVersion !== "number" || ecmaVersion <= 5) {
256 return 5;
257 }
258 return Math.min(ecmaVersion + 2009, REGEXPP_LATEST_ECMA_VERSION);
259 }
260
261 /**
262 * Makes a character escaped or else returns null.
263 * @param {string} character The character to escape.
264 * @returns {string} The resulting escaped character.
265 */
266 function resolveEscapes(character) {
267 switch (character) {
268 case "\n":
269 case "\\\n":
270 return "\\n";
271
272 case "\r":
273 case "\\\r":
274 return "\\r";
275
276 case "\t":
277 case "\\\t":
278 return "\\t";
279
280 case "\v":
281 case "\\\v":
282 return "\\v";
283
284 case "\f":
285 case "\\\f":
286 return "\\f";
287
288 case "/":
289 return "\\/";
290
291 default:
292 return null;
293 }
294 }
295
296 return {
297 Program() {
298 const scope = context.getScope();
299 const tracker = new ReferenceTracker(scope);
300 const traceMap = {
301 RegExp: {
302 [CALL]: true,
303 [CONSTRUCT]: true
304 }
305 };
306
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" });
311 } else {
312 context.report({ node, messageId: "unexpectedRedundantRegExp" });
313 }
314 } else if (hasOnlyStaticStringArguments(node)) {
315 let regexContent = getStringValue(node.arguments[0]);
316 let noFix = false;
317 let flags;
318
319 if (node.arguments[1]) {
320 flags = getStringValue(node.arguments[1]);
321 }
322
323 const regexppEcmaVersion = getRegexppEcmaVersion(context.parserOptions.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)) {
338 noFix = true;
339 }
340
341 if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
342 noFix = true;
343 }
344
345 if (sourceCode.getCommentsInside(node).length > 0) {
346 noFix = true;
347 }
348
349 if (regexContent && !noFix) {
350 let charIncrease = 0;
351
352 const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
353
354 visitRegExpAST(ast, {
355 onCharacterEnter(characterNode) {
356 const escaped = resolveEscapes(characterNode.raw);
357
358 if (escaped) {
359 regexContent =
360 regexContent.slice(0, characterNode.start + charIncrease) +
361 escaped +
362 regexContent.slice(characterNode.end + charIncrease);
363
364 if (characterNode.raw.length === 1) {
365 charIncrease += 1;
366 }
367 }
368 }
369 });
370 }
371
372 const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}`;
373
374 context.report({
375 node,
376 messageId: "unexpectedRegExp",
377 suggest: noFix ? [] : [{
378 messageId: "replaceWithLiteral",
379 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 );
388 }
389 }]
390 });
391 }
392 }
393 }
394 };
395 }
396 };