From 47fddf52d09cdca019d389571e006062729c19f2 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 13 Oct 2026 15:41:31 +0000 Subject: [PATCH 1/7] Reuse scanners across astnav child and token materialization createChildren allocated a fresh scanner for every node whose children were materialized, and the token lookups (getTokenAtPosition, findNextToken, findPrecedingToken) allocated one per query. Keep a per-source-file pool of idle scanners (WeakMap): check out by popping or creating, reset text/language variant on checkout, and in a finally block clear comment directives and token state before returning the scanner. Scanners are removed from the pool while in use, so nested or reentrant traversals always get distinct scanners. Add tests covering reuse across cached/repeated calls and token lookups, nested child creation, exceptions during child creation, trivia/comment directives/JSDoc state, exact token boundaries, and independent source files with different language variants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit acac55ed1e17b6d29eef12133e0eaa0ba29b23de) --- packages/typescript/src/ast/astnav.ts | 212 +++++++++++------- packages/typescript/test/sync/ast.test.ts | 249 ++++++++++++++++++++++ 2 files changed, 383 insertions(+), 78 deletions(-) diff --git a/packages/typescript/src/ast/astnav.ts b/packages/typescript/src/ast/astnav.ts index f401215034b9d..5fe6b732a2b96 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,59 @@ 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(); +// 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. +const scannerPools = new WeakMap(); + +function acquireScanner(sourceFile: SourceFile): Scanner { + const scanner = scannerPools.get(sourceFile)?.pop(); + if (scanner === undefined) { + return createScanner(/*skipTrivia*/ true, sourceFile.languageVariant, sourceFile.text); + } + // setText resets the token state (position, token, value, flags) to the start of the text. + scanner.setText(sourceFile.text); + scanner.setLanguageVariant(sourceFile.languageVariant); return scanner; } +function releaseScanner(sourceFile: SourceFile, scanner: Scanner): void { + // 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. + scanner.clearCommentDirectives(); + scanner.resetTokenState(0); + let pool = scannerPools.get(sourceFile); + if (pool === undefined) { + pool = []; + scannerPools.set(sourceFile, pool); + } + pool.push(scanner); +} + +function withScanner(sourceFile: SourceFile, pos: number | undefined, callback: (scanner: Scanner) => T): T { + const scanner = acquireScanner(sourceFile); + try { + if (pos !== undefined) { + scanner.resetTokenState(pos); + scanner.scan(); + } + return callback(scanner); + } + finally { + releaseScanner(sourceFile, scanner); + } +} + +/** + * Returns the idle scanners pooled for `sourceFile`. Exposed for tests only. + * @internal + */ +export function getPooledScannersForTesting(sourceFile: SourceFile): readonly Scanner[] { + return scannerPools.get(sourceFile) ?? 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,37 +720,42 @@ 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); - let pos = node.pos; - const processNode = (child: Node): undefined => { - // Reparsed subtrees (JSDoc types materialized into the AST) have positions inside - // the comment, not in this node's token range. - if (child.flags & NodeFlags.Reparsed) { - return; - } - addSyntheticNodes(children, pos, child.pos, node, sourceFile, scanner); - children.push(child); - pos = child.end; - }; - const processNodes = (nodes: NodeArray): undefined => { - if (nodes.length > 0 && !nodes.some(child => !(child.flags & NodeFlags.Reparsed))) { - return; - } - addSyntheticNodes(children, pos, nodes.pos, node, sourceFile, scanner); - children.push(createSyntaxListNode(nodes, node, sourceFile, scanner)); - pos = nodes.end; - }; + const scanner = acquireScanner(sourceFile); + try { + let pos = node.pos; + const processNode = (child: Node): undefined => { + // Reparsed subtrees (JSDoc types materialized into the AST) have positions inside + // the comment, not in this node's token range. + if (child.flags & NodeFlags.Reparsed) { + return; + } + addSyntheticNodes(children, pos, child.pos, node, sourceFile, scanner); + children.push(child); + pos = child.end; + }; + const processNodes = (nodes: NodeArray): undefined => { + if (nodes.length > 0 && !nodes.some(child => !(child.flags & NodeFlags.Reparsed))) { + return; + } + addSyntheticNodes(children, pos, nodes.pos, node, sourceFile, scanner); + children.push(createSyntaxListNode(nodes, node, sourceFile, scanner)); + pos = nodes.end; + }; - // JSDoc attached to the node is leading content, processed first. - if (node.jsDoc) { - for (const jsDoc of node.jsDoc) { - processNode(jsDoc); + // JSDoc attached to the node is leading content, processed first. + if (node.jsDoc) { + for (const jsDoc of node.jsDoc) { + processNode(jsDoc); + } } + pos = node.pos; + node.forEachChild(processNode, processNodes); + addSyntheticNodes(children, pos, node.end, node, sourceFile, scanner); + return children; + } + finally { + releaseScanner(sourceFile, scanner); } - pos = node.pos; - node.forEachChild(processNode, processNodes); - addSyntheticNodes(children, pos, node.end, node, sourceFile, scanner); - return children; } function addSyntheticNodes(children: Node[], pos: number, end: number, parent: Node, sourceFile: SourceFile, scanner: Scanner): void { 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); + }); + }); +}); From 8cf6d986e7c3e5d8ad63507b1d9fdc259639b439 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 13 Oct 2026 17:51:29 +0000 Subject: [PATCH 2/7] Omit zero-valued fields from type, symbol, and signature responses encoding/json/v2 only omits empty JSON values for omitempty, so every TypeResponse carried "objectFlags":0, "isTupleType":false, "value":null and "isThisType":false, every SymbolResponse "checkFlags":0, and every SignatureResponse "flags":0. Use omitzero and default the fields on the client. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 4551280766db7f2f4827d057af78cecf1a92ca06) --- packages/typescript/src/api/async/api.ts | 8 ++++---- packages/typescript/src/api/proto.generated.ts | 8 ++++---- packages/typescript/src/api/sync/api.ts | 8 ++++---- tsc/internal/api/proto.go | 14 +++++++------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 6c1eb480123e6..36c39b66caef1 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -3259,7 +3259,7 @@ export class Symbol { this.escapedName = data.name as __String; this.name = unescapeLeadingUnderscores(data.name as __String); this.flags = data.flags; - this.checkFlags = data.checkFlags; + this.checkFlags = data.checkFlags ?? 0; // 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; @@ -3410,7 +3410,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 +3436,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; @@ -3891,7 +3891,7 @@ 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.typeParameters = data.typeParameters ?? []; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 01e2fa27b12c2..abef8c6aa11cb 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; @@ -524,9 +524,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 */ @@ -708,7 +708,7 @@ export interface GetSignaturesOfTypeParams { export interface SignatureResponse { id: number; - flags: number; + flags?: number | undefined; declaration?: string | undefined; typeParameters?: number[] | undefined; parameters?: CompactSymbolReference[] | undefined; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index a2960224381ff..36a14d634d25a 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -6686,7 +6686,7 @@ export class Symbol { this.escapedName = data.name as __String; this.name = unescapeLeadingUnderscores(data.name as __String); this.flags = data.flags; - this.checkFlags = data.checkFlags; + this.checkFlags = data.checkFlags ?? 0; // 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; @@ -6975,7 +6975,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 +7001,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; @@ -8007,7 +8007,7 @@ 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.typeParameters = data.typeParameters ?? []; diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 8072445202b44..130e6b28f710c 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -1125,7 +1125,7 @@ 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"` @@ -1176,12 +1176,12 @@ type GetTypesOfSymbolsParams struct { 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 +1223,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 +1363,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"` From ecc6c2f4876b2efd37a5069e1aa3f65b42d9e54c Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 13 Oct 2026 17:53:53 +0000 Subject: [PATCH 3/7] Accept compact project handles in API requests Give projects a server-generated numeric identity preserved across snapshot clones. Resolve request handles directly through a lazily initialized index owned by each snapshot project collection, rather than a process-wide alias-to-path table. Keep public logical project IDs unchanged. Use typed numeric handles in request parameters, symbol owners, and project-scoped registries; regenerate both API clients and remove the custom JSON alias decoder. Cover snapshot isolation, cloning, removal and recreation, concurrent lookup, and omission of project identity from file-owned symbol responses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/typescript/src/api/async/api.ts | 176 ++++----- .../typescript/src/api/proto.generated.ts | 114 +++--- packages/typescript/src/api/proto.ts | 3 +- packages/typescript/src/api/sync/api.ts | 342 +++++++++--------- packages/typescript/test/proto.test.ts | 6 +- tsc/internal/api/projecthandle_test.go | 61 ++++ tsc/internal/api/proto.go | 257 ++++++------- tsc/internal/api/session.go | 101 +++--- tsc/internal/api/session_apistate_test.go | 9 +- tsc/internal/api/session_batch_test.go | 2 +- tsc/internal/api/session_completion_test.go | 20 +- .../api/session_createprogram_test.go | 4 +- .../api/session_module_resolution_test.go | 4 +- .../api/session_requestfilesystem_test.go | 6 +- .../api/session_symbolresponse_test.go | 9 +- tsc/internal/json/json.go | 1 + tsc/internal/project/project.go | 13 + tsc/internal/project/projectcollection.go | 24 ++ tsc/internal/project/projecthandle_test.go | 45 +++ 19 files changed, 685 insertions(+), 512 deletions(-) create mode 100644 tsc/internal/api/projecthandle_test.go create mode 100644 tsc/internal/project/projecthandle_test.go diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 36c39b66caef1..4532addaeef2b 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1227,7 +1227,7 @@ export class ModuleResolver { class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); - private readonly projectRegistries: Map = new Map(); + private readonly projectRegistries: Map = new Map(); private readonly snapshotId: number; constructor(snapshotId: number) { @@ -1235,7 +1235,7 @@ class SnapshotObjectRegistry { } addProjectRegistry(registry: ProjectObjectRegistry): void { - this.projectRegistries.set(registry.project.id, registry); + this.projectRegistries.set(registry.project.handle, registry); } getOrCreateSymbol(data: SymbolResponse): Symbol { @@ -1364,7 +1364,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 +1385,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 +1399,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 +1422,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 +1449,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 +1460,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 +1470,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 +1479,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 +1488,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 +1498,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 +1508,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 []; @@ -1523,7 +1523,7 @@ class ProjectObjectRegistry { 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 +1535,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 +1551,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 +1605,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, }); @@ -1654,7 +1660,7 @@ export class LanguageService { 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 +1690,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 +1784,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 +1803,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,7 +1814,7 @@ 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), }); @@ -1817,7 +1823,7 @@ export class Program implements FormatDiagnost 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, }); @@ -1843,7 +1849,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 +1863,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 +1874,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 +1908,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 +1945,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 +1957,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 +1977,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 +1993,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 +2009,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 +2025,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 +2041,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 +2053,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 +2064,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 +2075,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 +2088,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 +2111,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 +2123,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 +2135,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 +2284,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); @@ -2312,7 +2318,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; @@ -2343,7 +2349,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 +2357,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 +2370,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 +2394,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 +2415,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 +2430,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,7 +2439,7 @@ 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, }); @@ -2488,7 +2494,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 +2502,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,7 +2519,7 @@ 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, @@ -2532,7 +2538,7 @@ 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, file: isNode ? undefined : (location as DocumentPosition).document, @@ -2569,7 +2575,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 +2585,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); @@ -2608,7 +2614,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 +2627,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 +2637,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 +2645,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, }); @@ -2662,7 +2668,7 @@ 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), }); @@ -2672,7 +2678,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,7 +2723,7 @@ 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, flags, @@ -2729,7 +2735,7 @@ 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, @@ -2742,7 +2748,7 @@ 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, flags, @@ -2762,7 +2768,7 @@ export class Checker { 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 +2793,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 +2810,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 +2819,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 +2860,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, }); @@ -2873,7 +2879,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 +2901,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 +2910,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, }); @@ -2959,7 +2965,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 +2978,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 +2986,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 +2999,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 +3010,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 +3025,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 +3059,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 +3075,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 +3084,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 +3094,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 +3103,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 +3114,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)) : []; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index abef8c6aa11cb..93774869c4407 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -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; } @@ -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,7 +703,7 @@ export interface GetSymbolsInScopeParams { export interface GetSignaturesOfTypeParams { snapshot: number; - project: ProjectId; + project: number; type: number; kind: number; } @@ -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 36a14d634d25a..ea19e7be67eab 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1959,7 +1959,7 @@ export class ModuleResolver { class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); - private readonly projectRegistries: Map = new Map(); + private readonly projectRegistries: Map = new Map(); private readonly snapshotId: number; constructor(snapshotId: number) { @@ -1967,7 +1967,7 @@ class SnapshotObjectRegistry { } addProjectRegistry(registry: ProjectObjectRegistry): void { - this.projectRegistries.set(registry.project.id, registry); + this.projectRegistries.set(registry.project.handle, registry); } getOrCreateSymbol(data: SymbolResponse): Symbol { @@ -2104,7 +2104,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 +2119,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 +2165,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 +2179,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 +2203,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 +2216,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 +2249,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 +2271,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 +2332,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 +2342,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 +2363,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 +2372,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 +2392,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 +2400,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 +2419,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 +2427,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 +2446,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 +2455,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 +2475,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 +2484,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 +2504,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 []; @@ -2518,7 +2518,7 @@ class ProjectObjectRegistry { 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 []; @@ -2543,7 +2543,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 +2552,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 +2566,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 +2582,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 +2644,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 +2669,7 @@ export class LanguageService { const data = yield* apiRequest("getImportAdderEdits", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, actions: requestActions, }); @@ -2800,7 +2806,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 +2824,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 +2864,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 +2875,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 +3037,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 +3059,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 +3085,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 +3095,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,7 +3116,7 @@ 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), }); @@ -3118,7 +3124,7 @@ export class Program implements FormatDiagnost 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), }); @@ -3137,7 +3143,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 +3151,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, }); @@ -3193,7 +3199,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 +3209,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 +3230,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 +3240,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 +3261,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 +3343,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 +3353,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 +3428,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 +3457,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 +3470,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 +3501,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 +3512,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 +3538,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 +3549,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 +3575,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 +3586,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 +3612,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 +3623,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 +3649,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 +3660,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 +3682,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 +3710,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 +3738,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 +3768,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 +3787,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 +3820,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 +3828,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 +3850,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 +3858,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 +3880,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 +3888,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 +4154,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 +4162,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); @@ -4237,7 +4243,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 +4251,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; @@ -4312,7 +4318,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 +4326,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 +4338,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 +4346,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 +4368,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 +4386,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 +4419,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 +4437,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 +4468,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 +4476,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 +4501,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 +4509,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,7 +4528,7 @@ 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, }); @@ -4531,7 +4537,7 @@ export class Checker { 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, }); @@ -4653,7 +4659,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 +4667,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 +4679,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 +4687,7 @@ export class Checker { } const data = yield* apiRequest("getTypesAtPositions", { snapshot: owner.snapshotId, - project: owner.project.id, + project: owner.project.handle, file, positions: positionOrPositions, }); @@ -4703,7 +4709,7 @@ 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, @@ -4718,7 +4724,7 @@ 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, @@ -4747,7 +4753,7 @@ 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, file: isNode ? undefined : (location as DocumentPosition).document, @@ -4760,7 +4766,7 @@ 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, file: isNode ? undefined : (location as DocumentPosition).document, @@ -4859,7 +4865,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 +4873,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 +4893,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 +4901,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); @@ -4965,7 +4971,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 +4979,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 +5002,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 +5011,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 +5031,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 +5056,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 +5064,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, }); @@ -5109,7 +5115,7 @@ 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), }); @@ -5118,7 +5124,7 @@ export class Checker { 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), }); @@ -5138,14 +5144,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,7 +5362,7 @@ 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, flags, @@ -5368,7 +5374,7 @@ 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, flags, @@ -5391,7 +5397,7 @@ 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, @@ -5404,7 +5410,7 @@ 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, @@ -5428,7 +5434,7 @@ 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, flags, @@ -5439,7 +5445,7 @@ 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, flags, @@ -5486,14 +5492,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 +5560,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 +5607,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 +5615,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 +5634,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 +5648,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 +5764,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, }); @@ -5772,7 +5778,7 @@ export class Checker { 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, }); @@ -5814,7 +5820,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 +5829,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 +5887,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 +5895,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 +5914,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 +5923,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, }); @@ -6048,7 +6054,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 +6062,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 +6085,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 +6110,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 +6118,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 +6141,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 +6152,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 +6173,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 +6181,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 +6206,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 +6295,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 +6340,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 +6348,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 +6367,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 +6376,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 +6396,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 +6404,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 +6423,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 +6451,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 +6459,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)) : []; 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/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 130e6b28f710c..6b758641587dc 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -1030,7 +1030,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 +1078,7 @@ func NewProjectResponse(p *project.Project) *ProjectResponse { } return &ProjectResponse{ Id: p.ID(), + Handle: p.Handle(), ConfigFileName: configFileName, CurrentDirectory: p.CurrentDirectory(), Dirty: p.IsDirty(), @@ -1085,40 +1088,40 @@ 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 { @@ -1143,7 +1146,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,13 +1166,13 @@ 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"` } @@ -1373,32 +1376,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 +1409,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 +1424,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 +1479,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 +1492,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 +1501,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 +1513,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 +1543,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 +1565,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 +1579,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 +1589,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 +1621,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 +1643,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 +1745,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 +1758,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 +1784,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 +1818,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 +1869,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 +1900,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..477835db23f10 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,10 +161,10 @@ 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 } @@ -176,8 +176,8 @@ func (sd *snapshotData) nodeHandleFrom(node *ast.Node) NodeHandle { } // 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 } @@ -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 @@ -5521,9 +5526,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 9c0809aecb5cb..0327d1d76418d 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)) } 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/json/json.go b/tsc/internal/json/json.go index ddacc5b16451a..3ace40f2492d1 100644 --- a/tsc/internal/json/json.go +++ b/tsc/internal/json/json.go @@ -82,6 +82,7 @@ func NewDecoder(r io.Reader) *jsontext.Decoder { } type ( + Options = json.Options Value = jsontext.Value Kind = jsontext.Kind UnmarshalerFrom = json.UnmarshalerFrom 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) + }) + } +} From b060667a1c357aa622ab362f5eb45866e57dbdbf Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 13 Oct 2026 17:54:27 +0000 Subject: [PATCH 4/7] Identify API nodes by exact source-file AST identity Use the existing source-file node ID and node-table index as node identity. Client requests send "index.#sourceFileID"; server responses retain kind and path as "index.kind.#sourceFileID.path". Resolve request handles through a lazy index owned by the program. Remove the process-global ID-to-path registry and reject nodes from another AST, even when the path and text match. Client response handles verify the AST identity before resolving a node fetched by path. Cover shared ASTs, stale and separately parsed files, malformed requests, response metadata, and concurrent source-file identity lookup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/typescript/src/api/async/api.ts | 113 ++++++---- packages/typescript/src/api/node/node.ts | 53 ++++- packages/typescript/src/api/sync/api.ts | 207 ++++++++++-------- packages/typescript/test/async/api.test.ts | 43 +++- packages/typescript/test/path.test.ts | 16 +- .../test/sync/api-generators.test.ts | 2 + packages/typescript/test/sync/api.test.ts | 43 +++- tsc/internal/api/nodehandle_test.go | 76 +++++++ tsc/internal/api/proto.go | 4 +- tsc/internal/api/session.go | 29 ++- tsc/internal/compiler/program.go | 13 ++ 11 files changed, 431 insertions(+), 168 deletions(-) create mode 100644 tsc/internal/api/nodehandle_test.go diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 4532addaeef2b..4b7a7980c5565 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, @@ -1634,8 +1635,8 @@ 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 => ({ @@ -1648,8 +1649,8 @@ export class LanguageService { 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), @@ -1816,7 +1817,7 @@ export class Program implements FormatDiagnost snapshot: this.snapshotId, project: this.project.handle, file, - usage: getNodeId(usage), + usage: getNodeRequestHandle(usage), }); } @@ -1835,8 +1836,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; @@ -2297,8 +2298,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; } @@ -2307,8 +2308,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))!; @@ -2330,15 +2331,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; } @@ -2457,15 +2458,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); } @@ -2482,8 +2483,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); } @@ -2522,7 +2523,7 @@ export class Checker { 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, @@ -2540,7 +2541,7 @@ export class Checker { snapshot: this.snapshotId, 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, }); @@ -2556,8 +2557,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; } @@ -2565,8 +2566,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; @@ -2604,8 +2605,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); } @@ -2654,8 +2655,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; } @@ -2670,7 +2671,7 @@ export class Checker { snapshot: this.snapshotId, project: this.project.handle, symbol: symbol.reference, - location: getNodeId(location), + location: getNodeRequestHandle(location), }); return this.objectRegistry.getOrCreateType(data); } @@ -2725,7 +2726,7 @@ export class Checker { snapshot: this.snapshotId, project: this.project.handle, type: type.id, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (!binaryData) return undefined; @@ -2738,7 +2739,7 @@ export class Checker { project: this.project.handle, signature: signature.id, kind, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (!binaryData) return undefined; @@ -2750,7 +2751,7 @@ export class Checker { snapshot: this.snapshotId, 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"); @@ -2760,8 +2761,8 @@ 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), }); } @@ -2920,8 +2921,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; @@ -2942,8 +2943,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); } @@ -2951,8 +2952,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; } @@ -3168,12 +3169,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; @@ -3183,19 +3188,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 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 (sourceFile as unknown as RemoteSourceFile).getOrCreateNodeAtIndex(this.index) as T | undefined; + return node as T; } private async fetchOwnerFile(fileOwner: SourceFileOwner): 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/sync/api.ts b/packages/typescript/src/api/sync/api.ts index ea19e7be67eab..0e6ac34ef537c 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, @@ -2736,8 +2737,8 @@ 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 => ({ @@ -2749,8 +2750,8 @@ export class LanguageService { 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 => ({ @@ -2773,8 +2774,8 @@ 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), @@ -2784,8 +2785,8 @@ export class LanguageService { 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), @@ -3118,7 +3119,7 @@ export class Program implements FormatDiagnost snapshot: owner.snapshotId, project: owner.project.handle, file, - usage: getNodeId(usage), + usage: getNodeRequestHandle(usage), }); }, function* (file: DocumentIdentifier, usage: StringLiteralLikeNode): Generator { @@ -3126,7 +3127,7 @@ export class Program implements FormatDiagnost snapshot: owner.snapshotId, project: owner.project.handle, file, - usage: getNodeId(usage), + usage: getNodeRequestHandle(usage), }); }, ); @@ -3170,8 +3171,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; @@ -3179,8 +3180,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; @@ -4185,16 +4186,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; }, @@ -4213,8 +4214,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))!; @@ -4222,8 +4223,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))!; @@ -4272,15 +4273,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; } @@ -4290,15 +4291,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; } @@ -4564,15 +4565,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); } @@ -4582,15 +4583,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); } @@ -4630,16 +4631,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); }, @@ -4712,7 +4713,7 @@ export class Checker { 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, @@ -4727,7 +4728,7 @@ export class Checker { 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, @@ -4755,7 +4756,7 @@ export class Checker { snapshot: owner.snapshotId, 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, }); @@ -4768,7 +4769,7 @@ export class Checker { snapshot: owner.snapshotId, 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, }); @@ -4809,16 +4810,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; }, @@ -4836,8 +4837,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; @@ -4845,8 +4846,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; @@ -4943,16 +4944,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); }, @@ -5083,16 +5084,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; }, @@ -5117,7 +5118,7 @@ export class Checker { snapshot: owner.snapshotId, project: owner.project.handle, symbol: symbol.reference, - location: getNodeId(location), + location: getNodeRequestHandle(location), }); return owner.objectRegistry.getOrCreateType(data); }, @@ -5126,7 +5127,7 @@ export class Checker { snapshot: owner.snapshotId, project: owner.project.handle, symbol: symbol.reference, - location: getNodeId(location), + location: getNodeRequestHandle(location), }); return owner.objectRegistry.getOrCreateType(data); }, @@ -5364,7 +5365,7 @@ export class Checker { snapshot: owner.snapshotId, project: owner.project.handle, type: type.id, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (!binaryData) return undefined; @@ -5376,7 +5377,7 @@ export class Checker { snapshot: owner.snapshotId, project: owner.project.handle, type: type.id, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }), ); @@ -5400,7 +5401,7 @@ export class Checker { project: owner.project.handle, signature: signature.id, kind, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }); if (!binaryData) return undefined; @@ -5413,7 +5414,7 @@ export class Checker { project: owner.project.handle, signature: signature.id, kind, - location: enclosingDeclaration ? getNodeId(enclosingDeclaration) : undefined, + location: enclosingDeclaration ? getNodeRequestHandle(enclosingDeclaration) : undefined, flags, }), ); @@ -5436,7 +5437,7 @@ export class Checker { snapshot: owner.snapshotId, 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"); @@ -5447,7 +5448,7 @@ export class Checker { snapshot: owner.snapshotId, 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"); @@ -5467,15 +5468,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), }); }, ); @@ -5943,8 +5944,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; @@ -5963,8 +5964,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; @@ -5995,16 +5996,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); }, @@ -6022,16 +6023,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; }, @@ -6555,12 +6556,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; @@ -6570,7 +6575,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; @@ -6583,30 +6589,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; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index b9ef5842d4489..1610a4eeb0865 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({ 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/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..847bb2d6564d2 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({ 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/proto.go b/tsc/internal/api/proto.go index 6b758641587dc..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 diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 477835db23f10..cef38f0453201 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -169,7 +169,7 @@ func (sd *snapshotData) getProject(projectHandle project.Handle) (*project.Proje 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) @@ -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 @@ -5011,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) } diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 2c7aa08030d8a..cc32890187c16 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 @@ -2118,6 +2121,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 From 92c160840ad9052a47821e7452cbe7ff0bdeee56 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 13 Oct 2026 17:54:38 +0000 Subject: [PATCH 5/7] Materialize declaration node handles on first access Symbols and signatures parsed every declaration handle into a NodeHandle as soon as they were received, although most are never read. Keep the wire strings and create the NodeHandles when declarations, valueDeclaration, or declaration is first accessed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit c5f5be5e4b18b462923f7e3d360a0b18e4411787) --- packages/typescript/src/api/async/api.ts | 51 +++++++++++++++++++----- packages/typescript/src/api/sync/api.ts | 51 +++++++++++++++++++----- 2 files changed, 80 insertions(+), 22 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 4b7a7980c5565..93613b4e6dac9 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -3267,8 +3267,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; @@ -3283,18 +3286,33 @@ export class Symbol { this.name = unescapeLeadingUnderscores(data.name as __String); this.flags = data.flags; this.checkFlags = data.checkFlags ?? 0; - // 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.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 + ? new NodeHandle(handle, this.storage.registry.project) + : new NodeHandle(handle, undefined, this.storage.owner); + } + async getParent(): Promise { if (!this.parent) return undefined; return this.fetchSymbol("getParentOfSymbol", this.parent); @@ -3905,7 +3923,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; @@ -3916,7 +3936,9 @@ export class Signature { this.id = data.id; 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; @@ -3924,6 +3946,13 @@ export class Signature { this.returnType = false; } + get declaration(): NodeHandle | undefined { + if (this.declarationCache === undefined && this.declarationHandle) { + this.declarationCache = new NodeHandle(this.declarationHandle, this.project); + } + return this.declarationCache; + } + async getTypeParameters(): Promise { return this.objectRegistry.fetchTypes(this, "getTypeParametersOfSignature", this.typeParameters) as Promise; } diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 0e6ac34ef537c..fe93d26301774 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -6694,8 +6694,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; @@ -6710,18 +6713,33 @@ export class Symbol { this.name = unescapeLeadingUnderscores(data.name as __String); this.flags = data.flags; this.checkFlags = data.checkFlags ?? 0; - // 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.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 + ? new NodeHandle(handle, this.storage.registry.project) + : new NodeHandle(handle, undefined, this.storage.owner); + } + get getParent(): { (): Symbol | undefined; gen(): Generator; @@ -8021,7 +8039,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; @@ -8032,7 +8052,9 @@ export class Signature { this.id = data.id; 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; @@ -8040,6 +8062,13 @@ export class Signature { this.returnType = false; } + get declaration(): NodeHandle | undefined { + if (this.declarationCache === undefined && this.declarationHandle) { + this.declarationCache = new NodeHandle(this.declarationHandle, this.project); + } + return this.declarationCache; + } + get getTypeParameters(): { (): readonly TypeParameter[]; gen(): Generator; From 2541995faf1bd7f2b20d6b79d7a179582cdccd36 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 15 Oct 2026 09:04:16 -0700 Subject: [PATCH 6/7] PR feedback --- packages/typescript/src/ast/astnav.ts | 123 +++++++++++++------------- tsc/internal/json/json.go | 1 - 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/packages/typescript/src/ast/astnav.ts b/packages/typescript/src/ast/astnav.ts index 5fe6b732a2b96..53bc447d2a0d7 100644 --- a/packages/typescript/src/ast/astnav.ts +++ b/packages/typescript/src/ast/astnav.ts @@ -600,44 +600,49 @@ function isJSDocSingleCommentNodeList(nodes: NodeArray): boolean { // 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. -const scannerPools = new WeakMap(); - -function acquireScanner(sourceFile: SourceFile): Scanner { - const scanner = scannerPools.get(sourceFile)?.pop(); - if (scanner === undefined) { - return createScanner(/*skipTrivia*/ true, sourceFile.languageVariant, sourceFile.text); - } - // setText resets the token state (position, token, value, flags) to the start of the text. - scanner.setText(sourceFile.text); - scanner.setLanguageVariant(sourceFile.languageVariant); - return scanner; +interface ScannerLease extends Disposable { + readonly scanner: Scanner; } -function releaseScanner(sourceFile: SourceFile, scanner: Scanner): void { - // 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. - scanner.clearCommentDirectives(); - scanner.resetTokenState(0); +const scannerPools = new WeakMap(); + +function acquireScanner(sourceFile: SourceFile): ScannerLease { let pool = scannerPools.get(sourceFile); if (pool === undefined) { pool = []; scannerPools.set(sourceFile, pool); } - pool.push(scanner); + 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 { - const scanner = acquireScanner(sourceFile); - try { - if (pos !== undefined) { - scanner.resetTokenState(pos); - scanner.scan(); - } - return callback(scanner); - } - finally { - releaseScanner(sourceFile, scanner); + using lease = acquireScanner(sourceFile); + const { scanner } = lease; + if (pos !== undefined) { + scanner.resetTokenState(pos); + scanner.scan(); } + return callback(scanner); } /** @@ -645,7 +650,7 @@ function withScanner(sourceFile: SourceFile, pos: number | undefined, callbac * @internal */ export function getPooledScannersForTesting(sourceFile: SourceFile): readonly Scanner[] { - return scannerPools.get(sourceFile) ?? emptyScannerArray; + return scannerPools.get(sourceFile)?.map(lease => lease.scanner) ?? emptyScannerArray; } const emptyScannerArray: readonly Scanner[] = []; @@ -720,42 +725,38 @@ function createChildren(node: Node, sourceFile: SourceFile): readonly Node[] { } // One scanner serves every run of synthetic tokens materialized for this node. - const scanner = acquireScanner(sourceFile); - try { - let pos = node.pos; - const processNode = (child: Node): undefined => { - // Reparsed subtrees (JSDoc types materialized into the AST) have positions inside - // the comment, not in this node's token range. - if (child.flags & NodeFlags.Reparsed) { - return; - } - addSyntheticNodes(children, pos, child.pos, node, sourceFile, scanner); - children.push(child); - pos = child.end; - }; - const processNodes = (nodes: NodeArray): undefined => { - if (nodes.length > 0 && !nodes.some(child => !(child.flags & NodeFlags.Reparsed))) { - return; - } - addSyntheticNodes(children, pos, nodes.pos, node, sourceFile, scanner); - children.push(createSyntaxListNode(nodes, node, sourceFile, scanner)); - pos = nodes.end; - }; + 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 + // the comment, not in this node's token range. + if (child.flags & NodeFlags.Reparsed) { + return; + } + addSyntheticNodes(children, pos, child.pos, node, sourceFile, scanner); + children.push(child); + pos = child.end; + }; + const processNodes = (nodes: NodeArray): undefined => { + if (nodes.length > 0 && !nodes.some(child => !(child.flags & NodeFlags.Reparsed))) { + return; + } + addSyntheticNodes(children, pos, nodes.pos, node, sourceFile, scanner); + children.push(createSyntaxListNode(nodes, node, sourceFile, scanner)); + pos = nodes.end; + }; - // JSDoc attached to the node is leading content, processed first. - if (node.jsDoc) { - for (const jsDoc of node.jsDoc) { - processNode(jsDoc); - } + // JSDoc attached to the node is leading content, processed first. + if (node.jsDoc) { + for (const jsDoc of node.jsDoc) { + processNode(jsDoc); } - pos = node.pos; - node.forEachChild(processNode, processNodes); - addSyntheticNodes(children, pos, node.end, node, sourceFile, scanner); - return children; - } - finally { - releaseScanner(sourceFile, scanner); } + pos = node.pos; + node.forEachChild(processNode, processNodes); + addSyntheticNodes(children, pos, node.end, node, sourceFile, scanner); + return children; } function addSyntheticNodes(children: Node[], pos: number, end: number, parent: Node, sourceFile: SourceFile, scanner: Scanner): void { diff --git a/tsc/internal/json/json.go b/tsc/internal/json/json.go index 3ace40f2492d1..ddacc5b16451a 100644 --- a/tsc/internal/json/json.go +++ b/tsc/internal/json/json.go @@ -82,7 +82,6 @@ func NewDecoder(r io.Reader) *jsontext.Decoder { } type ( - Options = json.Options Value = jsontext.Value Kind = jsontext.Kind UnmarshalerFrom = json.UnmarshalerFrom From 69054b32d569f5c3dfbd8624c24239fba5f375b7 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 15 Oct 2026 13:35:28 -0700 Subject: [PATCH 7/7] Intern NodeHandle constructions --- packages/typescript/src/api/async/api.ts | 35 +++++++++---- packages/typescript/src/api/sync/api.ts | 49 +++++++++++------- packages/typescript/test/async/api.test.ts | 58 ++++++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 58 ++++++++++++++++++++++ 4 files changed, 171 insertions(+), 29 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 93613b4e6dac9..3beeee93c68c8 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1228,8 +1228,10 @@ export class ModuleResolver { class SnapshotObjectRegistry { private readonly symbols: 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; @@ -1259,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(); } } @@ -1330,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 { @@ -1517,7 +1530,7 @@ 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, })); } @@ -1640,9 +1653,9 @@ export class LanguageService { 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)), })); } @@ -1653,8 +1666,8 @@ export class LanguageService { 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, })); } @@ -2444,7 +2457,7 @@ export class Checker { file, symbol: symbol.reference, }); - return (data ?? []).map(h => new NodeHandle(h, this.project)); + return (data ?? []).map(h => this.objectRegistry.createNodeHandle(h)); } /** @@ -2869,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; } @@ -3160,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. */ @@ -3309,7 +3322,7 @@ export class Symbol { 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 - ? new NodeHandle(handle, this.storage.registry.project) + ? this.storage.registry.createNodeHandle(handle) : new NodeHandle(handle, undefined, this.storage.owner); } @@ -3948,7 +3961,7 @@ export class Signature { get declaration(): NodeHandle | undefined { if (this.declarationCache === undefined && this.declarationHandle) { - this.declarationCache = new NodeHandle(this.declarationHandle, this.project); + this.declarationCache = this.objectRegistry.createNodeHandle(this.declarationHandle); } return this.declarationCache; } diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index fe93d26301774..c938bb7a80919 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1960,8 +1960,10 @@ export class ModuleResolver { class SnapshotObjectRegistry { private readonly symbols: 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; @@ -1991,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(); } } @@ -2062,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 { @@ -2513,7 +2526,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, })); }, function* (source: Type): Generator { @@ -2527,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, })); }, ); @@ -2742,9 +2755,9 @@ export class LanguageService { 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 { @@ -2755,9 +2768,9 @@ export class LanguageService { 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)), })); }, ); @@ -2778,8 +2791,8 @@ export class LanguageService { 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 { @@ -2789,8 +2802,8 @@ export class LanguageService { 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, })); }, ); @@ -4533,7 +4546,7 @@ export class Checker { 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", { @@ -4542,7 +4555,7 @@ export class Checker { file, symbol: symbol.reference, }); - return (data ?? []).map(h => new NodeHandle(h, owner.project)); + return (data ?? []).map(h => owner.objectRegistry.createNodeHandle(h)); }, ); } @@ -5773,7 +5786,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; }, function* (type: Type, kind: IndexKind): Generator { @@ -5787,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; }, ); @@ -6547,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. */ @@ -6736,7 +6749,7 @@ export class Symbol { 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 - ? new NodeHandle(handle, this.storage.registry.project) + ? this.storage.registry.createNodeHandle(handle) : new NodeHandle(handle, undefined, this.storage.owner); } @@ -8064,7 +8077,7 @@ export class Signature { get declaration(): NodeHandle | undefined { if (this.declarationCache === undefined && this.declarationHandle) { - this.declarationCache = new NodeHandle(this.declarationHandle, this.project); + this.declarationCache = this.objectRegistry.createNodeHandle(this.declarationHandle); } return this.declarationCache; } diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 1610a4eeb0865..379f07b6b68f7 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -4156,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/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 847bb2d6564d2..f048e8ede87e6 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -3981,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({