diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 379f07b6b68f7..f04a12b5d74a8 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,44 @@ 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"]); + 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 () => { 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..158b4a81958af 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,44 @@ 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"]); + 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", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index c88ef9a1b3bee..1ec3b3c120d70 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -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..08be41746ae62 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -31161,7 +31161,9 @@ 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)) + // 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 new file mode 100644 index 0000000000000..8cd5fd19b3517 --- /dev/null +++ b/tsc/internal/checker/syntheticsignature.go @@ -0,0 +1,31 @@ +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 { + 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 +// call-signature declarations. It is not an input file of any program. +func GetSyntheticSignatureSourceFile() *ast.SourceFile { + return syntheticSignatureSourceFile() +}