Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/typescript/test/async/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
InternalSymbolName,
isCallExpression,
isClassDeclaration,
isDecorator,
isExpressionStatement,
isFunctionDeclaration,
isIdentifier,
Expand Down Expand Up @@ -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 } }),
Expand Down
39 changes: 39 additions & 0 deletions packages/typescript/test/sync/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
InternalSymbolName,
isCallExpression,
isClassDeclaration,
isDecorator,
isExpressionStatement,
isFunctionDeclaration,
isIdentifier,
Expand Down Expand Up @@ -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 } }),
Expand Down
12 changes: 11 additions & 1 deletion tsc/internal/api/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 3 additions & 1 deletion tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down
31 changes: 31 additions & 0 deletions tsc/internal/checker/syntheticsignature.go
Original file line number Diff line number Diff line change
@@ -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()
}