]>
Commit | Line | Data |
---|---|---|
6f036462 TL |
1 | /** |
2 | * @fileoverview Rule that warns when identifier names that are | |
3 | * specified in the configuration are used. | |
4 | * @author Keith Cirkel (http://keithcirkel.co.uk) | |
5 | */ | |
6 | ||
7 | "use strict"; | |
8 | ||
9 | //------------------------------------------------------------------------------ | |
10 | // Helpers | |
11 | //------------------------------------------------------------------------------ | |
12 | ||
13 | /** | |
14 | * Checks whether the given node represents assignment target in a normal assignment or destructuring. | |
15 | * @param {ASTNode} node The node to check. | |
16 | * @returns {boolean} `true` if the node is assignment target. | |
17 | */ | |
18 | function isAssignmentTarget(node) { | |
19 | const parent = node.parent; | |
20 | ||
21 | return ( | |
22 | ||
23 | // normal assignment | |
24 | ( | |
25 | parent.type === "AssignmentExpression" && | |
26 | parent.left === node | |
27 | ) || | |
28 | ||
29 | // destructuring | |
30 | parent.type === "ArrayPattern" || | |
31 | parent.type === "RestElement" || | |
32 | ( | |
33 | parent.type === "Property" && | |
34 | parent.value === node && | |
35 | parent.parent.type === "ObjectPattern" | |
36 | ) || | |
37 | ( | |
38 | parent.type === "AssignmentPattern" && | |
39 | parent.left === node | |
40 | ) | |
41 | ); | |
42 | } | |
43 | ||
44 | /** | |
45 | * Checks whether the given node represents an imported name that is renamed in the same import/export specifier. | |
46 | * | |
47 | * Examples: | |
48 | * import { a as b } from 'mod'; // node `a` is renamed import | |
49 | * export { a as b } from 'mod'; // node `a` is renamed import | |
50 | * @param {ASTNode} node `Identifier` node to check. | |
51 | * @returns {boolean} `true` if the node is a renamed import. | |
52 | */ | |
53 | function isRenamedImport(node) { | |
54 | const parent = node.parent; | |
55 | ||
56 | return ( | |
57 | ( | |
58 | parent.type === "ImportSpecifier" && | |
59 | parent.imported !== parent.local && | |
60 | parent.imported === node | |
61 | ) || | |
62 | ( | |
63 | parent.type === "ExportSpecifier" && | |
64 | parent.parent.source && // re-export | |
65 | parent.local !== parent.exported && | |
66 | parent.local === node | |
67 | ) | |
68 | ); | |
69 | } | |
70 | ||
71 | /** | |
609c276f | 72 | * Checks whether the given node is an ObjectPattern destructuring. |
6f036462 TL |
73 | * |
74 | * Examples: | |
609c276f | 75 | * const { a : b } = foo; |
6f036462 | 76 | * @param {ASTNode} node `Identifier` node to check. |
609c276f | 77 | * @returns {boolean} `true` if the node is in an ObjectPattern destructuring. |
6f036462 | 78 | */ |
609c276f | 79 | function isPropertyNameInDestructuring(node) { |
6f036462 TL |
80 | const parent = node.parent; |
81 | ||
82 | return ( | |
83 | ( | |
84 | !parent.computed && | |
85 | parent.type === "Property" && | |
86 | parent.parent.type === "ObjectPattern" && | |
6f036462 TL |
87 | parent.key === node |
88 | ) | |
89 | ); | |
90 | } | |
91 | ||
6f036462 TL |
92 | //------------------------------------------------------------------------------ |
93 | // Rule Definition | |
94 | //------------------------------------------------------------------------------ | |
95 | ||
34eeec05 | 96 | /** @type {import('../shared/types').Rule} */ |
6f036462 TL |
97 | module.exports = { |
98 | meta: { | |
99 | type: "suggestion", | |
100 | ||
101 | docs: { | |
8f9d1d4d | 102 | description: "Disallow specified identifiers", |
6f036462 TL |
103 | recommended: false, |
104 | url: "https://eslint.org/docs/rules/id-denylist" | |
105 | }, | |
106 | ||
107 | schema: { | |
108 | type: "array", | |
109 | items: { | |
110 | type: "string" | |
111 | }, | |
112 | uniqueItems: true | |
113 | }, | |
114 | messages: { | |
609c276f TL |
115 | restricted: "Identifier '{{name}}' is restricted.", |
116 | restrictedPrivate: "Identifier '#{{name}}' is restricted." | |
6f036462 TL |
117 | } |
118 | }, | |
119 | ||
120 | create(context) { | |
121 | ||
122 | const denyList = new Set(context.options); | |
123 | const reportedNodes = new Set(); | |
124 | ||
125 | let globalScope; | |
126 | ||
127 | /** | |
128 | * Checks whether the given name is restricted. | |
129 | * @param {string} name The name to check. | |
130 | * @returns {boolean} `true` if the name is restricted. | |
131 | * @private | |
132 | */ | |
133 | function isRestricted(name) { | |
134 | return denyList.has(name); | |
135 | } | |
136 | ||
137 | /** | |
138 | * Checks whether the given node represents a reference to a global variable that is not declared in the source code. | |
139 | * These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables. | |
140 | * @param {ASTNode} node `Identifier` node to check. | |
141 | * @returns {boolean} `true` if the node is a reference to a global variable. | |
142 | */ | |
143 | function isReferenceToGlobalVariable(node) { | |
144 | const variable = globalScope.set.get(node.name); | |
145 | ||
146 | return variable && variable.defs.length === 0 && | |
147 | variable.references.some(ref => ref.identifier === node); | |
148 | } | |
149 | ||
150 | /** | |
151 | * Determines whether the given node should be checked. | |
152 | * @param {ASTNode} node `Identifier` node. | |
153 | * @returns {boolean} `true` if the node should be checked. | |
154 | */ | |
155 | function shouldCheck(node) { | |
156 | const parent = node.parent; | |
157 | ||
158 | /* | |
159 | * Member access has special rules for checking property names. | |
160 | * Read access to a property with a restricted name is allowed, because it can be on an object that user has no control over. | |
161 | * Write access isn't allowed, because it potentially creates a new property with a restricted name. | |
162 | */ | |
163 | if ( | |
164 | parent.type === "MemberExpression" && | |
165 | parent.property === node && | |
166 | !parent.computed | |
167 | ) { | |
168 | return isAssignmentTarget(parent); | |
169 | } | |
170 | ||
171 | return ( | |
172 | parent.type !== "CallExpression" && | |
173 | parent.type !== "NewExpression" && | |
174 | !isRenamedImport(node) && | |
609c276f TL |
175 | !isPropertyNameInDestructuring(node) && |
176 | !isReferenceToGlobalVariable(node) | |
6f036462 TL |
177 | ); |
178 | } | |
179 | ||
180 | /** | |
181 | * Reports an AST node as a rule violation. | |
182 | * @param {ASTNode} node The node to report. | |
183 | * @returns {void} | |
184 | * @private | |
185 | */ | |
186 | function report(node) { | |
609c276f TL |
187 | |
188 | /* | |
189 | * We used the range instead of the node because it's possible | |
190 | * for the same identifier to be represented by two different | |
191 | * nodes, with the most clear example being shorthand properties: | |
192 | * { foo } | |
193 | * In this case, "foo" is represented by one node for the name | |
194 | * and one for the value. The only way to know they are the same | |
195 | * is to look at the range. | |
196 | */ | |
197 | if (!reportedNodes.has(node.range.toString())) { | |
198 | const isPrivate = node.type === "PrivateIdentifier"; | |
199 | ||
6f036462 TL |
200 | context.report({ |
201 | node, | |
609c276f | 202 | messageId: isPrivate ? "restrictedPrivate" : "restricted", |
6f036462 TL |
203 | data: { |
204 | name: node.name | |
205 | } | |
206 | }); | |
609c276f | 207 | reportedNodes.add(node.range.toString()); |
6f036462 TL |
208 | } |
209 | } | |
210 | ||
211 | return { | |
212 | ||
213 | Program() { | |
214 | globalScope = context.getScope(); | |
215 | }, | |
216 | ||
609c276f TL |
217 | [[ |
218 | "Identifier", | |
219 | "PrivateIdentifier" | |
220 | ]](node) { | |
6f036462 TL |
221 | if (isRestricted(node.name) && shouldCheck(node)) { |
222 | report(node); | |
223 | } | |
224 | } | |
225 | }; | |
226 | } | |
227 | }; |