.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / rules / prefer-const.js
1 /**
2  * @fileoverview A rule to suggest using of const declaration for variables that are never reassigned after declared.
3  * @author Toru Nagashima
4  */
5
6 "use strict";
7
8 //------------------------------------------------------------------------------
9 // Requirements
10 //------------------------------------------------------------------------------
11
12 const FixTracker = require("./utils/fix-tracker");
13 const astUtils = require("./utils/ast-utils");
14
15 //------------------------------------------------------------------------------
16 // Helpers
17 //------------------------------------------------------------------------------
18
19 const PATTERN_TYPE = /^(?:.+?Pattern|RestElement|SpreadProperty|ExperimentalRestProperty|Property)$/u;
20 const DECLARATION_HOST_TYPE = /^(?:Program|BlockStatement|SwitchCase)$/u;
21 const DESTRUCTURING_HOST_TYPE = /^(?:VariableDeclarator|AssignmentExpression)$/u;
22
23 /**
24  * Checks whether a given node is located at `ForStatement.init` or not.
25  * @param {ASTNode} node A node to check.
26  * @returns {boolean} `true` if the node is located at `ForStatement.init`.
27  */
28 function isInitOfForStatement(node) {
29     return node.parent.type === "ForStatement" && node.parent.init === node;
30 }
31
32 /**
33  * Checks whether a given Identifier node becomes a VariableDeclaration or not.
34  * @param {ASTNode} identifier An Identifier node to check.
35  * @returns {boolean} `true` if the node can become a VariableDeclaration.
36  */
37 function canBecomeVariableDeclaration(identifier) {
38     let node = identifier.parent;
39
40     while (PATTERN_TYPE.test(node.type)) {
41         node = node.parent;
42     }
43
44     return (
45         node.type === "VariableDeclarator" ||
46         (
47             node.type === "AssignmentExpression" &&
48             node.parent.type === "ExpressionStatement" &&
49             DECLARATION_HOST_TYPE.test(node.parent.parent.type)
50         )
51     );
52 }
53
54 /**
55  * Checks if an property or element is from outer scope or function parameters
56  * in destructing pattern.
57  * @param {string} name A variable name to be checked.
58  * @param {eslint-scope.Scope} initScope A scope to start find.
59  * @returns {boolean} Indicates if the variable is from outer scope or function parameters.
60  */
61 function isOuterVariableInDestructing(name, initScope) {
62
63     if (initScope.through.find(ref => ref.resolved && ref.resolved.name === name)) {
64         return true;
65     }
66
67     const variable = astUtils.getVariableByName(initScope, name);
68
69     if (variable !== null) {
70         return variable.defs.some(def => def.type === "Parameter");
71     }
72
73     return false;
74 }
75
76 /**
77  * Gets the VariableDeclarator/AssignmentExpression node that a given reference
78  * belongs to.
79  * This is used to detect a mix of reassigned and never reassigned in a
80  * destructuring.
81  * @param {eslint-scope.Reference} reference A reference to get.
82  * @returns {ASTNode|null} A VariableDeclarator/AssignmentExpression node or
83  *      null.
84  */
85 function getDestructuringHost(reference) {
86     if (!reference.isWrite()) {
87         return null;
88     }
89     let node = reference.identifier.parent;
90
91     while (PATTERN_TYPE.test(node.type)) {
92         node = node.parent;
93     }
94
95     if (!DESTRUCTURING_HOST_TYPE.test(node.type)) {
96         return null;
97     }
98     return node;
99 }
100
101 /**
102  * Determines if a destructuring assignment node contains
103  * any MemberExpression nodes. This is used to determine if a
104  * variable that is only written once using destructuring can be
105  * safely converted into a const declaration.
106  * @param {ASTNode} node The ObjectPattern or ArrayPattern node to check.
107  * @returns {boolean} True if the destructuring pattern contains
108  *      a MemberExpression, false if not.
109  */
110 function hasMemberExpressionAssignment(node) {
111     switch (node.type) {
112         case "ObjectPattern":
113             return node.properties.some(prop => {
114                 if (prop) {
115
116                     /*
117                      * Spread elements have an argument property while
118                      * others have a value property. Because different
119                      * parsers use different node types for spread elements,
120                      * we just check if there is an argument property.
121                      */
122                     return hasMemberExpressionAssignment(prop.argument || prop.value);
123                 }
124
125                 return false;
126             });
127
128         case "ArrayPattern":
129             return node.elements.some(element => {
130                 if (element) {
131                     return hasMemberExpressionAssignment(element);
132                 }
133
134                 return false;
135             });
136
137         case "AssignmentPattern":
138             return hasMemberExpressionAssignment(node.left);
139
140         case "MemberExpression":
141             return true;
142
143         // no default
144     }
145
146     return false;
147 }
148
149 /**
150  * Gets an identifier node of a given variable.
151  *
152  * If the initialization exists or one or more reading references exist before
153  * the first assignment, the identifier node is the node of the declaration.
154  * Otherwise, the identifier node is the node of the first assignment.
155  *
156  * If the variable should not change to const, this function returns null.
157  * - If the variable is reassigned.
158  * - If the variable is never initialized nor assigned.
159  * - If the variable is initialized in a different scope from the declaration.
160  * - If the unique assignment of the variable cannot change to a declaration.
161  *   e.g. `if (a) b = 1` / `return (b = 1)`
162  * - If the variable is declared in the global scope and `eslintUsed` is `true`.
163  *   `/*exported foo` directive comment makes such variables. This rule does not
164  *   warn such variables because this rule cannot distinguish whether the
165  *   exported variables are reassigned or not.
166  * @param {eslint-scope.Variable} variable A variable to get.
167  * @param {boolean} ignoreReadBeforeAssign
168  *      The value of `ignoreReadBeforeAssign` option.
169  * @returns {ASTNode|null}
170  *      An Identifier node if the variable should change to const.
171  *      Otherwise, null.
172  */
173 function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) {
174     if (variable.eslintUsed && variable.scope.type === "global") {
175         return null;
176     }
177
178     // Finds the unique WriteReference.
179     let writer = null;
180     let isReadBeforeInit = false;
181     const references = variable.references;
182
183     for (let i = 0; i < references.length; ++i) {
184         const reference = references[i];
185
186         if (reference.isWrite()) {
187             const isReassigned = (
188                 writer !== null &&
189                 writer.identifier !== reference.identifier
190             );
191
192             if (isReassigned) {
193                 return null;
194             }
195
196             const destructuringHost = getDestructuringHost(reference);
197
198             if (destructuringHost !== null && destructuringHost.left !== void 0) {
199                 const leftNode = destructuringHost.left;
200                 let hasOuterVariables = false,
201                     hasNonIdentifiers = false;
202
203                 if (leftNode.type === "ObjectPattern") {
204                     const properties = leftNode.properties;
205
206                     hasOuterVariables = properties
207                         .filter(prop => prop.value)
208                         .map(prop => prop.value.name)
209                         .some(name => isOuterVariableInDestructing(name, variable.scope));
210
211                     hasNonIdentifiers = hasMemberExpressionAssignment(leftNode);
212
213                 } else if (leftNode.type === "ArrayPattern") {
214                     const elements = leftNode.elements;
215
216                     hasOuterVariables = elements
217                         .map(element => element && element.name)
218                         .some(name => isOuterVariableInDestructing(name, variable.scope));
219
220                     hasNonIdentifiers = hasMemberExpressionAssignment(leftNode);
221                 }
222
223                 if (hasOuterVariables || hasNonIdentifiers) {
224                     return null;
225                 }
226
227             }
228
229             writer = reference;
230
231         } else if (reference.isRead() && writer === null) {
232             if (ignoreReadBeforeAssign) {
233                 return null;
234             }
235             isReadBeforeInit = true;
236         }
237     }
238
239     /*
240      * If the assignment is from a different scope, ignore it.
241      * If the assignment cannot change to a declaration, ignore it.
242      */
243     const shouldBeConst = (
244         writer !== null &&
245         writer.from === variable.scope &&
246         canBecomeVariableDeclaration(writer.identifier)
247     );
248
249     if (!shouldBeConst) {
250         return null;
251     }
252
253     if (isReadBeforeInit) {
254         return variable.defs[0].name;
255     }
256
257     return writer.identifier;
258 }
259
260 /**
261  * Groups by the VariableDeclarator/AssignmentExpression node that each
262  * reference of given variables belongs to.
263  * This is used to detect a mix of reassigned and never reassigned in a
264  * destructuring.
265  * @param {eslint-scope.Variable[]} variables Variables to group by destructuring.
266  * @param {boolean} ignoreReadBeforeAssign
267  *      The value of `ignoreReadBeforeAssign` option.
268  * @returns {Map<ASTNode, ASTNode[]>} Grouped identifier nodes.
269  */
270 function groupByDestructuring(variables, ignoreReadBeforeAssign) {
271     const identifierMap = new Map();
272
273     for (let i = 0; i < variables.length; ++i) {
274         const variable = variables[i];
275         const references = variable.references;
276         const identifier = getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign);
277         let prevId = null;
278
279         for (let j = 0; j < references.length; ++j) {
280             const reference = references[j];
281             const id = reference.identifier;
282
283             /*
284              * Avoid counting a reference twice or more for default values of
285              * destructuring.
286              */
287             if (id === prevId) {
288                 continue;
289             }
290             prevId = id;
291
292             // Add the identifier node into the destructuring group.
293             const group = getDestructuringHost(reference);
294
295             if (group) {
296                 if (identifierMap.has(group)) {
297                     identifierMap.get(group).push(identifier);
298                 } else {
299                     identifierMap.set(group, [identifier]);
300                 }
301             }
302         }
303     }
304
305     return identifierMap;
306 }
307
308 /**
309  * Finds the nearest parent of node with a given type.
310  * @param {ASTNode} node The node to search from.
311  * @param {string} type The type field of the parent node.
312  * @param {Function} shouldStop A predicate that returns true if the traversal should stop, and false otherwise.
313  * @returns {ASTNode} The closest ancestor with the specified type; null if no such ancestor exists.
314  */
315 function findUp(node, type, shouldStop) {
316     if (!node || shouldStop(node)) {
317         return null;
318     }
319     if (node.type === type) {
320         return node;
321     }
322     return findUp(node.parent, type, shouldStop);
323 }
324
325 //------------------------------------------------------------------------------
326 // Rule Definition
327 //------------------------------------------------------------------------------
328
329 module.exports = {
330     meta: {
331         type: "suggestion",
332
333         docs: {
334             description: "require `const` declarations for variables that are never reassigned after declared",
335             category: "ECMAScript 6",
336             recommended: false,
337             url: "https://eslint.org/docs/rules/prefer-const"
338         },
339
340         fixable: "code",
341
342         schema: [
343             {
344                 type: "object",
345                 properties: {
346                     destructuring: { enum: ["any", "all"], default: "any" },
347                     ignoreReadBeforeAssign: { type: "boolean", default: false }
348                 },
349                 additionalProperties: false
350             }
351         ],
352         messages: {
353             useConst: "'{{name}}' is never reassigned. Use 'const' instead."
354         }
355     },
356
357     create(context) {
358         const options = context.options[0] || {};
359         const sourceCode = context.getSourceCode();
360         const shouldMatchAnyDestructuredVariable = options.destructuring !== "all";
361         const ignoreReadBeforeAssign = options.ignoreReadBeforeAssign === true;
362         const variables = [];
363         let reportCount = 0;
364         let checkedId = null;
365         let checkedName = "";
366
367
368         /**
369          * Reports given identifier nodes if all of the nodes should be declared
370          * as const.
371          *
372          * The argument 'nodes' is an array of Identifier nodes.
373          * This node is the result of 'getIdentifierIfShouldBeConst()', so it's
374          * nullable. In simple declaration or assignment cases, the length of
375          * the array is 1. In destructuring cases, the length of the array can
376          * be 2 or more.
377          * @param {(eslint-scope.Reference|null)[]} nodes
378          *      References which are grouped by destructuring to report.
379          * @returns {void}
380          */
381         function checkGroup(nodes) {
382             const nodesToReport = nodes.filter(Boolean);
383
384             if (nodes.length && (shouldMatchAnyDestructuredVariable || nodesToReport.length === nodes.length)) {
385                 const varDeclParent = findUp(nodes[0], "VariableDeclaration", parentNode => parentNode.type.endsWith("Statement"));
386                 const isVarDecParentNull = varDeclParent === null;
387
388                 if (!isVarDecParentNull && varDeclParent.declarations.length > 0) {
389                     const firstDeclaration = varDeclParent.declarations[0];
390
391                     if (firstDeclaration.init) {
392                         const firstDecParent = firstDeclaration.init.parent;
393
394                         /*
395                          * First we check the declaration type and then depending on
396                          * if the type is a "VariableDeclarator" or its an "ObjectPattern"
397                          * we compare the name and id from the first identifier, if the names are different
398                          * we assign the new name, id and reset the count of reportCount and nodeCount in
399                          * order to check each block for the number of reported errors and base our fix
400                          * based on comparing nodes.length and nodesToReport.length.
401                          */
402
403                         if (firstDecParent.type === "VariableDeclarator") {
404
405                             if (firstDecParent.id.name !== checkedName) {
406                                 checkedName = firstDecParent.id.name;
407                                 reportCount = 0;
408                             }
409
410                             if (firstDecParent.id.type === "ObjectPattern") {
411                                 if (firstDecParent.init.name !== checkedName) {
412                                     checkedName = firstDecParent.init.name;
413                                     reportCount = 0;
414                                 }
415                             }
416
417                             if (firstDecParent.id !== checkedId) {
418                                 checkedId = firstDecParent.id;
419                                 reportCount = 0;
420                             }
421                         }
422                     }
423                 }
424
425                 let shouldFix = varDeclParent &&
426
427                     // Don't do a fix unless all variables in the declarations are initialized (or it's in a for-in or for-of loop)
428                     (varDeclParent.parent.type === "ForInStatement" || varDeclParent.parent.type === "ForOfStatement" ||
429                         varDeclParent.declarations.every(declaration => declaration.init)) &&
430
431                     /*
432                      * If options.destructuring is "all", then this warning will not occur unless
433                      * every assignment in the destructuring should be const. In that case, it's safe
434                      * to apply the fix.
435                      */
436                     nodesToReport.length === nodes.length;
437
438                 if (!isVarDecParentNull && varDeclParent.declarations && varDeclParent.declarations.length !== 1) {
439
440                     if (varDeclParent && varDeclParent.declarations && varDeclParent.declarations.length >= 1) {
441
442                         /*
443                          * Add nodesToReport.length to a count, then comparing the count to the length
444                          * of the declarations in the current block.
445                          */
446
447                         reportCount += nodesToReport.length;
448
449                         shouldFix = shouldFix && (reportCount === varDeclParent.declarations.length);
450                     }
451                 }
452
453                 nodesToReport.forEach(node => {
454                     context.report({
455                         node,
456                         messageId: "useConst",
457                         data: node,
458                         fix: shouldFix
459                             ? fixer => {
460                                 const letKeywordToken = sourceCode.getFirstToken(varDeclParent, t => t.value === varDeclParent.kind);
461
462                                 /**
463                                  * Extend the replacement range to the whole declaration,
464                                  * in order to prevent other fixes in the same pass
465                                  * https://github.com/eslint/eslint/issues/13899
466                                  */
467                                 return new FixTracker(fixer, sourceCode)
468                                     .retainRange(varDeclParent.range)
469                                     .replaceTextRange(letKeywordToken.range, "const");
470                             }
471                             : null
472                     });
473                 });
474             }
475         }
476
477         return {
478             "Program:exit"() {
479                 groupByDestructuring(variables, ignoreReadBeforeAssign).forEach(checkGroup);
480             },
481
482             VariableDeclaration(node) {
483                 if (node.kind === "let" && !isInitOfForStatement(node)) {
484                     variables.push(...context.getDeclaredVariables(node));
485                 }
486             }
487         };
488     }
489 };