diff --git a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts index d18bc50639a85..9bf491ee5c799 100644 --- a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts +++ b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts @@ -41,7 +41,13 @@ namespace ts.codefix { // so duplicates cannot occur. const abstractAndNonPrivateExtendsSymbols = checker.getPropertiesOfType(instantiatedExtendsType).filter(symbolPointsToNonPrivateAndAbstractMember); - createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, context, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member)); + // Delete existing import statements, since they get overwritten by the fix. + const existingImportDeclarations = sourceFile.statements.filter(isImportDeclaration); + if (existingImportDeclarations.length > 0) { + changeTracker.deleteNodeRange(sourceFile, first(existingImportDeclarations), last(existingImportDeclarations)); + } + + createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, context, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member), importStatement => insertImport(changeTracker, sourceFile, importStatement)); } function symbolPointsToNonPrivateAndAbstractMember(symbol: Symbol): boolean { diff --git a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts index d99a75132288d..cdb9dea545f4b 100644 --- a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts +++ b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts @@ -60,7 +60,13 @@ namespace ts.codefix { createMissingIndexSignatureDeclaration(implementedType, IndexKind.String); } - createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, context, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member)); + // Delete existing import statements, since they get overwritten by the fix. + const existingImportDeclarations = sourceFile.statements.filter(isImportDeclaration); + if (existingImportDeclarations.length > 0) { + changeTracker.deleteNodeRange(sourceFile, first(existingImportDeclarations), last(existingImportDeclarations)); + } + + createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, context, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member), importStatement => insertImport(changeTracker, sourceFile, importStatement)); function createMissingIndexSignatureDeclaration(type: InterfaceType, kind: IndexKind): void { const indexInfoOfKind = checker.getIndexInfoOfType(type, kind); diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 7c6434ed34a10..280c99f372746 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -6,13 +6,42 @@ namespace ts.codefix { * @param possiblyMissingSymbols The collection of symbols to filter and then get insertions for. * @returns Empty string iff there are no member insertions. */ - export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: ReadonlyArray, context: TypeConstructionContext, preferences: UserPreferences, out: (node: ClassElement) => void): void { + export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: ReadonlyArray, context: TypeConstructionContext, preferences: UserPreferences, out: (node: ClassElement) => void, outImportStatements: (statement: Statement) => void): void { const classMembers = classDeclaration.symbol.members!; + const sourceFile = classDeclaration.getSourceFile(); + const importStatements: ImportDeclaration[] = [...sourceFile.statements.filter(isImportDeclaration)]; for (const symbol of possiblyMissingSymbols) { if (!classMembers.has(symbol.escapedName)) { - addNewNodeForMemberSymbol(symbol, classDeclaration, context, preferences, out); + addNewNodeForMemberSymbol(symbol, classDeclaration, context, preferences, getQuotePreference(sourceFile, preferences), out, (newImport) => { + addNonDuplicateImport(importStatements, newImport); + }); } } + importStatements.forEach(outImportStatements); + } + + function addNonDuplicateImport(imports: ImportDeclaration[], newImport: ImportDeclaration) { + for (const existingImport of imports) { + if (isStringLiteralLike(existingImport.moduleSpecifier) && isStringLiteralLike(newImport.moduleSpecifier) && + existingImport.moduleSpecifier.text === newImport.moduleSpecifier.text && + existingImport.importClause && newImport.importClause) { + + if (newImport.importClause.name) { + existingImport.importClause.name = newImport.importClause.name; + return; + } + if (newImport.importClause.namedBindings && existingImport.importClause.namedBindings && + isNamedImportBindings(newImport.importClause.namedBindings) && isNamedImportBindings(existingImport.importClause.namedBindings)) { + + const existingNamedImports = (existingImport.importClause.namedBindings as NamedImports); + const newNamedImports = (newImport.importClause.namedBindings as NamedImports); + const newElements = newNamedImports.elements.filter(e => !existingNamedImports.elements.some(existing => existing.name.text === e.name.text)); + existingImport.importClause.namedBindings = createNamedImports([...existingNamedImports.elements, ...newElements]); + return; + } + } + } + imports.push(newImport); } function getModuleSpecifierResolverHost(context: TypeConstructionContext): SymbolTracker["moduleResolverHost"] { @@ -42,7 +71,7 @@ namespace ts.codefix { /** * @returns Empty string iff there we can't figure out a representation for `symbol` in `enclosingDeclaration`. */ - function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, context: TypeConstructionContext, preferences: UserPreferences, out: (node: Node) => void): void { + function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, context: TypeConstructionContext, preferences: UserPreferences, quotePreference: QuotePreference, out: (node: Node) => void, outImportStatements: (statement: ImportDeclaration) => void): void { const declarations = symbol.getDeclarations(); if (!(declarations && declarations.length)) { return undefined; @@ -66,7 +95,7 @@ namespace ts.codefix { modifiers, name, optional ? createToken(SyntaxKind.QuestionToken) : undefined, - typeNode, + replaceInlineImportAndEmitImportStatement(typeNode), /*initializer*/ undefined)); break; case SyntaxKind.GetAccessor: @@ -83,7 +112,7 @@ namespace ts.codefix { modifiers, name, emptyArray, - typeNode, + replaceInlineImportAndEmitImportStatement(typeNode), ambient ? undefined : createStubbedMethodBody(preferences))); } else { @@ -94,7 +123,7 @@ namespace ts.codefix { /*decorators*/ undefined, modifiers, name, - createDummyParameters(1, [parameterName], [typeNode], 1, /*inJs*/ false), + createDummyParameters(1, [parameterName], [replaceInlineImportAndEmitImportStatement(typeNode)], 1, /*inJs*/ false), ambient ? undefined : createStubbedMethodBody(preferences))); } } @@ -141,7 +170,35 @@ namespace ts.codefix { function outputMethod(signature: Signature, modifiers: NodeArray | undefined, name: PropertyName, body?: Block): void { const method = signatureToMethodDeclaration(context, signature, enclosingDeclaration, modifiers, name, optional, body); - if (method) out(method); + if (method) out(removeFunctionInlineImports(method)); + } + + function removeFunctionInlineImports(functionNode: FunctionTypeNode | MethodDeclaration) { + functionNode.parameters.forEach((parameter) => { + const parameterType = parameter.type; + if (parameterType && parameterType.kind === SyntaxKind.ImportType) { + const importNode = parameterType as ImportTypeNode; + parameter.type = replaceInlineImportAndEmitImportStatement(importNode); + } + }); + functionNode.type = replaceInlineImportAndEmitImportStatement(functionNode.type); + return functionNode; + } + + function replaceInlineImportAndEmitImportStatement(typeNode?: TypeNode) { + if (typeNode && typeNode.kind === SyntaxKind.ImportType) { + const imported = typeNode as ImportTypeNode; + const importedIdentifier = (imported.qualifier as Identifier).text; + const moduleFileName = ((imported.argument as LiteralTypeNode).literal as StringLiteral).text; + const importSpecifier = createImportSpecifier(undefined, createIdentifier((importedIdentifier))); + const importStatement = makeImportIfNecessary(/* defaultImport */ undefined, [importSpecifier], moduleFileName, quotePreference); + + if (importStatement) { + outImportStatements(importStatement); + } + return createTypeReferenceNode(importedIdentifier, /* typeArgument */ undefined); + } + return typeNode; } } diff --git a/tests/cases/fourslash/codeFixClassImplementInterface_typeInOtherFile.ts b/tests/cases/fourslash/codeFixClassImplementInterface_typeInOtherFile.ts index ed5d29cd767aa..f1fb7d5f7ca03 100644 --- a/tests/cases/fourslash/codeFixClassImplementInterface_typeInOtherFile.ts +++ b/tests/cases/fourslash/codeFixClassImplementInterface_typeInOtherFile.ts @@ -15,10 +15,10 @@ goTo.file("/C.ts"); verify.codeFix({ description: "Implement interface 'I'", newFileContent: -`import { I } from "./I"; +`import { I, J } from "./I"; export class C implements I { - x: import("./I").J; - m(): import("./I").J { + x: J; + m(): J { throw new Error("Method not implemented."); } }`, diff --git a/tests/cases/fourslash/quickfixImplementInterfaceUnreachableTypeUsesRelativeImport.ts b/tests/cases/fourslash/quickfixImplementInterfaceUnreachableTypeUsesRelativeImport.ts index ba6cd702ea00f..6d66793071eda 100644 --- a/tests/cases/fourslash/quickfixImplementInterfaceUnreachableTypeUsesRelativeImport.ts +++ b/tests/cases/fourslash/quickfixImplementInterfaceUnreachableTypeUsesRelativeImport.ts @@ -18,9 +18,10 @@ verify.codeFix({ description: "Implement interface 'Foo'", newFileContent: { "/tests/cases/fourslash/index.ts": `import { Foo } from './interface'; +import { Class } from './class'; class X implements Foo { - x: import("./class").Class; + x: Class; }` } });