1 | /**
|
2 | * @fileoverview Rule to disallow use of the `RegExp` constructor in favor of regular expression literals
|
3 | * @author Milos Djermanovic
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const astUtils = require("./utils/ast-utils");
|
13 | const { CALL, CONSTRUCT, ReferenceTracker, findVariable } = require("eslint-utils");
|
14 |
|
15 | //------------------------------------------------------------------------------
|
16 | // Helpers
|
17 | //------------------------------------------------------------------------------
|
18 |
|
19 | /**
|
20 | * Determines whether the given node is a string literal.
|
21 | * @param {ASTNode} node Node to check.
|
22 | * @returns {boolean} True if the node is a string literal.
|
23 | */
|
24 | function isStringLiteral(node) {
|
25 | return node.type === "Literal" && typeof node.value === "string";
|
26 | }
|
27 |
|
28 | /**
|
29 | * Determines whether the given node is a regex literal.
|
30 | * @param {ASTNode} node Node to check.
|
31 | * @returns {boolean} True if the node is a regex literal.
|
32 | */
|
33 | function isRegexLiteral(node) {
|
34 | return node.type === "Literal" && Object.prototype.hasOwnProperty.call(node, "regex");
|
35 | }
|
36 |
|
37 | /**
|
38 | * Determines whether the given node is a template literal without expressions.
|
39 | * @param {ASTNode} node Node to check.
|
40 | * @returns {boolean} True if the node is a template literal without expressions.
|
41 | */
|
42 | function isStaticTemplateLiteral(node) {
|
43 | return node.type === "TemplateLiteral" && node.expressions.length === 0;
|
44 | }
|
45 |
|
46 |
|
47 | //------------------------------------------------------------------------------
|
48 | // Rule Definition
|
49 | //------------------------------------------------------------------------------
|
50 |
|
51 | module.exports = {
|
52 | meta: {
|
53 | type: "suggestion",
|
54 |
|
55 | docs: {
|
56 | description: "disallow use of the `RegExp` constructor in favor of regular expression literals",
|
57 | category: "Best Practices",
|
58 | recommended: false,
|
59 | url: "https://eslint.org/docs/rules/prefer-regex-literals"
|
60 | },
|
61 |
|
62 | schema: [
|
63 | {
|
64 | type: "object",
|
65 | properties: {
|
66 | disallowRedundantWrapping: {
|
67 | type: "boolean",
|
68 | default: false
|
69 | }
|
70 | },
|
71 | additionalProperties: false
|
72 | }
|
73 | ],
|
74 |
|
75 | messages: {
|
76 | unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.",
|
77 | unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
|
78 | unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor."
|
79 | }
|
80 | },
|
81 |
|
82 | create(context) {
|
83 | const [{ disallowRedundantWrapping = false } = {}] = context.options;
|
84 |
|
85 | /**
|
86 | * Determines whether the given identifier node is a reference to a global variable.
|
87 | * @param {ASTNode} node `Identifier` node to check.
|
88 | * @returns {boolean} True if the identifier is a reference to a global variable.
|
89 | */
|
90 | function isGlobalReference(node) {
|
91 | const scope = context.getScope();
|
92 | const variable = findVariable(scope, node);
|
93 |
|
94 | return variable !== null && variable.scope.type === "global" && variable.defs.length === 0;
|
95 | }
|
96 |
|
97 | /**
|
98 | * Determines whether the given node is a String.raw`` tagged template expression
|
99 | * with a static template literal.
|
100 | * @param {ASTNode} node Node to check.
|
101 | * @returns {boolean} True if the node is String.raw`` with a static template.
|
102 | */
|
103 | function isStringRawTaggedStaticTemplateLiteral(node) {
|
104 | return node.type === "TaggedTemplateExpression" &&
|
105 | astUtils.isSpecificMemberAccess(node.tag, "String", "raw") &&
|
106 | isGlobalReference(astUtils.skipChainExpression(node.tag).object) &&
|
107 | isStaticTemplateLiteral(node.quasi);
|
108 | }
|
109 |
|
110 | /**
|
111 | * Determines whether the given node is considered to be a static string by the logic of this rule.
|
112 | * @param {ASTNode} node Node to check.
|
113 | * @returns {boolean} True if the node is a static string.
|
114 | */
|
115 | function isStaticString(node) {
|
116 | return isStringLiteral(node) ||
|
117 | isStaticTemplateLiteral(node) ||
|
118 | isStringRawTaggedStaticTemplateLiteral(node);
|
119 | }
|
120 |
|
121 | /**
|
122 | * Determines whether the relevant arguments of the given are all static string literals.
|
123 | * @param {ASTNode} node Node to check.
|
124 | * @returns {boolean} True if all arguments are static strings.
|
125 | */
|
126 | function hasOnlyStaticStringArguments(node) {
|
127 | const args = node.arguments;
|
128 |
|
129 | if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) {
|
130 | return true;
|
131 | }
|
132 |
|
133 | return false;
|
134 | }
|
135 |
|
136 | /**
|
137 | * Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped.
|
138 | * @param {ASTNode} node Node to check.
|
139 | * @returns {boolean} True if the node already contains a regex literal argument.
|
140 | */
|
141 | function isUnnecessarilyWrappedRegexLiteral(node) {
|
142 | const args = node.arguments;
|
143 |
|
144 | if (args.length === 1 && isRegexLiteral(args[0])) {
|
145 | return true;
|
146 | }
|
147 |
|
148 | if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) {
|
149 | return true;
|
150 | }
|
151 |
|
152 | return false;
|
153 | }
|
154 |
|
155 | return {
|
156 | Program() {
|
157 | const scope = context.getScope();
|
158 | const tracker = new ReferenceTracker(scope);
|
159 | const traceMap = {
|
160 | RegExp: {
|
161 | [CALL]: true,
|
162 | [CONSTRUCT]: true
|
163 | }
|
164 | };
|
165 |
|
166 | for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
|
167 | if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) {
|
168 | if (node.arguments.length === 2) {
|
169 | context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" });
|
170 | } else {
|
171 | context.report({ node, messageId: "unexpectedRedundantRegExp" });
|
172 | }
|
173 | } else if (hasOnlyStaticStringArguments(node)) {
|
174 | context.report({ node, messageId: "unexpectedRegExp" });
|
175 | }
|
176 | }
|
177 | }
|
178 | };
|
179 | }
|
180 | };
|