diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 97087bb9cb7..d1877e334c7 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -2231,6 +2231,50 @@ export class Checker { this.objectRegistry.clear(); } + /** Get the merged symbol, or the original symbol if it has not been merged. */ + async getMergedSymbol(symbol: Symbol): Promise { + const data = await this.client.apiRequest("getMergedSymbol", { + snapshot: this.snapshotId, + project: this.project.id, + symbol: symbol.reference, + }); + return this.objectRegistry.getOrCreateSymbol(data); + } + + /** + * Get a node's merged symbol. For a declaration, prefer + * {@link Checker.getSymbolOfDeclaration} for a non-nullable result. + */ + async getSymbolOfNode(node: Node): Promise { + const data = await this.client.apiRequest("getSymbolOfNode", { + snapshot: this.snapshotId, + project: this.project.id, + location: getNodeId(node), + }); + return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; + } + + /** Get a declaration's merged symbol. */ + async getSymbolOfDeclaration(node: Declaration): Promise { + const data = await this.client.apiRequest("getSymbolOfDeclarationForChecker", { + snapshot: this.snapshotId, + project: this.project.id, + location: getNodeId(node), + }); + // Declarations normally have symbols, but preserve unexpected null results at runtime. + return (data && this.objectRegistry.getOrCreateSymbol(data))!; + } + + /** Get a symbol's merged parent, if any. */ + async getParentOfSymbol(symbol: Symbol): Promise { + const data = await this.client.apiRequest("getParentOfSymbolForChecker", { + snapshot: this.snapshotId, + project: this.project.id, + symbol: symbol.reference, + }); + return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; + } + getSymbolAtLocation(node: Node): Promise; getSymbolAtLocation(nodes: readonly Node[]): Promise<(Symbol | undefined)[]>; async getSymbolAtLocation(nodeOrNodes: Node | readonly Node[]): Promise { diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index cfe6c4ebf0a..bf9760a3a70 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -145,6 +145,10 @@ export interface APIMethodInfo { getAliasedSymbol: APIMethod; getImmediateAliasedSymbol: APIMethod; getTargetSymbol: APIMethod; + getMergedSymbol: APIMethod; + getSymbolOfNode: APIMethod; + getSymbolOfDeclarationForChecker: APIMethod; + getParentOfSymbolForChecker: APIMethod; getExportSymbolOfSymbolForChecker: APIMethod; getFullyQualifiedName: APIMethod; getExportsOfModule: APIMethod; @@ -1193,6 +1197,7 @@ export interface BatchRequest { | "getLocalTypeParametersOfType" | "getMemberInModuleExports" | "getMembersOfSymbol" + | "getMergedSymbol" | "getModeForResolutionAtIndex" | "getModeForUsageLocation" | "getNameTypeOfMappedType" @@ -1207,6 +1212,7 @@ export interface BatchRequest { | "getParameterType" | "getParametersOfSignature" | "getParentOfSymbol" + | "getParentOfSymbolForChecker" | "getProgramDiagnostics" | "getPropertiesOfType" | "getPropertyOfType" @@ -1234,6 +1240,8 @@ export interface BatchRequest { | "getSymbolAtLocation" | "getSymbolAtPosition" | "getSymbolOfDeclaration" + | "getSymbolOfDeclarationForChecker" + | "getSymbolOfNode" | "getSymbolOfSourceFile" | "getSymbolOfType" | "getSymbolsAtLocations" @@ -1371,6 +1379,7 @@ export interface BatchResponse { | "getLocalTypeParametersOfType" | "getMemberInModuleExports" | "getMembersOfSymbol" + | "getMergedSymbol" | "getModeForResolutionAtIndex" | "getModeForUsageLocation" | "getNameTypeOfMappedType" @@ -1385,6 +1394,7 @@ export interface BatchResponse { | "getParameterType" | "getParametersOfSignature" | "getParentOfSymbol" + | "getParentOfSymbolForChecker" | "getProgramDiagnostics" | "getPropertiesOfType" | "getPropertyOfType" @@ -1412,6 +1422,8 @@ export interface BatchResponse { | "getSymbolAtLocation" | "getSymbolAtPosition" | "getSymbolOfDeclaration" + | "getSymbolOfDeclarationForChecker" + | "getSymbolOfNode" | "getSymbolOfSourceFile" | "getSymbolOfType" | "getSymbolsAtLocations" diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index a7f33e4c892..460c80c7f7b 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -4068,6 +4068,123 @@ export class Checker { this.objectRegistry.clear(); } + /** Get the merged symbol, or the original symbol if it has not been merged. */ + get getMergedSymbol(): { + (symbol: Symbol): Symbol; + gen(symbol: Symbol): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getMergedSymbol", + function (symbol: Symbol): Symbol { + const data = owner.client.apiRequest("getMergedSymbol", { + snapshot: owner.snapshotId, + project: owner.project.id, + symbol: symbol.reference, + }); + return owner.objectRegistry.getOrCreateSymbol(data); + }, + function* (symbol: Symbol): Generator { + const data = yield* apiRequest("getMergedSymbol", { + snapshot: owner.snapshotId, + project: owner.project.id, + symbol: symbol.reference, + }); + return owner.objectRegistry.getOrCreateSymbol(data); + }, + ); + } + + /** + * Get a node's merged symbol. For a declaration, prefer + * {@link Checker.getSymbolOfDeclaration} for a non-nullable result. + */ + get getSymbolOfNode(): { + (node: Node): Symbol | undefined; + gen(node: Node): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getSymbolOfNode", + function (node: Node): Symbol | undefined { + const data = owner.client.apiRequest("getSymbolOfNode", { + snapshot: owner.snapshotId, + project: owner.project.id, + location: getNodeId(node), + }); + return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; + }, + function* (node: Node): Generator { + const data = yield* apiRequest("getSymbolOfNode", { + snapshot: owner.snapshotId, + project: owner.project.id, + location: getNodeId(node), + }); + return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; + }, + ); + } + + /** Get a declaration's merged symbol. */ + get getSymbolOfDeclaration(): { + (node: Declaration): Symbol; + gen(node: Declaration): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getSymbolOfDeclaration", + function (node: Declaration): Symbol { + const data = owner.client.apiRequest("getSymbolOfDeclarationForChecker", { + snapshot: owner.snapshotId, + project: owner.project.id, + location: getNodeId(node), + }); + // Declarations normally have symbols, but preserve unexpected null results at runtime. + return (data && owner.objectRegistry.getOrCreateSymbol(data))!; + }, + function* (node: Declaration): Generator { + const data = yield* apiRequest("getSymbolOfDeclarationForChecker", { + snapshot: owner.snapshotId, + project: owner.project.id, + location: getNodeId(node), + }); + // Declarations normally have symbols, but preserve unexpected null results at runtime. + return (data && owner.objectRegistry.getOrCreateSymbol(data))!; + }, + ); + } + + /** Get a symbol's merged parent, if any. */ + get getParentOfSymbol(): { + (symbol: Symbol): Symbol | undefined; + gen(symbol: Symbol): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getParentOfSymbol", + function (symbol: Symbol): Symbol | undefined { + const data = owner.client.apiRequest("getParentOfSymbolForChecker", { + snapshot: owner.snapshotId, + project: owner.project.id, + symbol: symbol.reference, + }); + return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; + }, + function* (symbol: Symbol): Generator { + const data = yield* apiRequest("getParentOfSymbolForChecker", { + snapshot: owner.snapshotId, + project: owner.project.id, + symbol: symbol.reference, + }); + return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; + }, + ); + } + get getSymbolAtLocation(): { (node: Node): Symbol | undefined; (nodes: readonly Node[]): (Symbol | undefined)[]; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 925ec43a062..82ba7c9f3ef 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -21,6 +21,7 @@ import { isNamedImports, isObjectLiteralExpression, isPropertyAssignment, + isPropertyDeclaration, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, @@ -505,7 +506,7 @@ describe("API", { concurrency }, () => { }); test("remote declarations lazily fetch and cache binder symbols", async () => { - const sourceText = "function present() {}\nimport {} from './missing';"; + const sourceText = "function present() {}"; await using api = spawnAPI({ "/symbols.ts": sourceText, }); @@ -514,9 +515,7 @@ describe("API", { concurrency }, () => { const sourceFile = await project.program.getSourceFile("/symbols.ts"); assert.ok(sourceFile); const declaration = sourceFile.statements[0]; - const withoutSymbol = sourceFile.statements[1]; assert.ok(isFunctionDeclaration(declaration)); - assert.ok(isImportDeclaration(withoutSymbol)); const checkerSymbol = await project.checker.getSymbolAtPosition("/symbols.ts", sourceText.indexOf("present")); // @sync: const checkerSymbol = project.checker.getSymbolAtPosition("/symbols.ts", sourceText.indexOf("present")); assert.ok(checkerSymbol); @@ -537,11 +536,6 @@ describe("API", { concurrency }, () => { assert.strictEqual(await getSymbol(declaration), first); // @sync: assert.strictEqual(getSymbol(declaration), first); assert.equal(symbolRequests, 1); - symbolRequests = 0; - await assert.rejects(getSymbol(withoutSymbol), /has no binder symbol/); // @sync: assert.throws(() => getSymbol(withoutSymbol), /has no binder symbol/); - await assert.rejects(api.getSymbol(withoutSymbol), /has no binder symbol/); // @sync: assert.throws(() => api.getSymbol(withoutSymbol), /has no binder symbol/); - assert.equal(symbolRequests, 2); - await snapshot.dispose(); }); @@ -563,7 +557,7 @@ describe("API", { concurrency }, () => { test("source files own separate declaration result and request caches", async () => { await using api = spawnAPI(); - await using lease = await api.createSourceFile("/symbols.ts", "function present() {}\nimport {} from './missing';"); + await using lease = await api.createSourceFile("/symbols.ts", "function present() {}"); const file = lease.sourceFile; assert.ok(file instanceof RemoteSourceFile); const cache = file.symbolCache; @@ -578,11 +572,6 @@ describe("API", { concurrency }, () => { assert.strictEqual(cache.symbolsByDeclarationNodeIndex.get(index), symbol); assert.strictEqual(cache.symbolsById.get(symbol.reference.id), symbol); assert.equal(cache.declarationSymbolRequests.has(index), false); - const withoutSymbol = cast(file.statements[1], isImportDeclaration); - const absentIndex = parseNodeHandle(getNodeId(withoutSymbol)).index; - await assert.rejects(getSymbol(withoutSymbol), /has no binder symbol/); // @sync: assert.throws(() => getSymbol(withoutSymbol), /has no binder symbol/); - assert.equal(cache.symbolsByDeclarationNodeIndex.has(absentIndex), false); - assert.equal(cache.declarationSymbolRequests.has(absentIndex), false); api.clearSourceFileCache(); assert.strictEqual(file.symbolCache, cache); }); @@ -3953,6 +3942,69 @@ export const value = 1; assert.notStrictEqual(second, first); }); + test("checker symbol methods merge declarations and parents in their project", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ files: ["/src/a.ts", "/src/b.ts"] }), + "/single/tsconfig.json": JSON.stringify({ files: ["/src/a.ts"] }), + "/src/a.ts": "namespace Merged { export const a = 1; }", + "/src/b.ts": "namespace Merged { export const b = 1; }", + }); + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json", "/single/tsconfig.json"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + const singleProject = snapshot.getConfiguredProject("/single/tsconfig.json")!; + const fileA = await project.program.getSourceFile("/src/a.ts"); + const fileB = await project.program.getSourceFile("/src/b.ts"); + assert.ok(fileA && fileB); + const declarationA = cast(fileA.statements[0], isModuleDeclaration); + const declarationB = cast(fileB.statements[0], isModuleDeclaration); + const rawA = await getSymbol(declarationA); + const rawB = await getSymbol(declarationB); + assert.notStrictEqual(rawA, rawB); + + const merged = await project.checker.getSymbolOfDeclaration(declarationA); + assert.strictEqual(await project.checker.getSymbolOfDeclaration(declarationB), merged); + assert.strictEqual(await project.checker.getSymbolOfNode(declarationA), merged); + assert.strictEqual(await project.checker.getMergedSymbol(rawA), merged); + assert.strictEqual(await project.checker.getMergedSymbol(rawB), merged); + assert.strictEqual(await project.checker.getMergedSymbol(merged), merged); + assert.strictEqual(await singleProject.checker.getMergedSymbol(rawA), rawA); + assert.strictEqual(await singleProject.checker.getSymbolOfDeclaration(declarationA), rawA); + assert.equal(await project.checker.getSymbolOfNode(declarationA.name), undefined); + assert.equal(await project.checker.getSymbolOfNode(fileA), undefined); + assert.equal(await project.checker.getParentOfSymbol(merged), undefined); + + const member = (await rawA.getExports()).get("a" as __String); + assert.ok(member); + assert.strictEqual(await member.getParent(), rawA); + assert.strictEqual(await project.checker.getParentOfSymbol(member), merged); + assert.strictEqual(await singleProject.checker.getParentOfSymbol(member), rawA); + }); + + test("checker symbol methods late-bind computed members", async () => { + await using api = spawnAPI({ + "/src/computed.ts": "declare const key: unique symbol;\nclass Container { [key] = 1; ordinary = 2; }\nimport {} from './missing';", + }); + const snapshot = await api.createSnapshot({ openFiles: ["/src/computed.ts"] }); + const project = snapshot.getProjects()[0]; + const file = await project.program.getSourceFile("/src/computed.ts"); + assert.ok(file); + const declaration = cast(file.statements[1], isClassDeclaration); + const memberDeclaration = cast(declaration.members[0], isPropertyDeclaration); + const raw = await getSymbol(memberDeclaration); + const lateBound = await project.checker.getSymbolOfDeclaration(memberDeclaration); + assert.notStrictEqual(lateBound, raw); + assert.strictEqual(await project.checker.getSymbolOfNode(memberDeclaration), lateBound); + const container = await project.checker.getSymbolOfDeclaration(declaration); + assert.strictEqual(await project.checker.getParentOfSymbol(lateBound), container); + const type = await project.checker.getDeclaredTypeOfSymbol(container); + const property = (await type.getProperties()).find(symbol => symbol.escapedName === lateBound.escapedName); + assert.ok(property); + assert.strictEqual(await project.checker.getTargetSymbol(property), lateBound); + const ordinary = cast(declaration.members[1], isPropertyDeclaration); + assert.strictEqual(await project.checker.getSymbolOfDeclaration(ordinary), await getSymbol(ordinary)); + assert.equal(await project.checker.getSymbolOfNode(file.statements[2]), undefined); + }); + test("snapshot-owned symbols resolve cached file-owned parents", async () => { const source = `interface Box { value: T; }\ndeclare const box: Box;\nbox;`; await using api = spawnAPI({ diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 971e926caf5..ac3fe07e3e0 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -1642,6 +1642,11 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Program", "getJavaScriptEmit", program.getJavaScriptEmit, assertDeepEquivalent, ["/src/index.ts"]), parityCase("Program", "getDeclarationEmit", program.getDeclarationEmit, assertDeepEquivalent, ["/src/index.ts"]), + parityCase("Checker", "getMergedSymbol", checker.getMergedSymbol, assertSymbolsEquivalent, interfaceSymbol), + parityCase("Checker", "getSymbolOfNode", checker.getSymbolOfNode, assertOptionalSymbolsEquivalent, interfaceDeclaration), + parityCase("Checker", "getSymbolOfNode", checker.getSymbolOfNode, assertOptionalSymbolsEquivalent, interfaceDeclaration.name), + parityCase("Checker", "getSymbolOfDeclaration", checker.getSymbolOfDeclaration, assertSymbolsEquivalent, interfaceDeclaration), + parityCase("Checker", "getParentOfSymbol", checker.getParentOfSymbol, assertOptionalSymbolsEquivalent, unimportedSymbol), parityCase("Checker", "getSymbolAtLocation", selectGeneratorMethod<[node: Node], Symbol | undefined>(checker.getSymbolAtLocation), assertOptionalSymbolsEquivalent, importedDerived), parityCase("Checker", "getSymbolAtLocation", checker.getSymbolAtLocation, assertOptionalSymbolArraysEquivalent, [importedDerived, combineDeclaration.name!]), parityCase("Checker", "getSymbolAtPosition", selectGeneratorMethod<[file: string, position: number], Symbol | undefined>(checker.getSymbolAtPosition), assertOptionalSymbolsEquivalent, "/src/index.ts", importedDerived.pos), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 68f8fbffbc0..fc30107edc4 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -29,6 +29,7 @@ import { isNamedImports, isObjectLiteralExpression, isPropertyAssignment, + isPropertyDeclaration, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, @@ -493,7 +494,7 @@ describe("API", { concurrency }, () => { }); test("remote declarations lazily fetch and cache binder symbols", () => { - const sourceText = "function present() {}\nimport {} from './missing';"; + const sourceText = "function present() {}"; using api = spawnAPI({ "/symbols.ts": sourceText, }); @@ -502,9 +503,7 @@ describe("API", { concurrency }, () => { const sourceFile = project.program.getSourceFile("/symbols.ts"); assert.ok(sourceFile); const declaration = sourceFile.statements[0]; - const withoutSymbol = sourceFile.statements[1]; assert.ok(isFunctionDeclaration(declaration)); - assert.ok(isImportDeclaration(withoutSymbol)); const checkerSymbol = project.checker.getSymbolAtPosition("/symbols.ts", sourceText.indexOf("present")); assert.ok(checkerSymbol); @@ -526,11 +525,6 @@ describe("API", { concurrency }, () => { assert.strictEqual(getSymbol(declaration), first); assert.equal(symbolRequests, 1); - symbolRequests = 0; - assert.throws(() => getSymbol(withoutSymbol), /has no binder symbol/); - assert.throws(() => api.getSymbol(withoutSymbol), /has no binder symbol/); - assert.equal(symbolRequests, 2); - snapshot.dispose(); }); @@ -552,7 +546,7 @@ describe("API", { concurrency }, () => { test("source files own separate declaration result and request caches", () => { using api = spawnAPI(); - using lease = api.createSourceFile("/symbols.ts", "function present() {}\nimport {} from './missing';"); + using lease = api.createSourceFile("/symbols.ts", "function present() {}"); const file = lease.sourceFile; assert.ok(file instanceof RemoteSourceFile); const cache = file.symbolCache; @@ -565,11 +559,6 @@ describe("API", { concurrency }, () => { assert.strictEqual(cache.symbolsByDeclarationNodeIndex.get(index), symbol); assert.strictEqual(cache.symbolsById.get(symbol.reference.id), symbol); assert.equal(cache.declarationSymbolRequests.has(index), false); - const withoutSymbol = cast(file.statements[1], isImportDeclaration); - const absentIndex = parseNodeHandle(getNodeId(withoutSymbol)).index; - assert.throws(() => getSymbol(withoutSymbol), /has no binder symbol/); - assert.equal(cache.symbolsByDeclarationNodeIndex.has(absentIndex), false); - assert.equal(cache.declarationSymbolRequests.has(absentIndex), false); api.clearSourceFileCache(); assert.strictEqual(file.symbolCache, cache); }); @@ -3778,6 +3767,69 @@ export const value = 1; assert.notStrictEqual(second, first); }); + test("checker symbol methods merge declarations and parents in their project", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ files: ["/src/a.ts", "/src/b.ts"] }), + "/single/tsconfig.json": JSON.stringify({ files: ["/src/a.ts"] }), + "/src/a.ts": "namespace Merged { export const a = 1; }", + "/src/b.ts": "namespace Merged { export const b = 1; }", + }); + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json", "/single/tsconfig.json"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + const singleProject = snapshot.getConfiguredProject("/single/tsconfig.json")!; + const fileA = project.program.getSourceFile("/src/a.ts"); + const fileB = project.program.getSourceFile("/src/b.ts"); + assert.ok(fileA && fileB); + const declarationA = cast(fileA.statements[0], isModuleDeclaration); + const declarationB = cast(fileB.statements[0], isModuleDeclaration); + const rawA = getSymbol(declarationA); + const rawB = getSymbol(declarationB); + assert.notStrictEqual(rawA, rawB); + + const merged = project.checker.getSymbolOfDeclaration(declarationA); + assert.strictEqual(project.checker.getSymbolOfDeclaration(declarationB), merged); + assert.strictEqual(project.checker.getSymbolOfNode(declarationA), merged); + assert.strictEqual(project.checker.getMergedSymbol(rawA), merged); + assert.strictEqual(project.checker.getMergedSymbol(rawB), merged); + assert.strictEqual(project.checker.getMergedSymbol(merged), merged); + assert.strictEqual(singleProject.checker.getMergedSymbol(rawA), rawA); + assert.strictEqual(singleProject.checker.getSymbolOfDeclaration(declarationA), rawA); + assert.equal(project.checker.getSymbolOfNode(declarationA.name), undefined); + assert.equal(project.checker.getSymbolOfNode(fileA), undefined); + assert.equal(project.checker.getParentOfSymbol(merged), undefined); + + const member = (rawA.getExports()).get("a" as __String); + assert.ok(member); + assert.strictEqual(member.getParent(), rawA); + assert.strictEqual(project.checker.getParentOfSymbol(member), merged); + assert.strictEqual(singleProject.checker.getParentOfSymbol(member), rawA); + }); + + test("checker symbol methods late-bind computed members", () => { + using api = spawnAPI({ + "/src/computed.ts": "declare const key: unique symbol;\nclass Container { [key] = 1; ordinary = 2; }\nimport {} from './missing';", + }); + const snapshot = api.createSnapshot({ openFiles: ["/src/computed.ts"] }); + const project = snapshot.getProjects()[0]; + const file = project.program.getSourceFile("/src/computed.ts"); + assert.ok(file); + const declaration = cast(file.statements[1], isClassDeclaration); + const memberDeclaration = cast(declaration.members[0], isPropertyDeclaration); + const raw = getSymbol(memberDeclaration); + const lateBound = project.checker.getSymbolOfDeclaration(memberDeclaration); + assert.notStrictEqual(lateBound, raw); + assert.strictEqual(project.checker.getSymbolOfNode(memberDeclaration), lateBound); + const container = project.checker.getSymbolOfDeclaration(declaration); + assert.strictEqual(project.checker.getParentOfSymbol(lateBound), container); + const type = project.checker.getDeclaredTypeOfSymbol(container); + const property = (type.getProperties()).find(symbol => symbol.escapedName === lateBound.escapedName); + assert.ok(property); + assert.strictEqual(project.checker.getTargetSymbol(property), lateBound); + const ordinary = cast(declaration.members[1], isPropertyDeclaration); + assert.strictEqual(project.checker.getSymbolOfDeclaration(ordinary), getSymbol(ordinary)); + assert.equal(project.checker.getSymbolOfNode(file.statements[2]), undefined); + }); + test("snapshot-owned symbols resolve cached file-owned parents", () => { const source = `interface Box { value: T; }\ndeclare const box: Box;\nbox;`; using api = spawnAPI({ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 3deb6189e2b..e1a815e247e 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -205,6 +205,10 @@ const ( MethodGetAliasedSymbol Method = "getAliasedSymbol" MethodGetImmediateAliasedSymbol Method = "getImmediateAliasedSymbol" MethodGetTargetSymbol Method = "getTargetSymbol" + MethodGetMergedSymbol Method = "getMergedSymbol" + MethodGetSymbolOfNode Method = "getSymbolOfNode" + MethodGetSymbolOfDeclarationForChecker Method = "getSymbolOfDeclarationForChecker" + MethodGetParentOfSymbolForChecker Method = "getParentOfSymbolForChecker" MethodGetExportSymbolOfSymbolForChecker Method = "getExportSymbolOfSymbolForChecker" MethodGetFullyQualifiedName Method = "getFullyQualifiedName" MethodGetExportsOfModule Method = "getExportsOfModule" @@ -709,6 +713,10 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetAliasedSymbol: unmarshallerFor[CheckerSymbolParams], MethodGetImmediateAliasedSymbol: unmarshallerFor[CheckerSymbolParams], MethodGetTargetSymbol: unmarshallerFor[CheckerSymbolParams], + MethodGetMergedSymbol: unmarshallerFor[CheckerSymbolParams], + MethodGetSymbolOfNode: unmarshallerFor[CheckerNodeParams], + MethodGetSymbolOfDeclarationForChecker: unmarshallerFor[CheckerNodeParams], + MethodGetParentOfSymbolForChecker: unmarshallerFor[CheckerSymbolParams], MethodGetExportSymbolOfSymbolForChecker: unmarshallerFor[CheckerSymbolParams], MethodGetFullyQualifiedName: unmarshallerFor[CheckerSymbolParams], MethodGetExportsOfModule: unmarshallerFor[CheckerSymbolParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index ae7de5c19ea..068a082a010 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -1155,6 +1155,14 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetImmediateAliasedSymbol(ctx, parsed.(*CheckerSymbolParams)) case string(MethodGetTargetSymbol): return s.handleMethodGetTargetSymbol(ctx, parsed.(*CheckerSymbolParams)) + case string(MethodGetMergedSymbol): + return s.handleGetMergedSymbol(ctx, parsed.(*CheckerSymbolParams)) + case string(MethodGetSymbolOfNode): + return s.handleGetSymbolOfNode(ctx, parsed.(*CheckerNodeParams)) + case string(MethodGetSymbolOfDeclarationForChecker): + return s.handleGetSymbolOfDeclarationForChecker(ctx, parsed.(*CheckerNodeParams)) + case string(MethodGetParentOfSymbolForChecker): + return s.handleGetParentOfSymbolForChecker(ctx, parsed.(*CheckerSymbolParams)) case string(MethodGetExportSymbolOfSymbolForChecker): return s.handleGetExportSymbolOfSymbolForChecker(ctx, parsed.(*CheckerSymbolParams)) case string(MethodGetFullyQualifiedName): @@ -4654,6 +4662,68 @@ func (s *Session) handleGetExportSpecifierLocalTargetSymbol(ctx context.Context, return setup.newSymbolResponse(symbol), nil } +func (s *Session) handleGetMergedSymbol(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.GetMergedSymbol(symbol)), nil +} + +// @gen-proto-nullable +func (s *Session) handleGetSymbolOfNode(ctx context.Context, params *CheckerNodeParams) (*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + node, err := setup.sd.resolveNodeHandle(setup.program, params.Location) + if err != nil { + return nil, err + } + + return setup.newSymbolResponse(setup.checker.GetSymbolOfNode(node)), nil +} + +// @gen-proto-nullable +func (s *Session) handleGetSymbolOfDeclarationForChecker(ctx context.Context, params *CheckerNodeParams) (*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + node, err := setup.sd.resolveNodeHandle(setup.program, params.Location) + if err != nil { + return nil, err + } + return setup.newSymbolResponse(setup.checker.GetSymbolOfDeclaration(node)), nil +} + +// @gen-proto-nullable +func (s *Session) handleGetParentOfSymbolForChecker(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.GetParentOfSymbol(symbol)), nil +} + // handleGetAliasedSymbol resolves an alias symbol to its target. func (s *Session) handleGetAliasedSymbol(ctx context.Context, params *CheckerSymbolParams) (*SymbolResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) diff --git a/tsc/internal/api/session_createsourcefile_test.go b/tsc/internal/api/session_createsourcefile_test.go index 0f7ee648adf..06e7237f3e1 100644 --- a/tsc/internal/api/session_createsourcefile_test.go +++ b/tsc/internal/api/session_createsourcefile_test.go @@ -190,13 +190,6 @@ func TestCreateSourceFile(t *testing.T) { assert.Equal(t, present.Name, "present") assert.Equal(t, present.Reference.Kind, SymbolOwnerKindFile) - absent, err := session.handleGetSymbolOfDeclaration(&GetSymbolOfDeclarationParams{ - File: descriptor, - Index: table.GetIndex(sourceFile.Statements.Nodes[1]), - }) - assert.ErrorContains(t, err, "has no binder symbol") - assert.Assert(t, absent == nil) - _, err = session.handleGetSymbolOfDeclaration(&GetSymbolOfDeclarationParams{ File: descriptor, Index: 0, diff --git a/tsc/internal/checker/exports.go b/tsc/internal/checker/exports.go index c22901a84d2..e38a55be571 100644 --- a/tsc/internal/checker/exports.go +++ b/tsc/internal/checker/exports.go @@ -104,6 +104,18 @@ func (c *Checker) GetMergedSymbol(symbol *ast.Symbol) *ast.Symbol { return c.getMergedSymbol(symbol) } +func (c *Checker) GetSymbolOfNode(node *ast.Node) *ast.Symbol { + return c.getSymbolOfNode(node) +} + +func (c *Checker) GetSymbolOfDeclaration(node *ast.Node) *ast.Symbol { + return c.getSymbolOfDeclaration(node) +} + +func (c *Checker) GetParentOfSymbol(symbol *ast.Symbol) *ast.Symbol { + return c.getParentOfSymbol(symbol) +} + func (c *Checker) TryFindAmbientModule(moduleName string) *ast.Symbol { return c.tryFindAmbientModule(moduleName, true /* withAugmentations */) }