]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to flag updates of imported bindings. | |
3 | * @author Toru Nagashima <https://github.com/mysticatea> | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Helpers | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const { findVariable, getPropertyName } = require("eslint-utils"); | |
13 | ||
14 | const MutationMethods = { | |
15 | Object: new Set([ | |
16 | "assign", "defineProperties", "defineProperty", "freeze", | |
17 | "setPrototypeOf" | |
18 | ]), | |
19 | Reflect: new Set([ | |
20 | "defineProperty", "deleteProperty", "set", "setPrototypeOf" | |
21 | ]) | |
22 | }; | |
23 | ||
24 | /** | |
25 | * Check if a given node is LHS of an assignment node. | |
26 | * @param {ASTNode} node The node to check. | |
27 | * @returns {boolean} `true` if the node is LHS. | |
28 | */ | |
29 | function isAssignmentLeft(node) { | |
30 | const { parent } = node; | |
31 | ||
32 | return ( | |
33 | ( | |
34 | parent.type === "AssignmentExpression" && | |
35 | parent.left === node | |
36 | ) || | |
37 | ||
38 | // Destructuring assignments | |
39 | parent.type === "ArrayPattern" || | |
40 | ( | |
41 | parent.type === "Property" && | |
42 | parent.value === node && | |
43 | parent.parent.type === "ObjectPattern" | |
44 | ) || | |
45 | parent.type === "RestElement" || | |
46 | ( | |
47 | parent.type === "AssignmentPattern" && | |
48 | parent.left === node | |
49 | ) | |
50 | ); | |
51 | } | |
52 | ||
53 | /** | |
54 | * Check if a given node is the operand of mutation unary operator. | |
55 | * @param {ASTNode} node The node to check. | |
56 | * @returns {boolean} `true` if the node is the operand of mutation unary operator. | |
57 | */ | |
58 | function isOperandOfMutationUnaryOperator(node) { | |
59 | const { parent } = node; | |
60 | ||
61 | return ( | |
62 | ( | |
63 | parent.type === "UpdateExpression" && | |
64 | parent.argument === node | |
65 | ) || | |
66 | ( | |
67 | parent.type === "UnaryExpression" && | |
68 | parent.operator === "delete" && | |
69 | parent.argument === node | |
70 | ) | |
71 | ); | |
72 | } | |
73 | ||
74 | /** | |
75 | * Check if a given node is the iteration variable of `for-in`/`for-of` syntax. | |
76 | * @param {ASTNode} node The node to check. | |
77 | * @returns {boolean} `true` if the node is the iteration variable. | |
78 | */ | |
79 | function isIterationVariable(node) { | |
80 | const { parent } = node; | |
81 | ||
82 | return ( | |
83 | ( | |
84 | parent.type === "ForInStatement" && | |
85 | parent.left === node | |
86 | ) || | |
87 | ( | |
88 | parent.type === "ForOfStatement" && | |
89 | parent.left === node | |
90 | ) | |
91 | ); | |
92 | } | |
93 | ||
94 | /** | |
95 | * Check if a given node is the iteration variable of `for-in`/`for-of` syntax. | |
96 | * @param {ASTNode} node The node to check. | |
97 | * @param {Scope} scope A `escope.Scope` object to find variable (whichever). | |
98 | * @returns {boolean} `true` if the node is the iteration variable. | |
99 | */ | |
100 | function isArgumentOfWellKnownMutationFunction(node, scope) { | |
101 | const { parent } = node; | |
102 | ||
103 | if ( | |
104 | parent.type === "CallExpression" && | |
105 | parent.arguments[0] === node && | |
106 | parent.callee.type === "MemberExpression" && | |
107 | parent.callee.object.type === "Identifier" | |
108 | ) { | |
109 | const { callee } = parent; | |
110 | const { object } = callee; | |
111 | ||
112 | if (Object.keys(MutationMethods).includes(object.name)) { | |
113 | const variable = findVariable(scope, object); | |
114 | ||
115 | return ( | |
116 | variable !== null && | |
117 | variable.scope.type === "global" && | |
118 | MutationMethods[object.name].has(getPropertyName(callee, scope)) | |
119 | ); | |
120 | } | |
121 | } | |
122 | ||
123 | return false; | |
124 | } | |
125 | ||
126 | /** | |
127 | * Check if the identifier node is placed at to update members. | |
128 | * @param {ASTNode} id The Identifier node to check. | |
129 | * @param {Scope} scope A `escope.Scope` object to find variable (whichever). | |
130 | * @returns {boolean} `true` if the member of `id` was updated. | |
131 | */ | |
132 | function isMemberWrite(id, scope) { | |
133 | const { parent } = id; | |
134 | ||
135 | return ( | |
136 | ( | |
137 | parent.type === "MemberExpression" && | |
138 | parent.object === id && | |
139 | ( | |
140 | isAssignmentLeft(parent) || | |
141 | isOperandOfMutationUnaryOperator(parent) || | |
142 | isIterationVariable(parent) | |
143 | ) | |
144 | ) || | |
145 | isArgumentOfWellKnownMutationFunction(id, scope) | |
146 | ); | |
147 | } | |
148 | ||
149 | /** | |
150 | * Get the mutation node. | |
151 | * @param {ASTNode} id The Identifier node to get. | |
152 | * @returns {ASTNode} The mutation node. | |
153 | */ | |
154 | function getWriteNode(id) { | |
155 | let node = id.parent; | |
156 | ||
157 | while ( | |
158 | node && | |
159 | node.type !== "AssignmentExpression" && | |
160 | node.type !== "UpdateExpression" && | |
161 | node.type !== "UnaryExpression" && | |
162 | node.type !== "CallExpression" && | |
163 | node.type !== "ForInStatement" && | |
164 | node.type !== "ForOfStatement" | |
165 | ) { | |
166 | node = node.parent; | |
167 | } | |
168 | ||
169 | return node || id; | |
170 | } | |
171 | ||
172 | //------------------------------------------------------------------------------ | |
173 | // Rule Definition | |
174 | //------------------------------------------------------------------------------ | |
175 | ||
176 | module.exports = { | |
177 | meta: { | |
178 | type: "problem", | |
179 | ||
180 | docs: { | |
181 | description: "disallow assigning to imported bindings", | |
182 | category: "Possible Errors", | |
183 | recommended: true, | |
184 | url: "https://eslint.org/docs/rules/no-import-assign" | |
185 | }, | |
186 | ||
187 | schema: [], | |
188 | ||
189 | messages: { | |
190 | readonly: "'{{name}}' is read-only.", | |
191 | readonlyMember: "The members of '{{name}}' are read-only." | |
192 | } | |
193 | }, | |
194 | ||
195 | create(context) { | |
196 | return { | |
197 | ImportDeclaration(node) { | |
198 | const scope = context.getScope(); | |
199 | ||
200 | for (const variable of context.getDeclaredVariables(node)) { | |
201 | const shouldCheckMembers = variable.defs.some( | |
202 | d => d.node.type === "ImportNamespaceSpecifier" | |
203 | ); | |
204 | let prevIdNode = null; | |
205 | ||
206 | for (const reference of variable.references) { | |
207 | const idNode = reference.identifier; | |
208 | ||
209 | /* | |
210 | * AssignmentPattern (e.g. `[a = 0] = b`) makes two write | |
211 | * references for the same identifier. This should skip | |
212 | * the one of the two in order to prevent redundant reports. | |
213 | */ | |
214 | if (idNode === prevIdNode) { | |
215 | continue; | |
216 | } | |
217 | prevIdNode = idNode; | |
218 | ||
219 | if (reference.isWrite()) { | |
220 | context.report({ | |
221 | node: getWriteNode(idNode), | |
222 | messageId: "readonly", | |
223 | data: { name: idNode.name } | |
224 | }); | |
225 | } else if (shouldCheckMembers && isMemberWrite(idNode, scope)) { | |
226 | context.report({ | |
227 | node: getWriteNode(idNode), | |
228 | messageId: "readonlyMember", | |
229 | data: { name: idNode.name } | |
230 | }); | |
231 | } | |
232 | } | |
233 | } | |
234 | } | |
235 | }; | |
236 | ||
237 | } | |
238 | }; |