Skip to content

Commit e146f0d

Browse files
authored
Allow inferFromUsage to do auto-imports (#33915)
* Add test * Auto-import instead of using ImportTypeNodes * Write more tests and fix namespace case * Remove unused enum memmber * Update API baselines * Lint * Style nits and util consolidation
1 parent 1820df1 commit e146f0d

File tree

10 files changed

+200
-41
lines changed

10 files changed

+200
-41
lines changed

src/compiler/checker.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32242,23 +32242,6 @@ namespace ts {
3224232242
}
3224332243
}
3224432244

32245-
function getFirstIdentifier(node: EntityNameOrEntityNameExpression): Identifier {
32246-
switch (node.kind) {
32247-
case SyntaxKind.Identifier:
32248-
return node;
32249-
case SyntaxKind.QualifiedName:
32250-
do {
32251-
node = node.left;
32252-
} while (node.kind !== SyntaxKind.Identifier);
32253-
return node;
32254-
case SyntaxKind.PropertyAccessExpression:
32255-
do {
32256-
node = node.expression;
32257-
} while (node.kind !== SyntaxKind.Identifier);
32258-
return node;
32259-
}
32260-
}
32261-
3226232245
function getFirstNonModuleExportsIdentifier(node: EntityNameOrEntityNameExpression): Identifier {
3226332246
switch (node.kind) {
3226432247
case SyntaxKind.Identifier:

src/compiler/utilities.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4174,6 +4174,23 @@ namespace ts {
41744174
return node.kind === SyntaxKind.Identifier || isPropertyAccessEntityNameExpression(node);
41754175
}
41764176

4177+
export function getFirstIdentifier(node: EntityNameOrEntityNameExpression): Identifier {
4178+
switch (node.kind) {
4179+
case SyntaxKind.Identifier:
4180+
return node;
4181+
case SyntaxKind.QualifiedName:
4182+
do {
4183+
node = node.left;
4184+
} while (node.kind !== SyntaxKind.Identifier);
4185+
return node;
4186+
case SyntaxKind.PropertyAccessExpression:
4187+
do {
4188+
node = node.expression;
4189+
} while (node.kind !== SyntaxKind.Identifier);
4190+
return node;
4191+
}
4192+
}
4193+
41774194
export function isDottedName(node: Expression): boolean {
41784195
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.ThisKeyword ||
41794196
node.kind === SyntaxKind.PropertyAccessExpression && isDottedName((<PropertyAccessExpression>node).expression) ||

src/services/codefixes/inferFromUsage.ts

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,21 @@ namespace ts.codefix {
4949
registerCodeFix({
5050
errorCodes,
5151
getCodeActions(context) {
52-
const { sourceFile, program, span: { start }, errorCode, cancellationToken, host } = context;
52+
const { sourceFile, program, span: { start }, errorCode, cancellationToken, host, formatContext, preferences } = context;
5353

5454
const token = getTokenAtPosition(sourceFile, start);
5555
let declaration!: Declaration | undefined;
56-
const changes = textChanges.ChangeTracker.with(context, changes => { declaration = doChange(changes, sourceFile, token, errorCode, program, cancellationToken, /*markSeen*/ returnTrue, host); });
56+
const changes = textChanges.ChangeTracker.with(context, changes => { declaration = doChange(changes, sourceFile, token, errorCode, program, cancellationToken, /*markSeen*/ returnTrue, host, formatContext, preferences); });
5757
const name = declaration && getNameOfDeclaration(declaration);
5858
return !name || changes.length === 0 ? undefined
5959
: [createCodeFixAction(fixId, changes, [getDiagnostic(errorCode, token), name.getText(sourceFile)], fixId, Diagnostics.Infer_all_types_from_usage)];
6060
},
6161
fixIds: [fixId],
6262
getAllCodeActions(context) {
63-
const { sourceFile, program, cancellationToken, host } = context;
63+
const { sourceFile, program, cancellationToken, host, formatContext, preferences } = context;
6464
const markSeen = nodeSeenTracker();
6565
return codeFixAll(context, errorCodes, (changes, err) => {
66-
doChange(changes, sourceFile, getTokenAtPosition(err.file, err.start), err.code, program, cancellationToken, markSeen, host);
66+
doChange(changes, sourceFile, getTokenAtPosition(err.file, err.start), err.code, program, cancellationToken, markSeen, host, formatContext, preferences);
6767
});
6868
},
6969
});
@@ -106,7 +106,7 @@ namespace ts.codefix {
106106
return errorCode;
107107
}
108108

109-
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, errorCode: number, program: Program, cancellationToken: CancellationToken, markSeen: NodeSeenTracker, host: LanguageServiceHost): Declaration | undefined {
109+
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 {
110110
if (!isParameterPropertyModifier(token.kind) && token.kind !== SyntaxKind.Identifier && token.kind !== SyntaxKind.DotDotDotToken && token.kind !== SyntaxKind.ThisKeyword) {
111111
return undefined;
112112
}
@@ -118,7 +118,7 @@ namespace ts.codefix {
118118
case Diagnostics.Member_0_implicitly_has_an_1_type.code:
119119
case Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined.code:
120120
if ((isVariableDeclaration(parent) && markSeen(parent)) || isPropertyDeclaration(parent) || isPropertySignature(parent)) { // handle bad location
121-
annotateVariableDeclaration(changes, sourceFile, parent, program, host, cancellationToken);
121+
annotateVariableDeclaration(changes, sourceFile, parent, program, host, cancellationToken, formatContext, preferences);
122122
return parent;
123123
}
124124
if (isPropertyAccessExpression(parent)) {
@@ -136,7 +136,7 @@ namespace ts.codefix {
136136
case Diagnostics.Variable_0_implicitly_has_an_1_type.code: {
137137
const symbol = program.getTypeChecker().getSymbolAtLocation(token);
138138
if (symbol && symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && markSeen(symbol.valueDeclaration)) {
139-
annotateVariableDeclaration(changes, sourceFile, symbol.valueDeclaration, program, host, cancellationToken);
139+
annotateVariableDeclaration(changes, sourceFile, symbol.valueDeclaration, program, host, cancellationToken, formatContext, preferences);
140140
return symbol.valueDeclaration;
141141
}
142142
return undefined;
@@ -152,14 +152,14 @@ namespace ts.codefix {
152152
// Parameter declarations
153153
case Diagnostics.Parameter_0_implicitly_has_an_1_type.code:
154154
if (isSetAccessorDeclaration(containingFunction)) {
155-
annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken);
155+
annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken, formatContext, preferences);
156156
return containingFunction;
157157
}
158158
// falls through
159159
case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code:
160160
if (markSeen(containingFunction)) {
161161
const param = cast(parent, isParameter);
162-
annotateParameters(changes, sourceFile, param, containingFunction, program, host, cancellationToken);
162+
annotateParameters(changes, sourceFile, param, containingFunction, program, host, cancellationToken, formatContext, preferences);
163163
return param;
164164
}
165165
return undefined;
@@ -168,15 +168,15 @@ namespace ts.codefix {
168168
case Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code:
169169
case Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code:
170170
if (isGetAccessorDeclaration(containingFunction) && isIdentifier(containingFunction.name)) {
171-
annotate(changes, sourceFile, containingFunction, inferTypeForVariableFromUsage(containingFunction.name, program, cancellationToken), program, host);
171+
annotate(changes, sourceFile, containingFunction, inferTypeForVariableFromUsage(containingFunction.name, program, cancellationToken), program, host, formatContext, preferences);
172172
return containingFunction;
173173
}
174174
return undefined;
175175

176176
// Set Accessor declarations
177177
case Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code:
178178
if (isSetAccessorDeclaration(containingFunction)) {
179-
annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken);
179+
annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken, formatContext, preferences);
180180
return containingFunction;
181181
}
182182
return undefined;
@@ -194,13 +194,32 @@ namespace ts.codefix {
194194
}
195195
}
196196

197-
function annotateVariableDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: VariableDeclaration | PropertyDeclaration | PropertySignature, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken): void {
197+
function annotateVariableDeclaration(
198+
changes: textChanges.ChangeTracker,
199+
sourceFile: SourceFile,
200+
declaration: VariableDeclaration | PropertyDeclaration | PropertySignature,
201+
program: Program,
202+
host: LanguageServiceHost,
203+
cancellationToken: CancellationToken,
204+
formatContext: formatting.FormatContext,
205+
preferences: UserPreferences,
206+
): void {
198207
if (isIdentifier(declaration.name)) {
199-
annotate(changes, sourceFile, declaration, inferTypeForVariableFromUsage(declaration.name, program, cancellationToken), program, host);
208+
annotate(changes, sourceFile, declaration, inferTypeForVariableFromUsage(declaration.name, program, cancellationToken), program, host, formatContext, preferences);
200209
}
201210
}
202211

203-
function annotateParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parameterDeclaration: ParameterDeclaration, containingFunction: FunctionLike, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken): void {
212+
function annotateParameters(
213+
changes: textChanges.ChangeTracker,
214+
sourceFile: SourceFile,
215+
parameterDeclaration: ParameterDeclaration,
216+
containingFunction: FunctionLike,
217+
program: Program,
218+
host: LanguageServiceHost,
219+
cancellationToken: CancellationToken,
220+
formatContext: formatting.FormatContext,
221+
preferences: UserPreferences,
222+
): void {
204223
if (!isIdentifier(parameterDeclaration.name)) {
205224
return;
206225
}
@@ -216,7 +235,7 @@ namespace ts.codefix {
216235
if (needParens) changes.insertNodeBefore(sourceFile, first(containingFunction.parameters), createToken(SyntaxKind.OpenParenToken));
217236
for (const { declaration, type } of parameterInferences) {
218237
if (declaration && !declaration.type && !declaration.initializer) {
219-
annotate(changes, sourceFile, declaration, type, program, host);
238+
annotate(changes, sourceFile, declaration, type, program, host, formatContext, preferences);
220239
}
221240
}
222241
if (needParens) changes.insertNodeAfter(sourceFile, last(containingFunction.parameters), createToken(SyntaxKind.CloseParenToken));
@@ -248,7 +267,16 @@ namespace ts.codefix {
248267
]);
249268
}
250269

251-
function annotateSetAccessor(changes: textChanges.ChangeTracker, sourceFile: SourceFile, setAccessorDeclaration: SetAccessorDeclaration, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken): void {
270+
function annotateSetAccessor(
271+
changes: textChanges.ChangeTracker,
272+
sourceFile: SourceFile,
273+
setAccessorDeclaration: SetAccessorDeclaration,
274+
program: Program,
275+
host: LanguageServiceHost,
276+
cancellationToken: CancellationToken,
277+
formatContext: formatting.FormatContext,
278+
preferences: UserPreferences,
279+
): void {
252280
const param = firstOrUndefined(setAccessorDeclaration.parameters);
253281
if (param && isIdentifier(setAccessorDeclaration.name) && isIdentifier(param.name)) {
254282
let type = inferTypeForVariableFromUsage(setAccessorDeclaration.name, program, cancellationToken);
@@ -259,12 +287,12 @@ namespace ts.codefix {
259287
annotateJSDocParameters(changes, sourceFile, [{ declaration: param, type }], program, host);
260288
}
261289
else {
262-
annotate(changes, sourceFile, param, type, program, host);
290+
annotate(changes, sourceFile, param, type, program, host, formatContext, preferences);
263291
}
264292
}
265293
}
266294

267-
function annotate(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type, program: Program, host: LanguageServiceHost): void {
295+
function annotate(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type, program: Program, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): void {
268296
const typeNode = getTypeNodeIfAccessible(type, declaration, program, host);
269297
if (typeNode) {
270298
if (isInJSFile(sourceFile) && declaration.kind !== SyntaxKind.PropertySignature) {
@@ -276,12 +304,42 @@ namespace ts.codefix {
276304
const typeTag = isGetAccessorDeclaration(declaration) ? createJSDocReturnTag(typeExpression, "") : createJSDocTypeTag(typeExpression, "");
277305
addJSDocTags(changes, sourceFile, parent, [typeTag]);
278306
}
279-
else {
307+
else if (!tryReplaceImportTypeNodeWithAutoImport(typeNode, changes, sourceFile, declaration, type, program, host, formatContext, preferences)) {
280308
changes.tryInsertTypeAnnotation(sourceFile, declaration, typeNode);
281309
}
282310
}
283311
}
284312

313+
function tryReplaceImportTypeNodeWithAutoImport(typeNode: TypeNode, changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type, program: Program, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): boolean {
314+
if (isLiteralImportTypeNode(typeNode) && typeNode.qualifier && type.symbol) {
315+
// Replace 'import("./a").SomeType' with 'SomeType' and an actual import if possible
316+
const moduleSymbol = find(type.symbol.declarations, d => !!d.getSourceFile().externalModuleIndicator)?.getSourceFile().symbol;
317+
// Symbol for the left-most thing after the dot
318+
if (moduleSymbol) {
319+
const symbol = getFirstIdentifier(typeNode.qualifier).symbol;
320+
const action = getImportCompletionAction(
321+
symbol,
322+
moduleSymbol,
323+
sourceFile,
324+
symbol.name,
325+
host,
326+
program,
327+
formatContext,
328+
declaration.pos,
329+
preferences,
330+
);
331+
if (action.codeAction.changes.length && changes.tryInsertTypeAnnotation(sourceFile, declaration, createTypeReferenceNode(typeNode.qualifier, typeNode.typeArguments))) {
332+
for (const change of action.codeAction.changes) {
333+
const file = sourceFile.fileName === change.fileName ? sourceFile : Debug.assertDefined(program.getSourceFile(change.fileName));
334+
changes.pushRaw(file, change);
335+
}
336+
return true;
337+
}
338+
}
339+
}
340+
return false;
341+
}
342+
285343
function annotateJSDocParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parameterInferences: readonly ParameterInference[], program: Program, host: LanguageServiceHost): void {
286344
const signature = parameterInferences.length && parameterInferences[0].declaration.parent;
287345
if (!signature) {

src/services/textChanges.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,18 @@ namespace ts.textChanges {
248248
/** Public for tests only. Other callers should use `ChangeTracker.with`. */
249249
constructor(private readonly newLineCharacter: string, private readonly formatContext: formatting.FormatContext) {}
250250

251+
public pushRaw(sourceFile: SourceFile, change: FileTextChanges) {
252+
Debug.assertEqual(sourceFile.fileName, change.fileName);
253+
for (const c of change.textChanges) {
254+
this.changes.push({
255+
kind: ChangeKind.Text,
256+
sourceFile,
257+
text: c.newText,
258+
range: createTextRangeFromSpan(c.span),
259+
});
260+
}
261+
}
262+
251263
public deleteRange(sourceFile: SourceFile, range: TextRange): void {
252264
this.changes.push({ kind: ChangeKind.Remove, sourceFile, range });
253265
}
@@ -383,12 +395,12 @@ namespace ts.textChanges {
383395
}
384396

385397
/** Prefer this over replacing a node with another that has a type annotation, as it avoids reformatting the other parts of the node. */
386-
public tryInsertTypeAnnotation(sourceFile: SourceFile, node: TypeAnnotatable, type: TypeNode): void {
398+
public tryInsertTypeAnnotation(sourceFile: SourceFile, node: TypeAnnotatable, type: TypeNode): boolean {
387399
let endNode: Node | undefined;
388400
if (isFunctionLike(node)) {
389401
endNode = findChildOfKind(node, SyntaxKind.CloseParenToken, sourceFile);
390402
if (!endNode) {
391-
if (!isArrowFunction(node)) return; // Function missing parentheses, give up
403+
if (!isArrowFunction(node)) return false; // Function missing parentheses, give up
392404
// If no `)`, is an arrow function `x => x`, so use the end of the first parameter
393405
endNode = first(node.parameters);
394406
}
@@ -398,6 +410,7 @@ namespace ts.textChanges {
398410
}
399411

400412
this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " });
413+
return true;
401414
}
402415

403416
public tryInsertThisTypeAnnotation(sourceFile: SourceFile, node: ThisTypeAnnotatable, type: TypeNode): void {

src/services/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ namespace ts {
538538

539539
export interface FileTextChanges {
540540
fileName: string;
541-
textChanges: TextChange[];
541+
textChanges: readonly TextChange[];
542542
isNewFile?: boolean;
543543
}
544544

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5194,7 +5194,7 @@ declare namespace ts {
51945194
}
51955195
interface FileTextChanges {
51965196
fileName: string;
5197-
textChanges: TextChange[];
5197+
textChanges: readonly TextChange[];
51985198
isNewFile?: boolean;
51995199
}
52005200
interface CodeAction {

tests/baselines/reference/api/typescript.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5194,7 +5194,7 @@ declare namespace ts {
51945194
}
51955195
interface FileTextChanges {
51965196
fileName: string;
5197-
textChanges: TextChange[];
5197+
textChanges: readonly TextChange[];
51985198
isNewFile?: boolean;
51995199
}
52005200
interface CodeAction {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @strict: true
4+
// @noImplicitAny: true
5+
// @noLib: true
6+
7+
// @Filename: /a.ts
8+
////export interface User {}
9+
////export declare function getEmail(user: User): string;
10+
11+
// @Filename: /b.ts
12+
////import { getEmail } from "./a";
13+
////
14+
////export function f([|user|]) {
15+
//// getEmail(user);
16+
////}
17+
18+
goTo.file("/b.ts");
19+
20+
verify.codeFix({
21+
description: "Infer parameter types from usage",
22+
newFileContent:
23+
`import { getEmail, User } from "./a";
24+
25+
export function f(user: User) {
26+
getEmail(user);
27+
}`
28+
});

0 commit comments

Comments
 (0)