|
1 | 1 | import type { Reference, Variable, Scope } from '@typescript-eslint/scope-manager';
|
2 | 2 | import type { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/types';
|
3 | 3 | import { createRule } from '../utils';
|
4 |
| -import type { RuleFixer, SourceCode } from '../types'; |
| 4 | +import type { RuleContext, RuleFixer, SourceCode } from '../types'; |
5 | 5 | import { getSourceCode } from '../utils/compat';
|
6 | 6 |
|
7 | 7 | type ASTNode = TSESTree.Node;
|
@@ -112,6 +112,168 @@ class FixTracker {
|
112 | 112 | }
|
113 | 113 | }
|
114 | 114 |
|
| 115 | +type CheckOptions = { destructuring: 'any' | 'all' }; |
| 116 | +type VariableDeclaration = |
| 117 | + | TSESTree.LetOrConstOrVarDeclaration |
| 118 | + | TSESTree.UsingInForOfDeclaration |
| 119 | + | TSESTree.UsingInNormalContextDeclaration; |
| 120 | +type VariableDeclarator = |
| 121 | + | TSESTree.LetOrConstOrVarDeclarator |
| 122 | + | TSESTree.UsingInForOfDeclarator |
| 123 | + | TSESTree.UsingInNomalConextDeclarator; |
| 124 | +class GroupChecker { |
| 125 | + private reportCount = 0; |
| 126 | + |
| 127 | + private checkedId: TSESTree.BindingName | null = null; |
| 128 | + |
| 129 | + private checkedName = ''; |
| 130 | + |
| 131 | + private readonly shouldMatchAnyDestructuredVariable: boolean; |
| 132 | + |
| 133 | + private readonly context: RuleContext; |
| 134 | + |
| 135 | + public constructor(context: RuleContext, { destructuring }: CheckOptions) { |
| 136 | + this.context = context; |
| 137 | + this.shouldMatchAnyDestructuredVariable = destructuring !== 'all'; |
| 138 | + } |
| 139 | + |
| 140 | + public checkAndReportNodes(nodes: ASTNode[]) { |
| 141 | + const shouldCheckGroup = nodes.length && this.shouldMatchAnyDestructuredVariable; |
| 142 | + if (!shouldCheckGroup) { |
| 143 | + return; |
| 144 | + } |
| 145 | + |
| 146 | + const variableDeclarationParent = findUp( |
| 147 | + nodes[0], |
| 148 | + 'VariableDeclaration', |
| 149 | + (parentNode: ASTNode) => parentNode.type.endsWith('Statement') |
| 150 | + ); |
| 151 | + const isVarDecParentNull = variableDeclarationParent === null; |
| 152 | + const isValidDecParent = |
| 153 | + !isVarDecParentNull && |
| 154 | + 'declarations' in variableDeclarationParent && |
| 155 | + variableDeclarationParent.declarations.length > 0; |
| 156 | + if (!isValidDecParent) { |
| 157 | + return; |
| 158 | + } |
| 159 | + |
| 160 | + const dec = variableDeclarationParent.declarations[0]; |
| 161 | + this.checkDeclarator(dec); |
| 162 | + |
| 163 | + const shouldFix = this.checkShouldFix(variableDeclarationParent, nodes.length); |
| 164 | + if (!shouldFix) { |
| 165 | + return; |
| 166 | + } |
| 167 | + |
| 168 | + const sourceCode = getSourceCode(this.context); |
| 169 | + nodes.filter(skipReactiveValues).forEach((node) => { |
| 170 | + this.report(sourceCode, node, variableDeclarationParent); |
| 171 | + }); |
| 172 | + } |
| 173 | + |
| 174 | + private report( |
| 175 | + sourceCode: ReturnType<typeof getSourceCode>, |
| 176 | + node: ASTNode, |
| 177 | + nodeParent: VariableDeclaration |
| 178 | + ) { |
| 179 | + this.context.report({ |
| 180 | + node, |
| 181 | + messageId: 'useConst', |
| 182 | + // @ts-expect-error Name will exist at this point |
| 183 | + data: { name: node.name }, |
| 184 | + fix: (fixer) => { |
| 185 | + const letKeywordToken = sourceCode.getFirstToken(nodeParent, { |
| 186 | + includeComments: false, |
| 187 | + filter: (t) => 'kind' in nodeParent && t.value === nodeParent.kind |
| 188 | + }); |
| 189 | + if (!letKeywordToken) { |
| 190 | + return null; |
| 191 | + } |
| 192 | + |
| 193 | + return new FixTracker(fixer, sourceCode) |
| 194 | + .retainRange(nodeParent.range) |
| 195 | + .replaceTextRange(letKeywordToken.range, 'const'); |
| 196 | + } |
| 197 | + }); |
| 198 | + } |
| 199 | + |
| 200 | + private checkShouldFix(declaration: VariableDeclaration, totalNodes: number) { |
| 201 | + const shouldFix = |
| 202 | + declaration && |
| 203 | + (declaration.parent.type === 'ForInStatement' || |
| 204 | + declaration.parent.type === 'ForOfStatement' || |
| 205 | + ('declarations' in declaration && |
| 206 | + declaration.declarations.every((declaration) => declaration.init))); |
| 207 | + |
| 208 | + const totalDeclarationCount = this.checkDestructuredDeclaration(declaration, totalNodes); |
| 209 | + if (totalDeclarationCount === -1) { |
| 210 | + return shouldFix; |
| 211 | + } |
| 212 | + |
| 213 | + return shouldFix && this.reportCount === totalDeclarationCount; |
| 214 | + } |
| 215 | + |
| 216 | + private checkDestructuredDeclaration(declaration: VariableDeclaration, totalNodes: number) { |
| 217 | + const hasMultipleDeclarations = |
| 218 | + declaration !== null && |
| 219 | + 'declarations' in declaration && |
| 220 | + declaration.declarations.length !== 1; |
| 221 | + if (!hasMultipleDeclarations) { |
| 222 | + return -1; |
| 223 | + } |
| 224 | + |
| 225 | + const hasMoreThanOneDeclaration = |
| 226 | + declaration && declaration.declarations && declaration.declarations.length >= 1; |
| 227 | + if (!hasMoreThanOneDeclaration) { |
| 228 | + return -1; |
| 229 | + } |
| 230 | + |
| 231 | + this.reportCount += totalNodes; |
| 232 | + |
| 233 | + return declaration.declarations.reduce((total, declaration) => { |
| 234 | + if (declaration.id.type === 'ObjectPattern') { |
| 235 | + return total + declaration.id.properties.length; |
| 236 | + } |
| 237 | + |
| 238 | + if (declaration.id.type === 'ArrayPattern') { |
| 239 | + return total + declaration.id.elements.length; |
| 240 | + } |
| 241 | + |
| 242 | + return total + 1; |
| 243 | + }, 0); |
| 244 | + } |
| 245 | + |
| 246 | + private checkDeclarator(declarator: VariableDeclarator) { |
| 247 | + if (!declarator.init) { |
| 248 | + return; |
| 249 | + } |
| 250 | + |
| 251 | + const firstDecParent = declarator.init.parent; |
| 252 | + if (firstDecParent.type !== 'VariableDeclarator') { |
| 253 | + return; |
| 254 | + } |
| 255 | + |
| 256 | + const { id } = firstDecParent; |
| 257 | + if ('name' in id && id.name !== this.checkedName) { |
| 258 | + this.checkedName = id.name; |
| 259 | + this.reportCount = 0; |
| 260 | + } |
| 261 | + |
| 262 | + if (firstDecParent.id.type === 'ObjectPattern') { |
| 263 | + const { init } = firstDecParent; |
| 264 | + if (init && 'name' in init && init.name !== this.checkedName) { |
| 265 | + this.checkedName = init.name; |
| 266 | + this.reportCount = 0; |
| 267 | + } |
| 268 | + } |
| 269 | + |
| 270 | + if (firstDecParent.id !== this.checkedId) { |
| 271 | + this.checkedId = firstDecParent.id; |
| 272 | + this.reportCount = 0; |
| 273 | + } |
| 274 | + } |
| 275 | +} |
| 276 | + |
115 | 277 | export default createRule('prefer-const', {
|
116 | 278 | meta: {
|
117 | 279 | type: 'suggestion',
|
@@ -140,135 +302,18 @@ export default createRule('prefer-const', {
|
140 | 302 | const sourceCode = getSourceCode(context);
|
141 | 303 |
|
142 | 304 | const options = context.options[0] || {};
|
143 |
| - const shouldMatchAnyDestructuredVariable = options.destructuring !== 'all'; |
144 | 305 | const ignoreReadBeforeAssign = options.ignoreReadBeforeAssign === true;
|
145 | 306 |
|
146 | 307 | const variables: Variable[] = [];
|
147 |
| - let reportCount = 0; |
148 |
| - let checkedId: TSESTree.BindingName | null = null; |
149 |
| - let checkedName = ''; |
150 |
| - |
151 |
| - function checkGroup(nodes: (ASTNode | null)[]) { |
152 |
| - const nodesToReport = nodes.filter(Boolean); |
153 |
| - if ( |
154 |
| - nodes.length && |
155 |
| - (shouldMatchAnyDestructuredVariable || nodesToReport.length === nodes.length) && |
156 |
| - nodes[0] !== null |
157 |
| - ) { |
158 |
| - const varDeclParent = findUp(nodes[0], 'VariableDeclaration', (parentNode: ASTNode) => |
159 |
| - parentNode.type.endsWith('Statement') |
160 |
| - ); |
161 |
| - |
162 |
| - const isVarDecParentNull = varDeclParent === null; |
163 |
| - const isValidDecParent = |
164 |
| - !isVarDecParentNull && |
165 |
| - 'declarations' in varDeclParent && |
166 |
| - varDeclParent.declarations.length > 0; |
167 |
| - |
168 |
| - if (isValidDecParent) { |
169 |
| - const firstDeclaration = varDeclParent.declarations[0]; |
170 |
| - |
171 |
| - if (firstDeclaration.init) { |
172 |
| - const firstDecParent = firstDeclaration.init.parent; |
173 |
| - |
174 |
| - if (firstDecParent.type === 'VariableDeclarator') { |
175 |
| - const { id } = firstDecParent; |
176 |
| - if ('name' in id && id.name !== checkedName) { |
177 |
| - checkedName = id.name; |
178 |
| - reportCount = 0; |
179 |
| - } |
180 |
| - |
181 |
| - if (firstDecParent.id.type === 'ObjectPattern') { |
182 |
| - const { init } = firstDecParent; |
183 |
| - if (init && 'name' in init && init.name !== checkedName) { |
184 |
| - checkedName = init.name; |
185 |
| - reportCount = 0; |
186 |
| - } |
187 |
| - } |
188 |
| - |
189 |
| - if (firstDecParent.id !== checkedId) { |
190 |
| - checkedId = firstDecParent.id; |
191 |
| - reportCount = 0; |
192 |
| - } |
193 |
| - } |
194 |
| - } |
195 |
| - } |
196 |
| - |
197 |
| - let shouldFix = |
198 |
| - varDeclParent && |
199 |
| - (varDeclParent.parent.type === 'ForInStatement' || |
200 |
| - varDeclParent.parent.type === 'ForOfStatement' || |
201 |
| - ('declarations' in varDeclParent && |
202 |
| - varDeclParent.declarations.every((declaration) => declaration.init))) && |
203 |
| - nodesToReport.length === nodes.length; |
204 |
| - |
205 |
| - if ( |
206 |
| - !isVarDecParentNull && |
207 |
| - 'declarations' in varDeclParent && |
208 |
| - varDeclParent.declarations && |
209 |
| - varDeclParent.declarations.length !== 1 |
210 |
| - ) { |
211 |
| - if ( |
212 |
| - varDeclParent && |
213 |
| - varDeclParent.declarations && |
214 |
| - varDeclParent.declarations.length >= 1 |
215 |
| - ) { |
216 |
| - reportCount += nodesToReport.length; |
217 |
| - let totalDeclarationsCount = 0; |
218 |
| - |
219 |
| - varDeclParent.declarations.forEach((declaration) => { |
220 |
| - if (declaration.id.type === 'ObjectPattern') { |
221 |
| - totalDeclarationsCount += declaration.id.properties.length; |
222 |
| - return; |
223 |
| - } |
224 |
| - |
225 |
| - if (declaration.id.type === 'ArrayPattern') { |
226 |
| - totalDeclarationsCount += declaration.id.elements.length; |
227 |
| - return; |
228 |
| - } |
229 |
| - |
230 |
| - totalDeclarationsCount += 1; |
231 |
| - }); |
232 |
| - shouldFix = shouldFix && reportCount === totalDeclarationsCount; |
233 |
| - } |
234 |
| - } |
235 |
| - |
236 |
| - if (!shouldFix) { |
237 |
| - return; |
238 |
| - } |
239 |
| - |
240 |
| - nodesToReport.filter(skipReactiveValues).forEach((node) => { |
241 |
| - if (!node || !varDeclParent) { |
242 |
| - // TS check |
243 |
| - return; |
244 |
| - } |
245 |
| - |
246 |
| - context.report({ |
247 |
| - node, |
248 |
| - messageId: 'useConst', |
249 |
| - // @ts-expect-error Name will exist at this point |
250 |
| - data: { name: node.name }, |
251 |
| - fix: (fixer) => { |
252 |
| - const letKeywordToken = sourceCode.getFirstToken(varDeclParent, { |
253 |
| - includeComments: false, |
254 |
| - filter: (t) => 'kind' in varDeclParent && t.value === varDeclParent.kind |
255 |
| - }); |
256 |
| - if (!letKeywordToken) { |
257 |
| - return null; |
258 |
| - } |
259 |
| - |
260 |
| - return new FixTracker(fixer, sourceCode) |
261 |
| - .retainRange(varDeclParent.range) |
262 |
| - .replaceTextRange(letKeywordToken.range, 'const'); |
263 |
| - } |
264 |
| - }); |
265 |
| - }); |
266 |
| - } |
267 |
| - } |
268 | 308 |
|
269 | 309 | return {
|
270 | 310 | 'Program:exit'() {
|
271 |
| - groupByDestructuring(variables, ignoreReadBeforeAssign).forEach(checkGroup); |
| 311 | + const checker = new GroupChecker(context, { |
| 312 | + destructuring: options.destructuring |
| 313 | + }); |
| 314 | + groupByDestructuring(variables, ignoreReadBeforeAssign).forEach((group) => { |
| 315 | + checker.checkAndReportNodes(group); |
| 316 | + }); |
272 | 317 | },
|
273 | 318 | VariableDeclaration(node) {
|
274 | 319 | if (node.kind === 'let' && !isInitOfForStatement(node)) {
|
@@ -419,6 +464,7 @@ function getIdentifierIfShouldBeConst(variable: Variable, ignoreReadBeforeAssign
|
419 | 464 | if (variable.eslintUsed && variable.scope.type === 'global') {
|
420 | 465 | return null;
|
421 | 466 | }
|
| 467 | + |
422 | 468 | let writer = null;
|
423 | 469 | let isReadBeforeInit = false;
|
424 | 470 | const references = variable.references;
|
@@ -487,7 +533,7 @@ function groupByDestructuring(
|
487 | 533 | const references = variable.references;
|
488 | 534 | const identifier = getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign);
|
489 | 535 | if (!identifier) {
|
490 |
| - return identifierMap; |
| 536 | + continue; |
491 | 537 | }
|
492 | 538 |
|
493 | 539 | let prevId: TSESTree.Identifier | TSESTree.JSXIdentifier | null = null;
|
|
0 commit comments