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