no-import-assign.js 7.2 KB

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