/** * @fileoverview Disallow reassignment of function parameters. * @author Nat Burns * @copyright 2014 Nat Burns. All rights reserved. */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ var stopNodePattern = /(?:Statement|Declaration|Function(?:Expression)?|Program)$/; module.exports = function(context) { var props = context.options[0] && Boolean(context.options[0].props); /** * Checks whether or not a reference modifies its variable. * If the `props` option is `true`, this checks whether or not the reference modifies properties of its variable also. * @param {Reference} reference - A reference to check. * @returns {boolean} Whether or not the reference modifies its variable. */ function isModifying(reference) { if (reference.isWrite()) { return true; } // Checks whether its property is modified. if (props) { var node = reference.identifier; var parent = node.parent; while (parent && !stopNodePattern.test(parent.type)) { switch (parent.type) { // e.g. foo.a = 0; case "AssignmentExpression": return parent.left === node; // e.g. ++foo.a; case "UpdateExpression": return true; // e.g. delete foo.a; case "UnaryExpression": if (parent.operator === "delete") { return true; } break; // EXCLUDES: e.g. cache.get(foo.a).b = 0; case "CallExpression": if (parent.callee !== node) { return false; } break; // EXCLUDES: e.g. cache[foo.a] = 0; case "MemberExpression": if (parent.property === node) { return false; } break; default: break; } node = parent; parent = parent.parent; } } return false; } /** * Reports a reference if is non initializer and writable. * @param {Reference} reference - A reference to check. * @param {int} index - The index of the reference in the references. * @param {Reference[]} references - The array that the reference belongs to. * @returns {void} */ function checkReference(reference, index, references) { var identifier = reference.identifier; if (identifier && !reference.init && isModifying(reference) && // Destructuring assignments can have multiple default value, // so possibly there are multiple writeable references for the same identifier. (index === 0 || references[index - 1].identifier !== identifier) ) { context.report( identifier, "Assignment to function parameter '{{name}}'.", {name: identifier.name}); } } /** * Finds and reports references that are non initializer and writable. * @param {Variable} variable - A variable to check. * @returns {void} */ function checkVariable(variable) { if (variable.defs[0].type === "Parameter") { variable.references.forEach(checkReference); } } /** * Checks parameters of a given function node. * @param {ASTNode} node - A function node to check. * @returns {void} */ function checkForFunction(node) { context.getDeclaredVariables(node).forEach(checkVariable); } return { // `:exit` is needed for the `node.parent` property of identifier nodes. "FunctionDeclaration:exit": checkForFunction, "FunctionExpression:exit": checkForFunction, "ArrowFunctionExpression:exit": checkForFunction }; }; module.exports.schema = [ { "type": "object", "properties": { "props": {"type": "boolean"} }, "additionalProperties": false } ];