Skip to content

Account for type parameters in missing function codefix #49727

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
151 changes: 142 additions & 9 deletions src/services/codefixes/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,20 +295,19 @@ namespace ts.codefix {
const contextualType = isJs ? undefined : checker.getContextualType(call);
const names = map(args, arg =>
isIdentifier(arg) ? arg.text : isPropertyAccessExpression(arg) && isIdentifier(arg.name) ? arg.name.text : undefined);
const types = isJs ? [] : map(args, arg =>
typeToAutoImportableTypeNode(checker, importAdder, checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(arg)), contextNode, scriptTarget, /*flags*/ undefined, tracker));
const instanceTypes = isJs ? [] : map(args, arg => checker.getTypeAtLocation(arg));
const { argumentTypeNodes, argumentTypeParameters } = getArgumentTypesAndTypeParameters(
checker, importAdder, instanceTypes, contextNode, scriptTarget, /*flags*/ undefined, tracker
);

const modifiers = modifierFlags
? factory.createNodeArray(factory.createModifiersFromModifierFlags(modifierFlags))
: undefined;
const asteriskToken = isYieldExpression(parent)
? factory.createToken(SyntaxKind.AsteriskToken)
: undefined;
const typeParameters = isJs || typeArguments === undefined
? undefined
: map(typeArguments, (_, i) =>
factory.createTypeParameterDeclaration(/*modifiers*/ undefined, CharacterCodes.T + typeArguments.length - 1 <= CharacterCodes.Z ? String.fromCharCode(CharacterCodes.T + i) : `T${i}`));
const parameters = createDummyParameters(args.length, names, types, /*minArgumentCount*/ undefined, isJs);
const typeParameters = isJs ? undefined : createTypeParametersForArguments(checker, argumentTypeParameters, typeArguments);
const parameters = createDummyParameters(args.length, names, argumentTypeNodes, /*minArgumentCount*/ undefined, isJs);
const type = isJs || contextualType === undefined
? undefined
: checker.typeToTypeNode(contextualType, contextNode, /*flags*/ undefined, tracker);
Expand Down Expand Up @@ -349,6 +348,35 @@ namespace ts.codefix {
}
}

interface ArgumentTypeParameterAndConstraint {
argumentType: Type;
constraint?: TypeNode;
}

function createTypeParametersForArguments(checker: TypeChecker, argumentTypeParameters: [string, ArgumentTypeParameterAndConstraint | undefined][], typeArguments: NodeArray<TypeNode> | undefined) {
const usedNames = new Set(argumentTypeParameters.map(pair => pair[0]));
const constraintsByName = new Map(argumentTypeParameters);

if (typeArguments) {
const typeArgumentsWithNewTypes = typeArguments.filter(typeArgument => !argumentTypeParameters.some(pair => checker.getTypeAtLocation(typeArgument) === pair[1]?.argumentType));
const targetSize = usedNames.size + typeArgumentsWithNewTypes.length;
for (let i = 0; usedNames.size < targetSize; i += 1) {
usedNames.add(createTypeParameterName(i));
}
}

return map(
arrayFrom(usedNames.values()),
usedName => factory.createTypeParameterDeclaration(/*modifiers*/ undefined, usedName, constraintsByName.get(usedName)?.constraint),
);
}

function createTypeParameterName(index: number) {
return CharacterCodes.T + index <= CharacterCodes.Z
? String.fromCharCode(CharacterCodes.T + index)
: `T${index}`;
}

export function typeToAutoImportableTypeNode(checker: TypeChecker, importAdder: ImportAdder, type: Type, contextNode: Node | undefined, scriptTarget: ScriptTarget, flags?: NodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined {
let typeNode = checker.typeToTypeNode(type, contextNode, flags, tracker);
if (typeNode && isImportTypeNode(typeNode)) {
Expand All @@ -358,19 +386,124 @@ namespace ts.codefix {
typeNode = importableReference.typeNode;
}
}

// Ensure nodes are fresh so they can have different positions when going through formatting.
return getSynthesizedDeepClone(typeNode);
}

function typeContainsTypeParameter(type: Type) {
if (type.isUnionOrIntersection()) {
return type.types.some(typeContainsTypeParameter);
}

return type.flags & TypeFlags.TypeParameter;
}

export function getArgumentTypesAndTypeParameters(checker: TypeChecker, importAdder: ImportAdder, instanceTypes: Type[], contextNode: Node | undefined, scriptTarget: ScriptTarget, flags?: NodeBuilderFlags, tracker?: SymbolTracker) {
// Types to be used as the types of the parameters in the new function
// E.g. from this source:
// added("", 0)
// The value will look like:
// [{ typeName: { text: "string" } }, { typeName: { text: "number" }]
// And in the output function will generate:
// function added(a: string, b: number) { ... }
const argumentTypeNodes: TypeNode[] = [];

// Names of type parameters provided as arguments to the call
// E.g. from this source:
// added<T, U>(value);
// The value will look like:
// [
// ["T", { argumentType: { typeName: { text: "T" } } } ],
// ["U", { argumentType: { typeName: { text: "U" } } } ],
// ]
// And in the output function will generate:
// function added<T, U>() { ... }
const argumentTypeParameters = new Map<string, ArgumentTypeParameterAndConstraint | undefined>();

for (let i = 0; i < instanceTypes.length; i += 1) {
const instanceType = instanceTypes[i];

// If the instance type contains a deep reference to an existing type parameter,
// instead of copying the full union or intersection, create a new type parameter
// E.g. from this source:
// function existing<T, U>(value: T | U & string) {
// added/*1*/(value);
// We don't want to output this:
// function added<T>(value: T | U & string) { ... }
// We instead want to output:
// function added<T>(value: T) { ... }
if (instanceType.isUnionOrIntersection() && instanceType.types.some(typeContainsTypeParameter)) {
const synthesizedTypeParameterName = createTypeParameterName(i);
argumentTypeNodes.push(factory.createTypeReferenceNode(synthesizedTypeParameterName));
argumentTypeParameters.set(synthesizedTypeParameterName, undefined);
continue;
}

// Widen the type so we don't emit nonsense annotations like "function fn(x: 3) {"
const widenedInstanceType = checker.getBaseTypeOfLiteralType(instanceType);
const argumentTypeNode = typeToAutoImportableTypeNode(checker, importAdder, widenedInstanceType, contextNode, scriptTarget, flags, tracker);
if (!argumentTypeNode) {
continue;
}

argumentTypeNodes.push(argumentTypeNode);
const argumentTypeParameter = getFirstTypeParameterName(instanceType);

// If the instance type is a type parameter with a constraint (other than an anonymous object),
// remember that constraint for when we create the new type parameter
// E.g. from this source:
// function existing<T extends string>(value: T) {
// added/*1*/(value);
// We don't want to output this:
// function added<T>(value: T) { ... }
// We instead want to output:
// function added<T extends string>(value: T) { ... }
const instanceTypeConstraint = instanceType.isTypeParameter() && instanceType.constraint && !isAnonymousObjectConstraintType(instanceType.constraint)
? typeToAutoImportableTypeNode(checker, importAdder, instanceType.constraint, contextNode, scriptTarget, flags, tracker)
: undefined;

if (argumentTypeParameter) {
argumentTypeParameters.set(argumentTypeParameter, { argumentType: instanceType, constraint: instanceTypeConstraint });
}
}

return { argumentTypeNodes, argumentTypeParameters: arrayFrom(argumentTypeParameters.entries()) };
}

function isAnonymousObjectConstraintType(type: Type) {
return (type.flags & TypeFlags.Object) && (type as ObjectType).objectFlags === ObjectFlags.Anonymous;
}

function getFirstTypeParameterName(type: Type): string | undefined {
if (type.flags & (TypeFlags.Union | TypeFlags.Intersection)) {
for (const subType of (type as UnionType | IntersectionType).types) {
const subTypeName = getFirstTypeParameterName(subType);
if (subTypeName) {
return subTypeName;
}
}
}

return type.flags & TypeFlags.TypeParameter
? type.getSymbol()?.getName()
: undefined;
}

function createDummyParameters(argCount: number, names: (string | undefined)[] | undefined, types: (TypeNode | undefined)[] | undefined, minArgumentCount: number | undefined, inJs: boolean): ParameterDeclaration[] {
const parameters: ParameterDeclaration[] = [];
const parameterNameCounts = new Map<string, number>();
for (let i = 0; i < argCount; i++) {
const parameterName = names?.[i] || `arg${i}`;
const parameterNameCount = parameterNameCounts.get(parameterName);
parameterNameCounts.set(parameterName, (parameterNameCount || 0) + 1);

const newParameter = factory.createParameterDeclaration(
/*modifiers*/ undefined,
/*dotDotDotToken*/ undefined,
/*name*/ names && names[i] || `arg${i}`,
/*name*/ parameterName + (parameterNameCount || ""),
/*questionToken*/ minArgumentCount !== undefined && i >= minArgumentCount ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
/*type*/ inJs ? undefined : types && types[i] || factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword),
/*type*/ inJs ? undefined : types?.[i] || factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword),
/*initializer*/ undefined);
parameters.push(newParameter);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/cases/fourslash/codeFixUndeclaredMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ verify.codeFix({
// 8 type args
this.foo3<1,2,3,4,5,6,7,8>();
}
foo3<T0, T1, T2, T3, T4, T5, T6, T7>() {
foo3<T, U, V, W, X, Y, Z, T7>() {
throw new Error("Method not implemented.");
}
foo2<T, U, V, W, X, Y, Z>() {
Expand Down
22 changes: 22 additions & 0 deletions tests/cases/fourslash/incompleteFunctionCallCodefixTypeKeyof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// <reference path='fourslash.ts' />

// @noImplicitAny: true
////function existing<T>(value: T) {
//// const keyofTypeof = Object.keys(value)[0] as keyof T;
//// added/*1*/(keyofTypeof);
////}

goTo.marker("1");
verify.codeFix({
description: "Add missing function declaration 'added'",
index: 0,
newFileContent: `function existing<T>(value: T) {
const keyofTypeof = Object.keys(value)[0] as keyof T;
added(keyofTypeof);
}

function added(keyofTypeof: string | number | symbol) {
throw new Error("Function not implemented.");
}
`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference path='fourslash.ts' />

// @noImplicitAny: true
////function existing<T>(value: T) {
//// added/*1*/(value);
////}

goTo.marker("1");
verify.codeFix({
description: "Add missing function declaration 'added'",
index: 0,
newFileContent: `function existing<T>(value: T) {
added(value);
}

function added<T>(value: T) {
throw new Error("Function not implemented.");
}
`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference path='fourslash.ts' />

// @noImplicitAny: true
////function existing<T>(value: T) {
//// added/*1*/<T, string>(value, "");
////}

goTo.marker("1");
verify.codeFix({
description: "Add missing function declaration 'added'",
index: 0,
newFileContent: `function existing<T>(value: T) {
added<T, string>(value, "");
}

function added<T, U>(value: T, arg1: string) {
throw new Error("Function not implemented.");
}
`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference path='fourslash.ts' />

// @noImplicitAny: true
////function existing<T>(value: T) {
//// added/*1*/<T>(value, value);
////}

goTo.marker("1");
verify.codeFix({
description: "Add missing function declaration 'added'",
index: 0,
newFileContent: `function existing<T>(value: T) {
added<T>(value, value);
}

function added<T>(value: T, value1: T) {
throw new Error("Function not implemented.");
}
`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference path='fourslash.ts' />

// @noImplicitAny: true
////function existing<T extends string>(value: T) {
//// added/*1*/(value);
////}

goTo.marker("1");
verify.codeFix({
description: "Add missing function declaration 'added'",
index: 0,
newFileContent: `function existing<T extends string>(value: T) {
added(value);
}

function added<T extends string>(value: T) {
throw new Error("Function not implemented.");
}
`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// <reference path='fourslash.ts' />

// @noImplicitAny: true
////function existing<T>(value: T) {
//// if (typeof value === "number") {
//// added/*1*/(value);
//// }
////}

goTo.marker("1");
verify.codeFix({
description: "Add missing function declaration 'added'",
index: 0,
newFileContent: `function existing<T>(value: T) {
if (typeof value === "number") {
added(value);
}
}

function added<T>(value: T) {
throw new Error("Function not implemented.");
}
`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/// <reference path='fourslash.ts' />

// @noImplicitAny: true
////function e<T extends "phone" | "home">() {
//// let et: T = 'phone'
//// added1/*1*/(et)
//// et = 'home'
//// added2/*2*/(et)
////}

goTo.marker("1");
verify.codeFix({
description: "Add missing function declaration 'added1'",
index: 0,
newFileContent: `function e<T extends "phone" | "home">() {
let et: T = 'phone'
added1(et)
et = 'home'
added2(et)
}

function added1(et: string) {
throw new Error("Function not implemented.")
}
`,
});

goTo.marker("2");
verify.codeFix({
description: "Add missing function declaration 'added1'",
index: 0,
newFileContent: `function e<T extends "phone" | "home">() {
let et: T = 'phone'
added1(et)
et = 'home'
added2(et)
}

function added1(et: string) {
throw new Error("Function not implemented.")
}
`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference path='fourslash.ts' />

// @noImplicitAny: true
////function existing<T1, T, T2>(t1: T1, t: T, t2a: T2, t2b: T2, t2c: T2) {
//// added/*1*/(t2a, t2b, t2c, t1, t, t2a, t2c, t2b);
////}

goTo.marker("1");
verify.codeFix({
description: "Add missing function declaration 'added'",
index: 0,
newFileContent: `function existing<T1, T, T2>(t1: T1, t: T, t2a: T2, t2b: T2, t2c: T2) {
added(t2a, t2b, t2c, t1, t, t2a, t2c, t2b);
}

function added<T2, T1, T>(t2a: T2, t2b: T2, t2c: T2, t1: T1, t: T, t2a1: T2, t2c1: T2, t2b1: T2) {
throw new Error("Function not implemented.");
}
`,
});
Loading