diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ad787fb62495d..9904fe2a08491 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1385,11 +1385,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); } @@ -1865,11 +1875,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); } @@ -1886,11 +1906,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; } @@ -1951,21 +1981,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 35c08679d2aa4..a9d70ec019dc4 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; @@ -105,10 +106,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; @@ -679,6 +684,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; @@ -687,6 +699,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[]. @@ -1022,6 +1041,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 074471e86cfbb..36a7a02bf869c 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1393,11 +1393,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); } @@ -1873,11 +1883,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); } @@ -1894,11 +1914,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; } @@ -1959,21 +1989,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 fe08206bc486b..4acb5cc33da2e 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -4397,6 +4397,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 6974061a4a6fb..f6debbc897c33 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -4405,6 +4405,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/tools/gen-proto/main.go b/tools/gen-proto/main.go index 714c5a32652b8..802abc669aeba 100644 --- a/tools/gen-proto/main.go +++ b/tools/gen-proto/main.go @@ -35,12 +35,13 @@ func run() int { } type methodInfo struct { - name string - params types.Type - result types.Type - paramsText string - resultText string - resultNullable bool + name string + params types.Type + result types.Type + paramsText string + resultText string + resultNullable bool + resultNullableElements bool } func generate(inputPath string, outputPath string) error { @@ -76,11 +77,11 @@ func generate(inputPath string, outputPath string) error { if err != nil { return err } - resultTypeOverrides, nullableResults, err := discoverResultMetadata(pkg) + resultTypeOverrides, nullableResults, nullableResultElements, err := discoverResultMetadata(pkg) if err != nil { return err } - discoverSessionMethods(pkg, methodObjects, methods, resultTypeOverrides, nullableResults) + discoverSessionMethods(pkg, methodObjects, methods, resultTypeOverrides, nullableResults, nullableResultElements) discoverConnectionMethods(pkg, methodObjects, methods) for _, method := range methods { @@ -111,7 +112,7 @@ func generate(inputPath string, outputPath string) error { } result := method.resultText if result == "" { - result = renderer.resultType(method.result, method.resultNullable) + result = renderer.resultType(method.result, method.resultNullable, method.resultNullableElements) } fmt.Fprintf(&methodsOut, " %s: APIMethod<%s, %s>;\n", propertyName(method.name), params, result) } @@ -185,11 +186,13 @@ func declaredMethods(pkg *packages.Package, file *ast.File) ([]*methodInfo, map[ return methods, methodObjects, nil } -func discoverResultMetadata(pkg *packages.Package) (map[types.Object]types.Type, map[types.Object]bool, error) { +func discoverResultMetadata(pkg *packages.Package) (map[types.Object]types.Type, map[types.Object]bool, map[types.Object]bool, error) { const resultDirective = "@gen-proto-result:" const nullableDirective = "@gen-proto-nullable" + const nullableElementsDirective = "@gen-proto-nullable-element" overrides := make(map[types.Object]types.Type) nullableResults := make(map[types.Object]bool) + nullableResultElements := make(map[types.Object]bool) for _, file := range pkg.Syntax { for _, decl := range file.Decls { fn, ok := decl.(*ast.FuncDecl) @@ -203,6 +206,10 @@ func discoverResultMetadata(pkg *packages.Package) (map[types.Object]types.Type, nullableResults[fnObject] = true continue } + if text == nullableElementsDirective { + nullableResultElements[fnObject] = true + continue + } typeName, ok := strings.CutPrefix(text, resultDirective) if !ok { continue @@ -210,16 +217,16 @@ func discoverResultMetadata(pkg *packages.Package) (map[types.Object]types.Type, typeName = strings.TrimSpace(typeName) typeObject, ok := pkg.Types.Scope().Lookup(typeName).(*types.TypeName) if !ok { - return nil, nil, fmt.Errorf("result type override on %s refers to unknown type %q", fn.Name.Name, typeName) + return nil, nil, nil, fmt.Errorf("result type override on %s refers to unknown type %q", fn.Name.Name, typeName) } overrides[fnObject] = typeObject.Type() } } } - return overrides, nullableResults, nil + return overrides, nullableResults, nullableResultElements, nil } -func discoverSessionMethods(pkg *packages.Package, methodObjects map[types.Object]*methodInfo, methods []*methodInfo, resultTypeOverrides map[types.Object]types.Type, nullableResults map[types.Object]bool) { +func discoverSessionMethods(pkg *packages.Package, methodObjects map[types.Object]*methodInfo, methods []*methodInfo, resultTypeOverrides map[types.Object]types.Type, nullableResults map[types.Object]bool, nullableResultElements map[types.Object]bool) { for _, file := range pkg.Syntax { for _, decl := range file.Decls { fn, isFuncDecl := decl.(*ast.FuncDecl) @@ -267,6 +274,7 @@ func discoverSessionMethods(pkg *packages.Package, methodObjects map[types.Objec method.result = override } method.resultNullable = nullableResults[called] + method.resultNullableElements = nullableResultElements[called] } return false }) @@ -418,21 +426,21 @@ func (r *typeRenderer) requestType(t types.Type) string { if pointer, ok := types.Unalias(t).(*types.Pointer); ok { t = pointer.Elem() } - return r.typeString(t, false) + return r.typeString(t, false, false) } -func (r *typeRenderer) resultType(t types.Type, nullable bool) string { +func (r *typeRenderer) resultType(t types.Type, nullable bool, nullableElements bool) string { if pointer, ok := types.Unalias(t).(*types.Pointer); ok { t = pointer.Elem() } - result := r.typeString(t, false) + result := r.typeString(t, false, nullableElements) if nullable { result += " | null" } return result } -func (r *typeRenderer) typeString(t types.Type, allowNull bool) string { +func (r *typeRenderer) typeString(t types.Type, allowNull bool, nullableElements bool) string { if t == nil { return "void" } @@ -442,13 +450,13 @@ func (r *typeRenderer) typeString(t types.Type, allowNull bool) string { case *types.Basic: result = basicType(t) case *types.Pointer: - result = r.typeString(t.Elem(), false) + result = r.typeString(t.Elem(), false, false) case *types.Slice: - result = arrayElement(r.typeString(t.Elem(), false)) + "[]" + result = arrayElement(r.typeString(t.Elem(), nullableElements, false)) + "[]" case *types.Array: - result = arrayElement(r.typeString(t.Elem(), false)) + "[]" + result = arrayElement(r.typeString(t.Elem(), nullableElements, false)) + "[]" case *types.Map: - result = fmt.Sprintf("Record", r.typeString(t.Elem(), true)) + result = fmt.Sprintf("Record", r.typeString(t.Elem(), true, false)) case *types.Interface: result = "unknown" case *types.Struct: @@ -509,13 +517,13 @@ func (r *typeRenderer) namedType(named *types.Named) string { if named.TypeArgs().Len() != 2 { return "Record" } - return fmt.Sprintf("Record", r.typeString(named.TypeArgs().At(1), false)) + return fmt.Sprintf("Record", r.typeString(named.TypeArgs().At(1), false, false)) } if _, ok := named.Underlying().(*types.Struct); !ok { if literals := r.stringLiterals(named); len(literals) > 0 { return strings.Join(literals, " | ") } - return r.typeString(named.Underlying(), false) + return r.typeString(named.Underlying(), false, false) } tsName := exportedName(obj.Name()) if previous := r.names[tsName]; previous != nil && previous != obj { @@ -560,7 +568,7 @@ func (r *typeRenderer) inlineStruct(structType *types.Struct) string { if !include || deprecated || internal { continue } - fieldType := r.typeString(structType.Field(i).Type(), !optional && !nonnil) + fieldType := r.typeString(structType.Field(i).Type(), !optional && !nonnil, false) doc := r.docs[structType.Field(i)] multiline = multiline || doc != "" fields = append(fields, fmt.Sprintf("%s%s%s: %s", inlineDoc(doc), propertyName(field), optionalMarker(optional), fieldType)) @@ -600,7 +608,7 @@ func (r *typeRenderer) declarations() (string, error) { if !include || deprecated || internal { continue } - fieldType := r.typeString(structType.Field(i).Type(), !optional && !nonnil) + fieldType := r.typeString(structType.Field(i).Type(), !optional && !nonnil, false) if isParams && isArrayType(structType.Field(i).Type()) { fieldType = "readonly " + fieldType } diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 45793d96a61be..acf1c83b5616b 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" @@ -163,10 +164,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" @@ -428,6 +433,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], @@ -500,10 +506,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], @@ -1361,6 +1371,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 d5c668385b849..a892e11de7ce7 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): @@ -801,14 +803,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): @@ -1642,6 +1652,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) { @@ -3274,6 +3304,27 @@ 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. +// @gen-proto-nullable-element +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) { @@ -3319,6 +3370,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-element +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 @@ -3351,6 +3430,38 @@ 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) + 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) { @@ -3376,6 +3487,34 @@ func (s *Session) handleGetMemberInModuleExports(ctx context.Context, params *Ge return setup.newSymbolResponse(member), nil } +// handleGetMembersInModuleExports returns exports by name from module symbols. +// @gen-proto-nullable-element +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) {