]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
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 | ||
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 | ||
6f036462 TL |
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 | ||
eb39fafa DC |
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 | ||
6f036462 TL |
62 | schema: [ |
63 | { | |
64 | type: "object", | |
65 | properties: { | |
66 | disallowRedundantWrapping: { | |
67 | type: "boolean", | |
68 | default: false | |
69 | } | |
70 | }, | |
71 | additionalProperties: false | |
72 | } | |
73 | ], | |
eb39fafa DC |
74 | |
75 | messages: { | |
6f036462 TL |
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." | |
eb39fafa DC |
79 | } |
80 | }, | |
81 | ||
82 | create(context) { | |
6f036462 | 83 | const [{ disallowRedundantWrapping = false } = {}] = context.options; |
eb39fafa DC |
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" && | |
6f036462 TL |
105 | astUtils.isSpecificMemberAccess(node.tag, "String", "raw") && |
106 | isGlobalReference(astUtils.skipChainExpression(node.tag).object) && | |
eb39fafa DC |
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 | ||
6f036462 TL |
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 | ||
eb39fafa DC |
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)) { | |
6f036462 TL |
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)) { | |
eb39fafa DC |
174 | context.report({ node, messageId: "unexpectedRegExp" }); |
175 | } | |
176 | } | |
177 | } | |
178 | }; | |
179 | } | |
180 | }; |