Skip to content

Commit d2f9dd6

Browse files
authored
[api] Add various merged symbol checker methods (#64598)
1 parent 0ad3777 commit d2f9dd6

10 files changed

Lines changed: 400 additions & 35 deletions

File tree

‎packages/typescript/src/api/async/api.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,50 @@ export class Checker {
22312231
this.objectRegistry.clear();
22322232
}
22332233

2234+
/** Get the merged symbol, or the original symbol if it has not been merged. */
2235+
async getMergedSymbol(symbol: Symbol): Promise<Symbol> {
2236+
const data = await this.client.apiRequest("getMergedSymbol", {
2237+
snapshot: this.snapshotId,
2238+
project: this.project.id,
2239+
symbol: symbol.reference,
2240+
});
2241+
return this.objectRegistry.getOrCreateSymbol(data);
2242+
}
2243+
2244+
/**
2245+
* Get a node's merged symbol. For a declaration, prefer
2246+
* {@link Checker.getSymbolOfDeclaration} for a non-nullable result.
2247+
*/
2248+
async getSymbolOfNode(node: Node): Promise<Symbol | undefined> {
2249+
const data = await this.client.apiRequest("getSymbolOfNode", {
2250+
snapshot: this.snapshotId,
2251+
project: this.project.id,
2252+
location: getNodeId(node),
2253+
});
2254+
return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined;
2255+
}
2256+
2257+
/** Get a declaration's merged symbol. */
2258+
async getSymbolOfDeclaration(node: Declaration): Promise<Symbol> {
2259+
const data = await this.client.apiRequest("getSymbolOfDeclarationForChecker", {
2260+
snapshot: this.snapshotId,
2261+
project: this.project.id,
2262+
location: getNodeId(node),
2263+
});
2264+
// Declarations normally have symbols, but preserve unexpected null results at runtime.
2265+
return (data && this.objectRegistry.getOrCreateSymbol(data))!;
2266+
}
2267+
2268+
/** Get a symbol's merged parent, if any. */
2269+
async getParentOfSymbol(symbol: Symbol): Promise<Symbol | undefined> {
2270+
const data = await this.client.apiRequest("getParentOfSymbolForChecker", {
2271+
snapshot: this.snapshotId,
2272+
project: this.project.id,
2273+
symbol: symbol.reference,
2274+
});
2275+
return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined;
2276+
}
2277+
22342278
getSymbolAtLocation(node: Node): Promise<Symbol | undefined>;
22352279
getSymbolAtLocation(nodes: readonly Node[]): Promise<(Symbol | undefined)[]>;
22362280
async getSymbolAtLocation(nodeOrNodes: Node | readonly Node[]): Promise<Symbol | (Symbol | undefined)[] | undefined> {

‎packages/typescript/src/api/proto.generated.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ export interface APIMethodInfo {
145145
getAliasedSymbol: APIMethod<CheckerSymbolParams, SymbolResponse>;
146146
getImmediateAliasedSymbol: APIMethod<CheckerSymbolParams, SymbolResponse | null>;
147147
getTargetSymbol: APIMethod<CheckerSymbolParams, SymbolResponse>;
148+
getMergedSymbol: APIMethod<CheckerSymbolParams, SymbolResponse>;
149+
getSymbolOfNode: APIMethod<CheckerNodeParams, SymbolResponse | null>;
150+
getSymbolOfDeclarationForChecker: APIMethod<CheckerNodeParams, SymbolResponse | null>;
151+
getParentOfSymbolForChecker: APIMethod<CheckerSymbolParams, SymbolResponse | null>;
148152
getExportSymbolOfSymbolForChecker: APIMethod<CheckerSymbolParams, SymbolResponse>;
149153
getFullyQualifiedName: APIMethod<CheckerSymbolParams, string>;
150154
getExportsOfModule: APIMethod<CheckerSymbolParams, SymbolResponse[] | null>;
@@ -1193,6 +1197,7 @@ export interface BatchRequest {
11931197
| "getLocalTypeParametersOfType"
11941198
| "getMemberInModuleExports"
11951199
| "getMembersOfSymbol"
1200+
| "getMergedSymbol"
11961201
| "getModeForResolutionAtIndex"
11971202
| "getModeForUsageLocation"
11981203
| "getNameTypeOfMappedType"
@@ -1207,6 +1212,7 @@ export interface BatchRequest {
12071212
| "getParameterType"
12081213
| "getParametersOfSignature"
12091214
| "getParentOfSymbol"
1215+
| "getParentOfSymbolForChecker"
12101216
| "getProgramDiagnostics"
12111217
| "getPropertiesOfType"
12121218
| "getPropertyOfType"
@@ -1234,6 +1240,8 @@ export interface BatchRequest {
12341240
| "getSymbolAtLocation"
12351241
| "getSymbolAtPosition"
12361242
| "getSymbolOfDeclaration"
1243+
| "getSymbolOfDeclarationForChecker"
1244+
| "getSymbolOfNode"
12371245
| "getSymbolOfSourceFile"
12381246
| "getSymbolOfType"
12391247
| "getSymbolsAtLocations"
@@ -1371,6 +1379,7 @@ export interface BatchResponse {
13711379
| "getLocalTypeParametersOfType"
13721380
| "getMemberInModuleExports"
13731381
| "getMembersOfSymbol"
1382+
| "getMergedSymbol"
13741383
| "getModeForResolutionAtIndex"
13751384
| "getModeForUsageLocation"
13761385
| "getNameTypeOfMappedType"
@@ -1385,6 +1394,7 @@ export interface BatchResponse {
13851394
| "getParameterType"
13861395
| "getParametersOfSignature"
13871396
| "getParentOfSymbol"
1397+
| "getParentOfSymbolForChecker"
13881398
| "getProgramDiagnostics"
13891399
| "getPropertiesOfType"
13901400
| "getPropertyOfType"
@@ -1412,6 +1422,8 @@ export interface BatchResponse {
14121422
| "getSymbolAtLocation"
14131423
| "getSymbolAtPosition"
14141424
| "getSymbolOfDeclaration"
1425+
| "getSymbolOfDeclarationForChecker"
1426+
| "getSymbolOfNode"
14151427
| "getSymbolOfSourceFile"
14161428
| "getSymbolOfType"
14171429
| "getSymbolsAtLocations"

‎packages/typescript/src/api/sync/api.ts‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4068,6 +4068,123 @@ export class Checker {
40684068
this.objectRegistry.clear();
40694069
}
40704070

4071+
/** Get the merged symbol, or the original symbol if it has not been merged. */
4072+
get getMergedSymbol(): {
4073+
(symbol: Symbol): Symbol;
4074+
gen(symbol: Symbol): Generator<ProtocolRequest, Symbol, ProtocolResponse["result"]>;
4075+
} {
4076+
const owner = this;
4077+
return cacheGeneratorMethod(
4078+
owner,
4079+
"getMergedSymbol",
4080+
function (symbol: Symbol): Symbol {
4081+
const data = owner.client.apiRequest("getMergedSymbol", {
4082+
snapshot: owner.snapshotId,
4083+
project: owner.project.id,
4084+
symbol: symbol.reference,
4085+
});
4086+
return owner.objectRegistry.getOrCreateSymbol(data);
4087+
},
4088+
function* (symbol: Symbol): Generator<ProtocolRequest, Symbol, ProtocolResponse["result"]> {
4089+
const data = yield* apiRequest("getMergedSymbol", {
4090+
snapshot: owner.snapshotId,
4091+
project: owner.project.id,
4092+
symbol: symbol.reference,
4093+
});
4094+
return owner.objectRegistry.getOrCreateSymbol(data);
4095+
},
4096+
);
4097+
}
4098+
4099+
/**
4100+
* Get a node's merged symbol. For a declaration, prefer
4101+
* {@link Checker.getSymbolOfDeclaration} for a non-nullable result.
4102+
*/
4103+
get getSymbolOfNode(): {
4104+
(node: Node): Symbol | undefined;
4105+
gen(node: Node): Generator<ProtocolRequest, Symbol | undefined, ProtocolResponse["result"]>;
4106+
} {
4107+
const owner = this;
4108+
return cacheGeneratorMethod(
4109+
owner,
4110+
"getSymbolOfNode",
4111+
function (node: Node): Symbol | undefined {
4112+
const data = owner.client.apiRequest("getSymbolOfNode", {
4113+
snapshot: owner.snapshotId,
4114+
project: owner.project.id,
4115+
location: getNodeId(node),
4116+
});
4117+
return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined;
4118+
},
4119+
function* (node: Node): Generator<ProtocolRequest, Symbol | undefined, ProtocolResponse["result"]> {
4120+
const data = yield* apiRequest("getSymbolOfNode", {
4121+
snapshot: owner.snapshotId,
4122+
project: owner.project.id,
4123+
location: getNodeId(node),
4124+
});
4125+
return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined;
4126+
},
4127+
);
4128+
}
4129+
4130+
/** Get a declaration's merged symbol. */
4131+
get getSymbolOfDeclaration(): {
4132+
(node: Declaration): Symbol;
4133+
gen(node: Declaration): Generator<ProtocolRequest, Symbol, ProtocolResponse["result"]>;
4134+
} {
4135+
const owner = this;
4136+
return cacheGeneratorMethod(
4137+
owner,
4138+
"getSymbolOfDeclaration",
4139+
function (node: Declaration): Symbol {
4140+
const data = owner.client.apiRequest("getSymbolOfDeclarationForChecker", {
4141+
snapshot: owner.snapshotId,
4142+
project: owner.project.id,
4143+
location: getNodeId(node),
4144+
});
4145+
// Declarations normally have symbols, but preserve unexpected null results at runtime.
4146+
return (data && owner.objectRegistry.getOrCreateSymbol(data))!;
4147+
},
4148+
function* (node: Declaration): Generator<ProtocolRequest, Symbol, ProtocolResponse["result"]> {
4149+
const data = yield* apiRequest("getSymbolOfDeclarationForChecker", {
4150+
snapshot: owner.snapshotId,
4151+
project: owner.project.id,
4152+
location: getNodeId(node),
4153+
});
4154+
// Declarations normally have symbols, but preserve unexpected null results at runtime.
4155+
return (data && owner.objectRegistry.getOrCreateSymbol(data))!;
4156+
},
4157+
);
4158+
}
4159+
4160+
/** Get a symbol's merged parent, if any. */
4161+
get getParentOfSymbol(): {
4162+
(symbol: Symbol): Symbol | undefined;
4163+
gen(symbol: Symbol): Generator<ProtocolRequest, Symbol | undefined, ProtocolResponse["result"]>;
4164+
} {
4165+
const owner = this;
4166+
return cacheGeneratorMethod(
4167+
owner,
4168+
"getParentOfSymbol",
4169+
function (symbol: Symbol): Symbol | undefined {
4170+
const data = owner.client.apiRequest("getParentOfSymbolForChecker", {
4171+
snapshot: owner.snapshotId,
4172+
project: owner.project.id,
4173+
symbol: symbol.reference,
4174+
});
4175+
return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined;
4176+
},
4177+
function* (symbol: Symbol): Generator<ProtocolRequest, Symbol | undefined, ProtocolResponse["result"]> {
4178+
const data = yield* apiRequest("getParentOfSymbolForChecker", {
4179+
snapshot: owner.snapshotId,
4180+
project: owner.project.id,
4181+
symbol: symbol.reference,
4182+
});
4183+
return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined;
4184+
},
4185+
);
4186+
}
4187+
40714188
get getSymbolAtLocation(): {
40724189
(node: Node): Symbol | undefined;
40734190
(nodes: readonly Node[]): (Symbol | undefined)[];

‎packages/typescript/test/async/api.test.ts‎

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
isNamedImports,
2222
isObjectLiteralExpression,
2323
isPropertyAssignment,
24+
isPropertyDeclaration,
2425
isReturnStatement,
2526
isShorthandPropertyAssignment,
2627
isStringLiteral,
@@ -505,7 +506,7 @@ describe("API", { concurrency }, () => {
505506
});
506507

507508
test("remote declarations lazily fetch and cache binder symbols", async () => {
508-
const sourceText = "function present() {}\nimport {} from './missing';";
509+
const sourceText = "function present() {}";
509510
await using api = spawnAPI({
510511
"/symbols.ts": sourceText,
511512
});
@@ -514,9 +515,7 @@ describe("API", { concurrency }, () => {
514515
const sourceFile = await project.program.getSourceFile("/symbols.ts");
515516
assert.ok(sourceFile);
516517
const declaration = sourceFile.statements[0];
517-
const withoutSymbol = sourceFile.statements[1];
518518
assert.ok(isFunctionDeclaration(declaration));
519-
assert.ok(isImportDeclaration(withoutSymbol));
520519
const checkerSymbol = await project.checker.getSymbolAtPosition("/symbols.ts", sourceText.indexOf("present")); // @sync: const checkerSymbol = project.checker.getSymbolAtPosition("/symbols.ts", sourceText.indexOf("present"));
521520
assert.ok(checkerSymbol);
522521

@@ -537,11 +536,6 @@ describe("API", { concurrency }, () => {
537536
assert.strictEqual(await getSymbol(declaration), first); // @sync: assert.strictEqual(getSymbol(declaration), first);
538537
assert.equal(symbolRequests, 1);
539538

540-
symbolRequests = 0;
541-
await assert.rejects(getSymbol(withoutSymbol), /has no binder symbol/); // @sync: assert.throws(() => getSymbol(withoutSymbol), /has no binder symbol/);
542-
await assert.rejects(api.getSymbol(withoutSymbol), /has no binder symbol/); // @sync: assert.throws(() => api.getSymbol(withoutSymbol), /has no binder symbol/);
543-
assert.equal(symbolRequests, 2);
544-
545539
await snapshot.dispose();
546540
});
547541

@@ -563,7 +557,7 @@ describe("API", { concurrency }, () => {
563557

564558
test("source files own separate declaration result and request caches", async () => {
565559
await using api = spawnAPI();
566-
await using lease = await api.createSourceFile("/symbols.ts", "function present() {}\nimport {} from './missing';");
560+
await using lease = await api.createSourceFile("/symbols.ts", "function present() {}");
567561
const file = lease.sourceFile;
568562
assert.ok(file instanceof RemoteSourceFile);
569563
const cache = file.symbolCache;
@@ -578,11 +572,6 @@ describe("API", { concurrency }, () => {
578572
assert.strictEqual(cache.symbolsByDeclarationNodeIndex.get(index), symbol);
579573
assert.strictEqual(cache.symbolsById.get(symbol.reference.id), symbol);
580574
assert.equal(cache.declarationSymbolRequests.has(index), false);
581-
const withoutSymbol = cast(file.statements[1], isImportDeclaration);
582-
const absentIndex = parseNodeHandle(getNodeId(withoutSymbol)).index;
583-
await assert.rejects(getSymbol(withoutSymbol), /has no binder symbol/); // @sync: assert.throws(() => getSymbol(withoutSymbol), /has no binder symbol/);
584-
assert.equal(cache.symbolsByDeclarationNodeIndex.has(absentIndex), false);
585-
assert.equal(cache.declarationSymbolRequests.has(absentIndex), false);
586575
api.clearSourceFileCache();
587576
assert.strictEqual(file.symbolCache, cache);
588577
});
@@ -3953,6 +3942,69 @@ export const value = 1;
39533942
assert.notStrictEqual(second, first);
39543943
});
39553944

3945+
test("checker symbol methods merge declarations and parents in their project", async () => {
3946+
await using api = spawnAPI({
3947+
"/tsconfig.json": JSON.stringify({ files: ["/src/a.ts", "/src/b.ts"] }),
3948+
"/single/tsconfig.json": JSON.stringify({ files: ["/src/a.ts"] }),
3949+
"/src/a.ts": "namespace Merged { export const a = 1; }",
3950+
"/src/b.ts": "namespace Merged { export const b = 1; }",
3951+
});
3952+
const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json", "/single/tsconfig.json"] });
3953+
const project = snapshot.getConfiguredProject("/tsconfig.json")!;
3954+
const singleProject = snapshot.getConfiguredProject("/single/tsconfig.json")!;
3955+
const fileA = await project.program.getSourceFile("/src/a.ts");
3956+
const fileB = await project.program.getSourceFile("/src/b.ts");
3957+
assert.ok(fileA && fileB);
3958+
const declarationA = cast(fileA.statements[0], isModuleDeclaration);
3959+
const declarationB = cast(fileB.statements[0], isModuleDeclaration);
3960+
const rawA = await getSymbol(declarationA);
3961+
const rawB = await getSymbol(declarationB);
3962+
assert.notStrictEqual(rawA, rawB);
3963+
3964+
const merged = await project.checker.getSymbolOfDeclaration(declarationA);
3965+
assert.strictEqual(await project.checker.getSymbolOfDeclaration(declarationB), merged);
3966+
assert.strictEqual(await project.checker.getSymbolOfNode(declarationA), merged);
3967+
assert.strictEqual(await project.checker.getMergedSymbol(rawA), merged);
3968+
assert.strictEqual(await project.checker.getMergedSymbol(rawB), merged);
3969+
assert.strictEqual(await project.checker.getMergedSymbol(merged), merged);
3970+
assert.strictEqual(await singleProject.checker.getMergedSymbol(rawA), rawA);
3971+
assert.strictEqual(await singleProject.checker.getSymbolOfDeclaration(declarationA), rawA);
3972+
assert.equal(await project.checker.getSymbolOfNode(declarationA.name), undefined);
3973+
assert.equal(await project.checker.getSymbolOfNode(fileA), undefined);
3974+
assert.equal(await project.checker.getParentOfSymbol(merged), undefined);
3975+
3976+
const member = (await rawA.getExports()).get("a" as __String);
3977+
assert.ok(member);
3978+
assert.strictEqual(await member.getParent(), rawA);
3979+
assert.strictEqual(await project.checker.getParentOfSymbol(member), merged);
3980+
assert.strictEqual(await singleProject.checker.getParentOfSymbol(member), rawA);
3981+
});
3982+
3983+
test("checker symbol methods late-bind computed members", async () => {
3984+
await using api = spawnAPI({
3985+
"/src/computed.ts": "declare const key: unique symbol;\nclass Container { [key] = 1; ordinary = 2; }\nimport {} from './missing';",
3986+
});
3987+
const snapshot = await api.createSnapshot({ openFiles: ["/src/computed.ts"] });
3988+
const project = snapshot.getProjects()[0];
3989+
const file = await project.program.getSourceFile("/src/computed.ts");
3990+
assert.ok(file);
3991+
const declaration = cast(file.statements[1], isClassDeclaration);
3992+
const memberDeclaration = cast(declaration.members[0], isPropertyDeclaration);
3993+
const raw = await getSymbol(memberDeclaration);
3994+
const lateBound = await project.checker.getSymbolOfDeclaration(memberDeclaration);
3995+
assert.notStrictEqual(lateBound, raw);
3996+
assert.strictEqual(await project.checker.getSymbolOfNode(memberDeclaration), lateBound);
3997+
const container = await project.checker.getSymbolOfDeclaration(declaration);
3998+
assert.strictEqual(await project.checker.getParentOfSymbol(lateBound), container);
3999+
const type = await project.checker.getDeclaredTypeOfSymbol(container);
4000+
const property = (await type.getProperties()).find(symbol => symbol.escapedName === lateBound.escapedName);
4001+
assert.ok(property);
4002+
assert.strictEqual(await project.checker.getTargetSymbol(property), lateBound);
4003+
const ordinary = cast(declaration.members[1], isPropertyDeclaration);
4004+
assert.strictEqual(await project.checker.getSymbolOfDeclaration(ordinary), await getSymbol(ordinary));
4005+
assert.equal(await project.checker.getSymbolOfNode(file.statements[2]), undefined);
4006+
});
4007+
39564008
test("snapshot-owned symbols resolve cached file-owned parents", async () => {
39574009
const source = `interface Box<T> { value: T; }\ndeclare const box: Box<string>;\nbox;`;
39584010
await using api = spawnAPI({

0 commit comments

Comments
 (0)