Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/skills/api-client/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ What matters:
What doesn't matter:

* `uint64` overflow
* Passing objects between multiple `API` instances in the same JS process. We may eventually add some kind of central provenance validation. Do not add it on individual methods or flag it in reviews for now. It's not a realistic concern.
* Passing objects between multiple `API` instances in the same JS process. We may eventually add some kind of central provenance validation. Do not add it on individual methods or flag it in reviews for now. It's not a realistic concern.

## Reference equality guarantees are forfeited by disposing owners and clearing the source file cache

It's not a bug to observe a new object identity for an object whose owner was previously disposed or manually removed by `api.clearSourceFileCache()`. Review comments and tests that assert an ownership or lifetime bug that rely on testing reference equality after disposal or cache clearing will be rejected.
264 changes: 154 additions & 110 deletions packages/typescript/src/api/async/api.ts

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion packages/typescript/src/api/node/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
SyntaxKind,
TokenFlags,
} from "../../ast/index.ts";
import type { API as AsyncAPI } from "../async/api.ts";
import type { CachedSourceFile } from "../sourceFileCache.ts";
import type { API as SyncAPI } from "../sync/api.ts";
import type { TimingCollector } from "../timing.ts";
import { MsgpackReader } from "./msgpack.ts";
import {
Expand Down Expand Up @@ -78,6 +81,8 @@ for (const [index, offset] of Object.values(sourceFileExtendedDataOffsets).entri
const NO_STRUCTURED_DATA = 0xFFFFFFFF;

export class RemoteSourceFile extends RemoteNode implements SourceFileInfo {
readonly api: AsyncAPI<boolean> | SyncAPI<boolean> | undefined;
symbolCache: CachedSourceFile<unknown> | undefined;
readonly nodes: (RemoteNode | RemoteNodeList)[];
readonly _offsetNodes: number;
readonly _offsetStringTableOffsets: number;
Expand All @@ -100,7 +105,12 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo {
private _cachedDiagnosticDirectives: readonly MappedDiagnosticDirective[] | undefined;
private _diagnosticDirectivesRead = false;

constructor(data: Uint8Array, decoder: TextDecoder, timing?: TimingCollector) {
constructor(
data: Uint8Array,
decoder: TextDecoder,
timing?: TimingCollector,
api?: AsyncAPI<boolean> | SyncAPI<boolean> | undefined,
) {
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
const offsetNodes = view.getUint32(HEADER_OFFSET_NODES, true);
super(view, 1, undefined!, undefined!, offsetNodes);
Expand All @@ -112,6 +122,7 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo {
this._offsetStructuredData = view.getUint32(HEADER_OFFSET_STRUCTURED_DATA, true);
this._decoder = decoder;
this._timing = timing;
this.api = api;
this.nodes = Array((view.byteLength - offsetNodes) / NODE_LEN);
this.nodes[1] = this;
// Every node slot is materializable on demand except the nil sentinel at
Expand Down
76 changes: 42 additions & 34 deletions packages/typescript/src/api/proto.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface APIMethodInfo {
releaseSourceFile: APIMethod<ReleaseSourceFileParams, unknown>;
retainSourceFile: APIMethod<RetainSourceFileParams, RetainSourceFileResponse>;
getCachedSourceFile: APIMethod<GetCachedSourceFileParams, SourceFileResponse>;
getSymbolOfDeclaration: APIMethod<GetSymbolOfDeclarationParams, SymbolResponse>;
batchRequests: APIMethod<BatchRequestsParams, BatchRequestsResponse>;
initialize: APIMethod<null, InitializeResponse>;
createSnapshot: APIMethod<CreateSnapshotParams, CreateSnapshotResponse>;
Expand Down Expand Up @@ -241,6 +242,22 @@ export interface SourceFileResponse {
data: string;
}

export interface GetSymbolOfDeclarationParams {
file: SourceFileDescriptor;
index: number;
}

export interface SymbolResponse {
reference: SymbolReference;
name: string;
flags: number;
checkFlags: number;
declarations?: string[] | undefined;
valueDeclaration?: string | undefined;
parent?: CompactSymbolReference | undefined;
exportSymbol?: CompactSymbolReference | undefined;
}

export interface BatchRequestsParams {
requests: readonly BatchRequest[] | null;
continuationToken?: string | undefined;
Expand Down Expand Up @@ -443,17 +460,6 @@ export interface GetSymbolAtPositionParams {
position: number;
}

export interface SymbolResponse {
reference: SymbolReference;
name: string;
flags: number;
checkFlags: number;
declarations?: string[] | undefined;
valueDeclaration?: string | undefined;
parent?: CompactSymbolReference | undefined;
exportSymbol?: CompactSymbolReference | undefined;
}

export interface GetSymbolsAtPositionsParams {
snapshot: number;
project: ProjectId;
Expand Down Expand Up @@ -1104,6 +1110,22 @@ export interface SourceFileDescriptor {
nodeId: string;
}

/** SymbolReference identifies a symbol and its server-resolvable owner. */
export interface SymbolReference extends SymbolOwner {
id: number;
}

/**
* CompactSymbolReference is embedded in other responses. It identifies a cached
* symbol without repeating its owning file's full descriptor: File is the owning source file's
* node ID, or empty for a symbol owned by the response's snapshot. When the client has not cached
* the symbol, it fetches a full SymbolResponse through the corresponding property method.
*/
export interface CompactSymbolReference {
id: number;
file?: string | undefined;
}

export interface BatchRequest {
method:
| "batchRequests"
Expand Down Expand Up @@ -1214,6 +1236,7 @@ export interface BatchRequest {
| "getSuggestionDiagnostics"
| "getSymbolAtLocation"
| "getSymbolAtPosition"
| "getSymbolOfDeclaration"
| "getSymbolOfSourceFile"
| "getSymbolOfType"
| "getSymbolsAtLocations"
Expand Down Expand Up @@ -1391,6 +1414,7 @@ export interface BatchResponse {
| "getSuggestionDiagnostics"
| "getSymbolAtLocation"
| "getSymbolAtPosition"
| "getSymbolOfDeclaration"
| "getSymbolOfSourceFile"
| "getSymbolOfType"
| "getSymbolsAtLocations"
Expand Down Expand Up @@ -1718,22 +1742,6 @@ export interface TranspileOptions {
reportDiagnostics?: boolean | undefined;
}

/** SymbolReference identifies a symbol and its server-resolvable owner. */
export interface SymbolReference extends SymbolOwner {
id: number;
}

/**
* CompactSymbolReference is embedded in other responses. It identifies a cached
* symbol without repeating its owning file's full descriptor: File is the owning source file's
* node ID, or empty for a symbol owned by the response's snapshot. When the client has not cached
* the symbol, it fetches a full SymbolResponse through the corresponding property method.
*/
export interface CompactSymbolReference {
id: number;
file?: string | undefined;
}

export interface PackageId {
name: string;
subModuleName: string;
Expand Down Expand Up @@ -1775,6 +1783,13 @@ export interface EmitOutputFile {
sourceFileName?: string | undefined;
}

export interface SymbolOwner {
kind: SymbolOwnerKind;
file?: SourceFileDescriptor | undefined;
snapshot?: number | undefined;
project?: ProjectId | undefined;
}

export interface CreateSnapshotProgramParams {
rootFiles: readonly DocumentIdentifier[] | null;
compilerOptions: CompilerOptions;
Expand Down Expand Up @@ -1834,13 +1849,6 @@ export interface ModuleResolutionEntry {
result: StaticModuleResolution;
}

export interface SymbolOwner {
kind: SymbolOwnerKind;
file?: SourceFileDescriptor | undefined;
snapshot?: number | undefined;
project?: ProjectId | undefined;
}

/** CompletionEntryLabelDetailsResponse holds additional label display text for a completion entry. */
export interface CompletionEntryLabelDetailsResponse {
detail?: string | undefined;
Expand Down
48 changes: 35 additions & 13 deletions packages/typescript/src/api/sourceFileCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ export interface CachedSourceFile<TSymbol> {
/** Set of snapshot/project or direct-lease ref keys that reference this entry */
refs: Set<string>;
/** Binder symbols owned by this exact source-file incarnation. */
readonly symbols: Map<number, TSymbol>;
readonly symbolsById: Map<number, TSymbol>;
/** Successfully resolved declaration symbols. */
readonly symbolsByDeclarationNodeIndex: Map<number, TSymbol>;
/** In-flight async declaration symbol lookups. */
readonly declarationSymbolRequests: Map<number, Promise<TSymbol>>;
}

/**
Expand Down Expand Up @@ -103,7 +107,13 @@ export class SourceFileCache<TSymbol> {
}
let record = this.findDescriptor(file, entries);
if (!record) {
record = this.addRecord(entries, { descriptor: file, refs: new Set(), symbols: new Map() });
record = this.addRecord(entries, {
descriptor: file,
refs: new Set(),
symbolsById: new Map(),
symbolsByDeclarationNodeIndex: new Map(),
declarationSymbolRequests: new Map(),
});
}
this.retainRecordForSnapshot(record, snapshotId, projectId);
return record;
Expand All @@ -127,17 +137,19 @@ export class SourceFileCache<TSymbol> {
if (!descriptorsEqual(descriptorFromFile(file), record.descriptor)) {
throw new Error(`Source file does not match cached record '${record.descriptor.fileName}'`);
}
return record.file ??= file;
const result = record.file ??= file;
result.symbolCache = record;
return result;
}

getOrCreateSymbol(record: CachedSourceFile<TSymbol>, file: SourceFileDescriptor, id: number, create: () => TSymbol): TSymbol {
if (!descriptorsEqual(file, record.descriptor)) {
throw new Error(`Symbol ${id} does not belong to '${record.descriptor.fileName}'`);
}
let symbol = record.symbols.get(id);
let symbol = record.symbolsById.get(id);
if (!symbol) {
symbol = create();
record.symbols.set(id, symbol);
record.symbolsById.set(id, symbol);
}
return symbol;
}
Expand Down Expand Up @@ -171,14 +183,19 @@ export class SourceFileCache<TSymbol> {
entries = [];
this.cache.set(file.path, entries);
}
const existing = this.find(file, entries);
if (existing) {
existing.refs.add(ref);
existing.file ??= file;
return existing.file;
let record = this.find(file, entries);
if (!record) {
record = (file.symbolCache as CachedSourceFile<TSymbol> | undefined) ?? {
descriptor: descriptorFromFile(file),
refs: new Set(),
symbolsById: new Map(),
symbolsByDeclarationNodeIndex: new Map(),
declarationSymbolRequests: new Map(),
};
this.addRecord(entries, record);
}
this.addRecord(entries, { file, descriptor: descriptorFromFile(file), refs: new Set([ref]), symbols: new Map() });
return file;
record.refs.add(ref);
return this.attachFile(record, file);
}

private addRecord(entries: CachedSourceFile<TSymbol>[], record: CachedSourceFile<TSymbol>): CachedSourceFile<TSymbol> {
Expand Down Expand Up @@ -298,9 +315,14 @@ export class SourceFileCache<TSymbol> {
}

/**
* Clear all entries from the cache.
* Drop local cache ownership without releasing server-side snapshots or leases.
* Caller-held ASTs may keep detached records alive; newly cached records need not
* preserve object identity with those detached records.
*/
clear(): void {
for (const record of this.recordsByNodeId.values()) {
record.refs.clear();
}
this.cache.clear();
this.snapshotProjectPaths.clear();
this.leasePaths.clear();
Expand Down
Loading
Loading