diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index c692742a8..4ab067fc9 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -164,7 +164,33 @@ export function declaredReturnType( if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && ts.isGetAccessorDeclaration(decl)) return DYN; const sig = lowerer.checker.getSignatureFromDeclaration(decl); if (!sig) lowerer.unsupported("SC1090", decl, "this function form"); - const retTsType = lowerer.checker.getReturnTypeOfSignature(sig); + let retTsType = lowerer.checker.getReturnTypeOfSignature(sig); + // An ASYNC object-literal method with no return annotation and a + // contextual signature takes its return from that signature, as an async + // arrow does: the body's `return` builds at the contextually typed + // literal, so the checker's own inferred literal (narrower than the + // contextual record) could never hold it. + if ( + ts.isMethodDeclaration(decl) && + ts.isObjectLiteralExpression(decl.parent) && + decl.type === undefined && + decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true + ) { + const contextual = lowerer.checker.getContextualType(decl.parent); + const slot = + contextual === undefined || (!ts.isIdentifier(decl.name) && !ts.isStringLiteral(decl.name)) + ? undefined + : lowerer.checker.getPropertyOfType( + lowerer.checker.getNonNullableType(contextual), + decl.name.text, + ); + const signatures = + slot === undefined + ? [] + : lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(slot)); + if (signatures.length === 1) + retTsType = lowerer.checker.getReturnTypeOfSignature(signatures[0]!); + } // An inferred null/undefined return in JS can read a mutable checked-value // field. Keep the actual value instead of restricting it to the initializer. if ( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/async-object-method-contextual-return.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/async-object-method-contextual-return.ts.json new file mode 100644 index 000000000..5338193d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/async-object-method-contextual-return.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/async-object-method-contextual-return.ts" + ], + "diags": [] +} diff --git a/tests/corpus/async-object-method-contextual-return.ts b/tests/corpus/async-object-method-contextual-return.ts new file mode 100644 index 000000000..223063079 --- /dev/null +++ b/tests/corpus/async-object-method-contextual-return.ts @@ -0,0 +1,14 @@ +type Text = { type: "text"; text: string; textSignature?: string }; +type Image = { type: "image"; data: string; mimeType: string }; +interface ToolResult { content: (Text | Image)[]; details: D; isError?: boolean } +interface ToolDefinition { name: string; execute(id: string, q: string): Promise> } +function define(): ToolDefinition<{ loaded: string[] }> { + return { + name: "search", + async execute(_id, q) { + const text = `found ${q}`; + return { content: [{ type: "text", text }], details: { loaded: [q] } }; + }, + }; +} +define().execute("1", "x").then((r) => console.log(JSON.stringify(r))); diff --git a/tests/harness/corpus-costs/async-object-method-contextual-return.ts.json b/tests/harness/corpus-costs/async-object-method-contextual-return.ts.json new file mode 100644 index 000000000..b0f0df482 --- /dev/null +++ b/tests/harness/corpus-costs/async-object-method-contextual-return.ts.json @@ -0,0 +1 @@ +{ "plain": 3268, "san": 6404 }