| /** | |
|  * @fileoverview Rule to flag updates of imported bindings. | |
|  * @author Toru Nagashima <https://github.com/mysticatea> | |
|  */ | |
| 
 | |
| "use strict"; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
|  | |
| const { findVariable } = require("@eslint-community/eslint-utils"); | |
| const astUtils = require("./utils/ast-utils"); | |
| 
 | |
| const WellKnownMutationFunctions = { | |
|     Object: /^(?:assign|definePropert(?:y|ies)|freeze|setPrototypeOf)$/u, | |
|     Reflect: /^(?:(?:define|delete)Property|set(?:PrototypeOf)?)$/u | |
| }; | |
| 
 | |
| /** | |
|  * Check if a given node is LHS of an assignment node. | |
|  * @param {ASTNode} node The node to check. | |
|  * @returns {boolean} `true` if the node is LHS. | |
|  */ | |
| function isAssignmentLeft(node) { | |
|     const { parent } = node; | |
| 
 | |
|     return ( | |
|         ( | |
|             parent.type === "AssignmentExpression" && | |
|             parent.left === node | |
|         ) || | |
| 
 | |
|         // Destructuring assignments | |
|         parent.type === "ArrayPattern" || | |
|         ( | |
|             parent.type === "Property" && | |
|             parent.value === node && | |
|             parent.parent.type === "ObjectPattern" | |
|         ) || | |
|         parent.type === "RestElement" || | |
|         ( | |
|             parent.type === "AssignmentPattern" && | |
|             parent.left === node | |
|         ) | |
|     ); | |
| } | |
| 
 | |
| /** | |
|  * Check if a given node is the operand of mutation unary operator. | |
|  * @param {ASTNode} node The node to check. | |
|  * @returns {boolean} `true` if the node is the operand of mutation unary operator. | |
|  */ | |
| function isOperandOfMutationUnaryOperator(node) { | |
|     const argumentNode = node.parent.type === "ChainExpression" | |
|         ? node.parent | |
|         : node; | |
|     const { parent } = argumentNode; | |
| 
 | |
|     return ( | |
|         ( | |
|             parent.type === "UpdateExpression" && | |
|             parent.argument === argumentNode | |
|         ) || | |
|         ( | |
|             parent.type === "UnaryExpression" && | |
|             parent.operator === "delete" && | |
|             parent.argument === argumentNode | |
|         ) | |
|     ); | |
| } | |
| 
 | |
| /** | |
|  * Check if a given node is the iteration variable of `for-in`/`for-of` syntax. | |
|  * @param {ASTNode} node The node to check. | |
|  * @returns {boolean} `true` if the node is the iteration variable. | |
|  */ | |
| function isIterationVariable(node) { | |
|     const { parent } = node; | |
| 
 | |
|     return ( | |
|         ( | |
|             parent.type === "ForInStatement" && | |
|             parent.left === node | |
|         ) || | |
|         ( | |
|             parent.type === "ForOfStatement" && | |
|             parent.left === node | |
|         ) | |
|     ); | |
| } | |
| 
 | |
| /** | |
|  * Check if a given node is at the first argument of a well-known mutation function. | |
|  * - `Object.assign` | |
|  * - `Object.defineProperty` | |
|  * - `Object.defineProperties` | |
|  * - `Object.freeze` | |
|  * - `Object.setPrototypeOf` | |
|  * - `Reflect.defineProperty` | |
|  * - `Reflect.deleteProperty` | |
|  * - `Reflect.set` | |
|  * - `Reflect.setPrototypeOf` | |
|  * @param {ASTNode} node The node to check. | |
|  * @param {Scope} scope A `escope.Scope` object to find variable (whichever). | |
|  * @returns {boolean} `true` if the node is at the first argument of a well-known mutation function. | |
|  */ | |
| function isArgumentOfWellKnownMutationFunction(node, scope) { | |
|     const { parent } = node; | |
| 
 | |
|     if (parent.type !== "CallExpression" || parent.arguments[0] !== node) { | |
|         return false; | |
|     } | |
|     const callee = astUtils.skipChainExpression(parent.callee); | |
| 
 | |
|     if ( | |
|         !astUtils.isSpecificMemberAccess(callee, "Object", WellKnownMutationFunctions.Object) && | |
|         !astUtils.isSpecificMemberAccess(callee, "Reflect", WellKnownMutationFunctions.Reflect) | |
|     ) { | |
|         return false; | |
|     } | |
|     const variable = findVariable(scope, callee.object); | |
| 
 | |
|     return variable !== null && variable.scope.type === "global"; | |
| } | |
| 
 | |
| /** | |
|  * Check if the identifier node is placed at to update members. | |
|  * @param {ASTNode} id The Identifier node to check. | |
|  * @param {Scope} scope A `escope.Scope` object to find variable (whichever). | |
|  * @returns {boolean} `true` if the member of `id` was updated. | |
|  */ | |
| function isMemberWrite(id, scope) { | |
|     const { parent } = id; | |
| 
 | |
|     return ( | |
|         ( | |
|             parent.type === "MemberExpression" && | |
|             parent.object === id && | |
|             ( | |
|                 isAssignmentLeft(parent) || | |
|                 isOperandOfMutationUnaryOperator(parent) || | |
|                 isIterationVariable(parent) | |
|             ) | |
|         ) || | |
|         isArgumentOfWellKnownMutationFunction(id, scope) | |
|     ); | |
| } | |
| 
 | |
| /** | |
|  * Get the mutation node. | |
|  * @param {ASTNode} id The Identifier node to get. | |
|  * @returns {ASTNode} The mutation node. | |
|  */ | |
| function getWriteNode(id) { | |
|     let node = id.parent; | |
| 
 | |
|     while ( | |
|         node && | |
|         node.type !== "AssignmentExpression" && | |
|         node.type !== "UpdateExpression" && | |
|         node.type !== "UnaryExpression" && | |
|         node.type !== "CallExpression" && | |
|         node.type !== "ForInStatement" && | |
|         node.type !== "ForOfStatement" | |
|     ) { | |
|         node = node.parent; | |
|     } | |
| 
 | |
|     return node || id; | |
| } | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "problem", | |
| 
 | |
|         docs: { | |
|             description: "Disallow assigning to imported bindings", | |
|             recommended: true, | |
|             url: "https://eslint.org/docs/latest/rules/no-import-assign" | |
|         }, | |
| 
 | |
|         schema: [], | |
| 
 | |
|         messages: { | |
|             readonly: "'{{name}}' is read-only.", | |
|             readonlyMember: "The members of '{{name}}' are read-only." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
|         const sourceCode = context.sourceCode; | |
| 
 | |
|         return { | |
|             ImportDeclaration(node) { | |
|                 const scope = sourceCode.getScope(node); | |
| 
 | |
|                 for (const variable of sourceCode.getDeclaredVariables(node)) { | |
|                     const shouldCheckMembers = variable.defs.some( | |
|                         d => d.node.type === "ImportNamespaceSpecifier" | |
|                     ); | |
|                     let prevIdNode = null; | |
| 
 | |
|                     for (const reference of variable.references) { | |
|                         const idNode = reference.identifier; | |
| 
 | |
|                         /* | |
|                          * AssignmentPattern (e.g. `[a = 0] = b`) makes two write | |
|                          * references for the same identifier. This should skip | |
|                          * the one of the two in order to prevent redundant reports. | |
|                          */ | |
|                         if (idNode === prevIdNode) { | |
|                             continue; | |
|                         } | |
|                         prevIdNode = idNode; | |
| 
 | |
|                         if (reference.isWrite()) { | |
|                             context.report({ | |
|                                 node: getWriteNode(idNode), | |
|                                 messageId: "readonly", | |
|                                 data: { name: idNode.name } | |
|                             }); | |
|                         } else if (shouldCheckMembers && isMemberWrite(idNode, scope)) { | |
|                             context.report({ | |
|                                 node: getWriteNode(idNode), | |
|                                 messageId: "readonlyMember", | |
|                                 data: { name: idNode.name } | |
|                             }); | |
|                         } | |
|                     } | |
|                 } | |
|             } | |
|         }; | |
| 
 | |
|     } | |
| };
 |