diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 6c1eb480123e6..3beeee93c68c8 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -47,6 +47,7 @@ import { import { decodeNode, getNodeId, + getNodeRequestHandle, parseNodeHandleFromCompiler, readParseOptionsKey, readSourceFileHash, @@ -1227,15 +1228,17 @@ export class ModuleResolver { class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); - private readonly projectRegistries: Map = new Map(); + private readonly nodeHandles: Map = new Map(); + private readonly projectRegistries: Map = new Map(); private readonly snapshotId: number; + private disposed = false; constructor(snapshotId: number) { this.snapshotId = snapshotId; } addProjectRegistry(registry: ProjectObjectRegistry): void { - this.projectRegistries.set(registry.project.id, registry); + this.projectRegistries.set(registry.project.handle, registry); } getOrCreateSymbol(data: SymbolResponse): Symbol { @@ -1258,8 +1261,19 @@ class SnapshotObjectRegistry { return this.symbols.get(id); } + getOrCreateNodeHandle(handle: string, project: Project): NodeHandle { + let node = this.nodeHandles.get(handle); + if (!node) { + node = new NodeHandle(handle, project); + if (!this.disposed) this.nodeHandles.set(handle, node); + } + return node as NodeHandle; + } + clear(): void { + this.disposed = true; this.symbols.clear(); + this.nodeHandles.clear(); } } @@ -1329,7 +1343,7 @@ class ProjectObjectRegistry { } createNodeHandle(handle: string): NodeHandle { - return new NodeHandle(handle, this.project); + return this.snapshotRegistry.getOrCreateNodeHandle(handle, this.project); } getOrCreateSignature(data: SignatureResponse): Signature { @@ -1364,7 +1378,7 @@ class ProjectObjectRegistry { const data = await this.client.apiRequest(method, { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, objectId: source.id, }); if (!data) return undefined; @@ -1385,7 +1399,7 @@ class ProjectObjectRegistry { ? await this.client.apiRequest(method, { symbol: source.reference }) : await this.client.apiRequest(method, { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, objectId: source.id, }); if (!data) throw new Error(`${method} returned null symbol for ${source.constructor.name} ${source.id}`); @@ -1399,7 +1413,7 @@ class ProjectObjectRegistry { const data = await this.client.apiRequest(method, { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, objectId: source.id, }); if (!data) throw new Error(`${method} returned null signature for ${source.constructor.name} ${source.id}`); @@ -1422,7 +1436,7 @@ class ProjectObjectRegistry { } const typesData = await this.client.apiRequest(method, { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, objectId: source.id, }); if (typesData == null) return []; @@ -1449,7 +1463,7 @@ class ProjectObjectRegistry { ? await this.client.apiRequest(method, { symbol: source.reference }) : await this.client.apiRequest(method, { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, objectId: source.id, }); return data?.map(symbol => this.getOrCreateSymbol(symbol)) ?? []; @@ -1460,7 +1474,7 @@ class ProjectObjectRegistry { async fetchBaseTypes(source: Type): Promise { const typesData = await this.client.apiRequest("getBaseTypes", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: source.id, }); if (typesData == null) return []; @@ -1470,7 +1484,7 @@ class ProjectObjectRegistry { async fetchPropertiesOfType(source: Type): Promise { const data = await this.client.apiRequest("getPropertiesOfType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: source.id, }); return data ? data.map(symbol => this.getOrCreateSymbol(symbol)) : []; @@ -1479,7 +1493,7 @@ class ProjectObjectRegistry { async fetchApparentPropertiesOfType(source: Type): Promise { const data = await this.client.apiRequest("getApparentPropertiesOfType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, objectId: source.id, }); return data ? data.map(symbol => this.getOrCreateSymbol(symbol)) : []; @@ -1488,7 +1502,7 @@ class ProjectObjectRegistry { async fetchPropertyOfType(source: Type, name: string): Promise { const data = await this.client.apiRequest("getPropertyOfType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: source.id, name, }); @@ -1498,7 +1512,7 @@ class ProjectObjectRegistry { async fetchSignaturesOfType(source: Type, kind: SignatureKind): Promise { const data = await this.client.apiRequest("getSignaturesOfType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: source.id, kind, }); @@ -1508,7 +1522,7 @@ class ProjectObjectRegistry { async fetchIndexInfosOfType(source: Type): Promise { const data = await this.client.apiRequest("getIndexInfosOfType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: source.id, }); if (!data) return []; @@ -1516,14 +1530,14 @@ class ProjectObjectRegistry { keyType: this.getOrCreateType(info.keyType), valueType: this.getOrCreateType(info.valueType), isReadonly: info.isReadonly ?? false, - declaration: info.declaration ? new NodeHandle(info.declaration, this.project) : undefined, + declaration: info.declaration ? this.createNodeHandle(info.declaration) : undefined, })); } async fetchTypeParameterAtPosition(source: Signature, pos: number): Promise { const data = await this.client.apiRequest("getTypeParameterAtPosition", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, signature: source.id, index: pos, }); @@ -1535,6 +1549,11 @@ export class Project { /** @internal */ readonly api: API; readonly id: Id; + /** + * Server identity for this project's lifetime across snapshots. + * @internal + */ + readonly handle: number; readonly configFileName: RootedFilePath | undefined; readonly currentDirectory: RootedDirectoryPath; readonly dirty: boolean; @@ -1546,6 +1565,7 @@ export class Project { constructor(data: ProjectResponse, snapshotId: number, toPath: (fileName: string, basePath?: string) => PathKey, api: API, snapshotRegistry: SnapshotObjectRegistry) { this.id = data.id as Id; + this.handle = data.handle; this.api = api; this.configFileName = data.configFileName; this.currentDirectory = data.currentDirectory; @@ -1599,7 +1619,7 @@ export class LanguageService { const data = await this.client.apiRequest("getImportAdderEdits", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, actions: requestActions, }); @@ -1628,33 +1648,33 @@ export class LanguageService { async getReferencedSymbolsForNode(node: Node, position: number): Promise { const data = await this.client.apiRequest("getReferencedSymbolsForNode", { snapshot: this.snapshotId, - project: this.project.id, - node: getNodeId(node), + project: this.project.handle, + node: getNodeRequestHandle(node), position, }); return (data ?? []).map(entry => ({ - definition: new NodeHandle(entry.definition, this.project), + definition: this.objectRegistry.createNodeHandle(entry.definition), symbol: entry.symbol ? this.objectRegistry.getOrCreateSymbol(entry.symbol) : undefined, - references: (entry.references ?? []).map(h => new NodeHandle(h, this.project)), + references: (entry.references ?? []).map(h => this.objectRegistry.createNodeHandle(h)), })); } async getSignatureUsage(signatureDecl: Node): Promise { const data = await this.client.apiRequest("getSignatureUsages", { snapshot: this.snapshotId, - project: this.project.id, - signatureDecl: getNodeId(signatureDecl), + project: this.project.handle, + signatureDecl: getNodeRequestHandle(signatureDecl), }); return (data ?? []).map(entry => ({ - name: new NodeHandle(entry.name, this.project), - call: entry.call ? new NodeHandle(entry.call, this.project) : undefined, + name: this.objectRegistry.createNodeHandle(entry.name), + call: entry.call ? this.objectRegistry.createNodeHandle(entry.call) : undefined, })); } async getCompletionsAtPosition(document: string, position: number, options?: CompletionOptions): Promise { const data = await this.client.apiRequest("getCompletionsAtPosition", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file: document, position, triggerCharacter: options?.triggerCharacter, @@ -1684,7 +1704,7 @@ export class LanguageService { const base64 = uint8ArrayToBase64(encoded); return this.client.apiRequest("formatNodeForInsertion", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, position, data: base64, @@ -1778,7 +1798,7 @@ export class Program implements FormatDiagnost // Fetch from server const binaryData = await this.client.apiRequestBinary("getSourceFile", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, }); if (!binaryData) { @@ -1797,7 +1817,7 @@ export class Program implements FormatDiagnost ): Promise { const result = await this.client.apiRequest("getResolvedModule", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, moduleName, mode, @@ -1808,16 +1828,16 @@ export class Program implements FormatDiagnost async getModeForUsageLocation(file: DocumentIdentifier, usage: StringLiteralLikeNode): Promise { return this.client.apiRequest("getModeForUsageLocation", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, - usage: getNodeId(usage), + usage: getNodeRequestHandle(usage), }); } async getModeForResolutionAtIndex(file: DocumentIdentifier, index: number): Promise { return this.client.apiRequest("getModeForResolutionAtIndex", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, index, }); @@ -1829,8 +1849,8 @@ export class Program implements FormatDiagnost ): Promise { const result = await this.client.apiRequest("getResolvedModuleFromModuleSpecifier", { snapshot: this.snapshotId, - project: this.project.id, - moduleSpecifier: getNodeId(moduleSpecifier), + project: this.project.handle, + moduleSpecifier: getNodeRequestHandle(moduleSpecifier), sourceFile, }); return result ?? undefined; @@ -1843,7 +1863,7 @@ export class Program implements FormatDiagnost ): Promise { const result = await this.client.apiRequest("getResolvedTypeReferenceDirective", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, typeDirectiveName, mode, @@ -1857,7 +1877,7 @@ export class Program implements FormatDiagnost ): Promise { const result = await this.client.apiRequest("getResolvedTypeReferenceDirectiveFromTypeReferenceDirective", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, sourceFile, typeDirectiveName: typeReferenceDirective.fileName, resolutionMode: typeReferenceDirective.resolutionMode, @@ -1868,7 +1888,7 @@ export class Program implements FormatDiagnost async getSourceFileNames(): Promise { const data = await this.client.apiRequest("getSourceFileNames", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, }); return data ?? []; } @@ -1902,7 +1922,7 @@ export class Program implements FormatDiagnost // client-provided path text as untrusted input. const data = await this.client.apiRequest("getSourceFileMetadata", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file: path, }); return data ?? undefined; @@ -1939,7 +1959,7 @@ export class Program implements FormatDiagnost async getConfigFileNames(): Promise { const data = await this.client.apiRequest("getConfigFileNames", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, }); return data ?? []; } @@ -1951,7 +1971,7 @@ export class Program implements FormatDiagnost async getConfigSourceFile(file: DocumentIdentifier): Promise { const binaryData = await this.client.apiRequestBinary("getConfigSourceFile", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, }); if (!binaryData) { @@ -1971,7 +1991,7 @@ export class Program implements FormatDiagnost : [file]; const data = await this.client.apiRequest("getSyntacticDiagnostics", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, files, }); return data ?? []; @@ -1987,7 +2007,7 @@ export class Program implements FormatDiagnost : [file]; const data = await this.client.apiRequest("getBindDiagnostics", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, files, }); return data ?? []; @@ -2003,7 +2023,7 @@ export class Program implements FormatDiagnost : [file]; const data = await this.client.apiRequest("getSemanticDiagnostics", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, files, }); return data ?? []; @@ -2019,7 +2039,7 @@ export class Program implements FormatDiagnost : [file]; const data = await this.client.apiRequest("getSuggestionDiagnostics", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, files, }); return data ?? []; @@ -2035,7 +2055,7 @@ export class Program implements FormatDiagnost : [file]; const data = await this.client.apiRequest("getDeclarationDiagnostics", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, files, }); return data ?? []; @@ -2047,7 +2067,7 @@ export class Program implements FormatDiagnost async getProgramDiagnostics(): Promise { const data = await this.client.apiRequest("getProgramDiagnostics", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, }); return data ?? []; } @@ -2058,7 +2078,7 @@ export class Program implements FormatDiagnost async getGlobalDiagnostics(): Promise { const data = await this.client.apiRequest("getGlobalDiagnostics", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, }); return data ?? []; } @@ -2069,7 +2089,7 @@ export class Program implements FormatDiagnost async getConfigFileParsingDiagnostics(): Promise { const data = await this.client.apiRequest("getConfigFileParsingDiagnostics", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, }); return data ?? []; } @@ -2082,7 +2102,7 @@ export class Program implements FormatDiagnost async emit(emitOnly?: EmitOnly): Promise { const response = await this.client.apiRequest("emit", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, emitOnly, }); const fileSystem = response.emittedFilesContents.length @@ -2105,7 +2125,7 @@ export class Program implements FormatDiagnost async emitToString(emitOnly?: EmitOnly): Promise { const response = await this.client.apiRequest("emitToString", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, emitOnly, }); return toEmitOutput(response); @@ -2117,7 +2137,7 @@ export class Program implements FormatDiagnost async getJavaScriptEmit(files: readonly DocumentIdentifier[]): Promise { const response = await this.client.apiRequest("getJavaScriptEmit", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, files, }); return toEmitOutput(response); @@ -2129,7 +2149,7 @@ export class Program implements FormatDiagnost async getDeclarationEmit(files: readonly DocumentIdentifier[]): Promise { const response = await this.client.apiRequest("getDeclarationEmit", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, files, }); return toEmitOutput(response); @@ -2278,7 +2298,7 @@ export class Checker { async getMergedSymbol(symbol: Symbol): Promise { const data = await this.client.apiRequest("getMergedSymbol", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); return this.objectRegistry.getOrCreateSymbol(data); @@ -2291,8 +2311,8 @@ export class Checker { async getSymbolOfNode(node: Node): Promise { const data = await this.client.apiRequest("getSymbolOfNode", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } @@ -2301,8 +2321,8 @@ export class Checker { async getSymbolOfDeclaration(node: Declaration): Promise { const data = await this.client.apiRequest("getSymbolOfDeclarationForChecker", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), }); // Declarations normally have symbols, but preserve unexpected null results at runtime. return (data && this.objectRegistry.getOrCreateSymbol(data))!; @@ -2312,7 +2332,7 @@ export class Checker { async getParentOfSymbol(symbol: Symbol): Promise { const data = await this.client.apiRequest("getParentOfSymbolForChecker", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; @@ -2324,15 +2344,15 @@ export class Checker { if (Array.isArray(nodeOrNodes)) { const data = await this.client.apiRequest("getSymbolsAtLocations", { snapshot: this.snapshotId, - project: this.project.id, - locations: nodeOrNodes.map(node => getNodeId(node)), + project: this.project.handle, + locations: nodeOrNodes.map(node => getNodeRequestHandle(node)), }); return data.map(d => d ? this.objectRegistry.getOrCreateSymbol(d) : undefined); } const data = await this.client.apiRequest("getSymbolAtLocation", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(nodeOrNodes as Node), + project: this.project.handle, + location: getNodeRequestHandle(nodeOrNodes as Node), }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } @@ -2343,7 +2363,7 @@ export class Checker { if (typeof positionOrPositions === "number") { const data = await this.client.apiRequest("getSymbolAtPosition", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, position: positionOrPositions, }); @@ -2351,7 +2371,7 @@ export class Checker { } const data = await this.client.apiRequest("getSymbolsAtPositions", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, positions: positionOrPositions, }); @@ -2364,14 +2384,14 @@ export class Checker { if (Array.isArray(fileOrFiles)) { const data = await this.client.apiRequest("getSymbolsOfSourceFiles", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, files: fileOrFiles, }); return data.map(d => d ? this.objectRegistry.getOrCreateSymbol(d) : undefined); } const data = await this.client.apiRequest("getSymbolOfSourceFile", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file: fileOrFiles as DocumentIdentifier, }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; @@ -2388,14 +2408,14 @@ export class Checker { if (Array.isArray(symbolOrSymbols)) { const data = await this.client.apiRequest("getTypesOfSymbols", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbols: symbolOrSymbols.map(symbol => symbol.reference), }); return data.map(d => this.objectRegistry.getOrCreateType(d)); } const data = await this.client.apiRequest("getTypeOfSymbol", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: (symbolOrSymbols as Symbol).reference, }); return this.objectRegistry.getOrCreateType(data); @@ -2409,7 +2429,7 @@ export class Checker { async getDeclaredTypeOfSymbol(symbol: Symbol): Promise { const data = await this.client.apiRequest("getDeclaredTypeOfSymbol", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); return this.objectRegistry.getOrCreateType(data); @@ -2424,7 +2444,7 @@ export class Checker { async getNonMissingTypeOfSymbol(symbol: Symbol): Promise { const data = await this.client.apiRequest("getNonMissingTypeOfSymbol", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); return this.objectRegistry.getOrCreateType(data); @@ -2433,11 +2453,11 @@ export class Checker { async getReferencesToSymbolInFile(file: DocumentIdentifier, symbol: Symbol): Promise { const data = await this.client.apiRequest("getReferencesToSymbolInFile", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, symbol: symbol.reference, }); - return (data ?? []).map(h => new NodeHandle(h, this.project)); + return (data ?? []).map(h => this.objectRegistry.createNodeHandle(h)); } /** @@ -2451,15 +2471,15 @@ export class Checker { if (Array.isArray(nodeOrNodes)) { const data = await this.client.apiRequest("getTypeAtLocations", { snapshot: this.snapshotId, - project: this.project.id, - locations: nodeOrNodes.map(node => getNodeId(node)), + project: this.project.handle, + locations: nodeOrNodes.map(node => getNodeRequestHandle(node)), }); return data.map(d => this.objectRegistry.getOrCreateType(d)); } const data = await this.client.apiRequest("getTypeAtLocation", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(nodeOrNodes as Node), + project: this.project.handle, + location: getNodeRequestHandle(nodeOrNodes as Node), }); return this.objectRegistry.getOrCreateType(data); } @@ -2476,8 +2496,8 @@ export class Checker { async getResolvedSignature(node: Node): Promise { const data = await this.client.apiRequest("getResolvedSignature", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), }); return this.objectRegistry.getOrCreateSignature(data); } @@ -2488,7 +2508,7 @@ export class Checker { if (typeof positionOrPositions === "number") { const data = await this.client.apiRequest("getTypeAtPosition", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, position: positionOrPositions, }); @@ -2496,7 +2516,7 @@ export class Checker { } const data = await this.client.apiRequest("getTypesAtPositions", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, file, positions: positionOrPositions, }); @@ -2513,10 +2533,10 @@ export class Checker { const isNode = location && "kind" in location; const data = await this.client.apiRequest("resolveName", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, name, meaning, - location: isNode ? getNodeId(location as Node) : undefined, + location: isNode ? getNodeRequestHandle(location as Node) : undefined, file: !isNode && location ? (location as DocumentPosition).document : undefined, position: !isNode && location ? (location as DocumentPosition).position : undefined, excludeGlobals, @@ -2532,9 +2552,9 @@ export class Checker { const isNode = "kind" in location; const data = await this.client.apiRequest("getSymbolsInScope", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, meaning, - location: isNode ? getNodeId(location as Node) : undefined, + location: isNode ? getNodeRequestHandle(location as Node) : undefined, file: isNode ? undefined : (location as DocumentPosition).document, position: isNode ? undefined : (location as DocumentPosition).position, }); @@ -2550,8 +2570,8 @@ export class Checker { async getContextualType(node: Expression): Promise { const data = await this.client.apiRequest("getContextualType", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), }); return data ? this.objectRegistry.getOrCreateType(data) : undefined; } @@ -2559,8 +2579,8 @@ export class Checker { async getContextualTypeForArgumentAtIndex(node: CallLikeExpression, argIndex: number): Promise { const data = await this.client.apiRequest("getContextualTypeForArgument", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), index: argIndex, }); return data ? this.objectRegistry.getOrCreateType(data) : undefined; @@ -2569,7 +2589,7 @@ export class Checker { async getAwaitedType(type: Type): Promise { const data = await this.client.apiRequest("getAwaitedType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, }); return data ? this.objectRegistry.getOrCreateType(data) : undefined; @@ -2579,7 +2599,7 @@ export class Checker { async getBaseTypeOfLiteralType(type: Type): Promise { const data = await this.client.apiRequest("getBaseTypeOfLiteralType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, }); return this.objectRegistry.getOrCreateType(data); @@ -2598,8 +2618,8 @@ export class Checker { async getTypeFromTypeNode(node: TypeNode): Promise { const data = await this.client.apiRequest("getTypeFromTypeNode", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), }); return this.objectRegistry.getOrCreateType(data); } @@ -2608,7 +2628,7 @@ export class Checker { async getWidenedType(type: Type): Promise { const data = await this.client.apiRequest("getWidenedType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, }); return this.objectRegistry.getOrCreateType(data); @@ -2621,7 +2641,7 @@ export class Checker { async getParameterType(signature: Signature, index: number): Promise { const data = await this.client.apiRequest("getParameterType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, signature: signature.id, index, }); @@ -2631,7 +2651,7 @@ export class Checker { async isArrayLikeType(type: Type): Promise { return this.client.apiRequest("isArrayLikeType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, }); } @@ -2639,7 +2659,7 @@ export class Checker { async isTypeAssignableTo(source: Type, target: Type): Promise { return this.client.apiRequest("isTypeAssignableTo", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, source: source.id, target: target.id, }); @@ -2648,8 +2668,8 @@ export class Checker { async getShorthandAssignmentValueSymbol(node: Node): Promise { const data = await this.client.apiRequest("getShorthandAssignmentValueSymbol", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } @@ -2662,9 +2682,9 @@ export class Checker { async getTypeOfSymbolAtLocation(symbol: Symbol, location: Node): Promise { const data = await this.client.apiRequest("getTypeOfSymbolAtLocation", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, - location: getNodeId(location), + location: getNodeRequestHandle(location), }); return this.objectRegistry.getOrCreateType(data); } @@ -2672,7 +2692,7 @@ export class Checker { private async getIntrinsicType(method: IntrinsicTypeMethod): Promise { const data = await this.client.apiRequest(method, { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, }); return this.objectRegistry.getOrCreateType(data); } @@ -2717,9 +2737,9 @@ export class Checker { async typeToTypeNode(type: Type, enclosingDeclaration?: Node, flags?: number): Promise { const binaryData = await this.client.apiRequestBinary("typeToTypeNode", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (!binaryData) return undefined; @@ -2729,10 +2749,10 @@ export class Checker { async signatureToSignatureDeclaration(signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): Promise { const binaryData = await this.client.apiRequestBinary("signatureToSignatureDeclaration", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, signature: signature.id, kind, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (!binaryData) return undefined; @@ -2742,9 +2762,9 @@ export class Checker { async typeToString(type: Type, enclosingDeclaration?: Node, flags?: TypeFormatFlags): Promise { const result = await this.client.apiRequest("typeToString", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (typeof result !== "string") throw new TypeError("typeToString returned a non-string result"); @@ -2754,15 +2774,15 @@ export class Checker { async isContextSensitive(node: Node): Promise { return this.client.apiRequest("isContextSensitive", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), }); } async isArrayType(type: Type): Promise { return this.client.apiRequest("isArrayType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, }); } @@ -2787,7 +2807,7 @@ export class Checker { async isReadonlySymbol(symbol: Symbol): Promise { return this.client.apiRequest("isReadonlySymbol", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); } @@ -2804,7 +2824,7 @@ export class Checker { async getRestTypeOfSignature(signature: Signature): Promise { const data = await this.client.apiRequest("getRestTypeOfSignature", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, signature: signature.id, }); return this.objectRegistry.getOrCreateType(data); @@ -2813,7 +2833,7 @@ export class Checker { async getTypePredicateOfSignature(signature: Signature): Promise { const data = await this.client.apiRequest("getTypePredicateOfSignature", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, signature: signature.id, }); if (!data) return undefined; @@ -2854,7 +2874,7 @@ export class Checker { async getIndexInfoOfType(type: Type, kind: IndexKind): Promise { const data = await this.client.apiRequest("getIndexInfoOfType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, kind, }); @@ -2862,7 +2882,7 @@ export class Checker { keyType: this.objectRegistry.getOrCreateType(data.keyType), valueType: this.objectRegistry.getOrCreateType(data.valueType), isReadonly: data.isReadonly ?? false, - declaration: data.declaration ? new NodeHandle(data.declaration, this.project) : undefined, + declaration: data.declaration ? this.objectRegistry.createNodeHandle(data.declaration) : undefined, } : undefined; } @@ -2873,7 +2893,7 @@ export class Checker { async getTypeOfPropertyOfType(type: Type, propertyName: string): Promise { const data = await this.client.apiRequest("getTypeOfPropertyOfType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, name: propertyName, }); @@ -2895,7 +2915,7 @@ export class Checker { async getBaseConstraintOfType(type: Type): Promise { const data = await this.client.apiRequest("getBaseConstraintOfType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, }); return data ? this.objectRegistry.getOrCreateType(data) : undefined; @@ -2904,7 +2924,7 @@ export class Checker { async getPropertyOfType(type: Type, name: string): Promise { const data = await this.client.apiRequest("getPropertyOfType", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, name, }); @@ -2914,8 +2934,8 @@ export class Checker { async getConstantValue(node: Node): Promise { const data = await this.client.apiRequest("getConstantValue", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), }); if (!data || (typeof data.value !== "string" && typeof data.value !== "number")) { return undefined; @@ -2936,8 +2956,8 @@ export class Checker { async getSignatureFromDeclaration(node: Node): Promise { const data = await this.client.apiRequest("getSignatureFromDeclaration", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), }); return this.objectRegistry.getOrCreateSignature(data); } @@ -2945,8 +2965,8 @@ export class Checker { async getExportSpecifierLocalTargetSymbol(node: Node): Promise { const data = await this.client.apiRequest("getExportSpecifierLocalTargetSymbol", { snapshot: this.snapshotId, - project: this.project.id, - location: getNodeId(node), + project: this.project.handle, + location: getNodeRequestHandle(node), }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } @@ -2959,7 +2979,7 @@ export class Checker { async getAliasedSymbol(symbol: Symbol): Promise { const data = await this.client.apiRequest("getAliasedSymbol", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); return this.objectRegistry.getOrCreateSymbol(data); @@ -2972,7 +2992,7 @@ export class Checker { async getFullyQualifiedName(symbol: Symbol): Promise { return this.client.apiRequest("getFullyQualifiedName", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); } @@ -2980,7 +3000,7 @@ export class Checker { async getImmediateAliasedSymbol(symbol: Symbol): Promise { const data = await this.client.apiRequest("getImmediateAliasedSymbol", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; @@ -2993,7 +3013,7 @@ export class Checker { if (symbol.checkFlags & CheckFlags.Instantiated) { const data = await this.client.apiRequest("getTargetSymbol", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); return this.objectRegistry.getOrCreateSymbol(data); @@ -3004,7 +3024,7 @@ export class Checker { async getExportSymbolOfSymbol(symbol: Symbol): Promise { const data = await this.client.apiRequest("getExportSymbolOfSymbolForChecker", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); return this.objectRegistry.getOrCreateSymbol(data); @@ -3019,7 +3039,7 @@ export class Checker { private getWellKnownSymbols(): Promise<{ unknown: number; undefined: number; arguments: number; }> { return this.wellKnownSymbols ??= this.client.apiRequest("getWellKnownSymbols", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, }); } @@ -3053,7 +3073,7 @@ export class Checker { private getWellKnownSignatures(): Promise<{ unknown: number; }> { return this.wellKnownSignatures ??= this.client.apiRequest("getWellKnownSignatures", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, }); } @@ -3069,7 +3089,7 @@ export class Checker { async getExportsOfModule(symbol: Symbol): Promise { const data = await this.client.apiRequest("getExportsOfModule", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); return data ? data.map(d => this.objectRegistry.getOrCreateSymbol(d)) : []; @@ -3078,7 +3098,7 @@ export class Checker { async getMemberInModuleExports(symbol: Symbol, name: string): Promise { const data = await this.client.apiRequest("getMemberInModuleExports", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, name, }); @@ -3088,7 +3108,7 @@ export class Checker { async getJsDocTagsOfSymbol(symbol: Symbol): Promise { const data = await this.client.apiRequest("getJsDocTags", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); return data ?? []; @@ -3097,7 +3117,7 @@ export class Checker { async getDocumentationCommentOfSymbol(symbol: Symbol): Promise { return this.client.apiRequest("getDocumentationComment", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, symbol: symbol.reference, }); } @@ -3108,7 +3128,7 @@ export class Checker { async getTypeArguments(type: TypeReference): Promise { const data = await this.client.apiRequest("getTypeArguments", { snapshot: this.snapshotId, - project: this.project.id, + project: this.project.handle, type: type.id, }); return data ? data.map(d => this.objectRegistry.getOrCreateType(d)) : []; @@ -3153,7 +3173,7 @@ export class Printer { export class NodeHandle { /** - * The project this handle was produced in, used as the default for {@link resolve}. + * The project where this handle was first materialized, used as the default for {@link resolve}. * Node handles are only meaningful within a project's program, so the producing project * is remembered so callers don't have to pass it explicitly. */ @@ -3162,12 +3182,16 @@ export class NodeHandle { private readonly fileOwner: SourceFileOwner | undefined; readonly index: number; readonly kind: SyntaxKind; + /** Identity of the exact parsed source file, shared across programs that reuse that AST. */ + readonly sourceFileId: string; + /** Retrieval metadata; never used to substitute a different source-file AST. */ readonly path: PathKey; constructor(handle: string, canonicalProject: Project | undefined, fileOwner?: SourceFileOwner) { const parsed = parseNodeHandleFromCompiler(handle); this.index = parsed.index; this.kind = parsed.kind; + this.sourceFileId = parsed.sourceFileId; this.path = parsed.path; this.canonicalProject = canonicalProject; this.fileOwner = fileOwner; @@ -3177,19 +3201,31 @@ export class NodeHandle { * Resolve this handle to the actual AST node by fetching the source file from a project * and looking up the node by index. If no project is passed, the project that produced * the handle is used. Declarations of file-owned symbols identify an exact source file and - * resolve through it, independently of any project. + * resolve through it, independently of any project. A different AST at the + * same path is never substituted. */ async resolve(project: Project | undefined = this.canonicalProject): Promise { if (this.fileOwner) { const sourceFile = this.fileOwner.record.file ?? await this.fetchOwnerFile(this.fileOwner); - return sourceFile.getOrCreateNodeAtIndex(this.index) as T | undefined; + return this.resolveInFile(sourceFile); } - if (!project) throw new Error(`Node handle for '${this.path}' has no project context`); + if (!project) throw new Error(`Node handle for source file '${this.sourceFileId}' has no project context`); const sourceFile = await project.program.getSourceFileByPath(this.path); if (!sourceFile) { - return undefined; + throw new Error("Node handle source file is not part of the requested program"); } - return (sourceFile as unknown as RemoteSourceFile).getOrCreateNodeAtIndex(this.index) as T | undefined; + return this.resolveInFile(sourceFile as unknown as RemoteSourceFile); + } + + private resolveInFile(sourceFile: RemoteSourceFile): T | undefined { + if (sourceFile.nodeId !== this.sourceFileId) { + throw new Error(`Node handle source file is not part of the requested program`); + } + const node = sourceFile.getOrCreateNodeAtIndex(this.index); + if (!node || node.kind !== this.kind) { + throw new Error("Node handle does not identify a node of the expected kind"); + } + return node as T; } private async fetchOwnerFile(fileOwner: SourceFileOwner): Promise { @@ -3244,8 +3280,11 @@ export class Symbol { readonly checkFlags: CheckFlags; /** @internal */ readonly reference: SymbolReference; - readonly declarations: readonly NodeHandle[]; - readonly valueDeclaration: NodeHandle | undefined; + // Declaration handles are kept in their wire form until first accessed. + private readonly declarationHandles: readonly string[] | undefined; + private readonly valueDeclarationHandle: string | undefined; + private declarationsCache: readonly NodeHandle[] | undefined; + private valueDeclarationCache: NodeHandle | undefined; private readonly parent!: CompactSymbolReference; private readonly exportSymbol!: CompactSymbolReference; private membersCache: Promise> | undefined; @@ -3259,19 +3298,34 @@ export class Symbol { this.escapedName = data.name as __String; this.name = unescapeLeadingUnderscores(data.name as __String); this.flags = data.flags; - this.checkFlags = data.checkFlags; - // A file-owned symbol has no canonical project; its declarations resolve through its file. - const project = storage.kind === SymbolOwnerKind.Snapshot ? storage.registry.project : undefined; - const fileOwner = storage.kind === SymbolOwnerKind.File ? storage.owner : undefined; - this.declarations = (data.declarations ?? []).map(handle => new NodeHandle(handle, project, fileOwner)); - this.valueDeclaration = data.valueDeclaration - ? new NodeHandle(data.valueDeclaration, project, fileOwner) - : undefined; + this.checkFlags = data.checkFlags ?? 0; + this.declarationHandles = data.declarations; + this.valueDeclarationHandle = data.valueDeclaration; + this.declarationsCache = undefined; + this.valueDeclarationCache = undefined; if (data.parent !== undefined) this.parent = data.parent; if (data.exportSymbol !== undefined) this.exportSymbol = data.exportSymbol; } + get declarations(): readonly NodeHandle[] { + return this.declarationsCache ??= this.declarationHandles?.map(handle => this.createDeclarationHandle(handle)) ?? []; + } + + get valueDeclaration(): NodeHandle | undefined { + if (this.valueDeclarationCache === undefined && this.valueDeclarationHandle) { + this.valueDeclarationCache = this.createDeclarationHandle(this.valueDeclarationHandle); + } + return this.valueDeclarationCache; + } + + private createDeclarationHandle(handle: string): NodeHandle { + // A file-owned symbol has no canonical project; its declarations resolve through its file. + return this.storage.kind === SymbolOwnerKind.Snapshot + ? this.storage.registry.createNodeHandle(handle) + : new NodeHandle(handle, undefined, this.storage.owner); + } + async getParent(): Promise { if (!this.parent) return undefined; return this.fetchSymbol("getParentOfSymbol", this.parent); @@ -3410,7 +3464,7 @@ class TypeObject implements Type { this.id = data.id; this.flags = data.flags; - if (data.objectFlags !== undefined) this.objectFlags = data.objectFlags; + this.objectFlags = data.objectFlags ?? 0; if (data.symbol !== undefined) this.symbol = data.symbol; if (data.value != null) { // BigInt literal values are serialized as decimal strings (e.g. "-123") because @@ -3436,7 +3490,7 @@ class TypeObject implements Type { } } if (data.intrinsicName !== undefined) this.intrinsicName = data.intrinsicName; - if (data.isThisType !== undefined) this.isThisType = data.isThisType; + this.isThisType = data.isThisType ?? false; if (data.freshType !== undefined) this.freshType = data.freshType; if (data.regularType !== undefined) this.regularType = data.regularType; if (data.target !== undefined) this.target = data.target; @@ -3882,7 +3936,9 @@ export class Signature { private objectRegistry: ProjectObjectRegistry; readonly id: number; - readonly declaration?: NodeHandle | undefined; + private readonly project: Project; + private readonly declarationHandle: string | undefined; + private declarationCache: NodeHandle | undefined; readonly typeParameters?: readonly number[] | undefined; readonly parameters: readonly CompactSymbolReference[]; readonly thisParameter?: CompactSymbolReference | undefined; @@ -3891,9 +3947,11 @@ export class Signature { constructor(data: SignatureResponse, project: Project, objectRegistry: ProjectObjectRegistry) { this.id = data.id; - this.flags = data.flags; + this.flags = data.flags ?? 0; this.objectRegistry = objectRegistry; - this.declaration = data.declaration ? new NodeHandle(data.declaration, project) : undefined; + this.project = project; + this.declarationHandle = data.declaration; + this.declarationCache = undefined; this.typeParameters = data.typeParameters ?? []; this.parameters = data.parameters ?? []; this.thisParameter = data.thisParameter; @@ -3901,6 +3959,13 @@ export class Signature { this.returnType = false; } + get declaration(): NodeHandle | undefined { + if (this.declarationCache === undefined && this.declarationHandle) { + this.declarationCache = this.objectRegistry.createNodeHandle(this.declarationHandle); + } + return this.declarationCache; + } + async getTypeParameters(): Promise { return this.objectRegistry.fetchTypes(this, "getTypeParametersOfSignature", this.typeParameters) as Promise; } diff --git a/packages/typescript/src/api/node/node.ts b/packages/typescript/src/api/node/node.ts index bca2153328903..b365bf15945bf 100644 --- a/packages/typescript/src/api/node/node.ts +++ b/packages/typescript/src/api/node/node.ts @@ -34,6 +34,7 @@ import { import { HEADER_OFFSET_EXTENDED_DATA, HEADER_OFFSET_NODES, + HEADER_OFFSET_SOURCE_FILE_ID, HEADER_OFFSET_STRING_TABLE, HEADER_OFFSET_STRING_TABLE_OFFSETS, HEADER_OFFSET_STRUCTURED_DATA, @@ -106,6 +107,7 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo { private _cachedSupplementalSourceFileNames: readonly RootedFilePath[] | undefined; private _cachedDiagnosticDirectives: readonly MappedDiagnosticDirective[] | undefined; private _diagnosticDirectivesRead = false; + private _handleFileKey: string | undefined; constructor( data: Uint8Array, @@ -147,6 +149,22 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo { return readSourceFileNodeId(this.view); } + /** + * The file component of node handles sent to the server: `#` followed by the + * server's identity for this exact source file. + * @internal + */ + get handleFileKey(): string { + if (this._handleFileKey === undefined) { + const nodeId = this.view.getBigUint64(HEADER_OFFSET_SOURCE_FILE_ID, true); + if (nodeId === 0n) { + throw new Error("Node has no server source-file identity"); + } + this._handleFileKey = `#${nodeId}`; + } + return this._handleFileKey; + } + readFileReferences(structuredDataOffset: number): readonly FileReference[] { if (structuredDataOffset === NO_STRUCTURED_DATA) { return []; @@ -477,12 +495,13 @@ export function findDescendant(root: Node, pos: number, end: number, kind: Synta export interface ParsedNodeHandle { index: number; kind: SyntaxKind; + sourceFileId: string; path: PathKey; } /** * Parse a compiler-produced node handle into its components. - * Handle format: "index.kind.path" where path may contain dots. + * Handle format: "index.kind.#sourceFileId.path" where path may contain dots. */ export function parseNodeHandleFromCompiler(handle: string): ParsedNodeHandle { const firstDot = handle.indexOf("."); @@ -496,7 +515,13 @@ export function parseNodeHandleFromCompiler(handle: string): ParsedNodeHandle { const indexText = handle.slice(0, firstDot); const kindText = handle.slice(firstDot + 1, secondDot); - const path = handle.slice(secondDot + 1); + const thirdDot = handle.indexOf(".", secondDot + 1); + if (thirdDot === -1) { + throw new Error(`Invalid node handle: ${handle}`); + } + const fileKey = handle.slice(secondDot + 1, thirdDot); + const sourceFileId = fileKey.slice(1); + const path = handle.slice(thirdDot + 1); const index = Number(indexText); const kind = Number(kindText); const key = tryPathKeyFromCanonical(path); @@ -507,6 +532,8 @@ export function parseNodeHandleFromCompiler(handle: string): ParsedNodeHandle { !Number.isSafeInteger(kind) || kind < 0 || String(kind) !== kindText || + !/^#[1-9][0-9]*$/.test(fileKey) || + BigInt(sourceFileId) > 0xFFFFFFFFFFFFFFFFn || key === undefined ) { throw new Error(`Invalid node handle: ${handle}`); @@ -514,6 +541,7 @@ export function parseNodeHandleFromCompiler(handle: string): ParsedNodeHandle { return { index, kind: kind as SyntaxKind, + sourceFileId, path: key, }; } @@ -536,5 +564,24 @@ export function getNodeId(node: Node): string { if (!(node instanceof RemoteNode)) { throw new Error("getNodeId requires a RemoteNode"); } - return node.id; + const sourceFile = node["_sourceFile"]; + if (sourceFile instanceof RemoteSourceFile) { + return `${node["index"]}.${node.kind}.${sourceFile.handleFileKey}.${sourceFile.path}`; + } + throw new Error("Node has no server source-file identity"); +} + +/** + * Get the compact handle identifying a node in its exact server source-file AST. + * @internal + */ +export function getNodeRequestHandle(node: Node): string { + if (!(node instanceof RemoteNode)) { + throw new Error("getNodeRequestHandle requires a RemoteNode"); + } + const sourceFile = node["_sourceFile"]; + if (sourceFile instanceof RemoteSourceFile) { + return `${node["index"]}.${sourceFile.handleFileKey}`; + } + throw new Error("Node has no server source-file identity"); } diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 01e2fa27b12c2..93774869c4407 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -271,7 +271,7 @@ export interface SymbolResponse { reference: SymbolReference; name: string; flags: number; - checkFlags: number; + checkFlags?: number | undefined; declarations?: string[] | undefined; valueDeclaration?: string | undefined; parent?: CompactSymbolReference | undefined; @@ -467,6 +467,8 @@ export interface GetDefaultProjectForFileParams { export interface ProjectResponse { id: ProjectId; + /** Handle identifies this project's lifetime across snapshots; Id is its logical name. */ + handle: number; configFileName?: RootedFilePath | undefined; currentDirectory: RootedDirectoryPath; dirty: boolean; @@ -475,45 +477,45 @@ export interface ProjectResponse { export interface GetSymbolAtPositionParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; position: number; } export interface GetSymbolsAtPositionsParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; positions: readonly number[] | null; } export interface GetSymbolAtLocationParams { snapshot: number; - project: ProjectId; + project: number; location: string; } export interface GetSymbolsAtLocationsParams { snapshot: number; - project: ProjectId; + project: number; locations: readonly string[] | null; } export interface GetSymbolOfSourceFileParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; } export interface GetSymbolsOfSourceFilesParams { snapshot: number; - project: ProjectId; + project: number; files: readonly DocumentIdentifier[] | null; } export interface GetTypeOfSymbolParams { snapshot: number; - project: ProjectId; + project: number; symbol: SymbolReference; } @@ -524,9 +526,9 @@ export interface TypeResponse { isTupleType?: boolean | undefined; /** * Value is literal type data. BigInt literals are encoded as signed decimal - * strings because JSON cannot represent bigint; absent values are null. + * strings because JSON cannot represent bigint; absent values are omitted. */ - value: unknown; + value?: unknown | undefined; /** ObjectType / TypeReference / StringMappingType / IndexType target */ target?: number | undefined; /** InterfaceType type parameters */ @@ -572,19 +574,19 @@ export interface TypeResponse { export interface GetTypesOfSymbolsParams { snapshot: number; - project: ProjectId; + project: number; symbols: readonly SymbolReference[] | null; } export interface GetSourceFileParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; } export interface GetSourceFileNamesParams { snapshot: number; - project: ProjectId; + project: number; } /** SourceFileMetadata carries program-stored metadata about a single source file. */ @@ -598,21 +600,21 @@ export interface SourceFileMetadata { export interface GetModeForUsageLocationParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; usage: string; } export interface GetModeForResolutionAtIndexParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; index: number; } export interface GetResolvedModuleParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; moduleName: string; mode: ModuleKind; @@ -631,14 +633,14 @@ export interface ResolvedModule { export interface GetResolvedModuleFromModuleSpecifierParams { snapshot: number; - project: ProjectId; + project: number; moduleSpecifier: string; sourceFile?: DocumentIdentifier | undefined; } export interface GetResolvedTypeReferenceDirectiveParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; typeDirectiveName: string; mode: ModuleKind; @@ -654,7 +656,7 @@ export interface ResolvedTypeReferenceDirective { export interface GetResolvedTypeReferenceDirectiveFromReferenceParams { snapshot: number; - project: ProjectId; + project: number; sourceFile: DocumentIdentifier; typeDirectiveName: string; resolutionMode: ModuleKind; @@ -663,12 +665,12 @@ export interface GetResolvedTypeReferenceDirectiveFromReferenceParams { /** GetProjectDiagnosticsParams are parameters for project-wide diagnostic methods. */ export interface GetProjectDiagnosticsParams { snapshot: number; - project: ProjectId; + project: number; } export interface ResolveNameParams { snapshot: number; - project: ProjectId; + project: number; name: string; /** Optional: node handle for location context */ location?: string | undefined; @@ -688,7 +690,7 @@ export interface ResolveNameParams { */ export interface GetSymbolsInScopeParams { snapshot: number; - project: ProjectId; + project: number; /** Optional: node handle for location context */ location?: string | undefined; /** Optional: file for location context (alternative to Location) */ @@ -701,14 +703,14 @@ export interface GetSymbolsInScopeParams { export interface GetSignaturesOfTypeParams { snapshot: number; - project: ProjectId; + project: number; type: number; kind: number; } export interface SignatureResponse { id: number; - flags: number; + flags?: number | undefined; declaration?: string | undefined; typeParameters?: number[] | undefined; parameters?: CompactSymbolReference[] | undefined; @@ -718,32 +720,32 @@ export interface SignatureResponse { export interface GetResolvedSignatureParams { snapshot: number; - project: ProjectId; + project: number; location: string; } export interface GetTypeAtLocationParams { snapshot: number; - project: ProjectId; + project: number; location: string; } export interface GetTypeAtLocationsParams { snapshot: number; - project: ProjectId; + project: number; locations: readonly string[] | null; } export interface GetTypeAtPositionParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; position: number; } export interface GetTypesAtPositionsParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; positions: readonly number[] | null; } @@ -756,27 +758,27 @@ export interface GetSymbolPropertyParams { /** GetTypePropertyParams is used for all type sub-property endpoints. */ export interface GetTypePropertyParams { snapshot: number; - project: ProjectId; + project: number; objectId: number; } /** GetSignaturePropertyParams is used for all signature sub-property endpoints. */ export interface GetSignaturePropertyParams { snapshot: number; - project: ProjectId; + project: number; objectId: number; } /** GetContextualTypeParams returns the contextual type for a node. */ export interface GetContextualTypeParams { snapshot: number; - project: ProjectId; + project: number; location: string; } export interface GetContextualTypeForArgumentParams { snapshot: number; - project: ProjectId; + project: number; location: string; index: number; } @@ -784,35 +786,35 @@ export interface GetContextualTypeForArgumentParams { /** CheckerTypeParams are parameters for checker methods that operate on a type. */ export interface CheckerTypeParams { snapshot: number; - project: ProjectId; + project: number; type: number; } /** GetBaseTypeOfLiteralTypeParams returns the base type of a literal type. */ export interface GetBaseTypeOfLiteralTypeParams { snapshot: number; - project: ProjectId; + project: number; type: number; } /** GetTypeFromTypeNodeParams are the parameters for the getTypeFromTypeNode method. */ export interface GetTypeFromTypeNodeParams { snapshot: number; - project: ProjectId; + project: number; location: string; } /** GetWidenedTypeParams are the parameters for the getWidenedType method. */ export interface GetWidenedTypeParams { snapshot: number; - project: ProjectId; + project: number; type: number; } /** GetParameterTypeParams are the parameters for the getParameterType method. */ export interface GetParameterTypeParams { snapshot: number; - project: ProjectId; + project: number; signature: number; index: number; } @@ -820,14 +822,14 @@ export interface GetParameterTypeParams { /** IsArrayLikeTypeParams checks whether a type is array-like. */ export interface IsArrayLikeTypeParams { snapshot: number; - project: ProjectId; + project: number; type: number; } /** IsTypeAssignableToParams checks assignability between two types. */ export interface IsTypeAssignableToParams { snapshot: number; - project: ProjectId; + project: number; source: number; target: number; } @@ -835,7 +837,7 @@ export interface IsTypeAssignableToParams { /** GetTypeOfSymbolAtLocationParams returns the narrowed type of a symbol at a specific location. */ export interface GetTypeOfSymbolAtLocationParams { snapshot: number; - project: ProjectId; + project: number; symbol: SymbolReference; location: string; } @@ -843,7 +845,7 @@ export interface GetTypeOfSymbolAtLocationParams { /** TypeToTypeNodeParams are the parameters for the typeToTypeNode method. */ export interface TypeToTypeNodeParams { snapshot: number; - project: ProjectId; + project: number; type: number; location?: string | undefined; flags?: number | undefined; @@ -852,7 +854,7 @@ export interface TypeToTypeNodeParams { /** SignatureToSignatureDeclarationParams are the parameters for the signatureToSignatureDeclaration method. */ export interface SignatureToSignatureDeclarationParams { snapshot: number; - project: ProjectId; + project: number; signature: number; kind: number; location?: string | undefined; @@ -862,7 +864,7 @@ export interface SignatureToSignatureDeclarationParams { /** CheckerSignatureParams are parameters for checker methods that operate on a signature. */ export interface CheckerSignatureParams { snapshot: number; - project: ProjectId; + project: number; signature: number; } @@ -877,14 +879,14 @@ export interface TypePredicateResponse { /** GetPropertyOfTypeParams are parameters for getPropertyOfType (a named property of a type). */ export interface GetPropertyOfTypeParams { snapshot: number; - project: ProjectId; + project: number; type: number; name: string; } export interface GetIndexInfoOfTypeParams { snapshot: number; - project: ProjectId; + project: number; type: number; kind: number; } @@ -899,7 +901,7 @@ export interface IndexInfoResponse { export interface GetImportAdderEditsParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; actions: readonly ImportAdderAction[] | null; } @@ -913,7 +915,7 @@ export interface TextEdit { /** CheckerNodeParams are parameters for checker methods that operate on a node location. */ export interface CheckerNodeParams { snapshot: number; - project: ProjectId; + project: number; location: string; } @@ -925,14 +927,14 @@ export interface ConstantValueResponse { /** CheckerSymbolParams are parameters for checker methods that operate on a symbol. */ export interface CheckerSymbolParams { snapshot: number; - project: ProjectId; + project: number; symbol: SymbolReference; } /** GetMemberInModuleExportsParams are parameters for getMemberInModuleExports. */ export interface GetMemberInModuleExportsParams { snapshot: number; - project: ProjectId; + project: number; symbol: SymbolReference; name: string; } @@ -949,7 +951,7 @@ export interface JSDocTagInfo { /** GetReferencesToSymbolInFileParams are the parameters for the getReferencesToSymbolInFile method. */ export interface GetReferencesToSymbolInFileParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; symbol: SymbolReference; } @@ -957,7 +959,7 @@ export interface GetReferencesToSymbolInFileParams { /** GetReferencedSymbolsForNodeParams are the parameters for the getReferencedSymbolsForNode method. */ export interface GetReferencedSymbolsForNodeParams { snapshot: number; - project: ProjectId; + project: number; node: string; position: number; } @@ -972,7 +974,7 @@ export interface ReferencedSymbolEntry { /** GetSignatureUsagesParams are the parameters for the getSignatureUsages method. */ export interface GetSignatureUsagesParams { snapshot: number; - project: ProjectId; + project: number; signatureDecl: string; } @@ -985,7 +987,7 @@ export interface SignatureUsageResponse { /** GetCompletionsAtPositionParams are the parameters for the getCompletionsAtPosition method. */ export interface GetCompletionsAtPositionParams { snapshot: number; - project: ProjectId; + project: number; file: DocumentIdentifier; position: number; triggerCharacter?: string | undefined; @@ -1001,7 +1003,7 @@ export interface CompletionInfoResponse { /** FormatNodeForInsertionParams are the parameters for the formatNodeForInsertion method. */ export interface FormatNodeForInsertionParams { snapshot: number; - project: ProjectId; + project: number; /** target file where the node will be inserted */ file: DocumentIdentifier; /** UTF-16 code-unit offset of the insertion position in the target file */ @@ -1013,7 +1015,7 @@ export interface FormatNodeForInsertionParams { /** GetDiagnosticsParams are parameters for per-file diagnostic methods. */ export interface GetDiagnosticsParams { snapshot: number; - project: ProjectId; + project: number; files?: readonly DocumentIdentifier[] | undefined; } @@ -1060,7 +1062,7 @@ export interface PrintNodeParams { export interface EmitParams { snapshot: number; - project: ProjectId; + project: number; emitOnly?: number | undefined; } @@ -1083,14 +1085,14 @@ export interface EmitOutputResponse { export interface SelectedFilesEmitParams { snapshot: number; - project: ProjectId; + project: number; files: readonly DocumentIdentifier[] | null; } /** GetIntrinsicTypeParams is used for intrinsic type getters (anyType, stringType, etc.). */ export interface GetIntrinsicTypeParams { snapshot: number; - project: ProjectId; + project: number; } /** @@ -1706,7 +1708,7 @@ export interface SymbolOwner { kind: SymbolOwnerKind; file?: SourceFileDescriptor | undefined; snapshot?: number | undefined; - project?: ProjectId | undefined; + project?: number | undefined; } export interface CreateSnapshotProgramParams { diff --git a/packages/typescript/src/api/proto.ts b/packages/typescript/src/api/proto.ts index 1cf9a8a7ab83b..57536ef418a4e 100644 --- a/packages/typescript/src/api/proto.ts +++ b/packages/typescript/src/api/proto.ts @@ -7,7 +7,6 @@ import type { APIMethodInfo, CreateSnapshotParams as CoreCreateSnapshotParams, DocumentIdentifier, - ProjectId, SignatureResponse, SourceFileDescriptor, SourceFileResponse, @@ -32,7 +31,7 @@ export interface SnapshotSymbolOwner { readonly kind: typeof SymbolOwnerKind.Snapshot; readonly file?: never; readonly snapshot: number; - readonly project: ProjectId; + readonly project: number; } export type SymbolOwner = FileSymbolOwner | SnapshotSymbolOwner; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index a2960224381ff..c938bb7a80919 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -64,6 +64,7 @@ import { import { decodeNode, getNodeId, + getNodeRequestHandle, parseNodeHandleFromCompiler, readParseOptionsKey, readSourceFileHash, @@ -1959,15 +1960,17 @@ export class ModuleResolver { class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); - private readonly projectRegistries: Map = new Map(); + private readonly nodeHandles: Map = new Map(); + private readonly projectRegistries: Map = new Map(); private readonly snapshotId: number; + private disposed = false; constructor(snapshotId: number) { this.snapshotId = snapshotId; } addProjectRegistry(registry: ProjectObjectRegistry): void { - this.projectRegistries.set(registry.project.id, registry); + this.projectRegistries.set(registry.project.handle, registry); } getOrCreateSymbol(data: SymbolResponse): Symbol { @@ -1990,8 +1993,19 @@ class SnapshotObjectRegistry { return this.symbols.get(id); } + getOrCreateNodeHandle(handle: string, project: Project): NodeHandle { + let node = this.nodeHandles.get(handle); + if (!node) { + node = new NodeHandle(handle, project); + if (!this.disposed) this.nodeHandles.set(handle, node); + } + return node as NodeHandle; + } + clear(): void { + this.disposed = true; this.symbols.clear(); + this.nodeHandles.clear(); } } @@ -2061,7 +2075,7 @@ class ProjectObjectRegistry { } createNodeHandle(handle: string): NodeHandle { - return new NodeHandle(handle, this.project); + return this.snapshotRegistry.getOrCreateNodeHandle(handle, this.project); } getOrCreateSignature(data: SignatureResponse): Signature { @@ -2104,7 +2118,7 @@ class ProjectObjectRegistry { const data = owner.client.apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); if (!data) return undefined; @@ -2119,7 +2133,7 @@ class ProjectObjectRegistry { const data = yield* apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); if (!data) return undefined; @@ -2165,7 +2179,7 @@ class ProjectObjectRegistry { ? owner.client.apiRequest(method, { symbol: source.reference }) : owner.client.apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); if (!data) throw new Error(`${method} returned null symbol for ${source.constructor.name} ${source.id}`); @@ -2179,7 +2193,7 @@ class ProjectObjectRegistry { ? yield* apiRequest(method, { symbol: source.reference }) : yield* apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); if (!data) throw new Error(`${method} returned null symbol for ${source.constructor.name} ${source.id}`); @@ -2203,7 +2217,7 @@ class ProjectObjectRegistry { const data = owner.client.apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); if (!data) throw new Error(`${method} returned null signature for ${source.constructor.name} ${source.id}`); @@ -2216,7 +2230,7 @@ class ProjectObjectRegistry { const data = yield* apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); if (!data) throw new Error(`${method} returned null signature for ${source.constructor.name} ${source.id}`); @@ -2249,7 +2263,7 @@ class ProjectObjectRegistry { } const typesData = owner.client.apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); if (typesData == null) return []; @@ -2271,7 +2285,7 @@ class ProjectObjectRegistry { } const typesData = yield* apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); if (typesData == null) return []; @@ -2332,7 +2346,7 @@ class ProjectObjectRegistry { ? owner.client.apiRequest(method, { symbol: source.reference }) : owner.client.apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); return data?.map(symbol => owner.getOrCreateSymbol(symbol)) ?? []; @@ -2342,7 +2356,7 @@ class ProjectObjectRegistry { ? yield* apiRequest(method, { symbol: source.reference }) : yield* apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); return data?.map(symbol => owner.getOrCreateSymbol(symbol)) ?? []; @@ -2363,7 +2377,7 @@ class ProjectObjectRegistry { function (source: Type): readonly Type[] { const typesData = owner.client.apiRequest("getBaseTypes", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: source.id, }); if (typesData == null) return []; @@ -2372,7 +2386,7 @@ class ProjectObjectRegistry { function* (source: Type): Generator { const typesData = yield* apiRequest("getBaseTypes", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: source.id, }); if (typesData == null) return []; @@ -2392,7 +2406,7 @@ class ProjectObjectRegistry { function (source: Type): readonly Symbol[] { const data = owner.client.apiRequest("getPropertiesOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: source.id, }); return data ? data.map(symbol => owner.getOrCreateSymbol(symbol)) : []; @@ -2400,7 +2414,7 @@ class ProjectObjectRegistry { function* (source: Type): Generator { const data = yield* apiRequest("getPropertiesOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: source.id, }); return data ? data.map(symbol => owner.getOrCreateSymbol(symbol)) : []; @@ -2419,7 +2433,7 @@ class ProjectObjectRegistry { function (source: Type): readonly Symbol[] { const data = owner.client.apiRequest("getApparentPropertiesOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); return data ? data.map(symbol => owner.getOrCreateSymbol(symbol)) : []; @@ -2427,7 +2441,7 @@ class ProjectObjectRegistry { function* (source: Type): Generator { const data = yield* apiRequest("getApparentPropertiesOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, objectId: source.id, }); return data ? data.map(symbol => owner.getOrCreateSymbol(symbol)) : []; @@ -2446,7 +2460,7 @@ class ProjectObjectRegistry { function (source: Type, name: string): Symbol | undefined { const data = owner.client.apiRequest("getPropertyOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: source.id, name, }); @@ -2455,7 +2469,7 @@ class ProjectObjectRegistry { function* (source: Type, name: string): Generator { const data = yield* apiRequest("getPropertyOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: source.id, name, }); @@ -2475,7 +2489,7 @@ class ProjectObjectRegistry { function (source: Type, kind: SignatureKind): readonly Signature[] { const data = owner.client.apiRequest("getSignaturesOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: source.id, kind, }); @@ -2484,7 +2498,7 @@ class ProjectObjectRegistry { function* (source: Type, kind: SignatureKind): Generator { const data = yield* apiRequest("getSignaturesOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: source.id, kind, }); @@ -2504,7 +2518,7 @@ class ProjectObjectRegistry { function (source: Type): readonly IndexInfo[] { const data = owner.client.apiRequest("getIndexInfosOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: source.id, }); if (!data) return []; @@ -2512,13 +2526,13 @@ class ProjectObjectRegistry { keyType: owner.getOrCreateType(info.keyType), valueType: owner.getOrCreateType(info.valueType), isReadonly: info.isReadonly ?? false, - declaration: info.declaration ? new NodeHandle(info.declaration, owner.project) : undefined, + declaration: info.declaration ? owner.createNodeHandle(info.declaration) : undefined, })); }, function* (source: Type): Generator { const data = yield* apiRequest("getIndexInfosOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: source.id, }); if (!data) return []; @@ -2526,7 +2540,7 @@ class ProjectObjectRegistry { keyType: owner.getOrCreateType(info.keyType), valueType: owner.getOrCreateType(info.valueType), isReadonly: info.isReadonly ?? false, - declaration: info.declaration ? new NodeHandle(info.declaration, owner.project) : undefined, + declaration: info.declaration ? owner.createNodeHandle(info.declaration) : undefined, })); }, ); @@ -2543,7 +2557,7 @@ class ProjectObjectRegistry { function (source: Signature, pos: number): Type { const data = owner.client.apiRequest("getTypeParameterAtPosition", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, signature: source.id, index: pos, }); @@ -2552,7 +2566,7 @@ class ProjectObjectRegistry { function* (source: Signature, pos: number): Generator { const data = yield* apiRequest("getTypeParameterAtPosition", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, signature: source.id, index: pos, }); @@ -2566,6 +2580,11 @@ export class Project { /** @internal */ readonly api: API; readonly id: Id; + /** + * Server identity for this project's lifetime across snapshots. + * @internal + */ + readonly handle: number; readonly configFileName: RootedFilePath | undefined; readonly currentDirectory: RootedDirectoryPath; readonly dirty: boolean; @@ -2577,6 +2596,7 @@ export class Project { constructor(data: ProjectResponse, snapshotId: number, toPath: (fileName: string, basePath?: string) => PathKey, api: API, snapshotRegistry: SnapshotObjectRegistry) { this.id = data.id as Id; + this.handle = data.handle; this.api = api; this.configFileName = data.configFileName; this.currentDirectory = data.currentDirectory; @@ -2638,7 +2658,7 @@ export class LanguageService { const data = owner.client.apiRequest("getImportAdderEdits", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, actions: requestActions, }); @@ -2663,7 +2683,7 @@ export class LanguageService { const data = yield* apiRequest("getImportAdderEdits", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, actions: requestActions, }); @@ -2730,27 +2750,27 @@ export class LanguageService { function (node: Node, position: number): ReferencedSymbolEntry[] { const data = owner.client.apiRequest("getReferencedSymbolsForNode", { snapshot: owner.snapshotId, - project: owner.project.id, - node: getNodeId(node), + project: owner.project.handle, + node: getNodeRequestHandle(node), position, }); return (data ?? []).map(entry => ({ - definition: new NodeHandle(entry.definition, owner.project), + definition: owner.objectRegistry.createNodeHandle(entry.definition), symbol: entry.symbol ? owner.objectRegistry.getOrCreateSymbol(entry.symbol) : undefined, - references: (entry.references ?? []).map(h => new NodeHandle(h, owner.project)), + references: (entry.references ?? []).map(h => owner.objectRegistry.createNodeHandle(h)), })); }, function* (node: Node, position: number): Generator { const data = yield* apiRequest("getReferencedSymbolsForNode", { snapshot: owner.snapshotId, - project: owner.project.id, - node: getNodeId(node), + project: owner.project.handle, + node: getNodeRequestHandle(node), position, }); return (data ?? []).map(entry => ({ - definition: new NodeHandle(entry.definition, owner.project), + definition: owner.objectRegistry.createNodeHandle(entry.definition), symbol: entry.symbol ? owner.objectRegistry.getOrCreateSymbol(entry.symbol) : undefined, - references: (entry.references ?? []).map(h => new NodeHandle(h, owner.project)), + references: (entry.references ?? []).map(h => owner.objectRegistry.createNodeHandle(h)), })); }, ); @@ -2767,23 +2787,23 @@ export class LanguageService { function (signatureDecl: Node): SignatureUsage[] { const data = owner.client.apiRequest("getSignatureUsages", { snapshot: owner.snapshotId, - project: owner.project.id, - signatureDecl: getNodeId(signatureDecl), + project: owner.project.handle, + signatureDecl: getNodeRequestHandle(signatureDecl), }); return (data ?? []).map(entry => ({ - name: new NodeHandle(entry.name, owner.project), - call: entry.call ? new NodeHandle(entry.call, owner.project) : undefined, + name: owner.objectRegistry.createNodeHandle(entry.name), + call: entry.call ? owner.objectRegistry.createNodeHandle(entry.call) : undefined, })); }, function* (signatureDecl: Node): Generator { const data = yield* apiRequest("getSignatureUsages", { snapshot: owner.snapshotId, - project: owner.project.id, - signatureDecl: getNodeId(signatureDecl), + project: owner.project.handle, + signatureDecl: getNodeRequestHandle(signatureDecl), }); return (data ?? []).map(entry => ({ - name: new NodeHandle(entry.name, owner.project), - call: entry.call ? new NodeHandle(entry.call, owner.project) : undefined, + name: owner.objectRegistry.createNodeHandle(entry.name), + call: entry.call ? owner.objectRegistry.createNodeHandle(entry.call) : undefined, })); }, ); @@ -2800,7 +2820,7 @@ export class LanguageService { function (document: string, position: number, options?: CompletionOptions): CompletionInfo | undefined { const data = owner.client.apiRequest("getCompletionsAtPosition", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file: document, position, triggerCharacter: options?.triggerCharacter, @@ -2818,7 +2838,7 @@ export class LanguageService { function* (document: string, position: number, options?: CompletionOptions): Generator { const data = yield* apiRequest("getCompletionsAtPosition", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file: document, position, triggerCharacter: options?.triggerCharacter, @@ -2858,7 +2878,7 @@ export class LanguageService { const base64 = uint8ArrayToBase64(encoded); return owner.client.apiRequest("formatNodeForInsertion", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, position, data: base64, @@ -2869,7 +2889,7 @@ export class LanguageService { const base64 = uint8ArrayToBase64(encoded); return yield* apiRequest("formatNodeForInsertion", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, position, data: base64, @@ -3031,7 +3051,7 @@ export class Program implements FormatDiagnost // Fetch from server const binaryData = owner.client.apiRequestBinary("getSourceFile", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, }); if (!binaryData) { @@ -3053,7 +3073,7 @@ export class Program implements FormatDiagnost const binaryData = sourceFileResponseToUint8Array( yield* apiRequest("getSourceFile", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, }), ); @@ -3079,7 +3099,7 @@ export class Program implements FormatDiagnost function (file: DocumentIdentifier, moduleName: string, mode: ModuleKind): ResolvedModule | undefined { const result = owner.client.apiRequest("getResolvedModule", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, moduleName, mode, @@ -3089,7 +3109,7 @@ export class Program implements FormatDiagnost function* (file: DocumentIdentifier, moduleName: string, mode: ModuleKind): Generator { const result = yield* apiRequest("getResolvedModule", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, moduleName, mode, @@ -3110,17 +3130,17 @@ export class Program implements FormatDiagnost function (file: DocumentIdentifier, usage: StringLiteralLikeNode): ModuleKind { return owner.client.apiRequest("getModeForUsageLocation", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, - usage: getNodeId(usage), + usage: getNodeRequestHandle(usage), }); }, function* (file: DocumentIdentifier, usage: StringLiteralLikeNode): Generator { return yield* apiRequest("getModeForUsageLocation", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, - usage: getNodeId(usage), + usage: getNodeRequestHandle(usage), }); }, ); @@ -3137,7 +3157,7 @@ export class Program implements FormatDiagnost function (file: DocumentIdentifier, index: number): ModuleKind { return owner.client.apiRequest("getModeForResolutionAtIndex", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, index, }); @@ -3145,7 +3165,7 @@ export class Program implements FormatDiagnost function* (file: DocumentIdentifier, index: number): Generator { return yield* apiRequest("getModeForResolutionAtIndex", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, index, }); @@ -3164,8 +3184,8 @@ export class Program implements FormatDiagnost function (moduleSpecifier: StringLiteralLikeNode, sourceFile?: DocumentIdentifier): ResolvedModule | undefined { const result = owner.client.apiRequest("getResolvedModuleFromModuleSpecifier", { snapshot: owner.snapshotId, - project: owner.project.id, - moduleSpecifier: getNodeId(moduleSpecifier), + project: owner.project.handle, + moduleSpecifier: getNodeRequestHandle(moduleSpecifier), sourceFile, }); return result ?? undefined; @@ -3173,8 +3193,8 @@ export class Program implements FormatDiagnost function* (moduleSpecifier: StringLiteralLikeNode, sourceFile?: DocumentIdentifier): Generator { const result = yield* apiRequest("getResolvedModuleFromModuleSpecifier", { snapshot: owner.snapshotId, - project: owner.project.id, - moduleSpecifier: getNodeId(moduleSpecifier), + project: owner.project.handle, + moduleSpecifier: getNodeRequestHandle(moduleSpecifier), sourceFile, }); return result ?? undefined; @@ -3193,7 +3213,7 @@ export class Program implements FormatDiagnost function (file: DocumentIdentifier, typeDirectiveName: string, mode: ModuleKind): ResolvedTypeReferenceDirective | undefined { const result = owner.client.apiRequest("getResolvedTypeReferenceDirective", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, typeDirectiveName, mode, @@ -3203,7 +3223,7 @@ export class Program implements FormatDiagnost function* (file: DocumentIdentifier, typeDirectiveName: string, mode: ModuleKind): Generator { const result = yield* apiRequest("getResolvedTypeReferenceDirective", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, typeDirectiveName, mode, @@ -3224,7 +3244,7 @@ export class Program implements FormatDiagnost function (typeReferenceDirective: FileReference, sourceFile: DocumentIdentifier): ResolvedTypeReferenceDirective | undefined { const result = owner.client.apiRequest("getResolvedTypeReferenceDirectiveFromTypeReferenceDirective", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, sourceFile, typeDirectiveName: typeReferenceDirective.fileName, resolutionMode: typeReferenceDirective.resolutionMode, @@ -3234,7 +3254,7 @@ export class Program implements FormatDiagnost function* (typeReferenceDirective: FileReference, sourceFile: DocumentIdentifier): Generator { const result = yield* apiRequest("getResolvedTypeReferenceDirectiveFromTypeReferenceDirective", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, sourceFile, typeDirectiveName: typeReferenceDirective.fileName, resolutionMode: typeReferenceDirective.resolutionMode, @@ -3255,14 +3275,14 @@ export class Program implements FormatDiagnost function (): readonly RootedFilePath[] { const data = owner.client.apiRequest("getSourceFileNames", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return data ?? []; }, function* (): Generator { const data = yield* apiRequest("getSourceFileNames", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return data ?? []; }, @@ -3337,7 +3357,7 @@ export class Program implements FormatDiagnost // client-provided path text as untrusted input. const data = owner.client.apiRequest("getSourceFileMetadata", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file: path, }); return data ?? undefined; @@ -3347,7 +3367,7 @@ export class Program implements FormatDiagnost // client-provided path text as untrusted input. const data = yield* apiRequest("getSourceFileMetadata", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file: path, }); return data ?? undefined; @@ -3422,14 +3442,14 @@ export class Program implements FormatDiagnost function (): readonly RootedFilePath[] { const data = owner.client.apiRequest("getConfigFileNames", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return data ?? []; }, function* (): Generator { const data = yield* apiRequest("getConfigFileNames", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return data ?? []; }, @@ -3451,7 +3471,7 @@ export class Program implements FormatDiagnost function (file: DocumentIdentifier): SourceFile | undefined { const binaryData = owner.client.apiRequestBinary("getConfigSourceFile", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, }); if (!binaryData) { @@ -3464,7 +3484,7 @@ export class Program implements FormatDiagnost const binaryData = sourceFileResponseToUint8Array( yield* apiRequest("getConfigSourceFile", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, }), ); @@ -3495,7 +3515,7 @@ export class Program implements FormatDiagnost : [file]; const data = owner.client.apiRequest("getSyntacticDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return data ?? []; @@ -3506,7 +3526,7 @@ export class Program implements FormatDiagnost : [file]; const data = yield* apiRequest("getSyntacticDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return data ?? []; @@ -3532,7 +3552,7 @@ export class Program implements FormatDiagnost : [file]; const data = owner.client.apiRequest("getBindDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return data ?? []; @@ -3543,7 +3563,7 @@ export class Program implements FormatDiagnost : [file]; const data = yield* apiRequest("getBindDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return data ?? []; @@ -3569,7 +3589,7 @@ export class Program implements FormatDiagnost : [file]; const data = owner.client.apiRequest("getSemanticDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return data ?? []; @@ -3580,7 +3600,7 @@ export class Program implements FormatDiagnost : [file]; const data = yield* apiRequest("getSemanticDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return data ?? []; @@ -3606,7 +3626,7 @@ export class Program implements FormatDiagnost : [file]; const data = owner.client.apiRequest("getSuggestionDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return data ?? []; @@ -3617,7 +3637,7 @@ export class Program implements FormatDiagnost : [file]; const data = yield* apiRequest("getSuggestionDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return data ?? []; @@ -3643,7 +3663,7 @@ export class Program implements FormatDiagnost : [file]; const data = owner.client.apiRequest("getDeclarationDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return data ?? []; @@ -3654,7 +3674,7 @@ export class Program implements FormatDiagnost : [file]; const data = yield* apiRequest("getDeclarationDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return data ?? []; @@ -3676,14 +3696,14 @@ export class Program implements FormatDiagnost function (): readonly Diagnostic[] { const data = owner.client.apiRequest("getProgramDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return data ?? []; }, function* (): Generator { const data = yield* apiRequest("getProgramDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return data ?? []; }, @@ -3704,14 +3724,14 @@ export class Program implements FormatDiagnost function (): readonly Diagnostic[] { const data = owner.client.apiRequest("getGlobalDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return data ?? []; }, function* (): Generator { const data = yield* apiRequest("getGlobalDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return data ?? []; }, @@ -3732,14 +3752,14 @@ export class Program implements FormatDiagnost function (): readonly Diagnostic[] { const data = owner.client.apiRequest("getConfigFileParsingDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return data ?? []; }, function* (): Generator { const data = yield* apiRequest("getConfigFileParsingDiagnostics", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return data ?? []; }, @@ -3762,7 +3782,7 @@ export class Program implements FormatDiagnost function (emitOnly?: EmitOnly): EmitResult { const response = owner.client.apiRequest("emit", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, emitOnly, }); const fileSystem = response.emittedFilesContents.length @@ -3781,7 +3801,7 @@ export class Program implements FormatDiagnost function* (emitOnly?: EmitOnly): Generator { const response = yield* apiRequest("emit", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, emitOnly, }); const fileSystem = response.emittedFilesContents.length @@ -3814,7 +3834,7 @@ export class Program implements FormatDiagnost function (emitOnly?: EmitOnly): EmitOutput { const response = owner.client.apiRequest("emitToString", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, emitOnly, }); return toEmitOutput(response); @@ -3822,7 +3842,7 @@ export class Program implements FormatDiagnost function* (emitOnly?: EmitOnly): Generator { const response = yield* apiRequest("emitToString", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, emitOnly, }); return toEmitOutput(response); @@ -3844,7 +3864,7 @@ export class Program implements FormatDiagnost function (files: readonly DocumentIdentifier[]): EmitOutput { const response = owner.client.apiRequest("getJavaScriptEmit", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return toEmitOutput(response); @@ -3852,7 +3872,7 @@ export class Program implements FormatDiagnost function* (files: readonly DocumentIdentifier[]): Generator { const response = yield* apiRequest("getJavaScriptEmit", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return toEmitOutput(response); @@ -3874,7 +3894,7 @@ export class Program implements FormatDiagnost function (files: readonly DocumentIdentifier[]): EmitOutput { const response = owner.client.apiRequest("getDeclarationEmit", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return toEmitOutput(response); @@ -3882,7 +3902,7 @@ export class Program implements FormatDiagnost function* (files: readonly DocumentIdentifier[]): Generator { const response = yield* apiRequest("getDeclarationEmit", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files, }); return toEmitOutput(response); @@ -4148,7 +4168,7 @@ export class Checker { function (symbol: Symbol): Symbol { const data = owner.client.apiRequest("getMergedSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateSymbol(data); @@ -4156,7 +4176,7 @@ export class Checker { function* (symbol: Symbol): Generator { const data = yield* apiRequest("getMergedSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateSymbol(data); @@ -4179,16 +4199,16 @@ export class Checker { function (node: Node): Symbol | undefined { const data = owner.client.apiRequest("getSymbolOfNode", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; }, function* (node: Node): Generator { const data = yield* apiRequest("getSymbolOfNode", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; }, @@ -4207,8 +4227,8 @@ export class Checker { function (node: Declaration): Symbol { const data = owner.client.apiRequest("getSymbolOfDeclarationForChecker", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); // Declarations normally have symbols, but preserve unexpected null results at runtime. return (data && owner.objectRegistry.getOrCreateSymbol(data))!; @@ -4216,8 +4236,8 @@ export class Checker { function* (node: Declaration): Generator { const data = yield* apiRequest("getSymbolOfDeclarationForChecker", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); // Declarations normally have symbols, but preserve unexpected null results at runtime. return (data && owner.objectRegistry.getOrCreateSymbol(data))!; @@ -4237,7 +4257,7 @@ export class Checker { function (symbol: Symbol): Symbol | undefined { const data = owner.client.apiRequest("getParentOfSymbolForChecker", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; @@ -4245,7 +4265,7 @@ export class Checker { function* (symbol: Symbol): Generator { const data = yield* apiRequest("getParentOfSymbolForChecker", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; @@ -4266,15 +4286,15 @@ export class Checker { if (Array.isArray(nodeOrNodes)) { const data = owner.client.apiRequest("getSymbolsAtLocations", { snapshot: owner.snapshotId, - project: owner.project.id, - locations: nodeOrNodes.map(node => getNodeId(node)), + project: owner.project.handle, + locations: nodeOrNodes.map(node => getNodeRequestHandle(node)), }); return data.map(d => d ? owner.objectRegistry.getOrCreateSymbol(d) : undefined); } const data = owner.client.apiRequest("getSymbolAtLocation", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(nodeOrNodes as Node), + project: owner.project.handle, + location: getNodeRequestHandle(nodeOrNodes as Node), }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; } @@ -4284,15 +4304,15 @@ export class Checker { if (Array.isArray(nodeOrNodes)) { const data = yield* apiRequest("getSymbolsAtLocations", { snapshot: owner.snapshotId, - project: owner.project.id, - locations: nodeOrNodes.map(node => getNodeId(node)), + project: owner.project.handle, + locations: nodeOrNodes.map(node => getNodeRequestHandle(node)), }); return data.map(d => d ? owner.objectRegistry.getOrCreateSymbol(d) : undefined); } const data = yield* apiRequest("getSymbolAtLocation", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(nodeOrNodes as Node), + project: owner.project.handle, + location: getNodeRequestHandle(nodeOrNodes as Node), }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; } @@ -4312,7 +4332,7 @@ export class Checker { if (typeof positionOrPositions === "number") { const data = owner.client.apiRequest("getSymbolAtPosition", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, position: positionOrPositions, }); @@ -4320,7 +4340,7 @@ export class Checker { } const data = owner.client.apiRequest("getSymbolsAtPositions", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, positions: positionOrPositions, }); @@ -4332,7 +4352,7 @@ export class Checker { if (typeof positionOrPositions === "number") { const data = yield* apiRequest("getSymbolAtPosition", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, position: positionOrPositions, }); @@ -4340,7 +4360,7 @@ export class Checker { } const data = yield* apiRequest("getSymbolsAtPositions", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, positions: positionOrPositions, }); @@ -4362,14 +4382,14 @@ export class Checker { if (Array.isArray(fileOrFiles)) { const data = owner.client.apiRequest("getSymbolsOfSourceFiles", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files: fileOrFiles, }); return data.map(d => d ? owner.objectRegistry.getOrCreateSymbol(d) : undefined); } const data = owner.client.apiRequest("getSymbolOfSourceFile", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file: fileOrFiles as DocumentIdentifier, }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; @@ -4380,14 +4400,14 @@ export class Checker { if (Array.isArray(fileOrFiles)) { const data = yield* apiRequest("getSymbolsOfSourceFiles", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, files: fileOrFiles, }); return data.map(d => d ? owner.objectRegistry.getOrCreateSymbol(d) : undefined); } const data = yield* apiRequest("getSymbolOfSourceFile", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file: fileOrFiles as DocumentIdentifier, }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; @@ -4413,14 +4433,14 @@ export class Checker { if (Array.isArray(symbolOrSymbols)) { const data = owner.client.apiRequest("getTypesOfSymbols", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbols: symbolOrSymbols.map(symbol => symbol.reference), }); return data.map(d => owner.objectRegistry.getOrCreateType(d)); } const data = owner.client.apiRequest("getTypeOfSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: (symbolOrSymbols as Symbol).reference, }); return owner.objectRegistry.getOrCreateType(data); @@ -4431,14 +4451,14 @@ export class Checker { if (Array.isArray(symbolOrSymbols)) { const data = yield* apiRequest("getTypesOfSymbols", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbols: symbolOrSymbols.map(symbol => symbol.reference), }); return data.map(d => owner.objectRegistry.getOrCreateType(d)); } const data = yield* apiRequest("getTypeOfSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: (symbolOrSymbols as Symbol).reference, }); return owner.objectRegistry.getOrCreateType(data); @@ -4462,7 +4482,7 @@ export class Checker { function (symbol: Symbol): Type { const data = owner.client.apiRequest("getDeclaredTypeOfSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateType(data); @@ -4470,7 +4490,7 @@ export class Checker { function* (symbol: Symbol): Generator { const data = yield* apiRequest("getDeclaredTypeOfSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateType(data); @@ -4495,7 +4515,7 @@ export class Checker { function (symbol: Symbol): Type { const data = owner.client.apiRequest("getNonMissingTypeOfSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateType(data); @@ -4503,7 +4523,7 @@ export class Checker { function* (symbol: Symbol): Generator { const data = yield* apiRequest("getNonMissingTypeOfSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateType(data); @@ -4522,20 +4542,20 @@ export class Checker { function (file: DocumentIdentifier, symbol: Symbol): NodeHandle[] { const data = owner.client.apiRequest("getReferencesToSymbolInFile", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, symbol: symbol.reference, }); - return (data ?? []).map(h => new NodeHandle(h, owner.project)); + return (data ?? []).map(h => owner.objectRegistry.createNodeHandle(h)); }, function* (file: DocumentIdentifier, symbol: Symbol): Generator { const data = yield* apiRequest("getReferencesToSymbolInFile", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, symbol: symbol.reference, }); - return (data ?? []).map(h => new NodeHandle(h, owner.project)); + return (data ?? []).map(h => owner.objectRegistry.createNodeHandle(h)); }, ); } @@ -4558,15 +4578,15 @@ export class Checker { if (Array.isArray(nodeOrNodes)) { const data = owner.client.apiRequest("getTypeAtLocations", { snapshot: owner.snapshotId, - project: owner.project.id, - locations: nodeOrNodes.map(node => getNodeId(node)), + project: owner.project.handle, + locations: nodeOrNodes.map(node => getNodeRequestHandle(node)), }); return data.map(d => owner.objectRegistry.getOrCreateType(d)); } const data = owner.client.apiRequest("getTypeAtLocation", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(nodeOrNodes as Node), + project: owner.project.handle, + location: getNodeRequestHandle(nodeOrNodes as Node), }); return owner.objectRegistry.getOrCreateType(data); } @@ -4576,15 +4596,15 @@ export class Checker { if (Array.isArray(nodeOrNodes)) { const data = yield* apiRequest("getTypeAtLocations", { snapshot: owner.snapshotId, - project: owner.project.id, - locations: nodeOrNodes.map(node => getNodeId(node)), + project: owner.project.handle, + locations: nodeOrNodes.map(node => getNodeRequestHandle(node)), }); return data.map(d => owner.objectRegistry.getOrCreateType(d)); } const data = yield* apiRequest("getTypeAtLocation", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(nodeOrNodes as Node), + project: owner.project.handle, + location: getNodeRequestHandle(nodeOrNodes as Node), }); return owner.objectRegistry.getOrCreateType(data); } @@ -4624,16 +4644,16 @@ export class Checker { function (node: Node): Signature { const data = owner.client.apiRequest("getResolvedSignature", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return owner.objectRegistry.getOrCreateSignature(data); }, function* (node: Node): Generator { const data = yield* apiRequest("getResolvedSignature", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return owner.objectRegistry.getOrCreateSignature(data); }, @@ -4653,7 +4673,7 @@ export class Checker { if (typeof positionOrPositions === "number") { const data = owner.client.apiRequest("getTypeAtPosition", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, position: positionOrPositions, }); @@ -4661,7 +4681,7 @@ export class Checker { } const data = owner.client.apiRequest("getTypesAtPositions", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, positions: positionOrPositions, }); @@ -4673,7 +4693,7 @@ export class Checker { if (typeof positionOrPositions === "number") { const data = yield* apiRequest("getTypeAtPosition", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, position: positionOrPositions, }); @@ -4681,7 +4701,7 @@ export class Checker { } const data = yield* apiRequest("getTypesAtPositions", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, positions: positionOrPositions, }); @@ -4703,10 +4723,10 @@ export class Checker { const isNode = location && "kind" in location; const data = owner.client.apiRequest("resolveName", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, name, meaning, - location: isNode ? getNodeId(location as Node) : undefined, + location: isNode ? getNodeRequestHandle(location as Node) : undefined, file: !isNode && location ? (location as DocumentPosition).document : undefined, position: !isNode && location ? (location as DocumentPosition).position : undefined, excludeGlobals, @@ -4718,10 +4738,10 @@ export class Checker { const isNode = location && "kind" in location; const data = yield* apiRequest("resolveName", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, name, meaning, - location: isNode ? getNodeId(location as Node) : undefined, + location: isNode ? getNodeRequestHandle(location as Node) : undefined, file: !isNode && location ? (location as DocumentPosition).document : undefined, position: !isNode && location ? (location as DocumentPosition).position : undefined, excludeGlobals, @@ -4747,9 +4767,9 @@ export class Checker { const isNode = "kind" in location; const data = owner.client.apiRequest("getSymbolsInScope", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, meaning, - location: isNode ? getNodeId(location as Node) : undefined, + location: isNode ? getNodeRequestHandle(location as Node) : undefined, file: isNode ? undefined : (location as DocumentPosition).document, position: isNode ? undefined : (location as DocumentPosition).position, }); @@ -4760,9 +4780,9 @@ export class Checker { const isNode = "kind" in location; const data = yield* apiRequest("getSymbolsInScope", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, meaning, - location: isNode ? getNodeId(location as Node) : undefined, + location: isNode ? getNodeRequestHandle(location as Node) : undefined, file: isNode ? undefined : (location as DocumentPosition).document, position: isNode ? undefined : (location as DocumentPosition).position, }); @@ -4803,16 +4823,16 @@ export class Checker { function (node: Expression): Type | undefined { const data = owner.client.apiRequest("getContextualType", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return data ? owner.objectRegistry.getOrCreateType(data) : undefined; }, function* (node: Expression): Generator { const data = yield* apiRequest("getContextualType", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return data ? owner.objectRegistry.getOrCreateType(data) : undefined; }, @@ -4830,8 +4850,8 @@ export class Checker { function (node: CallLikeExpression, argIndex: number): Type | undefined { const data = owner.client.apiRequest("getContextualTypeForArgument", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), index: argIndex, }); return data ? owner.objectRegistry.getOrCreateType(data) : undefined; @@ -4839,8 +4859,8 @@ export class Checker { function* (node: CallLikeExpression, argIndex: number): Generator { const data = yield* apiRequest("getContextualTypeForArgument", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), index: argIndex, }); return data ? owner.objectRegistry.getOrCreateType(data) : undefined; @@ -4859,7 +4879,7 @@ export class Checker { function (type: Type): Type | undefined { const data = owner.client.apiRequest("getAwaitedType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); return data ? owner.objectRegistry.getOrCreateType(data) : undefined; @@ -4867,7 +4887,7 @@ export class Checker { function* (type: Type): Generator { const data = yield* apiRequest("getAwaitedType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); return data ? owner.objectRegistry.getOrCreateType(data) : undefined; @@ -4887,7 +4907,7 @@ export class Checker { function (type: Type): Type { const data = owner.client.apiRequest("getBaseTypeOfLiteralType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); return owner.objectRegistry.getOrCreateType(data); @@ -4895,7 +4915,7 @@ export class Checker { function* (type: Type): Generator { const data = yield* apiRequest("getBaseTypeOfLiteralType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); return owner.objectRegistry.getOrCreateType(data); @@ -4937,16 +4957,16 @@ export class Checker { function (node: TypeNode): Type { const data = owner.client.apiRequest("getTypeFromTypeNode", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return owner.objectRegistry.getOrCreateType(data); }, function* (node: TypeNode): Generator { const data = yield* apiRequest("getTypeFromTypeNode", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return owner.objectRegistry.getOrCreateType(data); }, @@ -4965,7 +4985,7 @@ export class Checker { function (type: Type): Type { const data = owner.client.apiRequest("getWidenedType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); return owner.objectRegistry.getOrCreateType(data); @@ -4973,7 +4993,7 @@ export class Checker { function* (type: Type): Generator { const data = yield* apiRequest("getWidenedType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); return owner.objectRegistry.getOrCreateType(data); @@ -4996,7 +5016,7 @@ export class Checker { function (signature: Signature, index: number): Type { const data = owner.client.apiRequest("getParameterType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, signature: signature.id, index, }); @@ -5005,7 +5025,7 @@ export class Checker { function* (signature: Signature, index: number): Generator { const data = yield* apiRequest("getParameterType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, signature: signature.id, index, }); @@ -5025,14 +5045,14 @@ export class Checker { function (type: Type): boolean { return owner.client.apiRequest("isArrayLikeType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); }, function* (type: Type): Generator { return yield* apiRequest("isArrayLikeType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); }, @@ -5050,7 +5070,7 @@ export class Checker { function (source: Type, target: Type): boolean { return owner.client.apiRequest("isTypeAssignableTo", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, source: source.id, target: target.id, }); @@ -5058,7 +5078,7 @@ export class Checker { function* (source: Type, target: Type): Generator { return yield* apiRequest("isTypeAssignableTo", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, source: source.id, target: target.id, }); @@ -5077,16 +5097,16 @@ export class Checker { function (node: Node): Symbol | undefined { const data = owner.client.apiRequest("getShorthandAssignmentValueSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; }, function* (node: Node): Generator { const data = yield* apiRequest("getShorthandAssignmentValueSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; }, @@ -5109,18 +5129,18 @@ export class Checker { function (symbol: Symbol, location: Node): Type { const data = owner.client.apiRequest("getTypeOfSymbolAtLocation", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, - location: getNodeId(location), + location: getNodeRequestHandle(location), }); return owner.objectRegistry.getOrCreateType(data); }, function* (symbol: Symbol, location: Node): Generator { const data = yield* apiRequest("getTypeOfSymbolAtLocation", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, - location: getNodeId(location), + location: getNodeRequestHandle(location), }); return owner.objectRegistry.getOrCreateType(data); }, @@ -5138,14 +5158,14 @@ export class Checker { function (method: IntrinsicTypeMethod): Type { const data = owner.client.apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return owner.objectRegistry.getOrCreateType(data); }, function* (method: IntrinsicTypeMethod): Generator { const data = yield* apiRequest(method, { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); return owner.objectRegistry.getOrCreateType(data); }, @@ -5356,9 +5376,9 @@ export class Checker { function (type: Type, enclosingDeclaration?: Node, flags?: number): TypeNode | undefined { const binaryData = owner.client.apiRequestBinary("typeToTypeNode", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (!binaryData) return undefined; @@ -5368,9 +5388,9 @@ export class Checker { const binaryData = sourceFileResponseToUint8Array( yield* apiRequest("typeToTypeNode", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }), ); @@ -5391,10 +5411,10 @@ export class Checker { function (signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): Node | undefined { const binaryData = owner.client.apiRequestBinary("signatureToSignatureDeclaration", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, signature: signature.id, kind, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (!binaryData) return undefined; @@ -5404,10 +5424,10 @@ export class Checker { const binaryData = sourceFileResponseToUint8Array( yield* apiRequest("signatureToSignatureDeclaration", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, signature: signature.id, kind, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }), ); @@ -5428,9 +5448,9 @@ export class Checker { function (type: Type, enclosingDeclaration?: Node, flags?: TypeFormatFlags): string { const result = owner.client.apiRequest("typeToString", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (typeof result !== "string") throw new TypeError("typeToString returned a non-string result"); @@ -5439,9 +5459,9 @@ export class Checker { function* (type: Type, enclosingDeclaration?: Node, flags?: TypeFormatFlags): Generator { const result = yield* apiRequest("typeToString", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (typeof result !== "string") throw new TypeError("typeToString returned a non-string result"); @@ -5461,15 +5481,15 @@ export class Checker { function (node: Node): boolean { return owner.client.apiRequest("isContextSensitive", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); }, function* (node: Node): Generator { return yield* apiRequest("isContextSensitive", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); }, ); @@ -5486,14 +5506,14 @@ export class Checker { function (type: Type): boolean { return owner.client.apiRequest("isArrayType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); }, function* (type: Type): Generator { return yield* apiRequest("isArrayType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); }, @@ -5554,14 +5574,14 @@ export class Checker { function (symbol: Symbol): boolean { return owner.client.apiRequest("isReadonlySymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); }, function* (symbol: Symbol): Generator { return yield* apiRequest("isReadonlySymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); }, @@ -5601,7 +5621,7 @@ export class Checker { function (signature: Signature): Type { const data = owner.client.apiRequest("getRestTypeOfSignature", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, signature: signature.id, }); return owner.objectRegistry.getOrCreateType(data); @@ -5609,7 +5629,7 @@ export class Checker { function* (signature: Signature): Generator { const data = yield* apiRequest("getRestTypeOfSignature", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, signature: signature.id, }); return owner.objectRegistry.getOrCreateType(data); @@ -5628,7 +5648,7 @@ export class Checker { function (signature: Signature): TypePredicate | undefined { const data = owner.client.apiRequest("getTypePredicateOfSignature", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, signature: signature.id, }); if (!data) return undefined; @@ -5642,7 +5662,7 @@ export class Checker { function* (signature: Signature): Generator { const data = yield* apiRequest("getTypePredicateOfSignature", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, signature: signature.id, }); if (!data) return undefined; @@ -5758,7 +5778,7 @@ export class Checker { function (type: Type, kind: IndexKind): IndexInfo | undefined { const data = owner.client.apiRequest("getIndexInfoOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, kind, }); @@ -5766,13 +5786,13 @@ export class Checker { keyType: owner.objectRegistry.getOrCreateType(data.keyType), valueType: owner.objectRegistry.getOrCreateType(data.valueType), isReadonly: data.isReadonly ?? false, - declaration: data.declaration ? new NodeHandle(data.declaration, owner.project) : undefined, + declaration: data.declaration ? owner.objectRegistry.createNodeHandle(data.declaration) : undefined, } : undefined; }, function* (type: Type, kind: IndexKind): Generator { const data = yield* apiRequest("getIndexInfoOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, kind, }); @@ -5780,7 +5800,7 @@ export class Checker { keyType: owner.objectRegistry.getOrCreateType(data.keyType), valueType: owner.objectRegistry.getOrCreateType(data.valueType), isReadonly: data.isReadonly ?? false, - declaration: data.declaration ? new NodeHandle(data.declaration, owner.project) : undefined, + declaration: data.declaration ? owner.objectRegistry.createNodeHandle(data.declaration) : undefined, } : undefined; }, ); @@ -5814,7 +5834,7 @@ export class Checker { function (type: Type, propertyName: string): Type | undefined { const data = owner.client.apiRequest("getTypeOfPropertyOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, name: propertyName, }); @@ -5823,7 +5843,7 @@ export class Checker { function* (type: Type, propertyName: string): Generator { const data = yield* apiRequest("getTypeOfPropertyOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, name: propertyName, }); @@ -5881,7 +5901,7 @@ export class Checker { function (type: Type): Type | undefined { const data = owner.client.apiRequest("getBaseConstraintOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); return data ? owner.objectRegistry.getOrCreateType(data) : undefined; @@ -5889,7 +5909,7 @@ export class Checker { function* (type: Type): Generator { const data = yield* apiRequest("getBaseConstraintOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); return data ? owner.objectRegistry.getOrCreateType(data) : undefined; @@ -5908,7 +5928,7 @@ export class Checker { function (type: Type, name: string): Symbol | undefined { const data = owner.client.apiRequest("getPropertyOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, name, }); @@ -5917,7 +5937,7 @@ export class Checker { function* (type: Type, name: string): Generator { const data = yield* apiRequest("getPropertyOfType", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, name, }); @@ -5937,8 +5957,8 @@ export class Checker { function (node: Node): string | number | undefined { const data = owner.client.apiRequest("getConstantValue", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); if (!data || (typeof data.value !== "string" && typeof data.value !== "number")) { return undefined; @@ -5957,8 +5977,8 @@ export class Checker { function* (node: Node): Generator { const data = yield* apiRequest("getConstantValue", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); if (!data || (typeof data.value !== "string" && typeof data.value !== "number")) { return undefined; @@ -5989,16 +6009,16 @@ export class Checker { function (node: Node): Signature { const data = owner.client.apiRequest("getSignatureFromDeclaration", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return owner.objectRegistry.getOrCreateSignature(data); }, function* (node: Node): Generator { const data = yield* apiRequest("getSignatureFromDeclaration", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return owner.objectRegistry.getOrCreateSignature(data); }, @@ -6016,16 +6036,16 @@ export class Checker { function (node: Node): Symbol | undefined { const data = owner.client.apiRequest("getExportSpecifierLocalTargetSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; }, function* (node: Node): Generator { const data = yield* apiRequest("getExportSpecifierLocalTargetSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, - location: getNodeId(node), + project: owner.project.handle, + location: getNodeRequestHandle(node), }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; }, @@ -6048,7 +6068,7 @@ export class Checker { function (symbol: Symbol): Symbol { const data = owner.client.apiRequest("getAliasedSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateSymbol(data); @@ -6056,7 +6076,7 @@ export class Checker { function* (symbol: Symbol): Generator { const data = yield* apiRequest("getAliasedSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateSymbol(data); @@ -6079,14 +6099,14 @@ export class Checker { function (symbol: Symbol): string { return owner.client.apiRequest("getFullyQualifiedName", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); }, function* (symbol: Symbol): Generator { return yield* apiRequest("getFullyQualifiedName", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); }, @@ -6104,7 +6124,7 @@ export class Checker { function (symbol: Symbol): Symbol | undefined { const data = owner.client.apiRequest("getImmediateAliasedSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; @@ -6112,7 +6132,7 @@ export class Checker { function* (symbol: Symbol): Generator { const data = yield* apiRequest("getImmediateAliasedSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return data ? owner.objectRegistry.getOrCreateSymbol(data) : undefined; @@ -6135,7 +6155,7 @@ export class Checker { if (symbol.checkFlags & CheckFlags.Instantiated) { const data = owner.client.apiRequest("getTargetSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateSymbol(data); @@ -6146,7 +6166,7 @@ export class Checker { if (symbol.checkFlags & CheckFlags.Instantiated) { const data = yield* apiRequest("getTargetSymbol", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateSymbol(data); @@ -6167,7 +6187,7 @@ export class Checker { function (symbol: Symbol): Symbol { const data = owner.client.apiRequest("getExportSymbolOfSymbolForChecker", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateSymbol(data); @@ -6175,7 +6195,7 @@ export class Checker { function* (symbol: Symbol): Generator { const data = yield* apiRequest("getExportSymbolOfSymbolForChecker", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return owner.objectRegistry.getOrCreateSymbol(data); @@ -6200,13 +6220,13 @@ export class Checker { function (): { unknown: number; undefined: number; arguments: number; } { return owner.wellKnownSymbols ??= owner.client.apiRequest("getWellKnownSymbols", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); }, function* (): Generator { return owner.wellKnownSymbols ??= yield* apiRequest("getWellKnownSymbols", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); }, ); @@ -6289,13 +6309,13 @@ export class Checker { function (): { unknown: number; } { return owner.wellKnownSignatures ??= owner.client.apiRequest("getWellKnownSignatures", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); }, function* (): Generator { return owner.wellKnownSignatures ??= yield* apiRequest("getWellKnownSignatures", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, }); }, ); @@ -6334,7 +6354,7 @@ export class Checker { function (symbol: Symbol): readonly Symbol[] { const data = owner.client.apiRequest("getExportsOfModule", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return data ? data.map(d => owner.objectRegistry.getOrCreateSymbol(d)) : []; @@ -6342,7 +6362,7 @@ export class Checker { function* (symbol: Symbol): Generator { const data = yield* apiRequest("getExportsOfModule", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return data ? data.map(d => owner.objectRegistry.getOrCreateSymbol(d)) : []; @@ -6361,7 +6381,7 @@ export class Checker { function (symbol: Symbol, name: string): Symbol | undefined { const data = owner.client.apiRequest("getMemberInModuleExports", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, name, }); @@ -6370,7 +6390,7 @@ export class Checker { function* (symbol: Symbol, name: string): Generator { const data = yield* apiRequest("getMemberInModuleExports", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, name, }); @@ -6390,7 +6410,7 @@ export class Checker { function (symbol: Symbol): readonly JSDocTagInfo[] { const data = owner.client.apiRequest("getJsDocTags", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return data ?? []; @@ -6398,7 +6418,7 @@ export class Checker { function* (symbol: Symbol): Generator { const data = yield* apiRequest("getJsDocTags", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); return data ?? []; @@ -6417,14 +6437,14 @@ export class Checker { function (symbol: Symbol): string { return owner.client.apiRequest("getDocumentationComment", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); }, function* (symbol: Symbol): Generator { return yield* apiRequest("getDocumentationComment", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, symbol: symbol.reference, }); }, @@ -6445,7 +6465,7 @@ export class Checker { function (type: TypeReference): readonly Type[] { const data = owner.client.apiRequest("getTypeArguments", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); return data ? data.map(d => owner.objectRegistry.getOrCreateType(d)) : []; @@ -6453,7 +6473,7 @@ export class Checker { function* (type: TypeReference): Generator { const data = yield* apiRequest("getTypeArguments", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, type: type.id, }); return data ? data.map(d => owner.objectRegistry.getOrCreateType(d)) : []; @@ -6540,7 +6560,7 @@ export class Printer { export class NodeHandle { /** - * The project this handle was produced in, used as the default for {@link resolve}. + * The project where this handle was first materialized, used as the default for {@link resolve}. * Node handles are only meaningful within a project's program, so the producing project * is remembered so callers don't have to pass it explicitly. */ @@ -6549,12 +6569,16 @@ export class NodeHandle { private readonly fileOwner: SourceFileOwner | undefined; readonly index: number; readonly kind: SyntaxKind; + /** Identity of the exact parsed source file, shared across programs that reuse that AST. */ + readonly sourceFileId: string; + /** Retrieval metadata; never used to substitute a different source-file AST. */ readonly path: PathKey; constructor(handle: string, canonicalProject: Project | undefined, fileOwner?: SourceFileOwner) { const parsed = parseNodeHandleFromCompiler(handle); this.index = parsed.index; this.kind = parsed.kind; + this.sourceFileId = parsed.sourceFileId; this.path = parsed.path; this.canonicalProject = canonicalProject; this.fileOwner = fileOwner; @@ -6564,7 +6588,8 @@ export class NodeHandle { * Resolve this handle to the actual AST node by fetching the source file from a project * and looking up the node by index. If no project is passed, the project that produced * the handle is used. Declarations of file-owned symbols identify an exact source file and - * resolve through it, independently of any project. + * resolve through it, independently of any project. A different AST at the + * same path is never substituted. */ get resolve(): { (project?: Project | undefined): T | undefined; @@ -6577,30 +6602,41 @@ export class NodeHandle { function (project: Project | undefined = owner.canonicalProject): T | undefined { if (owner.fileOwner) { const sourceFile = owner.fileOwner.record.file ?? owner.fetchOwnerFile(owner.fileOwner); - return sourceFile.getOrCreateNodeAtIndex(owner.index) as T | undefined; + return owner.resolveInFile(sourceFile); } - if (!project) throw new Error(`Node handle for '${owner.path}' has no project context`); + if (!project) throw new Error(`Node handle for source file '${owner.sourceFileId}' has no project context`); const sourceFile = project.program.getSourceFileByPath(owner.path); if (!sourceFile) { - return undefined; + throw new Error("Node handle source file is not part of the requested program"); } - return (sourceFile as unknown as RemoteSourceFile).getOrCreateNodeAtIndex(owner.index) as T | undefined; + return owner.resolveInFile(sourceFile as unknown as RemoteSourceFile); }, function* (project: Project | undefined = owner.canonicalProject): Generator { if (owner.fileOwner) { const sourceFile = owner.fileOwner.record.file ?? (yield* owner.fetchOwnerFile.gen(owner.fileOwner)); - return sourceFile.getOrCreateNodeAtIndex(owner.index) as T | undefined; + return owner.resolveInFile(sourceFile); } - if (!project) throw new Error(`Node handle for '${owner.path}' has no project context`); + if (!project) throw new Error(`Node handle for source file '${owner.sourceFileId}' has no project context`); const sourceFile = yield* project.program.getSourceFileByPath.gen(owner.path); if (!sourceFile) { - return undefined; + throw new Error("Node handle source file is not part of the requested program"); } - return (sourceFile as unknown as RemoteSourceFile).getOrCreateNodeAtIndex(owner.index) as T | undefined; + return owner.resolveInFile(sourceFile as unknown as RemoteSourceFile); }, ); } + private resolveInFile(sourceFile: RemoteSourceFile): T | undefined { + if (sourceFile.nodeId !== this.sourceFileId) { + throw new Error(`Node handle source file is not part of the requested program`); + } + const node = sourceFile.getOrCreateNodeAtIndex(this.index); + if (!node || node.kind !== this.kind) { + throw new Error("Node handle does not identify a node of the expected kind"); + } + return node as T; + } + private get fetchOwnerFile(): { (fileOwner: SourceFileOwner): RemoteSourceFile; gen(fileOwner: SourceFileOwner): Generator; @@ -6671,8 +6707,11 @@ export class Symbol { readonly checkFlags: CheckFlags; /** @internal */ readonly reference: SymbolReference; - readonly declarations: readonly NodeHandle[]; - readonly valueDeclaration: NodeHandle | undefined; + // Declaration handles are kept in their wire form until first accessed. + private readonly declarationHandles: readonly string[] | undefined; + private readonly valueDeclarationHandle: string | undefined; + private declarationsCache: readonly NodeHandle[] | undefined; + private valueDeclarationCache: NodeHandle | undefined; private readonly parent!: CompactSymbolReference; private readonly exportSymbol!: CompactSymbolReference; private membersCache: ReadonlyMap<__String, Symbol> | undefined; @@ -6686,19 +6725,34 @@ export class Symbol { this.escapedName = data.name as __String; this.name = unescapeLeadingUnderscores(data.name as __String); this.flags = data.flags; - this.checkFlags = data.checkFlags; - // A file-owned symbol has no canonical project; its declarations resolve through its file. - const project = storage.kind === SymbolOwnerKind.Snapshot ? storage.registry.project : undefined; - const fileOwner = storage.kind === SymbolOwnerKind.File ? storage.owner : undefined; - this.declarations = (data.declarations ?? []).map(handle => new NodeHandle(handle, project, fileOwner)); - this.valueDeclaration = data.valueDeclaration - ? new NodeHandle(data.valueDeclaration, project, fileOwner) - : undefined; + this.checkFlags = data.checkFlags ?? 0; + this.declarationHandles = data.declarations; + this.valueDeclarationHandle = data.valueDeclaration; + this.declarationsCache = undefined; + this.valueDeclarationCache = undefined; if (data.parent !== undefined) this.parent = data.parent; if (data.exportSymbol !== undefined) this.exportSymbol = data.exportSymbol; } + get declarations(): readonly NodeHandle[] { + return this.declarationsCache ??= this.declarationHandles?.map(handle => this.createDeclarationHandle(handle)) ?? []; + } + + get valueDeclaration(): NodeHandle | undefined { + if (this.valueDeclarationCache === undefined && this.valueDeclarationHandle) { + this.valueDeclarationCache = this.createDeclarationHandle(this.valueDeclarationHandle); + } + return this.valueDeclarationCache; + } + + private createDeclarationHandle(handle: string): NodeHandle { + // A file-owned symbol has no canonical project; its declarations resolve through its file. + return this.storage.kind === SymbolOwnerKind.Snapshot + ? this.storage.registry.createNodeHandle(handle) + : new NodeHandle(handle, undefined, this.storage.owner); + } + get getParent(): { (): Symbol | undefined; gen(): Generator; @@ -6975,7 +7029,7 @@ class TypeObject implements Type { this.id = data.id; this.flags = data.flags; - if (data.objectFlags !== undefined) this.objectFlags = data.objectFlags; + this.objectFlags = data.objectFlags ?? 0; if (data.symbol !== undefined) this.symbol = data.symbol; if (data.value != null) { // BigInt literal values are serialized as decimal strings (e.g. "-123") because @@ -7001,7 +7055,7 @@ class TypeObject implements Type { } } if (data.intrinsicName !== undefined) this.intrinsicName = data.intrinsicName; - if (data.isThisType !== undefined) this.isThisType = data.isThisType; + this.isThisType = data.isThisType ?? false; if (data.freshType !== undefined) this.freshType = data.freshType; if (data.regularType !== undefined) this.regularType = data.regularType; if (data.target !== undefined) this.target = data.target; @@ -7998,7 +8052,9 @@ export class Signature { private objectRegistry: ProjectObjectRegistry; readonly id: number; - readonly declaration?: NodeHandle | undefined; + private readonly project: Project; + private readonly declarationHandle: string | undefined; + private declarationCache: NodeHandle | undefined; readonly typeParameters?: readonly number[] | undefined; readonly parameters: readonly CompactSymbolReference[]; readonly thisParameter?: CompactSymbolReference | undefined; @@ -8007,9 +8063,11 @@ export class Signature { constructor(data: SignatureResponse, project: Project, objectRegistry: ProjectObjectRegistry) { this.id = data.id; - this.flags = data.flags; + this.flags = data.flags ?? 0; this.objectRegistry = objectRegistry; - this.declaration = data.declaration ? new NodeHandle(data.declaration, project) : undefined; + this.project = project; + this.declarationHandle = data.declaration; + this.declarationCache = undefined; this.typeParameters = data.typeParameters ?? []; this.parameters = data.parameters ?? []; this.thisParameter = data.thisParameter; @@ -8017,6 +8075,13 @@ export class Signature { this.returnType = false; } + get declaration(): NodeHandle | undefined { + if (this.declarationCache === undefined && this.declarationHandle) { + this.declarationCache = this.objectRegistry.createNodeHandle(this.declarationHandle); + } + return this.declarationCache; + } + get getTypeParameters(): { (): readonly TypeParameter[]; gen(): Generator; diff --git a/packages/typescript/src/ast/astnav.ts b/packages/typescript/src/ast/astnav.ts index f401215034b9d..53bc447d2a0d7 100644 --- a/packages/typescript/src/ast/astnav.ts +++ b/packages/typescript/src/ast/astnav.ts @@ -94,12 +94,13 @@ export function findNextToken(previousToken: Node, parent: Node, sourceFile: Sou // The scanner is initialized at `previousToken.end`, so tokenFullStart === previousToken.end. const startPos = previousToken.end; if (startPos >= n.pos && startPos < n.end) { - const scanner = getScannerForSourceFile(sourceFile, startPos); - const token = scanner.getToken(); - const tokenFullStart = scanner.getTokenFullStart(); - const tokenEnd = scanner.getTokenEnd(); - const flags = scanner.getTokenFlags(); - return getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, n, flags); + return withScanner(sourceFile, startPos, scanner => { + const token = scanner.getToken(); + const tokenFullStart = scanner.getTokenFullStart(); + const tokenEnd = scanner.getTokenEnd(); + const flags = scanner.getTokenFlags(); + return getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, n, flags); + }); } return undefined; @@ -270,40 +271,42 @@ function getTokenAtPositionImpl( return current; } // Use the scanner to find a token not stored in the AST. - const scanner = getScannerForSourceFile(sourceFile, state.left); - let end = current.end; + const containing = current; + let end = containing.end; const afterLeft = nodeAfterLeft; if (afterLeft !== undefined) { end = afterLeft.pos; } - while (state.left < end) { - const token = scanner.getToken(); - const tokenFullStart = scanner.getTokenFullStart(); - const tokenStart = allowPositionInLeadingTrivia ? tokenFullStart : scanner.getTokenStart(); - const tokenEnd = scanner.getTokenEnd(); - const flags = scanner.getTokenFlags(); - if (tokenEnd > end) { - break; - } - if (tokenStart <= position && position < tokenEnd) { - if (token === SyntaxKind.Identifier || !isTokenKind(token)) { - if (isJSDocNodeKind(current.kind)) { - return current; + return withScanner(sourceFile, state.left, scanner => { + while (state.left < end) { + const token = scanner.getToken(); + const tokenFullStart = scanner.getTokenFullStart(); + const tokenStart = allowPositionInLeadingTrivia ? tokenFullStart : scanner.getTokenStart(); + const tokenEnd = scanner.getTokenEnd(); + const flags = scanner.getTokenFlags(); + if (tokenEnd > end) { + break; + } + if (tokenStart <= position && position < tokenEnd) { + if (token === SyntaxKind.Identifier || !isTokenKind(token)) { + if (isJSDocNodeKind(containing.kind)) { + return containing; + } + throw new Error(`did not expect ${SyntaxKind[containing.kind]} to have ${SyntaxKind[token]} in its trivia`); } - throw new Error(`did not expect ${SyntaxKind[current.kind]} to have ${SyntaxKind[token]} in its trivia`); + return getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, containing, flags); } - return getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, current, flags); - } - if (includePrecedingTokenAtEndPosition !== undefined && tokenEnd === position) { - const prevToken = getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, current, flags); - if (includePrecedingTokenAtEndPosition(prevToken)) { - return prevToken; + if (includePrecedingTokenAtEndPosition !== undefined && tokenEnd === position) { + const prevToken = getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, containing, flags); + if (includePrecedingTokenAtEndPosition(prevToken)) { + return prevToken; + } } + state.left = tokenEnd; + scanner.scan(); } - state.left = tokenEnd; - scanner.scan(); - } - return current; + return containing; + }); } current = state.next; @@ -513,20 +516,22 @@ function findRightmostValidToken(sourceFile: SourceFile, endPos: number, contain const startPos = rightmostValidNode !== undefined ? rightmostValidNode.end : n.pos; const targetEnd = Math.min(endPos, position); if (startPos < targetEnd) { - const scanner = getScannerForSourceFile(sourceFile, startPos); - let pos = startPos; - let lastScannedToken: Node | undefined; - while (pos < targetEnd) { - const tokenStart = scanner.getTokenStart(); - if (tokenStart >= position) break; - const tokenFullStart = scanner.getTokenFullStart(); - const tokenEnd = scanner.getTokenEnd(); - const token = scanner.getToken(); - const flags = scanner.getTokenFlags(); - lastScannedToken = getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, n, flags); - pos = tokenEnd; - scanner.scan(); - } + const lastScannedToken = withScanner(sourceFile, startPos, scanner => { + let pos = startPos; + let lastToken: Node | undefined; + while (pos < targetEnd) { + const tokenStart = scanner.getTokenStart(); + if (tokenStart >= position) break; + const tokenFullStart = scanner.getTokenFullStart(); + const tokenEnd = scanner.getTokenEnd(); + const token = scanner.getToken(); + const flags = scanner.getTokenFlags(); + lastToken = getOrCreateToken(sourceFile, token, tokenFullStart, tokenEnd, n, flags); + pos = tokenEnd; + scanner.scan(); + } + return lastToken; + }); if (lastScannedToken !== undefined) { return lastScannedToken; } @@ -592,13 +597,64 @@ function isJSDocSingleCommentNodeList(nodes: NodeArray): boolean { return nodes.length === 1 && isJSDocCommentChildKind(nodes[0].kind); } -function getScannerForSourceFile(sourceFile: SourceFile, pos: number) { - const scanner = createScanner(/*skipTrivia*/ true, sourceFile.languageVariant, sourceFile.text); - scanner.resetTokenState(pos); - scanner.scan(); - return scanner; +// Idle scanners available for reuse, keyed by the source file whose text they scan. +// A scanner is removed from its pool while in use, so nested or reentrant traversals +// always receive distinct scanners. Pooled scanners retain only the source file's text. +interface ScannerLease extends Disposable { + readonly scanner: Scanner; } +const scannerPools = new WeakMap(); + +function acquireScanner(sourceFile: SourceFile): ScannerLease { + let pool = scannerPools.get(sourceFile); + if (pool === undefined) { + pool = []; + scannerPools.set(sourceFile, pool); + } + const lease = pool.pop(); + if (lease === undefined) { + return createPooledScanner(sourceFile, pool); + } + // setText resets the token state (position, token, value, flags) to the start of the text. + lease.scanner.setText(sourceFile.text); + lease.scanner.setLanguageVariant(sourceFile.languageVariant); + return lease; +} + +function createPooledScanner(sourceFile: SourceFile, pool: ScannerLease[]): ScannerLease { + return { + scanner: createScanner(/*skipTrivia*/ true, sourceFile.languageVariant, sourceFile.text), + [Symbol.dispose]() { + // Drop per-use state so an idle scanner holds no token values or comment directives. + // The JSDoc leading-asterisk depth is never changed by this module, so it stays zero. + this.scanner.clearCommentDirectives(); + this.scanner.resetTokenState(0); + pool.push(this); + }, + }; +} + +function withScanner(sourceFile: SourceFile, pos: number | undefined, callback: (scanner: Scanner) => T): T { + using lease = acquireScanner(sourceFile); + const { scanner } = lease; + if (pos !== undefined) { + scanner.resetTokenState(pos); + scanner.scan(); + } + return callback(scanner); +} + +/** + * Returns the idle scanners pooled for `sourceFile`. Exposed for tests only. + * @internal + */ +export function getPooledScannersForTesting(sourceFile: SourceFile): readonly Scanner[] { + return scannerPools.get(sourceFile)?.map(lease => lease.scanner) ?? emptyScannerArray; +} + +const emptyScannerArray: readonly Scanner[] = []; + type Mutable = { -readonly [K in keyof T]: T[K]; }; function getOrCreateToken(sourceFile: SourceFile, kind: SyntaxKind, pos: number, end: number, parent: Node, _flags: number): Node { @@ -669,7 +725,8 @@ function createChildren(node: Node, sourceFile: SourceFile): readonly Node[] { } // One scanner serves every run of synthetic tokens materialized for this node. - const scanner = createScanner(/*skipTrivia*/ true, sourceFile.languageVariant, sourceFile.text); + using lease = acquireScanner(sourceFile); + const { scanner } = lease; let pos = node.pos; const processNode = (child: Node): undefined => { // Reparsed subtrees (JSDoc types materialized into the AST) have positions inside diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index b9ef5842d4489..379f07b6b68f7 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -138,6 +138,8 @@ import { parseNodeHandleFromCompiler, RemoteSourceFile, } from "../../src/api/node/node.ts"; +// @ts-ignore -- Internal request format is exercised by source tests. +import { getNodeRequestHandle } from "../../src/api/node/node.ts"; import { isSignatureDeclaration } from "../../src/ast/is.ts"; import { areTestsFiltered, @@ -664,7 +666,7 @@ describe("API", { concurrency }, () => { assert.equal((await getSymbol(recreatedDeclaration))?.name, "present"); }); - test("createSourceFile can be used with a compatible program", async () => { + test("createSourceFile cannot substitute a distinct program AST with the same text", async () => { const sourceText = "export const element =
;"; await using api = spawnAPI({ "/component.tsx": sourceText, @@ -674,7 +676,8 @@ describe("API", { concurrency }, () => { const snapshot = await api.createSnapshot({ openFiles: ["/component.tsx"] }); const project = snapshot.getProjects()[0]; assert.equal((await api.printer.printNode(sourceFile)).trimEnd(), sourceText); - assert.ok(await project.checker.getTypeAtLocation(sourceFile.statements[0])); + assert.notStrictEqual(await project.program.getSourceFile("/component.tsx"), sourceFile); + await assert.rejects(project.checker.getTypeAtLocation(sourceFile.statements[0]), /could not be resolved/); // @sync: assert.throws(() => project.checker.getTypeAtLocation(sourceFile.statements[0]), /could not be resolved/); assert.equal(await project.program.isSourceFileDefaultLibrary(sourceFile), false); await snapshot.dispose(); }); @@ -691,6 +694,42 @@ describe("API", { concurrency }, () => { await snapshot.dispose(); }); + test("node handles reject a different source-file version but accept an unchanged AST", async () => { + const { api: disposableAPI, fs } = spawnAPIWithFS({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/index.ts": "export function f() { return 1; }", + "/other.ts": "export const other = 1;", + }); + await using api = disposableAPI; + const first = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const firstProject = first.getConfiguredProject("/tsconfig.json")!; + const file = await firstProject.program.getSourceFile("/index.ts"); + assert.ok(file instanceof RemoteSourceFile); + const declaration = file.statements[0]; + const symbol = await firstProject.checker.getSymbolAtPosition("/index.ts", 16); + assert.ok(symbol); + const type = await firstProject.checker.getTypeOfSymbol(symbol); + const signatures = await firstProject.checker.getSignaturesOfType(type, SignatureKind.Call); + const handle = signatures[0].declaration; + assert.ok(handle); + assert.equal(handle.path, file.path); + assert.equal(handle.sourceFileId, parseNodeHandleFromCompiler(getNodeId(file)).sourceFileId); + const parsed = parseNodeHandleFromCompiler(getNodeId(declaration)); + assert.equal(getNodeRequestHandle(declaration), `${parsed.index}.#${parsed.sourceFileId}`); + + fs.writeFile!(toRootedFilePath("/other.ts", undefined), "export const other = 2;"); + const unchanged = await api.createSnapshot({ openProjects: ["/tsconfig.json"], fileNotifications: { changed: ["/other.ts"] } }); + const unchangedProject = unchanged.getConfiguredProject("/tsconfig.json")!; + assert.ok(await unchangedProject.checker.getTypeAtLocation(declaration)); + assert.strictEqual(await handle.resolve(unchangedProject), declaration); + + fs.writeFile!(toRootedFilePath("/index.ts", undefined), "export function f() { return 'changed'; }"); + const changed = await api.createSnapshot({ openProjects: ["/tsconfig.json"], fileNotifications: { changed: ["/index.ts"] } }); + const changedProject = changed.getConfiguredProject("/tsconfig.json")!; + await assert.rejects(changedProject.checker.getTypeAtLocation(declaration), /could not be resolved/); // @sync: assert.throws(() => changedProject.checker.getTypeAtLocation(declaration), /could not be resolved/); + await assert.rejects(handle.resolve(changedProject), /source file is not part of the requested program/); // @sync: assert.throws(() => handle.resolve(changedProject), /source file is not part of the requested program/); + }); + test("retainSourceFile keeps a borrowed program file available", async () => { const sourceText = "export declare const value: number;"; await using api = spawnAPI({ @@ -4117,6 +4156,64 @@ export const value = 1; assert.equal((await api.getTimingInfo()).totals.requestCount, 0); }); + test("node handles are lazily interned within a snapshot", async () => { + const source = `interface Box { value: T; method(value: T): T; extra: T; } +declare const first: Box; +declare const second: Box; +first; +second;`; + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ files: ["/index.ts"] }), + "/other/tsconfig.json": JSON.stringify({ files: ["/index.ts"] }), + "/index.ts": source, + }); + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json", "/other/tsconfig.json"] }); + const registry = (snapshot as unknown as { + snapshotRegistry: { nodeHandles: Map; }; + }).snapshotRegistry; + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + const otherProject = snapshot.getConfiguredProject("/other/tsconfig.json")!; + const firstType = await project.checker.getTypeAtPosition("/index.ts", source.lastIndexOf("first")); + const secondType = await otherProject.checker.getTypeAtPosition("/index.ts", source.lastIndexOf("second")); + assert.ok(firstType && secondType); + const [firstValue, firstMethod, firstExtra] = await firstType.getProperties(); + const [secondValue, secondMethod] = await secondType.getProperties(); + assert.ok(firstValue.checkFlags & CheckFlags.Instantiated); + assert.ok(secondValue.checkFlags & CheckFlags.Instantiated); + assert.equal(registry.nodeHandles.size, 0); + + const handle = firstValue.declarations[0]; + assert.ok(handle); + assert.strictEqual(handle, firstValue.valueDeclaration); + assert.strictEqual(handle, secondValue.declarations[0]); + assert.strictEqual(handle, secondValue.valueDeclaration); + assert.strictEqual(await handle.resolve(), await handle.resolve(otherProject)); + + const firstMethodType = await project.checker.getTypeOfSymbol(firstMethod); + const secondMethodType = await otherProject.checker.getTypeOfSymbol(secondMethod); + const [firstSignature] = await firstMethodType.getCallSignatures(); + const [secondSignature] = await secondMethodType.getCallSignatures(); + assert.ok(firstSignature.declaration); + assert.strictEqual(firstSignature.declaration, secondSignature.declaration); + assert.strictEqual(firstSignature.declaration, firstMethod.declarations[0]); + assert.strictEqual(firstSignature.declaration, secondMethod.declarations[0]); + + const updated = await snapshot.update({}); + const updatedProject = updated.getConfiguredProject("/tsconfig.json")!; + const updatedType = await updatedProject.checker.getTypeAtPosition("/index.ts", source.lastIndexOf("first")); + assert.ok(updatedType); + const [updatedValue] = await updatedType.getProperties(); + const updatedHandle = updatedValue.declarations[0]; + assert.notStrictEqual(updatedHandle, handle); + assert.equal(updatedHandle.sourceFileId, handle.sourceFileId); + await snapshot.dispose(); + assert.equal(registry.nodeHandles.size, 0); + assert.ok(firstExtra.valueDeclaration); + assert.equal(registry.nodeHandles.size, 0); + assert.ok(await updatedHandle.resolve()); + await updated.dispose(); + }); + test("compact symbol references retain reused file-owned symbols", async () => { const source = `interface Box { value: T; }\ndeclare const box: Box;\nbox;`; await using api = spawnAPI({ diff --git a/packages/typescript/test/path.test.ts b/packages/typescript/test/path.test.ts index ebcada46ece30..61081dff6ca09 100644 --- a/packages/typescript/test/path.test.ts +++ b/packages/typescript/test/path.test.ts @@ -266,11 +266,18 @@ test("rooted path constructors enforce the type lattice", () => { }); test("parseNodeHandleFromCompiler validates serialized fields", () => { - assert.deepStrictEqual(parseNodeHandleFromCompiler("12.80./workspace/src/file.ts"), { + assert.deepStrictEqual(parseNodeHandleFromCompiler("12.80.#42./workspace/src/file.ts"), { index: 12, kind: 80, + sourceFileId: "42", path: "/workspace/src/file.ts", }); + assert.deepStrictEqual(parseNodeHandleFromCompiler("12.80.#18446744073709551615./file.ts"), { + index: 12, + kind: 80, + sourceFileId: "18446744073709551615", + path: "/file.ts", + }); for ( const handle of [ @@ -278,6 +285,13 @@ test("parseNodeHandleFromCompiler validates serialized fields", () => { "12.x./workspace/src/file.ts", "12.80.", "12.80./workspace/../src/file.ts", + "12.80.#0", + "12.80.#01", + "12.80.#-1", + "12.80.#18446744073709551616", + "12.80.#42.", + "12.80.#42", + "12.80.#42./workspace/../src/file.ts", ] ) { assert.throws(() => parseNodeHandleFromCompiler(handle), /Invalid node handle/); diff --git a/packages/typescript/test/proto.test.ts b/packages/typescript/test/proto.test.ts index 18bd6d9a3dec7..39159e79c6c9f 100644 --- a/packages/typescript/test/proto.test.ts +++ b/packages/typescript/test/proto.test.ts @@ -17,7 +17,7 @@ import type { import { SymbolOwnerKind } from "../src/enums/symbolOwnerKind.ts"; test("symbol ownership protocol validation rejects mixed variants", () => { - const project = "/tsconfig.json" as ProjectId; + const project = 1; const file = { fileName: "/file.ts" as RootedFilePath, path: "/file.ts" as PathKey, @@ -41,9 +41,9 @@ test("symbol ownership protocol validation rejects mixed variants", () => { const owner of [ { kind: SymbolOwnerKind.File }, { kind: SymbolOwnerKind.File, file, snapshot: 1 }, - { kind: SymbolOwnerKind.Snapshot, project: "/tsconfig.json" }, + { kind: SymbolOwnerKind.Snapshot, project }, { kind: SymbolOwnerKind.Snapshot, snapshot: 1 }, - { kind: SymbolOwnerKind.Snapshot, snapshot: 1, project: "/tsconfig.json", file }, + { kind: SymbolOwnerKind.Snapshot, snapshot: 1, project, file }, { kind: 99 }, ] as ProtocolSymbolOwner[] ) { diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index dbe13a87c3593..30c6ad3172da4 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -170,6 +170,7 @@ const privateGeneratorGetters = new Set([ "Checker.getWellKnownSignatures", "Checker.getWellKnownSymbols", "NodeHandle.fetchOwnerFile", + "NodeHandle.resolveInFile", "Program.disposeWorker", "Program.fetchSourceFileMetadata", "Program.getSourceFileWorker", @@ -297,6 +298,7 @@ function assertSignatureArraysEquivalent(actual: readonly Signature[], expected: function assertNodeHandlesEquivalent(actual: NodeHandle, expected: NodeHandle, message?: string): void { assert.equal(actual.index, expected.index, message); assert.equal(actual.kind, expected.kind, message); + assert.equal(actual.sourceFileId, expected.sourceFileId, message); assert.equal(actual.path, expected.path, message); } diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index dbe34fb1b2d4b..f048e8ede87e6 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -146,6 +146,8 @@ import { parseNodeHandleFromCompiler, RemoteSourceFile, } from "../../src/api/node/node.ts"; +// @ts-ignore -- Internal request format is exercised by source tests. +import { getNodeRequestHandle } from "../../src/api/node/node.ts"; import { isSignatureDeclaration } from "../../src/ast/is.ts"; import { areTestsFiltered, @@ -651,7 +653,7 @@ describe("API", { concurrency }, () => { assert.equal((getSymbol(recreatedDeclaration))?.name, "present"); }); - test("createSourceFile can be used with a compatible program", () => { + test("createSourceFile cannot substitute a distinct program AST with the same text", () => { const sourceText = "export const element =
;"; using api = spawnAPI({ "/component.tsx": sourceText, @@ -661,7 +663,8 @@ describe("API", { concurrency }, () => { const snapshot = api.createSnapshot({ openFiles: ["/component.tsx"] }); const project = snapshot.getProjects()[0]; assert.equal((api.printer.printNode(sourceFile)).trimEnd(), sourceText); - assert.ok(project.checker.getTypeAtLocation(sourceFile.statements[0])); + assert.notStrictEqual(project.program.getSourceFile("/component.tsx"), sourceFile); + assert.throws(() => project.checker.getTypeAtLocation(sourceFile.statements[0]), /could not be resolved/); assert.equal(project.program.isSourceFileDefaultLibrary(sourceFile), false); snapshot.dispose(); }); @@ -678,6 +681,42 @@ describe("API", { concurrency }, () => { snapshot.dispose(); }); + test("node handles reject a different source-file version but accept an unchanged AST", () => { + const { api: disposableAPI, fs } = spawnAPIWithFS({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/index.ts": "export function f() { return 1; }", + "/other.ts": "export const other = 1;", + }); + using api = disposableAPI; + const first = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const firstProject = first.getConfiguredProject("/tsconfig.json")!; + const file = firstProject.program.getSourceFile("/index.ts"); + assert.ok(file instanceof RemoteSourceFile); + const declaration = file.statements[0]; + const symbol = firstProject.checker.getSymbolAtPosition("/index.ts", 16); + assert.ok(symbol); + const type = firstProject.checker.getTypeOfSymbol(symbol); + const signatures = firstProject.checker.getSignaturesOfType(type, SignatureKind.Call); + const handle = signatures[0].declaration; + assert.ok(handle); + assert.equal(handle.path, file.path); + assert.equal(handle.sourceFileId, parseNodeHandleFromCompiler(getNodeId(file)).sourceFileId); + const parsed = parseNodeHandleFromCompiler(getNodeId(declaration)); + assert.equal(getNodeRequestHandle(declaration), `${parsed.index}.#${parsed.sourceFileId}`); + + fs.writeFile!(toRootedFilePath("/other.ts", undefined), "export const other = 2;"); + const unchanged = api.createSnapshot({ openProjects: ["/tsconfig.json"], fileNotifications: { changed: ["/other.ts"] } }); + const unchangedProject = unchanged.getConfiguredProject("/tsconfig.json")!; + assert.ok(unchangedProject.checker.getTypeAtLocation(declaration)); + assert.strictEqual(handle.resolve(unchangedProject), declaration); + + fs.writeFile!(toRootedFilePath("/index.ts", undefined), "export function f() { return 'changed'; }"); + const changed = api.createSnapshot({ openProjects: ["/tsconfig.json"], fileNotifications: { changed: ["/index.ts"] } }); + const changedProject = changed.getConfiguredProject("/tsconfig.json")!; + assert.throws(() => changedProject.checker.getTypeAtLocation(declaration), /could not be resolved/); + assert.throws(() => handle.resolve(changedProject), /source file is not part of the requested program/); + }); + test("retainSourceFile keeps a borrowed program file available", () => { const sourceText = "export declare const value: number;"; using api = spawnAPI({ @@ -3942,6 +3981,64 @@ export const value = 1; assert.equal((api.getTimingInfo()).totals.requestCount, 0); }); + test("node handles are lazily interned within a snapshot", () => { + const source = `interface Box { value: T; method(value: T): T; extra: T; } +declare const first: Box; +declare const second: Box; +first; +second;`; + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ files: ["/index.ts"] }), + "/other/tsconfig.json": JSON.stringify({ files: ["/index.ts"] }), + "/index.ts": source, + }); + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json", "/other/tsconfig.json"] }); + const registry = (snapshot as unknown as { + snapshotRegistry: { nodeHandles: Map; }; + }).snapshotRegistry; + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + const otherProject = snapshot.getConfiguredProject("/other/tsconfig.json")!; + const firstType = project.checker.getTypeAtPosition("/index.ts", source.lastIndexOf("first")); + const secondType = otherProject.checker.getTypeAtPosition("/index.ts", source.lastIndexOf("second")); + assert.ok(firstType && secondType); + const [firstValue, firstMethod, firstExtra] = firstType.getProperties(); + const [secondValue, secondMethod] = secondType.getProperties(); + assert.ok(firstValue.checkFlags & CheckFlags.Instantiated); + assert.ok(secondValue.checkFlags & CheckFlags.Instantiated); + assert.equal(registry.nodeHandles.size, 0); + + const handle = firstValue.declarations[0]; + assert.ok(handle); + assert.strictEqual(handle, firstValue.valueDeclaration); + assert.strictEqual(handle, secondValue.declarations[0]); + assert.strictEqual(handle, secondValue.valueDeclaration); + assert.strictEqual(handle.resolve(), handle.resolve(otherProject)); + + const firstMethodType = project.checker.getTypeOfSymbol(firstMethod); + const secondMethodType = otherProject.checker.getTypeOfSymbol(secondMethod); + const [firstSignature] = firstMethodType.getCallSignatures(); + const [secondSignature] = secondMethodType.getCallSignatures(); + assert.ok(firstSignature.declaration); + assert.strictEqual(firstSignature.declaration, secondSignature.declaration); + assert.strictEqual(firstSignature.declaration, firstMethod.declarations[0]); + assert.strictEqual(firstSignature.declaration, secondMethod.declarations[0]); + + const updated = snapshot.update({}); + const updatedProject = updated.getConfiguredProject("/tsconfig.json")!; + const updatedType = updatedProject.checker.getTypeAtPosition("/index.ts", source.lastIndexOf("first")); + assert.ok(updatedType); + const [updatedValue] = updatedType.getProperties(); + const updatedHandle = updatedValue.declarations[0]; + assert.notStrictEqual(updatedHandle, handle); + assert.equal(updatedHandle.sourceFileId, handle.sourceFileId); + snapshot.dispose(); + assert.equal(registry.nodeHandles.size, 0); + assert.ok(firstExtra.valueDeclaration); + assert.equal(registry.nodeHandles.size, 0); + assert.ok(updatedHandle.resolve()); + updated.dispose(); + }); + test("compact symbol references retain reused file-owned symbols", () => { const source = `interface Box { value: T; }\ndeclare const box: Box;\nbox;`; using api = spawnAPI({ diff --git a/packages/typescript/test/sync/ast.test.ts b/packages/typescript/test/sync/ast.test.ts index 57b5f086efd38..1c0cab8becb8c 100644 --- a/packages/typescript/test/sync/ast.test.ts +++ b/packages/typescript/test/sync/ast.test.ts @@ -17,12 +17,16 @@ import type { ObjectBindingPattern, ObjectLiteralElement, ObjectLiteralExpression, + Scanner, SourceFile, StringLiteralLikeNode, TypeElement, VariableStatement, } from "@typescript/typescript/ast"; +import * as astModule from "@typescript/typescript/ast"; import { + formatSyntaxKind, + getChildren, getCombinedModifierFlags, getNameOfDeclaration, getTokenAtPosition, @@ -1550,3 +1554,248 @@ describe("RemoteNode + child/token getters", { concurrency }, () => { }); } }); + +describe("getChildren scanner reuse", { concurrency }, () => { + // Not part of the public declarations; reached through the module namespace for tests. + const getPooledScanners = (astModule as unknown as { + getPooledScannersForTesting(sourceFile: SourceFile): readonly Scanner[]; + }).getPooledScannersForTesting; + + type TreeSnapshot = [kind: string, pos: number, end: number, children: TreeSnapshot[]]; + + function snapshotTree(node: Node, sf: SourceFile): TreeSnapshot { + const children = node.getChildren(sf); + return [formatSyntaxKind(node.kind), node.pos, node.end, children.map(child => snapshotTree(child, sf))]; + } + + function leafTokenTexts(node: Node, sf: SourceFile): string[] { + const children = node.getChildren(sf); + if (children.length === 0) { + return [node.getText(sf)]; + } + return children.flatMap(child => leafTokenTexts(child, sf)); + } + + function withSourceFiles(files: Record, fn: (sourceFiles: SourceFile[]) => void): void { + const tsconfig = { compilerOptions: { jsx: "react-jsx", allowJs: true, checkJs: true } }; + const api = spawnAPI({ "/tsconfig.json": JSON.stringify(tsconfig), ...files }); + try { + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; + fn(Object.keys(files).map(fileName => program.getSourceFile(fileName)!)); + } + finally { + api.close(); + } + } + + // Snapshots each file in isolation (its own API process) so results can't depend on pooling. + function referenceSnapshot(fileName: string, text: string): TreeSnapshot { + let result!: TreeSnapshot; + withSourceFiles({ [fileName]: text }, ([sf]) => { + result = snapshotTree(sf, sf); + }); + return result; + } + + // Wraps `node` so that `during` runs right after forEachChild visits its first child, + // i.e. while getChildren has a scanner checked out for the wrapped node mid-traversal. + // Methods are bound to the real node, so pass the wrapper to the free getChildren. + function interceptForEachChild(node: Node, during: () => void): Node { + return new Proxy(node, { + get(target, prop) { + if (prop === "forEachChild") { + return (visitor: (node: Node) => T, visitArray?: (nodes: NodeArray) => T) => { + let ran = false; + return target.forEachChild(child => { + const result = visitor(child); + if (!ran) { + ran = true; + during(); + } + return result; + }, visitArray); + }; + } + const value = Reflect.get(target, prop, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + } + + function assertIdle(scanner: Scanner, sf: SourceFile): void { + assert.strictEqual(scanner.getText(), sf.text); + assert.strictEqual(scanner.getToken(), SyntaxKind.Unknown); + assert.strictEqual(scanner.getTokenFullStart(), 0); + assert.strictEqual(scanner.getTokenEnd(), 0); + assert.strictEqual(scanner.getTokenValue(), undefined); + assert.strictEqual(scanner.getTokenFlags(), TokenFlags.None); + assert.strictEqual(scanner.getCommentDirectives(), undefined); + } + + const sample = [ + "/** docs */", + "export function f(a: number, b = [1, 2]): number {", + " if (a) { return b[0]; } else { return a ?? 0; }", + "}", + "", + ].join("\n"); + + test("a full traversal and repeated cached calls reuse a single scanner", () => { + withSourceFiles({ "/src/a.ts": sample }, ([sf]) => { + assert.strictEqual(getPooledScanners(sf).length, 0); + const first = snapshotTree(sf, sf); + const pool = getPooledScanners(sf); + assert.strictEqual(pool.length, 1); + const scanner = pool[0]; + + assert.deepStrictEqual(snapshotTree(sf, sf), first); + const stmt = sf.statements[0]; + assert.strictEqual(stmt.getChildren(sf), stmt.getChildren(sf)); + assert.deepStrictEqual(getPooledScanners(sf), [scanner]); + + // Token lookups that scan between AST children check out the same pooled scanner. + for (let pos = 0; pos <= sf.end; pos++) { + getTokenAtPosition(sf, pos); + } + assert.deepStrictEqual(getPooledScanners(sf), [scanner]); + assertIdle(scanner, sf); + + assert.deepStrictEqual(first, referenceSnapshot("/src/a.ts", sample)); + }); + }); + + test("nested child creation uses a distinct scanner and does not disturb the outer one", () => { + const text = "if (a) { x(1, 2); } else { y[0] = 3; }\nwhile (b) { z(); }\n"; + const reference = referenceSnapshot("/src/n.ts", text); + withSourceFiles({ "/src/n.ts": text }, ([sf]) => { + const [ifStmt, whileStmt] = sf.statements; + let nestedRuns = 0; + let poolSizeDuringNested = -1; + const wrapped = interceptForEachChild(ifStmt, () => { + nestedRuns++; + // The outer scanner is still checked out, so the pool is empty here. + poolSizeDuringNested = getPooledScanners(sf).length; + // Materialize a whole other subtree, including its own nested getChildren calls. + snapshotTree(whileStmt, sf); + }); + + const outerChildren = getChildren(wrapped, sf); + assert.strictEqual(nestedRuns, 1); + assert.strictEqual(poolSizeDuringNested, 0); + assert.deepStrictEqual( + outerChildren.map(c => [formatSyntaxKind(c.kind), c.pos, c.end]), + reference[3][0][3][0][3].map(([kind, pos, end]) => [kind, pos, end]), + ); + assert.deepStrictEqual(snapshotTree(whileStmt, sf), reference[3][0][3][1]); + + const pool = getPooledScanners(sf); + assert.strictEqual(pool.length, 2); + assert.notStrictEqual(pool[0], pool[1]); + for (const scanner of pool) { + assertIdle(scanner, sf); + } + }); + }); + + test("a scanner is returned to the pool when child creation throws", () => { + const text = "if (a) { b(); } else { c(); }\n"; + withSourceFiles({ "/src/t.ts": text }, ([sf]) => { + const stmt = sf.statements[0]; + const throwing = interceptForEachChild(stmt, () => { + throw new Error("boom"); + }); + assert.throws(() => getChildren(throwing, sf), /boom/); + const pool = getPooledScanners(sf); + assert.strictEqual(pool.length, 1); + assertIdle(pool[0], sf); + + // A failed attempt is not cached, and the pooled scanner works for later calls. + assert.throws(() => getChildren(throwing, sf), /boom/); + assert.strictEqual(getPooledScanners(sf).length, 1); + assert.deepStrictEqual(stmt.getChildren(sf).map(c => c.getText(sf)), ["if", "(", "a", ")", "{ b(); }", "else", "{ c(); }"]); + assert.strictEqual(getPooledScanners(sf).length, 1); + }); + }); + + test("trivia, comment directives and JSDoc do not leak state between uses", () => { + const text = [ + "/** leading doc */", + "function f(/* a */ a: number /* b */, b: string) // @ts-ignore", + "{", + " // @ts-expect-error", + " return a /* mid */ + 1;", + "}", + "/** @type {number} */", + "const x = /* @ts-ignore */ (1);", + "", + ].join("\n"); + const reference = referenceSnapshot("/src/c.ts", text); + withSourceFiles({ "/src/c.ts": text }, ([sf]) => { + const fn = sf.statements[0]; + assert.strictEqual(fn.getChildren(sf)[0].kind, SyntaxKind.JSDoc); + assert.strictEqual(sf.statements[1].getChildren(sf)[0].kind, SyntaxKind.JSDoc); + assert.deepStrictEqual(snapshotTree(sf, sf), reference); + // Leaf tokens absorb all interior trivia (including JSDoc, which is also exposed + // as its own child), so their full texts tile the file. + const leaves: Node[] = []; + const collect = (node: Node): void => { + if (node.kind === SyntaxKind.JSDoc) { + return; + } + const children = node.getChildren(sf); + if (children.length === 0) { + leaves.push(node); + return; + } + children.forEach(collect); + }; + collect(sf); + assert.strictEqual(leaves.map(leaf => leaf.getFullText(sf)).join(""), sf.text); + + const pool = getPooledScanners(sf); + assert.strictEqual(pool.length, 1); + assertIdle(pool[0], sf); + }); + }); + + test("token boundaries between adjacent tokens are exact", () => { + const text = "a?.b!(c)[d] ??= e=>f;x>>=y>>>z;q=(v:T)=>v\n"; + withSourceFiles({ "/src/b.ts": text }, ([sf]) => { + const expected = "a ?. b ! ( c ) [ d ] ??= e => f ; x >>= y >>> z ; q = < T , > ( v : T ) => v".split(" "); + // The trailing EndOfFile token is empty. + assert.deepStrictEqual(leafTokenTexts(sf, sf), [...expected, ""]); + assert.strictEqual(getPooledScanners(sf).length, 1); + }); + }); + + test("source files with different language variants keep independent scanners", () => { + const files = { + "/src/plain.ts": "const a = x;\nconst f = (v: T) => v;\nlet c = a < b && b > c;\n", + "/src/element.tsx": 'const e =
hi {name}
;\nconst g = (v: T) => v;\n', + "/src/component.jsx": "/** @param {string} s */\nexport const h = s => {s};\n", + }; + const references = Object.entries(files).map(([fileName, text]) => referenceSnapshot(fileName, text)); + withSourceFiles(files, sourceFiles => { + // Interleave materialization across files so each file's scanner is used between + // uses of the others. + const queues = sourceFiles.map(sf => [sf as Node]); + while (queues.some(queue => queue.length > 0)) { + queues.forEach((queue, i) => { + const node = queue.shift(); + if (node) { + queue.push(...node.getChildren(sourceFiles[i])); + } + }); + } + sourceFiles.forEach((sf, i) => { + assert.deepStrictEqual(snapshotTree(sf, sf), references[i]); + const pool = getPooledScanners(sf); + assert.strictEqual(pool.length, 1); + assertIdle(pool[0], sf); + }); + const scanners = new Set(sourceFiles.map(sf => getPooledScanners(sf)[0])); + assert.strictEqual(scanners.size, sourceFiles.length); + }); + }); +}); diff --git a/tsc/internal/api/nodehandle_test.go b/tsc/internal/api/nodehandle_test.go new file mode 100644 index 0000000000000..0bbf6b7536ae5 --- /dev/null +++ b/tsc/internal/api/nodehandle_test.go @@ -0,0 +1,76 @@ +package api + +import ( + "context" + "fmt" + "sync" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/api/encoder" + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" + "gotest.tools/v3/assert" +) + +func TestNodeHandlesIdentifyExactSourceFiles(t *testing.T) { + t.Parallel() + const text = "export const x = 1;" + projectSession, _ := projecttestutil.Setup(map[string]any{"/index.ts": text}) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: "/index.ts"}}, + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + }}, + }) + assert.NilError(t, err) + sd, err := session.getSnapshotData(response.Snapshot) + assert.NilError(t, err) + program, err := sd.getProgram(response.Projects[0].Handle) + assert.NilError(t, err) + file := program.GetSourceFiles()[0] + node := file.Statements.Nodes[0] + var lookups sync.WaitGroup + for range 8 { + lookups.Go(func() { + if got := program.GetSourceFileByNodeID(ast.GetNodeId(file.AsNode())); got != file { + t.Errorf("source-file identity lookup returned %p, expected %p", got, file) + } + }) + } + lookups.Wait() + handle := NodeHandle(fmt.Sprintf("%d.#%d", encoder.GetNodeIndexTable(file).GetIndex(node), sourceFileNodeID(file))) + resolved, err := sd.resolveNodeHandle(program, handle) + assert.NilError(t, err) + assert.Equal(t, resolved, node) + + // Equal text and path do not make a separately parsed AST part of this program. + separate := parseAndBind(t, "/index.ts", text) + _, err = sd.resolveNodeHandle(program, NodeHandle(fmt.Sprintf("1.#%d", sourceFileNodeID(separate)))) + assert.ErrorContains(t, err, "could not be resolved") + + resolved, err = sd.resolveNodeHandle(program, NodeHandle(fmt.Sprintf("1.#%d", sourceFileNodeID(file)))) + assert.NilError(t, err) + assert.Equal(t, resolved, file.AsNode()) + assert.Equal(t, nodeHandleFrom(node), NodeHandle(fmt.Sprintf("%d.%d.#%d.%s", + encoder.GetNodeIndexTable(file).GetIndex(node), node.Kind, sourceFileNodeID(file), file.PathKey()))) + + for _, handle := range []NodeHandle{ + nodeHandleFrom(node), + NodeHandle(fmt.Sprintf("1.%d.#%d", node.Kind, sourceFileNodeID(file))), + NodeHandle(fmt.Sprintf("1.%d.%s", file.Kind, file.PathKey())), + "1.#0", + "1.#x", + "x.#1", + "1.1", + "1.#18446744073709551616", + NodeHandle(fmt.Sprintf("0.#%d", sourceFileNodeID(file))), + NodeHandle(fmt.Sprintf("4294967295.#%d", sourceFileNodeID(file))), + } { + _, err = sd.resolveNodeHandle(program, handle) + assert.Assert(t, err != nil, "unexpectedly resolved %q", handle) + } +} diff --git a/tsc/internal/api/projecthandle_test.go b/tsc/internal/api/projecthandle_test.go new file mode 100644 index 0000000000000..01376ae7f6271 --- /dev/null +++ b/tsc/internal/api/projecthandle_test.go @@ -0,0 +1,61 @@ +package api + +import ( + "context" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/project" + "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" + "gotest.tools/v3/assert" +) + +func TestProjectHandles(t *testing.T) { + t.Parallel() + + decoded, err := unmarshalPayload(string(MethodGetSourceFileNames), []byte(`{"snapshot":1,"project":42}`)) + assert.NilError(t, err) + assert.Equal(t, decoded.(*GetSourceFileNamesParams).Project, project.Handle(42)) + + for _, value := range []string{`"#42"`, `"/full/tsconfig.json"`} { + _, err := unmarshalPayload(string(MethodGetSourceFileNames), []byte(`{"snapshot":1,"project":`+value+`}`)) + assert.Assert(t, err != nil) + } +} + +func TestProjectHandlesAreScopedToSnapshots(t *testing.T) { + t.Parallel() + + projectSession, _ := projecttestutil.Setup(map[string]any{ + "/index.ts": "export const x = 1;", + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + + params := &CreateSnapshotParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: "/index.ts"}}, + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + }}, + } + first, err := session.handleCreateSnapshot(context.Background(), params) + assert.NilError(t, err) + second, err := session.handleCreateSnapshot(context.Background(), params) + assert.NilError(t, err) + assert.Equal(t, first.Projects[0].Id, second.Projects[0].Id) + assert.Assert(t, first.Projects[0].Handle != second.Projects[0].Handle) + + for _, response := range []*CreateSnapshotResponse{first, second} { + _, err = session.handleGetSourceFileNames(context.Background(), &GetSourceFileNamesParams{ + Snapshot: response.Snapshot, + Project: response.Projects[0].Handle, + }) + assert.NilError(t, err) + } + _, err = session.handleGetSourceFileNames(context.Background(), &GetSourceFileNamesParams{ + Snapshot: second.Snapshot, + Project: first.Projects[0].Handle, + }) + assert.ErrorContains(t, err, "not found") +} diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 8072445202b44..c03e7ff85ed46 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -44,7 +44,9 @@ type ( SymbolID uint64 TypeID uint32 SignatureID uint64 - NodeHandle string + // NodeHandle identifies a node in an exact source-file AST. Requests use + // "index.#sourceFileID"; responses use "index.kind.#sourceFileID.path". + NodeHandle string ) var nextBuildOrchestratorId atomic.Uint64 @@ -1030,7 +1032,9 @@ type GetDefaultProjectForFileParams struct { } type ProjectResponse struct { - Id project.ID `json:"id"` + Id project.ID `json:"id"` + // Handle identifies this project's lifetime across snapshots; Id is its logical name. + Handle project.Handle `json:"handle"` ConfigFileName *tspath.RootedFilePath `json:"configFileName,omitempty"` CurrentDirectory tspath.RootedDirectoryPath `json:"currentDirectory"` Dirty bool `json:"dirty"` @@ -1076,6 +1080,7 @@ func NewProjectResponse(p *project.Project) *ProjectResponse { } return &ProjectResponse{ Id: p.ID(), + Handle: p.Handle(), ConfigFileName: configFileName, CurrentDirectory: p.CurrentDirectory(), Dirty: p.IsDirty(), @@ -1085,47 +1090,47 @@ func NewProjectResponse(p *project.Project) *ProjectResponse { type GetSymbolAtPositionParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` Position uint32 `json:"position"` } type GetSymbolsAtPositionsParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` Positions []uint32 `json:"positions"` } type GetSymbolOfSourceFileParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` } type GetSymbolsOfSourceFilesParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` Files []DocumentIdentifier `json:"files"` } type GetSymbolAtLocationParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Location NodeHandle `json:"location"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Location NodeHandle `json:"location"` } type GetSymbolsAtLocationsParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Locations []NodeHandle `json:"locations"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Locations []NodeHandle `json:"locations"` } type SymbolResponse struct { Reference SymbolReference `json:"reference"` Name string `json:"name"` Flags uint32 `json:"flags"` - CheckFlags uint32 `json:"checkFlags"` + CheckFlags uint32 `json:"checkFlags,omitzero"` Declarations []NodeHandle `json:"declarations,omitempty"` ValueDeclaration NodeHandle `json:"valueDeclaration,omitempty"` Parent *CompactSymbolReference `json:"parent,omitempty"` @@ -1143,7 +1148,7 @@ type SymbolOwner struct { Kind SymbolOwnerKind `json:"kind"` File *SourceFileDescriptor `json:"file,omitempty"` Snapshot SnapshotID `json:"snapshot,omitzero"` - Project project.ID `json:"project,omitempty"` + Project project.Handle `json:"project,omitzero"` } // SymbolReference identifies a symbol and its server-resolvable owner. @@ -1163,25 +1168,25 @@ type CompactSymbolReference struct { type GetTypeOfSymbolParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` Symbol SymbolReference `json:"symbol"` } type GetTypesOfSymbolsParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` Symbols []SymbolReference `json:"symbols"` } type TypeResponse struct { Id TypeID `json:"id"` Flags uint32 `json:"flags"` - ObjectFlags uint32 `json:"objectFlags,omitempty"` - IsTupleType bool `json:"isTupleType,omitempty"` + ObjectFlags uint32 `json:"objectFlags,omitzero"` + IsTupleType bool `json:"isTupleType,omitzero"` // Value is literal type data. BigInt literals are encoded as signed decimal - // strings because JSON cannot represent bigint; absent values are null. - Value any `json:"value"` + // strings because JSON cannot represent bigint; absent values are omitted. + Value any `json:"value,omitzero"` // ObjectType / TypeReference / StringMappingType / IndexType target Target TypeID `json:"target,omitzero"` @@ -1223,7 +1228,7 @@ type TypeResponse struct { RegularType TypeID `json:"regularType,omitzero"` // TypeParameter data - IsThisType bool `json:"isThisType,omitempty"` + IsThisType bool `json:"isThisType,omitzero"` // InterfaceType data ThisType TypeID `json:"thisType,omitzero"` @@ -1363,7 +1368,7 @@ type ConstantValueResponse struct { type SignatureResponse struct { Id SignatureID `json:"id"` - Flags uint32 `json:"flags"` + Flags uint32 `json:"flags,omitzero"` Declaration NodeHandle `json:"declaration,omitempty"` TypeParameters []TypeID `json:"typeParameters,omitempty"` Parameters []CompactSymbolReference `json:"parameters,omitempty"` @@ -1373,32 +1378,32 @@ type SignatureResponse struct { type GetSourceFileParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` } type GetSourceFileNamesParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` } type GetModeForUsageLocationParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` Usage NodeHandle `json:"usage"` } type GetModeForResolutionAtIndexParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` Index int `json:"index"` } type GetResolvedModuleParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` ModuleName string `json:"moduleName"` Mode core.ResolutionMode `json:"mode"` @@ -1406,14 +1411,14 @@ type GetResolvedModuleParams struct { type GetResolvedModuleFromModuleSpecifierParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` ModuleSpecifier NodeHandle `json:"moduleSpecifier"` SourceFile *DocumentIdentifier `json:"sourceFile,omitempty"` } type GetResolvedTypeReferenceDirectiveParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` TypeDirectiveName string `json:"typeDirectiveName"` Mode core.ResolutionMode `json:"mode"` @@ -1421,7 +1426,7 @@ type GetResolvedTypeReferenceDirectiveParams struct { type GetResolvedTypeReferenceDirectiveFromReferenceParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` SourceFile DocumentIdentifier `json:"sourceFile"` TypeDirectiveName string `json:"typeDirectiveName"` ResolutionMode core.ResolutionMode `json:"resolutionMode"` @@ -1476,7 +1481,7 @@ type SourceFileMetadata struct { type ResolveNameParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` Name string `json:"name"` Location NodeHandle `json:"location,omitempty"` // Optional: node handle for location context File *DocumentIdentifier `json:"file,omitempty"` // Optional: file for location context (alternative to Location) @@ -1489,7 +1494,7 @@ type ResolveNameParams struct { // all symbols visible at a given location. type GetSymbolsInScopeParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` Location NodeHandle `json:"location,omitempty"` // Optional: node handle for location context File *DocumentIdentifier `json:"file,omitempty"` // Optional: file for location context (alternative to Location) Position *uint32 `json:"position,omitempty"` // Optional: position in file for location context (with File) @@ -1498,9 +1503,9 @@ type GetSymbolsInScopeParams struct { // GetTypePropertyParams is used for all type sub-property endpoints. type GetTypePropertyParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Type TypeID `json:"objectId"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Type TypeID `json:"objectId"` } // GetSymbolPropertyParams is used for all symbol sub-property endpoints. @@ -1510,29 +1515,29 @@ type GetSymbolPropertyParams struct { // GetSignaturePropertyParams is used for all signature sub-property endpoints. type GetSignaturePropertyParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Signature SignatureID `json:"objectId"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Signature SignatureID `json:"objectId"` } // GetContextualTypeParams returns the contextual type for a node. type GetContextualTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Location NodeHandle `json:"location"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Location NodeHandle `json:"location"` } type GetContextualTypeForArgumentParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Location NodeHandle `json:"location"` - Index int32 `json:"index"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Location NodeHandle `json:"location"` + Index int32 `json:"index"` } // GetTypeOfSymbolAtLocationParams returns the narrowed type of a symbol at a specific location. type GetTypeOfSymbolAtLocationParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` Symbol SymbolReference `json:"symbol"` Location NodeHandle `json:"location"` } @@ -1540,17 +1545,17 @@ type GetTypeOfSymbolAtLocationParams struct { // GetReferencesToSymbolInFileParams are the parameters for the getReferencesToSymbolInFile method. type GetReferencesToSymbolInFileParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` Symbol SymbolReference `json:"symbol"` } // GetReferencedSymbolsForNodeParams are the parameters for the getReferencedSymbolsForNode method. type GetReferencedSymbolsForNodeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Node NodeHandle `json:"node"` - Position int `json:"position"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Node NodeHandle `json:"node"` + Position int `json:"position"` } // ReferencedSymbolEntry represents a symbol definition and its references. @@ -1562,9 +1567,9 @@ type ReferencedSymbolEntry struct { // GetSignatureUsagesParams are the parameters for the getSignatureUsages method. type GetSignatureUsagesParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - SignatureDecl NodeHandle `json:"signatureDecl"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + SignatureDecl NodeHandle `json:"signatureDecl"` } // SignatureUsageResponse represents a single usage of a signature as a name-call pair. @@ -1576,7 +1581,7 @@ type SignatureUsageResponse struct { // GetCompletionsAtPositionParams are the parameters for the getCompletionsAtPosition method. type GetCompletionsAtPositionParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` Position uint32 `json:"position"` TriggerCharacter *string `json:"triggerCharacter,omitempty"` @@ -1586,7 +1591,7 @@ type GetCompletionsAtPositionParams struct { // FormatNodeForInsertionParams are the parameters for the formatNodeForInsertion method. type FormatNodeForInsertionParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` // target file where the node will be inserted Position uint32 `json:"position"` // UTF-16 code-unit offset of the insertion position in the target file Data string `json:"data"` // base64-encoded binary AST data for the synthesized node @@ -1618,8 +1623,8 @@ type CompletionInfoResponse struct { // GetIntrinsicTypeParams is used for intrinsic type getters (anyType, stringType, etc.). type GetIntrinsicTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` } // WellKnownSymbolsResponse carries the handle ids of the per-checker singleton @@ -1640,90 +1645,90 @@ type WellKnownSignaturesResponse struct { // GetBaseTypeOfLiteralTypeParams returns the base type of a literal type. type GetBaseTypeOfLiteralTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Type TypeID `json:"type"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Type TypeID `json:"type"` } // GetNonNullableTypeParams are the parameters for the getNonNullableType method. type GetNonNullableTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Type TypeID `json:"type"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Type TypeID `json:"type"` } // GetTypeFromTypeNodeParams are the parameters for the getTypeFromTypeNode method. type GetTypeFromTypeNodeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Location NodeHandle `json:"location"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Location NodeHandle `json:"location"` } // GetWidenedTypeParams are the parameters for the getWidenedType method. type GetWidenedTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Type TypeID `json:"type"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Type TypeID `json:"type"` } // GetParameterTypeParams are the parameters for the getParameterType method. type GetParameterTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Signature SignatureID `json:"signature"` - Index int32 `json:"index"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Signature SignatureID `json:"signature"` + Index int32 `json:"index"` } // IsArrayLikeTypeParams checks whether a type is array-like. type IsArrayLikeTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Type TypeID `json:"type"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Type TypeID `json:"type"` } // IsTypeAssignableToParams checks assignability between two types. type IsTypeAssignableToParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Source TypeID `json:"source"` - Target TypeID `json:"target"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Source TypeID `json:"source"` + Target TypeID `json:"target"` } type GetSignaturesOfTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Type TypeID `json:"type"` - Kind int32 `json:"kind"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Type TypeID `json:"type"` + Kind int32 `json:"kind"` } type GetResolvedSignatureParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Location NodeHandle `json:"location"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Location NodeHandle `json:"location"` } type GetTypeAtLocationParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Location NodeHandle `json:"location"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Location NodeHandle `json:"location"` } type GetTypeAtLocationsParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Locations []NodeHandle `json:"locations"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Locations []NodeHandle `json:"locations"` } type GetTypeAtPositionParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` Position uint32 `json:"position"` } type GetTypesAtPositionsParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` Positions []uint32 `json:"positions"` } @@ -1742,7 +1747,7 @@ type ImportAdderAction struct { type GetImportAdderEditsParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` File DocumentIdentifier `json:"file"` Actions []ImportAdderAction `json:"actions"` } @@ -1755,21 +1760,21 @@ type TextEdit struct { // TypeToTypeNodeParams are the parameters for the typeToTypeNode method. type TypeToTypeNodeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Type TypeID `json:"type"` - Location NodeHandle `json:"location,omitempty"` - Flags int32 `json:"flags,omitempty"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Type TypeID `json:"type"` + Location NodeHandle `json:"location,omitempty"` + Flags int32 `json:"flags,omitempty"` } // SignatureToSignatureDeclarationParams are the parameters for the signatureToSignatureDeclaration method. type SignatureToSignatureDeclarationParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Signature SignatureID `json:"signature"` - Kind int32 `json:"kind"` - Location NodeHandle `json:"location,omitempty"` - Flags int32 `json:"flags,omitempty"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Signature SignatureID `json:"signature"` + Kind int32 `json:"kind"` + Location NodeHandle `json:"location,omitempty"` + Flags int32 `json:"flags,omitempty"` } // PrintNodeParams are the parameters for the printNode method. @@ -1781,14 +1786,14 @@ type PrintNodeParams struct { } type EmitParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - EmitOnly *uint32 `json:"emitOnly,omitempty"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + EmitOnly *uint32 `json:"emitOnly,omitempty"` } type SelectedFilesEmitParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` Files []DocumentIdentifier `json:"files"` } @@ -1815,45 +1820,45 @@ type EmitOutputResponse struct { // CheckerTypeParams are parameters for checker methods that operate on a type. type CheckerTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Type TypeID `json:"type"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Type TypeID `json:"type"` } // GetPropertyOfTypeParams are parameters for getPropertyOfType (a named property of a type). type GetPropertyOfTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Type TypeID `json:"type"` - Name string `json:"name"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Type TypeID `json:"type"` + Name string `json:"name"` } type GetIndexInfoOfTypeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Type TypeID `json:"type"` - Kind int32 `json:"kind"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Type TypeID `json:"type"` + Kind int32 `json:"kind"` } // GetMemberInModuleExportsParams are parameters for getMemberInModuleExports. type GetMemberInModuleExportsParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` Symbol SymbolReference `json:"symbol"` Name string `json:"name"` } // CheckerNodeParams are parameters for checker methods that operate on a node location. type CheckerNodeParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Location NodeHandle `json:"location"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Location NodeHandle `json:"location"` } // CheckerSymbolParams are parameters for checker methods that operate on a symbol. type CheckerSymbolParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` Symbol SymbolReference `json:"symbol"` } @@ -1866,9 +1871,9 @@ type JSDocTagInfo struct { // CheckerSignatureParams are parameters for checker methods that operate on a signature. type CheckerSignatureParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` - Signature SignatureID `json:"signature"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + Signature SignatureID `json:"signature"` } // TypePredicateResponse is the response for getTypePredicateOfSignature. @@ -1897,14 +1902,14 @@ type SourceFileResponse struct { // GetDiagnosticsParams are parameters for per-file diagnostic methods. type GetDiagnosticsParams struct { Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Project project.Handle `json:"project"` Files []DocumentIdentifier `json:"files,omitempty"` } // GetProjectDiagnosticsParams are parameters for project-wide diagnostic methods. type GetProjectDiagnosticsParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.ID `json:"project"` + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` } // DiagnosticResponse is the API response for a single diagnostic. diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 93d6ab53c303d..cef38f0453201 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -121,9 +121,9 @@ type snapshotData struct { // a project context (e.g. member/export ordering, node handle resolution) but don't // receive one from the caller default to this canonical project. First-writer wins so // the choice is stable. Guarded by symbolRegistryMu. - symbolCanonicalProjects map[SymbolID]project.ID + symbolCanonicalProjects map[SymbolID]project.Handle - projectRegistries map[project.ID]*projectRegistryData + projectRegistries map[project.Handle]*projectRegistryData projectRegistriesMu sync.RWMutex } @@ -147,7 +147,7 @@ type projectRegistryData struct { } // getProgram looks up a program from a project handle within this snapshot. -func (sd *snapshotData) getProgram(projectHandle project.ID) (*compiler.Program, error) { +func (sd *snapshotData) getProgram(projectHandle project.Handle) (*compiler.Program, error) { proj, err := sd.getProject(projectHandle) if err != nil { return nil, err @@ -161,23 +161,23 @@ func (sd *snapshotData) getProgram(projectHandle project.ID) (*compiler.Program, } // getProject looks up a project from a project handle within this snapshot. -func (sd *snapshotData) getProject(projectHandle project.ID) (*project.Project, error) { - proj := sd.snapshot.ProjectCollection.GetProject(projectHandle) +func (sd *snapshotData) getProject(projectHandle project.Handle) (*project.Project, error) { + proj := sd.snapshot.ProjectCollection.GetProjectByHandle(projectHandle) if proj == nil { - return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectHandle) + return nil, fmt.Errorf("%w: project %d not found", ErrClientError, projectHandle) } return proj, nil } -// nodeHandleFrom creates an index-based node handle (index.kind.path), building a node index table +// nodeHandleFrom creates an exact-AST response handle (index.kind.#fileID.path), building a node index table // for the file on-demand if needed. func (sd *snapshotData) nodeHandleFrom(node *ast.Node) NodeHandle { return nodeHandleFrom(node) } // getOrCreateProjectRegistry returns the registry for the given project, creating it if needed. -func (sd *snapshotData) getOrCreateProjectRegistry(projectID project.ID) *projectRegistryData { - if projectID == "" { +func (sd *snapshotData) getOrCreateProjectRegistry(projectID project.Handle) *projectRegistryData { + if projectID == 0 { panic("getOrCreateProjectRegistry: empty project ID") } // Fast path: registry already exists — read lock only. @@ -216,7 +216,7 @@ func symbolOwnerFile(symbol *ast.Symbol) *ast.SourceFile { // newSymbolResponse classifies a symbol's ownership before exposing its identity to a client. // Only snapshot-owned symbols are registered in the snapshot; file-owned symbols are resolved // through their source file. -func (sd *snapshotData) newSymbolResponse(symbol *ast.Symbol, canonicalProject project.ID) *SymbolResponse { +func (sd *snapshotData) newSymbolResponse(symbol *ast.Symbol, canonicalProject project.Handle) *SymbolResponse { if symbol == nil { return nil } @@ -295,7 +295,7 @@ func nodeHandleFrom(node *ast.Node) NodeHandle { sourceFile := ast.GetSourceFileOfNode(node) table := encoder.GetNodeIndexTable(sourceFile) idx := table.GetIndex(node) - return NodeHandle(fmt.Sprintf("%d.%d.%s", idx, node.Kind, sourceFile.PathKey())) + return NodeHandle(fmt.Sprintf("%d.%d.#%d.%s", idx, node.Kind, sourceFileNodeID(sourceFile), sourceFile.PathKey())) } // registerSymbol registers a symbol in the snapshot's registry and returns its handle along with @@ -303,11 +303,11 @@ func nodeHandleFrom(node *ast.Node) NodeHandle { // (first writer wins for stability) and is always non-empty: every symbol handed to a client must // carry a project so that project-scoped follow-up lookups (members/exports, parent, node // resolution) have a default context. Callers must supply a non-empty project. -func (sd *snapshotData) registerSymbol(symbol *ast.Symbol, canonicalProject project.ID) (SymbolID, project.ID) { +func (sd *snapshotData) registerSymbol(symbol *ast.Symbol, canonicalProject project.Handle) (SymbolID, project.Handle) { if symbol == nil { - return 0, project.ID("") + return 0, project.Handle(0) } - if canonicalProject == "" { + if canonicalProject == 0 { panic("registerSymbol requires a non-empty canonical project") } id := SymbolHandle(symbol) @@ -330,7 +330,7 @@ func (sd *snapshotData) registerSymbol(symbol *ast.Symbol, canonicalProject proj } // newTypeResponse registers a type in the project's registry and returns the response. -func (sd *snapshotData) newTypeResponse(projectID project.ID, t *checker.Type, c *checker.Checker) *TypeResponse { +func (sd *snapshotData) newTypeResponse(projectID project.Handle, t *checker.Type, c *checker.Checker) *TypeResponse { if t == nil { return nil } @@ -361,7 +361,7 @@ func (sd *snapshotData) newTypeResponse(projectID project.ID, t *checker.Type, c return resp } -func (sd *snapshotData) registerType(projectID project.ID, t *checker.Type) TypeID { +func (sd *snapshotData) registerType(projectID project.Handle, t *checker.Type) TypeID { if t == nil { return 0 } @@ -401,46 +401,46 @@ func (sd *snapshotData) resolveSymbolHandle(handle SymbolID) (*ast.Symbol, error // resolveSymbolReference resolves a symbol without a semantic context. A file reference holds the // exact cached AST until the returned release function is called; a snapshot reference also returns // the snapshot and canonical project that own the symbol. -func (s *Session) resolveSymbolReference(ref SymbolReference) (*ast.Symbol, *snapshotData, project.ID, func(), error) { +func (s *Session) resolveSymbolReference(ref SymbolReference) (*ast.Symbol, *snapshotData, project.Handle, func(), error) { switch ref.Kind { case SymbolOwnerKindFile: - if ref.File == nil || ref.Snapshot != 0 || ref.Project != "" { - return nil, nil, "", nil, fmt.Errorf("%w: invalid file symbol reference", ErrClientError) + if ref.File == nil || ref.Snapshot != 0 || ref.Project != 0 { + return nil, nil, 0, nil, fmt.Errorf("%w: invalid file symbol reference", ErrClientError) } lease, err := s.acquireCachedSourceFile(*ref.File) if err != nil { - return nil, nil, "", nil, err + return nil, nil, 0, nil, err } symbol := getSourceFileSymbolIndex(lease.SourceFile())[ref.Id] if symbol == nil { lease.Release() - return nil, nil, "", nil, fmt.Errorf("%w: symbol %d not found in source file", ErrClientError, ref.Id) + return nil, nil, 0, nil, fmt.Errorf("%w: symbol %d not found in source file", ErrClientError, ref.Id) } - return symbol, nil, "", lease.Release, nil + return symbol, nil, 0, lease.Release, nil case SymbolOwnerKindSnapshot: - if ref.File != nil || ref.Snapshot == 0 || ref.Project == "" { - return nil, nil, "", nil, fmt.Errorf("%w: invalid snapshot symbol reference", ErrClientError) + if ref.File != nil || ref.Snapshot == 0 || ref.Project == 0 { + return nil, nil, 0, nil, fmt.Errorf("%w: invalid snapshot symbol reference", ErrClientError) } sd, err := s.getSnapshotData(ref.Snapshot) if err != nil { - return nil, nil, "", nil, err + return nil, nil, 0, nil, err } symbol, err := sd.resolveSymbolHandle(ref.Id) if err != nil { - return nil, nil, "", nil, err + return nil, nil, 0, nil, err } return symbol, sd, ref.Project, func() {}, nil default: - return nil, nil, "", nil, fmt.Errorf("%w: invalid symbol reference kind %d", ErrClientError, ref.Kind) + return nil, nil, 0, nil, fmt.Errorf("%w: invalid symbol reference kind %d", ErrClientError, ref.Kind) } } // resolveTypeHandle resolves a type handle within the project's registry. -func (sd *snapshotData) resolveTypeHandle(projectID project.ID, handle TypeID) (*checker.Type, error) { +func (sd *snapshotData) resolveTypeHandle(projectID project.Handle, handle TypeID) (*checker.Type, error) { if handle == 0 { return nil, fmt.Errorf("%w: empty type handle", ErrClientError) } - if projectID == "" { + if projectID == 0 { return nil, fmt.Errorf("%w: empty project ID for type handle %d", ErrClientError, handle) } @@ -449,7 +449,7 @@ func (sd *snapshotData) resolveTypeHandle(projectID project.ID, handle TypeID) ( sd.projectRegistriesMu.RUnlock() if reg == nil { - return nil, fmt.Errorf("%w: type handle %d not found (no registry for project %s)", ErrClientError, handle, projectID) + return nil, fmt.Errorf("%w: type handle %d not found (no registry for project %d)", ErrClientError, handle, projectID) } reg.typeRegistryMu.RLock() @@ -464,11 +464,11 @@ func (sd *snapshotData) resolveTypeHandle(projectID project.ID, handle TypeID) ( } // resolveSignatureHandle resolves a signature handle within the project's registry. -func (sd *snapshotData) resolveSignatureHandle(projectID project.ID, handle SignatureID) (*checker.Signature, error) { +func (sd *snapshotData) resolveSignatureHandle(projectID project.Handle, handle SignatureID) (*checker.Signature, error) { if handle == 0 { return nil, fmt.Errorf("%w: empty signature handle", ErrClientError) } - if projectID == "" { + if projectID == 0 { return nil, fmt.Errorf("%w: empty project ID for signature handle %d", ErrClientError, handle) } @@ -477,7 +477,7 @@ func (sd *snapshotData) resolveSignatureHandle(projectID project.ID, handle Sign sd.projectRegistriesMu.RUnlock() if reg == nil { - return nil, fmt.Errorf("%w: signature handle %d not found (no registry for project %s)", ErrClientError, handle, projectID) + return nil, fmt.Errorf("%w: signature handle %d not found (no registry for project %d)", ErrClientError, handle, projectID) } reg.signatureRegistryMu.RLock() @@ -492,7 +492,7 @@ func (sd *snapshotData) resolveSignatureHandle(projectID project.ID, handle Sign } // newSignatureResponse registers a signature in the project's registry and returns the response. -func (sd *snapshotData) newSignatureResponse(projectID project.ID, sig *checker.Signature) *SignatureResponse { +func (sd *snapshotData) newSignatureResponse(projectID project.Handle, sig *checker.Signature) *SignatureResponse { if sig == nil { return nil } @@ -527,7 +527,7 @@ func (sd *snapshotData) newSignatureResponse(projectID project.ID, sig *checker. return resp } -func (sd *snapshotData) registerSignature(projectID project.ID, sig *checker.Signature) SignatureID { +func (sd *snapshotData) registerSignature(projectID project.Handle, sig *checker.Signature) SignatureID { if sig == nil { return 0 } @@ -747,7 +747,7 @@ type checkerSetup struct { program *compiler.Program checker *checker.Checker done func() - projectID project.ID + projectID project.Handle } func (setup checkerSetup) newSymbolResponse(sym *ast.Symbol) *SymbolResponse { @@ -784,7 +784,7 @@ func (setup checkerSetup) resolveSymbolHandle(ref SymbolReference) (*ast.Symbol, } return setup.sd.resolveSymbolHandle(ref.Id) } else if ref.Kind == SymbolOwnerKindFile { - if ref.File == nil || ref.Snapshot != 0 || ref.Project != "" { + if ref.File == nil || ref.Snapshot != 0 || ref.Project != 0 { return nil, fmt.Errorf("%w: invalid file symbol reference", ErrClientError) } sourceFile := setup.program.GetSourceFileByPath(ref.File.Path) @@ -823,7 +823,7 @@ func (setup checkerSetup) resolveLocation(handle NodeHandle, file *DocumentIdent // setupChecker resolves snapshot, program, and type checker for a project. // Callers must defer setup.done() to release the checker. -func (s *Session) setupChecker(ctx context.Context, snapshot SnapshotID, projectHandle project.ID) (checkerSetup, error) { +func (s *Session) setupChecker(ctx context.Context, snapshot SnapshotID, projectHandle project.Handle) (checkerSetup, error) { sd, err := s.getSnapshotData(snapshot) if err != nil { return checkerSetup{}, err @@ -856,10 +856,10 @@ func (s *Session) setupChecker(ctx context.Context, snapshot SnapshotID, project // are produced on the persistent API checker and stay resolvable. Only safe when the // LS operation acquires a checker exactly once; nested acquisitions (e.g. find-all- // references) would deadlock on the single-slot persistent checker. -func (s *Session) setupLanguageService(snapshot *project.Snapshot, program *compiler.Program, projectHandle project.ID, activeFile string) (*ls.LanguageService, error) { - proj := snapshot.ProjectCollection.GetProject(projectHandle) +func (s *Session) setupLanguageService(snapshot *project.Snapshot, program *compiler.Program, projectHandle project.Handle, activeFile string) (*ls.LanguageService, error) { + proj := snapshot.ProjectCollection.GetProjectByHandle(projectHandle) if proj == nil { - return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectHandle) + return nil, fmt.Errorf("%w: project %d not found", ErrClientError, projectHandle) } return ls.NewLanguageService(proj.ID(), program, snapshot, activeFile), nil } @@ -1780,8 +1780,8 @@ func (s *Session) registerSnapshot(snapshot *project.Snapshot, openState snapsho openProjects: *openState.openProjects.Clone(), openFiles: *openState.openFiles.Clone(), symbolRegistry: make(map[SymbolID]*ast.Symbol), - symbolCanonicalProjects: make(map[SymbolID]project.ID), - projectRegistries: make(map[project.ID]*projectRegistryData), + symbolCanonicalProjects: make(map[SymbolID]project.Handle), + projectRegistries: make(map[project.Handle]*projectRegistryData), } s.snapshots[handle] = sd } @@ -3227,6 +3227,11 @@ func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImpo } projectID := params.Project + proj, err := sd.getProject(projectID) + if err != nil { + return nil, err + } + logicalProjectID := proj.ID() workingSnapshot := sd.snapshot program, err := sd.getProgram(params.Project) if err != nil { @@ -3239,7 +3244,7 @@ func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImpo userPreferences := workingSnapshot.UserPreferences() if registry := workingSnapshot.AutoImportRegistry(); registry == nil || - !registry.IsPreparedForImportingFile(sourceFile.FileName(), projectID, userPreferences) { + !registry.IsPreparedForImportingFile(sourceFile.FileName(), logicalProjectID, userPreferences) { preparedSnapshot := s.snapshotHost.CloneSnapshotWithAutoImports(ctx, workingSnapshot, lsconv.FileNameToDocumentURI(sourceFile.FileName()), nil) if s.projectSession != nil { s.projectSession.TryAdoptSnapshotInBackground(workingSnapshot, preparedSnapshot) @@ -3247,9 +3252,9 @@ func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImpo defer preparedSnapshot.Deref() workingSnapshot = preparedSnapshot - proj := workingSnapshot.ProjectCollection.GetProject(projectID) + proj := workingSnapshot.ProjectCollection.GetProjectByHandle(projectID) if proj == nil { - return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectID) + return nil, fmt.Errorf("%w: project %d not found", ErrClientError, projectID) } program = proj.GetProgram() if program == nil { @@ -3273,7 +3278,7 @@ func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImpo view := autoimport.NewView( registry, sourceFile, - projectID, + logicalProjectID, program, ch, userPreferences.ModuleSpecifierPreferences(), @@ -4098,7 +4103,7 @@ func (s *Session) getEmitOptions(params *EmitParams) (*compiler.Program, compile }, nil } -func (s *Session) getEmitProgram(snapshot SnapshotID, projectID project.ID) (*compiler.Program, error) { +func (s *Session) getEmitProgram(snapshot SnapshotID, projectID project.Handle) (*compiler.Program, error) { sd, err := s.getSnapshotData(snapshot) if err != nil { return nil, err @@ -5006,27 +5011,24 @@ func (s *Session) handleGetFalseTypeOfConditionalType(ctx context.Context, param func (sd *snapshotData) resolveNodeHandle(program *compiler.Program, handle NodeHandle) (*ast.Node, error) { s := string(handle) - // Format: "index.kind.path" — we need index and path, kind is informational only. - firstDot := strings.IndexByte(s, '.') - if firstDot == -1 { - return nil, fmt.Errorf("%w: invalid node handle %q", ErrClientError, handle) - } - secondDot := strings.IndexByte(s[firstDot+1:], '.') - if secondDot == -1 { + // Request format: "index.#sourceFileID". + indexText, fileKey, ok := strings.Cut(s, ".") + if !ok { return nil, fmt.Errorf("%w: invalid node handle %q", ErrClientError, handle) } - secondDot += firstDot + 1 // adjust to absolute index - - idx, err := strconv.ParseUint(s[:firstDot], 10, 32) + idx, err := strconv.ParseUint(indexText, 10, 32) if err != nil { return nil, fmt.Errorf("%w: invalid node handle %q: %w", ErrClientError, handle, err) } - path, ok := tspath.TryPathKeyFromCanonical(s[secondDot+1:]) - if !ok { + + if !strings.HasPrefix(fileKey, "#") { return nil, fmt.Errorf("%w: invalid node handle %q", ErrClientError, handle) } - - sourceFile := program.GetSourceFileByPath(path) + nodeID, err := strconv.ParseUint(fileKey[1:], 10, 64) + if err != nil || nodeID == 0 { + return nil, fmt.Errorf("%w: invalid node handle %q", ErrClientError, handle) + } + sourceFile := program.GetSourceFileByNodeID(ast.NodeId(nodeID)) if sourceFile == nil { return nil, fmt.Errorf("%w: node handle %q could not be resolved (file may not be loaded or handle may be stale)", ErrClientError, handle) } @@ -5521,9 +5523,9 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge return nil, err } projectID := params.Project - proj := preparedSnapshot.ProjectCollection.GetProject(projectID) + proj := preparedSnapshot.ProjectCollection.GetProjectByHandle(projectID) if proj == nil { - return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectID) + return nil, fmt.Errorf("%w: project %d not found", ErrClientError, projectID) } program = proj.GetProgram() if program == nil { diff --git a/tsc/internal/api/session_apistate_test.go b/tsc/internal/api/session_apistate_test.go index 05407b492dd9d..585e2aa3442fc 100644 --- a/tsc/internal/api/session_apistate_test.go +++ b/tsc/internal/api/session_apistate_test.go @@ -52,6 +52,8 @@ func TestGetCurrentLanguageServerSnapshotAdoptsChanges(t *testing.T) { assert.NilError(t, err) assert.Equal(t, session.openProjects.Len(), 1) assert.Equal(t, response.Snapshot, snapshotHandle(projectSession.Snapshot())) + projectHandle := response.Projects[0].Handle + assert.Assert(t, projectHandle != 0) assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKeyFromCanonical(configFileName)) != nil) assert.NilError(t, utils.FS().WriteFile(fileName, `export const x = 2;`)) projectSession.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{{ @@ -70,6 +72,7 @@ func TestGetCurrentLanguageServerSnapshotAdoptsChanges(t *testing.T) { }) assert.NilError(t, err) assert.Equal(t, unchanged.Projects[0].Dirty, false) + assert.Equal(t, unchanged.Projects[0].Handle, projectHandle) assert.Equal(t, session.openProjects.Len(), 1) removed, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ @@ -82,6 +85,10 @@ func TestGetCurrentLanguageServerSnapshotAdoptsChanges(t *testing.T) { assert.Equal(t, len(removed.Projects), 0) assert.DeepEqual(t, removed.Changes.RemovedProjects, []project.ID{configuredProjectID(configFileName)}) assert.Equal(t, session.openProjects.Len(), 0) + _, err = session.snapshots[removed.Snapshot].getProject(projectHandle) + assert.ErrorContains(t, err, "not found") + _, err = session.snapshots[response.Snapshot].getProject(projectHandle) + assert.NilError(t, err) session.Close() assert.Equal(t, session.openProjects.Len(), 0) @@ -303,7 +310,7 @@ func TestOpenFilePreservesWindowsDriveLetterCase(t *testing.T) { project := response.Projects[0] snapshot, err := session.getSnapshotData(response.Snapshot) assert.NilError(t, err) - program, err := snapshot.getProgram(project.Id) + program, err := snapshot.getProgram(project.Handle) assert.NilError(t, err) assert.Equal(t, program.GetSourceFile(fileName).FileName(), tspath.RootedFilePathFromNormalized(fileName)) } @@ -524,9 +531,11 @@ func TestOpenProjectsWithCircularExtendsReportCircularity(t *testing.T) { OpenProjects: []DocumentIdentifier{{FileName: second}}, }) assert.NilError(t, err) + secondProject := session.snapshots[response.Snapshot].snapshot.ProjectCollection.GetProject(configuredProjectID(second)) + assert.Assert(t, secondProject != nil) diagnostics, err := session.handleGetConfigFileParsingDiagnostics(context.Background(), &GetProjectDiagnosticsParams{ Snapshot: response.Snapshot, - Project: configuredProjectID(second), + Project: secondProject.Handle(), }) assert.NilError(t, err) codes := make([]int32, 0, len(diagnostics)) diff --git a/tsc/internal/api/session_batch_test.go b/tsc/internal/api/session_batch_test.go index d5fc1a8aa0d7d..9a28caca19fc0 100644 --- a/tsc/internal/api/session_batch_test.go +++ b/tsc/internal/api/session_batch_test.go @@ -42,7 +42,7 @@ func TestHandleBatchRequestsRecoversPerRequestPanics(t *testing.T) { response, err := session.handleBatchRequests(context.Background(), &BatchRequestsParams{ Requests: []BatchRequest{ {Method: "ping", Params: json.Value{}}, - {Method: MethodGetAnyType, Params: json.Value(`{"snapshot":1,"project":"project"}`)}, + {Method: MethodGetAnyType, Params: json.Value(`{"snapshot":1,"project":1}`)}, {Method: "ping", Params: json.Value{}}, }, }) diff --git a/tsc/internal/api/session_completion_test.go b/tsc/internal/api/session_completion_test.go index 91c44ece0de93..ca415027f491b 100644 --- a/tsc/internal/api/session_completion_test.go +++ b/tsc/internal/api/session_completion_test.go @@ -59,7 +59,7 @@ func TestCompletionSymbolTypeIsResolvable(t *testing.T) { // content is pure ASCII, so the UTF-16 caret offset equals the byte length. completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{ Snapshot: snapshotResp.Snapshot, - Project: proj.Id, + Project: proj.Handle, File: DocumentIdentifier{FileName: fileName}, Position: uint32(len(content)), IncludeSymbol: true, @@ -77,7 +77,7 @@ func TestCompletionSymbolTypeIsResolvable(t *testing.T) { sawSymbol = true typeResp, err := session.handleGetTypeOfSymbol(t.Context(), &GetTypeOfSymbolParams{ Snapshot: snapshotResp.Snapshot, - Project: proj.Id, + Project: proj.Handle, Symbol: entry.Symbol.Reference, }) assert.NilError(t, err) @@ -132,7 +132,7 @@ func TestCompletionOnInferredProject(t *testing.T) { // content is pure ASCII, so the UTF-16 caret offset equals the byte length. completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{ Snapshot: snapshotResp.Snapshot, - Project: proj.Id, + Project: proj.Handle, File: DocumentIdentifier{FileName: fileName}, Position: uint32(len(content)), }) @@ -175,7 +175,7 @@ func TestCompletionRetriesWithAutoImports(t *testing.T) { completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{ Snapshot: snapshotResp.Snapshot, - Project: proj.Id, + Project: proj.Handle, File: DocumentIdentifier{FileName: fileName}, Position: uint32(len(content)), }) @@ -222,7 +222,7 @@ func TestCompletionWithSymbolsRequiresPreparedAutoImports(t *testing.T) { _, err = session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{ Snapshot: snapshotResp.Snapshot, - Project: proj.Id, + Project: proj.Handle, File: DocumentIdentifier{FileName: fileName}, Position: uint32(len(content)), IncludeSymbol: true, @@ -264,7 +264,7 @@ func TestCompletionWithSymbolsUsesPreparedSnapshot(t *testing.T) { completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{ Snapshot: snapshotResp.Snapshot, - Project: proj.Id, + Project: proj.Handle, File: DocumentIdentifier{FileName: fileName}, Position: uint32(len(content)), IncludeSymbol: true, @@ -312,7 +312,7 @@ func TestCompletionUsesSnapshotPreferences(t *testing.T) { completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{ Snapshot: snapshotResp.Snapshot, - Project: proj.Id, + Project: proj.Handle, File: DocumentIdentifier{FileName: fileName}, Position: uint32(len(content)), IncludeSymbol: true, @@ -403,7 +403,7 @@ func TestPreparedIndependentSnapshotPreservesLSPOverlays(t *testing.T) { assert.Equal(t, proj.GetProgram().GetSourceFile(fileName).Text(), "packageV") completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{ Snapshot: prepared.Snapshot, - Project: proj.ID(), + Project: proj.Handle(), File: DocumentIdentifier{FileName: fileName}, Position: 8, IncludeSymbol: true, @@ -449,7 +449,7 @@ func TestFreshSnapshotIncludesDependencyAutoImports(t *testing.T) { proj := snapshot.GetDefaultProject("file:///home/projects/MixedCase/index.ts") completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{ Snapshot: response.Snapshot, - Project: proj.ID(), + Project: proj.Handle(), File: DocumentIdentifier{FileName: fileName}, Position: 8, IncludeSymbol: prepare, @@ -512,7 +512,7 @@ func TestCompletionWithSymbolsAndExistingImportDoesNotDeadlock(t *testing.T) { go func() { completions, e := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{ Snapshot: snapshotResp.Snapshot, - Project: proj.Id, + Project: proj.Handle, File: DocumentIdentifier{FileName: fileName}, Position: uint32(len(content)), IncludeSymbol: true, diff --git a/tsc/internal/api/session_createprogram_test.go b/tsc/internal/api/session_createprogram_test.go index 54709aec92d6a..809979d5f7440 100644 --- a/tsc/internal/api/session_createprogram_test.go +++ b/tsc/internal/api/session_createprogram_test.go @@ -108,7 +108,7 @@ func TestCreateSnapshotPreservesWindowsRootDriveLetterCase(t *testing.T) { snapshot, err := session.getSnapshotData(response.Snapshot) assert.NilError(t, err) - program, err := snapshot.getProgram(response.Projects[0].Id) + program, err := snapshot.getProgram(response.Projects[0].Handle) assert.NilError(t, err) assert.Equal(t, program.GetSourceFile(fileName).FileName(), tspath.RootedFilePathFromNormalized(fileName)) } @@ -304,7 +304,7 @@ func TestCreateProgramReportsNonCompositeProjectReference(t *testing.T) { }}, }) assert.NilError(t, err) - projectID := (*response.Operation.CreatedPrograms)[0].AsID() + projectID := session.snapshots[response.Snapshot].snapshot.ProjectCollection.GetProject((*response.Operation.CreatedPrograms)[0].AsID()).Handle() diagnostics, err := session.handleGetProgramDiagnostics(context.Background(), &GetProjectDiagnosticsParams{ Snapshot: response.Snapshot, Project: projectID, diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index 635b3f5e44ddb..ccffe90b69709 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -172,7 +172,7 @@ func TestCreateProgramUsesStaticModuleResolutions(t *testing.T) { }, }) assert.NilError(t, err) - projectID := (*response.Operation.CreatedPrograms)[0].AsID() + projectID := session.snapshots[response.Snapshot].snapshot.ProjectCollection.GetProject((*response.Operation.CreatedPrograms)[0].AsID()).Handle() fileNames, err := session.handleGetSourceFileNames(context.Background(), &GetSourceFileNamesParams{ Snapshot: response.Snapshot, Project: projectID, @@ -231,7 +231,7 @@ func TestCustomModuleResolutionsSkipUnsafeRewriteDiagnostic(t *testing.T) { assert.NilError(t, err) diagnostics, err := session.handleGetSemanticDiagnostics(t.Context(), &GetDiagnosticsParams{ Snapshot: response.Snapshot, - Project: (*response.Operation.CreatedPrograms)[0].AsID(), + Project: session.snapshots[response.Snapshot].snapshot.ProjectCollection.GetProject((*response.Operation.CreatedPrograms)[0].AsID()).Handle(), Files: []DocumentIdentifier{{FileName: root}}, }) assert.NilError(t, err) diff --git a/tsc/internal/api/session_requestfilesystem_test.go b/tsc/internal/api/session_requestfilesystem_test.go index dbd9f4d984953..bf0311e577215 100644 --- a/tsc/internal/api/session_requestfilesystem_test.go +++ b/tsc/internal/api/session_requestfilesystem_test.go @@ -523,7 +523,7 @@ func TestCreateProgramRetainsFullFileSystem(t *testing.T) { snapshot, err := session.getSnapshotData(created.Snapshot) assert.NilError(t, err) - program, err := snapshot.getProgram(project.ID((*created.Operation.CreatedPrograms)[0])) + program, err := snapshot.getProgram(snapshot.snapshot.ProjectCollection.GetProject((*created.Operation.CreatedPrograms)[0].AsID()).Handle()) assert.NilError(t, err) assert.Assert(t, program.GetSourceFile("/new.ts") != nil) } @@ -753,7 +753,7 @@ func TestEmitFromLayerOverFullFileSystemReturnsFileContents(t *testing.T) { emitted, err := session.handleEmit(ctx, &EmitParams{ Snapshot: layered.Snapshot, - Project: base.Projects[0].Id, + Project: base.Projects[0].Handle, }) assert.NilError(t, err) assert.DeepEqual(t, emitted.EmittedFiles, []tspath.RootedFilePath{"/out/src/main.js"}) @@ -763,7 +763,7 @@ func TestEmitFromLayerOverFullFileSystemReturnsFileContents(t *testing.T) { assert.NilError(t, err) emittedAfterRelease, err := session.handleEmit(ctx, &EmitParams{ Snapshot: layered.Snapshot, - Project: base.Projects[0].Id, + Project: base.Projects[0].Handle, }) assert.NilError(t, err) assert.DeepEqual(t, emittedAfterRelease.EmittedFiles, emitted.EmittedFiles) diff --git a/tsc/internal/api/session_symbolresponse_test.go b/tsc/internal/api/session_symbolresponse_test.go index ca60a1bb1fbf4..90b19bc927beb 100644 --- a/tsc/internal/api/session_symbolresponse_test.go +++ b/tsc/internal/api/session_symbolresponse_test.go @@ -30,7 +30,7 @@ func newTestSnapshotData() *snapshotData { return &snapshotData{ handle: 1, symbolRegistry: make(map[SymbolID]*ast.Symbol), - symbolCanonicalProjects: make(map[SymbolID]project.ID), + symbolCanonicalProjects: make(map[SymbolID]project.Handle), } } @@ -42,6 +42,7 @@ func TestFileSymbolResponseSerializesDescriptorOnce(t *testing.T) { encoded, err := json.Marshal(response) assert.NilError(t, err) assert.Equal(t, strings.Count(string(encoded), `"contentHash"`), 1) + assert.Assert(t, !strings.Contains(string(encoded), `"project"`)) } func TestFileOwnedSymbolsAreNotRegisteredInSnapshot(t *testing.T) { @@ -49,7 +50,7 @@ func TestFileOwnedSymbolsAreNotRegisteredInSnapshot(t *testing.T) { sourceFile := parseAndBind(t, "/file.ts", `export class C { property = 1 }`) sd := newTestSnapshotData() - response := sd.newSymbolResponse(sourceFile.Statements.Nodes[0].Symbol(), "/tsconfig.json") + response := sd.newSymbolResponse(sourceFile.Statements.Nodes[0].Symbol(), 1) assert.Equal(t, response.Reference.Kind, SymbolOwnerKindFile) assert.Equal(t, len(sd.symbolRegistry), 0) assert.Equal(t, len(sd.symbolCanonicalProjects), 0) @@ -65,7 +66,7 @@ func TestTransientSymbolWithFileDeclarationIsSnapshotOwned(t *testing.T) { symbol.SetDeclarations([]*ast.Node{class}) sd := newTestSnapshotData() - response := sd.newSymbolResponse(symbol, "/tsconfig.json") + response := sd.newSymbolResponse(symbol, 1) assert.Equal(t, response.Reference.Kind, SymbolOwnerKindSnapshot) resolved, err := sd.resolveSymbolHandle(response.Reference.Id) assert.NilError(t, err) @@ -101,7 +102,7 @@ func TestContentMappedSymbolsAreSnapshotOwned(t *testing.T) { class := sourceFile.Statements.Nodes[0].Symbol() sd := newTestSnapshotData() - response := sd.newSymbolResponse(class, "/tsconfig.json") + response := sd.newSymbolResponse(class, 1) assert.Equal(t, response.Reference.Kind, SymbolOwnerKindSnapshot) assert.Assert(t, response.Reference.File == nil) assert.Equal(t, response.Reference.Snapshot, SnapshotID(1)) diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 0e9a59aa2a08a..9238fbe11a175 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -111,6 +111,9 @@ type Program struct { processedFiles + sourceFilesByNodeIDOnce sync.Once + sourceFilesByNodeID map[ast.NodeId]*ast.SourceFile + usesUriStyleNodeCoreModules core.Tristate commonSourceDirectory tspath.RootedDirectoryPath @@ -2128,6 +2131,16 @@ func (p *Program) GetSourceFileByPath(path tspath.PathKey) *ast.SourceFile { return p.filesByPath[path] } +func (p *Program) GetSourceFileByNodeID(id ast.NodeId) *ast.SourceFile { + p.sourceFilesByNodeIDOnce.Do(func() { + p.sourceFilesByNodeID = make(map[ast.NodeId]*ast.SourceFile, len(p.GetSourceFiles())) + for _, file := range p.GetSourceFiles() { + p.sourceFilesByNodeID[ast.GetNodeId(file.AsNode())] = file + } + }) + return p.sourceFilesByNodeID[id] +} + func (p *Program) HasSameFileNames(other *Program) bool { return maps.EqualFunc(p.filesByPath, other.filesByPath, func(a, b *ast.SourceFile) bool { // checks for casing differences on case-insensitive file systems diff --git a/tsc/internal/project/project.go b/tsc/internal/project/project.go index f88a09b297a35..43083788fb2e3 100644 --- a/tsc/internal/project/project.go +++ b/tsc/internal/project/project.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/collections" @@ -31,6 +32,11 @@ const ( type ID string +// Handle identifies a project's lifetime across snapshot clones. +type Handle uint64 + +var nextProjectHandle atomic.Uint64 + type ConfiguredProjectID struct { pathKey tspath.PathKey } @@ -153,6 +159,7 @@ const ( type Project struct { Kind Kind id ID + handle Handle projectDirectory tspath.RootedDirectoryPath configFileName tspath.RootedFilePath configFilePath tspath.PathKey @@ -292,6 +299,7 @@ func NewProject( project := &Project{ Kind: kind, id: id, + handle: Handle(nextProjectHandle.Add(1)), projectDirectory: projectDirectory, dirty: true, } @@ -351,6 +359,10 @@ func (p *Project) ID() ID { return p.id } +func (p *Project) Handle() Handle { + return p.handle +} + // ConfigFileName panics if Kind() is not KindConfigured. func (p *Project) ConfigFileName() tspath.RootedFilePath { if p.Kind != KindConfigured { @@ -410,6 +422,7 @@ func (p *Project) Clone() *Project { return &Project{ Kind: p.Kind, id: p.id, + handle: p.handle, projectDirectory: p.projectDirectory, configFileName: p.configFileName, configFilePath: p.configFilePath, diff --git a/tsc/internal/project/projectcollection.go b/tsc/internal/project/projectcollection.go index 4f64a3a275095..609985e2c8bd5 100644 --- a/tsc/internal/project/projectcollection.go +++ b/tsc/internal/project/projectcollection.go @@ -37,6 +37,9 @@ type ProjectCollection struct { openConfiguredProjectsOnce sync.Once openConfiguredProjects *collections.Set[ConfiguredProjectID] + + projectsByHandleOnce sync.Once + projectsByHandle map[Handle]*Project } // APIState tracks the projects and files that API clients have explicitly opened. @@ -80,6 +83,7 @@ func (c *ProjectCollection) GetProject(id ID) *Project { if _, ok := id.Inferred(); ok { return c.inferredProject } + if syntheticID, ok := id.Synthetic(); ok { return c.syntheticProjects[syntheticID] } @@ -89,6 +93,26 @@ func (c *ProjectCollection) GetProject(id ID) *Project { return nil } +// GetProjectByHandle resolves a project identity only within this snapshot's collection. +func (c *ProjectCollection) GetProjectByHandle(handle Handle) *Project { + if handle == 0 { + return nil + } + c.projectsByHandleOnce.Do(func() { + c.projectsByHandle = make(map[Handle]*Project, len(c.configuredProjects)+len(c.syntheticProjects)+1) + for _, p := range c.configuredProjects { + c.projectsByHandle[p.Handle()] = p + } + for _, p := range c.syntheticProjects { + c.projectsByHandle[p.Handle()] = p + } + if p := c.inferredProject; p != nil { + c.projectsByHandle[p.Handle()] = p + } + }) + return c.projectsByHandle[handle] +} + // ConfiguredProjects returns all configured projects in a stable order. func (c *ProjectCollection) ConfiguredProjects() []*Project { projects := make([]*Project, 0, len(c.configuredProjects)) diff --git a/tsc/internal/project/projecthandle_test.go b/tsc/internal/project/projecthandle_test.go new file mode 100644 index 0000000000000..1c06377216e4c --- /dev/null +++ b/tsc/internal/project/projecthandle_test.go @@ -0,0 +1,45 @@ +package project + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestProjectHandleLookup(t *testing.T) { + t.Parallel() + + p := &Project{id: "/tsconfig.json", handle: Handle(nextProjectHandle.Add(1))} + clone := p.Clone() + assert.Equal(t, clone.Handle(), p.Handle()) + + first := &ProjectCollection{inferredProject: p} + second := first.clone() + second.inferredProject = clone + assert.Equal(t, first.GetProjectByHandle(p.Handle()), p) + assert.Equal(t, second.GetProjectByHandle(p.Handle()), clone) + assert.Assert(t, first.GetProjectByHandle(0) == nil) + + removed := first.clone() + removed.inferredProject = nil + assert.Assert(t, removed.GetProjectByHandle(p.Handle()) == nil) + + recreated := &Project{id: p.ID(), handle: Handle(nextProjectHandle.Add(1))} + assert.Assert(t, recreated.Handle() != p.Handle()) + replacement := &ProjectCollection{inferredProject: recreated} + assert.Assert(t, replacement.GetProjectByHandle(p.Handle()) == nil) + assert.Equal(t, replacement.GetProjectByHandle(recreated.Handle()), recreated) +} + +func TestConcurrentProjectHandleLookup(t *testing.T) { + t.Parallel() + + p := &Project{handle: Handle(nextProjectHandle.Add(1))} + collection := &ProjectCollection{inferredProject: p} + for range 16 { + t.Run("lookup", func(t *testing.T) { + t.Parallel() + assert.Equal(t, collection.GetProjectByHandle(p.Handle()), p) + }) + } +}