From ff5ebb9c3a57304d5b434a4dffb04f731f68c52d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:22:35 +0000 Subject: [PATCH 1/5] Initial plan From dc4fa253ef0a0c638dfb3c787826b299b360d44f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:32:27 +0000 Subject: [PATCH 2/5] Add regression test for decorator contextual signature panic Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- packages/typescript/test/async/api.test.ts | 29 ++++++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 29 ++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 379f07b6b68f7..f4c99f02c81d8 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -10,6 +10,7 @@ import { InternalSymbolName, isCallExpression, isClassDeclaration, + isDecorator, isExpressionStatement, isFunctionDeclaration, isIdentifier, @@ -4941,6 +4942,34 @@ describe("Checker - getBaseTypeOfLiteralType", { concurrency }, () => { }); describe("Checker - getContextualType", { concurrency }, () => { + test("signatures of a decorator contextual type", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true } }), + "/src/main.ts": "class C { @((target, context) => target) get value(): number { return 1; } }", + }); + + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + const sourceFile = await project.program.getSourceFile("/src/main.ts"); + assert.ok(sourceFile); + + let expression: Expression | undefined; + sourceFile.forEachChild(function visit(node) { + if (isDecorator(node)) { + expression = node.expression; + } + node.forEachChild(visit); + }); + assert.ok(expression); + + const type = await project.checker.getContextualType(expression); + assert.ok(type); + const signatures = await project.checker.getSignaturesOfType(type, SignatureKind.Call); + assert.equal(signatures.length, 1); + assert.deepEqual((await signatures[0].getParameters()).map(parameter => parameter.name), ["target", "context"]); + assert.equal(signatures[0].declaration, undefined); + }); + test("contextual type from function parameter", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index f048e8ede87e6..de2a6a1579f58 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -18,6 +18,7 @@ import { InternalSymbolName, isCallExpression, isClassDeclaration, + isDecorator, isExpressionStatement, isFunctionDeclaration, isIdentifier, @@ -4766,6 +4767,34 @@ describe("Checker - getBaseTypeOfLiteralType", { concurrency }, () => { }); describe("Checker - getContextualType", { concurrency }, () => { + test("signatures of a decorator contextual type", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true } }), + "/src/main.ts": "class C { @((target, context) => target) get value(): number { return 1; } }", + }); + + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + const sourceFile = project.program.getSourceFile("/src/main.ts"); + assert.ok(sourceFile); + + let expression: Expression | undefined; + sourceFile.forEachChild(function visit(node) { + if (isDecorator(node)) { + expression = node.expression; + } + node.forEachChild(visit); + }); + assert.ok(expression); + + const type = project.checker.getContextualType(expression); + assert.ok(type); + const signatures = project.checker.getSignaturesOfType(type, SignatureKind.Call); + assert.equal(signatures.length, 1); + assert.deepEqual((signatures[0].getParameters()).map(parameter => parameter.name), ["target", "context"]); + assert.equal(signatures[0].declaration, undefined); + }); + test("contextual type from function parameter", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), From 869dce4097e479a7177ba292d9a4ae0b474cf5a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:37:04 +0000 Subject: [PATCH 3/5] Skip source-less signature declarations in API responses Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- tsc/internal/api/session.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index c88ef9a1b3bee..5c46aad60fd23 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -501,8 +501,8 @@ func (sd *snapshotData) newSignatureResponse(projectID project.Handle, sig *chec Flags: uint32(sig.Flags()), } - if sig.Declaration() != nil { - resp.Declaration = sd.nodeHandleFrom(sig.Declaration()) + if declaration := sig.Declaration(); declaration != nil && ast.GetSourceFileOfNode(declaration) != nil { + resp.Declaration = sd.nodeHandleFrom(declaration) } if len(sig.TypeParameters()) > 0 { From a6da8d9eabc50fb106488d93ba1dff796dfe499b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:57:35 +0000 Subject: [PATCH 4/5] Create serializable synthetic call-signature declarations Co-authored-by: weswigham <2932786+weswigham@users.noreply.github.com> --- packages/typescript/test/async/api.test.ts | 12 ++++++++++- packages/typescript/test/sync/api.test.ts | 12 ++++++++++- tsc/internal/api/session.go | 16 +++++++++++--- tsc/internal/checker/checker.go | 2 +- tsc/internal/checker/syntheticsignature.go | 25 ++++++++++++++++++++++ 5 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 tsc/internal/checker/syntheticsignature.go diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index f4c99f02c81d8..f04a12b5d74a8 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -4967,7 +4967,17 @@ describe("Checker - getContextualType", { concurrency }, () => { const signatures = await project.checker.getSignaturesOfType(type, SignatureKind.Call); assert.equal(signatures.length, 1); assert.deepEqual((await signatures[0].getParameters()).map(parameter => parameter.name), ["target", "context"]); - assert.equal(signatures[0].declaration, undefined); + const declarationHandle = signatures[0].declaration; + assert.ok(declarationHandle); + assert.equal(declarationHandle.kind, SyntaxKind.FunctionType); + const declaration = await declarationHandle.resolve(); + assert.ok(declaration); + assert.ok(isSignatureDeclaration(declaration)); + assert.equal(declaration.kind, SyntaxKind.FunctionType); + const declarationSignature = await project.checker.getSignatureFromDeclaration(declaration); + assert.ok(declarationSignature); + assert.strictEqual(declarationSignature.declaration, declarationHandle); + assert.deepEqual(await project.program.getSourceFileNames(), ["/src/main.ts"]); }); test("contextual type from function parameter", async () => { diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index de2a6a1579f58..158b4a81958af 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -4792,7 +4792,17 @@ describe("Checker - getContextualType", { concurrency }, () => { const signatures = project.checker.getSignaturesOfType(type, SignatureKind.Call); assert.equal(signatures.length, 1); assert.deepEqual((signatures[0].getParameters()).map(parameter => parameter.name), ["target", "context"]); - assert.equal(signatures[0].declaration, undefined); + const declarationHandle = signatures[0].declaration; + assert.ok(declarationHandle); + assert.equal(declarationHandle.kind, SyntaxKind.FunctionType); + const declaration = declarationHandle.resolve(); + assert.ok(declaration); + assert.ok(isSignatureDeclaration(declaration)); + assert.equal(declaration.kind, SyntaxKind.FunctionType); + const declarationSignature = project.checker.getSignatureFromDeclaration(declaration); + assert.ok(declarationSignature); + assert.strictEqual(declarationSignature.declaration, declarationHandle); + assert.deepEqual(project.program.getSourceFileNames(), ["/src/main.ts"]); }); test("contextual type from function parameter", () => { diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 5c46aad60fd23..1ec3b3c120d70 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -501,8 +501,8 @@ func (sd *snapshotData) newSignatureResponse(projectID project.Handle, sig *chec Flags: uint32(sig.Flags()), } - if declaration := sig.Declaration(); declaration != nil && ast.GetSourceFileOfNode(declaration) != nil { - resp.Declaration = sd.nodeHandleFrom(declaration) + if sig.Declaration() != nil { + resp.Declaration = sd.nodeHandleFrom(sig.Declaration()) } if len(sig.TypeParameters()) > 0 { @@ -2362,7 +2362,11 @@ func (s *Session) handleGetSourceFile(ctx context.Context, params *GetSourceFile return nil, err } - return s.encodeSourceFileResponse(program.GetSourceFile(params.File.ToFileName(program.BaseDirectory()))) + fileName := params.File.ToFileName(program.BaseDirectory()) + if fileName == checker.SyntheticSignatureFileName { + return s.encodeSourceFileResponse(checker.GetSyntheticSignatureSourceFile()) + } + return s.encodeSourceFileResponse(program.GetSourceFile(fileName)) } // handleGetConfigFileNames returns tsconfig file names associated with the project's command line. @@ -5029,6 +5033,12 @@ func (sd *snapshotData) resolveNodeHandle(program *compiler.Program, handle Node return nil, fmt.Errorf("%w: invalid node handle %q", ErrClientError, handle) } sourceFile := program.GetSourceFileByNodeID(ast.NodeId(nodeID)) + if sourceFile == nil { + syntheticFile := checker.GetSyntheticSignatureSourceFile() + if sourceFileNodeID(syntheticFile) == nodeID { + sourceFile = syntheticFile + } + } if sourceFile == nil { return nil, fmt.Errorf("%w: node handle %q could not be resolved (file may not be loaded or handle may be stale)", ErrClientError, handle) } diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 10ddb3c0c88b4..2f51cb9333cba 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -31161,7 +31161,7 @@ func (c *Checker) newSetterFunctionType(t *Type) *Type { // Creates a synthetic `Signature` corresponding to a call signature. func (c *Checker) newCallSignature(typeParameters []*Type, thisParameter *ast.Symbol, parameters []*ast.Symbol, returnType *Type) *Signature { - decl := c.factory.NewFunctionTypeNode(nil, nil, c.factory.NewKeywordTypeNode(ast.KindAnyKeyword)) + decl := GetSyntheticSignatureSourceFile().Statements.Nodes[0].Type() return c.newSignature(SignatureFlagsNone, decl, typeParameters, thisParameter, parameters, returnType, nil, len(parameters)) } diff --git a/tsc/internal/checker/syntheticsignature.go b/tsc/internal/checker/syntheticsignature.go new file mode 100644 index 0000000000000..0f871b085fd5f --- /dev/null +++ b/tsc/internal/checker/syntheticsignature.go @@ -0,0 +1,25 @@ +package checker + +import ( + "sync" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/parser" + "github.com/microsoft/TypeScript/tsc/internal/tspath" +) + +const SyntheticSignatureFileName tspath.RootedFilePath = "tsgo://synthetic/signatures.d.ts" + +var syntheticSignatureSourceFile = sync.OnceValue(func() *ast.SourceFile { + return parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: SyntheticSignatureFileName, + PathKey: tspath.CaseSensitive.PathKey(tspath.RootedPath(SyntheticSignatureFileName)), + }, "type SyntheticSignature = () => any;", core.ScriptKindTS) +}) + +// GetSyntheticSignatureSourceFile provides an immutable, shared AST for synthetic +// call-signature declarations. It is not an input file of any program. +func GetSyntheticSignatureSourceFile() *ast.SourceFile { + return syntheticSignatureSourceFile() +} From f7d20b6c377ffd5978c6eb5be740b71068c1cc86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:15:33 +0000 Subject: [PATCH 5/5] Preserve synthesized signature semantics in owned template AST Co-authored-by: weswigham <2932786+weswigham@users.noreply.github.com> --- tsc/internal/checker/checker.go | 2 ++ tsc/internal/checker/syntheticsignature.go | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 2f51cb9333cba..08be41746ae62 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -31161,6 +31161,8 @@ func (c *Checker) newSetterFunctionType(t *Type) *Type { // Creates a synthetic `Signature` corresponding to a call signature. func (c *Checker) newCallSignature(typeParameters []*Type, thisParameter *ast.Symbol, parameters []*ast.Symbol, returnType *Type) *Signature { + // The shared declaration supplies syntax and source-file identity only; + // parameter and return types are stored on the signature. decl := GetSyntheticSignatureSourceFile().Statements.Nodes[0].Type() return c.newSignature(SignatureFlagsNone, decl, typeParameters, thisParameter, parameters, returnType, nil, len(parameters)) } diff --git a/tsc/internal/checker/syntheticsignature.go b/tsc/internal/checker/syntheticsignature.go index 0f871b085fd5f..8cd5fd19b3517 100644 --- a/tsc/internal/checker/syntheticsignature.go +++ b/tsc/internal/checker/syntheticsignature.go @@ -12,10 +12,16 @@ import ( const SyntheticSignatureFileName tspath.RootedFilePath = "tsgo://synthetic/signatures.d.ts" var syntheticSignatureSourceFile = sync.OnceValue(func() *ast.SourceFile { - return parser.ParseSourceFile(ast.SourceFileParseOptions{ + file := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: SyntheticSignatureFileName, PathKey: tspath.CaseSensitive.PathKey(tspath.RootedPath(SyntheticSignatureFileName)), }, "type SyntheticSignature = () => any;", core.ScriptKindTS) + // Keep the placeholder synthesized so declaration serialization uses the + // signature's types rather than treating this as a bound source declaration. + declaration := file.Statements.Nodes[0].Type() + declaration.Loc = core.UndefinedTextRange() + declaration.Type().Loc = core.UndefinedTextRange() + return file }) // GetSyntheticSignatureSourceFile provides an immutable, shared AST for synthetic