diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 09f38832fe3b3..b3199f8dd8d9e 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1384,11 +1384,21 @@ export class Checker { * declared type cannot be determined the checker yields the error type (use * {@link Type.isErrorType} to detect it). */ - async getDeclaredTypeOfSymbol(symbol: Symbol): Promise { + async getDeclaredTypeOfSymbol(symbol: Symbol): Promise; + async getDeclaredTypeOfSymbol(symbols: readonly Symbol[]): Promise; + async getDeclaredTypeOfSymbol(symbolOrSymbols: Symbol | readonly Symbol[]): Promise { + if (Array.isArray(symbolOrSymbols)) { + const data = await this.client.apiRequest("getDeclaredTypesOfSymbols", { + snapshot: this.snapshotId, + project: this.project.id, + symbols: symbolOrSymbols.map(s => s.id), + }); + return data.map(d => this.objectRegistry.getOrCreateType(d)); + } const data = await this.client.apiRequest("getDeclaredTypeOfSymbol", { snapshot: this.snapshotId, project: this.project.id, - symbol: symbol.id, + symbol: (symbolOrSymbols as Symbol).id, }); return this.objectRegistry.getOrCreateType(data); } @@ -1859,11 +1869,21 @@ export class Checker { * an unresolved alias the checker yields the unknown symbol (use * {@link Checker.isUnknownSymbol} to detect it). */ - async getAliasedSymbol(symbol: Symbol): Promise { + async getAliasedSymbol(symbol: Symbol): Promise; + async getAliasedSymbol(symbols: readonly Symbol[]): Promise; + async getAliasedSymbol(symbolOrSymbols: Symbol | readonly Symbol[]): Promise { + if (Array.isArray(symbolOrSymbols)) { + const data = await this.client.apiRequest("getAliasedSymbols", { + snapshot: this.snapshotId, + project: this.project.id, + symbols: symbolOrSymbols.map(s => s.id), + }); + return data.map(d => this.objectRegistry.getOrCreateSymbol(d)); + } const data = await this.client.apiRequest("getAliasedSymbol", { snapshot: this.snapshotId, project: this.project.id, - symbol: symbol.id, + symbol: (symbolOrSymbols as Symbol).id, }); return this.objectRegistry.getOrCreateSymbol(data); } @@ -1880,11 +1900,21 @@ export class Checker { }); } - async getImmediateAliasedSymbol(symbol: Symbol): Promise { + async getImmediateAliasedSymbol(symbol: Symbol): Promise; + async getImmediateAliasedSymbol(symbols: readonly Symbol[]): Promise<(Symbol | undefined)[]>; + async getImmediateAliasedSymbol(symbolOrSymbols: Symbol | readonly Symbol[]): Promise { + if (Array.isArray(symbolOrSymbols)) { + const data = await this.client.apiRequest("getImmediateAliasedSymbols", { + snapshot: this.snapshotId, + project: this.project.id, + symbols: symbolOrSymbols.map(s => s.id), + }); + return data ? data.map(d => d ? this.objectRegistry.getOrCreateSymbol(d) : undefined) : symbolOrSymbols.map(() => undefined); + } const data = await this.client.apiRequest("getImmediateAliasedSymbol", { snapshot: this.snapshotId, project: this.project.id, - symbol: symbol.id, + symbol: (symbolOrSymbols as Symbol).id, }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } @@ -1945,21 +1975,44 @@ export class Checker { return signature.id === (await this.getWellKnownSignatures()).unknown; } - async getExportsOfModule(symbol: Symbol): Promise { + async getExportsOfModule(symbol: Symbol): Promise; + async getExportsOfModule(symbols: readonly Symbol[]): Promise; + async getExportsOfModule(symbolOrSymbols: Symbol | readonly Symbol[]): Promise { + if (Array.isArray(symbolOrSymbols)) { + const data = await this.client.apiRequest("getExportsOfModules", { + snapshot: this.snapshotId, + project: this.project.id, + symbols: symbolOrSymbols.map(s => s.id), + }); + return data.map(d => d ? d.map(s => this.objectRegistry.getOrCreateSymbol(s)) : []); + } const data = await this.client.apiRequest("getExportsOfModule", { snapshot: this.snapshotId, project: this.project.id, - symbol: symbol.id, + symbol: (symbolOrSymbols as Symbol).id, }); return data ? data.map(d => this.objectRegistry.getOrCreateSymbol(d)) : []; } - async getMemberInModuleExports(symbol: Symbol, name: string): Promise { + async getMemberInModuleExports(symbol: Symbol, name: string): Promise; + async getMemberInModuleExports(requests: readonly { symbol: Symbol; name: string; }[]): Promise<(Symbol | undefined)[]>; + async getMemberInModuleExports( + symbolOrRequests: Symbol | readonly { symbol: Symbol; name: string; }[], + name?: string, + ): Promise { + if (Array.isArray(symbolOrRequests)) { + const data = await this.client.apiRequest("getMembersInModuleExports", { + snapshot: this.snapshotId, + project: this.project.id, + requests: symbolOrRequests.map(r => ({ symbol: r.symbol.id, name: r.name })), + }); + return data.map(d => d ? this.objectRegistry.getOrCreateSymbol(d) : undefined); + } const data = await this.client.apiRequest("getMemberInModuleExports", { snapshot: this.snapshotId, project: this.project.id, - symbol: symbol.id, - name, + symbol: (symbolOrRequests as Symbol).id, + name: name!, }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 5e30d12bae247..a12bf21d7ffe4 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -32,6 +32,7 @@ export interface APIMethodInfo { getTypeOfSymbol: APIMethod; getTypesOfSymbols: APIMethod; getDeclaredTypeOfSymbol: APIMethod; + getDeclaredTypesOfSymbols: APIMethod; getSourceFile: APIMethod; getSourceFileNames: APIMethod; getSourceFileMetadata: APIMethod; @@ -104,10 +105,14 @@ export interface APIMethodInfo { getSignatureFromDeclaration: APIMethod; getExportSpecifierLocalTargetSymbol: APIMethod; getAliasedSymbol: APIMethod; + getAliasedSymbols: APIMethod; getImmediateAliasedSymbol: APIMethod; + getImmediateAliasedSymbols: APIMethod; getFullyQualifiedName: APIMethod; getExportsOfModule: APIMethod; + getExportsOfModules: APIMethod; getMemberInModuleExports: APIMethod; + getMembersInModuleExports: APIMethod; getJsDocTags: APIMethod; getDocumentationComment: APIMethod; isArrayType: APIMethod; @@ -678,6 +683,13 @@ export interface CheckerSymbolParams { symbol: number; } +/** CheckerSymbolsParams are parameters for checker methods that operate on a list of symbols. */ +export interface CheckerSymbolsParams { + snapshot: number; + project: string; + symbols: readonly number[] | null; +} + /** GetMemberInModuleExportsParams are parameters for getMemberInModuleExports. */ export interface GetMemberInModuleExportsParams { snapshot: number; @@ -686,6 +698,13 @@ export interface GetMemberInModuleExportsParams { name: string; } +/** GetMembersInModuleExportsParams are parameters for getMembersInModuleExports. */ +export interface GetMembersInModuleExportsParams { + snapshot: number; + project: string; + requests: readonly MemberInModuleExportsRequest[] | null; +} + /** * JSDocTagInfo is a single JSDoc tag, mirroring Strada's JSDocTagInfo but with the tag text * rendered as a plain string rather than SymbolDisplayPart[]. @@ -1021,6 +1040,11 @@ export interface ImportAdderAction { isValidTypeOnlyUseSite?: boolean; } +export interface MemberInModuleExportsRequest { + symbol: number; + name: string; +} + /** CompletionEntryResponse represents a single completion item. */ export interface CompletionEntryResponse { name: string; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 582c0b6c54359..b0eaee2c301d1 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1392,11 +1392,21 @@ export class Checker { * declared type cannot be determined the checker yields the error type (use * {@link Type.isErrorType} to detect it). */ - getDeclaredTypeOfSymbol(symbol: Symbol): Type { + getDeclaredTypeOfSymbol(symbol: Symbol): Type; + getDeclaredTypeOfSymbol(symbols: readonly Symbol[]): Type[]; + getDeclaredTypeOfSymbol(symbolOrSymbols: Symbol | readonly Symbol[]): Type | Type[] { + if (Array.isArray(symbolOrSymbols)) { + const data = this.client.apiRequest("getDeclaredTypesOfSymbols", { + snapshot: this.snapshotId, + project: this.project.id, + symbols: symbolOrSymbols.map(s => s.id), + }); + return data.map(d => this.objectRegistry.getOrCreateType(d)); + } const data = this.client.apiRequest("getDeclaredTypeOfSymbol", { snapshot: this.snapshotId, project: this.project.id, - symbol: symbol.id, + symbol: (symbolOrSymbols as Symbol).id, }); return this.objectRegistry.getOrCreateType(data); } @@ -1867,11 +1877,21 @@ export class Checker { * an unresolved alias the checker yields the unknown symbol (use * {@link Checker.isUnknownSymbol} to detect it). */ - getAliasedSymbol(symbol: Symbol): Symbol { + getAliasedSymbol(symbol: Symbol): Symbol; + getAliasedSymbol(symbols: readonly Symbol[]): Symbol[]; + getAliasedSymbol(symbolOrSymbols: Symbol | readonly Symbol[]): Symbol | Symbol[] { + if (Array.isArray(symbolOrSymbols)) { + const data = this.client.apiRequest("getAliasedSymbols", { + snapshot: this.snapshotId, + project: this.project.id, + symbols: symbolOrSymbols.map(s => s.id), + }); + return data.map(d => this.objectRegistry.getOrCreateSymbol(d)); + } const data = this.client.apiRequest("getAliasedSymbol", { snapshot: this.snapshotId, project: this.project.id, - symbol: symbol.id, + symbol: (symbolOrSymbols as Symbol).id, }); return this.objectRegistry.getOrCreateSymbol(data); } @@ -1888,11 +1908,21 @@ export class Checker { }); } - getImmediateAliasedSymbol(symbol: Symbol): Symbol | undefined { + getImmediateAliasedSymbol(symbol: Symbol): Symbol | undefined; + getImmediateAliasedSymbol(symbols: readonly Symbol[]): (Symbol | undefined)[]; + getImmediateAliasedSymbol(symbolOrSymbols: Symbol | readonly Symbol[]): Symbol | (Symbol | undefined)[] | undefined { + if (Array.isArray(symbolOrSymbols)) { + const data = this.client.apiRequest("getImmediateAliasedSymbols", { + snapshot: this.snapshotId, + project: this.project.id, + symbols: symbolOrSymbols.map(s => s.id), + }); + return data ? data.map(d => d ? this.objectRegistry.getOrCreateSymbol(d) : undefined) : symbolOrSymbols.map(() => undefined); + } const data = this.client.apiRequest("getImmediateAliasedSymbol", { snapshot: this.snapshotId, project: this.project.id, - symbol: symbol.id, + symbol: (symbolOrSymbols as Symbol).id, }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } @@ -1953,21 +1983,44 @@ export class Checker { return signature.id === (this.getWellKnownSignatures()).unknown; } - getExportsOfModule(symbol: Symbol): readonly Symbol[] { + getExportsOfModule(symbol: Symbol): readonly Symbol[]; + getExportsOfModule(symbols: readonly Symbol[]): readonly (readonly Symbol[])[]; + getExportsOfModule(symbolOrSymbols: Symbol | readonly Symbol[]): readonly Symbol[] | readonly (readonly Symbol[])[] { + if (Array.isArray(symbolOrSymbols)) { + const data = this.client.apiRequest("getExportsOfModules", { + snapshot: this.snapshotId, + project: this.project.id, + symbols: symbolOrSymbols.map(s => s.id), + }); + return data.map(d => d ? d.map(s => this.objectRegistry.getOrCreateSymbol(s)) : []); + } const data = this.client.apiRequest("getExportsOfModule", { snapshot: this.snapshotId, project: this.project.id, - symbol: symbol.id, + symbol: (symbolOrSymbols as Symbol).id, }); return data ? data.map(d => this.objectRegistry.getOrCreateSymbol(d)) : []; } - getMemberInModuleExports(symbol: Symbol, name: string): Symbol | undefined { + getMemberInModuleExports(symbol: Symbol, name: string): Symbol | undefined; + getMemberInModuleExports(requests: readonly { symbol: Symbol; name: string; }[]): (Symbol | undefined)[]; + getMemberInModuleExports( + symbolOrRequests: Symbol | readonly { symbol: Symbol; name: string; }[], + name?: string, + ): Symbol | (Symbol | undefined)[] | undefined { + if (Array.isArray(symbolOrRequests)) { + const data = this.client.apiRequest("getMembersInModuleExports", { + snapshot: this.snapshotId, + project: this.project.id, + requests: symbolOrRequests.map(r => ({ symbol: r.symbol.id, name: r.name })), + }); + return data.map(d => d ? this.objectRegistry.getOrCreateSymbol(d) : undefined); + } const data = this.client.apiRequest("getMemberInModuleExports", { snapshot: this.snapshotId, project: this.project.id, - symbol: symbol.id, - name, + symbol: (symbolOrRequests as Symbol).id, + name: name!, }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 24ae9c84f1eb8..e694a1f2a4d41 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -4355,6 +4355,143 @@ function f() { }); }); +describe("Checker - batched methods", () => { + test("getImmediateAliasedSymbol resolves multiple aliases", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/a.ts": `export const a = 1;`, + "/src/b.ts": `export const b = 2;`, + "/src/main.ts": `import { a } from "./a";\nimport { b } from "./b";\nexport const usage = a + b;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const posA = `import { a } from "./a";`.indexOf("a }"); + const posB = `import { a } from "./a";\nimport { b } from "./b";`.indexOf("b }"); + const symA = await project.checker.getSymbolAtPosition("/src/main.ts", posA); + const symB = await project.checker.getSymbolAtPosition("/src/main.ts", posB); + assert.ok(symA); + assert.ok(symB); + const results = await project.checker.getImmediateAliasedSymbol([symA, symB]); + assert.equal(results.length, 2); + assert.equal(results[0]?.name, "a"); + assert.equal(results[1]?.name, "b"); + } + finally { + await api.close(); + } + }); + + test("getAliasedSymbol resolves multiple import aliases", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/a.ts": `export const a = 1;`, + "/src/b.ts": `export const b = 2;`, + "/src/main.ts": `import { a } from "./a";\nimport { b } from "./b";`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const posA = `import { a } from "./a";`.indexOf("a }"); + const posB = `import { a } from "./a";\nimport { b } from "./b";`.indexOf("b }"); + const symA = await project.checker.getSymbolAtPosition("/src/main.ts", posA); + const symB = await project.checker.getSymbolAtPosition("/src/main.ts", posB); + assert.ok(symA); + assert.ok(symB); + const results = await project.checker.getAliasedSymbol([symA, symB]); + assert.equal(results.length, 2); + assert.equal(results[0].name, "a"); + assert.ok(!(results[0].flags & SymbolFlags.Alias)); + assert.equal(results[1].name, "b"); + assert.ok(!(results[1].flags & SymbolFlags.Alias)); + } + finally { + await api.close(); + } + }); + + test("getExportsOfModule returns exports for multiple modules", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/a.ts": `export const alpha = 1;`, + "/src/b.ts": `export const beta = 2;\nexport const gamma = 3;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sfA = await project.program.getSourceFile("/src/a.ts"); + const sfB = await project.program.getSourceFile("/src/b.ts"); + assert.ok(sfA); + assert.ok(sfB); + const modA = await project.checker.getSymbolAtLocation(sfA); + const modB = await project.checker.getSymbolAtLocation(sfB); + assert.ok(modA); + assert.ok(modB); + const results = await project.checker.getExportsOfModule([modA, modB]); + assert.equal(results.length, 2); + assert.deepEqual(results[0].map(e => e.name), ["alpha"]); + const namesB = results[1].map(e => e.name); + assert.ok(namesB.includes("beta")); + assert.ok(namesB.includes("gamma")); + } + finally { + await api.close(); + } + }); + + test("getDeclaredTypeOfSymbol returns types for multiple symbols", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `interface Foo { x: number; }\ninterface Bar { y: string; }`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `interface Foo { x: number; }\ninterface Bar { y: string; }`; + const posFoo = src.indexOf("Foo"); + const posBar = src.indexOf("Bar"); + const symFoo = await project.checker.getSymbolAtPosition("/src/main.ts", posFoo); + const symBar = await project.checker.getSymbolAtPosition("/src/main.ts", posBar); + assert.ok(symFoo); + assert.ok(symBar); + const types = await project.checker.getDeclaredTypeOfSymbol([symFoo, symBar]); + assert.equal(types.length, 2); + assert.ok(types[0].flags & TypeFlags.Object); + assert.ok(types[1].flags & TypeFlags.Object); + } + finally { + await api.close(); + } + }); + + test("getMemberInModuleExports resolves multiple members", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/index.ts": `export const alpha = 1;\nexport const beta = 2;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = await project.program.getSourceFile("/src/index.ts"); + assert.ok(sourceFile); + const moduleSymbol = await project.checker.getSymbolAtLocation(sourceFile); + assert.ok(moduleSymbol); + const results = await project.checker.getMemberInModuleExports([ + { symbol: moduleSymbol, name: "alpha" }, + { symbol: moduleSymbol, name: "missing" }, + { symbol: moduleSymbol, name: "beta" }, + ]); + assert.equal(results.length, 3); + assert.equal(results[0]?.name, "alpha"); + assert.equal(results[1], undefined); + assert.equal(results[2]?.name, "beta"); + } + finally { + await api.close(); + } + }); +}); + describe("Symbol - getDocumentationComment and getJsDocTags", () => { const docFiles = { "/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 93fe5eae5255f..dc0a71b50da9b 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -4363,6 +4363,143 @@ function f() { }); }); +describe("Checker - batched methods", () => { + test("getImmediateAliasedSymbol resolves multiple aliases", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/a.ts": `export const a = 1;`, + "/src/b.ts": `export const b = 2;`, + "/src/main.ts": `import { a } from "./a";\nimport { b } from "./b";\nexport const usage = a + b;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const posA = `import { a } from "./a";`.indexOf("a }"); + const posB = `import { a } from "./a";\nimport { b } from "./b";`.indexOf("b }"); + const symA = project.checker.getSymbolAtPosition("/src/main.ts", posA); + const symB = project.checker.getSymbolAtPosition("/src/main.ts", posB); + assert.ok(symA); + assert.ok(symB); + const results = project.checker.getImmediateAliasedSymbol([symA, symB]); + assert.equal(results.length, 2); + assert.equal(results[0]?.name, "a"); + assert.equal(results[1]?.name, "b"); + } + finally { + api.close(); + } + }); + + test("getAliasedSymbol resolves multiple import aliases", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/a.ts": `export const a = 1;`, + "/src/b.ts": `export const b = 2;`, + "/src/main.ts": `import { a } from "./a";\nimport { b } from "./b";`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const posA = `import { a } from "./a";`.indexOf("a }"); + const posB = `import { a } from "./a";\nimport { b } from "./b";`.indexOf("b }"); + const symA = project.checker.getSymbolAtPosition("/src/main.ts", posA); + const symB = project.checker.getSymbolAtPosition("/src/main.ts", posB); + assert.ok(symA); + assert.ok(symB); + const results = project.checker.getAliasedSymbol([symA, symB]); + assert.equal(results.length, 2); + assert.equal(results[0].name, "a"); + assert.ok(!(results[0].flags & SymbolFlags.Alias)); + assert.equal(results[1].name, "b"); + assert.ok(!(results[1].flags & SymbolFlags.Alias)); + } + finally { + api.close(); + } + }); + + test("getExportsOfModule returns exports for multiple modules", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/a.ts": `export const alpha = 1;`, + "/src/b.ts": `export const beta = 2;\nexport const gamma = 3;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sfA = project.program.getSourceFile("/src/a.ts"); + const sfB = project.program.getSourceFile("/src/b.ts"); + assert.ok(sfA); + assert.ok(sfB); + const modA = project.checker.getSymbolAtLocation(sfA); + const modB = project.checker.getSymbolAtLocation(sfB); + assert.ok(modA); + assert.ok(modB); + const results = project.checker.getExportsOfModule([modA, modB]); + assert.equal(results.length, 2); + assert.deepEqual(results[0].map(e => e.name), ["alpha"]); + const namesB = results[1].map(e => e.name); + assert.ok(namesB.includes("beta")); + assert.ok(namesB.includes("gamma")); + } + finally { + api.close(); + } + }); + + test("getDeclaredTypeOfSymbol returns types for multiple symbols", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `interface Foo { x: number; }\ninterface Bar { y: string; }`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const src = `interface Foo { x: number; }\ninterface Bar { y: string; }`; + const posFoo = src.indexOf("Foo"); + const posBar = src.indexOf("Bar"); + const symFoo = project.checker.getSymbolAtPosition("/src/main.ts", posFoo); + const symBar = project.checker.getSymbolAtPosition("/src/main.ts", posBar); + assert.ok(symFoo); + assert.ok(symBar); + const types = project.checker.getDeclaredTypeOfSymbol([symFoo, symBar]); + assert.equal(types.length, 2); + assert.ok(types[0].flags & TypeFlags.Object); + assert.ok(types[1].flags & TypeFlags.Object); + } + finally { + api.close(); + } + }); + + test("getMemberInModuleExports resolves multiple members", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/index.ts": `export const alpha = 1;\nexport const beta = 2;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = project.program.getSourceFile("/src/index.ts"); + assert.ok(sourceFile); + const moduleSymbol = project.checker.getSymbolAtLocation(sourceFile); + assert.ok(moduleSymbol); + const results = project.checker.getMemberInModuleExports([ + { symbol: moduleSymbol, name: "alpha" }, + { symbol: moduleSymbol, name: "missing" }, + { symbol: moduleSymbol, name: "beta" }, + ]); + assert.equal(results.length, 3); + assert.equal(results[0]?.name, "alpha"); + assert.equal(results[1], undefined); + assert.equal(results[2]?.name, "beta"); + } + finally { + api.close(); + } + }); +}); + describe("Symbol - getDocumentationComment and getJsDocTags", () => { const docFiles = { "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 8178bf0cbef04..91e3b21f0692c 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -82,6 +82,7 @@ const ( MethodGetTypeOfSymbol Method = "getTypeOfSymbol" MethodGetTypesOfSymbols Method = "getTypesOfSymbols" MethodGetDeclaredTypeOfSymbol Method = "getDeclaredTypeOfSymbol" + MethodGetDeclaredTypesOfSymbols Method = "getDeclaredTypesOfSymbols" MethodGetSourceFile Method = "getSourceFile" MethodGetSourceFileNames Method = "getSourceFileNames" MethodGetSourceFileMetadata Method = "getSourceFileMetadata" @@ -162,10 +163,14 @@ const ( MethodGetSignatureFromDeclaration Method = "getSignatureFromDeclaration" MethodGetExportSpecifierLocalTarget Method = "getExportSpecifierLocalTargetSymbol" MethodGetAliasedSymbol Method = "getAliasedSymbol" + MethodGetAliasedSymbols Method = "getAliasedSymbols" MethodGetImmediateAliasedSymbol Method = "getImmediateAliasedSymbol" + MethodGetImmediateAliasedSymbols Method = "getImmediateAliasedSymbols" MethodGetFullyQualifiedName Method = "getFullyQualifiedName" MethodGetExportsOfModule Method = "getExportsOfModule" + MethodGetExportsOfModules Method = "getExportsOfModules" MethodGetMemberInModuleExports Method = "getMemberInModuleExports" + MethodGetMembersInModuleExports Method = "getMembersInModuleExports" MethodGetJSDocTags Method = "getJsDocTags" MethodGetDocumentationComment Method = "getDocumentationComment" MethodIsArrayType Method = "isArrayType" @@ -427,6 +432,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], MethodGetTypesOfSymbols: unmarshallerFor[GetTypesOfSymbolsParams], MethodGetDeclaredTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], + MethodGetDeclaredTypesOfSymbols: unmarshallerFor[GetTypesOfSymbolsParams], MethodResolveName: unmarshallerFor[ResolveNameParams], MethodGetSymbolsInScope: unmarshallerFor[GetSymbolsInScopeParams], MethodGetSignaturesOfType: unmarshallerFor[GetSignaturesOfTypeParams], @@ -498,10 +504,14 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetSignatureFromDeclaration: unmarshallerFor[CheckerNodeParams], MethodGetExportSpecifierLocalTarget: unmarshallerFor[CheckerNodeParams], MethodGetAliasedSymbol: unmarshallerFor[CheckerSymbolParams], + MethodGetAliasedSymbols: unmarshallerFor[CheckerSymbolsParams], MethodGetImmediateAliasedSymbol: unmarshallerFor[CheckerSymbolParams], + MethodGetImmediateAliasedSymbols: unmarshallerFor[CheckerSymbolsParams], MethodGetFullyQualifiedName: unmarshallerFor[CheckerSymbolParams], MethodGetExportsOfModule: unmarshallerFor[CheckerSymbolParams], + MethodGetExportsOfModules: unmarshallerFor[CheckerSymbolsParams], MethodGetMemberInModuleExports: unmarshallerFor[GetMemberInModuleExportsParams], + MethodGetMembersInModuleExports: unmarshallerFor[GetMembersInModuleExportsParams], MethodGetJSDocTags: unmarshallerFor[CheckerSymbolParams], MethodGetDocumentationComment: unmarshallerFor[CheckerSymbolParams], MethodIsArrayType: unmarshallerFor[CheckerTypeParams], @@ -1359,6 +1369,25 @@ type CheckerSymbolParams struct { Symbol SymbolID `json:"symbol"` } +// CheckerSymbolsParams are parameters for checker methods that operate on a list of symbols. +type CheckerSymbolsParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + Symbols []SymbolID `json:"symbols"` +} + +type MemberInModuleExportsRequest struct { + Symbol SymbolID `json:"symbol"` + Name string `json:"name"` +} + +// GetMembersInModuleExportsParams are parameters for getMembersInModuleExports. +type GetMembersInModuleExportsParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + Requests []MemberInModuleExportsRequest `json:"requests"` +} + // JSDocTagInfo is a single JSDoc tag, mirroring Strada's JSDocTagInfo but with the tag text // rendered as a plain string rather than SymbolDisplayPart[]. type JSDocTagInfo struct { diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index e87e02c9b7ca3..06c449ebed1fc 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -653,6 +653,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetTypesOfSymbols(ctx, parsed.(*GetTypesOfSymbolsParams)) case string(MethodGetDeclaredTypeOfSymbol): return s.handleGetDeclaredTypeOfSymbol(ctx, parsed.(*GetTypeOfSymbolParams)) + case string(MethodGetDeclaredTypesOfSymbols): + return s.handleGetDeclaredTypesOfSymbols(ctx, parsed.(*GetTypesOfSymbolsParams)) case string(MethodResolveName): return s.handleResolveName(ctx, parsed.(*ResolveNameParams)) case string(MethodGetSymbolsInScope): @@ -799,14 +801,22 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetExportSpecifierLocalTargetSymbol(ctx, parsed.(*CheckerNodeParams)) case string(MethodGetAliasedSymbol): return s.handleGetAliasedSymbol(ctx, parsed.(*CheckerSymbolParams)) + case string(MethodGetAliasedSymbols): + return s.handleGetAliasedSymbols(ctx, parsed.(*CheckerSymbolsParams)) case string(MethodGetImmediateAliasedSymbol): return s.handleGetImmediateAliasedSymbol(ctx, parsed.(*CheckerSymbolParams)) + case string(MethodGetImmediateAliasedSymbols): + return s.handleGetImmediateAliasedSymbols(ctx, parsed.(*CheckerSymbolsParams)) case string(MethodGetFullyQualifiedName): return s.handleGetFullyQualifiedName(ctx, parsed.(*CheckerSymbolParams)) case string(MethodGetExportsOfModule): return s.handleGetExportsOfModule(ctx, parsed.(*CheckerSymbolParams)) + case string(MethodGetExportsOfModules): + return s.handleGetExportsOfModules(ctx, parsed.(*CheckerSymbolsParams)) case string(MethodGetMemberInModuleExports): return s.handleGetMemberInModuleExports(ctx, parsed.(*GetMemberInModuleExportsParams)) + case string(MethodGetMembersInModuleExports): + return s.handleGetMembersInModuleExports(ctx, parsed.(*GetMembersInModuleExportsParams)) case string(MethodGetJSDocTags): return s.handleGetJSDocTags(ctx, parsed.(*CheckerSymbolParams)) case string(MethodGetDocumentationComment): @@ -1640,6 +1650,26 @@ func (s *Session) handleGetDeclaredTypeOfSymbol(ctx context.Context, params *Get return setup.newTypeResponse(setup.checker.GetDeclaredTypeOfSymbol(symbol)), nil } +// handleGetDeclaredTypesOfSymbols returns the declared types of multiple symbols. +func (s *Session) handleGetDeclaredTypesOfSymbols(ctx context.Context, params *GetTypesOfSymbolsParams) ([]*TypeResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + results := make([]*TypeResponse, len(params.Symbols)) + for i, symHandle := range params.Symbols { + symbol, err := setup.resolveSymbolHandle(symHandle) + if err != nil { + return nil, err + } + results[i] = setup.newTypeResponse(setup.checker.GetDeclaredTypeOfSymbol(symbol)) + } + + return results, nil +} + // handleResolveName resolves a name to a symbol at a given location. // @gen-proto-nullable func (s *Session) handleResolveName(ctx context.Context, params *ResolveNameParams) (*SymbolResponse, error) { @@ -3256,6 +3286,26 @@ func (s *Session) handleGetAliasedSymbol(ctx context.Context, params *CheckerSym return setup.newSymbolResponse(setup.checker.GetAliasedSymbol(symbol)), nil } +// handleGetAliasedSymbols resolves multiple alias symbols to their targets. +func (s *Session) handleGetAliasedSymbols(ctx context.Context, params *CheckerSymbolsParams) ([]*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + results := make([]*SymbolResponse, len(params.Symbols)) + for i, symHandle := range params.Symbols { + symbol, err := setup.resolveSymbolHandle(symHandle) + if err != nil { + return nil, err + } + results[i] = setup.newSymbolResponse(setup.checker.GetAliasedSymbol(symbol)) + } + + return results, nil +} + // handleGetFullyQualifiedName returns the fully qualified name of a symbol // (e.g. `"/path/to/module".Namespace.Name`). func (s *Session) handleGetFullyQualifiedName(ctx context.Context, params *CheckerSymbolParams) (string, error) { @@ -3301,6 +3351,34 @@ func (s *Session) handleGetImmediateAliasedSymbol(ctx context.Context, params *C return setup.newSymbolResponse(aliased), nil } +// handleGetImmediateAliasedSymbols resolves one level of alias indirection for multiple symbols. +// @gen-proto-nullable +func (s *Session) handleGetImmediateAliasedSymbols(ctx context.Context, params *CheckerSymbolsParams) ([]*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + results := make([]*SymbolResponse, len(params.Symbols)) + for i, symHandle := range params.Symbols { + symbol, err := setup.resolveSymbolHandle(symHandle) + if err != nil { + return nil, err + } + if symbol == nil { + continue + } + aliased := setup.checker.GetImmediateAliasedSymbol(symbol) + if aliased == nil { + continue + } + results[i] = setup.newSymbolResponse(aliased) + } + + return results, nil +} + // handleGetExportsOfModule returns the resolved exports of a module symbol, // including those introduced by `export *` and re-exports. // @gen-proto-nullable @@ -3333,6 +3411,41 @@ func (s *Session) handleGetExportsOfModule(ctx context.Context, params *CheckerS return results, nil } +// handleGetExportsOfModules returns the resolved exports of multiple module symbols, +// including those introduced by `export *` and re-exports. +func (s *Session) handleGetExportsOfModules(ctx context.Context, params *CheckerSymbolsParams) ([][]*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + results := make([][]*SymbolResponse, len(params.Symbols)) + for i, symHandle := range params.Symbols { + symbol, err := setup.resolveSymbolHandle(symHandle) + if err != nil { + return nil, err + } + if symbol == nil { + continue + } + + exports := setup.checker.GetExportsOfModule(symbol) + if len(exports) == 0 { + continue + } + slices.SortFunc(exports, setup.checker.CompareSymbols) + + symbolResponses := make([]*SymbolResponse, len(exports)) + for j, exp := range exports { + symbolResponses[j] = setup.newSymbolResponse(exp) + } + results[i] = symbolResponses + } + + return results, nil +} + // handleGetMemberInModuleExports returns an export by name from a module symbol. // @gen-proto-nullable func (s *Session) handleGetMemberInModuleExports(ctx context.Context, params *GetMemberInModuleExportsParams) (*SymbolResponse, error) { @@ -3358,6 +3471,33 @@ func (s *Session) handleGetMemberInModuleExports(ctx context.Context, params *Ge return setup.newSymbolResponse(member), nil } +// handleGetMembersInModuleExports returns exports by name from module symbols. +func (s *Session) handleGetMembersInModuleExports(ctx context.Context, params *GetMembersInModuleExportsParams) ([]*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + results := make([]*SymbolResponse, len(params.Requests)) + for i, req := range params.Requests { + symbol, err := setup.resolveSymbolHandle(req.Symbol) + if err != nil { + return nil, err + } + if symbol == nil { + continue + } + member := setup.checker.TryGetMemberInModuleExports(req.Name, symbol) + if member == nil { + continue + } + results[i] = setup.newSymbolResponse(member) + } + + return results, nil +} + // handleGetJSDocTags returns the JSDoc tags of a symbol as structured name/text pairs. // @gen-proto-nullable func (s *Session) handleGetJSDocTags(ctx context.Context, params *CheckerSymbolParams) ([]*JSDocTagInfo, error) {