diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 06b519ff4b898..438e46eca11b6 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1641,6 +1641,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { getBaseTypeOfLiteralType, getWidenedType, getWidenedLiteralType, + fillMissingTypeArguments, getTypeFromTypeNode: nodeIn => { const node = getParseTreeNode(nodeIn, isTypeNode); return node ? getTypeFromTypeNode(node) : errorType; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 590911b3f6e99..f6414327a2be5 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5421,6 +5421,7 @@ export interface TypeChecker { /** @internal */ isTypeParameterPossiblyReferenced(tp: TypeParameter, node: Node): boolean; /** @internal */ typeHasCallOrConstructSignatures(type: Type): boolean; /** @internal */ getSymbolFlags(symbol: Symbol): SymbolFlags; + /** @internal */ fillMissingTypeArguments(typeArguments: readonly Type[], typeParameters: readonly TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): Type[]; } /** @internal */ diff --git a/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts b/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts index 4d961dfa979cc..bc0a14dbfac51 100644 --- a/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts +++ b/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts @@ -4,8 +4,9 @@ import { createImportAdder, eachDiagnostic, registerCodeFix, + typeNodeToAutoImportableTypeNode, typePredicateToAutoImportableTypeNode, - typeToAutoImportableTypeNode, + typeToMinimizedReferenceType, } from "../_namespaces/ts.codefix.js"; import { ArrayBindingPattern, @@ -1096,9 +1097,9 @@ function withContext( return emptyInferenceResult; } - function typeToTypeNode(type: Type, enclosingDeclaration: Node, flags = NodeBuilderFlags.None) { + function typeToTypeNode(type: Type, enclosingDeclaration: Node, flags = NodeBuilderFlags.None): TypeNode | undefined { let isTruncated = false; - const result = typeToAutoImportableTypeNode(typeChecker, importAdder, type, enclosingDeclaration, scriptTarget, declarationEmitNodeBuilderFlags | flags, declarationEmitInternalNodeBuilderFlags, { + const minimizedTypeNode = typeToMinimizedReferenceType(typeChecker, type, enclosingDeclaration, declarationEmitNodeBuilderFlags | flags, declarationEmitInternalNodeBuilderFlags, { moduleResolverHost: program, trackSymbol() { return true; @@ -1107,6 +1108,10 @@ function withContext( isTruncated = true; }, }); + if (!minimizedTypeNode) { + return undefined; + } + const result = typeNodeToAutoImportableTypeNode(minimizedTypeNode, importAdder, scriptTarget); return isTruncated ? factory.createKeywordTypeNode(SyntaxKind.AnyKeyword) : result; } diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 3040e062dabaa..7957b28b110f5 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -23,6 +23,7 @@ import { flatMap, FunctionDeclaration, FunctionExpression, + GenericType, GetAccessorDeclaration, getAllAccessorDeclarations, getCheckFlags, @@ -59,6 +60,7 @@ import { isSetAccessorDeclaration, isStringLiteral, isTypeNode, + isTypeReferenceNode, isTypeUsableAsPropertyName, isYieldExpression, LanguageServiceHost, @@ -595,7 +597,15 @@ function createTypeParameterName(index: number) { /** @internal */ export function typeToAutoImportableTypeNode(checker: TypeChecker, importAdder: ImportAdder, type: Type, contextNode: Node | undefined, scriptTarget: ScriptTarget, flags?: NodeBuilderFlags, internalFlags?: InternalNodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined { - let typeNode = checker.typeToTypeNode(type, contextNode, flags, internalFlags, tracker); + const typeNode = checker.typeToTypeNode(type, contextNode, flags, internalFlags, tracker); + if (!typeNode) { + return undefined; + } + return typeNodeToAutoImportableTypeNode(typeNode, importAdder, scriptTarget); +} + +/** @internal */ +export function typeNodeToAutoImportableTypeNode(typeNode: TypeNode, importAdder: ImportAdder, scriptTarget: ScriptTarget): TypeNode | undefined { if (typeNode && isImportTypeNode(typeNode)) { const importableReference = tryGetAutoImportableReferenceFromTypeNode(typeNode, scriptTarget); if (importableReference) { @@ -608,6 +618,40 @@ export function typeToAutoImportableTypeNode(checker: TypeChecker, importAdder: return getSynthesizedDeepClone(typeNode); } +function endOfRequiredTypeParameters(checker: TypeChecker, type: GenericType): number { + Debug.assert(type.typeArguments); + const fullTypeArguments = type.typeArguments; + const target = type.target; + for (let cutoff = 0; cutoff < fullTypeArguments.length; cutoff++) { + const typeArguments = fullTypeArguments.slice(0, cutoff); + const filledIn = checker.fillMissingTypeArguments(typeArguments, target.typeParameters, cutoff, /*isJavaScriptImplicitAny*/ false); + if (filledIn.every((fill, i) => fill === fullTypeArguments[i])) { + return cutoff; + } + } + // If we make it all the way here, all the type arguments are required. + return fullTypeArguments.length; +} + +/** @internal */ +export function typeToMinimizedReferenceType(checker: TypeChecker, type: Type, contextNode: Node | undefined, flags?: NodeBuilderFlags, internalFlags?: InternalNodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined { + let typeNode = checker.typeToTypeNode(type, contextNode, flags, internalFlags, tracker); + if (!typeNode) { + return undefined; + } + if (isTypeReferenceNode(typeNode)) { + const genericType = type as GenericType; + if (genericType.typeArguments && typeNode.typeArguments) { + const cutoff = endOfRequiredTypeParameters(checker, genericType); + if (cutoff < typeNode.typeArguments.length) { + const newTypeArguments = factory.createNodeArray(typeNode.typeArguments.slice(0, cutoff)); + typeNode = factory.updateTypeReferenceNode(typeNode, typeNode.typeName, newTypeArguments); + } + } + } + return typeNode; +} + /** @internal */ export function typePredicateToAutoImportableTypeNode(checker: TypeChecker, importAdder: ImportAdder, typePredicate: TypePredicate, contextNode: Node | undefined, scriptTarget: ScriptTarget, flags?: NodeBuilderFlags, internalFlags?: InternalNodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined { let typePredicateNode = checker.typePredicateToTypePredicateNode(typePredicate, contextNode, flags, internalFlags, tracker); diff --git a/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports50-generics-with-default.ts b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports50-generics-with-default.ts new file mode 100644 index 0000000000000..8091543900ea2 --- /dev/null +++ b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports50-generics-with-default.ts @@ -0,0 +1,15 @@ +/// + +// @isolatedDeclarations: true +// @declaration: true +// @lib: es2015 +////let x: Iterator; +////export const y = x; + +verify.codeFix({ + description: "Add annotation of type 'Iterator'", + index: 0, + newFileContent: +`let x: Iterator; +export const y: Iterator = x;`, +}); diff --git a/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports51-slightly-more-complex-generics-with-default.ts b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports51-slightly-more-complex-generics-with-default.ts new file mode 100644 index 0000000000000..048125a895e89 --- /dev/null +++ b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports51-slightly-more-complex-generics-with-default.ts @@ -0,0 +1,19 @@ +/// + +// @isolatedDeclarations: true +// @declaration: true + +////export interface Foo {} +////export function foo(x: Foo) { +//// return x; +////} + +verify.codeFix({ + description: "Add return type 'Foo'", + index: 0, + newFileContent: +`export interface Foo {} +export function foo(x: Foo): Foo { + return x; +}`, +}); diff --git a/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports52-generics-oversimplification.ts b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports52-generics-oversimplification.ts new file mode 100644 index 0000000000000..41166c64ce918 --- /dev/null +++ b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports52-generics-oversimplification.ts @@ -0,0 +1,23 @@ +/// + +// In the abstract, we might prefer the inferred return type annotation to +// be identical to the parameter type (with 2 type parameters). +// Our current heuristic to avoid overly complex types in this case creates +// "overly simple" types, but this tradeoff seems reasonable. + +// @isolatedDeclarations: true +// @declaration: true +////export interface Foo {} +////export function foo(x: Foo) { +//// return x; +////} + +verify.codeFix({ + description: "Add return type 'Foo'", + index: 0, + newFileContent: +`export interface Foo {} +export function foo(x: Foo): Foo { + return x; +}`, +}); diff --git a/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports53-nested-generic-types.ts b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports53-nested-generic-types.ts new file mode 100644 index 0000000000000..cebddd3c390c4 --- /dev/null +++ b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports53-nested-generic-types.ts @@ -0,0 +1,21 @@ +/// + +// Our current heursitic to avoid overly verbose generic types +// doesn't handle generic types nested inside other types. + +// @isolatedDeclarations: true +// @declaration: true +////export interface Foo {} +////export function foo(x: Map>) { +//// return x; +////} + +verify.codeFix({ + description: "Add return type 'Map>'", + index: 0, + newFileContent: +`export interface Foo {} +export function foo(x: Map>): Map> { + return x; +}`, +}); diff --git a/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports54-generator-generics.ts b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports54-generator-generics.ts new file mode 100644 index 0000000000000..d0075bd14ee44 --- /dev/null +++ b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports54-generator-generics.ts @@ -0,0 +1,17 @@ +/// + +// @isolatedDeclarations: true +// @declaration: true +// @lib: es2015 +//// export function foo(x: Generator) { +//// return x; +//// } + +verify.codeFix({ + description: "Add return type 'Generator'", + index: 0, + newFileContent: +`export function foo(x: Generator): Generator { + return x; +}` +}); diff --git a/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports55-generator-return.ts b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports55-generator-return.ts new file mode 100644 index 0000000000000..5c9e3ade6adce --- /dev/null +++ b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports55-generator-return.ts @@ -0,0 +1,17 @@ +/// + +// @isolatedDeclarations: true +// @declaration: true +// @lib: es2015 +//// export function *foo() { +//// yield 5; +//// } + +verify.codeFix({ + description: "Add return type 'Generator'", + index: 0, + newFileContent: +`export function *foo(): Generator { + yield 5; +}` +});