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 12const { findVariable } = require("eslint-utils"); 13const astUtils = require("./utils/ast-utils"); 14 15const WellKnownMutationFunctions = { 16 Object: /^(?:assign|definePropert(?:y|ies)|freeze|setPrototypeOf)$/u, 17 Reflect: /^(?:(?:define|delete)Property|set(?:PrototypeOf)?)$/u 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 */ 25function 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 */ 54function isOperandOfMutationUnaryOperator(node) { 55 const argumentNode = node.parent.type === "ChainExpression" 56 ? node.parent 57 : node; 58 const { parent } = argumentNode; 59 60 return ( 61 ( 62 parent.type === "UpdateExpression" && 63 parent.argument === argumentNode 64 ) || 65 ( 66 parent.type === "UnaryExpression" && 67 parent.operator === "delete" && 68 parent.argument === argumentNode 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 */ 78function 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/** 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` 100 * - `Refrect.defineProperty` 101 * - `Refrect.deleteProperty` 102 * - `Refrect.set` 103 * - `Refrect.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. 107 */ 108function isArgumentOfWellKnownMutationFunction(node, scope) { 109 const { parent } = node; 110 111 if (parent.type !== "CallExpression" || parent.arguments[0] !== node) { 112 return false; 113 } 114 const callee = astUtils.skipChainExpression(parent.callee); 115 116 if ( 117 !astUtils.isSpecificMemberAccess(callee, "Object", WellKnownMutationFunctions.Object) && 118 !astUtils.isSpecificMemberAccess(callee, "Reflect", WellKnownMutationFunctions.Reflect) 119 ) { 120 return false; 121 } 122 const variable = findVariable(scope, callee.object); 123 124 return variable !== null && variable.scope.type === "global"; 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 */ 133function 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 */ 155function 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 177module.exports = { 178 meta: { 179 type: "problem", 180 181 docs: { 182 description: "disallow assigning to imported bindings", 183 category: "Possible Errors", 184 recommended: true, 185 url: "https://eslint.org/docs/rules/no-import-assign" 186 }, 187 188 schema: [], 189 190 messages: { 191 readonly: "'{{name}}' is read-only.", 192 readonlyMember: "The members of '{{name}}' are read-only." 193 } 194 }, 195 196 create(context) { 197 return { 198 ImportDeclaration(node) { 199 const scope = context.getScope(); 200 201 for (const variable of context.getDeclaredVariables(node)) { 202 const shouldCheckMembers = variable.defs.some( 203 d => d.node.type === "ImportNamespaceSpecifier" 204 ); 205 let prevIdNode = null; 206 207 for (const reference of variable.references) { 208 const idNode = reference.identifier; 209 210 /* 211 * AssignmentPattern (e.g. `[a = 0] = b`) makes two write 212 * references for the same identifier. This should skip 213 * the one of the two in order to prevent redundant reports. 214 */ 215 if (idNode === prevIdNode) { 216 continue; 217 } 218 prevIdNode = idNode; 219 220 if (reference.isWrite()) { 221 context.report({ 222 node: getWriteNode(idNode), 223 messageId: "readonly", 224 data: { name: idNode.name } 225 }); 226 } else if (shouldCheckMembers && isMemberWrite(idNode, scope)) { 227 context.report({ 228 node: getWriteNode(idNode), 229 messageId: "readonlyMember", 230 data: { name: idNode.name } 231 }); 232 } 233 } 234 } 235 } 236 }; 237 238 } 239}; 240