• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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