no-import-assign.js 6.9 KB

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