diff --git a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts index d18bc50639a85..a27ded9d403f8 100644 --- a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts +++ b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts @@ -41,7 +41,9 @@ 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)); + const importAdder = createImportAdder(sourceFile, context.program, preferences, context.host); + createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, context, preferences, importAdder, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member)); + importAdder.writeFixes(changeTracker); } function symbolPointsToNonPrivateAndAbstractMember(symbol: Symbol): boolean { diff --git a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts index 0735e85bc7004..58e5a137c6b5e 100644 --- a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts +++ b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts @@ -63,7 +63,9 @@ namespace ts.codefix { createMissingIndexSignatureDeclaration(implementedType, IndexKind.String); } - createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, context, preferences, member => insertInterfaceMemberNode(sourceFile, classDeclaration, member)); + const importAdder = createImportAdder(sourceFile, context.program, preferences, context.host); + createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, context, preferences, importAdder, member => insertInterfaceMemberNode(sourceFile, classDeclaration, member)); + importAdder.writeFixes(changeTracker); 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 b1893f2e93c24..b2e05fab06c3f 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -4,13 +4,14 @@ namespace ts.codefix { * Finds members of the resolved type that are missing in the class pointed to by class decl * and generates source code for the missing members. * @param possiblyMissingSymbols The collection of symbols to filter and then get insertions for. + * @param importAdder If provided, type annotations will use identifier type references instead of ImportTypeNodes, and the missing imports will be added to the importAdder. * @returns Empty string iff there are no member insertions. */ - export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: readonly Symbol[], context: TypeConstructionContext, preferences: UserPreferences, out: (node: ClassElement) => void): void { + export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: readonly Symbol[], context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: ClassElement) => void): void { const classMembers = classDeclaration.symbol.members!; for (const symbol of possiblyMissingSymbols) { if (!classMembers.has(symbol.escapedName)) { - addNewNodeForMemberSymbol(symbol, classDeclaration, context, preferences, out); + addNewNodeForMemberSymbol(symbol, classDeclaration, context, preferences, importAdder, addClassElement); } } } @@ -19,7 +20,7 @@ namespace ts.codefix { return { directoryExists: context.host.directoryExists ? d => context.host.directoryExists!(d) : undefined, fileExists: context.host.fileExists ? f => context.host.fileExists!(f) : undefined, - getCurrentDirectory: context.host.getCurrentDirectory ? () => context.host.getCurrentDirectory!() : undefined, + getCurrentDirectory: context.host.getCurrentDirectory ? () => context.host.getCurrentDirectory() : undefined, readFile: context.host.readFile ? f => context.host.readFile!(f) : undefined, useCaseSensitiveFileNames: context.host.useCaseSensitiveFileNames ? () => context.host.useCaseSensitiveFileNames!() : undefined, getSourceFiles: () => context.program.getSourceFiles(), @@ -36,19 +37,19 @@ namespace ts.codefix { export interface TypeConstructionContext { program: Program; - host: ModuleSpecifierResolutionHost; + host: LanguageServiceHost; } /** * @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, importAdder: ImportAdder | undefined, addClassElement: (node: Node) => void): void { const declarations = symbol.getDeclarations(); if (!(declarations && declarations.length)) { return undefined; } const checker = context.program.getTypeChecker(); - + const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions()); const declaration = declarations[0]; const name = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName; const visibilityModifier = createVisibilityModifier(getModifierFlags(declaration)); @@ -61,8 +62,15 @@ namespace ts.codefix { case SyntaxKind.PropertySignature: case SyntaxKind.PropertyDeclaration: const flags = preferences.quotePreference === "single" ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : undefined; - const typeNode = checker.typeToTypeNode(type, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context)); - out(createProperty( + let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context)); + if (importAdder) { + const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(typeNode, type, scriptTarget); + if (importableReference) { + typeNode = importableReference.typeReference; + importSymbols(importAdder, importableReference.symbols); + } + } + addClassElement(createProperty( /*decorators*/undefined, modifiers, name, @@ -72,14 +80,21 @@ namespace ts.codefix { break; case SyntaxKind.GetAccessor: case SyntaxKind.SetAccessor: { + let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context)); const allAccessors = getAllAccessorDeclarations(declarations, declaration as AccessorDeclaration); - const typeNode = checker.typeToTypeNode(type, enclosingDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context)); const orderedAccessors = allAccessors.secondAccessor ? [allAccessors.firstAccessor, allAccessors.secondAccessor] : [allAccessors.firstAccessor]; + if (importAdder) { + const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(typeNode, type, scriptTarget); + if (importableReference) { + typeNode = importableReference.typeReference; + importSymbols(importAdder, importableReference.symbols); + } + } for (const accessor of orderedAccessors) { if (isGetAccessorDeclaration(accessor)) { - out(createGetAccessor( + addClassElement(createGetAccessor( /*decorators*/ undefined, modifiers, name, @@ -91,7 +106,7 @@ namespace ts.codefix { Debug.assertNode(accessor, isSetAccessorDeclaration, "The counterpart to a getter should be a setter"); const parameter = getSetAccessorValueParameter(accessor); const parameterName = parameter && isIdentifier(parameter.name) ? idText(parameter.name) : undefined; - out(createSetAccessor( + addClassElement(createSetAccessor( /*decorators*/ undefined, modifiers, name, @@ -134,15 +149,15 @@ namespace ts.codefix { } else { Debug.assert(declarations.length === signatures.length, "Declarations and signatures should match count"); - out(createMethodImplementingSignatures(signatures, name, optional, modifiers, preferences)); + addClassElement(createMethodImplementingSignatures(signatures, name, optional, modifiers, preferences)); } } break; } 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); + const method = signatureToMethodDeclaration(context, signature, enclosingDeclaration, modifiers, name, optional, body, importAdder); + if (method) addClassElement(method); } } @@ -154,13 +169,53 @@ namespace ts.codefix { name: PropertyName, optional: boolean, body: Block | undefined, + importAdder: ImportAdder | undefined, ): MethodDeclaration | undefined { const program = context.program; - const signatureDeclaration = program.getTypeChecker().signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, NodeBuilderFlags.NoTruncation | NodeBuilderFlags.SuppressAnyReturnType, getNoopSymbolTrackerWithResolver(context)); + const checker = program.getTypeChecker(); + const scriptTarget = getEmitScriptTarget(program.getCompilerOptions()); + const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, NodeBuilderFlags.NoTruncation | NodeBuilderFlags.SuppressAnyReturnType, getNoopSymbolTrackerWithResolver(context)); if (!signatureDeclaration) { return undefined; } + if (importAdder) { + if (signatureDeclaration.typeParameters) { + forEach(signatureDeclaration.typeParameters, (typeParameterDecl, i) => { + const typeParameter = signature.typeParameters![i]; + if (typeParameterDecl.constraint) { + const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(typeParameterDecl.constraint, typeParameter.constraint, scriptTarget); + if (importableReference) { + typeParameterDecl.constraint = importableReference.typeReference; + importSymbols(importAdder, importableReference.symbols); + } + } + if (typeParameterDecl.default) { + const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(typeParameterDecl.default, typeParameter.default, scriptTarget); + if (importableReference) { + typeParameterDecl.default = importableReference.typeReference; + importSymbols(importAdder, importableReference.symbols); + } + } + }); + } + forEach(signatureDeclaration.parameters, (parameterDecl, i) => { + const parameter = signature.parameters[i]; + const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(parameterDecl.type, checker.getTypeAtLocation(parameter.valueDeclaration), scriptTarget); + if (importableReference) { + parameterDecl.type = importableReference.typeReference; + importSymbols(importAdder, importableReference.symbols); + } + }); + if (signatureDeclaration.type) { + const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(signatureDeclaration.type, signature.resolvedReturnType, scriptTarget); + if (importableReference) { + signatureDeclaration.type = importableReference.typeReference; + importSymbols(importAdder, importableReference.symbols); + } + } + } + signatureDeclaration.decorators = undefined; signatureDeclaration.modifiers = modifiers; signatureDeclaration.name = name; @@ -359,4 +414,51 @@ namespace ts.codefix { export function findJsonProperty(obj: ObjectLiteralExpression, name: string): PropertyAssignment | undefined { return find(obj.properties, (p): p is PropertyAssignment => isPropertyAssignment(p) && !!p.name && isStringLiteral(p.name) && p.name.text === name); } + + /** + * Given an ImportTypeNode 'import("./a").SomeType>', + * returns an equivalent type reference node with any nested ImportTypeNodes also replaced + * with type references, and a list of symbols that must be imported to use the type reference. + */ + export function tryGetAutoImportableReferenceFromImportTypeNode(importTypeNode: TypeNode | undefined, type: Type | undefined, scriptTarget: ScriptTarget) { + if (importTypeNode && isLiteralImportTypeNode(importTypeNode) && importTypeNode.qualifier && (!type || type.symbol)) { + // Symbol for the left-most thing after the dot + const firstIdentifier = getFirstIdentifier(importTypeNode.qualifier); + const name = getNameForExportedSymbol(firstIdentifier.symbol, scriptTarget); + const qualifier = name !== firstIdentifier.text + ? replaceFirstIdentifierOfEntityName(importTypeNode.qualifier, createIdentifier(name)) + : importTypeNode.qualifier; + + const symbols = [firstIdentifier.symbol]; + const typeArguments: TypeNode[] = []; + if (importTypeNode.typeArguments) { + importTypeNode.typeArguments.forEach(arg => { + const ref = tryGetAutoImportableReferenceFromImportTypeNode(arg, /*undefined*/ type, scriptTarget); + if (ref) { + symbols.push(...ref.symbols); + typeArguments.push(ref.typeReference); + } + else { + typeArguments.push(arg); + } + }); + } + + return { + symbols, + typeReference: createTypeReferenceNode(qualifier, typeArguments) + }; + } + } + + function replaceFirstIdentifierOfEntityName(name: EntityName, newIdentifier: Identifier): EntityName { + if (name.kind === SyntaxKind.Identifier) { + return newIdentifier; + } + return createQualifiedName(replaceFirstIdentifierOfEntityName(name.left, newIdentifier), name.right); + } + + function importSymbols(importAdder: ImportAdder, symbols: readonly Symbol[]) { + symbols.forEach(s => importAdder.addImportFromExportedSymbol(s, /*usageIsTypeOnly*/ true)); + } } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 64c9c93189842..d66bc414fa750 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -24,90 +24,121 @@ namespace ts.codefix { }, fixIds: [importFixId], getAllCodeActions: context => { - const { sourceFile, preferences } = context; - - // Namespace fixes don't conflict, so just build a list. - const addToNamespace: FixUseNamespaceImport[] = []; - const importType: FixUseImportType[] = []; - // Keys are import clause node IDs. - const addToExisting = createMap<{ readonly importClause: ImportClause, defaultImport: string | undefined; readonly namedImports: string[], canUseTypeOnlyImport: boolean }>(); - const newImports = createMap>(); - let lastModuleSpecifier: string | undefined; - - eachDiagnostic(context, errorCodes, diag => { - const info = getFixesInfo(context, diag.code, diag.start); - if (!info || !info.fixes.length) return; - const { fixes, symbolName } = info; - - const fix = first(fixes); - switch (fix.kind) { - case ImportFixKind.UseNamespace: - addToNamespace.push(fix); - break; - case ImportFixKind.ImportType: - importType.push(fix); - break; - case ImportFixKind.AddToExisting: { - const { importClause, importKind, canUseTypeOnlyImport } = fix; - const key = String(getNodeId(importClause)); - let entry = addToExisting.get(key); - if (!entry) { - addToExisting.set(key, entry = { importClause, defaultImport: undefined, namedImports: [], canUseTypeOnlyImport }); - } - if (importKind === ImportKind.Named) { - pushIfUnique(entry.namedImports, symbolName); - } - else { - Debug.assert(entry.defaultImport === undefined || entry.defaultImport === symbolName, "(Add to Existing) Default import should be missing or match symbolName"); - entry.defaultImport = symbolName; - } - break; + const { sourceFile, program, preferences, host } = context; + const importAdder = createImportAdder(sourceFile, program, preferences, host); + eachDiagnostic(context, errorCodes, diag => importAdder.addImportFromDiagnostic(diag, context)); + return createCombinedCodeActions(textChanges.ChangeTracker.with(context, importAdder.writeFixes)); + }, + }); + + export interface ImportAdder { + addImportFromDiagnostic: (diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) => void; + addImportFromExportedSymbol: (exportedSymbol: Symbol, usageIsTypeOnly?: boolean) => void; + writeFixes: (changeTracker: textChanges.ChangeTracker) => void; + } + + export function createImportAdder(sourceFile: SourceFile, program: Program, preferences: UserPreferences, host: LanguageServiceHost): ImportAdder { + const compilerOptions = program.getCompilerOptions(); + // Namespace fixes don't conflict, so just build a list. + const addToNamespace: FixUseNamespaceImport[] = []; + const importType: FixUseImportType[] = []; + // Keys are import clause node IDs. + const addToExisting = createMap<{ readonly importClause: ImportClause, defaultImport: string | undefined; readonly namedImports: string[], canUseTypeOnlyImport: boolean }>(); + const newImports = createMap>(); + let lastModuleSpecifier: string | undefined; + return { addImportFromDiagnostic, addImportFromExportedSymbol, writeFixes }; + + function addImportFromDiagnostic(diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) { + const info = getFixesInfo(context, diagnostic.code, diagnostic.start); + if (!info || !info.fixes.length) return; + addImport(info); + } + + function addImportFromExportedSymbol(exportedSymbol: Symbol, usageIsTypeOnly?: boolean) { + const moduleSymbol = Debug.assertDefined(exportedSymbol.parent); + const symbolName = getNameForExportedSymbol(exportedSymbol, getEmitScriptTarget(compilerOptions)); + const checker = program.getTypeChecker(); + const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker)); + const exportInfos = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, sourceFile, compilerOptions, checker, program.getSourceFiles()); + const preferTypeOnlyImport = !!usageIsTypeOnly && compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error; + const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, /*position*/ undefined, preferTypeOnlyImport, host, preferences); + addImport({ fixes: [fix], symbolName }); + } + + function addImport(info: FixesInfo) { + const { fixes, symbolName } = info; + const fix = first(fixes); + switch (fix.kind) { + case ImportFixKind.UseNamespace: + addToNamespace.push(fix); + break; + case ImportFixKind.ImportType: + importType.push(fix); + break; + case ImportFixKind.AddToExisting: { + const { importClause, importKind, canUseTypeOnlyImport } = fix; + const key = String(getNodeId(importClause)); + let entry = addToExisting.get(key); + if (!entry) { + addToExisting.set(key, entry = { importClause, defaultImport: undefined, namedImports: [], canUseTypeOnlyImport }); } - case ImportFixKind.AddNew: { - const { moduleSpecifier, importKind } = fix; - let entry = newImports.get(moduleSpecifier); - if (!entry) { - newImports.set(moduleSpecifier, entry = { defaultImport: undefined, namedImports: [], namespaceLikeImport: undefined }); - lastModuleSpecifier = moduleSpecifier; - } - switch (importKind) { - case ImportKind.Default: - Debug.assert(entry.defaultImport === undefined || entry.defaultImport === symbolName, "(Add new) Default import should be missing or match symbolName"); - entry.defaultImport = symbolName; - break; - case ImportKind.Named: - pushIfUnique(entry.namedImports, symbolName); - break; - case ImportKind.Equals: - case ImportKind.Namespace: - Debug.assert(entry.namespaceLikeImport === undefined || entry.namespaceLikeImport.name === symbolName, "Namespacelike import shoudl be missing or match symbolName"); - entry.namespaceLikeImport = { importKind, name: symbolName }; - break; - } - break; + if (importKind === ImportKind.Named) { + pushIfUnique(entry.namedImports, symbolName); } - default: - Debug.assertNever(fix, `fix wasn't never - got kind ${(fix as ImportFix).kind}`); - } - }); - - return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { - const quotePreference = getQuotePreference(sourceFile, preferences); - for (const fix of addToNamespace) { - addNamespaceQualifier(changes, sourceFile, fix); + else { + Debug.assert(entry.defaultImport === undefined || entry.defaultImport === symbolName, "(Add to Existing) Default import should be missing or match symbolName"); + entry.defaultImport = symbolName; + } + break; } - for (const fix of importType) { - addImportType(changes, sourceFile, fix, quotePreference); + case ImportFixKind.AddNew: { + const { moduleSpecifier, importKind, typeOnly } = fix; + let entry = newImports.get(moduleSpecifier); + if (!entry) { + newImports.set(moduleSpecifier, entry = { defaultImport: undefined, namedImports: [], namespaceLikeImport: undefined, typeOnly }); + lastModuleSpecifier = moduleSpecifier; + } + else { + // An import clause can only be type-only if every import fix contributing to it can be type-only. + entry.typeOnly = entry.typeOnly && typeOnly; + } + switch (importKind) { + case ImportKind.Default: + Debug.assert(entry.defaultImport === undefined || entry.defaultImport === symbolName, "(Add new) Default import should be missing or match symbolName"); + entry.defaultImport = symbolName; + break; + case ImportKind.Named: + pushIfUnique(entry.namedImports, symbolName); + break; + case ImportKind.Equals: + case ImportKind.Namespace: + Debug.assert(entry.namespaceLikeImport === undefined || entry.namespaceLikeImport.name === symbolName, "Namespacelike import shoudl be missing or match symbolName"); + entry.namespaceLikeImport = { importKind, name: symbolName }; + break; + } + break; } - addToExisting.forEach(({ importClause, defaultImport, namedImports, canUseTypeOnlyImport }) => { - doAddExistingFix(changes, sourceFile, importClause, defaultImport, namedImports, canUseTypeOnlyImport); - }); - newImports.forEach((imports, moduleSpecifier) => { - addNewImports(changes, sourceFile, moduleSpecifier, quotePreference, imports, /*blankLineBetween*/ lastModuleSpecifier === moduleSpecifier); - }); - })); - }, - }); + default: + Debug.assertNever(fix, `fix wasn't never - got kind ${(fix as ImportFix).kind}`); + } + } + + function writeFixes(changeTracker: textChanges.ChangeTracker) { + const quotePreference = getQuotePreference(sourceFile, preferences); + for (const fix of addToNamespace) { + addNamespaceQualifier(changeTracker, sourceFile, fix); + } + for (const fix of importType) { + addImportType(changeTracker, sourceFile, fix, quotePreference); + } + addToExisting.forEach(({ importClause, defaultImport, namedImports, canUseTypeOnlyImport }) => { + doAddExistingFix(changeTracker, sourceFile, importClause, defaultImport, namedImports, canUseTypeOnlyImport); + }); + newImports.forEach((imports, moduleSpecifier) => { + addNewImports(changeTracker, sourceFile, moduleSpecifier, quotePreference, imports, /*blankLineBetween*/ lastModuleSpecifier === moduleSpecifier); + }); + } + } // Sorted with the preferred fix coming first. const enum ImportFixKind { UseNamespace, ImportType, AddToExisting, AddNew } @@ -132,6 +163,7 @@ namespace ts.codefix { readonly kind: ImportFixKind.AddNew; readonly moduleSpecifier: string; readonly importKind: ImportKind; + readonly typeOnly: boolean; } const enum ImportKind { @@ -167,14 +199,20 @@ namespace ts.codefix { position: number, preferences: UserPreferences, ): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } { - const exportInfos = getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, sourceFile, program.getCompilerOptions(), program.getTypeChecker(), program.getSourceFiles()); - Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol), "Some exportInfo should match the specified moduleSymbol"); - // We sort the best codefixes first, so taking `first` is best for completions. - const moduleSpecifier = first(getNewImportInfos(program, sourceFile, position, exportInfos, host, preferences)).moduleSpecifier; - const fix = first(getFixForImport(exportInfos, symbolName, position, program, sourceFile, host, preferences)); + const compilerOptions = program.getCompilerOptions(); + const exportInfos = getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, sourceFile, compilerOptions, program.getTypeChecker(), program.getSourceFiles()); + const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position)); + const moduleSpecifier = first(getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, exportInfos, host, preferences)).moduleSpecifier; + const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, preferTypeOnlyImport, host, preferences); return { moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) }; } + function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, preferTypeOnlyImport: boolean, host: LanguageServiceHost, preferences: UserPreferences) { + Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol), "Some exportInfo should match the specified moduleSymbol"); + // We sort the best codefixes first, so taking `first` is best. + return first(getFixForImport(exportInfos, symbolName, position, preferTypeOnlyImport, program, sourceFile, host, preferences)); + } + function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction { return { description, changes, commands }; } @@ -214,6 +252,7 @@ namespace ts.codefix { symbolName: string, /** undefined only for missing JSX namespace */ position: number | undefined, + preferTypeOnlyImport: boolean, program: Program, sourceFile: SourceFile, host: LanguageServiceHost, @@ -224,7 +263,7 @@ namespace ts.codefix { const useNamespace = position === undefined ? undefined : tryUseExistingNamespaceImport(existingImports, symbolName, position, checker); const addToExisting = tryAddToExistingImport(existingImports, position !== undefined && isTypeOnlyPosition(sourceFile, position)); // Don't bother providing an action to add a new import if we can add to an existing one. - const addImport = addToExisting ? [addToExisting] : getFixesForAddImport(exportInfos, existingImports, program, sourceFile, position, host, preferences); + const addImport = addToExisting ? [addToExisting] : getFixesForAddImport(exportInfos, existingImports, program, sourceFile, position, preferTypeOnlyImport, host, preferences); return [...(useNamespace ? [useNamespace] : emptyArray), ...addImport]; } @@ -287,6 +326,7 @@ namespace ts.codefix { program: Program, sourceFile: SourceFile, position: number | undefined, + preferTypeOnlyImport: boolean, moduleSymbols: readonly SymbolExportInfo[], host: LanguageServiceHost, preferences: UserPreferences, @@ -300,7 +340,7 @@ namespace ts.codefix { // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types. exportedSymbolIsTypeOnly && isJs ? { kind: ImportFixKind.ImportType, moduleSpecifier, position: Debug.assertDefined(position, "position should be defined") } - : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind })); + : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, typeOnly: preferTypeOnlyImport })); // Sort by presence in package.json, then shortest paths first return sort(choicesForEachExportingModule, (a, b) => { @@ -322,21 +362,22 @@ namespace ts.codefix { program: Program, sourceFile: SourceFile, position: number | undefined, + preferTypeOnlyImport: boolean, host: LanguageServiceHost, preferences: UserPreferences, ): readonly (FixAddNewImport | FixUseImportType)[] { - const existingDeclaration = firstDefined(existingImports, newImportInfoFromExistingSpecifier); - return existingDeclaration ? [existingDeclaration] : getNewImportInfos(program, sourceFile, position, exportInfos, host, preferences); + const existingDeclaration = firstDefined(existingImports, info => newImportInfoFromExistingSpecifier(info, preferTypeOnlyImport)); + return existingDeclaration ? [existingDeclaration] : getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, exportInfos, host, preferences); } - function newImportInfoFromExistingSpecifier({ declaration, importKind }: FixAddToExistingImportInfo): FixAddNewImport | undefined { + function newImportInfoFromExistingSpecifier({ declaration, importKind }: FixAddToExistingImportInfo, preferTypeOnlyImport: boolean): FixAddNewImport | undefined { const expression = declaration.kind === SyntaxKind.ImportDeclaration ? declaration.moduleSpecifier : declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference ? declaration.moduleReference.expression : undefined; return expression && isStringLiteral(expression) - ? { kind: ImportFixKind.AddNew, moduleSpecifier: expression.text, importKind } + ? { kind: ImportFixKind.AddNew, moduleSpecifier: expression.text, importKind, typeOnly: preferTypeOnlyImport } : undefined; } @@ -356,7 +397,7 @@ namespace ts.codefix { const symbol = checker.getAliasedSymbol(umdSymbol); const symbolName = umdSymbol.name; const exportInfos: readonly SymbolExportInfo[] = [{ moduleSymbol: symbol, importKind: getUmdImportKind(sourceFile, program.getCompilerOptions()), exportedSymbolIsTypeOnly: false }]; - const fixes = getFixForImport(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, program, sourceFile, host, preferences); + const fixes = getFixForImport(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, program, sourceFile, host, preferences); return { fixes, symbolName }; } function getUmdSymbol(token: Node, checker: TypeChecker): Symbol | undefined { @@ -410,9 +451,10 @@ namespace ts.codefix { // "default" is a keyword and not a legal identifier for the import, so we don't expect it here Debug.assert(symbolName !== InternalSymbolName.Default, "'default' isn't a legal identifier and couldn't occur here"); + const preferTypeOnlyImport = program.getCompilerOptions().importsNotUsedAsValues === ImportsNotUsedAsValues.Error && isValidTypeOnlyAliasUseSite(symbolToken); const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program, host); const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) => - getFixForImport(exportInfos, symbolName, symbolToken.getStart(sourceFile), program, sourceFile, host, preferences))); + getFixForImport(exportInfos, symbolName, symbolToken.getStart(sourceFile), preferTypeOnlyImport, program, sourceFile, host, preferences))); return { fixes, symbolName }; } @@ -546,10 +588,10 @@ namespace ts.codefix { return [importKind === ImportKind.Default ? Diagnostics.Add_default_import_0_to_existing_import_declaration_from_1 : Diagnostics.Add_0_to_existing_import_declaration_from_1, symbolName, moduleSpecifierWithoutQuotes]; // you too! } case ImportFixKind.AddNew: { - const { importKind, moduleSpecifier } = fix; - addNewImports(changes, sourceFile, moduleSpecifier, quotePreference, importKind === ImportKind.Default ? { defaultImport: symbolName, namedImports: emptyArray, namespaceLikeImport: undefined } - : importKind === ImportKind.Named ? { defaultImport: undefined, namedImports: [symbolName], namespaceLikeImport: undefined } - : { defaultImport: undefined, namedImports: emptyArray, namespaceLikeImport: { importKind, name: symbolName } }, /*blankLineBetween*/ true); + const { importKind, moduleSpecifier, typeOnly } = fix; + addNewImports(changes, sourceFile, moduleSpecifier, quotePreference, importKind === ImportKind.Default ? { defaultImport: symbolName, namedImports: emptyArray, namespaceLikeImport: undefined, typeOnly } + : importKind === ImportKind.Named ? { defaultImport: undefined, namedImports: [symbolName], namespaceLikeImport: undefined, typeOnly } + : { defaultImport: undefined, namedImports: emptyArray, namespaceLikeImport: { importKind, name: symbolName }, typeOnly }, /*blankLineBetween*/ true); return [importKind === ImportKind.Default ? Diagnostics.Import_default_0_from_module_1 : Diagnostics.Import_0_from_module_1, symbolName, moduleSpecifier]; } default: @@ -603,6 +645,7 @@ namespace ts.codefix { } interface ImportsCollection { + readonly typeOnly: boolean; readonly defaultImport: string | undefined; readonly namedImports: string[]; readonly namespaceLikeImport: { @@ -610,13 +653,13 @@ namespace ts.codefix { readonly name: string; } | undefined; } - function addNewImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, moduleSpecifier: string, quotePreference: QuotePreference, { defaultImport, namedImports, namespaceLikeImport }: ImportsCollection, blankLineBetween: boolean): void { + function addNewImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, moduleSpecifier: string, quotePreference: QuotePreference, { defaultImport, namedImports, namespaceLikeImport, typeOnly }: ImportsCollection, blankLineBetween: boolean): void { const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference); if (defaultImport !== undefined || namedImports.length) { insertImport(changes, sourceFile, makeImport( defaultImport === undefined ? undefined : createIdentifier(defaultImport), - namedImports.map(n => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(n))), moduleSpecifier, quotePreference), /*blankLineBetween*/ blankLineBetween); + namedImports.map(n => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(n))), moduleSpecifier, quotePreference, typeOnly), /*blankLineBetween*/ blankLineBetween); } if (namespaceLikeImport) { insertImport( @@ -624,7 +667,7 @@ namespace ts.codefix { sourceFile, namespaceLikeImport.importKind === ImportKind.Equals ? createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createIdentifier(namespaceLikeImport.name), createExternalModuleReference(quotedModuleSpecifier)) : namespaceLikeImport.importKind === ImportKind.ConstEquals ? createConstEqualsRequireDeclaration(namespaceLikeImport.name, quotedModuleSpecifier) : - createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(namespaceLikeImport.name))), quotedModuleSpecifier), /*blankLineBetween*/ blankLineBetween); + createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(namespaceLikeImport.name)), typeOnly), quotedModuleSpecifier), /*blankLineBetween*/ blankLineBetween); } } diff --git a/src/services/codefixes/inferFromUsage.ts b/src/services/codefixes/inferFromUsage.ts index 5718f592cb82a..888128834046c 100644 --- a/src/services/codefixes/inferFromUsage.ts +++ b/src/services/codefixes/inferFromUsage.ts @@ -49,21 +49,21 @@ namespace ts.codefix { registerCodeFix({ errorCodes, getCodeActions(context) { - const { sourceFile, program, span: { start }, errorCode, cancellationToken, host, formatContext, preferences } = context; + const { sourceFile, program, span: { start }, errorCode, cancellationToken, host, preferences } = context; const token = getTokenAtPosition(sourceFile, start); - let declaration!: Declaration | undefined; - const changes = textChanges.ChangeTracker.with(context, changes => { declaration = doChange(changes, sourceFile, token, errorCode, program, cancellationToken, /*markSeen*/ returnTrue, host, formatContext, preferences); }); + let declaration: Declaration | undefined; + const changes = textChanges.ChangeTracker.with(context, changes => { declaration = doChange(changes, sourceFile, token, errorCode, program, cancellationToken, /*markSeen*/ returnTrue, host, preferences); }); const name = declaration && getNameOfDeclaration(declaration); return !name || changes.length === 0 ? undefined : [createCodeFixAction(fixId, changes, [getDiagnostic(errorCode, token), name.getText(sourceFile)], fixId, Diagnostics.Infer_all_types_from_usage)]; }, fixIds: [fixId], getAllCodeActions(context) { - const { sourceFile, program, cancellationToken, host, formatContext, preferences } = context; + const { sourceFile, program, cancellationToken, host, preferences } = context; const markSeen = nodeSeenTracker(); return codeFixAll(context, errorCodes, (changes, err) => { - doChange(changes, sourceFile, getTokenAtPosition(err.file, err.start), err.code, program, cancellationToken, markSeen, host, formatContext, preferences); + doChange(changes, sourceFile, getTokenAtPosition(err.file, err.start), err.code, program, cancellationToken, markSeen, host, preferences); }); }, }); @@ -106,19 +106,21 @@ namespace ts.codefix { return errorCode; } - function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, errorCode: number, program: Program, cancellationToken: CancellationToken, markSeen: NodeSeenTracker, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): Declaration | undefined { + function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, errorCode: number, program: Program, cancellationToken: CancellationToken, markSeen: NodeSeenTracker, host: LanguageServiceHost, preferences: UserPreferences): Declaration | undefined { if (!isParameterPropertyModifier(token.kind) && token.kind !== SyntaxKind.Identifier && token.kind !== SyntaxKind.DotDotDotToken && token.kind !== SyntaxKind.ThisKeyword) { return undefined; } const { parent } = token; + const importAdder = createImportAdder(sourceFile, program, preferences, host); errorCode = mapSuggestionDiagnostic(errorCode); switch (errorCode) { // Variable and Property declarations case Diagnostics.Member_0_implicitly_has_an_1_type.code: case Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined.code: if ((isVariableDeclaration(parent) && markSeen(parent)) || isPropertyDeclaration(parent) || isPropertySignature(parent)) { // handle bad location - annotateVariableDeclaration(changes, sourceFile, parent, program, host, cancellationToken, formatContext, preferences); + annotateVariableDeclaration(changes, importAdder, sourceFile, parent, program, host, cancellationToken); + importAdder.writeFixes(changes); return parent; } if (isPropertyAccessExpression(parent)) { @@ -129,6 +131,7 @@ namespace ts.codefix { const typeTag = createJSDocTypeTag(createJSDocTypeExpression(typeNode), /*comment*/ ""); addJSDocTags(changes, sourceFile, cast(parent.parent.parent, isExpressionStatement), [typeTag]); } + importAdder.writeFixes(changes); return parent; } return undefined; @@ -136,7 +139,8 @@ namespace ts.codefix { case Diagnostics.Variable_0_implicitly_has_an_1_type.code: { const symbol = program.getTypeChecker().getSymbolAtLocation(token); if (symbol && symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && markSeen(symbol.valueDeclaration)) { - annotateVariableDeclaration(changes, sourceFile, symbol.valueDeclaration, program, host, cancellationToken, formatContext, preferences); + annotateVariableDeclaration(changes, importAdder, sourceFile, symbol.valueDeclaration, program, host, cancellationToken); + importAdder.writeFixes(changes); return symbol.valueDeclaration; } return undefined; @@ -148,77 +152,80 @@ namespace ts.codefix { return undefined; } + let declaration: Declaration | undefined; switch (errorCode) { // Parameter declarations case Diagnostics.Parameter_0_implicitly_has_an_1_type.code: if (isSetAccessorDeclaration(containingFunction)) { - annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken, formatContext, preferences); - return containingFunction; + annotateSetAccessor(changes, importAdder, sourceFile, containingFunction, program, host, cancellationToken); + declaration = containingFunction; + break; } // falls through case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code: if (markSeen(containingFunction)) { const param = cast(parent, isParameter); - annotateParameters(changes, sourceFile, param, containingFunction, program, host, cancellationToken, formatContext, preferences); - return param; + annotateParameters(changes, importAdder, sourceFile, param, containingFunction, program, host, cancellationToken); + declaration = param; } - return undefined; + break; // Get Accessor declarations case Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code: case Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code: if (isGetAccessorDeclaration(containingFunction) && isIdentifier(containingFunction.name)) { - annotate(changes, sourceFile, containingFunction, inferTypeForVariableFromUsage(containingFunction.name, program, cancellationToken), program, host, formatContext, preferences); - return containingFunction; + annotate(changes, importAdder, sourceFile, containingFunction, inferTypeForVariableFromUsage(containingFunction.name, program, cancellationToken), program, host); + declaration = containingFunction; } - return undefined; + break; // Set Accessor declarations case Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code: if (isSetAccessorDeclaration(containingFunction)) { - annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken, formatContext, preferences); - return containingFunction; + annotateSetAccessor(changes, importAdder, sourceFile, containingFunction, program, host, cancellationToken); + declaration = containingFunction; } - return undefined; + break; // Function 'this' case Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation.code: if (textChanges.isThisTypeAnnotatable(containingFunction) && markSeen(containingFunction)) { annotateThis(changes, sourceFile, containingFunction, program, host, cancellationToken); - return containingFunction; + declaration = containingFunction; } - return undefined; + break; default: return Debug.fail(String(errorCode)); } + + importAdder.writeFixes(changes); + return declaration; } function annotateVariableDeclaration( changes: textChanges.ChangeTracker, + importAdder: ImportAdder, sourceFile: SourceFile, declaration: VariableDeclaration | PropertyDeclaration | PropertySignature, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken, - formatContext: formatting.FormatContext, - preferences: UserPreferences, ): void { if (isIdentifier(declaration.name)) { - annotate(changes, sourceFile, declaration, inferTypeForVariableFromUsage(declaration.name, program, cancellationToken), program, host, formatContext, preferences); + annotate(changes, importAdder, sourceFile, declaration, inferTypeForVariableFromUsage(declaration.name, program, cancellationToken), program, host); } } function annotateParameters( changes: textChanges.ChangeTracker, + importAdder: ImportAdder, sourceFile: SourceFile, parameterDeclaration: ParameterDeclaration, containingFunction: FunctionLike, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken, - formatContext: formatting.FormatContext, - preferences: UserPreferences, ): void { if (!isIdentifier(parameterDeclaration.name)) { return; @@ -235,7 +242,7 @@ namespace ts.codefix { if (needParens) changes.insertNodeBefore(sourceFile, first(containingFunction.parameters), createToken(SyntaxKind.OpenParenToken)); for (const { declaration, type } of parameterInferences) { if (declaration && !declaration.type && !declaration.initializer) { - annotate(changes, sourceFile, declaration, type, program, host, formatContext, preferences); + annotate(changes, importAdder, sourceFile, declaration, type, program, host); } } if (needParens) changes.insertNodeAfter(sourceFile, last(containingFunction.parameters), createToken(SyntaxKind.CloseParenToken)); @@ -269,13 +276,12 @@ namespace ts.codefix { function annotateSetAccessor( changes: textChanges.ChangeTracker, + importAdder: ImportAdder, sourceFile: SourceFile, setAccessorDeclaration: SetAccessorDeclaration, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken, - formatContext: formatting.FormatContext, - preferences: UserPreferences, ): void { const param = firstOrUndefined(setAccessorDeclaration.parameters); if (param && isIdentifier(setAccessorDeclaration.name) && isIdentifier(param.name)) { @@ -287,12 +293,12 @@ namespace ts.codefix { annotateJSDocParameters(changes, sourceFile, [{ declaration: param, type }], program, host); } else { - annotate(changes, sourceFile, param, type, program, host, formatContext, preferences); + annotate(changes, importAdder, sourceFile, param, type, program, host); } } } - function annotate(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type, program: Program, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): void { + function annotate(changes: textChanges.ChangeTracker, importAdder: ImportAdder, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type, program: Program, host: LanguageServiceHost): void { const typeNode = getTypeNodeIfAccessible(type, declaration, program, host); if (typeNode) { if (isInJSFile(sourceFile) && declaration.kind !== SyntaxKind.PropertySignature) { @@ -304,38 +310,25 @@ namespace ts.codefix { const typeTag = isGetAccessorDeclaration(declaration) ? createJSDocReturnTag(typeExpression, "") : createJSDocTypeTag(typeExpression, ""); addJSDocTags(changes, sourceFile, parent, [typeTag]); } - else if (!tryReplaceImportTypeNodeWithAutoImport(typeNode, changes, sourceFile, declaration, type, program, host, formatContext, preferences)) { + else if (!tryReplaceImportTypeNodeWithAutoImport(typeNode, declaration, type, sourceFile, changes, importAdder, getEmitScriptTarget(program.getCompilerOptions()))) { changes.tryInsertTypeAnnotation(sourceFile, declaration, typeNode); } } } - function tryReplaceImportTypeNodeWithAutoImport(typeNode: TypeNode, changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type, program: Program, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): boolean { - if (isLiteralImportTypeNode(typeNode) && typeNode.qualifier && type.symbol) { - // Replace 'import("./a").SomeType' with 'SomeType' and an actual import if possible - const moduleSymbol = find(type.symbol.declarations, d => !!d.getSourceFile().externalModuleIndicator)?.getSourceFile().symbol; - // Symbol for the left-most thing after the dot - if (moduleSymbol) { - const symbol = getFirstIdentifier(typeNode.qualifier).symbol; - const action = getImportCompletionAction( - symbol, - moduleSymbol, - sourceFile, - symbol.name, - host, - program, - formatContext, - declaration.pos, - preferences, - ); - if (action.codeAction.changes.length && changes.tryInsertTypeAnnotation(sourceFile, declaration, createTypeReferenceNode(typeNode.qualifier, typeNode.typeArguments))) { - for (const change of action.codeAction.changes) { - const file = sourceFile.fileName === change.fileName ? sourceFile : Debug.assertDefined(program.getSourceFile(change.fileName)); - changes.pushRaw(file, change); - } - return true; - } - } + function tryReplaceImportTypeNodeWithAutoImport( + typeNode: TypeNode, + declaration: textChanges.TypeAnnotatable, + type: Type, + sourceFile: SourceFile, + changes: textChanges.ChangeTracker, + importAdder: ImportAdder, + scriptTarget: ScriptTarget + ): boolean { + const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(typeNode, type, scriptTarget); + if (importableReference && changes.tryInsertTypeAnnotation(sourceFile, declaration, importableReference.typeReference)) { + forEach(importableReference.symbols, s => importAdder.addImportFromExportedSymbol(s, /*usageIsTypeOnly*/ true)); + return true; } return false; } diff --git a/src/services/completions.ts b/src/services/completions.ts index fdbc01ce503df..e7e06cb7dfc71 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -40,8 +40,8 @@ namespace ts.Completions { return !!(origin.kind & SymbolOriginInfoKind.SymbolMember); } - function originIsExport(origin: SymbolOriginInfo): origin is SymbolOriginInfoExport { - return !!(origin.kind & SymbolOriginInfoKind.Export); + function originIsExport(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoExport { + return !!(origin && origin.kind & SymbolOriginInfoKind.Export); } function originIsPromise(origin: SymbolOriginInfo): boolean { @@ -559,16 +559,6 @@ namespace ts.Completions { }) || { type: "none" }; } - function getSymbolName(symbol: Symbol, origin: SymbolOriginInfo | undefined, target: ScriptTarget): string { - return origin && originIsExport(origin) && ( - (origin.isDefaultExport && symbol.escapedName === InternalSymbolName.Default) || - (symbol.escapedName === InternalSymbolName.ExportEquals)) - // Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase. - ? firstDefined(symbol.declarations, d => isExportAssignment(d) && isIdentifier(d.expression) ? d.expression.text : undefined) - || codefix.moduleSymbolToValidIdentifier(origin.moduleSymbol, target) - : symbol.name; - } - export interface CompletionEntryIdentifier { name: string; source?: string; @@ -671,7 +661,7 @@ namespace ts.Completions { exportedSymbol, moduleSymbol, sourceFile, - getSymbolName(symbol, symbolOriginInfo, compilerOptions.target!), + getNameForExportedSymbol(symbol, compilerOptions.target!), host, program, formatContext, @@ -1632,7 +1622,7 @@ namespace ts.Completions { const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport }; results.push({ symbol, - symbolName: getSymbolName(symbol, origin, target), + symbolName: getNameForExportedSymbol(symbol, target), origin, skipFilter, }); @@ -2392,7 +2382,7 @@ namespace ts.Completions { origin: SymbolOriginInfo | undefined, kind: CompletionKind, ): CompletionEntryDisplayNameForSymbol | undefined { - const name = getSymbolName(symbol, origin, target); + const name = originIsExport(origin) ? getNameForExportedSymbol(symbol, target) : symbol.name; if (name === undefined // If the symbol is external module, don't show it in the completion list // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 655d6d890bad5..d94106486446d 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2748,5 +2748,14 @@ namespace ts { return isArray(valueOrArray) ? first(valueOrArray) : valueOrArray; } + export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget) { + if (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default) { + // Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase. + return firstDefined(symbol.declarations, d => isExportAssignment(d) && isIdentifier(d.expression) ? d.expression.text : undefined) + || codefix.moduleSymbolToValidIdentifier(Debug.assertDefined(symbol.parent), scriptTarget); + } + return symbol.name; + } + // #endregion } diff --git a/tests/cases/fourslash/codeFixClassImplementInterfaceAutoImports.ts b/tests/cases/fourslash/codeFixClassImplementInterfaceAutoImports.ts new file mode 100644 index 0000000000000..3a08ffa5e5a2a --- /dev/null +++ b/tests/cases/fourslash/codeFixClassImplementInterfaceAutoImports.ts @@ -0,0 +1,40 @@ +/// + +// @Filename: types1.ts +////type A = {}; +////export default A; + +// @Filename: types2.ts +////export type B = {}; +////export type C = {}; +////export type D = {}; + +// @Filename: interface.ts +////import A from './types1'; +////import { B, C, D } from './types2'; +//// +////export interface Base { +//// a: A; +//// b(p1: C): D; +////} + +// @Filename: index.ts +////import { Base } from './interface'; +//// +////export class C implements Base {[| |]} + +goTo.file('index.ts'); +verify.codeFix({ + description: "Implement interface 'Base'", + newFileContent: +`import { Base } from './interface'; +import A from './types1'; +import { B, C, D } from './types2'; + +export class C implements Base { + a: A; + b(p1: C): D { + throw new Error("Method not implemented."); + } +}`, +}); diff --git a/tests/cases/fourslash/codeFixClassImplementInterfaceAutoImports_typeOnly.ts b/tests/cases/fourslash/codeFixClassImplementInterfaceAutoImports_typeOnly.ts new file mode 100644 index 0000000000000..9f261a72c2aed --- /dev/null +++ b/tests/cases/fourslash/codeFixClassImplementInterfaceAutoImports_typeOnly.ts @@ -0,0 +1,42 @@ +/// + +// @importsNotUsedAsValues: error + +// @Filename: types1.ts +////type A = {}; +////export default A; + +// @Filename: types2.ts +////export type B = {}; +////export type C = {}; +////export type D = {}; + +// @Filename: interface.ts +////import type A from './types1'; +////import type { B, C, D } from './types2'; +//// +////export interface Base { +//// a: A; +//// b(p1: C): D; +////} + +// @Filename: index.ts +////import type { Base } from './interface'; +//// +////export class C implements Base {[| |]} + +goTo.file('index.ts'); +verify.codeFix({ + description: "Implement interface 'Base'", + newFileContent: +`import type { Base } from './interface'; +import type A from './types1'; +import type { B, C, D } from './types2'; + +export class C implements Base { + a: A; + b(p1: C): D { + throw new Error("Method not implemented."); + } +}`, +}); 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/codeFixInferFromUsageContextualImport4.ts b/tests/cases/fourslash/codeFixInferFromUsageContextualImport4.ts new file mode 100644 index 0000000000000..fbb6336a869b0 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageContextualImport4.ts @@ -0,0 +1,34 @@ +/// + +// @strict: true +// @noImplicitAny: true +// @noLib: true + +// @Filename: /getEmail.ts +////import { User, Settings } from './a'; +////export declare function getEmail(user: User, settings: Settings): string; + +// @Filename: /a.ts +////export interface User {} +////export interface Settings {} + +// @Filename: /b.ts +////import { getEmail } from './getEmail'; +//// +////export function f([|user|], settings) { +//// getEmail(user, settings); +////} + +goTo.file("/b.ts"); + +verify.codeFix({ + index: 0, + description: "Infer parameter types from usage", + newFileContent: +`import { getEmail } from './getEmail'; +import { User, Settings } from './a'; + +export function f(user: User, settings: Settings) { + getEmail(user, settings); +}` +}); diff --git a/tests/cases/fourslash/importNameCodeFix_typeOnly.ts b/tests/cases/fourslash/importNameCodeFix_typeOnly.ts new file mode 100644 index 0000000000000..ad16ea4203229 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_typeOnly.ts @@ -0,0 +1,16 @@ +/// + +// @importsNotUsedAsValues: error + +// @Filename: types.ts +////export class A {} + +// @Filename: index.ts +////const a: /**/A + +goTo.marker(""); +verify.importFixAtPosition([ +`import type { A } from "./types"; + +const a: A` +]); diff --git a/tests/cases/fourslash/importNameCodeFix_typeOnly2.ts b/tests/cases/fourslash/importNameCodeFix_typeOnly2.ts new file mode 100644 index 0000000000000..534275fd6e3c2 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_typeOnly2.ts @@ -0,0 +1,19 @@ +/// + +// @importsNotUsedAsValues: error + +// @Filename: types.ts +////export class A {} + +// @Filename: index.ts +////const a: A = new A(); + +goTo.file("index.ts"); +verify.codeFixAll({ + fixAllDescription: ts.Diagnostics.Add_all_missing_imports.message, + fixId: "fixMissingImport", + newFileContent: +`import { A } from "./types"; + +const a: A = new A();` +}); diff --git a/tests/cases/fourslash/quickfixImplementInterfaceUnreachableTypeUsesRelativeImport.ts b/tests/cases/fourslash/quickfixImplementInterfaceUnreachableTypeUsesRelativeImport.ts index ba6cd702ea00f..5db9047035513 100644 --- a/tests/cases/fourslash/quickfixImplementInterfaceUnreachableTypeUsesRelativeImport.ts +++ b/tests/cases/fourslash/quickfixImplementInterfaceUnreachableTypeUsesRelativeImport.ts @@ -17,10 +17,12 @@ verify.codeFix({ index: 0, description: "Implement interface 'Foo'", newFileContent: { - "/tests/cases/fourslash/index.ts": `import { Foo } from './interface'; + "/tests/cases/fourslash/index.ts": +`import { Foo } from './interface'; +import { Class } from './class'; class X implements Foo { - x: import("./class").Class; + x: Class; }` } });