]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/prefer-regex-literals.js
bump version to 8.41.0-3
[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-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");
17
18 //------------------------------------------------------------------------------
19 // Helpers
20 //------------------------------------------------------------------------------
21
22 /**
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.
26 */
27 function isStringLiteral(node) {
28 return node.type === "Literal" && typeof node.value === "string";
29 }
30
31 /**
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.
35 */
36 function isRegexLiteral(node) {
37 return node.type === "Literal" && Object.prototype.hasOwnProperty.call(node, "regex");
38 }
39
40 /**
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.
44 */
45 function isStaticTemplateLiteral(node) {
46 return node.type === "TemplateLiteral" && node.expressions.length === 0;
47 }
48
49 const validPrecedingTokens = new Set([
50 "(",
51 ";",
52 "[",
53 ",",
54 "=",
55 "+",
56 "*",
57 "-",
58 "?",
59 "~",
60 "%",
61 "**",
62 "!",
63 "typeof",
64 "instanceof",
65 "&&",
66 "||",
67 "??",
68 "return",
69 "...",
70 "delete",
71 "void",
72 "in",
73 "<",
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 "do",
107 "break",
108 "continue",
109 "debugger",
110 "case",
111 "throw"
112 ]);
113
114
115 //------------------------------------------------------------------------------
116 // Rule Definition
117 //------------------------------------------------------------------------------
118
119 /** @type {import('../shared/types').Rule} */
120 module.exports = {
121 meta: {
122 type: "suggestion",
123
124 docs: {
125 description: "Disallow use of the `RegExp` constructor in favor of regular expression literals",
126 recommended: false,
127 url: "https://eslint.org/docs/latest/rules/prefer-regex-literals"
128 },
129
130 hasSuggestions: true,
131
132 schema: [
133 {
134 type: "object",
135 properties: {
136 disallowRedundantWrapping: {
137 type: "boolean",
138 default: false
139 }
140 },
141 additionalProperties: false
142 }
143 ],
144
145 messages: {
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."
152 }
153 },
154
155 create(context) {
156 const [{ disallowRedundantWrapping = false } = {}] = context.options;
157 const sourceCode = context.sourceCode;
158
159 /**
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.
163 */
164 function isGlobalReference(node) {
165 const scope = sourceCode.getScope(node);
166 const variable = findVariable(scope, node);
167
168 return variable !== null && variable.scope.type === "global" && variable.defs.length === 0;
169 }
170
171 /**
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.
176 */
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);
182 }
183
184 /**
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.
188 */
189 function getStringValue(node) {
190 if (isStringLiteral(node)) {
191 return node.value;
192 }
193
194 if (isStaticTemplateLiteral(node)) {
195 return node.quasis[0].value.cooked;
196 }
197
198 if (isStringRawTaggedStaticTemplateLiteral(node)) {
199 return node.quasi.quasis[0].value.raw;
200 }
201
202 return null;
203 }
204
205 /**
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.
209 */
210 function isStaticString(node) {
211 return isStringLiteral(node) ||
212 isStaticTemplateLiteral(node) ||
213 isStringRawTaggedStaticTemplateLiteral(node);
214 }
215
216 /**
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.
220 */
221 function hasOnlyStaticStringArguments(node) {
222 const args = node.arguments;
223
224 if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) {
225 return true;
226 }
227
228 return false;
229 }
230
231 /**
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.
235 */
236 function isUnnecessarilyWrappedRegexLiteral(node) {
237 const args = node.arguments;
238
239 if (args.length === 1 && isRegexLiteral(args[0])) {
240 return true;
241 }
242
243 if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) {
244 return true;
245 }
246
247 return false;
248 }
249
250 /**
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.
254 */
255 function getRegexppEcmaVersion(ecmaVersion) {
256 if (ecmaVersion <= 5) {
257 return 5;
258 }
259 return Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION);
260 }
261
262 const regexppEcmaVersion = getRegexppEcmaVersion(context.languageOptions.ecmaVersion);
263
264 /**
265 * Makes a character escaped or else returns null.
266 * @param {string} character The character to escape.
267 * @returns {string} The resulting escaped character.
268 */
269 function resolveEscapes(character) {
270 switch (character) {
271 case "\n":
272 case "\\\n":
273 return "\\n";
274
275 case "\r":
276 case "\\\r":
277 return "\\r";
278
279 case "\t":
280 case "\\\t":
281 return "\\t";
282
283 case "\v":
284 case "\\\v":
285 return "\\v";
286
287 case "\f":
288 case "\\\f":
289 return "\\f";
290
291 case "/":
292 return "\\/";
293
294 default:
295 return null;
296 }
297 }
298
299 /**
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.
304 */
305 function isValidRegexForEcmaVersion(pattern, flags) {
306 const validator = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
307
308 try {
309 validator.validatePattern(pattern, 0, pattern.length, flags ? flags.includes("u") : false);
310 if (flags) {
311 validator.validateFlags(flags);
312 }
313 return true;
314 } catch {
315 return false;
316 }
317 }
318
319 /**
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.
324 */
325 function areFlagsEqual(flagsA, flagsB) {
326 return [...flagsA].sort().join("") === [...flagsB].sort().join("");
327 }
328
329
330 /**
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.
335 */
336 function mergeRegexFlags(flagsA, flagsB) {
337 const flagsSet = new Set([
338 ...flagsA,
339 ...flagsB
340 ]);
341
342 return [...flagsSet].join("");
343 }
344
345 /**
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.
351 */
352 function canFixTo(node, pattern, flags) {
353 const tokenBefore = sourceCode.getTokenBefore(node);
354
355 return sourceCode.getCommentsInside(node).length === 0 &&
356 (!tokenBefore || validPrecedingTokens.has(tokenBefore.value)) &&
357 isValidRegexForEcmaVersion(pattern, flags);
358 }
359
360 /**
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.
365 */
366 function getSafeOutput(node, newRegExpValue) {
367 const tokenBefore = sourceCode.getTokenBefore(node);
368 const tokenAfter = sourceCode.getTokenAfter(node);
369
370 return (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
371 newRegExpValue +
372 (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "");
373
374 }
375
376 return {
377 Program(node) {
378 const scope = sourceCode.getScope(node);
379 const tracker = new ReferenceTracker(scope);
380 const traceMap = {
381 RegExp: {
382 [CALL]: true,
383 [CONSTRUCT]: true
384 }
385 };
386
387 for (const { node: refNode } of tracker.iterateGlobalReferences(traceMap)) {
388 if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(refNode)) {
389 const regexNode = refNode.arguments[0];
390
391 if (refNode.arguments.length === 2) {
392 const suggests = [];
393
394 const argFlags = getStringValue(refNode.arguments[1]) || "";
395
396 if (canFixTo(refNode, regexNode.regex.pattern, argFlags)) {
397 suggests.push({
398 messageId: "replaceWithLiteralAndFlags",
399 pattern: regexNode.regex.pattern,
400 flags: argFlags
401 });
402 }
403
404 const literalFlags = regexNode.regex.flags || "";
405 const mergedFlags = mergeRegexFlags(literalFlags, argFlags);
406
407 if (
408 !areFlagsEqual(mergedFlags, argFlags) &&
409 canFixTo(refNode, regexNode.regex.pattern, mergedFlags)
410 ) {
411 suggests.push({
412 messageId: "replaceWithIntendedLiteralAndFlags",
413 pattern: regexNode.regex.pattern,
414 flags: mergedFlags
415 });
416 }
417
418 context.report({
419 node: refNode,
420 messageId: "unexpectedRedundantRegExpWithFlags",
421 suggest: suggests.map(({ flags, pattern, messageId }) => ({
422 messageId,
423 data: {
424 flags
425 },
426 fix(fixer) {
427 return fixer.replaceText(refNode, getSafeOutput(refNode, `/${pattern}/${flags}`));
428 }
429 }))
430 });
431 } else {
432 const outputs = [];
433
434 if (canFixTo(refNode, regexNode.regex.pattern, regexNode.regex.flags)) {
435 outputs.push(sourceCode.getText(regexNode));
436 }
437
438
439 context.report({
440 node: refNode,
441 messageId: "unexpectedRedundantRegExp",
442 suggest: outputs.map(output => ({
443 messageId: "replaceWithLiteral",
444 fix(fixer) {
445 return fixer.replaceText(
446 refNode,
447 getSafeOutput(refNode, output)
448 );
449 }
450 }))
451 });
452 }
453 } else if (hasOnlyStaticStringArguments(refNode)) {
454 let regexContent = getStringValue(refNode.arguments[0]);
455 let noFix = false;
456 let flags;
457
458 if (refNode.arguments[1]) {
459 flags = getStringValue(refNode.arguments[1]);
460 }
461
462 if (!canFixTo(refNode, regexContent, flags)) {
463 noFix = true;
464 }
465
466 if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
467 noFix = true;
468 }
469
470 if (regexContent && !noFix) {
471 let charIncrease = 0;
472
473 const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
474
475 visitRegExpAST(ast, {
476 onCharacterEnter(characterNode) {
477 const escaped = resolveEscapes(characterNode.raw);
478
479 if (escaped) {
480 regexContent =
481 regexContent.slice(0, characterNode.start + charIncrease) +
482 escaped +
483 regexContent.slice(characterNode.end + charIncrease);
484
485 if (characterNode.raw.length === 1) {
486 charIncrease += 1;
487 }
488 }
489 }
490 });
491 }
492
493 const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}`;
494
495 context.report({
496 node: refNode,
497 messageId: "unexpectedRegExp",
498 suggest: noFix ? [] : [{
499 messageId: "replaceWithLiteral",
500 fix(fixer) {
501 return fixer.replaceText(refNode, getSafeOutput(refNode, newRegExpValue));
502 }
503 }]
504 });
505 }
506 }
507 }
508 };
509 }
510 };