diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ad787fb62495d..a5b875ffd3354 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1895,6 +1895,21 @@ export class Checker { return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } + /** + * Get the target symbol if instantiated, or the provided symbol otherwise. + */ + async getTargetSymbol(symbol: Symbol): Promise { + if (symbol.checkFlags & CheckFlags.Instantiated) { + const data = await this.client.apiRequest("getTargetSymbol", { + snapshot: this.snapshotId, + project: this.project.id, + symbol: symbol.id, + }); + return this.objectRegistry.getOrCreateSymbol(data); + } + return symbol; + } + /** * Fetch (once, then cache) the handle ids of the per-checker singleton * symbols (unknown, undefined, arguments). These ids are stable for the life diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 35c08679d2aa4..7683935723e9e 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -106,6 +106,7 @@ export interface APIMethodInfo { getExportSpecifierLocalTargetSymbol: APIMethod; getAliasedSymbol: APIMethod; getImmediateAliasedSymbol: APIMethod; + getTargetSymbol: APIMethod; getFullyQualifiedName: APIMethod; getExportsOfModule: APIMethod; getMemberInModuleExports: APIMethod; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 074471e86cfbb..9601d3b521565 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1903,6 +1903,21 @@ export class Checker { return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } + /** + * Get the target symbol if instantiated, or the provided symbol otherwise. + */ + getTargetSymbol(symbol: Symbol): Symbol { + if (symbol.checkFlags & CheckFlags.Instantiated) { + const data = this.client.apiRequest("getTargetSymbol", { + snapshot: this.snapshotId, + project: this.project.id, + symbol: symbol.id, + }); + return this.objectRegistry.getOrCreateSymbol(data); + } + return symbol; + } + /** * Fetch (once, then cache) the handle ids of the per-checker singleton * symbols (unknown, undefined, arguments). These ids are stable for the life diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index fe08206bc486b..a02ec7fd3e5d9 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -7,6 +7,7 @@ import { getSynthesizedDeepClone, InternalSymbolName, isCallExpression, + isExpressionStatement, isFunctionDeclaration, isIdentifier, isImportDeclaration, @@ -403,6 +404,48 @@ describe("Checker - getImmediateAliasedSymbol", () => { }); }); +describe("Checker - getTargetSymbol", () => { + test("gets the target symbol of instantiated symbol", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +class Base { + private value!: T; +} +class Alpha extends Base {} +class Bravo extends Base {} + +declare function test(): void; +test(); +test(); +`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = await project.program.getSourceFile("/src/main.ts"); + assert.ok(sourceFile); + const nodes: Array = []; + sourceFile.forEachChild(node => { + if (isExpressionStatement(node) && isCallExpression(node.expression) && node.expression.typeArguments) { + nodes.push(node.expression.typeArguments[0]); + } + }); + const aType = await project.checker.getTypeAtLocation(nodes[0]); + const bType = await project.checker.getTypeAtLocation(nodes[1]); + const aProperty = (await project.checker.getPropertiesOfType(aType))[0]; + const bProperty = (await project.checker.getPropertiesOfType(bType))[0]; + assert.ok(aProperty); + assert.ok(bProperty); + assert.equal(aProperty === bProperty, false); + assert.equal(await project.checker.getTargetSymbol(aProperty) === await project.checker.getTargetSymbol(bProperty), true); + } + finally { + await api.close(); + } + }); +}); + describe("Snapshot", () => { test("updateSnapshot returns snapshot with projects", async () => { const api = spawnAPI(); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 6974061a4a6fb..b3125390d0879 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -15,6 +15,7 @@ import { getSynthesizedDeepClone, InternalSymbolName, isCallExpression, + isExpressionStatement, isFunctionDeclaration, isIdentifier, isImportDeclaration, @@ -411,6 +412,48 @@ describe("Checker - getImmediateAliasedSymbol", () => { }); }); +describe("Checker - getTargetSymbol", () => { + test("gets the target symbol of instantiated symbol", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +class Base { + private value!: T; +} +class Alpha extends Base {} +class Bravo extends Base {} + +declare function test(): void; +test(); +test(); +`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = project.program.getSourceFile("/src/main.ts"); + assert.ok(sourceFile); + const nodes: Array = []; + sourceFile.forEachChild(node => { + if (isExpressionStatement(node) && isCallExpression(node.expression) && node.expression.typeArguments) { + nodes.push(node.expression.typeArguments[0]); + } + }); + const aType = project.checker.getTypeAtLocation(nodes[0]); + const bType = project.checker.getTypeAtLocation(nodes[1]); + const aProperty = (project.checker.getPropertiesOfType(aType))[0]; + const bProperty = (project.checker.getPropertiesOfType(bType))[0]; + assert.ok(aProperty); + assert.ok(bProperty); + assert.equal(aProperty === bProperty, false); + assert.equal(project.checker.getTargetSymbol(aProperty) === project.checker.getTargetSymbol(bProperty), true); + } + finally { + api.close(); + } + }); +}); + describe("Snapshot", () => { test("updateSnapshot returns snapshot with projects", () => { const api = spawnAPI(); diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 45793d96a61be..6ed2ea5531a38 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -164,6 +164,7 @@ const ( MethodGetExportSpecifierLocalTarget Method = "getExportSpecifierLocalTargetSymbol" MethodGetAliasedSymbol Method = "getAliasedSymbol" MethodGetImmediateAliasedSymbol Method = "getImmediateAliasedSymbol" + MethodGetTargetSymbol Method = "getTargetSymbol" MethodGetFullyQualifiedName Method = "getFullyQualifiedName" MethodGetExportsOfModule Method = "getExportsOfModule" MethodGetMemberInModuleExports Method = "getMemberInModuleExports" @@ -501,6 +502,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetExportSpecifierLocalTarget: unmarshallerFor[CheckerNodeParams], MethodGetAliasedSymbol: unmarshallerFor[CheckerSymbolParams], MethodGetImmediateAliasedSymbol: unmarshallerFor[CheckerSymbolParams], + MethodGetTargetSymbol: unmarshallerFor[CheckerSymbolParams], MethodGetFullyQualifiedName: unmarshallerFor[CheckerSymbolParams], MethodGetExportsOfModule: unmarshallerFor[CheckerSymbolParams], MethodGetMemberInModuleExports: unmarshallerFor[GetMemberInModuleExportsParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index d5c668385b849..10aeff1673522 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -803,6 +803,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetAliasedSymbol(ctx, parsed.(*CheckerSymbolParams)) case string(MethodGetImmediateAliasedSymbol): return s.handleGetImmediateAliasedSymbol(ctx, parsed.(*CheckerSymbolParams)) + case string(MethodGetTargetSymbol): + return s.handleMethodGetTargetSymbol(ctx, parsed.(*CheckerSymbolParams)) case string(MethodGetFullyQualifiedName): return s.handleGetFullyQualifiedName(ctx, parsed.(*CheckerSymbolParams)) case string(MethodGetExportsOfModule): @@ -3319,6 +3321,23 @@ func (s *Session) handleGetImmediateAliasedSymbol(ctx context.Context, params *C return setup.newSymbolResponse(aliased), nil } +// handleGetTargetSymbol returns the target symbol if the symbol is instantiated, +// otherwise returns the provided symbol. +func (s *Session) handleMethodGetTargetSymbol(ctx context.Context, params *CheckerSymbolParams) (*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + symbol, err := setup.resolveSymbolHandle(params.Symbol) + if err != nil { + return nil, err + } + + return setup.newSymbolResponse(setup.checker.GetTargetSymbol(symbol)), nil +} + // handleGetExportsOfModule returns the resolved exports of a module symbol, // including those introduced by `export *` and re-exports. // @gen-proto-nullable diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index ba4c3fa2718b6..df115f4115ae1 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -21777,7 +21777,6 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s func (c *Checker) getTargetSymbol(s *ast.Symbol) *ast.Symbol { // if symbol is instantiated its flags are not copied from the 'target' // so we'll need to get back original 'target' symbol to work with correct set of flags - // NOTE: cast to TransientSymbol should be safe because only TransientSymbols have CheckFlags.Instantiated if s != nil && s.CheckFlags&ast.CheckFlagsInstantiated != 0 { return c.valueSymbolLinks.Get(s).target } diff --git a/tsc/internal/checker/exports.go b/tsc/internal/checker/exports.go index 49efc3208d430..13d0ccc5ad4c8 100644 --- a/tsc/internal/checker/exports.go +++ b/tsc/internal/checker/exports.go @@ -113,6 +113,10 @@ func (c *Checker) GetImmediateAliasedSymbol(symbol *ast.Symbol) *ast.Symbol { return c.getImmediateAliasedSymbol(symbol) } +func (c *Checker) GetTargetSymbol(symbol *ast.Symbol) *ast.Symbol { + return c.getTargetSymbol(symbol) +} + func (c *Checker) GetTypeOnlyAliasDeclaration(symbol *ast.Symbol) *ast.Node { return c.getTypeOnlyAliasDeclaration(symbol) }