| /** |
| * @fileoverview Rule to flag updates of imported bindings. |
| * @author Toru Nagashima <https://github.com/mysticatea> |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| const { findVariable } = require("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/rules/no-import-assign" |
| }, |
| |
| schema: [], |
| |
| messages: { |
| readonly: "'{{name}}' is read-only.", |
| readonlyMember: "The members of '{{name}}' are read-only." |
| } |
| }, |
| |
| create(context) { |
| return { |
| ImportDeclaration(node) { |
| const scope = context.getScope(); |
| |
| for (const variable of context.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 } |
| }); |
| } |
| } |
| } |
| } |
| }; |
| |
| } |
| }; |