|
10 | 10 | // Helpers
|
11 | 11 | //------------------------------------------------------------------------------
|
12 | 12 |
|
13 |
| -var LOOP_TYPES = /^(?:While|DoWhile|For|ForIn|ForOf)Statement$/; |
14 |
| -var FOR_IN_OF_TYPES = /^For(?:In|Of)Statement$/; |
15 |
| -var SENTINEL_TYPES = /(?:Declaration|Statement)$/; |
16 |
| -var END_POSITION_TYPES = /^(?:Assignment|Update)/; |
| 13 | +var PATTERN_TYPE = /^(?:.+?Pattern|RestElement|Property)$/; |
17 | 14 |
|
18 | 15 | /**
|
19 |
| - * Gets a write reference from a given variable if the variable is never |
20 |
| - * reassigned. |
| 16 | + * Adds multiple items to the tail of an array. |
21 | 17 | *
|
22 |
| - * @param {escope.Variable} variable - A variable to get. |
23 |
| - * @returns {escope.Reference|null} A write reference or null. |
| 18 | + * @param {any[]} array - A destination to add. |
| 19 | + * @param {any[]} values - Items to be added. |
| 20 | + * @returns {void} |
24 | 21 | */
|
25 |
| -function getWriteReferenceIfOnce(variable) { |
26 |
| - var retv = null; |
27 |
| - |
28 |
| - var references = variable.references; |
29 |
| - for (var i = 0; i < references.length; ++i) { |
30 |
| - var reference = references[i]; |
31 |
| - |
32 |
| - if (reference.isWrite()) { |
33 |
| - if (retv && !(retv.init && reference.init)) { |
34 |
| - // This variable is reassigned. |
35 |
| - return null; |
36 |
| - } |
37 |
| - retv = reference; |
38 |
| - } |
39 |
| - } |
40 |
| - |
41 |
| - return retv; |
42 |
| -} |
| 22 | +var pushAll = Function.apply.bind(Array.prototype.push); |
43 | 23 |
|
44 | 24 | /**
|
45 |
| - * Checks whether or not a given reference is in a loop condition or a |
46 |
| - * for-loop's updater. |
| 25 | + * Checks whether a given node is located at `ForStatement.init` or not. |
47 | 26 | *
|
48 |
| - * @param {escope.Reference} reference - A reference to check. |
49 |
| - * @returns {boolean} `true` if the reference is in a loop condition or a |
50 |
| - * for-loop's updater. |
| 27 | + * @param {ASTNode} node - A node to check. |
| 28 | + * @returns {boolean} `true` if the node is located at `ForStatement.init`. |
51 | 29 | */
|
52 |
| -function isInLoopHead(reference) { |
53 |
| - var node = reference.identifier; |
54 |
| - var parent = node.parent; |
55 |
| - var assignment = false; |
56 |
| - |
57 |
| - while (parent) { |
58 |
| - if (LOOP_TYPES.test(parent.type)) { |
59 |
| - return true; |
60 |
| - } |
61 |
| - |
62 |
| - // VariableDeclaration can be at ForInStatement.left |
63 |
| - // This is catching the code like `for (const {b = ++a} of foo()) { ... }` |
64 |
| - if (assignment && |
65 |
| - parent.type === "VariableDeclaration" && |
66 |
| - FOR_IN_OF_TYPES.test(parent.parent.type) && |
67 |
| - parent.parent.left === parent |
68 |
| - ) { |
69 |
| - return true; |
70 |
| - } |
71 |
| - if (parent.type === "AssignmentPattern") { |
72 |
| - assignment = true; |
73 |
| - } |
74 |
| - |
75 |
| - // If a declaration or a statement was found before a loop, |
76 |
| - // it's not in the head of a loop. |
77 |
| - if (SENTINEL_TYPES.test(parent.type)) { |
78 |
| - break; |
79 |
| - } |
80 |
| - |
81 |
| - node = parent; |
82 |
| - parent = parent.parent; |
83 |
| - } |
84 |
| - |
85 |
| - return false; |
| 30 | +function isInitOfForStatement(node) { |
| 31 | + return node.parent.type === "ForStatement" && node.parent.init === node; |
86 | 32 | }
|
87 | 33 |
|
88 | 34 | /**
|
89 |
| - * Gets the end position of a given reference. |
90 |
| - * This position is used to check every ReadReferences exist after the given |
91 |
| - * reference. |
92 |
| - * |
93 |
| - * If the reference is belonging to AssignmentExpression or UpdateExpression, |
94 |
| - * this function returns the most rear position of those nodes. |
95 |
| - * The range of those nodes are executed before the assignment. |
| 35 | + * Checks whether a given Identifier node becomes a VariableDeclaration or not. |
96 | 36 | *
|
97 |
| - * @param {escope.Reference} writer - A reference to get. |
98 |
| - * @returns {number} The end position of the reference. |
| 37 | + * @param {ASTNode} identifier - An Identifier node to check. |
| 38 | + * @returns {boolean} `true` if the node can become a VariableDeclaration. |
99 | 39 | */
|
100 |
| -function getEndPosition(writer) { |
101 |
| - var node = writer.identifier; |
102 |
| - var end = node.range[1]; |
103 |
| - |
104 |
| - // Detects the end position of the assignment expression the reference is |
105 |
| - // belonging to. |
106 |
| - while ((node = node.parent)) { |
107 |
| - if (END_POSITION_TYPES.test(node.type)) { |
108 |
| - end = node.range[1]; |
109 |
| - } |
110 |
| - if (SENTINEL_TYPES.test(node.type)) { |
111 |
| - break; |
112 |
| - } |
| 40 | +function canBecomeVariableDeclaration(identifier) { |
| 41 | + var node = identifier.parent; |
| 42 | + while (PATTERN_TYPE.test(node.type)) { |
| 43 | + node = node.parent; |
113 | 44 | }
|
114 | 45 |
|
115 |
| - return end; |
| 46 | + return ( |
| 47 | + node.type === "VariableDeclarator" || |
| 48 | + ( |
| 49 | + node.type === "AssignmentExpression" && |
| 50 | + node.parent.type === "ExpressionStatement" |
| 51 | + ) |
| 52 | + ); |
116 | 53 | }
|
117 | 54 |
|
118 | 55 | /**
|
119 |
| - * Gets a function which checks a given reference with the following condition: |
120 |
| - * |
121 |
| - * - The reference is a ReadReference. |
122 |
| - * - The reference exists after a specific WriteReference. |
123 |
| - * - The reference exists inside of the scope a specific WriteReference is |
124 |
| - * belonging to. |
| 56 | + * Gets the WriteReference of a given variable if the variable is never |
| 57 | + * reassigned. |
125 | 58 | *
|
126 |
| - * @param {escope.Reference} writer - A reference to check. |
127 |
| - * @returns {function} A function which checks a given reference. |
| 59 | + * @param {escope.Variable} variable - A variable to get. |
| 60 | + * @returns {escope.Reference|null} The singular WriteReference or null. |
128 | 61 | */
|
129 |
| -function isInScope(writer) { |
130 |
| - var start = getEndPosition(writer); |
131 |
| - var end = writer.from.block.range[1]; |
| 62 | +function getWriteReferenceIfOnce(variable) { |
| 63 | + var retv = null; |
132 | 64 |
|
133 |
| - return function(reference) { |
134 |
| - if (!reference.isRead()) { |
135 |
| - return true; |
| 65 | + var references = variable.references; |
| 66 | + for (var i = 0; i < references.length; ++i) { |
| 67 | + var reference = references[i]; |
| 68 | + |
| 69 | + if (reference.isWrite()) { |
| 70 | + if (retv && !(retv.init && reference.init)) { |
| 71 | + // This variable is reassigned. |
| 72 | + return null; |
| 73 | + } |
| 74 | + retv = reference; |
136 | 75 | }
|
| 76 | + } |
137 | 77 |
|
138 |
| - var range = reference.identifier.range; |
139 |
| - return start <= range[0] && range[1] <= end; |
140 |
| - }; |
| 78 | + return retv; |
141 | 79 | }
|
142 | 80 |
|
143 | 81 | //------------------------------------------------------------------------------
|
144 | 82 | // Rule Definition
|
145 | 83 | //------------------------------------------------------------------------------
|
146 | 84 |
|
147 | 85 | module.exports = function(context) {
|
| 86 | + var variables = null; |
148 | 87 |
|
149 | 88 | /**
|
150 |
| - * Searches and reports variables that are never reassigned after declared. |
151 |
| - * @param {Scope} scope - A scope of the search domain. |
| 89 | + * Reports a given variable if the singular WriteReference of the variable |
| 90 | + * exists in the same scope as the declaration. |
| 91 | + * |
| 92 | + * @param {escope.Variable} variable - A variable to check. |
152 | 93 | * @returns {void}
|
153 | 94 | */
|
154 |
| - function checkForVariables(scope) { |
155 |
| - // Skip the TDZ type. |
156 |
| - if (scope.type === "TDZ") { |
| 95 | + function checkVariable(variable) { |
| 96 | + if (variable.eslintUsed) { |
157 | 97 | return;
|
158 | 98 | }
|
159 | 99 |
|
160 |
| - var variables = scope.variables; |
161 |
| - for (var i = 0; i < variables.length; ++i) { |
162 |
| - var variable = variables[i]; |
163 |
| - var def = variable.defs[0]; |
164 |
| - var declaration = def && def.parent; |
165 |
| - var statement = declaration && declaration.parent; |
166 |
| - var references = variable.references; |
167 |
| - var identifier = variable.identifiers[0]; |
168 |
| - |
169 |
| - // Skips excludes `let`. |
170 |
| - // And skips if it's at `ForStatement.init`. |
171 |
| - if (!declaration || |
172 |
| - declaration.type !== "VariableDeclaration" || |
173 |
| - declaration.kind !== "let" || |
174 |
| - (statement.type === "ForStatement" && statement.init === declaration) |
175 |
| - ) { |
176 |
| - continue; |
177 |
| - } |
178 |
| - |
179 |
| - // Checks references. |
180 |
| - // - One WriteReference exists. |
181 |
| - // - Two or more WriteReference don't exist. |
182 |
| - // - Every ReadReference exists after the WriteReference. |
183 |
| - // - The WriteReference doesn't exist in a loop condition. |
184 |
| - // - If `eslintUsed` is true, we cannot know where it was used from. |
185 |
| - // In this case, if the scope of the variable would change, it |
186 |
| - // skips the variable. |
187 |
| - var writer = getWriteReferenceIfOnce(variable); |
188 |
| - if (writer && |
189 |
| - !(variable.eslintUsed && variable.scope !== writer.from) && |
190 |
| - !isInLoopHead(writer) && |
191 |
| - references.every(isInScope(writer)) |
192 |
| - ) { |
193 |
| - context.report({ |
194 |
| - node: identifier, |
195 |
| - message: "'{{name}}' is never reassigned, use 'const' instead.", |
196 |
| - data: identifier |
197 |
| - }); |
198 |
| - } |
| 100 | + var writer = getWriteReferenceIfOnce(variable); |
| 101 | + if (writer && |
| 102 | + writer.from === variable.scope && |
| 103 | + canBecomeVariableDeclaration(writer.identifier) |
| 104 | + ) { |
| 105 | + context.report({ |
| 106 | + node: writer.identifier, |
| 107 | + message: "'{{name}}' is never reassigned, use 'const' instead.", |
| 108 | + data: variable |
| 109 | + }); |
199 | 110 | }
|
200 | 111 | }
|
201 | 112 |
|
202 |
| - /** |
203 |
| - * Adds multiple items to the tail of an array. |
204 |
| - * @param {any[]} array - A destination to add. |
205 |
| - * @param {any[]} values - Items to be added. |
206 |
| - * @returns {void} |
207 |
| - */ |
208 |
| - var pushAll = Function.apply.bind(Array.prototype.push); |
209 |
| - |
210 | 113 | return {
|
| 114 | + "Program": function() { |
| 115 | + variables = []; |
| 116 | + }, |
| 117 | + |
211 | 118 | "Program:exit": function() {
|
212 |
| - var stack = [context.getScope()]; |
213 |
| - while (stack.length) { |
214 |
| - var scope = stack.pop(); |
215 |
| - pushAll(stack, scope.childScopes); |
| 119 | + variables.forEach(checkVariable); |
| 120 | + variables = null; |
| 121 | + }, |
216 | 122 |
|
217 |
| - checkForVariables(scope); |
| 123 | + "VariableDeclaration": function(node) { |
| 124 | + if (node.kind === "let" && !isInitOfForStatement(node)) { |
| 125 | + pushAll(variables, context.getDeclaredVariables(node)); |
218 | 126 | }
|
219 | 127 | }
|
220 | 128 | };
|
221 |
| - |
222 | 129 | };
|
223 | 130 |
|
224 | 131 | module.exports.schema = [];
|
0 commit comments