no-param-reassign.js 6.2 KB

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