2 * @fileoverview Rule to flag updates of imported bindings.
3 * @author Toru Nagashima <https://github.com/mysticatea>
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const { findVariable
} = require("@eslint-community/eslint-utils");
13 const astUtils
= require("./utils/ast-utils");
15 const WellKnownMutationFunctions
= {
16 Object
: /^(?:assign|definePropert(?:y|ies)|freeze|setPrototypeOf)$/u,
17 Reflect
: /^(?:(?:define|delete)Property|set(?:PrototypeOf)?)$/u
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.
25 function isAssignmentLeft(node
) {
26 const { parent
} = node
;
30 parent
.type
=== "AssignmentExpression" &&
34 // Destructuring assignments
35 parent
.type
=== "ArrayPattern" ||
37 parent
.type
=== "Property" &&
38 parent
.value
=== node
&&
39 parent
.parent
.type
=== "ObjectPattern"
41 parent
.type
=== "RestElement" ||
43 parent
.type
=== "AssignmentPattern" &&
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.
54 function isOperandOfMutationUnaryOperator(node
) {
55 const argumentNode
= node
.parent
.type
=== "ChainExpression"
58 const { parent
} = argumentNode
;
62 parent
.type
=== "UpdateExpression" &&
63 parent
.argument
=== argumentNode
66 parent
.type
=== "UnaryExpression" &&
67 parent
.operator
=== "delete" &&
68 parent
.argument
=== argumentNode
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.
78 function isIterationVariable(node
) {
79 const { parent
} = node
;
83 parent
.type
=== "ForInStatement" &&
87 parent
.type
=== "ForOfStatement" &&
94 * Check if a given node is at the first argument of a well-known mutation function.
96 * - `Object.defineProperty`
97 * - `Object.defineProperties`
99 * - `Object.setPrototypeOf`
100 * - `Reflect.defineProperty`
101 * - `Reflect.deleteProperty`
103 * - `Reflect.setPrototypeOf`
104 * @param {ASTNode} node The node to check.
105 * @param {Scope} scope A `escope.Scope` object to find variable (whichever).
106 * @returns {boolean} `true` if the node is at the first argument of a well-known mutation function.
108 function isArgumentOfWellKnownMutationFunction(node
, scope
) {
109 const { parent
} = node
;
111 if (parent
.type
!== "CallExpression" || parent
.arguments
[0] !== node
) {
114 const callee
= astUtils
.skipChainExpression(parent
.callee
);
117 !astUtils
.isSpecificMemberAccess(callee
, "Object", WellKnownMutationFunctions
.Object
) &&
118 !astUtils
.isSpecificMemberAccess(callee
, "Reflect", WellKnownMutationFunctions
.Reflect
)
122 const variable
= findVariable(scope
, callee
.object
);
124 return variable
!== null && variable
.scope
.type
=== "global";
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.
133 function isMemberWrite(id
, scope
) {
134 const { parent
} = id
;
138 parent
.type
=== "MemberExpression" &&
139 parent
.object
=== id
&&
141 isAssignmentLeft(parent
) ||
142 isOperandOfMutationUnaryOperator(parent
) ||
143 isIterationVariable(parent
)
146 isArgumentOfWellKnownMutationFunction(id
, scope
)
151 * Get the mutation node.
152 * @param {ASTNode} id The Identifier node to get.
153 * @returns {ASTNode} The mutation node.
155 function getWriteNode(id
) {
156 let node
= id
.parent
;
160 node
.type
!== "AssignmentExpression" &&
161 node
.type
!== "UpdateExpression" &&
162 node
.type
!== "UnaryExpression" &&
163 node
.type
!== "CallExpression" &&
164 node
.type
!== "ForInStatement" &&
165 node
.type
!== "ForOfStatement"
173 //------------------------------------------------------------------------------
175 //------------------------------------------------------------------------------
177 /** @type {import('../shared/types').Rule} */
183 description
: "Disallow assigning to imported bindings",
185 url
: "https://eslint.org/docs/latest/rules/no-import-assign"
191 readonly
: "'{{name}}' is read-only.",
192 readonlyMember
: "The members of '{{name}}' are read-only."
197 const sourceCode
= context
.sourceCode
;
200 ImportDeclaration(node
) {
201 const scope
= sourceCode
.getScope(node
);
203 for (const variable
of sourceCode
.getDeclaredVariables(node
)) {
204 const shouldCheckMembers
= variable
.defs
.some(
205 d
=> d
.node
.type
=== "ImportNamespaceSpecifier"
207 let prevIdNode
= null;
209 for (const reference
of variable
.references
) {
210 const idNode
= reference
.identifier
;
213 * AssignmentPattern (e.g. `[a = 0] = b`) makes two write
214 * references for the same identifier. This should skip
215 * the one of the two in order to prevent redundant reports.
217 if (idNode
=== prevIdNode
) {
222 if (reference
.isWrite()) {
224 node
: getWriteNode(idNode
),
225 messageId
: "readonly",
226 data
: { name
: idNode
.name
}
228 } else if (shouldCheckMembers
&& isMemberWrite(idNode
, scope
)) {
230 node
: getWriteNode(idNode
),
231 messageId
: "readonlyMember",
232 data
: { name
: idNode
.name
}