Skip to content

Commit ad8c209

Browse files
authored
Use type-only imports in auto-imports when it would be an error not to, and use auto-imports in “implement interface” fix (#36615)
* Refactor fix-all-missing-imports to be reusable by other codefixes * Migrate infer-from-usage to use ImportAdder * Add infer from usage test importing more than one thing in a single fix * Migrate implement interface / abstract members fixes to use ImportAdder * Update old tests * Use type-only imports when it would be an error not to * Add another test * Rename stuff
1 parent aece8c0 commit ad8c209

14 files changed

+489
-195
lines changed

src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ namespace ts.codefix {
4141
// so duplicates cannot occur.
4242
const abstractAndNonPrivateExtendsSymbols = checker.getPropertiesOfType(instantiatedExtendsType).filter(symbolPointsToNonPrivateAndAbstractMember);
4343

44-
createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, context, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member));
44+
const importAdder = createImportAdder(sourceFile, context.program, preferences, context.host);
45+
createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, context, preferences, importAdder, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member));
46+
importAdder.writeFixes(changeTracker);
4547
}
4648

4749
function symbolPointsToNonPrivateAndAbstractMember(symbol: Symbol): boolean {

src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ namespace ts.codefix {
6363
createMissingIndexSignatureDeclaration(implementedType, IndexKind.String);
6464
}
6565

66-
createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, context, preferences, member => insertInterfaceMemberNode(sourceFile, classDeclaration, member));
66+
const importAdder = createImportAdder(sourceFile, context.program, preferences, context.host);
67+
createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, context, preferences, importAdder, member => insertInterfaceMemberNode(sourceFile, classDeclaration, member));
68+
importAdder.writeFixes(changeTracker);
6769

6870
function createMissingIndexSignatureDeclaration(type: InterfaceType, kind: IndexKind): void {
6971
const indexInfoOfKind = checker.getIndexInfoOfType(type, kind);

src/services/codefixes/helpers.ts

+117-15
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ namespace ts.codefix {
44
* Finds members of the resolved type that are missing in the class pointed to by class decl
55
* and generates source code for the missing members.
66
* @param possiblyMissingSymbols The collection of symbols to filter and then get insertions for.
7+
* @param importAdder If provided, type annotations will use identifier type references instead of ImportTypeNodes, and the missing imports will be added to the importAdder.
78
* @returns Empty string iff there are no member insertions.
89
*/
9-
export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: readonly Symbol[], context: TypeConstructionContext, preferences: UserPreferences, out: (node: ClassElement) => void): void {
10+
export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: readonly Symbol[], context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: ClassElement) => void): void {
1011
const classMembers = classDeclaration.symbol.members!;
1112
for (const symbol of possiblyMissingSymbols) {
1213
if (!classMembers.has(symbol.escapedName)) {
13-
addNewNodeForMemberSymbol(symbol, classDeclaration, context, preferences, out);
14+
addNewNodeForMemberSymbol(symbol, classDeclaration, context, preferences, importAdder, addClassElement);
1415
}
1516
}
1617
}
@@ -19,7 +20,7 @@ namespace ts.codefix {
1920
return {
2021
directoryExists: context.host.directoryExists ? d => context.host.directoryExists!(d) : undefined,
2122
fileExists: context.host.fileExists ? f => context.host.fileExists!(f) : undefined,
22-
getCurrentDirectory: context.host.getCurrentDirectory ? () => context.host.getCurrentDirectory!() : undefined,
23+
getCurrentDirectory: context.host.getCurrentDirectory ? () => context.host.getCurrentDirectory() : undefined,
2324
readFile: context.host.readFile ? f => context.host.readFile!(f) : undefined,
2425
useCaseSensitiveFileNames: context.host.useCaseSensitiveFileNames ? () => context.host.useCaseSensitiveFileNames!() : undefined,
2526
getSourceFiles: () => context.program.getSourceFiles(),
@@ -36,19 +37,19 @@ namespace ts.codefix {
3637

3738
export interface TypeConstructionContext {
3839
program: Program;
39-
host: ModuleSpecifierResolutionHost;
40+
host: LanguageServiceHost;
4041
}
4142

4243
/**
4344
* @returns Empty string iff there we can't figure out a representation for `symbol` in `enclosingDeclaration`.
4445
*/
45-
function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, context: TypeConstructionContext, preferences: UserPreferences, out: (node: Node) => void): void {
46+
function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: Node) => void): void {
4647
const declarations = symbol.getDeclarations();
4748
if (!(declarations && declarations.length)) {
4849
return undefined;
4950
}
5051
const checker = context.program.getTypeChecker();
51-
52+
const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions());
5253
const declaration = declarations[0];
5354
const name = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName;
5455
const visibilityModifier = createVisibilityModifier(getModifierFlags(declaration));
@@ -61,8 +62,15 @@ namespace ts.codefix {
6162
case SyntaxKind.PropertySignature:
6263
case SyntaxKind.PropertyDeclaration:
6364
const flags = preferences.quotePreference === "single" ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : undefined;
64-
const typeNode = checker.typeToTypeNode(type, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context));
65-
out(createProperty(
65+
let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context));
66+
if (importAdder) {
67+
const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(typeNode, type, scriptTarget);
68+
if (importableReference) {
69+
typeNode = importableReference.typeReference;
70+
importSymbols(importAdder, importableReference.symbols);
71+
}
72+
}
73+
addClassElement(createProperty(
6674
/*decorators*/undefined,
6775
modifiers,
6876
name,
@@ -72,14 +80,21 @@ namespace ts.codefix {
7280
break;
7381
case SyntaxKind.GetAccessor:
7482
case SyntaxKind.SetAccessor: {
83+
let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context));
7584
const allAccessors = getAllAccessorDeclarations(declarations, declaration as AccessorDeclaration);
76-
const typeNode = checker.typeToTypeNode(type, enclosingDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context));
7785
const orderedAccessors = allAccessors.secondAccessor
7886
? [allAccessors.firstAccessor, allAccessors.secondAccessor]
7987
: [allAccessors.firstAccessor];
88+
if (importAdder) {
89+
const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(typeNode, type, scriptTarget);
90+
if (importableReference) {
91+
typeNode = importableReference.typeReference;
92+
importSymbols(importAdder, importableReference.symbols);
93+
}
94+
}
8095
for (const accessor of orderedAccessors) {
8196
if (isGetAccessorDeclaration(accessor)) {
82-
out(createGetAccessor(
97+
addClassElement(createGetAccessor(
8398
/*decorators*/ undefined,
8499
modifiers,
85100
name,
@@ -91,7 +106,7 @@ namespace ts.codefix {
91106
Debug.assertNode(accessor, isSetAccessorDeclaration, "The counterpart to a getter should be a setter");
92107
const parameter = getSetAccessorValueParameter(accessor);
93108
const parameterName = parameter && isIdentifier(parameter.name) ? idText(parameter.name) : undefined;
94-
out(createSetAccessor(
109+
addClassElement(createSetAccessor(
95110
/*decorators*/ undefined,
96111
modifiers,
97112
name,
@@ -134,15 +149,15 @@ namespace ts.codefix {
134149
}
135150
else {
136151
Debug.assert(declarations.length === signatures.length, "Declarations and signatures should match count");
137-
out(createMethodImplementingSignatures(signatures, name, optional, modifiers, preferences));
152+
addClassElement(createMethodImplementingSignatures(signatures, name, optional, modifiers, preferences));
138153
}
139154
}
140155
break;
141156
}
142157

143158
function outputMethod(signature: Signature, modifiers: NodeArray<Modifier> | undefined, name: PropertyName, body?: Block): void {
144-
const method = signatureToMethodDeclaration(context, signature, enclosingDeclaration, modifiers, name, optional, body);
145-
if (method) out(method);
159+
const method = signatureToMethodDeclaration(context, signature, enclosingDeclaration, modifiers, name, optional, body, importAdder);
160+
if (method) addClassElement(method);
146161
}
147162
}
148163

@@ -154,13 +169,53 @@ namespace ts.codefix {
154169
name: PropertyName,
155170
optional: boolean,
156171
body: Block | undefined,
172+
importAdder: ImportAdder | undefined,
157173
): MethodDeclaration | undefined {
158174
const program = context.program;
159-
const signatureDeclaration = <MethodDeclaration>program.getTypeChecker().signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, NodeBuilderFlags.NoTruncation | NodeBuilderFlags.SuppressAnyReturnType, getNoopSymbolTrackerWithResolver(context));
175+
const checker = program.getTypeChecker();
176+
const scriptTarget = getEmitScriptTarget(program.getCompilerOptions());
177+
const signatureDeclaration = <MethodDeclaration>checker.signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, NodeBuilderFlags.NoTruncation | NodeBuilderFlags.SuppressAnyReturnType, getNoopSymbolTrackerWithResolver(context));
160178
if (!signatureDeclaration) {
161179
return undefined;
162180
}
163181

182+
if (importAdder) {
183+
if (signatureDeclaration.typeParameters) {
184+
forEach(signatureDeclaration.typeParameters, (typeParameterDecl, i) => {
185+
const typeParameter = signature.typeParameters![i];
186+
if (typeParameterDecl.constraint) {
187+
const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(typeParameterDecl.constraint, typeParameter.constraint, scriptTarget);
188+
if (importableReference) {
189+
typeParameterDecl.constraint = importableReference.typeReference;
190+
importSymbols(importAdder, importableReference.symbols);
191+
}
192+
}
193+
if (typeParameterDecl.default) {
194+
const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(typeParameterDecl.default, typeParameter.default, scriptTarget);
195+
if (importableReference) {
196+
typeParameterDecl.default = importableReference.typeReference;
197+
importSymbols(importAdder, importableReference.symbols);
198+
}
199+
}
200+
});
201+
}
202+
forEach(signatureDeclaration.parameters, (parameterDecl, i) => {
203+
const parameter = signature.parameters[i];
204+
const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(parameterDecl.type, checker.getTypeAtLocation(parameter.valueDeclaration), scriptTarget);
205+
if (importableReference) {
206+
parameterDecl.type = importableReference.typeReference;
207+
importSymbols(importAdder, importableReference.symbols);
208+
}
209+
});
210+
if (signatureDeclaration.type) {
211+
const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(signatureDeclaration.type, signature.resolvedReturnType, scriptTarget);
212+
if (importableReference) {
213+
signatureDeclaration.type = importableReference.typeReference;
214+
importSymbols(importAdder, importableReference.symbols);
215+
}
216+
}
217+
}
218+
164219
signatureDeclaration.decorators = undefined;
165220
signatureDeclaration.modifiers = modifiers;
166221
signatureDeclaration.name = name;
@@ -359,4 +414,51 @@ namespace ts.codefix {
359414
export function findJsonProperty(obj: ObjectLiteralExpression, name: string): PropertyAssignment | undefined {
360415
return find(obj.properties, (p): p is PropertyAssignment => isPropertyAssignment(p) && !!p.name && isStringLiteral(p.name) && p.name.text === name);
361416
}
417+
418+
/**
419+
* Given an ImportTypeNode 'import("./a").SomeType<import("./b").OtherType<...>>',
420+
* returns an equivalent type reference node with any nested ImportTypeNodes also replaced
421+
* with type references, and a list of symbols that must be imported to use the type reference.
422+
*/
423+
export function tryGetAutoImportableReferenceFromImportTypeNode(importTypeNode: TypeNode | undefined, type: Type | undefined, scriptTarget: ScriptTarget) {
424+
if (importTypeNode && isLiteralImportTypeNode(importTypeNode) && importTypeNode.qualifier && (!type || type.symbol)) {
425+
// Symbol for the left-most thing after the dot
426+
const firstIdentifier = getFirstIdentifier(importTypeNode.qualifier);
427+
const name = getNameForExportedSymbol(firstIdentifier.symbol, scriptTarget);
428+
const qualifier = name !== firstIdentifier.text
429+
? replaceFirstIdentifierOfEntityName(importTypeNode.qualifier, createIdentifier(name))
430+
: importTypeNode.qualifier;
431+
432+
const symbols = [firstIdentifier.symbol];
433+
const typeArguments: TypeNode[] = [];
434+
if (importTypeNode.typeArguments) {
435+
importTypeNode.typeArguments.forEach(arg => {
436+
const ref = tryGetAutoImportableReferenceFromImportTypeNode(arg, /*undefined*/ type, scriptTarget);
437+
if (ref) {
438+
symbols.push(...ref.symbols);
439+
typeArguments.push(ref.typeReference);
440+
}
441+
else {
442+
typeArguments.push(arg);
443+
}
444+
});
445+
}
446+
447+
return {
448+
symbols,
449+
typeReference: createTypeReferenceNode(qualifier, typeArguments)
450+
};
451+
}
452+
}
453+
454+
function replaceFirstIdentifierOfEntityName(name: EntityName, newIdentifier: Identifier): EntityName {
455+
if (name.kind === SyntaxKind.Identifier) {
456+
return newIdentifier;
457+
}
458+
return createQualifiedName(replaceFirstIdentifierOfEntityName(name.left, newIdentifier), name.right);
459+
}
460+
461+
function importSymbols(importAdder: ImportAdder, symbols: readonly Symbol[]) {
462+
symbols.forEach(s => importAdder.addImportFromExportedSymbol(s, /*usageIsTypeOnly*/ true));
463+
}
362464
}

0 commit comments

Comments
 (0)