diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index edcfe0a960c4c..7300ddf868ab9 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -295,8 +295,10 @@ 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)) @@ -304,11 +306,8 @@ namespace ts.codefix { 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); @@ -349,6 +348,35 @@ namespace ts.codefix { } } + interface ArgumentTypeParameterAndConstraint { + argumentType: Type; + constraint?: TypeNode; + } + + function createTypeParametersForArguments(checker: TypeChecker, argumentTypeParameters: [string, ArgumentTypeParameterAndConstraint | undefined][], typeArguments: NodeArray | 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)) { @@ -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(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() { ... } + const argumentTypeParameters = new Map(); + + 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(value: T | U & string) { + // added/*1*/(value); + // We don't want to output this: + // function added(value: T | U & string) { ... } + // We instead want to output: + // function added(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(value: T) { + // added/*1*/(value); + // We don't want to output this: + // function added(value: T) { ... } + // We instead want to output: + // function added(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(); 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); } diff --git a/tests/cases/fourslash/codeFixUndeclaredMethod.ts b/tests/cases/fourslash/codeFixUndeclaredMethod.ts index ac7a8b32b2f51..96d3f92390584 100644 --- a/tests/cases/fourslash/codeFixUndeclaredMethod.ts +++ b/tests/cases/fourslash/codeFixUndeclaredMethod.ts @@ -63,7 +63,7 @@ verify.codeFix({ // 8 type args this.foo3<1,2,3,4,5,6,7,8>(); } - foo3() { + foo3() { throw new Error("Method not implemented."); } foo2() { diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeKeyof.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeKeyof.ts new file mode 100644 index 0000000000000..ce95eb8cab030 --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeKeyof.ts @@ -0,0 +1,22 @@ +/// + +// @noImplicitAny: true +////function existing(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(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."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameter.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameter.ts new file mode 100644 index 0000000000000..9aebe3f9abab0 --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameter.ts @@ -0,0 +1,20 @@ +/// + +// @noImplicitAny: true +////function existing(value: T) { +//// added/*1*/(value); +////} + +goTo.marker("1"); +verify.codeFix({ + description: "Add missing function declaration 'added'", + index: 0, + newFileContent: `function existing(value: T) { + added(value); +} + +function added(value: T) { + throw new Error("Function not implemented."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterArgumentDifferent.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterArgumentDifferent.ts new file mode 100644 index 0000000000000..e42867758b224 --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterArgumentDifferent.ts @@ -0,0 +1,20 @@ +/// + +// @noImplicitAny: true +////function existing(value: T) { +//// added/*1*/(value, ""); +////} + +goTo.marker("1"); +verify.codeFix({ + description: "Add missing function declaration 'added'", + index: 0, + newFileContent: `function existing(value: T) { + added(value, ""); +} + +function added(value: T, arg1: string) { + throw new Error("Function not implemented."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterArgumentSame.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterArgumentSame.ts new file mode 100644 index 0000000000000..98ec122734d26 --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterArgumentSame.ts @@ -0,0 +1,20 @@ +/// + +// @noImplicitAny: true +////function existing(value: T) { +//// added/*1*/(value, value); +////} + +goTo.marker("1"); +verify.codeFix({ + description: "Add missing function declaration 'added'", + index: 0, + newFileContent: `function existing(value: T) { + added(value, value); +} + +function added(value: T, value1: T) { + throw new Error("Function not implemented."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterConstrained.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterConstrained.ts new file mode 100644 index 0000000000000..fb59a249b34a4 --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterConstrained.ts @@ -0,0 +1,20 @@ +/// + +// @noImplicitAny: true +////function existing(value: T) { +//// added/*1*/(value); +////} + +goTo.marker("1"); +verify.codeFix({ + description: "Add missing function declaration 'added'", + index: 0, + newFileContent: `function existing(value: T) { + added(value); +} + +function added(value: T) { + throw new Error("Function not implemented."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterNarrowed.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterNarrowed.ts new file mode 100644 index 0000000000000..8135300a459d7 --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterNarrowed.ts @@ -0,0 +1,24 @@ +/// + +// @noImplicitAny: true +////function existing(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(value: T) { + if (typeof value === "number") { + added(value); + } +} + +function added(value: T) { + throw new Error("Function not implemented."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterVariable.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterVariable.ts new file mode 100644 index 0000000000000..93b819724e29f --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameterVariable.ts @@ -0,0 +1,43 @@ +/// + +// @noImplicitAny: true +////function e() { +//// 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() { + 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() { + let et: T = 'phone' + added1(et) + et = 'home' + added2(et) +} + +function added1(et: string) { + throw new Error("Function not implemented.") +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameters.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameters.ts new file mode 100644 index 0000000000000..9a236190cd5cb --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParameters.ts @@ -0,0 +1,20 @@ +/// + +// @noImplicitAny: true +////function existing(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: T1, t: T, t2a: T2, t2b: T2, t2c: T2) { + added(t2a, t2b, t2c, t1, t, t2a, t2c, t2b); +} + +function added(t2a: T2, t2b: T2, t2c: T2, t1: T1, t: T, t2a1: T2, t2c1: T2, t2b1: T2) { + throw new Error("Function not implemented."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParametersConstrained.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParametersConstrained.ts new file mode 100644 index 0000000000000..9f8311ecec4f2 --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParametersConstrained.ts @@ -0,0 +1,20 @@ +/// + +// @noImplicitAny: true +////function existing(value: T, other: U) { +//// added/*1*/(value, other); +////} + +goTo.marker("1"); +verify.codeFix({ + description: "Add missing function declaration 'added'", + index: 0, + newFileContent: `function existing(value: T, other: U) { + added(value, other); +} + +function added(value: T, other: U) { + throw new Error("Function not implemented."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParametersNested2D.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParametersNested2D.ts new file mode 100644 index 0000000000000..1cddf611b431c --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParametersNested2D.ts @@ -0,0 +1,24 @@ +/// + +// @noImplicitAny: true +////function outer(o: O) { +//// return function inner(i: I) { +//// added/*1*/(o, i); +//// } +////} + +goTo.marker("1"); +verify.codeFix({ + description: "Add missing function declaration 'added'", + index: 0, + newFileContent: `function outer(o: O) { + return function inner(i: I) { + added(o, i); + } +} + +function added(o: O, i: I) { + throw new Error("Function not implemented."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParametersNested3D.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParametersNested3D.ts new file mode 100644 index 0000000000000..46f7d67bd69b0 --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeParametersNested3D.ts @@ -0,0 +1,28 @@ +/// + +// @noImplicitAny: true +////function outer(o: O) { +//// return function middle(m: M) { +//// return function inner(i: I) { +//// added/*1*/(o, m, i); +//// } +//// } +////} + +goTo.marker("1"); +verify.codeFix({ + description: "Add missing function declaration 'added'", + index: 0, + newFileContent: `function outer(o: O) { + return function middle(m: M) { + return function inner(i: I) { + added(o, m, i); + } + } +} + +function added(o: O, m: M, i: I) { + throw new Error("Function not implemented."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeUnion.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeUnion.ts new file mode 100644 index 0000000000000..967d94ac9ef65 --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeUnion.ts @@ -0,0 +1,20 @@ +/// + +// @noImplicitAny: true +////function existing(value: T | U & string) { +//// added/*1*/(value); +////} + +goTo.marker("1"); +verify.codeFix({ + description: "Add missing function declaration 'added'", + index: 0, + newFileContent: `function existing(value: T | U & string) { + added(value); +} + +function added(value: T) { + throw new Error("Function not implemented."); +} +`, +}); diff --git a/tests/cases/fourslash/incompleteFunctionCallCodefixTypeUnions.ts b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeUnions.ts new file mode 100644 index 0000000000000..8d761ff31585b --- /dev/null +++ b/tests/cases/fourslash/incompleteFunctionCallCodefixTypeUnions.ts @@ -0,0 +1,20 @@ +/// + +// @noImplicitAny: true +////function existing(value1: T | U & string, value2: U & T, value3: U | T, value4: U) { +//// added/*1*/(value1, value2, value3, value4); +////} + +goTo.marker("1"); +verify.codeFix({ + description: "Add missing function declaration 'added'", + index: 0, + newFileContent: `function existing(value1: T | U & string, value2: U & T, value3: U | T, value4: U) { + added(value1, value2, value3, value4); +} + +function added(value1: T, value2: U, value3: V, value4: U) { + throw new Error("Function not implemented."); +} +`, +});