no-param-reassign.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. /**
  2. * @fileoverview Disallow reassignment of function parameters.
  3. * @author Nat Burns
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Rule Definition
  8. //------------------------------------------------------------------------------
  9. const stopNodePattern = /(?:Statement|Declaration|Function(?:Expression)?|Program)$/u;
  10. /** @type {import('../shared/types').Rule} */
  11. module.exports = {
  12. meta: {
  13. type: "suggestion",
  14. docs: {
  15. description: "Disallow reassigning `function` parameters",
  16. recommended: false,
  17. url: "https://eslint.org/docs/rules/no-param-reassign"
  18. },
  19. schema: [
  20. {
  21. oneOf: [
  22. {
  23. type: "object",
  24. properties: {
  25. props: {
  26. enum: [false]
  27. }
  28. },
  29. additionalProperties: false
  30. },
  31. {
  32. type: "object",
  33. properties: {
  34. props: {
  35. enum: [true]
  36. },
  37. ignorePropertyModificationsFor: {
  38. type: "array",
  39. items: {
  40. type: "string"
  41. },
  42. uniqueItems: true
  43. },
  44. ignorePropertyModificationsForRegex: {
  45. type: "array",
  46. items: {
  47. type: "string"
  48. },
  49. uniqueItems: true
  50. }
  51. },
  52. additionalProperties: false
  53. }
  54. ]
  55. }
  56. ],
  57. messages: {
  58. assignmentToFunctionParam: "Assignment to function parameter '{{name}}'.",
  59. assignmentToFunctionParamProp: "Assignment to property of function parameter '{{name}}'."
  60. }
  61. },
  62. create(context) {
  63. const props = context.options[0] && context.options[0].props;
  64. const ignoredPropertyAssignmentsFor = context.options[0] && context.options[0].ignorePropertyModificationsFor || [];
  65. const ignoredPropertyAssignmentsForRegex = context.options[0] && context.options[0].ignorePropertyModificationsForRegex || [];
  66. /**
  67. * Checks whether or not the reference modifies properties of its variable.
  68. * @param {Reference} reference A reference to check.
  69. * @returns {boolean} Whether or not the reference modifies properties of its variable.
  70. */
  71. function isModifyingProp(reference) {
  72. let node = reference.identifier;
  73. let parent = node.parent;
  74. while (parent && (!stopNodePattern.test(parent.type) ||
  75. parent.type === "ForInStatement" || parent.type === "ForOfStatement")) {
  76. switch (parent.type) {
  77. // e.g. foo.a = 0;
  78. case "AssignmentExpression":
  79. return parent.left === node;
  80. // e.g. ++foo.a;
  81. case "UpdateExpression":
  82. return true;
  83. // e.g. delete foo.a;
  84. case "UnaryExpression":
  85. if (parent.operator === "delete") {
  86. return true;
  87. }
  88. break;
  89. // e.g. for (foo.a in b) {}
  90. case "ForInStatement":
  91. case "ForOfStatement":
  92. if (parent.left === node) {
  93. return true;
  94. }
  95. // this is a stop node for parent.right and parent.body
  96. return false;
  97. // EXCLUDES: e.g. cache.get(foo.a).b = 0;
  98. case "CallExpression":
  99. if (parent.callee !== node) {
  100. return false;
  101. }
  102. break;
  103. // EXCLUDES: e.g. cache[foo.a] = 0;
  104. case "MemberExpression":
  105. if (parent.property === node) {
  106. return false;
  107. }
  108. break;
  109. // EXCLUDES: e.g. ({ [foo]: a }) = bar;
  110. case "Property":
  111. if (parent.key === node) {
  112. return false;
  113. }
  114. break;
  115. // EXCLUDES: e.g. (foo ? a : b).c = bar;
  116. case "ConditionalExpression":
  117. if (parent.test === node) {
  118. return false;
  119. }
  120. break;
  121. // no default
  122. }
  123. node = parent;
  124. parent = node.parent;
  125. }
  126. return false;
  127. }
  128. /**
  129. * Tests that an identifier name matches any of the ignored property assignments.
  130. * First we test strings in ignoredPropertyAssignmentsFor.
  131. * Then we instantiate and test RegExp objects from ignoredPropertyAssignmentsForRegex strings.
  132. * @param {string} identifierName A string that describes the name of an identifier to
  133. * ignore property assignments for.
  134. * @returns {boolean} Whether the string matches an ignored property assignment regular expression or not.
  135. */
  136. function isIgnoredPropertyAssignment(identifierName) {
  137. return ignoredPropertyAssignmentsFor.includes(identifierName) ||
  138. ignoredPropertyAssignmentsForRegex.some(ignored => new RegExp(ignored, "u").test(identifierName));
  139. }
  140. /**
  141. * Reports a reference if is non initializer and writable.
  142. * @param {Reference} reference A reference to check.
  143. * @param {int} index The index of the reference in the references.
  144. * @param {Reference[]} references The array that the reference belongs to.
  145. * @returns {void}
  146. */
  147. function checkReference(reference, index, references) {
  148. const identifier = reference.identifier;
  149. if (identifier &&
  150. !reference.init &&
  151. /*
  152. * Destructuring assignments can have multiple default value,
  153. * so possibly there are multiple writeable references for the same identifier.
  154. */
  155. (index === 0 || references[index - 1].identifier !== identifier)
  156. ) {
  157. if (reference.isWrite()) {
  158. context.report({
  159. node: identifier,
  160. messageId: "assignmentToFunctionParam",
  161. data: { name: identifier.name }
  162. });
  163. } else if (props && isModifyingProp(reference) && !isIgnoredPropertyAssignment(identifier.name)) {
  164. context.report({
  165. node: identifier,
  166. messageId: "assignmentToFunctionParamProp",
  167. data: { name: identifier.name }
  168. });
  169. }
  170. }
  171. }
  172. /**
  173. * Finds and reports references that are non initializer and writable.
  174. * @param {Variable} variable A variable to check.
  175. * @returns {void}
  176. */
  177. function checkVariable(variable) {
  178. if (variable.defs[0].type === "Parameter") {
  179. variable.references.forEach(checkReference);
  180. }
  181. }
  182. /**
  183. * Checks parameters of a given function node.
  184. * @param {ASTNode} node A function node to check.
  185. * @returns {void}
  186. */
  187. function checkForFunction(node) {
  188. context.getDeclaredVariables(node).forEach(checkVariable);
  189. }
  190. return {
  191. // `:exit` is needed for the `node.parent` property of identifier nodes.
  192. "FunctionDeclaration:exit": checkForFunction,
  193. "FunctionExpression:exit": checkForFunction,
  194. "ArrowFunctionExpression:exit": checkForFunction
  195. };
  196. }
  197. };