Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
getBaseTypeOfLiteralType,
getWidenedType,
getWidenedLiteralType,
fillMissingTypeArguments,
getTypeFromTypeNode: nodeIn => {
const node = getParseTreeNode(nodeIn, isTypeNode);
return node ? getTypeFromTypeNode(node) : errorType;
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5031,6 +5031,9 @@ export interface TypeCheckerHost extends ModuleSpecifierResolutionHost {
}

export interface TypeChecker {

Comment thread
blickly marked this conversation as resolved.
Outdated
/** @internal */
fillMissingTypeArguments(typeArguments: readonly Type[], typeParameters: readonly TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): Type[];
Comment thread
jakebailey marked this conversation as resolved.
Outdated
getTypeOfSymbolAtLocation(symbol: Symbol, node: Node): Type;
getTypeOfSymbol(symbol: Symbol): Type;
getDeclaredTypeOfSymbol(symbol: Symbol): Type;
Expand Down
11 changes: 8 additions & 3 deletions src/services/codefixes/fixMissingTypeAnnotationOnExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
eachDiagnostic,
registerCodeFix,
typePredicateToAutoImportableTypeNode,
typeToAutoImportableTypeNode,
typeToMinimizedReferenceType,
typeNodeToAutoImportableTypeNode,
} from "../_namespaces/ts.codefix.js";
import {
ArrayBindingPattern,
Expand Down Expand Up @@ -1096,9 +1097,9 @@ function withContext<T>(
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;
Expand All @@ -1107,6 +1108,10 @@ function withContext<T>(
isTruncated = true;
},
});
if (!minimizedTypeNode) {
return undefined;
}
const result = typeNodeToAutoImportableTypeNode(minimizedTypeNode, importAdder, scriptTarget);
return isTruncated ? factory.createKeywordTypeNode(SyntaxKind.AnyKeyword) : result;
}

Expand Down
50 changes: 49 additions & 1 deletion src/services/codefixes/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
flatMap,
FunctionDeclaration,
FunctionExpression,
GenericType,
GetAccessorDeclaration,
getAllAccessorDeclarations,
getCheckFlags,
Expand Down Expand Up @@ -59,6 +60,8 @@ import {
isSetAccessorDeclaration,
isStringLiteral,
isTypeNode,
isTypeReferenceNode,
TypeReferenceNode,
isTypeUsableAsPropertyName,
isYieldExpression,
LanguageServiceHost,
Expand Down Expand Up @@ -595,7 +598,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) {
Expand All @@ -608,6 +619,43 @@ export function typeToAutoImportableTypeNode(checker: TypeChecker, importAdder:
return getSynthesizedDeepClone(typeNode);
}

function endOfRequiredTypeParameters(checker: TypeChecker, type: GenericType, fullTypeArguments: readonly Type[]) : number {
if (fullTypeArguments !== type.typeArguments!) {
throw new Error('fullTypeArguments should be set')
Comment thread
jakebailey marked this conversation as resolved.
Outdated
}
const target = type.target;
next_cutoff: for (let cutoff = 0; cutoff < fullTypeArguments.length; cutoff++) {
const typeArguments = fullTypeArguments.slice(0, cutoff);
const filledIn = checker.fillMissingTypeArguments(typeArguments, target.typeParameters, cutoff, /*isJavaScriptImplicitAny*/ false);
for (let i = 0; i < filledIn.length; i++) {
// If they don't match, then we haven't yet reached the right cutoff
if (filledIn[i] !== fullTypeArguments[i]) continue next_cutoff;
}
return cutoff;
Comment thread
blickly marked this conversation as resolved.
Outdated
}
// If we make it all the way here, all the type arguments are required.
return fullTypeArguments.length;
}

export function typeToMinimizedReferenceType(checker: TypeChecker, type: Type, contextNode: Node | undefined, flags?: NodeBuilderFlags, internalFlags?: InternalNodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined {
const typeNode = checker.typeToTypeNode(type, contextNode, flags, internalFlags, tracker);
if (!typeNode) {
return undefined;
}
if (isTypeReferenceNode(typeNode)) {
const genericType = type as GenericType;
if (genericType.typeArguments) {
const cutoff = endOfRequiredTypeParameters(checker, genericType, genericType.typeArguments);
if (cutoff !== undefined && typeNode.typeArguments) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cutoff can't be undefined; should this instead be a check that skips this block if the length is different than the existing node?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Done.

// Looks like the wrong way to do this. What APIs should I use here?
(typeNode as any).typeArguments = typeNode.typeArguments.slice(0, cutoff);
Comment thread
jakebailey marked this conversation as resolved.
Outdated
}
}

}
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path='fourslash.ts'/>

// @isolatedDeclarations: true
// @declaration: true
// @file lib.d.ts
////interface MyIterator<T, TReturn = any, TNext = undefined> {}
////let x: MyIterator<number>;
////export const y = x;

verify.codeFix({
description: "Add annotation of type 'MyIterator<number>'",
index: 0,
newFileContent:
`interface MyIterator<T, TReturn = any, TNext = undefined> {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have tests for generators elsewhere? I seem to recall us auto-adding type long type annotations for those...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can add a case with Generator.

For reasons I don't fully understand, Generator<T> and Iterator<T> get generated with the full 3 type arguments, whereas Iterable<T> and IterableIterator<T> are special cased inside the TypeChecker to omit the extra arguments when unnecessary.

The special casing inside typeReferenceToTypeNode:

                            // Maybe we should do this for more types, but for now we only elide type arguments that are
                            // identical to their associated type parameters' defaults for `Iterable`, `IterableIterator`,
                            // `AsyncIterable`, and `AsyncIterableIterator` to provide backwards-compatible .d.ts emit due
                            // to each now having three type parameters instead of only one.
                            if (
                                isReferenceToType(type, getGlobalIterableType(/*reportErrors*/ false)) ||
                                isReferenceToType(type, getGlobalIterableIteratorType(/*reportErrors*/ false)) ||
                                isReferenceToType(type, getGlobalAsyncIterableType(/*reportErrors*/ false)) ||
                                isReferenceToType(type, getGlobalAsyncIterableIteratorType(/*reportErrors*/ false))
                            ) {
                                if (
                                    !type.node || !isTypeReferenceNode(type.node) || !type.node.typeArguments ||
                                    type.node.typeArguments.length < typeParameterCount
                                ) {
                                    while (typeParameterCount > 0) {
                                        const typeArgument = typeArguments[typeParameterCount - 1];
                                        const typeParameter = type.target.typeParameters[typeParameterCount - 1];
                                        const defaultType = getDefaultFromTypeParameter(typeParameter);
                                        if (!defaultType || !isTypeIdenticalTo(typeArgument, defaultType)) {
                                            break;
                                        }
                                        typeParameterCount--;
                                    }
                                }
                            }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was a part of #58243.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Thanks for the context.

let x: MyIterator<number>;
export const y: MyIterator<number> = x;`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <reference path='fourslash.ts'/>

// @isolatedDeclarations: true
// @declaration: true

////export interface Foo<T, U = T[]> {}
////export function foo(x: Foo<string>) {
//// return x;
////}

verify.codeFix({
description: "Add return type 'Foo<string>'",
index: 0,
newFileContent:
`export interface Foo<T, U = T[]> {}
export function foo(x: Foo<string>): Foo<string> {
return x;
}`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference path='fourslash.ts'/>
Comment thread
sandersn marked this conversation as resolved.

// In the abstract, we might prefer the inferred return type annotation to
// be identical to the parameter type (with 2 type parameters).
// Our current heursitic to avoid overly complex types in this case creates
Comment thread
blickly marked this conversation as resolved.
Outdated
// "overly simple" types, but this tradeoff seems reasonable.
Comment thread
sandersn marked this conversation as resolved.

// @isolatedDeclarations: true
// @declaration: true
////export interface Foo<T, U = T[]> {}
////export function foo(x: Foo<string, string[]>) {
//// return x;
////}

verify.codeFix({
description: "Add return type 'Foo<string>'",
index: 0,
newFileContent:
`export interface Foo<T, U = T[]> {}
export function foo(x: Foo<string, string[]>): Foo<string> {
return x;
}`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// <reference path='fourslash.ts'/>

// 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<T, U = T[]> {}
////export function foo(x: Map<number, Foo<string>>) {
//// return x;
////}

verify.codeFix({
description: "Add return type 'Map<number, Foo<string, string[]>>'",
index: 0,
newFileContent:
`export interface Foo<T, U = T[]> {}
export function foo(x: Map<number, Foo<string>>): Map<number, Foo<string, string[]>> {
return x;
}`,
});