1/* 2 * Copyright (c) 2023-2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import * as ts from 'typescript'; 17import { TsUtils } from '../utils/TsUtils'; 18import { scopeContainsThis } from '../utils/functions/ContainsThis'; 19import { forEachNodeInSubtree } from '../utils/functions/ForEachNodeInSubtree'; 20import { NameGenerator } from '../utils/functions/NameGenerator'; 21import { isAssignmentOperator } from '../utils/functions/isAssignmentOperator'; 22import { SymbolCache } from './SymbolCache'; 23 24const GENERATED_OBJECT_LITERAL_INTERFACE_NAME = 'GeneratedObjectLiteralInterface_'; 25const GENERATED_OBJECT_LITERAL_INTERFACE_TRESHOLD = 1000; 26 27const GENERATED_TYPE_LITERAL_INTERFACE_NAME = 'GeneratedTypeLiteralInterface_'; 28const GENERATED_TYPE_LITERAL_INTERFACE_TRESHOLD = 1000; 29 30export interface Autofix { 31 replacementText: string; 32 start: number; 33 end: number; 34} 35 36export class Autofixer { 37 constructor( 38 private readonly typeChecker: ts.TypeChecker, 39 private readonly utils: TsUtils, 40 readonly sourceFile: ts.SourceFile, 41 readonly cancellationToken?: ts.CancellationToken 42 ) { 43 this.symbolCache = new SymbolCache(this.typeChecker, this.utils, sourceFile, cancellationToken); 44 } 45 46 fixLiteralAsPropertyNamePropertyAssignment(node: ts.PropertyAssignment): Autofix[] | undefined { 47 const contextualType = this.typeChecker.getContextualType(node.parent); 48 if (contextualType === undefined) { 49 return undefined; 50 } 51 52 const symbol = this.utils.getPropertySymbol(contextualType, node); 53 if (symbol === undefined) { 54 return undefined; 55 } 56 57 return this.renameSymbolAsIdentifier(symbol); 58 } 59 60 fixLiteralAsPropertyNamePropertyName(node: ts.PropertyName): Autofix[] | undefined { 61 const symbol = this.typeChecker.getSymbolAtLocation(node); 62 if (symbol === undefined) { 63 return undefined; 64 } 65 66 return this.renameSymbolAsIdentifier(symbol); 67 } 68 69 fixPropertyAccessByIndex(node: ts.ElementAccessExpression): Autofix[] | undefined { 70 const symbol = this.typeChecker.getSymbolAtLocation(node.argumentExpression); 71 if (symbol === undefined) { 72 return undefined; 73 } 74 75 return this.renameSymbolAsIdentifier(symbol); 76 } 77 78 private renameSymbolAsIdentifier(symbol: ts.Symbol): Autofix[] | undefined { 79 if (this.renameSymbolAsIdentifierCache.has(symbol)) { 80 return this.renameSymbolAsIdentifierCache.get(symbol); 81 } 82 83 if (!TsUtils.isPropertyOfInternalClassOrInterface(symbol)) { 84 this.renameSymbolAsIdentifierCache.set(symbol, undefined); 85 return undefined; 86 } 87 88 const newName = this.utils.findIdentifierNameForSymbol(symbol); 89 if (newName === undefined) { 90 this.renameSymbolAsIdentifierCache.set(symbol, undefined); 91 return undefined; 92 } 93 94 let result: Autofix[] | undefined = []; 95 this.symbolCache.getReferences(symbol).forEach((node) => { 96 if (result === undefined) { 97 return; 98 } 99 100 let autofix: Autofix[] | undefined; 101 if (ts.isPropertyDeclaration(node) || ts.isPropertyAssignment(node) || ts.isPropertySignature(node)) { 102 autofix = Autofixer.renamePropertyName(node.name, newName); 103 } else if (ts.isElementAccessExpression(node)) { 104 autofix = Autofixer.renameElementAccessExpression(node, newName); 105 } 106 107 if (autofix === undefined) { 108 result = undefined; 109 return; 110 } 111 112 result.push(...autofix); 113 }); 114 if (!result?.length) { 115 result = undefined; 116 } 117 118 this.renameSymbolAsIdentifierCache.set(symbol, result); 119 return result; 120 } 121 122 private readonly renameSymbolAsIdentifierCache = new Map<ts.Symbol, Autofix[] | undefined>(); 123 124 private static renamePropertyName(node: ts.PropertyName, newName: string): Autofix[] | undefined { 125 if (ts.isComputedPropertyName(node)) { 126 return undefined; 127 } 128 129 if (ts.isMemberName(node)) { 130 if (ts.idText(node) !== newName) { 131 return undefined; 132 } 133 134 return []; 135 } 136 137 return [{ replacementText: newName, start: node.getStart(), end: node.getEnd() }]; 138 } 139 140 private static renameElementAccessExpression( 141 node: ts.ElementAccessExpression, 142 newName: string 143 ): Autofix[] | undefined { 144 const argExprKind = node.argumentExpression.kind; 145 if (argExprKind !== ts.SyntaxKind.NumericLiteral && argExprKind !== ts.SyntaxKind.StringLiteral) { 146 return undefined; 147 } 148 149 return [ 150 { 151 replacementText: node.expression.getText() + '.' + newName, 152 start: node.getStart(), 153 end: node.getEnd() 154 } 155 ]; 156 } 157 158 fixFunctionExpression( 159 funcExpr: ts.FunctionExpression, 160 // eslint-disable-next-line default-param-last 161 retType: ts.TypeNode | undefined = funcExpr.type, 162 modifiers: readonly ts.Modifier[] | undefined, 163 isGenerator: boolean, 164 hasUnfixableReturnType: boolean 165 ): Autofix[] | undefined { 166 const hasThisKeyword = scopeContainsThis(funcExpr.body); 167 const isCalledRecursively = this.utils.isFunctionCalledRecursively(funcExpr); 168 if (isGenerator || hasThisKeyword || isCalledRecursively || hasUnfixableReturnType) { 169 return undefined; 170 } 171 172 let arrowFunc: ts.Expression = ts.factory.createArrowFunction( 173 modifiers, 174 funcExpr.typeParameters, 175 funcExpr.parameters, 176 retType, 177 ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 178 funcExpr.body 179 ); 180 if (Autofixer.needsParentheses(funcExpr)) { 181 arrowFunc = ts.factory.createParenthesizedExpression(arrowFunc); 182 } 183 const text = this.printer.printNode(ts.EmitHint.Unspecified, arrowFunc, funcExpr.getSourceFile()); 184 return [{ start: funcExpr.getStart(), end: funcExpr.getEnd(), replacementText: text }]; 185 } 186 187 private static isNodeInWhileOrIf(node: ts.Node): boolean { 188 return ( 189 node.kind === ts.SyntaxKind.WhileStatement || 190 node.kind === ts.SyntaxKind.DoStatement || 191 node.kind === ts.SyntaxKind.IfStatement 192 ); 193 } 194 195 private static isNodeInForLoop(node: ts.Node): boolean { 196 return ( 197 node.kind === ts.SyntaxKind.ForInStatement || 198 node.kind === ts.SyntaxKind.ForOfStatement || 199 node.kind === ts.SyntaxKind.ForStatement 200 ); 201 } 202 203 private static parentInFor(node: ts.Node): ts.Node | undefined { 204 let parentNode = node.parent; 205 while (parentNode) { 206 if (Autofixer.isNodeInForLoop(parentNode)) { 207 return parentNode; 208 } 209 parentNode = parentNode.parent; 210 } 211 return undefined; 212 } 213 214 private static parentInCaseOrWhile(varDeclList: ts.VariableDeclarationList): boolean { 215 let parentNode: ts.Node = varDeclList.parent; 216 while (parentNode) { 217 if (parentNode.kind === ts.SyntaxKind.CaseClause || Autofixer.isNodeInWhileOrIf(parentNode)) { 218 return false; 219 } 220 parentNode = parentNode.parent; 221 } 222 return true; 223 } 224 225 private static isFunctionLikeDeclarationKind(node: ts.Node): boolean { 226 switch (node.kind) { 227 case ts.SyntaxKind.FunctionDeclaration: 228 case ts.SyntaxKind.MethodDeclaration: 229 case ts.SyntaxKind.Constructor: 230 case ts.SyntaxKind.GetAccessor: 231 case ts.SyntaxKind.SetAccessor: 232 case ts.SyntaxKind.FunctionExpression: 233 case ts.SyntaxKind.ArrowFunction: 234 return true; 235 default: 236 return false; 237 } 238 } 239 240 private static findVarScope(node: ts.Node): ts.Node { 241 while (node !== undefined) { 242 if (node.kind === ts.SyntaxKind.Block || node.kind === ts.SyntaxKind.SourceFile) { 243 break; 244 } 245 // eslint-disable-next-line no-param-reassign 246 node = node.parent; 247 } 248 return node; 249 } 250 251 private static varHasScope(node: ts.Node, scope: ts.Node): boolean { 252 while (node !== undefined) { 253 if (node === scope) { 254 return true; 255 } 256 // eslint-disable-next-line no-param-reassign 257 node = node.parent; 258 } 259 return false; 260 } 261 262 private static varInFunctionForScope(node: ts.Node, scope: ts.Node): boolean { 263 while (node !== undefined) { 264 if (Autofixer.isFunctionLikeDeclarationKind(node)) { 265 break; 266 } 267 // eslint-disable-next-line no-param-reassign 268 node = node.parent; 269 } 270 // node now Function like declaration 271 272 // node need to check that function like declaration is in scope 273 if (Autofixer.varHasScope(node, scope)) { 274 // var use is in function scope, which is in for scope 275 return true; 276 } 277 return false; 278 } 279 280 private static selfDeclared(decl: ts.Node, ident: ts.Node): boolean { 281 // Do not check the same node 282 if (ident === decl) { 283 return false; 284 } 285 286 while (ident !== undefined) { 287 if (ident.kind === ts.SyntaxKind.VariableDeclaration) { 288 const declName = (ident as ts.VariableDeclaration).name; 289 if (declName === decl) { 290 return true; 291 } 292 } 293 // eslint-disable-next-line no-param-reassign 294 ident = ident.parent; 295 } 296 return false; 297 } 298 299 private static analizeTDZ(decl: ts.VariableDeclaration, identifiers: ts.Node[]): boolean { 300 for (const ident of identifiers) { 301 if (Autofixer.selfDeclared(decl.name, ident)) { 302 return false; 303 } 304 if (ident.pos < decl.pos) { 305 return false; 306 } 307 } 308 return true; 309 } 310 311 private static analizeScope(decl: ts.VariableDeclaration, identifiers: ts.Node[]): boolean { 312 const scope = Autofixer.findVarScope(decl); 313 if (scope === undefined) { 314 return false; 315 } else if (scope.kind === ts.SyntaxKind.Block) { 316 for (const ident of identifiers) { 317 if (!Autofixer.varHasScope(ident, scope)) { 318 return false; 319 } 320 } 321 } else if (scope.kind === ts.SyntaxKind.SourceFile) { 322 // Do nothing 323 } else { 324 // Unreachable, but check it 325 return false; 326 } 327 return true; 328 } 329 330 private static analizeFor(decl: ts.VariableDeclaration, identifiers: ts.Node[]): boolean { 331 const forNode = Autofixer.parentInFor(decl); 332 if (forNode) { 333 // analize that var is initialized 334 if (forNode.kind === ts.SyntaxKind.ForInStatement || forNode.kind === ts.SyntaxKind.ForOfStatement) { 335 const typedForNode = forNode as ts.ForInOrOfStatement; 336 const forVarDeclarations = (typedForNode.initializer as ts.VariableDeclarationList).declarations; 337 if (forVarDeclarations.length !== 1) { 338 return false; 339 } 340 const forVarDecl = forVarDeclarations[0]; 341 342 // our goal to skip declarations in for of/in initializer 343 if (forVarDecl !== decl && decl.initializer === undefined) { 344 return false; 345 } 346 } else if (decl.initializer === undefined) { 347 return false; 348 } 349 350 // analize that var uses are only in function block 351 for (const ident of identifiers) { 352 if (ident !== decl && !Autofixer.varHasScope(ident, forNode)) { 353 return false; 354 } 355 } 356 357 // analize that var is not in function 358 for (const ident of identifiers) { 359 if (ident !== decl && Autofixer.varInFunctionForScope(ident, forNode)) { 360 return false; 361 } 362 } 363 } 364 return true; 365 } 366 367 private checkVarDeclarations(varDeclList: ts.VariableDeclarationList): boolean { 368 for (const decl of varDeclList.declarations) { 369 const symbol = this.typeChecker.getSymbolAtLocation(decl.name); 370 if (!symbol) { 371 return false; 372 } 373 374 const identifiers = this.symbolCache.getReferences(symbol); 375 376 const declLength = symbol.declarations?.length; 377 if (!declLength || declLength >= 2) { 378 return false; 379 } 380 381 // Check for var use in tdz oe self declaration 382 if (!Autofixer.analizeTDZ(decl, identifiers)) { 383 return false; 384 } 385 386 // Has use outside scope of declaration? 387 if (!Autofixer.analizeScope(decl, identifiers)) { 388 return false; 389 } 390 391 // For analisys 392 if (!Autofixer.analizeFor(decl, identifiers)) { 393 return false; 394 } 395 396 if (symbol.getName() === 'let') { 397 return false; 398 } 399 } 400 return true; 401 } 402 403 private canAutofixNoVar(varDeclList: ts.VariableDeclarationList): boolean { 404 if (!Autofixer.parentInCaseOrWhile(varDeclList)) { 405 return false; 406 } 407 408 if (!this.checkVarDeclarations(varDeclList)) { 409 return false; 410 } 411 412 return true; 413 } 414 415 fixVarDeclaration(node: ts.VariableDeclarationList): Autofix[] | undefined { 416 const newNode = ts.factory.createVariableDeclarationList(node.declarations, ts.NodeFlags.Let); 417 const text = this.printer.printNode(ts.EmitHint.Unspecified, newNode, node.getSourceFile()); 418 return this.canAutofixNoVar(node) ? 419 [{ start: node.getStart(), end: node.getEnd(), replacementText: text }] : 420 undefined; 421 } 422 423 private getFixReturnTypeArrowFunction(funcLikeDecl: ts.FunctionLikeDeclaration, typeNode: ts.TypeNode): string { 424 if (!funcLikeDecl.body) { 425 return ''; 426 } 427 const node = ts.factory.createArrowFunction( 428 undefined, 429 funcLikeDecl.typeParameters, 430 funcLikeDecl.parameters, 431 typeNode, 432 ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 433 funcLikeDecl.body 434 ); 435 return this.printer.printNode(ts.EmitHint.Unspecified, node, funcLikeDecl.getSourceFile()); 436 } 437 438 fixMissingReturnType(funcLikeDecl: ts.FunctionLikeDeclaration, typeNode: ts.TypeNode): Autofix[] { 439 if (ts.isArrowFunction(funcLikeDecl)) { 440 const text = this.getFixReturnTypeArrowFunction(funcLikeDecl, typeNode); 441 const startPos = funcLikeDecl.getStart(); 442 const endPos = funcLikeDecl.getEnd(); 443 return [{ start: startPos, end: endPos, replacementText: text }]; 444 } 445 const text = ': ' + this.printer.printNode(ts.EmitHint.Unspecified, typeNode, funcLikeDecl.getSourceFile()); 446 const pos = Autofixer.getReturnTypePosition(funcLikeDecl); 447 return [{ start: pos, end: pos, replacementText: text }]; 448 } 449 450 dropTypeOnVarDecl(varDecl: ts.VariableDeclaration): Autofix[] { 451 const newVarDecl = ts.factory.createVariableDeclaration(varDecl.name, undefined, undefined, undefined); 452 const text = this.printer.printNode(ts.EmitHint.Unspecified, newVarDecl, varDecl.getSourceFile()); 453 return [{ start: varDecl.getStart(), end: varDecl.getEnd(), replacementText: text }]; 454 } 455 456 fixTypeAssertion(typeAssertion: ts.TypeAssertion): Autofix[] { 457 const asExpr = ts.factory.createAsExpression(typeAssertion.expression, typeAssertion.type); 458 const text = this.nonCommentPrinter.printNode(ts.EmitHint.Unspecified, asExpr, typeAssertion.getSourceFile()); 459 return [{ start: typeAssertion.getStart(), end: typeAssertion.getEnd(), replacementText: text }]; 460 } 461 462 fixCommaOperator(tsNode: ts.Node): Autofix[] { 463 const tsExprNode = tsNode as ts.BinaryExpression; 464 const text = this.recursiveCommaOperator(tsExprNode); 465 return [{ start: tsExprNode.parent.getFullStart(), end: tsExprNode.parent.getEnd(), replacementText: text }]; 466 } 467 468 private recursiveCommaOperator(tsExprNode: ts.BinaryExpression): string { 469 let text = ''; 470 if (tsExprNode.operatorToken.kind !== ts.SyntaxKind.CommaToken) { 471 return tsExprNode.getFullText() + ';'; 472 } 473 474 if (tsExprNode.left.kind === ts.SyntaxKind.BinaryExpression) { 475 text += this.recursiveCommaOperator(tsExprNode.left as ts.BinaryExpression); 476 text += '\n' + tsExprNode.right.getFullText() + ';'; 477 } else { 478 const leftText = tsExprNode.left.getFullText(); 479 const rightText = tsExprNode.right.getFullText(); 480 text = leftText + ';\n' + rightText + ';'; 481 } 482 483 return text; 484 } 485 486 private getEnumMembers(node: ts.Node, enumDeclsInFile: ts.Declaration[], result: Autofix[] | undefined): void { 487 if (result === undefined || !ts.isEnumDeclaration(node)) { 488 return; 489 } 490 491 if (result.length) { 492 result.push({ start: node.getStart(), end: node.getEnd(), replacementText: '' }); 493 return; 494 } 495 496 const members: ts.EnumMember[] = []; 497 for (const decl of enumDeclsInFile) { 498 for (const member of (decl as ts.EnumDeclaration).members) { 499 if ( 500 member.initializer && 501 member.initializer.kind !== ts.SyntaxKind.NumericLiteral && 502 member.initializer.kind !== ts.SyntaxKind.StringLiteral 503 ) { 504 result = undefined; 505 return; 506 } 507 } 508 members.push(...(decl as ts.EnumDeclaration).members); 509 } 510 511 const fullEnum = ts.factory.createEnumDeclaration(node.modifiers, node.name, members); 512 const fullText = this.printer.printNode(ts.EmitHint.Unspecified, fullEnum, node.getSourceFile()); 513 result.push({ start: node.getStart(), end: node.getEnd(), replacementText: fullText }); 514 } 515 516 fixEnumMerging(enumSymbol: ts.Symbol, enumDeclsInFile: ts.Declaration[]): Autofix[] | undefined { 517 if (this.enumMergingCache.has(enumSymbol)) { 518 return this.enumMergingCache.get(enumSymbol); 519 } 520 521 if (enumDeclsInFile.length <= 1) { 522 this.enumMergingCache.set(enumSymbol, undefined); 523 return undefined; 524 } 525 526 let result: Autofix[] | undefined = []; 527 this.symbolCache.getReferences(enumSymbol).forEach((node) => { 528 this.getEnumMembers(node, enumDeclsInFile, result); 529 }); 530 if (!result?.length) { 531 result = undefined; 532 } 533 534 this.enumMergingCache.set(enumSymbol, result); 535 return result; 536 } 537 538 private readonly enumMergingCache = new Map<ts.Symbol, Autofix[] | undefined>(); 539 540 private readonly printer: ts.Printer = ts.createPrinter({ 541 omitTrailingSemicolon: false, 542 removeComments: false, 543 newLine: ts.NewLineKind.LineFeed 544 }); 545 546 private readonly nonCommentPrinter: ts.Printer = ts.createPrinter({ 547 omitTrailingSemicolon: false, 548 removeComments: true, 549 newLine: ts.NewLineKind.LineFeed 550 }); 551 552 private static getReturnTypePosition(funcLikeDecl: ts.FunctionLikeDeclaration): number { 553 if (funcLikeDecl.body) { 554 555 /* 556 * Find position of the first node or token that follows parameters. 557 * After that, iterate over child nodes in reverse order, until found 558 * first closing parenthesis. 559 */ 560 const postParametersPosition = ts.isArrowFunction(funcLikeDecl) ? 561 funcLikeDecl.equalsGreaterThanToken.getStart() : 562 funcLikeDecl.body.getStart(); 563 564 const children = funcLikeDecl.getChildren(); 565 for (let i = children.length - 1; i >= 0; i--) { 566 const child = children[i]; 567 if (child.kind === ts.SyntaxKind.CloseParenToken && child.getEnd() <= postParametersPosition) { 568 return child.getEnd(); 569 } 570 } 571 } 572 573 // Shouldn't get here. 574 return -1; 575 } 576 577 private static needsParentheses(node: ts.FunctionExpression): boolean { 578 const parent = node.parent; 579 return ( 580 ts.isPrefixUnaryExpression(parent) || 581 ts.isPostfixUnaryExpression(parent) || 582 ts.isPropertyAccessExpression(parent) || 583 ts.isElementAccessExpression(parent) || 584 ts.isTypeOfExpression(parent) || 585 ts.isVoidExpression(parent) || 586 ts.isAwaitExpression(parent) || 587 ts.isCallExpression(parent) && node === parent.expression || 588 ts.isBinaryExpression(parent) && !isAssignmentOperator(parent.operatorToken) 589 ); 590 } 591 592 fixCtorParameterProperties( 593 ctorDecl: ts.ConstructorDeclaration, 594 paramTypes: ts.TypeNode[] | undefined 595 ): Autofix[] | undefined { 596 if (paramTypes === undefined) { 597 return undefined; 598 } 599 600 const fieldInitStmts: ts.Statement[] = []; 601 const newFieldPos = ctorDecl.getStart(); 602 const autofixes: Autofix[] = [{ start: newFieldPos, end: newFieldPos, replacementText: '' }]; 603 604 for (let i = 0; i < ctorDecl.parameters.length; i++) { 605 this.fixCtorParameterPropertiesProcessParam( 606 ctorDecl.parameters[i], 607 paramTypes[i], 608 ctorDecl.getSourceFile(), 609 fieldInitStmts, 610 autofixes 611 ); 612 } 613 614 // Note: Bodyless ctors can't have parameter properties. 615 if (ctorDecl.body) { 616 const newBody = ts.factory.createBlock(fieldInitStmts.concat(ctorDecl.body.statements), true); 617 const newBodyText = this.printer.printNode(ts.EmitHint.Unspecified, newBody, ctorDecl.getSourceFile()); 618 autofixes.push({ start: ctorDecl.body.getStart(), end: ctorDecl.body.getEnd(), replacementText: newBodyText }); 619 } 620 621 return autofixes; 622 } 623 624 private fixCtorParameterPropertiesProcessParam( 625 param: ts.ParameterDeclaration, 626 paramType: ts.TypeNode, 627 sourceFile: ts.SourceFile, 628 fieldInitStmts: ts.Statement[], 629 autofixes: Autofix[] 630 ): void { 631 // Parameter property can not be a destructuring parameter. 632 if (!ts.isIdentifier(param.name)) { 633 return; 634 } 635 636 if (this.utils.hasAccessModifier(param)) { 637 const propIdent = ts.factory.createIdentifier(param.name.text); 638 639 const newFieldNode = ts.factory.createPropertyDeclaration( 640 ts.getModifiers(param), 641 propIdent, 642 undefined, 643 paramType, 644 undefined 645 ); 646 const newFieldText = this.printer.printNode(ts.EmitHint.Unspecified, newFieldNode, sourceFile) + '\n'; 647 autofixes[0].replacementText += newFieldText; 648 649 const newParamDecl = ts.factory.createParameterDeclaration( 650 undefined, 651 undefined, 652 param.name, 653 param.questionToken, 654 param.type, 655 param.initializer 656 ); 657 const newParamText = this.printer.printNode(ts.EmitHint.Unspecified, newParamDecl, sourceFile); 658 autofixes.push({ start: param.getStart(), end: param.getEnd(), replacementText: newParamText }); 659 660 fieldInitStmts.push( 661 ts.factory.createExpressionStatement( 662 ts.factory.createAssignment( 663 ts.factory.createPropertyAccessExpression(ts.factory.createThis(), propIdent), 664 propIdent 665 ) 666 ) 667 ); 668 } 669 } 670 671 fixPrivateIdentifier(node: ts.PrivateIdentifier): Autofix[] | undefined { 672 const classMember = this.typeChecker.getSymbolAtLocation(node); 673 if (!classMember || (classMember.getFlags() & ts.SymbolFlags.ClassMember) === 0 || !classMember.valueDeclaration) { 674 return undefined; 675 } 676 677 if (this.privateIdentifierCache.has(classMember)) { 678 return this.privateIdentifierCache.get(classMember); 679 } 680 681 const memberDecl = classMember.valueDeclaration as ts.ClassElement; 682 const parentDecl = memberDecl.parent; 683 if (!ts.isClassLike(parentDecl) || this.utils.classMemberHasDuplicateName(memberDecl, parentDecl, true)) { 684 this.privateIdentifierCache.set(classMember, undefined); 685 return undefined; 686 } 687 688 let result: Autofix[] | undefined = []; 689 this.symbolCache.getReferences(classMember).forEach((ident) => { 690 if (ts.isPrivateIdentifier(ident)) { 691 result!.push(this.fixSinglePrivateIdentifier(ident)); 692 } 693 }); 694 if (!result.length) { 695 result = undefined; 696 } 697 698 this.privateIdentifierCache.set(classMember, result); 699 return result; 700 } 701 702 private isFunctionDeclarationFirst(tsFunctionDeclaration: ts.FunctionDeclaration): boolean { 703 if (tsFunctionDeclaration.name === undefined) { 704 return false; 705 } 706 707 const symbol = this.typeChecker.getSymbolAtLocation(tsFunctionDeclaration.name); 708 if (symbol === undefined) { 709 return false; 710 } 711 712 let minPos = tsFunctionDeclaration.pos; 713 this.symbolCache.getReferences(symbol).forEach((ident) => { 714 if (ident.pos < minPos) { 715 minPos = ident.pos; 716 } 717 }); 718 719 return minPos >= tsFunctionDeclaration.pos; 720 } 721 722 fixNestedFunction(tsFunctionDeclaration: ts.FunctionDeclaration): Autofix[] | undefined { 723 const isGenerator = tsFunctionDeclaration.asteriskToken !== undefined; 724 const hasThisKeyword = 725 tsFunctionDeclaration.body === undefined ? false : scopeContainsThis(tsFunctionDeclaration.body); 726 const canBeFixed = !isGenerator && !hasThisKeyword; 727 if (!canBeFixed) { 728 return undefined; 729 } 730 731 const name = tsFunctionDeclaration.name?.escapedText; 732 const type = tsFunctionDeclaration.type; 733 const body = tsFunctionDeclaration.body; 734 if (!name || !type || !body) { 735 return undefined; 736 } 737 738 // Check only illegal decorators, cause all decorators for function declaration are illegal 739 if (ts.getIllegalDecorators(tsFunctionDeclaration)) { 740 return undefined; 741 } 742 743 if (!this.isFunctionDeclarationFirst(tsFunctionDeclaration)) { 744 return undefined; 745 } 746 747 const typeParameters = tsFunctionDeclaration.typeParameters; 748 const parameters = tsFunctionDeclaration.parameters; 749 const modifiers = ts.getModifiers(tsFunctionDeclaration); 750 751 const token = ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken); 752 const typeDecl = ts.factory.createFunctionTypeNode(typeParameters, parameters, type); 753 const arrowFunc = ts.factory.createArrowFunction(modifiers, typeParameters, parameters, type, token, body); 754 755 const declaration: ts.VariableDeclaration = ts.factory.createVariableDeclaration( 756 name, 757 undefined, 758 typeDecl, 759 arrowFunc 760 ); 761 const list: ts.VariableDeclarationList = ts.factory.createVariableDeclarationList([declaration], ts.NodeFlags.Let); 762 763 const statement = ts.factory.createVariableStatement(modifiers, list); 764 const text = this.printer.printNode(ts.EmitHint.Unspecified, statement, tsFunctionDeclaration.getSourceFile()); 765 return [{ start: tsFunctionDeclaration.getStart(), end: tsFunctionDeclaration.getEnd(), replacementText: text }]; 766 } 767 768 fixMultipleStaticBlocks(nodes: ts.Node[]): Autofix[] | undefined { 769 const autofix: Autofix[] | undefined = []; 770 let body = (nodes[0] as ts.ClassStaticBlockDeclaration).body; 771 let bodyStatements: ts.Statement[] = []; 772 bodyStatements = bodyStatements.concat(body.statements); 773 for (let i = 1; i < nodes.length; i++) { 774 bodyStatements = bodyStatements.concat((nodes[i] as ts.ClassStaticBlockDeclaration).body.statements); 775 autofix[i] = { start: nodes[i].getStart(), end: nodes[i].getEnd(), replacementText: '' }; 776 } 777 body = ts.factory.createBlock(bodyStatements, true); 778 // static blocks shouldn't have modifiers 779 const statickBlock = ts.factory.createClassStaticBlockDeclaration(body); 780 const text = this.printer.printNode(ts.EmitHint.Unspecified, statickBlock, nodes[0].getSourceFile()); 781 autofix[0] = { start: nodes[0].getStart(), end: nodes[0].getEnd(), replacementText: text }; 782 return autofix; 783 } 784 785 private readonly privateIdentifierCache = new Map<ts.Symbol, Autofix[] | undefined>(); 786 787 private fixSinglePrivateIdentifier(ident: ts.PrivateIdentifier): Autofix { 788 if ( 789 ts.isPropertyDeclaration(ident.parent) || 790 ts.isMethodDeclaration(ident.parent) || 791 ts.isGetAccessorDeclaration(ident.parent) || 792 ts.isSetAccessorDeclaration(ident.parent) 793 ) { 794 // Note: 'private' modifier should always be first. 795 const mods = ts.getModifiers(ident.parent); 796 const newMods: ts.Modifier[] = [ts.factory.createModifier(ts.SyntaxKind.PrivateKeyword)]; 797 if (mods) { 798 for (const mod of mods) { 799 newMods.push(ts.factory.createModifier(mod.kind)); 800 } 801 } 802 803 const newName = ident.text.slice(1, ident.text.length); 804 const newDecl = Autofixer.replacePrivateIdentInDeclarationName(newMods, newName, ident.parent); 805 const text = this.printer.printNode(ts.EmitHint.Unspecified, newDecl, ident.getSourceFile()); 806 return { start: ident.parent.getStart(), end: ident.parent.getEnd(), replacementText: text }; 807 } 808 809 return { 810 start: ident.getStart(), 811 end: ident.getEnd(), 812 replacementText: ident.text.slice(1, ident.text.length) 813 }; 814 } 815 816 private static replacePrivateIdentInDeclarationName( 817 mods: ts.Modifier[], 818 name: string, 819 oldDecl: ts.PropertyDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration 820 ): ts.Declaration { 821 if (ts.isPropertyDeclaration(oldDecl)) { 822 return ts.factory.createPropertyDeclaration( 823 mods, 824 ts.factory.createIdentifier(name), 825 oldDecl.questionToken ?? oldDecl.exclamationToken, 826 oldDecl.type, 827 oldDecl.initializer 828 ); 829 } else if (ts.isMethodDeclaration(oldDecl)) { 830 return ts.factory.createMethodDeclaration( 831 mods, 832 oldDecl.asteriskToken, 833 ts.factory.createIdentifier(name), 834 oldDecl.questionToken, 835 oldDecl.typeParameters, 836 oldDecl.parameters, 837 oldDecl.type, 838 oldDecl.body 839 ); 840 } else if (ts.isGetAccessorDeclaration(oldDecl)) { 841 return ts.factory.createGetAccessorDeclaration( 842 mods, 843 ts.factory.createIdentifier(name), 844 oldDecl.parameters, 845 oldDecl.type, 846 oldDecl.body 847 ); 848 } 849 return ts.factory.createSetAccessorDeclaration( 850 mods, 851 ts.factory.createIdentifier(name), 852 oldDecl.parameters, 853 oldDecl.body 854 ); 855 } 856 857 fixRecordObjectLiteral(objectLiteralExpr: ts.ObjectLiteralExpression): Autofix[] | undefined { 858 const autofix: Autofix[] = []; 859 860 for (const prop of objectLiteralExpr.properties) { 861 if (!prop.name) { 862 return undefined; 863 } 864 if (this.utils.isValidRecordObjectLiteralKey(prop.name)) { 865 // Skip property with a valid property key. 866 continue; 867 } 868 if (!ts.isIdentifier(prop.name)) { 869 // Can only fix identifier name. 870 return undefined; 871 } 872 873 const stringLiteralName = ts.factory.createStringLiteralFromNode(prop.name, true); 874 const text = this.printer.printNode(ts.EmitHint.Unspecified, stringLiteralName, prop.name.getSourceFile()); 875 autofix.push({ start: prop.name.getStart(), end: prop.name.getEnd(), replacementText: text }); 876 } 877 878 return autofix; 879 } 880 881 fixUntypedObjectLiteral( 882 objectLiteralExpr: ts.ObjectLiteralExpression, 883 objectLiteralType: ts.Type | undefined 884 ): Autofix[] | undefined { 885 if (objectLiteralType) { 886 887 /* 888 * Special case for object literal of Record type: fix object's property names 889 * by replacing identifiers with string literals. 890 */ 891 if (this.utils.isStdRecordType(this.utils.getNonNullableType(objectLiteralType))) { 892 return this.fixRecordObjectLiteral(objectLiteralExpr); 893 } 894 895 // Can't fix when object literal has a contextual type. 896 return undefined; 897 } 898 899 const enclosingStmt = TsUtils.getEnclosingTopLevelStatement(objectLiteralExpr); 900 if (!enclosingStmt) { 901 return undefined; 902 } 903 904 const newInterfaceProps = this.getInterfacePropertiesFromObjectLiteral(objectLiteralExpr, enclosingStmt); 905 if (!newInterfaceProps) { 906 return undefined; 907 } 908 909 const srcFile = objectLiteralExpr.getSourceFile(); 910 const newInterfaceName = TsUtils.generateUniqueName(this.objectLiteralInterfaceNameGenerator, srcFile); 911 if (!newInterfaceName) { 912 return undefined; 913 } 914 915 return [ 916 this.createNewInterface(srcFile, newInterfaceName, newInterfaceProps, enclosingStmt.getStart()), 917 this.fixObjectLiteralExpression(srcFile, newInterfaceName, objectLiteralExpr) 918 ]; 919 } 920 921 private getInterfacePropertiesFromObjectLiteral( 922 objectLiteralExpr: ts.ObjectLiteralExpression, 923 enclosingStmt: ts.Node 924 ): ts.PropertySignature[] | undefined { 925 const interfaceProps: ts.PropertySignature[] = []; 926 for (const prop of objectLiteralExpr.properties) { 927 const interfaceProp = this.getInterfacePropertyFromObjectLiteralElement(prop, enclosingStmt); 928 if (!interfaceProp) { 929 return undefined; 930 } 931 interfaceProps.push(interfaceProp); 932 } 933 return interfaceProps; 934 } 935 936 private getInterfacePropertyFromObjectLiteralElement( 937 prop: ts.ObjectLiteralElementLike, 938 enclosingStmt: ts.Node 939 ): ts.PropertySignature | undefined { 940 // Can't fix if property is not a key-value pair, or the property name is a computed value. 941 if (!ts.isPropertyAssignment(prop) || ts.isComputedPropertyName(prop.name)) { 942 return undefined; 943 } 944 945 const propType = this.typeChecker.getTypeAtLocation(prop); 946 947 // Can't capture generic type parameters of enclosing declarations. 948 if (this.utils.hasGenericTypeParameter(propType)) { 949 return undefined; 950 } 951 952 if (Autofixer.propertyTypeIsCapturedFromEnclosingLocalScope(propType, enclosingStmt)) { 953 return undefined; 954 } 955 956 const propTypeNode = this.typeChecker.typeToTypeNode(propType, undefined, ts.NodeBuilderFlags.None); 957 if (!propTypeNode || !this.utils.isSupportedType(propTypeNode)) { 958 return undefined; 959 } 960 961 const newProp: ts.PropertySignature = ts.factory.createPropertySignature( 962 undefined, 963 prop.name, 964 undefined, 965 propTypeNode 966 ); 967 return newProp; 968 } 969 970 private static propertyTypeIsCapturedFromEnclosingLocalScope(type: ts.Type, enclosingStmt: ts.Node): boolean { 971 const sym = type.getSymbol(); 972 let symNode: ts.Node | undefined = TsUtils.getDeclaration(sym); 973 974 while (symNode) { 975 if (symNode === enclosingStmt) { 976 return true; 977 } 978 symNode = symNode.parent; 979 } 980 981 return false; 982 } 983 984 private createNewInterface( 985 srcFile: ts.SourceFile, 986 interfaceName: string, 987 members: ts.TypeElement[], 988 pos: number 989 ): Autofix { 990 const newInterfaceDecl = ts.factory.createInterfaceDeclaration( 991 undefined, 992 interfaceName, 993 undefined, 994 undefined, 995 members 996 ); 997 const text = this.printer.printNode(ts.EmitHint.Unspecified, newInterfaceDecl, srcFile) + '\n'; 998 return { start: pos, end: pos, replacementText: text }; 999 } 1000 1001 private fixObjectLiteralExpression( 1002 srcFile: ts.SourceFile, 1003 newInterfaceName: string, 1004 objectLiteralExpr: ts.ObjectLiteralExpression 1005 ): Autofix { 1006 1007 /* 1008 * If object literal is initializing a variable or property, 1009 * then simply add new 'contextual' type to the declaration. 1010 * Otherwise, cast object literal to newly created interface type. 1011 */ 1012 if ( 1013 (ts.isVariableDeclaration(objectLiteralExpr.parent) || 1014 ts.isPropertyDeclaration(objectLiteralExpr.parent) || 1015 ts.isParameter(objectLiteralExpr.parent)) && 1016 !objectLiteralExpr.parent.type 1017 ) { 1018 const text = ': ' + newInterfaceName; 1019 const pos = Autofixer.getDeclarationTypePositionForObjectLiteral(objectLiteralExpr.parent); 1020 return { start: pos, end: pos, replacementText: text }; 1021 } 1022 1023 const newTypeRef = ts.factory.createTypeReferenceNode(newInterfaceName); 1024 let newExpr: ts.Expression = ts.factory.createAsExpression( 1025 ts.factory.createObjectLiteralExpression(objectLiteralExpr.properties), 1026 newTypeRef 1027 ); 1028 if (!ts.isParenthesizedExpression(objectLiteralExpr.parent)) { 1029 newExpr = ts.factory.createParenthesizedExpression(newExpr); 1030 } 1031 const text = this.printer.printNode(ts.EmitHint.Unspecified, newExpr, srcFile); 1032 return { start: objectLiteralExpr.getStart(), end: objectLiteralExpr.getEnd(), replacementText: text }; 1033 } 1034 1035 private static getDeclarationTypePositionForObjectLiteral( 1036 decl: ts.VariableDeclaration | ts.PropertyDeclaration | ts.ParameterDeclaration 1037 ): number { 1038 if (ts.isPropertyDeclaration(decl)) { 1039 return (decl.questionToken || decl.exclamationToken || decl.name).getEnd(); 1040 } else if (ts.isParameter(decl)) { 1041 return (decl.questionToken || decl.name).getEnd(); 1042 } 1043 return (decl.exclamationToken || decl.name).getEnd(); 1044 } 1045 1046 private readonly objectLiteralInterfaceNameGenerator = new NameGenerator( 1047 GENERATED_OBJECT_LITERAL_INTERFACE_NAME, 1048 GENERATED_OBJECT_LITERAL_INTERFACE_TRESHOLD 1049 ); 1050 1051 /* 1052 * In case of type alias initialized with type literal, replace 1053 * entire type alias with identical interface declaration. 1054 */ 1055 private proceedTypeAliasDeclaration(typeLiteral: ts.TypeLiteralNode): Autofix[] | undefined { 1056 if (ts.isTypeAliasDeclaration(typeLiteral.parent)) { 1057 const typeAlias = typeLiteral.parent; 1058 const newInterfaceDecl = ts.factory.createInterfaceDeclaration( 1059 typeAlias.modifiers, 1060 typeAlias.name, 1061 typeAlias.typeParameters, 1062 undefined, 1063 typeLiteral.members 1064 ); 1065 const text = this.printer.printNode(ts.EmitHint.Unspecified, newInterfaceDecl, typeLiteral.getSourceFile()); 1066 return [{ start: typeAlias.getStart(), end: typeAlias.getEnd(), replacementText: text }]; 1067 } 1068 return undefined; 1069 } 1070 1071 fixTypeliteral(typeLiteral: ts.TypeLiteralNode): Autofix[] | undefined { 1072 const typeAliasAutofix = this.proceedTypeAliasDeclaration(typeLiteral); 1073 if (typeAliasAutofix) { 1074 return typeAliasAutofix; 1075 } 1076 1077 /* 1078 * Create new interface declaration with members of type literal 1079 * and put the interface name in place of the type literal. 1080 */ 1081 const srcFile = typeLiteral.getSourceFile(); 1082 const enclosingStmt = TsUtils.getEnclosingTopLevelStatement(typeLiteral); 1083 if (!enclosingStmt) { 1084 return undefined; 1085 } 1086 1087 if (this.typeLiteralCapturesTypeFromEnclosingLocalScope(typeLiteral, enclosingStmt)) { 1088 return undefined; 1089 } 1090 1091 const newInterfaceName = TsUtils.generateUniqueName(this.typeLiteralInterfaceNameGenerator, srcFile); 1092 if (!newInterfaceName) { 1093 return undefined; 1094 } 1095 const newInterfacePos = enclosingStmt.getStart(); 1096 const newInterfaceDecl = ts.factory.createInterfaceDeclaration( 1097 undefined, 1098 newInterfaceName, 1099 undefined, 1100 undefined, 1101 typeLiteral.members 1102 ); 1103 const interfaceText = this.printer.printNode(ts.EmitHint.Unspecified, newInterfaceDecl, srcFile) + '\n'; 1104 1105 return [ 1106 { start: newInterfacePos, end: newInterfacePos, replacementText: interfaceText }, 1107 { start: typeLiteral.getStart(), end: typeLiteral.getEnd(), replacementText: newInterfaceName } 1108 ]; 1109 } 1110 1111 typeLiteralCapturesTypeFromEnclosingLocalScope(typeLiteral: ts.TypeLiteralNode, enclosingStmt: ts.Node): boolean { 1112 let found = false; 1113 1114 const callback = (node: ts.Node): void => { 1115 if (!ts.isIdentifier(node)) { 1116 return; 1117 } 1118 const sym = this.typeChecker.getSymbolAtLocation(node); 1119 let symNode: ts.Node | undefined = TsUtils.getDeclaration(sym); 1120 while (symNode) { 1121 if (symNode === typeLiteral) { 1122 return; 1123 } 1124 if (symNode === enclosingStmt) { 1125 found = true; 1126 return; 1127 } 1128 symNode = symNode.parent; 1129 } 1130 }; 1131 1132 const stopCondition = (node: ts.Node): boolean => { 1133 void node; 1134 return found; 1135 }; 1136 1137 forEachNodeInSubtree(typeLiteral, callback, stopCondition); 1138 return found; 1139 } 1140 1141 // eslint-disable-next-line class-methods-use-this 1142 removeDecorator(decorator: ts.Decorator): Autofix[] { 1143 return [{ start: decorator.getStart(), end: decorator.getEnd(), replacementText: '' }]; 1144 } 1145 1146 private readonly typeLiteralInterfaceNameGenerator = new NameGenerator( 1147 GENERATED_TYPE_LITERAL_INTERFACE_NAME, 1148 GENERATED_TYPE_LITERAL_INTERFACE_TRESHOLD 1149 ); 1150 1151 private readonly symbolCache: SymbolCache; 1152} 1153