diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 09f38832fe3b3..ddde8cdcfc478 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -68,6 +68,7 @@ import type { SymbolsPropertyMethod, TextEdit, TypeAcquisition, + TypeArraysPropertyMethod, TypePropertyMethod, TypeResponse, TypesPropertyMethod, @@ -565,6 +566,51 @@ class SnapshotObjectRegistry { if (symbolData == null) return []; else return symbolData.map(data => this.getOrCreateSymbol(data)); } + + async fetchSymbolsForTypes( + types: readonly Type[], + method: SymbolsPropertyMethod, + handles: readonly (number | undefined)[], + projectId: Path, + ): Promise<(Symbol | undefined)[]> { + const result: (Symbol | undefined | null)[] = new Array(types.length); + const pendingTypeIds: number[] = []; + for (let i = 0; i < types.length; i++) { + const handle = handles[i]; + if (!handle) { + result[i] = undefined; // no symbol/aliasSymbol for this type + continue; + } + const cached = this.getSymbol(handle); + if (cached) { + result[i] = cached; + } + else { + result[i] = null; // pending: resolved from the batched response below + pendingTypeIds.push(types[i].id); + } + } + if (pendingTypeIds.length > 0) { + // The response array can contain nulls at indices with no result (e.g. no aliasSymbol + // for that type); the generated SymbolResponse[] result type doesn't express this. + const data = await this.client.apiRequest(method, { + snapshot: this.snapshotId, + project: projectId, + types: pendingTypeIds, + }) as (SymbolResponse | null)[]; + let j = 0; + for (let i = 0; i < result.length; i++) { + if (result[i] === null) { + const d = data[j++]; + result[i] = d ? this.getOrCreateSymbol(d) : undefined; + } + } + if (j !== data.length) { + throw new Error(`${method} response was not fully consumed: used ${j} of ${data.length} results`); + } + } + return result as (Symbol | undefined)[]; + } } class ProjectObjectRegistry { @@ -689,10 +735,61 @@ class ProjectObjectRegistry { else return typesData.map(data => this.getOrCreateType(data)); } + async fetchTypeArraysForTypes( + types: readonly Type[], + method: TypeArraysPropertyMethod, + handles: readonly (readonly number[])[], + ): Promise { + const result: (Type[] | null)[] = new Array(types.length); + const pendingTypeIds: number[] = []; + for (let i = 0; i < types.length; i++) { + const typeHandles = handles[i]; + const cached = new Array(typeHandles.length); + let allCached = true; + for (let j = 0; j < typeHandles.length; j++) { + const c = this.getType(typeHandles[j]); + if (!c) { + allCached = false; + break; + } + cached[j] = c; + } + if (allCached) { + result[i] = cached; + } + else { + result[i] = null; // pending: resolved from the batched response below + pendingTypeIds.push(types[i].id); + } + } + if (pendingTypeIds.length > 0) { + const data = await this.client.apiRequest(method, { + snapshot: this.snapshotId, + project: this.project.id, + types: pendingTypeIds, + }); + let j = 0; + for (let i = 0; i < result.length; i++) { + if (result[i] === null) { + const d = data[j++]; + result[i] = d ? d.map(x => this.getOrCreateType(x)) : []; + } + } + if (j !== data.length) { + throw new Error(`${method} response was not fully consumed: used ${j} of ${data.length} results`); + } + } + return result as Type[][]; + } + async fetchSymbols(source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles?: readonly number[]): Promise { return this.snapshotRegistry.fetchSymbols(source, method, handles, this.project.id); } + async fetchSymbolsForTypes(types: readonly Type[], method: SymbolsPropertyMethod, handles: readonly (number | undefined)[]): Promise<(Symbol | undefined)[]> { + return this.snapshotRegistry.fetchSymbolsForTypes(types, method, handles, this.project.id); + } + // getBaseTypes is a checker-level endpoint keyed by `type` (not `objectId`), // so it cannot go through fetchTypes. This helper reuses that server method. async fetchBaseTypes(source: Type): Promise { @@ -1945,6 +2042,36 @@ export class Checker { return signature.id === (await this.getWellKnownSignatures()).unknown; } + getSymbolOfType(type: Type): Promise; + getSymbolOfType(types: readonly Type[]): Promise<(Symbol | undefined)[]>; + async getSymbolOfType(typeOrTypes: Type | readonly Type[]): Promise { + if (Array.isArray(typeOrTypes)) { + const types = typeOrTypes as readonly TypeObject[]; + return this.objectRegistry.fetchSymbolsForTypes(types, "getSymbolsOfTypes", types.map(t => t.symbol)); + } + return (typeOrTypes as Type).getSymbol(); + } + + getAliasSymbolOfType(type: Type): Promise; + getAliasSymbolOfType(types: readonly Type[]): Promise<(Symbol | undefined)[]>; + async getAliasSymbolOfType(typeOrTypes: Type | readonly Type[]): Promise { + if (Array.isArray(typeOrTypes)) { + const types = typeOrTypes as readonly TypeObject[]; + return this.objectRegistry.fetchSymbolsForTypes(types, "getAliasSymbolsOfTypes", types.map(t => t.aliasSymbol)); + } + return (typeOrTypes as Type).getAliasSymbol(); + } + + getAliasTypeArgumentsOfType(type: Type): Promise; + getAliasTypeArgumentsOfType(types: readonly Type[]): Promise; + async getAliasTypeArgumentsOfType(typeOrTypes: Type | readonly Type[]): Promise { + if (Array.isArray(typeOrTypes)) { + const types = typeOrTypes as readonly TypeObject[]; + return this.objectRegistry.fetchTypeArraysForTypes(types, "getAliasTypeArgumentsOfTypes", types.map(t => t.aliasTypeArguments)); + } + return (typeOrTypes as Type).getAliasTypeArguments(); + } + async getExportsOfModule(symbol: Symbol): Promise { const data = await this.client.apiRequest("getExportsOfModule", { snapshot: this.snapshotId, diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 5e30d12bae247..17a489bfdb23f 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -50,6 +50,7 @@ export interface APIMethodInfo { getExportsOfSymbol: APIMethod; getExportSymbolOfSymbol: APIMethod; getSymbolOfType: APIMethod; + getSymbolsOfTypes: APIMethod; getTargetOfType: APIMethod; getFreshTypeOfType: APIMethod; getRegularTypeOfType: APIMethod; @@ -58,7 +59,9 @@ export interface APIMethodInfo { getOuterTypeParametersOfType: APIMethod; getLocalTypeParametersOfType: APIMethod; getAliasTypeArgumentsOfType: APIMethod; + getAliasTypeArgumentsOfTypes: APIMethod; getAliasSymbolOfType: APIMethod; + getAliasSymbolsOfTypes: APIMethod; getObjectTypeOfType: APIMethod; getIndexTypeOfType: APIMethod; getCheckTypeOfType: APIMethod; @@ -528,6 +531,16 @@ export interface GetTypePropertyParams { objectId: number; } +/** + * GetSymbolsOfTypesParams is used for batched endpoints that take a list of types and return one + * result per type (getSymbolsOfTypes, getAliasSymbolsOfTypes, getAliasTypeArgumentsOfTypes). + */ +export interface GetSymbolsOfTypesParams { + snapshot: number; + project: string; + types: readonly number[] | null; +} + /** GetSignaturePropertyParams is used for all signature sub-property endpoints. */ export interface GetSignaturePropertyParams { snapshot: number; diff --git a/packages/typescript/src/api/proto.ts b/packages/typescript/src/api/proto.ts index 4f9b56b55b6ee..49ae7c40eeb9e 100644 --- a/packages/typescript/src/api/proto.ts +++ b/packages/typescript/src/api/proto.ts @@ -23,6 +23,7 @@ export type SymbolsPropertyMethod = APIMethodsReturning; export type SignaturePropertyMethod = APIMethodsReturning; export type TypePropertyMethod = Exclude, IntrinsicTypeMethod>; export type TypesPropertyMethod = APIMethodsReturning; +export type TypeArraysPropertyMethod = APIMethodsReturning; export type IntrinsicTypeMethod = "getAnyType" | "getBigIntType" | "getBooleanType" | "getESSymbolType" | "getNeverType" | "getNonPrimitiveType" | "getNullType" | "getNumberType" | "getStringType" | "getUndefinedType" | "getUnknownType" | "getVoidType"; /** diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 582c0b6c54359..e0460e79bde25 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -76,6 +76,7 @@ import type { SymbolsPropertyMethod, TextEdit, TypeAcquisition, + TypeArraysPropertyMethod, TypePropertyMethod, TypeResponse, TypesPropertyMethod, @@ -573,6 +574,51 @@ class SnapshotObjectRegistry { if (symbolData == null) return []; else return symbolData.map(data => this.getOrCreateSymbol(data)); } + + fetchSymbolsForTypes( + types: readonly Type[], + method: SymbolsPropertyMethod, + handles: readonly (number | undefined)[], + projectId: Path, + ): (Symbol | undefined)[] { + const result: (Symbol | undefined | null)[] = new Array(types.length); + const pendingTypeIds: number[] = []; + for (let i = 0; i < types.length; i++) { + const handle = handles[i]; + if (!handle) { + result[i] = undefined; // no symbol/aliasSymbol for this type + continue; + } + const cached = this.getSymbol(handle); + if (cached) { + result[i] = cached; + } + else { + result[i] = null; // pending: resolved from the batched response below + pendingTypeIds.push(types[i].id); + } + } + if (pendingTypeIds.length > 0) { + // The response array can contain nulls at indices with no result (e.g. no aliasSymbol + // for that type); the generated SymbolResponse[] result type doesn't express this. + const data = this.client.apiRequest(method, { + snapshot: this.snapshotId, + project: projectId, + types: pendingTypeIds, + }) as (SymbolResponse | null)[]; + let j = 0; + for (let i = 0; i < result.length; i++) { + if (result[i] === null) { + const d = data[j++]; + result[i] = d ? this.getOrCreateSymbol(d) : undefined; + } + } + if (j !== data.length) { + throw new Error(`${method} response was not fully consumed: used ${j} of ${data.length} results`); + } + } + return result as (Symbol | undefined)[]; + } } class ProjectObjectRegistry { @@ -697,10 +743,61 @@ class ProjectObjectRegistry { else return typesData.map(data => this.getOrCreateType(data)); } + fetchTypeArraysForTypes( + types: readonly Type[], + method: TypeArraysPropertyMethod, + handles: readonly (readonly number[])[], + ): readonly Type[][] { + const result: (Type[] | null)[] = new Array(types.length); + const pendingTypeIds: number[] = []; + for (let i = 0; i < types.length; i++) { + const typeHandles = handles[i]; + const cached = new Array(typeHandles.length); + let allCached = true; + for (let j = 0; j < typeHandles.length; j++) { + const c = this.getType(typeHandles[j]); + if (!c) { + allCached = false; + break; + } + cached[j] = c; + } + if (allCached) { + result[i] = cached; + } + else { + result[i] = null; // pending: resolved from the batched response below + pendingTypeIds.push(types[i].id); + } + } + if (pendingTypeIds.length > 0) { + const data = this.client.apiRequest(method, { + snapshot: this.snapshotId, + project: this.project.id, + types: pendingTypeIds, + }); + let j = 0; + for (let i = 0; i < result.length; i++) { + if (result[i] === null) { + const d = data[j++]; + result[i] = d ? d.map(x => this.getOrCreateType(x)) : []; + } + } + if (j !== data.length) { + throw new Error(`${method} response was not fully consumed: used ${j} of ${data.length} results`); + } + } + return result as Type[][]; + } + fetchSymbols(source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles?: readonly number[]): readonly Symbol[] { return this.snapshotRegistry.fetchSymbols(source, method, handles, this.project.id); } + fetchSymbolsForTypes(types: readonly Type[], method: SymbolsPropertyMethod, handles: readonly (number | undefined)[]): (Symbol | undefined)[] { + return this.snapshotRegistry.fetchSymbolsForTypes(types, method, handles, this.project.id); + } + // getBaseTypes is a checker-level endpoint keyed by `type` (not `objectId`), // so it cannot go through fetchTypes. This helper reuses that server method. fetchBaseTypes(source: Type): readonly Type[] { @@ -1953,6 +2050,36 @@ export class Checker { return signature.id === (this.getWellKnownSignatures()).unknown; } + getSymbolOfType(type: Type): Symbol | undefined; + getSymbolOfType(types: readonly Type[]): (Symbol | undefined)[]; + getSymbolOfType(typeOrTypes: Type | readonly Type[]): Symbol | (Symbol | undefined)[] | undefined { + if (Array.isArray(typeOrTypes)) { + const types = typeOrTypes as readonly TypeObject[]; + return this.objectRegistry.fetchSymbolsForTypes(types, "getSymbolsOfTypes", types.map(t => t.symbol)); + } + return (typeOrTypes as Type).getSymbol(); + } + + getAliasSymbolOfType(type: Type): Symbol | undefined; + getAliasSymbolOfType(types: readonly Type[]): (Symbol | undefined)[]; + getAliasSymbolOfType(typeOrTypes: Type | readonly Type[]): Symbol | (Symbol | undefined)[] | undefined { + if (Array.isArray(typeOrTypes)) { + const types = typeOrTypes as readonly TypeObject[]; + return this.objectRegistry.fetchSymbolsForTypes(types, "getAliasSymbolsOfTypes", types.map(t => t.aliasSymbol)); + } + return (typeOrTypes as Type).getAliasSymbol(); + } + + getAliasTypeArgumentsOfType(type: Type): readonly Type[]; + getAliasTypeArgumentsOfType(types: readonly Type[]): readonly Type[][]; + getAliasTypeArgumentsOfType(typeOrTypes: Type | readonly Type[]): readonly Type[] | readonly Type[][] { + if (Array.isArray(typeOrTypes)) { + const types = typeOrTypes as readonly TypeObject[]; + return this.objectRegistry.fetchTypeArraysForTypes(types, "getAliasTypeArgumentsOfTypes", types.map(t => t.aliasTypeArguments)); + } + return (typeOrTypes as Type).getAliasTypeArguments(); + } + getExportsOfModule(symbol: Symbol): readonly Symbol[] { const data = this.client.apiRequest("getExportsOfModule", { snapshot: this.snapshotId, diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 24ae9c84f1eb8..68a8cf3f09882 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -4353,6 +4353,152 @@ function f() { await api.close(); } }); + + test("getSymbolOfType resolves symbols for multiple types", async () => { + const src = `export class Foo { x: number = 0; }\nexport class Bar { y: string = ""; }\nexport const foo: Foo = new Foo();\nexport const bar: Bar = new Bar();`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + 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 typeFoo = await project.checker.getTypeOfSymbol(symFoo); + const typeBar = await project.checker.getTypeOfSymbol(symBar); + const results = await project.checker.getSymbolOfType([typeFoo, typeBar]); + assert.equal(results.length, 2); + assert.equal(results[0]?.name, "Foo"); + assert.equal(results[1]?.name, "Bar"); + } + finally { + await api.close(); + } + }); + + test("getAliasSymbolOfType resolves alias symbols for multiple types", async () => { + const src = `type Point = { x: number; y: number };\ntype Size = { w: number; h: number };\nexport const p: Point = { x: 1, y: 2 };\nexport const s: Size = { w: 1, h: 2 };`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const posP = src.indexOf("p:"); + const posS = src.indexOf("s:"); + const symP = await project.checker.getSymbolAtPosition("/src/main.ts", posP); + const symS = await project.checker.getSymbolAtPosition("/src/main.ts", posS); + assert.ok(symP); + assert.ok(symS); + const typeP = await project.checker.getTypeOfSymbol(symP); + const typeS = await project.checker.getTypeOfSymbol(symS); + const results = await project.checker.getAliasSymbolOfType([typeP, typeS]); + assert.equal(results.length, 2); + assert.equal(results[0]?.name, "Point"); + assert.equal(results[1]?.name, "Size"); + } + finally { + await api.close(); + } + }); + + test("getSymbolOfType reuses already-cached symbols instead of re-fetching them", async () => { + const src = `export class Foo { x: number = 0; }\nexport class Bar { y: string = ""; }\nexport const foo: Foo = new Foo();\nexport const bar: Bar = new Bar();`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + 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 typeFoo = await project.checker.getTypeOfSymbol(symFoo); + const typeBar = await project.checker.getTypeOfSymbol(symBar); + // Populate the object cache for typeFoo's symbol via the singular accessor first. + const cachedFooSymbol = await typeFoo.getSymbol(); + assert.ok(cachedFooSymbol); + const results = await project.checker.getSymbolOfType([typeFoo, typeBar]); + assert.equal(results.length, 2); + // The already-cached symbol should be reused as-is (same object identity), not re-fetched. + assert.strictEqual(results[0], cachedFooSymbol); + assert.equal(results[1]?.name, "Bar"); + } + finally { + await api.close(); + } + }); + + test("getAliasTypeArgumentsOfType resolves alias type arguments for multiple types", async () => { + const src = `type Box = { value: T };\ntype Pair = [A, B];\nexport const x: Box = { value: "hi" };\nexport const p: Pair = ["hello", 42];`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const posX = src.indexOf("x:"); + const posP = src.indexOf("p:"); + const symX = await project.checker.getSymbolAtPosition("/src/main.ts", posX); + const symP = await project.checker.getSymbolAtPosition("/src/main.ts", posP); + assert.ok(symX); + assert.ok(symP); + const typeX = await project.checker.getTypeOfSymbol(symX); + const typeP = await project.checker.getTypeOfSymbol(symP); + const results = await project.checker.getAliasTypeArgumentsOfType([typeX, typeP]); + assert.equal(results.length, 2); + assert.equal(results[0].length, 1); + assert.ok(results[0][0].flags & TypeFlags.String); + assert.equal(results[1].length, 2); + assert.ok(results[1][0].flags & TypeFlags.String); + assert.ok(results[1][1].flags & TypeFlags.Number); + } + finally { + await api.close(); + } + }); + + test("getAliasTypeArgumentsOfType reuses already-cached type arguments instead of re-fetching them", async () => { + const src = `type Box = { value: T };\ntype Pair = [A, B];\nexport const x: Box = { value: "hi" };\nexport const p: Pair = ["hello", 42];`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const posX = src.indexOf("x:"); + const posP = src.indexOf("p:"); + const symX = await project.checker.getSymbolAtPosition("/src/main.ts", posX); + const symP = await project.checker.getSymbolAtPosition("/src/main.ts", posP); + assert.ok(symX); + assert.ok(symP); + const typeX = await project.checker.getTypeOfSymbol(symX); + const typeP = await project.checker.getTypeOfSymbol(symP); + // Populate the object cache for typeX's alias type arguments via the singular accessor first. + const cachedXArgs = await typeX.getAliasTypeArguments(); + assert.equal(cachedXArgs.length, 1); + const results = await project.checker.getAliasTypeArgumentsOfType([typeX, typeP]); + assert.equal(results.length, 2); + // The already-cached array should be reused as-is (same object identity), not re-fetched. + assert.strictEqual(results[0][0], cachedXArgs[0]); + assert.equal(results[1].length, 2); + } + finally { + await api.close(); + } + }); }); describe("Symbol - getDocumentationComment and getJsDocTags", () => { diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 93fe5eae5255f..019d748e28509 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -4361,6 +4361,152 @@ function f() { api.close(); } }); + + test("getSymbolOfType resolves symbols for multiple types", () => { + const src = `export class Foo { x: number = 0; }\nexport class Bar { y: string = ""; }\nexport const foo: Foo = new Foo();\nexport const bar: Bar = new Bar();`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + 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 typeFoo = project.checker.getTypeOfSymbol(symFoo); + const typeBar = project.checker.getTypeOfSymbol(symBar); + const results = project.checker.getSymbolOfType([typeFoo, typeBar]); + assert.equal(results.length, 2); + assert.equal(results[0]?.name, "Foo"); + assert.equal(results[1]?.name, "Bar"); + } + finally { + api.close(); + } + }); + + test("getAliasSymbolOfType resolves alias symbols for multiple types", () => { + const src = `type Point = { x: number; y: number };\ntype Size = { w: number; h: number };\nexport const p: Point = { x: 1, y: 2 };\nexport const s: Size = { w: 1, h: 2 };`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const posP = src.indexOf("p:"); + const posS = src.indexOf("s:"); + const symP = project.checker.getSymbolAtPosition("/src/main.ts", posP); + const symS = project.checker.getSymbolAtPosition("/src/main.ts", posS); + assert.ok(symP); + assert.ok(symS); + const typeP = project.checker.getTypeOfSymbol(symP); + const typeS = project.checker.getTypeOfSymbol(symS); + const results = project.checker.getAliasSymbolOfType([typeP, typeS]); + assert.equal(results.length, 2); + assert.equal(results[0]?.name, "Point"); + assert.equal(results[1]?.name, "Size"); + } + finally { + api.close(); + } + }); + + test("getSymbolOfType reuses already-cached symbols instead of re-fetching them", () => { + const src = `export class Foo { x: number = 0; }\nexport class Bar { y: string = ""; }\nexport const foo: Foo = new Foo();\nexport const bar: Bar = new Bar();`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + 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 typeFoo = project.checker.getTypeOfSymbol(symFoo); + const typeBar = project.checker.getTypeOfSymbol(symBar); + // Populate the object cache for typeFoo's symbol via the singular accessor first. + const cachedFooSymbol = typeFoo.getSymbol(); + assert.ok(cachedFooSymbol); + const results = project.checker.getSymbolOfType([typeFoo, typeBar]); + assert.equal(results.length, 2); + // The already-cached symbol should be reused as-is (same object identity), not re-fetched. + assert.strictEqual(results[0], cachedFooSymbol); + assert.equal(results[1]?.name, "Bar"); + } + finally { + api.close(); + } + }); + + test("getAliasTypeArgumentsOfType resolves alias type arguments for multiple types", () => { + const src = `type Box = { value: T };\ntype Pair = [A, B];\nexport const x: Box = { value: "hi" };\nexport const p: Pair = ["hello", 42];`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const posX = src.indexOf("x:"); + const posP = src.indexOf("p:"); + const symX = project.checker.getSymbolAtPosition("/src/main.ts", posX); + const symP = project.checker.getSymbolAtPosition("/src/main.ts", posP); + assert.ok(symX); + assert.ok(symP); + const typeX = project.checker.getTypeOfSymbol(symX); + const typeP = project.checker.getTypeOfSymbol(symP); + const results = project.checker.getAliasTypeArgumentsOfType([typeX, typeP]); + assert.equal(results.length, 2); + assert.equal(results[0].length, 1); + assert.ok(results[0][0].flags & TypeFlags.String); + assert.equal(results[1].length, 2); + assert.ok(results[1][0].flags & TypeFlags.String); + assert.ok(results[1][1].flags & TypeFlags.Number); + } + finally { + api.close(); + } + }); + + test("getAliasTypeArgumentsOfType reuses already-cached type arguments instead of re-fetching them", () => { + const src = `type Box = { value: T };\ntype Pair = [A, B];\nexport const x: Box = { value: "hi" };\nexport const p: Pair = ["hello", 42];`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const posX = src.indexOf("x:"); + const posP = src.indexOf("p:"); + const symX = project.checker.getSymbolAtPosition("/src/main.ts", posX); + const symP = project.checker.getSymbolAtPosition("/src/main.ts", posP); + assert.ok(symX); + assert.ok(symP); + const typeX = project.checker.getTypeOfSymbol(symX); + const typeP = project.checker.getTypeOfSymbol(symP); + // Populate the object cache for typeX's alias type arguments via the singular accessor first. + const cachedXArgs = typeX.getAliasTypeArguments(); + assert.equal(cachedXArgs.length, 1); + const results = project.checker.getAliasTypeArgumentsOfType([typeX, typeP]); + assert.equal(results.length, 2); + // The already-cached array should be reused as-is (same object identity), not re-fetched. + assert.strictEqual(results[0][0], cachedXArgs[0]); + assert.equal(results[1].length, 2); + } + finally { + api.close(); + } + }); }); describe("Symbol - getDocumentationComment and getJsDocTags", () => { diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 8178bf0cbef04..304e6eb958936 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -104,6 +104,7 @@ const ( // Type sub-property methods MethodGetSymbolOfType Method = "getSymbolOfType" + MethodGetSymbolsOfTypes Method = "getSymbolsOfTypes" MethodGetTargetOfType Method = "getTargetOfType" MethodGetFreshTypeOfType Method = "getFreshTypeOfType" MethodGetRegularTypeOfType Method = "getRegularTypeOfType" @@ -112,7 +113,9 @@ const ( MethodGetOuterTypeParametersOfType Method = "getOuterTypeParametersOfType" MethodGetLocalTypeParametersOfType Method = "getLocalTypeParametersOfType" MethodGetAliasTypeArgumentsOfType Method = "getAliasTypeArgumentsOfType" + MethodGetAliasTypeArgumentsOfTypes Method = "getAliasTypeArgumentsOfTypes" MethodGetAliasSymbolOfType Method = "getAliasSymbolOfType" + MethodGetAliasSymbolsOfTypes Method = "getAliasSymbolsOfTypes" MethodGetObjectTypeOfType Method = "getObjectTypeOfType" MethodGetIndexTypeOfType Method = "getIndexTypeOfType" MethodGetCheckTypeOfType Method = "getCheckTypeOfType" @@ -442,6 +445,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetExportSymbolOfSymbol: unmarshallerFor[GetSymbolPropertyParams], MethodGetSymbolOfType: unmarshallerFor[GetTypePropertyParams], + MethodGetSymbolsOfTypes: unmarshallerFor[GetSymbolsOfTypesParams], MethodGetTargetOfType: unmarshallerFor[GetTypePropertyParams], MethodGetFreshTypeOfType: unmarshallerFor[GetTypePropertyParams], MethodGetRegularTypeOfType: unmarshallerFor[GetTypePropertyParams], @@ -450,7 +454,9 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetOuterTypeParametersOfType: unmarshallerFor[GetTypePropertyParams], MethodGetLocalTypeParametersOfType: unmarshallerFor[GetTypePropertyParams], MethodGetAliasTypeArgumentsOfType: unmarshallerFor[GetTypePropertyParams], + MethodGetAliasTypeArgumentsOfTypes: unmarshallerFor[GetSymbolsOfTypesParams], MethodGetAliasSymbolOfType: unmarshallerFor[GetTypePropertyParams], + MethodGetAliasSymbolsOfTypes: unmarshallerFor[GetSymbolsOfTypesParams], MethodGetObjectTypeOfType: unmarshallerFor[GetTypePropertyParams], MethodGetIndexTypeOfType: unmarshallerFor[GetTypePropertyParams], MethodGetCheckTypeOfType: unmarshallerFor[GetTypePropertyParams], @@ -1020,6 +1026,14 @@ type GetTypePropertyParams struct { Type TypeID `json:"objectId"` } +// GetSymbolsOfTypesParams is used for batched endpoints that take a list of types and return one +// result per type (getSymbolsOfTypes, getAliasSymbolsOfTypes, getAliasTypeArgumentsOfTypes). +type GetSymbolsOfTypesParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + Types []TypeID `json:"types"` +} + // GetSymbolPropertyParams is used for all symbol sub-property endpoints. type GetSymbolPropertyParams struct { Snapshot SnapshotID `json:"snapshot"` diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index e87e02c9b7ca3..0d162d6a57157 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -679,6 +679,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetExportSymbolOfSymbol(ctx, parsed.(*GetSymbolPropertyParams)) case string(MethodGetSymbolOfType): return s.handleGetSymbolOfType(ctx, parsed.(*GetTypePropertyParams)) + case string(MethodGetSymbolsOfTypes): + return s.handleGetSymbolsOfTypes(ctx, parsed.(*GetSymbolsOfTypesParams)) case string(MethodGetTargetOfType): return s.handleGetTargetOfType(ctx, parsed.(*GetTypePropertyParams)) case string(MethodGetFreshTypeOfType): @@ -695,8 +697,12 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetLocalTypeParametersOfType(ctx, parsed.(*GetTypePropertyParams)) case string(MethodGetAliasTypeArgumentsOfType): return s.handleGetAliasTypeArgumentsOfType(ctx, parsed.(*GetTypePropertyParams)) + case string(MethodGetAliasTypeArgumentsOfTypes): + return s.handleGetAliasTypeArgumentsOfTypes(ctx, parsed.(*GetSymbolsOfTypesParams)) case string(MethodGetAliasSymbolOfType): return s.handleGetAliasSymbolOfType(ctx, parsed.(*GetTypePropertyParams)) + case string(MethodGetAliasSymbolsOfTypes): + return s.handleGetAliasSymbolsOfTypes(ctx, parsed.(*GetSymbolsOfTypesParams)) case string(MethodGetObjectTypeOfType): return s.handleGetObjectTypeOfType(ctx, parsed.(*GetTypePropertyParams)) case string(MethodGetIndexTypeOfType): @@ -1850,6 +1856,10 @@ func (s *Session) handleGetSymbolOfType(_ context.Context, params *GetTypeProper return s.resolveSymbolPropertyOfType(params, (*checker.Type).Symbol) } +func (s *Session) handleGetSymbolsOfTypes(_ context.Context, params *GetSymbolsOfTypesParams) ([]*SymbolResponse, error) { + return s.resolveSymbolPropertyOfTypes(params, (*checker.Type).Symbol) +} + func (s *Session) handleGetTargetOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) { return s.resolveTypePropertyOfType(params, (*checker.Type).Target) } @@ -1894,6 +1904,15 @@ func (s *Session) handleGetAliasTypeArgumentsOfType(_ context.Context, params *G }) } +func (s *Session) handleGetAliasTypeArgumentsOfTypes(_ context.Context, params *GetSymbolsOfTypesParams) ([][]*TypeResponse, error) { + return s.resolveTypeArrayPropertyOfTypes(params, func(t *checker.Type) []*checker.Type { + if t.Alias() == nil { + return nil + } + return t.Alias().TypeArguments() + }) +} + // @gen-proto-nullable func (s *Session) handleGetAliasSymbolOfType(_ context.Context, params *GetTypePropertyParams) (*SymbolResponse, error) { return s.resolveSymbolPropertyOfType(params, func(t *checker.Type) *ast.Symbol { @@ -1904,6 +1923,15 @@ func (s *Session) handleGetAliasSymbolOfType(_ context.Context, params *GetTypeP }) } +func (s *Session) handleGetAliasSymbolsOfTypes(_ context.Context, params *GetSymbolsOfTypesParams) ([]*SymbolResponse, error) { + return s.resolveSymbolPropertyOfTypes(params, func(t *checker.Type) *ast.Symbol { + if t.Alias() == nil { + return nil + } + return t.Alias().Symbol() + }) +} + func (s *Session) handleGetObjectTypeOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) { return s.resolveTypePropertyOfType(params, func(t *checker.Type) *checker.Type { return t.AsIndexedAccessType().ObjectType() }) } @@ -2120,6 +2148,33 @@ func (s *Session) resolveTypeArrayPropertyOfType(params *GetTypePropertyParams, return results, nil } +// resolveTypeArrayPropertyOfTypes resolves a type property of an array of types for multiple types +// and returns arrays of type responses in the same order as the input handles. +func (s *Session) resolveTypeArrayPropertyOfTypes(params *GetSymbolsOfTypesParams, getter func(*checker.Type) []*checker.Type) ([][]*TypeResponse, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + + results := make([][]*TypeResponse, len(params.Types)) + for i, typeHandle := range params.Types { + t, err := sd.resolveTypeHandle(params.Project, typeHandle) + if err != nil { + return nil, err + } + types := getter(t) + if len(types) == 0 { + continue + } + sub := make([]*TypeResponse, len(types)) + for j, subType := range types { + sub[j] = sd.newTypeResponse(params.Project, subType) + } + results[i] = sub + } + return results, nil +} + // resolveSymbolPropertyOfType resolves a type property of type `Symbol` and returns a symbol response. func (s *Session) resolveSymbolPropertyOfType(params *GetTypePropertyParams, getter func(*checker.Type) *ast.Symbol) (*SymbolResponse, error) { sd, err := s.getSnapshotData(params.Snapshot) @@ -2139,6 +2194,29 @@ func (s *Session) resolveSymbolPropertyOfType(params *GetTypePropertyParams, get return sd.newSymbolResponse(result, params.Project), nil } +// resolveSymbolPropertyOfTypes resolves a type property of type `Symbol` for multiple types +// and returns symbol responses in the same order as the input handles. +func (s *Session) resolveSymbolPropertyOfTypes(params *GetSymbolsOfTypesParams, getter func(*checker.Type) *ast.Symbol) ([]*SymbolResponse, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + + results := make([]*SymbolResponse, len(params.Types)) + for i, typeHandle := range params.Types { + t, err := sd.resolveTypeHandle(params.Project, typeHandle) + if err != nil { + return nil, err + } + result := getter(t) + if result == nil { + continue + } + results[i] = sd.newSymbolResponse(result, params.Project) + } + return results, nil +} + // resolveSymbolTablePropertyOfSymbol resolves a symbol property of type `Symbol` and returns a symbol response. func (s *Session) resolveSymbolPropertyOfSymbol(params *GetSymbolPropertyParams, getter func(*ast.Symbol) *ast.Symbol) (*SymbolResponse, error) { sd, err := s.getSnapshotData(params.Snapshot)