]>
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 | ||
6f036462 TL |
12 | const { findVariable } = require("eslint-utils"); |
13 | const astUtils = require("./utils/ast-utils"); | |
14 | ||
15 | const WellKnownMutationFunctions = { | |
16 | Object: /^(?:assign|definePropert(?:y|ies)|freeze|setPrototypeOf)$/u, | |
17 | Reflect: /^(?:(?:define|delete)Property|set(?:PrototypeOf)?)$/u | |
eb39fafa DC |
18 | }; |
19 | ||
20 | /** | |
21 | * Check if a given node is LHS of an assignment node. | |
22 | * @param {ASTNode} node The node to check. | |
23 | * @returns {boolean} `true` if the node is LHS. | |
24 | */ | |
25 | function isAssignmentLeft(node) { | |
26 | const { parent } = node; | |
27 | ||
28 | return ( | |
29 | ( | |
30 | parent.type === "AssignmentExpression" && | |
31 | parent.left === node | |
32 | ) || | |
33 | ||
34 | // Destructuring assignments | |
35 | parent.type === "ArrayPattern" || | |
36 | ( | |
37 | parent.type === "Property" && | |
38 | parent.value === node && | |
39 | parent.parent.type === "ObjectPattern" | |
40 | ) || | |
41 | parent.type === "RestElement" || | |
42 | ( | |
43 | parent.type === "AssignmentPattern" && | |
44 | parent.left === node | |
45 | ) | |
46 | ); | |
47 | } | |
48 | ||
49 | /** | |
50 | * Check if a given node is the operand of mutation unary operator. | |
51 | * @param {ASTNode} node The node to check. | |
52 | * @returns {boolean} `true` if the node is the operand of mutation unary operator. | |
53 | */ | |
54 | function isOperandOfMutationUnaryOperator(node) { | |
6f036462 TL |
55 | const argumentNode = node.parent.type === "ChainExpression" |
56 | ? node.parent | |
57 | : node; | |
58 | const { parent } = argumentNode; | |
eb39fafa DC |
59 | |
60 | return ( | |
61 | ( | |
62 | parent.type === "UpdateExpression" && | |
6f036462 | 63 | parent.argument === argumentNode |
eb39fafa DC |
64 | ) || |
65 | ( | |
66 | parent.type === "UnaryExpression" && | |
67 | parent.operator === "delete" && | |
6f036462 | 68 | parent.argument === argumentNode |
eb39fafa DC |
69 | ) |
70 | ); | |
71 | } | |
72 | ||
73 | /** | |
74 | * Check if a given node is the iteration variable of `for-in`/`for-of` syntax. | |
75 | * @param {ASTNode} node The node to check. | |
76 | * @returns {boolean} `true` if the node is the iteration variable. | |
77 | */ | |
78 | function isIterationVariable(node) { | |
79 | const { parent } = node; | |
80 | ||
81 | return ( | |
82 | ( | |
83 | parent.type === "ForInStatement" && | |
84 | parent.left === node | |
85 | ) || | |
86 | ( | |
87 | parent.type === "ForOfStatement" && | |
88 | parent.left === node | |
89 | ) | |
90 | ); | |
91 | } | |
92 | ||
93 | /** | |
6f036462 TL |
94 | * Check if a given node is at the first argument of a well-known mutation function. |
95 | * - `Object.assign` | |
96 | * - `Object.defineProperty` | |
97 | * - `Object.defineProperties` | |
98 | * - `Object.freeze` | |
99 | * - `Object.setPrototypeOf` | |
456be15e TL |
100 | * - `Reflect.defineProperty` |
101 | * - `Reflect.deleteProperty` | |
102 | * - `Reflect.set` | |
103 | * - `Reflect.setPrototypeOf` | |
eb39fafa DC |
104 | * @param {ASTNode} node The node to check. |
105 | * @param {Scope} scope A `escope.Scope` object to find variable (whichever). | |
6f036462 | 106 | * @returns {boolean} `true` if the node is at the first argument of a well-known mutation function. |
eb39fafa DC |
107 | */ |
108 | function isArgumentOfWellKnownMutationFunction(node, scope) { | |
109 | const { parent } = node; | |
110 | ||
6f036462 TL |
111 | if (parent.type !== "CallExpression" || parent.arguments[0] !== node) { |
112 | return false; | |
113 | } | |
114 | const callee = astUtils.skipChainExpression(parent.callee); | |
115 | ||
eb39fafa | 116 | if ( |
6f036462 TL |
117 | !astUtils.isSpecificMemberAccess(callee, "Object", WellKnownMutationFunctions.Object) && |
118 | !astUtils.isSpecificMemberAccess(callee, "Reflect", WellKnownMutationFunctions.Reflect) | |
eb39fafa | 119 | ) { |
6f036462 | 120 | return false; |
eb39fafa | 121 | } |
6f036462 | 122 | const variable = findVariable(scope, callee.object); |
eb39fafa | 123 | |
6f036462 | 124 | return variable !== null && variable.scope.type === "global"; |
eb39fafa DC |
125 | } |
126 | ||
127 | /** | |
128 | * Check if the identifier node is placed at to update members. | |
129 | * @param {ASTNode} id The Identifier node to check. | |
130 | * @param {Scope} scope A `escope.Scope` object to find variable (whichever). | |
131 | * @returns {boolean} `true` if the member of `id` was updated. | |
132 | */ | |
133 | function isMemberWrite(id, scope) { | |
134 | const { parent } = id; | |
135 | ||
136 | return ( | |
137 | ( | |
138 | parent.type === "MemberExpression" && | |
139 | parent.object === id && | |
140 | ( | |
141 | isAssignmentLeft(parent) || | |
142 | isOperandOfMutationUnaryOperator(parent) || | |
143 | isIterationVariable(parent) | |
144 | ) | |
145 | ) || | |
146 | isArgumentOfWellKnownMutationFunction(id, scope) | |
147 | ); | |
148 | } | |
149 | ||
150 | /** | |
151 | * Get the mutation node. | |
152 | * @param {ASTNode} id The Identifier node to get. | |
153 | * @returns {ASTNode} The mutation node. | |
154 | */ | |
155 | function getWriteNode(id) { | |
156 | let node = id.parent; | |
157 | ||
158 | while ( | |
159 | node && | |
160 | node.type !== "AssignmentExpression" && | |
161 | node.type !== "UpdateExpression" && | |
162 | node.type !== "UnaryExpression" && | |
163 | node.type !== "CallExpression" && | |
164 | node.type !== "ForInStatement" && | |
165 | node.type !== "ForOfStatement" | |
166 | ) { | |
167 | node = node.parent; | |
168 | } | |
169 | ||
170 | return node || id; | |
171 | } | |
172 | ||
173 | //------------------------------------------------------------------------------ | |
174 | // Rule Definition | |
175 | //------------------------------------------------------------------------------ | |
176 | ||
177 | module.exports = { | |
178 | meta: { | |
179 | type: "problem", | |
180 | ||
181 | docs: { | |
182 | description: "disallow assigning to imported bindings", | |
eb39fafa DC |
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 | }; |