123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- /**
- * @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 }
- });
- }
- }
- }
- }
- };
- }
- };
|