no-import-assign.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. /**
  2. * @fileoverview Rule to flag updates of imported bindings.
  3. * @author Toru Nagashima <https://github.com/mysticatea>
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Helpers
  8. //------------------------------------------------------------------------------
  9. const { findVariable } = require("@eslint-community/eslint-utils");
  10. const astUtils = require("./utils/ast-utils");
  11. const WellKnownMutationFunctions = {
  12. Object: /^(?:assign|definePropert(?:y|ies)|freeze|setPrototypeOf)$/u,
  13. Reflect: /^(?:(?:define|delete)Property|set(?:PrototypeOf)?)$/u,
  14. };
  15. /**
  16. * Check if a given node is LHS of an assignment node.
  17. * @param {ASTNode} node The node to check.
  18. * @returns {boolean} `true` if the node is LHS.
  19. */
  20. function isAssignmentLeft(node) {
  21. const { parent } = node;
  22. return (
  23. (parent.type === "AssignmentExpression" && parent.left === node) ||
  24. // Destructuring assignments
  25. parent.type === "ArrayPattern" ||
  26. (parent.type === "Property" &&
  27. parent.value === node &&
  28. parent.parent.type === "ObjectPattern") ||
  29. parent.type === "RestElement" ||
  30. (parent.type === "AssignmentPattern" && parent.left === node)
  31. );
  32. }
  33. /**
  34. * Check if a given node is the operand of mutation unary operator.
  35. * @param {ASTNode} node The node to check.
  36. * @returns {boolean} `true` if the node is the operand of mutation unary operator.
  37. */
  38. function isOperandOfMutationUnaryOperator(node) {
  39. const argumentNode =
  40. node.parent.type === "ChainExpression" ? node.parent : node;
  41. const { parent } = argumentNode;
  42. return (
  43. (parent.type === "UpdateExpression" &&
  44. parent.argument === argumentNode) ||
  45. (parent.type === "UnaryExpression" &&
  46. parent.operator === "delete" &&
  47. parent.argument === argumentNode)
  48. );
  49. }
  50. /**
  51. * Check if a given node is the iteration variable of `for-in`/`for-of` syntax.
  52. * @param {ASTNode} node The node to check.
  53. * @returns {boolean} `true` if the node is the iteration variable.
  54. */
  55. function isIterationVariable(node) {
  56. const { parent } = node;
  57. return (
  58. (parent.type === "ForInStatement" && parent.left === node) ||
  59. (parent.type === "ForOfStatement" && parent.left === node)
  60. );
  61. }
  62. /**
  63. * Check if a given node is at the first argument of a well-known mutation function.
  64. * - `Object.assign`
  65. * - `Object.defineProperty`
  66. * - `Object.defineProperties`
  67. * - `Object.freeze`
  68. * - `Object.setPrototypeOf`
  69. * - `Reflect.defineProperty`
  70. * - `Reflect.deleteProperty`
  71. * - `Reflect.set`
  72. * - `Reflect.setPrototypeOf`
  73. * @param {ASTNode} node The node to check.
  74. * @param {Scope} scope A `escope.Scope` object to find variable (whichever).
  75. * @returns {boolean} `true` if the node is at the first argument of a well-known mutation function.
  76. */
  77. function isArgumentOfWellKnownMutationFunction(node, scope) {
  78. const { parent } = node;
  79. if (parent.type !== "CallExpression" || parent.arguments[0] !== node) {
  80. return false;
  81. }
  82. const callee = astUtils.skipChainExpression(parent.callee);
  83. if (
  84. !astUtils.isSpecificMemberAccess(
  85. callee,
  86. "Object",
  87. WellKnownMutationFunctions.Object,
  88. ) &&
  89. !astUtils.isSpecificMemberAccess(
  90. callee,
  91. "Reflect",
  92. WellKnownMutationFunctions.Reflect,
  93. )
  94. ) {
  95. return false;
  96. }
  97. const variable = findVariable(scope, callee.object);
  98. return variable !== null && variable.scope.type === "global";
  99. }
  100. /**
  101. * Check if the identifier node is placed at to update members.
  102. * @param {ASTNode} id The Identifier node to check.
  103. * @param {Scope} scope A `escope.Scope` object to find variable (whichever).
  104. * @returns {boolean} `true` if the member of `id` was updated.
  105. */
  106. function isMemberWrite(id, scope) {
  107. const { parent } = id;
  108. return (
  109. (parent.type === "MemberExpression" &&
  110. parent.object === id &&
  111. (isAssignmentLeft(parent) ||
  112. isOperandOfMutationUnaryOperator(parent) ||
  113. isIterationVariable(parent))) ||
  114. isArgumentOfWellKnownMutationFunction(id, scope)
  115. );
  116. }
  117. /**
  118. * Get the mutation node.
  119. * @param {ASTNode} id The Identifier node to get.
  120. * @returns {ASTNode} The mutation node.
  121. */
  122. function getWriteNode(id) {
  123. let node = id.parent;
  124. while (
  125. node &&
  126. node.type !== "AssignmentExpression" &&
  127. node.type !== "UpdateExpression" &&
  128. node.type !== "UnaryExpression" &&
  129. node.type !== "CallExpression" &&
  130. node.type !== "ForInStatement" &&
  131. node.type !== "ForOfStatement"
  132. ) {
  133. node = node.parent;
  134. }
  135. return node || id;
  136. }
  137. //------------------------------------------------------------------------------
  138. // Rule Definition
  139. //------------------------------------------------------------------------------
  140. /** @type {import('../types').Rule.RuleModule} */
  141. module.exports = {
  142. meta: {
  143. type: "problem",
  144. docs: {
  145. description: "Disallow assigning to imported bindings",
  146. recommended: true,
  147. url: "https://eslint.org/docs/latest/rules/no-import-assign",
  148. },
  149. schema: [],
  150. messages: {
  151. readonly: "'{{name}}' is read-only.",
  152. readonlyMember: "The members of '{{name}}' are read-only.",
  153. },
  154. },
  155. create(context) {
  156. const sourceCode = context.sourceCode;
  157. return {
  158. ImportDeclaration(node) {
  159. const scope = sourceCode.getScope(node);
  160. for (const variable of sourceCode.getDeclaredVariables(node)) {
  161. const shouldCheckMembers = variable.defs.some(
  162. d => d.node.type === "ImportNamespaceSpecifier",
  163. );
  164. let prevIdNode = null;
  165. for (const reference of variable.references) {
  166. const idNode = reference.identifier;
  167. /*
  168. * AssignmentPattern (e.g. `[a = 0] = b`) makes two write
  169. * references for the same identifier. This should skip
  170. * the one of the two in order to prevent redundant reports.
  171. */
  172. if (idNode === prevIdNode) {
  173. continue;
  174. }
  175. prevIdNode = idNode;
  176. if (reference.isWrite()) {
  177. context.report({
  178. node: getWriteNode(idNode),
  179. messageId: "readonly",
  180. data: { name: idNode.name },
  181. });
  182. } else if (
  183. shouldCheckMembers &&
  184. isMemberWrite(idNode, scope)
  185. ) {
  186. context.report({
  187. node: getWriteNode(idNode),
  188. messageId: "readonlyMember",
  189. data: { name: idNode.name },
  190. });
  191. }
  192. }
  193. }
  194. },
  195. };
  196. },
  197. };