|
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