mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
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
This commit is contained in:
@@ -32242,23 +32242,6 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
function getFirstIdentifier(node: EntityNameOrEntityNameExpression): Identifier {
|
||||
switch (node.kind) {
|
||||
case SyntaxKind.Identifier:
|
||||
return node;
|
||||
case SyntaxKind.QualifiedName:
|
||||
do {
|
||||
node = node.left;
|
||||
} while (node.kind !== SyntaxKind.Identifier);
|
||||
return node;
|
||||
case SyntaxKind.PropertyAccessExpression:
|
||||
do {
|
||||
node = node.expression;
|
||||
} while (node.kind !== SyntaxKind.Identifier);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
function getFirstNonModuleExportsIdentifier(node: EntityNameOrEntityNameExpression): Identifier {
|
||||
switch (node.kind) {
|
||||
case SyntaxKind.Identifier:
|
||||
|
||||
@@ -4174,6 +4174,23 @@ namespace ts {
|
||||
return node.kind === SyntaxKind.Identifier || isPropertyAccessEntityNameExpression(node);
|
||||
}
|
||||
|
||||
export function getFirstIdentifier(node: EntityNameOrEntityNameExpression): Identifier {
|
||||
switch (node.kind) {
|
||||
case SyntaxKind.Identifier:
|
||||
return node;
|
||||
case SyntaxKind.QualifiedName:
|
||||
do {
|
||||
node = node.left;
|
||||
} while (node.kind !== SyntaxKind.Identifier);
|
||||
return node;
|
||||
case SyntaxKind.PropertyAccessExpression:
|
||||
do {
|
||||
node = node.expression;
|
||||
} while (node.kind !== SyntaxKind.Identifier);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
export function isDottedName(node: Expression): boolean {
|
||||
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.ThisKeyword ||
|
||||
node.kind === SyntaxKind.PropertyAccessExpression && isDottedName((<PropertyAccessExpression>node).expression) ||
|
||||
|
||||
@@ -49,21 +49,21 @@ namespace ts.codefix {
|
||||
registerCodeFix({
|
||||
errorCodes,
|
||||
getCodeActions(context) {
|
||||
const { sourceFile, program, span: { start }, errorCode, cancellationToken, host } = context;
|
||||
const { sourceFile, program, span: { start }, errorCode, cancellationToken, host, formatContext, 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); });
|
||||
const changes = textChanges.ChangeTracker.with(context, changes => { declaration = doChange(changes, sourceFile, token, errorCode, program, cancellationToken, /*markSeen*/ returnTrue, host, formatContext, 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 } = context;
|
||||
const { sourceFile, program, cancellationToken, host, formatContext, 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);
|
||||
doChange(changes, sourceFile, getTokenAtPosition(err.file, err.start), err.code, program, cancellationToken, markSeen, host, formatContext, preferences);
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -106,7 +106,7 @@ namespace ts.codefix {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, errorCode: number, program: Program, cancellationToken: CancellationToken, markSeen: NodeSeenTracker, host: LanguageServiceHost): Declaration | undefined {
|
||||
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 {
|
||||
if (!isParameterPropertyModifier(token.kind) && token.kind !== SyntaxKind.Identifier && token.kind !== SyntaxKind.DotDotDotToken && token.kind !== SyntaxKind.ThisKeyword) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -118,7 +118,7 @@ namespace ts.codefix {
|
||||
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);
|
||||
annotateVariableDeclaration(changes, sourceFile, parent, program, host, cancellationToken, formatContext, preferences);
|
||||
return parent;
|
||||
}
|
||||
if (isPropertyAccessExpression(parent)) {
|
||||
@@ -136,7 +136,7 @@ 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);
|
||||
annotateVariableDeclaration(changes, sourceFile, symbol.valueDeclaration, program, host, cancellationToken, formatContext, preferences);
|
||||
return symbol.valueDeclaration;
|
||||
}
|
||||
return undefined;
|
||||
@@ -152,14 +152,14 @@ namespace ts.codefix {
|
||||
// Parameter declarations
|
||||
case Diagnostics.Parameter_0_implicitly_has_an_1_type.code:
|
||||
if (isSetAccessorDeclaration(containingFunction)) {
|
||||
annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken);
|
||||
annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken, formatContext, preferences);
|
||||
return containingFunction;
|
||||
}
|
||||
// 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);
|
||||
annotateParameters(changes, sourceFile, param, containingFunction, program, host, cancellationToken, formatContext, preferences);
|
||||
return param;
|
||||
}
|
||||
return undefined;
|
||||
@@ -168,7 +168,7 @@ namespace ts.codefix {
|
||||
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);
|
||||
annotate(changes, sourceFile, containingFunction, inferTypeForVariableFromUsage(containingFunction.name, program, cancellationToken), program, host, formatContext, preferences);
|
||||
return containingFunction;
|
||||
}
|
||||
return undefined;
|
||||
@@ -176,7 +176,7 @@ namespace ts.codefix {
|
||||
// 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);
|
||||
annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken, formatContext, preferences);
|
||||
return containingFunction;
|
||||
}
|
||||
return undefined;
|
||||
@@ -194,13 +194,32 @@ namespace ts.codefix {
|
||||
}
|
||||
}
|
||||
|
||||
function annotateVariableDeclaration(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: VariableDeclaration | PropertyDeclaration | PropertySignature, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken): void {
|
||||
function annotateVariableDeclaration(
|
||||
changes: textChanges.ChangeTracker,
|
||||
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);
|
||||
annotate(changes, sourceFile, declaration, inferTypeForVariableFromUsage(declaration.name, program, cancellationToken), program, host, formatContext, preferences);
|
||||
}
|
||||
}
|
||||
|
||||
function annotateParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parameterDeclaration: ParameterDeclaration, containingFunction: FunctionLike, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken): void {
|
||||
function annotateParameters(
|
||||
changes: textChanges.ChangeTracker,
|
||||
sourceFile: SourceFile,
|
||||
parameterDeclaration: ParameterDeclaration,
|
||||
containingFunction: FunctionLike,
|
||||
program: Program,
|
||||
host: LanguageServiceHost,
|
||||
cancellationToken: CancellationToken,
|
||||
formatContext: formatting.FormatContext,
|
||||
preferences: UserPreferences,
|
||||
): void {
|
||||
if (!isIdentifier(parameterDeclaration.name)) {
|
||||
return;
|
||||
}
|
||||
@@ -216,7 +235,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);
|
||||
annotate(changes, sourceFile, declaration, type, program, host, formatContext, preferences);
|
||||
}
|
||||
}
|
||||
if (needParens) changes.insertNodeAfter(sourceFile, last(containingFunction.parameters), createToken(SyntaxKind.CloseParenToken));
|
||||
@@ -248,7 +267,16 @@ namespace ts.codefix {
|
||||
]);
|
||||
}
|
||||
|
||||
function annotateSetAccessor(changes: textChanges.ChangeTracker, sourceFile: SourceFile, setAccessorDeclaration: SetAccessorDeclaration, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken): void {
|
||||
function annotateSetAccessor(
|
||||
changes: textChanges.ChangeTracker,
|
||||
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)) {
|
||||
let type = inferTypeForVariableFromUsage(setAccessorDeclaration.name, program, cancellationToken);
|
||||
@@ -259,12 +287,12 @@ namespace ts.codefix {
|
||||
annotateJSDocParameters(changes, sourceFile, [{ declaration: param, type }], program, host);
|
||||
}
|
||||
else {
|
||||
annotate(changes, sourceFile, param, type, program, host);
|
||||
annotate(changes, sourceFile, param, type, program, host, formatContext, preferences);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function annotate(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type, program: Program, host: LanguageServiceHost): void {
|
||||
function annotate(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type, program: Program, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): void {
|
||||
const typeNode = getTypeNodeIfAccessible(type, declaration, program, host);
|
||||
if (typeNode) {
|
||||
if (isInJSFile(sourceFile) && declaration.kind !== SyntaxKind.PropertySignature) {
|
||||
@@ -276,12 +304,42 @@ namespace ts.codefix {
|
||||
const typeTag = isGetAccessorDeclaration(declaration) ? createJSDocReturnTag(typeExpression, "") : createJSDocTypeTag(typeExpression, "");
|
||||
addJSDocTags(changes, sourceFile, parent, [typeTag]);
|
||||
}
|
||||
else {
|
||||
else if (!tryReplaceImportTypeNodeWithAutoImport(typeNode, changes, sourceFile, declaration, type, program, host, formatContext, preferences)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function annotateJSDocParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parameterInferences: readonly ParameterInference[], program: Program, host: LanguageServiceHost): void {
|
||||
const signature = parameterInferences.length && parameterInferences[0].declaration.parent;
|
||||
if (!signature) {
|
||||
|
||||
@@ -248,6 +248,18 @@ namespace ts.textChanges {
|
||||
/** Public for tests only. Other callers should use `ChangeTracker.with`. */
|
||||
constructor(private readonly newLineCharacter: string, private readonly formatContext: formatting.FormatContext) {}
|
||||
|
||||
public pushRaw(sourceFile: SourceFile, change: FileTextChanges) {
|
||||
Debug.assertEqual(sourceFile.fileName, change.fileName);
|
||||
for (const c of change.textChanges) {
|
||||
this.changes.push({
|
||||
kind: ChangeKind.Text,
|
||||
sourceFile,
|
||||
text: c.newText,
|
||||
range: createTextRangeFromSpan(c.span),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public deleteRange(sourceFile: SourceFile, range: TextRange): void {
|
||||
this.changes.push({ kind: ChangeKind.Remove, sourceFile, range });
|
||||
}
|
||||
@@ -383,12 +395,12 @@ namespace ts.textChanges {
|
||||
}
|
||||
|
||||
/** Prefer this over replacing a node with another that has a type annotation, as it avoids reformatting the other parts of the node. */
|
||||
public tryInsertTypeAnnotation(sourceFile: SourceFile, node: TypeAnnotatable, type: TypeNode): void {
|
||||
public tryInsertTypeAnnotation(sourceFile: SourceFile, node: TypeAnnotatable, type: TypeNode): boolean {
|
||||
let endNode: Node | undefined;
|
||||
if (isFunctionLike(node)) {
|
||||
endNode = findChildOfKind(node, SyntaxKind.CloseParenToken, sourceFile);
|
||||
if (!endNode) {
|
||||
if (!isArrowFunction(node)) return; // Function missing parentheses, give up
|
||||
if (!isArrowFunction(node)) return false; // Function missing parentheses, give up
|
||||
// If no `)`, is an arrow function `x => x`, so use the end of the first parameter
|
||||
endNode = first(node.parameters);
|
||||
}
|
||||
@@ -398,6 +410,7 @@ namespace ts.textChanges {
|
||||
}
|
||||
|
||||
this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " });
|
||||
return true;
|
||||
}
|
||||
|
||||
public tryInsertThisTypeAnnotation(sourceFile: SourceFile, node: ThisTypeAnnotatable, type: TypeNode): void {
|
||||
|
||||
@@ -538,7 +538,7 @@ namespace ts {
|
||||
|
||||
export interface FileTextChanges {
|
||||
fileName: string;
|
||||
textChanges: TextChange[];
|
||||
textChanges: readonly TextChange[];
|
||||
isNewFile?: boolean;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -5194,7 +5194,7 @@ declare namespace ts {
|
||||
}
|
||||
interface FileTextChanges {
|
||||
fileName: string;
|
||||
textChanges: TextChange[];
|
||||
textChanges: readonly TextChange[];
|
||||
isNewFile?: boolean;
|
||||
}
|
||||
interface CodeAction {
|
||||
|
||||
+1
-1
@@ -5194,7 +5194,7 @@ declare namespace ts {
|
||||
}
|
||||
interface FileTextChanges {
|
||||
fileName: string;
|
||||
textChanges: TextChange[];
|
||||
textChanges: readonly TextChange[];
|
||||
isNewFile?: boolean;
|
||||
}
|
||||
interface CodeAction {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @strict: true
|
||||
// @noImplicitAny: true
|
||||
// @noLib: true
|
||||
|
||||
// @Filename: /a.ts
|
||||
////export interface User {}
|
||||
////export declare function getEmail(user: User): string;
|
||||
|
||||
// @Filename: /b.ts
|
||||
////import { getEmail } from "./a";
|
||||
////
|
||||
////export function f([|user|]) {
|
||||
//// getEmail(user);
|
||||
////}
|
||||
|
||||
goTo.file("/b.ts");
|
||||
|
||||
verify.codeFix({
|
||||
description: "Infer parameter types from usage",
|
||||
newFileContent:
|
||||
`import { getEmail, User } from "./a";
|
||||
|
||||
export function f(user: User) {
|
||||
getEmail(user);
|
||||
}`
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @strict: true
|
||||
// @noImplicitAny: true
|
||||
// @noLib: true
|
||||
|
||||
// @Filename: /types.d.ts
|
||||
////declare function getEmail(user: import('./a').User): string;
|
||||
|
||||
// @Filename: /a.ts
|
||||
////export interface User {}
|
||||
|
||||
// @Filename: /b.ts
|
||||
////export function f([|user|]) {
|
||||
//// getEmail(user);
|
||||
////}
|
||||
|
||||
goTo.file("/b.ts");
|
||||
|
||||
verify.codeFix({
|
||||
description: "Infer parameter types from usage",
|
||||
newFileContent:
|
||||
`import { User } from "./a";
|
||||
|
||||
export function f(user: User) {
|
||||
getEmail(user);
|
||||
}`
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @strict: true
|
||||
// @noImplicitAny: true
|
||||
// @noLib: true
|
||||
|
||||
// @Filename: /a.ts
|
||||
////export namespace things {
|
||||
//// export namespace stuff {
|
||||
//// export class User<T> {}
|
||||
//// }
|
||||
////}
|
||||
////export declare function getEmail<T>(thing: T, user: things.stuff.User<T>): string;
|
||||
|
||||
// @Filename: /b.ts
|
||||
////import { getEmail } from "./a";
|
||||
////
|
||||
////export function f([|user|]) {
|
||||
//// getEmail(42, user);
|
||||
////}
|
||||
|
||||
goTo.file("/b.ts");
|
||||
|
||||
verify.codeFix({
|
||||
description: "Infer parameter types from usage",
|
||||
newFileContent:
|
||||
`import { getEmail, things } from "./a";
|
||||
|
||||
export function f(user: things.stuff.User<number>) {
|
||||
getEmail(42, user);
|
||||
}`
|
||||
});
|
||||
Reference in New Issue
Block a user