diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 0963630fbd9..b24b2405044 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -5979,6 +5979,10 @@ "category": "Message", "code": 95147 }, + "Infer function return type": { + "category": "Message", + "code": 95148 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/services/refactors/inferFunctionReturnType.ts b/src/services/refactors/inferFunctionReturnType.ts new file mode 100644 index 00000000000..3f46c0edb4c --- /dev/null +++ b/src/services/refactors/inferFunctionReturnType.ts @@ -0,0 +1,84 @@ +/* @internal */ +namespace ts.refactor.inferFunctionReturnType { + const refactorName = "Infer function return type"; + const refactorDescription = Diagnostics.Infer_function_return_type.message; + registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); + + function getEditsForAction(context: RefactorContext): RefactorEditInfo | undefined { + const info = getInfo(context); + if (info) { + const edits = textChanges.ChangeTracker.with(context, t => + t.tryInsertTypeAnnotation(context.file, info.declaration, info.returnTypeNode)); + return { renameFilename: undefined, renameLocation: undefined, edits }; + } + return undefined; + } + + function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { + const info = getInfo(context); + if (info) { + return [{ + name: refactorName, + description: refactorDescription, + actions: [{ + name: refactorName, + description: refactorDescription + }] + }]; + } + return emptyArray; + } + + type ConvertibleDeclaration = + | FunctionDeclaration + | FunctionExpression + | ArrowFunction + | MethodDeclaration; + + interface Info { + declaration: ConvertibleDeclaration; + returnTypeNode: TypeNode; + } + + function getInfo(context: RefactorContext): Info | undefined { + if (isInJSFile(context.file)) return; + + const token = getTokenAtPosition(context.file, context.startPosition); + const declaration = findAncestor(token, isConvertibleDeclaration); + if (!declaration || !declaration.body || declaration.type) return; + + const typeChecker = context.program.getTypeChecker(); + const returnType = tryGetReturnType(typeChecker, declaration); + if (!returnType) return; + + const returnTypeNode = typeChecker.typeToTypeNode(returnType, declaration, NodeBuilderFlags.NoTruncation); + if (returnTypeNode) { + return { declaration, returnTypeNode }; + } + } + + function isConvertibleDeclaration(node: Node): node is ConvertibleDeclaration { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.MethodDeclaration: + return true; + default: + return false; + } + } + + function tryGetReturnType(typeChecker: TypeChecker, node: ConvertibleDeclaration): Type | undefined { + if (typeChecker.isImplementationOfOverload(node)) { + const signatures = typeChecker.getTypeAtLocation(node).getCallSignatures(); + if (signatures.length > 1) { + return typeChecker.getUnionType(mapDefined(signatures, s => s.getReturnType())); + } + } + const signature = typeChecker.getSignatureFromDeclaration(node); + if (signature) { + return typeChecker.getReturnTypeOfSignature(signature); + } + } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 31683489ee1..079b8e6ed4f 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -121,6 +121,7 @@ "refactors/convertParamsToDestructuredObject.ts", "refactors/convertStringOrTemplateLiteral.ts", "refactors/convertArrowFunctionOrFunctionExpression.ts", + "refactors/inferFunctionReturnType.ts", "services.ts", "breakpoints.ts", "transform.ts", diff --git a/tests/cases/fourslash/refactorConvertExport_namedToDefault_alreadyHasDefault.ts b/tests/cases/fourslash/refactorConvertExport_namedToDefault_alreadyHasDefault.ts index 1e9faf5bd5c..d9f22cd59bb 100644 --- a/tests/cases/fourslash/refactorConvertExport_namedToDefault_alreadyHasDefault.ts +++ b/tests/cases/fourslash/refactorConvertExport_namedToDefault_alreadyHasDefault.ts @@ -5,4 +5,4 @@ ////export default function g() {} goTo.select("a", "b"); -verify.refactorsAvailable([]); +verify.refactorsAvailable(["Infer function return type"]); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType1.ts b/tests/cases/fourslash/refactorInferFunctionReturnType1.ts new file mode 100644 index 00000000000..6e46bb7deb5 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType1.ts @@ -0,0 +1,16 @@ +/// + +////function /*a*/foo/*b*/() { +//// return { x: 1, y: 1 }; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`function foo(): { x: number; y: number; } { + return { x: 1, y: 1 }; +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType10.ts b/tests/cases/fourslash/refactorInferFunctionReturnType10.ts new file mode 100644 index 00000000000..f2e7ebccf3e --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType10.ts @@ -0,0 +1,16 @@ +/// + +////function /*a*/foo/*b*/(x: number) { +//// return x ? x : x > 1; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`function foo(x: number): number | boolean { + return x ? x : x > 1; +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType11.ts b/tests/cases/fourslash/refactorInferFunctionReturnType11.ts new file mode 100644 index 00000000000..9503410e177 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType11.ts @@ -0,0 +1,36 @@ +/// + +////interface F1 { x: number; y: number; } +////type T1 = [number, number]; +//// +////function /*a*/foo/*b*/(num: number) { +//// switch (num) { +//// case 1: +//// return { x: num, y: num } as F1; +//// case 2: +//// return [num, num] as T1; +//// default: +//// return num; +//// } +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`interface F1 { x: number; y: number; } +type T1 = [number, number]; + +function foo(num: number): number | F1 | T1 { + switch (num) { + case 1: + return { x: num, y: num } as F1; + case 2: + return [num, num] as T1; + default: + return num; + } +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType12.ts b/tests/cases/fourslash/refactorInferFunctionReturnType12.ts new file mode 100644 index 00000000000..6363b71eca1 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType12.ts @@ -0,0 +1,22 @@ +/// + +////function f(x: number) { +//// return 1; +////} +////function /*a*/f/*b*/(x: number) { +//// return "1"; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`function f(x: number) { + return 1; +} +function f(x: number): string { + return "1"; +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType13.ts b/tests/cases/fourslash/refactorInferFunctionReturnType13.ts new file mode 100644 index 00000000000..55d37f1c880 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType13.ts @@ -0,0 +1,9 @@ +/// + +////function /*a*/f/*b*/(x: string): number; +////function f(x: string | number) { +//// return 1; +////} + +goTo.select("a", "b"); +verify.not.refactorAvailable("Infer function return type"); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType14.ts b/tests/cases/fourslash/refactorInferFunctionReturnType14.ts new file mode 100644 index 00000000000..feb3b9e168b --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType14.ts @@ -0,0 +1,20 @@ +/// + +////function f(x: number): string; +////function f(x: string): number; +////function /*a*/f/*b*/(x: string | number) { +//// return x === 1 ? 1 : "quit"; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`function f(x: number): string; +function f(x: string): number; +function f(x: string | number): string | number { + return x === 1 ? 1 : "quit"; +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType15.ts b/tests/cases/fourslash/refactorInferFunctionReturnType15.ts new file mode 100644 index 00000000000..9b04ba070c0 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType15.ts @@ -0,0 +1,26 @@ +/// + +////interface Foo { +//// x: number; +////} +////function f(x: number): Foo; +////function f(x: string): number; +////function /*a*/f/*b*/(x: string | number) { +//// return x === 1 ? 1 : { x }; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`interface Foo { + x: number; +} +function f(x: number): Foo; +function f(x: string): number; +function f(x: string | number): number | Foo { + return x === 1 ? 1 : { x }; +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType2.ts b/tests/cases/fourslash/refactorInferFunctionReturnType2.ts new file mode 100644 index 00000000000..ad7d51f01c8 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType2.ts @@ -0,0 +1,20 @@ +/// + +////class Foo { +//// /*a*/method/*b*/() { +//// return { x: 1, y: 1 }; +//// } +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`class Foo { + method(): { x: number; y: number; } { + return { x: 1, y: 1 }; + } +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType3.ts b/tests/cases/fourslash/refactorInferFunctionReturnType3.ts new file mode 100644 index 00000000000..42756122278 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType3.ts @@ -0,0 +1,16 @@ +/// + +////const foo = /*a*/function/*b*/() { +//// return { x: 1, y: 1 }; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`const foo = function(): { x: number; y: number; } { + return { x: 1, y: 1 }; +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType4.ts b/tests/cases/fourslash/refactorInferFunctionReturnType4.ts new file mode 100644 index 00000000000..ccc030f2d12 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType4.ts @@ -0,0 +1,16 @@ +/// + +////const foo = /*a*/()/*b*/ => { +//// return { x: 1, y: 1 }; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`const foo = (): { x: number; y: number; } => { + return { x: 1, y: 1 }; +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType5.ts b/tests/cases/fourslash/refactorInferFunctionReturnType5.ts new file mode 100644 index 00000000000..b007d729db7 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType5.ts @@ -0,0 +1,16 @@ +/// + +////function /*a*/foo/*b*/() { +//// return 1; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`function foo(): number { + return 1; +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType6.ts b/tests/cases/fourslash/refactorInferFunctionReturnType6.ts new file mode 100644 index 00000000000..56899ba099c --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType6.ts @@ -0,0 +1,16 @@ +/// + +////function /*a*/foo/*b*/() { +//// return ""; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`function foo(): string { + return ""; +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType7.ts b/tests/cases/fourslash/refactorInferFunctionReturnType7.ts new file mode 100644 index 00000000000..a0d64d36ca0 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType7.ts @@ -0,0 +1,14 @@ +/// + +////function /*a*/foo/*b*/() { +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`function foo(): void { +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType8.ts b/tests/cases/fourslash/refactorInferFunctionReturnType8.ts new file mode 100644 index 00000000000..c746ccc15c7 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType8.ts @@ -0,0 +1,18 @@ +/// + +////function /*a*/foo/*b*/() { +//// const bar = 1 as any; +//// return bar; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`function foo(): any { + const bar = 1 as any; + return bar; +}` +}); diff --git a/tests/cases/fourslash/refactorInferFunctionReturnType9.ts b/tests/cases/fourslash/refactorInferFunctionReturnType9.ts new file mode 100644 index 00000000000..f5b2b32aad8 --- /dev/null +++ b/tests/cases/fourslash/refactorInferFunctionReturnType9.ts @@ -0,0 +1,16 @@ +/// + +////function /*a*/foo/*b*/() { +//// return 1 as T; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Infer function return type", + actionName: "Infer function return type", + actionDescription: "Infer function return type", + newContent: +`function foo(): T { + return 1 as T; +}` +});