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