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