Skip to content

Commit 3d2bf6a

Browse files
authored
Fix implement interface quickfix import types (#29410)
* Pass module specifier resolution host thru types constructed by implements quickfixes * Add regression test * Fix scope node for generated methods, fix lints
1 parent dc0f4af commit 3d2bf6a

8 files changed

+83
-26
lines changed

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3037,8 +3037,10 @@ namespace ts {
30373037
/* @internal */ typeToTypeNode(type: Type, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined; // tslint:disable-line unified-signatures
30383038
/** Note that the resulting nodes cannot be checked. */
30393039
signatureToSignatureDeclaration(signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): SignatureDeclaration & {typeArguments?: NodeArray<TypeNode>} | undefined;
3040+
/* @internal */ signatureToSignatureDeclaration(signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker): SignatureDeclaration & {typeArguments?: NodeArray<TypeNode>} | undefined; // tslint:disable-line unified-signatures
30403041
/** Note that the resulting nodes cannot be checked. */
30413042
indexInfoToIndexSignatureDeclaration(indexInfo: IndexInfo, kind: IndexKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): IndexSignatureDeclaration | undefined;
3043+
/* @internal */ indexInfoToIndexSignatureDeclaration(indexInfo: IndexInfo, kind: IndexKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker): IndexSignatureDeclaration | undefined; // tslint:disable-line unified-signatures
30423044
/** Note that the resulting nodes cannot be checked. */
30433045
symbolToEntityName(symbol: Symbol, meaning: SymbolFlags, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): EntityName | undefined;
30443046
/** Note that the resulting nodes cannot be checked. */

src/services/codefixes/fixAddMissingMember.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ namespace ts.codefix {
275275
inJs: boolean,
276276
preferences: UserPreferences,
277277
): void {
278-
const methodDeclaration = createMethodFromCallExpression(context, callExpression, token.text, inJs, makeStatic, preferences, !isInterfaceDeclaration(typeDecl));
278+
const methodDeclaration = createMethodFromCallExpression(context, callExpression, token.text, inJs, makeStatic, preferences, typeDecl);
279279
const containingMethodDeclaration = getAncestor(callExpression, SyntaxKind.MethodDeclaration);
280280

281281
if (containingMethodDeclaration && containingMethodDeclaration.parent === typeDecl) {

src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ namespace ts.codefix {
88
registerCodeFix({
99
errorCodes,
1010
getCodeActions(context) {
11-
const { program, sourceFile, span } = context;
11+
const { sourceFile, span } = context;
1212
const changes = textChanges.ChangeTracker.with(context, t =>
13-
addMissingMembers(getClass(sourceFile, span.start), sourceFile, program.getTypeChecker(), t, context.preferences));
13+
addMissingMembers(getClass(sourceFile, span.start), sourceFile, context, t, context.preferences));
1414
return changes.length === 0 ? undefined : [createCodeFixAction(fixId, changes, Diagnostics.Implement_inherited_abstract_class, fixId, Diagnostics.Implement_all_inherited_abstract_classes)];
1515
},
1616
fixIds: [fixId],
@@ -19,7 +19,7 @@ namespace ts.codefix {
1919
return codeFixAll(context, errorCodes, (changes, diag) => {
2020
const classDeclaration = getClass(diag.file, diag.start);
2121
if (addToSeen(seenClassDeclarations, getNodeId(classDeclaration))) {
22-
addMissingMembers(classDeclaration, context.sourceFile, context.program.getTypeChecker(), changes, context.preferences);
22+
addMissingMembers(classDeclaration, context.sourceFile, context, changes, context.preferences);
2323
}
2424
});
2525
},
@@ -32,15 +32,16 @@ namespace ts.codefix {
3232
return cast(token.parent, isClassLike);
3333
}
3434

35-
function addMissingMembers(classDeclaration: ClassLikeDeclaration, sourceFile: SourceFile, checker: TypeChecker, changeTracker: textChanges.ChangeTracker, preferences: UserPreferences): void {
35+
function addMissingMembers(classDeclaration: ClassLikeDeclaration, sourceFile: SourceFile, context: TypeConstructionContext, changeTracker: textChanges.ChangeTracker, preferences: UserPreferences): void {
3636
const extendsNode = getEffectiveBaseTypeNode(classDeclaration)!;
37+
const checker = context.program.getTypeChecker();
3738
const instantiatedExtendsType = checker.getTypeAtLocation(extendsNode);
3839

3940
// Note that this is ultimately derived from a map indexed by symbol names,
4041
// so duplicates cannot occur.
4142
const abstractAndNonPrivateExtendsSymbols = checker.getPropertiesOfType(instantiatedExtendsType).filter(symbolPointsToNonPrivateAndAbstractMember);
4243

43-
createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, checker, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member));
44+
createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, context, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member));
4445
}
4546

4647
function symbolPointsToNonPrivateAndAbstractMember(symbol: Symbol): boolean {

src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ namespace ts.codefix {
66
registerCodeFix({
77
errorCodes,
88
getCodeActions(context) {
9-
const { program, sourceFile, span } = context;
9+
const { sourceFile, span } = context;
1010
const classDeclaration = getClass(sourceFile, span.start);
11-
const checker = program.getTypeChecker();
1211
return mapDefined<ExpressionWithTypeArguments, CodeFixAction>(getClassImplementsHeritageClauseElements(classDeclaration), implementedTypeNode => {
13-
const changes = textChanges.ChangeTracker.with(context, t => addMissingDeclarations(checker, implementedTypeNode, sourceFile, classDeclaration, t, context.preferences));
12+
const changes = textChanges.ChangeTracker.with(context, t => addMissingDeclarations(context, implementedTypeNode, sourceFile, classDeclaration, t, context.preferences));
1413
return changes.length === 0 ? undefined : createCodeFixAction(fixId, changes, [Diagnostics.Implement_interface_0, implementedTypeNode.getText(sourceFile)], fixId, Diagnostics.Implement_all_unimplemented_interfaces);
1514
});
1615
},
@@ -21,7 +20,7 @@ namespace ts.codefix {
2120
const classDeclaration = getClass(diag.file, diag.start);
2221
if (addToSeen(seenClassDeclarations, getNodeId(classDeclaration))) {
2322
for (const implementedTypeNode of getClassImplementsHeritageClauseElements(classDeclaration)!) {
24-
addMissingDeclarations(context.program.getTypeChecker(), implementedTypeNode, diag.file, classDeclaration, changes, context.preferences);
23+
addMissingDeclarations(context, implementedTypeNode, diag.file, classDeclaration, changes, context.preferences);
2524
}
2625
}
2726
});
@@ -37,13 +36,14 @@ namespace ts.codefix {
3736
}
3837

3938
function addMissingDeclarations(
40-
checker: TypeChecker,
39+
context: TypeConstructionContext,
4140
implementedTypeNode: ExpressionWithTypeArguments,
4241
sourceFile: SourceFile,
4342
classDeclaration: ClassLikeDeclaration,
4443
changeTracker: textChanges.ChangeTracker,
4544
preferences: UserPreferences,
4645
): void {
46+
const checker = context.program.getTypeChecker();
4747
const maybeHeritageClauseSymbol = getHeritageClauseSymbolTable(classDeclaration, checker);
4848
// Note that this is ultimately derived from a map indexed by symbol names,
4949
// so duplicates cannot occur.
@@ -60,12 +60,12 @@ namespace ts.codefix {
6060
createMissingIndexSignatureDeclaration(implementedType, IndexKind.String);
6161
}
6262

63-
createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, checker, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member));
63+
createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, context, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member));
6464

6565
function createMissingIndexSignatureDeclaration(type: InterfaceType, kind: IndexKind): void {
6666
const indexInfoOfKind = checker.getIndexInfoOfType(type, kind);
6767
if (indexInfoOfKind) {
68-
changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, checker.indexInfoToIndexSignatureDeclaration(indexInfoOfKind, kind, classDeclaration)!);
68+
changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, checker.indexInfoToIndexSignatureDeclaration(indexInfoOfKind, kind, classDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context))!);
6969
}
7070
}
7171
}

src/services/codefixes/helpers.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,48 @@ namespace ts.codefix {
66
* @param possiblyMissingSymbols The collection of symbols to filter and then get insertions for.
77
* @returns Empty string iff there are no member insertions.
88
*/
9-
export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: ReadonlyArray<Symbol>, checker: TypeChecker, preferences: UserPreferences, out: (node: ClassElement) => void): void {
9+
export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: ReadonlyArray<Symbol>, context: TypeConstructionContext, preferences: UserPreferences, out: (node: ClassElement) => void): void {
1010
const classMembers = classDeclaration.symbol.members!;
1111
for (const symbol of possiblyMissingSymbols) {
1212
if (!classMembers.has(symbol.escapedName)) {
13-
addNewNodeForMemberSymbol(symbol, classDeclaration, checker, preferences, out);
13+
addNewNodeForMemberSymbol(symbol, classDeclaration, context, preferences, out);
1414
}
1515
}
1616
}
1717

18+
function getModuleSpecifierResolverHost(context: TypeConstructionContext): SymbolTracker["moduleResolverHost"] {
19+
return {
20+
directoryExists: context.host.directoryExists ? d => context.host.directoryExists!(d) : undefined,
21+
fileExists: context.host.fileExists ? f => context.host.fileExists!(f) : undefined,
22+
getCurrentDirectory: context.host.getCurrentDirectory ? () => context.host.getCurrentDirectory!() : undefined,
23+
readFile: context.host.readFile ? f => context.host.readFile!(f) : undefined,
24+
useCaseSensitiveFileNames: context.host.useCaseSensitiveFileNames ? () => context.host.useCaseSensitiveFileNames!() : undefined,
25+
getSourceFiles: () => context.program.getSourceFiles(),
26+
getCommonSourceDirectory: () => context.program.getCommonSourceDirectory(),
27+
};
28+
}
29+
30+
export function getNoopSymbolTrackerWithResolver(context: TypeConstructionContext): SymbolTracker {
31+
return {
32+
trackSymbol: noop,
33+
moduleResolverHost: getModuleSpecifierResolverHost(context),
34+
};
35+
}
36+
37+
export interface TypeConstructionContext {
38+
program: Program;
39+
host: ModuleSpecifierResolutionHost;
40+
}
41+
1842
/**
1943
* @returns Empty string iff there we can't figure out a representation for `symbol` in `enclosingDeclaration`.
2044
*/
21-
function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, checker: TypeChecker, preferences: UserPreferences, out: (node: Node) => void): void {
45+
function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, context: TypeConstructionContext, preferences: UserPreferences, out: (node: Node) => void): void {
2246
const declarations = symbol.getDeclarations();
2347
if (!(declarations && declarations.length)) {
2448
return undefined;
2549
}
50+
const checker = context.program.getTypeChecker();
2651

2752
const declaration = declarations[0];
2853
const name = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName;
@@ -36,7 +61,7 @@ namespace ts.codefix {
3661
case SyntaxKind.SetAccessor:
3762
case SyntaxKind.PropertySignature:
3863
case SyntaxKind.PropertyDeclaration:
39-
const typeNode = checker.typeToTypeNode(type, enclosingDeclaration);
64+
const typeNode = checker.typeToTypeNode(type, enclosingDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context));
4065
out(createProperty(
4166
/*decorators*/undefined,
4267
modifiers,
@@ -83,21 +108,22 @@ namespace ts.codefix {
83108
}
84109

85110
function outputMethod(signature: Signature, modifiers: NodeArray<Modifier> | undefined, name: PropertyName, body?: Block): void {
86-
const method = signatureToMethodDeclaration(checker, signature, enclosingDeclaration, modifiers, name, optional, body);
111+
const method = signatureToMethodDeclaration(context, signature, enclosingDeclaration, modifiers, name, optional, body);
87112
if (method) out(method);
88113
}
89114
}
90115

91116
function signatureToMethodDeclaration(
92-
checker: TypeChecker,
117+
context: TypeConstructionContext,
93118
signature: Signature,
94119
enclosingDeclaration: ClassLikeDeclaration,
95120
modifiers: NodeArray<Modifier> | undefined,
96121
name: PropertyName,
97122
optional: boolean,
98123
body: Block | undefined,
99124
): MethodDeclaration | undefined {
100-
const signatureDeclaration = <MethodDeclaration>checker.signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, NodeBuilderFlags.NoTruncation | NodeBuilderFlags.SuppressAnyReturnType);
125+
const program = context.program;
126+
const signatureDeclaration = <MethodDeclaration>program.getTypeChecker().signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, NodeBuilderFlags.NoTruncation | NodeBuilderFlags.SuppressAnyReturnType, getNoopSymbolTrackerWithResolver(context));
101127
if (!signatureDeclaration) {
102128
return undefined;
103129
}
@@ -117,18 +143,20 @@ namespace ts.codefix {
117143
inJs: boolean,
118144
makeStatic: boolean,
119145
preferences: UserPreferences,
120-
body: boolean,
146+
contextNode: Node,
121147
): MethodDeclaration {
148+
const body = !isInterfaceDeclaration(contextNode);
122149
const { typeArguments, arguments: args, parent } = call;
123150
const checker = context.program.getTypeChecker();
151+
const tracker = getNoopSymbolTrackerWithResolver(context);
124152
const types = map(args, arg =>
125153
// Widen the type so we don't emit nonsense annotations like "function fn(x: 3) {"
126-
checker.typeToTypeNode(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(arg))));
154+
checker.typeToTypeNode(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(arg)), contextNode, /*flags*/ undefined, tracker));
127155
const names = map(args, arg =>
128156
isIdentifier(arg) ? arg.text :
129157
isPropertyAccessExpression(arg) ? arg.name.text : undefined);
130158
const contextualType = checker.getContextualType(call);
131-
const returnType = inJs ? undefined : contextualType && checker.typeToTypeNode(contextualType, call) || createKeywordTypeNode(SyntaxKind.AnyKeyword);
159+
const returnType = inJs ? undefined : contextualType && checker.typeToTypeNode(contextualType, contextNode, /*flags*/ undefined, tracker) || createKeywordTypeNode(SyntaxKind.AnyKeyword);
132160
return createMethod(
133161
/*decorators*/ undefined,
134162
/*modifiers*/ makeStatic ? [createToken(SyntaxKind.StaticKeyword)] : undefined,

tests/cases/fourslash/codeFixClassImplementInterface_typeInOtherFile.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ verify.codeFix({
1717
newFileContent:
1818
`import { I } from "./I";
1919
export class C implements I {
20-
x: import("/I").J;
21-
m(): import("/I").J {
20+
x: import("./I").J;
21+
m(): import("./I").J {
2222
throw new Error("Method not implemented.");
2323
}
2424
}`,

tests/cases/fourslash/codeFixUndeclaredAcrossFiles3.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
verify.getAndApplyCodeFix(/*errorCode*/ undefined, 0);
2121

2222
verify.rangeIs(`
23-
m0(arg0: D): any {
23+
m0(arg0: import("./f2").D): any {
2424
throw new Error("Method not implemented.");
2525
}
2626
`);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: class.ts
4+
////export class Class { }
5+
// @Filename: interface.ts
6+
////import { Class } from './class';
7+
////
8+
////export interface Foo {
9+
//// x: Class;
10+
////}
11+
// @Filename: index.ts
12+
////import { Foo } from './interface';
13+
////
14+
////class /*1*/X implements Foo {}
15+
goTo.marker("1");
16+
verify.codeFix({
17+
index: 0,
18+
description: "Implement interface 'Foo'",
19+
newFileContent: {
20+
"/tests/cases/fourslash/index.ts": `import { Foo } from './interface';
21+
22+
class X implements Foo {
23+
x: import("./class").Class;
24+
}`
25+
}
26+
});

0 commit comments

Comments
 (0)