From 77aee43889319d1e51e7f79501be0e715e0ae9bf Mon Sep 17 00:00:00 2001 From: isabel duan Date: Tue, 27 Oct 2026 13:11:53 -0700 Subject: [PATCH 01/13] program init --- packages/typescript/src/api/async/api.ts | 96 +++++- .../typescript/src/api/proto.generated.ts | 133 +++++--- packages/typescript/src/api/sync/api.ts | 306 +++++++++++++++++- packages/typescript/test/async/api.test.ts | 127 ++++++++ .../test/sync/api-generators.test.ts | 18 ++ packages/typescript/test/sync/api.test.ts | 127 ++++++++ tsc/internal/api/proto.go | 29 +- tsc/internal/api/session.go | 99 ++++++ tsc/internal/api/session_program_test.go | 52 +++ tsc/internal/checker/exports.go | 9 + tsc/internal/checker/types.go | 7 + tsc/internal/compiler/program.go | 21 ++ 12 files changed, 973 insertions(+), 51 deletions(-) create mode 100644 tsc/internal/api/session_program_test.go diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 64dbd13a61a75..820915ccd6f78 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -42,6 +42,7 @@ import { import { assertNever } from "../../internal/utils.ts"; import { encodeNode, + sourceFileResponseToUint8Array, uint8ArrayToBase64, } from "../node/encoder.ts"; import { @@ -95,6 +96,7 @@ import type { ModuleResolutionSpec, PackageId, ParsedCommandLine, + ProgramStatisticsResponse, ProjectId, ProjectReference, ProjectResponse, @@ -102,6 +104,7 @@ import type { RawCompilerOptions, ReadConfigFileResponse, ReconfigureSnapshotProgramParams as ProtocolReconfigureSnapshotProgramParams, + RelationCacheSizes, ResolutionMode, ResolvedModule, ResolvedTypeReferenceDirective, @@ -241,6 +244,7 @@ export type { ProjectReference, RawCompilerOptions, ReadConfigFileResponse, + RelationCacheSizes, RequestTiming, ResolutionMode, ResolvedModule, @@ -343,7 +347,6 @@ export interface TranspileOutput { // @sync-only-start // export { all, defer, type APIRequestGenerator, type AnyAPIRequestGenerator, type AllAPIRequestGenerator, type DeferredAPIRequestGenerator, type ExecutedGeneratorsResults } from "./generatorSupport.ts"; // import {executeRequestGenerators, type ExecutedGeneratorsResults, type AnyAPIRequestGenerator} from "./generatorSupport.ts"; -// import { sourceFileResponseToUint8Array } from "../node/encoder.ts"; // @sync-only-end export class API implements FormatDiagnosticsHost { @@ -1712,6 +1715,12 @@ export class LanguageService { } } +export interface ResolvedProjectReference { + commandLine: ParsedCommandLine; + sourceFile: SourceFile; + references?: readonly (ResolvedProjectReference | undefined)[] | undefined; +} + export class Program implements FormatDiagnosticsHost { /** @internal */ readonly snapshotId: number; @@ -1722,6 +1731,7 @@ export class Program implements FormatDiagnost private readonly sourceFileMetadataCache = new Map>(); private ownedSnapshot: Snapshot | undefined; private disposePromise: Promise | undefined; + private resolvedProjectReferences: Promise | undefined; private get client(): Client { return this.project.api.client; @@ -1773,6 +1783,82 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.options; } + async getRootFileNames(): Promise { + return this.project.parsedCommandLine.fileNames; + } + + async getProjectReferences(): Promise { + return this.project.parsedCommandLine.projectReferences; + } + + async getTypeChecker(): Promise { + await this.getStatistics(); + return this.project.checker; + } + + async getSourceFiles(): Promise { + const files: SourceFile[] = []; + for (const fileName of await this.getSourceFileNames()) { + const file = await this.getSourceFile(fileName); + if (!file) { + throw new Error(`Program source file '${fileName}' could not be loaded`); + } + files.push(file); + } + return files; + } + + private async getStatistics(): Promise { + return this.client.apiRequest("getProgramStatistics", { + snapshot: this.snapshotId, + project: this.project.handle, + }); + } + + async getIdentifierCount(): Promise { + return (await this.getStatistics()).identifierCount; + } + + async getInstantiationCount(): Promise { + return (await this.getStatistics()).instantiationCount; + } + + async getNodeCount(): Promise { + return (await this.getStatistics()).nodeCount; + } + + async getSymbolCount(): Promise { + return (await this.getStatistics()).symbolCount; + } + + async getTypeCount(): Promise { + return (await this.getStatistics()).typeCount; + } + + async getRelationCacheSizes(): Promise { + return (await this.getStatistics()).relationCacheSizes; + } + + getResolvedProjectReferences(): Promise { + return this.resolvedProjectReferences ??= this.fetchResolvedProjectReferences(); + } + + private async fetchResolvedProjectReferences(): Promise { + const data = await this.client.apiRequest("getResolvedProjectReferences", { + snapshot: this.snapshotId, + project: this.project.handle, + }); + const projects: ResolvedProjectReference[] = data.projects.map(project => ({ + commandLine: project.commandLine, + sourceFile: new RemoteSourceFile(sourceFileResponseToUint8Array(project.sourceFile)!, this.decoder, undefined, this.project.api) as unknown as SourceFile, + })); + const resolve = (indices: readonly number[] | null | undefined): (ResolvedProjectReference | undefined)[] | undefined => indices?.map(index => index === 0 ? undefined : projects[index - 1]); + for (let i = 0; i < projects.length; i++) { + projects[i].references = resolve(data.projects[i].references); + } + return resolve(data.references); + } + async getSourceFile(file: DocumentIdentifier): Promise { const fileName = resolveFileName(file); const path = this.pathKeyForFileName(fileName); @@ -2072,6 +2158,14 @@ export class Program implements FormatDiagnost return data ?? []; } + async getOptionsDiagnostics(): Promise { + const data = await this.client.apiRequest("getOptionsDiagnostics", { + snapshot: this.snapshotId, + project: this.project.handle, + }); + return data ?? []; + } + /** * Get global (non-file-specific) semantic diagnostics for the project. */ diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 6f49fab3ca6ab..5b753e044042d 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -80,6 +80,9 @@ export interface APIMethodInfo { getNonMissingTypeOfSymbol: APIMethod; getSourceFile: APIMethod; getSourceFileNames: APIMethod; + getProgramStatistics: APIMethod; + getResolvedProjectReferences: APIMethod; + getOptionsDiagnostics: APIMethod; getSourceFileMetadata: APIMethod; getModeForUsageLocation: APIMethod; getModeForResolutionAtIndex: APIMethod; @@ -589,6 +592,59 @@ export interface GetSourceFileNamesParams { project: number; } +/** GetProjectDiagnosticsParams are parameters for project-wide diagnostic methods. */ +export interface GetProjectDiagnosticsParams { + snapshot: number; + project: number; +} + +export interface ProgramStatisticsResponse { + nodeCount: number; + identifierCount: number; + symbolCount: number; + typeCount: number; + instantiationCount: number; + relationCacheSizes: RelationCacheSizes; +} + +/** References use one-based indices into Projects; zero denotes an unresolved reference. */ +export interface ResolvedProjectReferencesResponse { + references?: number[] | undefined; + projects: ResolvedProjectReferenceResponse[]; +} + +/** DiagnosticResponse is the API response for a single diagnostic. */ +export interface DiagnosticResponse { + /** The file name of the file this diagnostic belongs to, if any. */ + fileName?: RootedFilePath | undefined; + /** Pos is the start position of the diagnostic in the source file. */ + pos: number; + /** End is the end position of the diagnostic in the source file. */ + end: number; + /** StartPosition is the zero-based line and UTF-16 character position of Pos. */ + startPosition?: DiagnosticPositionResponse | undefined; + /** EndPosition is the zero-based line and UTF-16 character position of End. */ + endPosition?: DiagnosticPositionResponse | undefined; + /** SourceLines contains the source lines needed to render this diagnostic with context. */ + sourceLines?: DiagnosticSourceLineResponse[] | undefined; + /** Code is the diagnostic error code. */ + code: number; + /** Category is the diagnostic category (error, warning, suggestion, message). */ + category: number; + /** Source is a custom diagnostic-code prefix. An empty value uses the default "TS". */ + source?: string | undefined; + /** Text is the localized diagnostic message text. */ + text: string; + /** ReportsUnnecessary indicates this diagnostic highlights unnecessary code. */ + reportsUnnecessary?: boolean | undefined; + /** ReportsDeprecated indicates this diagnostic highlights deprecated code. */ + reportsDeprecated?: boolean | undefined; + /** MessageChain contains chained diagnostic messages, if any. */ + messageChain?: DiagnosticResponse[] | undefined; + /** RelatedInformation contains related diagnostic information, if any. */ + relatedInformation?: DiagnosticResponse[] | undefined; +} + /** SourceFileMetadata carries program-stored metadata about a single source file. */ export interface SourceFileMetadata { isDefaultLibrary: boolean; @@ -662,12 +718,6 @@ export interface GetResolvedTypeReferenceDirectiveFromReferenceParams { resolutionMode: ModuleKind; } -/** GetProjectDiagnosticsParams are parameters for project-wide diagnostic methods. */ -export interface GetProjectDiagnosticsParams { - snapshot: number; - project: number; -} - export interface ResolveNameParams { snapshot: number; project: number; @@ -1019,38 +1069,6 @@ export interface GetDiagnosticsParams { files?: readonly DocumentIdentifier[] | undefined; } -/** DiagnosticResponse is the API response for a single diagnostic. */ -export interface DiagnosticResponse { - /** The file name of the file this diagnostic belongs to, if any. */ - fileName?: RootedFilePath | undefined; - /** Pos is the start position of the diagnostic in the source file. */ - pos: number; - /** End is the end position of the diagnostic in the source file. */ - end: number; - /** StartPosition is the zero-based line and UTF-16 character position of Pos. */ - startPosition?: DiagnosticPositionResponse | undefined; - /** EndPosition is the zero-based line and UTF-16 character position of End. */ - endPosition?: DiagnosticPositionResponse | undefined; - /** SourceLines contains the source lines needed to render this diagnostic with context. */ - sourceLines?: DiagnosticSourceLineResponse[] | undefined; - /** Code is the diagnostic error code. */ - code: number; - /** Category is the diagnostic category (error, warning, suggestion, message). */ - category: number; - /** Source is a custom diagnostic-code prefix. An empty value uses the default "TS". */ - source?: string | undefined; - /** Text is the localized diagnostic message text. */ - text: string; - /** ReportsUnnecessary indicates this diagnostic highlights unnecessary code. */ - reportsUnnecessary?: boolean | undefined; - /** ReportsDeprecated indicates this diagnostic highlights deprecated code. */ - reportsDeprecated?: boolean | undefined; - /** MessageChain contains chained diagnostic messages, if any. */ - messageChain?: DiagnosticResponse[] | undefined; - /** RelatedInformation contains related diagnostic information, if any. */ - relatedInformation?: DiagnosticResponse[] | undefined; -} - /** PrintNodeParams are the parameters for the printNode method. */ export interface PrintNodeParams { /** base64-encoded binary AST data */ @@ -1229,12 +1247,14 @@ export interface BatchRequest { | "getNullType" | "getNumberType" | "getObjectTypeOfType" + | "getOptionsDiagnostics" | "getOuterTypeParametersOfType" | "getParameterType" | "getParametersOfSignature" | "getParentOfSymbol" | "getParentOfSymbolForChecker" | "getProgramDiagnostics" + | "getProgramStatistics" | "getPropertiesOfType" | "getPropertyOfType" | "getReducedType" @@ -1243,6 +1263,7 @@ export interface BatchRequest { | "getRegularTypeOfType" | "getResolvedModule" | "getResolvedModuleFromModuleSpecifier" + | "getResolvedProjectReferences" | "getResolvedSignature" | "getResolvedTypeReferenceDirective" | "getResolvedTypeReferenceDirectiveFromTypeReferenceDirective" @@ -1411,12 +1432,14 @@ export interface BatchResponse { | "getNullType" | "getNumberType" | "getObjectTypeOfType" + | "getOptionsDiagnostics" | "getOuterTypeParametersOfType" | "getParameterType" | "getParametersOfSignature" | "getParentOfSymbol" | "getParentOfSymbolForChecker" | "getProgramDiagnostics" + | "getProgramStatistics" | "getPropertiesOfType" | "getPropertyOfType" | "getReducedType" @@ -1425,6 +1448,7 @@ export interface BatchResponse { | "getRegularTypeOfType" | "getResolvedModule" | "getResolvedModuleFromModuleSpecifier" + | "getResolvedProjectReferences" | "getResolvedSignature" | "getResolvedTypeReferenceDirective" | "getResolvedTypeReferenceDirectiveFromTypeReferenceDirective" @@ -1663,6 +1687,29 @@ export interface TranspileOptions { reportDiagnostics?: boolean | undefined; } +export interface RelationCacheSizes { + assignable: number; + identity: number; + subtype: number; + strictSubtype: number; +} + +export interface ResolvedProjectReferenceResponse { + commandLine: ConfigFileResponse; + sourceFile: SourceFileResponse; + references?: number[] | undefined; +} + +export interface DiagnosticPositionResponse { + line: number; + character: number; +} + +export interface DiagnosticSourceLineResponse { + line: number; + text: string; +} + export interface PackageId { name: string; subModuleName: string; @@ -1688,16 +1735,6 @@ export interface CompletionEntryResponse { symbol?: SymbolResponse | undefined; } -export interface DiagnosticPositionResponse { - line: number; - character: number; -} - -export interface DiagnosticSourceLineResponse { - line: number; - text: string; -} - export interface EmitOutputFile { fileName: RootedFilePath; text: string; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index f08ef255288b9..50ba3adfbe182 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -59,6 +59,7 @@ import { import { assertNever } from "../../internal/utils.ts"; import { encodeNode, + sourceFileResponseToUint8Array, uint8ArrayToBase64, } from "../node/encoder.ts"; import { @@ -112,6 +113,7 @@ import type { ModuleResolutionSpec, PackageId, ParsedCommandLine, + ProgramStatisticsResponse, ProjectId, ProjectReference, ProjectResponse, @@ -119,6 +121,7 @@ import type { RawCompilerOptions, ReadConfigFileResponse, ReconfigureSnapshotProgramParams as ProtocolReconfigureSnapshotProgramParams, + RelationCacheSizes, ResolutionMode, ResolvedModule, ResolvedTypeReferenceDirective, @@ -258,6 +261,7 @@ export type { ProjectReference, RawCompilerOptions, ReadConfigFileResponse, + RelationCacheSizes, RequestTiming, ResolutionMode, ResolvedModule, @@ -358,7 +362,6 @@ export interface TranspileOutput { } export { all, type AllAPIRequestGenerator, type AnyAPIRequestGenerator, type APIRequestGenerator, defer, type DeferredAPIRequestGenerator, type ExecutedGeneratorsResults } from "./generatorSupport.ts"; -import { sourceFileResponseToUint8Array } from "../node/encoder.ts"; import { type AnyAPIRequestGenerator, type ExecutedGeneratorsResults, @@ -2899,6 +2902,12 @@ export class LanguageService { } } +export interface ResolvedProjectReference { + commandLine: ParsedCommandLine; + sourceFile: SourceFile; + references?: readonly (ResolvedProjectReference | undefined)[] | undefined; +} + export class Program implements FormatDiagnosticsHost { /** @internal */ readonly snapshotId: number; @@ -2909,6 +2918,7 @@ export class Program implements FormatDiagnost private readonly sourceFileMetadataCache = new Map(); private ownedSnapshot: Snapshot | undefined; private disposePromise: void | undefined; + private resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined | undefined; private get client(): Client { return this.project.api.client; @@ -2988,6 +2998,275 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.options; } + get getRootFileNames(): { + (): readonly RootedFilePath[]; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getRootFileNames", + function (): readonly RootedFilePath[] { + return owner.project.parsedCommandLine.fileNames; + }, + function* (): Generator { + return owner.project.parsedCommandLine.fileNames; + }, + ); + } + + get getProjectReferences(): { + (): readonly ProjectReference[] | undefined; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getProjectReferences", + function (): readonly ProjectReference[] | undefined { + return owner.project.parsedCommandLine.projectReferences; + }, + function* (): Generator { + return owner.project.parsedCommandLine.projectReferences; + }, + ); + } + + get getTypeChecker(): { + (): Checker; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getTypeChecker", + function (): Checker { + owner.getStatistics(); + return owner.project.checker; + }, + function* (): Generator { + yield* owner.getStatistics.gen(); + return owner.project.checker; + }, + ); + } + + get getSourceFiles(): { + (): readonly SourceFile[]; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getSourceFiles", + function (): readonly SourceFile[] { + const files: SourceFile[] = []; + for (const fileName of owner.getSourceFileNames()) { + const file = owner.getSourceFile(fileName); + if (!file) { + throw new Error(`Program source file '${fileName}' could not be loaded`); + } + files.push(file); + } + return files; + }, + function* (): Generator { + const files: SourceFile[] = []; + for (const fileName of (yield* owner.getSourceFileNames.gen())) { + const file = yield* owner.getSourceFile.gen(fileName); + if (!file) { + throw new Error(`Program source file '${fileName}' could not be loaded`); + } + files.push(file); + } + return files; + }, + ); + } + + private get getStatistics(): { + (): ProgramStatisticsResponse; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getStatistics", + function (): ProgramStatisticsResponse { + return owner.client.apiRequest("getProgramStatistics", { + snapshot: owner.snapshotId, + project: owner.project.handle, + }); + }, + function* (): Generator { + return yield* apiRequest("getProgramStatistics", { + snapshot: owner.snapshotId, + project: owner.project.handle, + }); + }, + ); + } + + get getIdentifierCount(): { + (): number; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getIdentifierCount", + function (): number { + return (owner.getStatistics()).identifierCount; + }, + function* (): Generator { + return (yield* owner.getStatistics.gen()).identifierCount; + }, + ); + } + + get getInstantiationCount(): { + (): number; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getInstantiationCount", + function (): number { + return (owner.getStatistics()).instantiationCount; + }, + function* (): Generator { + return (yield* owner.getStatistics.gen()).instantiationCount; + }, + ); + } + + get getNodeCount(): { + (): number; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getNodeCount", + function (): number { + return (owner.getStatistics()).nodeCount; + }, + function* (): Generator { + return (yield* owner.getStatistics.gen()).nodeCount; + }, + ); + } + + get getSymbolCount(): { + (): number; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getSymbolCount", + function (): number { + return (owner.getStatistics()).symbolCount; + }, + function* (): Generator { + return (yield* owner.getStatistics.gen()).symbolCount; + }, + ); + } + + get getTypeCount(): { + (): number; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getTypeCount", + function (): number { + return (owner.getStatistics()).typeCount; + }, + function* (): Generator { + return (yield* owner.getStatistics.gen()).typeCount; + }, + ); + } + + get getRelationCacheSizes(): { + (): RelationCacheSizes; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getRelationCacheSizes", + function (): RelationCacheSizes { + return (owner.getStatistics()).relationCacheSizes; + }, + function* (): Generator { + return (yield* owner.getStatistics.gen()).relationCacheSizes; + }, + ); + } + + get getResolvedProjectReferences(): { + (): readonly (ResolvedProjectReference | undefined)[] | undefined; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getResolvedProjectReferences", + function (): readonly (ResolvedProjectReference | undefined)[] | undefined { + return owner.resolvedProjectReferences ??= owner.fetchResolvedProjectReferences(); + }, + function* (): Generator { + return owner.resolvedProjectReferences ??= yield* owner.fetchResolvedProjectReferences.gen(); + }, + ); + } + + private get fetchResolvedProjectReferences(): { + (): readonly (ResolvedProjectReference | undefined)[] | undefined; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "fetchResolvedProjectReferences", + function (): readonly (ResolvedProjectReference | undefined)[] | undefined { + const data = owner.client.apiRequest("getResolvedProjectReferences", { + snapshot: owner.snapshotId, + project: owner.project.handle, + }); + const projects: ResolvedProjectReference[] = data.projects.map(project => ({ + commandLine: project.commandLine, + sourceFile: new RemoteSourceFile(sourceFileResponseToUint8Array(project.sourceFile)!, owner.decoder, undefined, owner.project.api) as unknown as SourceFile, + })); + const resolve = (indices: readonly number[] | null | undefined): (ResolvedProjectReference | undefined)[] | undefined => indices?.map(index => index === 0 ? undefined : projects[index - 1]); + for (let i = 0; i < projects.length; i++) { + projects[i].references = resolve(data.projects[i].references); + } + return resolve(data.references); + }, + function* (): Generator { + const data = yield* apiRequest("getResolvedProjectReferences", { + snapshot: owner.snapshotId, + project: owner.project.handle, + }); + const projects: ResolvedProjectReference[] = data.projects.map(project => ({ + commandLine: project.commandLine, + sourceFile: new RemoteSourceFile(sourceFileResponseToUint8Array(project.sourceFile)!, owner.decoder, undefined, owner.project.api) as unknown as SourceFile, + })); + const resolve = (indices: readonly number[] | null | undefined): (ResolvedProjectReference | undefined)[] | undefined => indices?.map(index => index === 0 ? undefined : projects[index - 1]); + for (let i = 0; i < projects.length; i++) { + projects[i].references = resolve(data.projects[i].references); + } + return resolve(data.references); + }, + ); + } + get getSourceFile(): { (file: DocumentIdentifier): SourceFile | undefined; gen(file: DocumentIdentifier): Generator; @@ -3710,6 +3989,31 @@ export class Program implements FormatDiagnost ); } + get getOptionsDiagnostics(): { + (): readonly Diagnostic[]; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getOptionsDiagnostics", + function (): readonly Diagnostic[] { + const data = owner.client.apiRequest("getOptionsDiagnostics", { + snapshot: owner.snapshotId, + project: owner.project.handle, + }); + return data ?? []; + }, + function* (): Generator { + const data = yield* apiRequest("getOptionsDiagnostics", { + snapshot: owner.snapshotId, + project: owner.project.handle, + }); + return data ?? []; + }, + ); + } + /** * Get global (non-file-specific) semantic diagnostics for the project. */ diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 379f07b6b68f7..a29b6388dfb17 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -2650,6 +2650,133 @@ describe("SourceFile", { concurrency }, () => { ); }); + test("Program accessors distinguish root files from source files and reuse the checker", async () => { + await using api = spawnAPI({ + "/src/main.ts": `import { value } from "./dependency"; export const result = value;`, + "/src/dependency.ts": `export const value = 1;`, + }); + await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); + assert.deepEqual(await program.getRootFileNames(), ["/src/main.ts"]); + const files = await program.getSourceFiles(); + assert.deepEqual(files.map(file => file.fileName), ["/src/dependency.ts", "/src/main.ts"]); + assert.strictEqual(files[1], await program.getSourceFile("/src/main.ts")); + const checker = await program.getTypeChecker(); + assert.strictEqual(await program.getTypeChecker(), checker); + assert.ok(await checker.getSymbolAtLocation(cast(files[1].statements[1], isVariableStatement).declarationList.declarations[0].name)); + assert.equal(await program.getProjectReferences(), undefined); + assert.equal(await program.getResolvedProjectReferences(), undefined); + }); + + test("Program counters reflect work on its type checker without forcing diagnostics", async () => { + await using api = spawnAPI({ + "/src/main.ts": `type Box = { value: T }; +export const box: Box = { value: "hello" }; +export const other: { value: string } = box;`, + }); + await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); + const identifiers = await program.getIdentifierCount(); + const nodes = await program.getNodeCount(); + const symbols = await program.getSymbolCount(); + const types = await program.getTypeCount(); + const instantiations = await program.getInstantiationCount(); + const relations = await program.getRelationCacheSizes(); + assert.ok(identifiers > 0); + assert.ok(nodes > identifiers); + assert.ok(symbols > 0); + assert.ok(types > 0); + assert.equal(instantiations, 0); + assert.deepEqual(relations, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); + + const file = (await program.getSourceFile("/src/main.ts"))!; + const checker = await program.getTypeChecker(); + const declaration = cast(file.statements[1], isVariableStatement).declarationList.declarations[0]; + const type = await checker.getTypeAtLocation(declaration.name); + await type.getProperties(); + assert.ok(await program.getInstantiationCount() > instantiations); + assert.ok(await program.getTypeCount() > types); + assert.ok(await program.getSymbolCount() >= symbols); + const other = cast(file.statements[2], isVariableStatement).declarationList.declarations[0]; + await checker.isTypeAssignableTo(type, await checker.getTypeAtLocation(other.name)); + assert.ok((await program.getRelationCacheSizes()).assignable > relations.assignable); + assert.equal(await program.getIdentifierCount(), identifiers); + assert.equal(await program.getNodeCount(), nodes); + }); + + test("Program accessors handle empty programs and missing root files", async () => { + await using api = spawnAPI({}); + await using empty = await api.createProgram([], { noLib: true }); + assert.deepEqual(await empty.getRootFileNames(), []); + assert.deepEqual(await empty.getSourceFiles(), []); + assert.equal(await empty.getNodeCount(), 0); + assert.equal(await empty.getIdentifierCount(), 0); + assert.equal(await empty.getProjectReferences(), undefined); + assert.equal(await empty.getResolvedProjectReferences(), undefined); + assert.deepEqual(await empty.getOptionsDiagnostics(), []); + + await using noReferences = await api.createProgram([], { noLib: true }, { projectReferences: [] }); + assert.deepEqual(await noReferences.getProjectReferences(), []); + assert.deepEqual(await noReferences.getResolvedProjectReferences(), []); + + await using missing = await api.createProgram(["/missing.ts"], { noLib: true }); + assert.deepEqual(await missing.getRootFileNames(), ["/missing.ts"]); + assert.deepEqual(await missing.getSourceFiles(), []); + assert.ok((await missing.getOptionsDiagnostics()).some(diagnostic => diagnostic.code === 6053)); + }); + + test("Program options diagnostics exclude source-file and config parsing diagnostics", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ + compilerOptions: { noLib: true, strictNullChecks: false, exactOptionalPropertyTypes: true, unknownOption: true }, + files: ["src/main.ts"], + }), + "/src/main.ts": `/// +const value: string = 1;`, + }); + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; + const diagnostics = await program.getOptionsDiagnostics(); + assert.ok(diagnostics.some(diagnostic => diagnostic.code === 5052)); + assert.ok(diagnostics.every(diagnostic => !diagnostic.fileName || diagnostic.fileName === "/tsconfig.json")); + assert.ok(!(diagnostics.some(diagnostic => diagnostic.code === 5023 || diagnostic.code === 6053 || diagnostic.code === 2322))); + assert.ok((await program.getConfigFileParsingDiagnostics()).some(diagnostic => diagnostic.code === 5023)); + }); + + test("Program resolved project references preserve missing, nested, shared, and circular references", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ files: [], references: [{ path: "./a" }, { path: "./missing" }, { path: "./b" }, { path: "./a" }] }), + "/a/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, files: ["index.ts"], references: [{ path: "../b" }] }), + "/a/index.ts": "export const a = 1;", + "/b/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, files: ["index.ts"], references: [{ path: "../a", circular: true }, { path: "../missing" }] }), + "/b/index.ts": "export const b = 1;", + }); + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; + const references = await program.getProjectReferences(); + assert.equal(references?.length, 4); + const resolved = await program.getResolvedProjectReferences(); + assert.equal(resolved?.length, 4); + assert.equal(resolved![1], undefined); + const a = resolved![0]!; + const b = resolved![2]!; + assert.strictEqual(resolved![3], a); + assert.strictEqual(a.references![0], b); + assert.strictEqual(b.references![0], a); + assert.equal(b.references![1], undefined); + assert.equal(a.sourceFile.fileName, "/a/tsconfig.json"); + assert.equal(a.commandLine.options.composite, true); + assert.deepEqual(a.commandLine.fileNames, ["/a/index.ts"]); + assert.strictEqual((await program.getResolvedProjectReferences())![0], a); + + await using synthetic = await api.createProgram([], { noLib: true }, { + projectReferences: [{ path: "/a/tsconfig.json" }, { path: "/missing/tsconfig.json" }], + }); + assert.equal((await synthetic.getProjectReferences())!.length, 2); + const syntheticResolved = await synthetic.getResolvedProjectReferences(); + assert.equal(syntheticResolved!.length, 2); + assert.equal(syntheticResolved![0]!.sourceFile.fileName, "/a/tsconfig.json"); + assert.equal(syntheticResolved![1], undefined); + }); + test("getSourceFileNames returns all program files, not just root files", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 314052b63f789..4c33e43ab3797 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -172,6 +172,8 @@ const privateGeneratorGetters = new Set([ "NodeHandle.fetchOwnerFile", "NodeHandle.resolveInFile", "Program.disposeWorker", + "Program.getStatistics", + "Program.fetchResolvedProjectReferences", "Program.fetchSourceFileMetadata", "Program.getSourceFileWorker", "Snapshot.disposeWorker", @@ -1615,6 +1617,13 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Program", "getResolvedTypeReferenceDirectiveFromTypeReferenceDirective", program.getResolvedTypeReferenceDirectiveFromTypeReferenceDirective, assertDeepEquivalent, typeReferenceDirective, "/src/index.ts"), parityCase("Program", "getSourceFileByPath", program.getSourceFileByPath, assertOptionalSourceFilesEquivalent, indexFile.path), parityCase("Program", "getSourceFileNames", program.getSourceFileNames, assertDeepEquivalent), + parityCase("Program", "getRootFileNames", program.getRootFileNames, assertDeepEquivalent), + parityCase("Program", "getSourceFiles", program.getSourceFiles, (actual, expected, message) => { + assertArrayElementsEquivalent(actual, expected, assertSourceFilesEquivalent, message); + }), + parityCase("Program", "getProjectReferences", program.getProjectReferences, assertDeepEquivalent), + parityCase("Program", "getResolvedProjectReferences", program.getResolvedProjectReferences, assertDeepEquivalent), + parityCase("Program", "getTypeChecker", program.getTypeChecker, assert.strictEqual), parityCase("Program", "getSourceFileMetadata", program.getSourceFileMetadata, assertDeepEquivalent, "/src/index.ts"), parityCase("Program", "getSourceFileMetadataByPath", program.getSourceFileMetadataByPath, assertDeepEquivalent, indexFile.path), parityCase("Program", "isSourceFileFromExternalLibrary", program.isSourceFileFromExternalLibrary, assertDeepEquivalent, indexFile), @@ -1631,6 +1640,7 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Program", "getSuggestionDiagnostics", program.getSuggestionDiagnostics, assertDeepEquivalent, "/src/suggestions.ts"), parityCase("Program", "getDeclarationDiagnostics", program.getDeclarationDiagnostics, assertDeepEquivalent, "/src/index.ts"), parityCase("Program", "getProgramDiagnostics", program.getProgramDiagnostics, assertDeepEquivalent), + parityCase("Program", "getOptionsDiagnostics", program.getOptionsDiagnostics, assertDeepEquivalent), parityCase("Program", "getGlobalDiagnostics", program.getGlobalDiagnostics, assertDeepEquivalent), parityCase("Program", "getConfigFileParsingDiagnostics", program.getConfigFileParsingDiagnostics, assertDeepEquivalent), parityCase("Program", "emit", program.emit, assertDeepEquivalent), @@ -1788,6 +1798,14 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => ]; runParityBatch(api, cases); + runParityBatch(api, [ + parityCase("Program", "getIdentifierCount", program.getIdentifierCount, assertDeepEquivalent), + parityCase("Program", "getNodeCount", program.getNodeCount, assertDeepEquivalent), + parityCase("Program", "getSymbolCount", program.getSymbolCount, assertDeepEquivalent), + parityCase("Program", "getTypeCount", program.getTypeCount, assertDeepEquivalent), + parityCase("Program", "getInstantiationCount", program.getInstantiationCount, assertDeepEquivalent), + parityCase("Program", "getRelationCacheSizes", program.getRelationCacheSizes, assertDeepEquivalent), + ]); assert.deepEqual(temporaryProjects, ["/tsconfig.json", "/tsconfig.json"]); const snapshotGeneratorAPI = spawnAPI(parityFiles); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index f048e8ede87e6..2cccd99abedce 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2508,6 +2508,133 @@ describe("SourceFile", { concurrency }, () => { ); }); + test("Program accessors distinguish root files from source files and reuse the checker", () => { + using api = spawnAPI({ + "/src/main.ts": `import { value } from "./dependency"; export const result = value;`, + "/src/dependency.ts": `export const value = 1;`, + }); + using program = api.createProgram(["/src/main.ts"], { noLib: true }); + assert.deepEqual(program.getRootFileNames(), ["/src/main.ts"]); + const files = program.getSourceFiles(); + assert.deepEqual(files.map(file => file.fileName), ["/src/dependency.ts", "/src/main.ts"]); + assert.strictEqual(files[1], program.getSourceFile("/src/main.ts")); + const checker = program.getTypeChecker(); + assert.strictEqual(program.getTypeChecker(), checker); + assert.ok(checker.getSymbolAtLocation(cast(files[1].statements[1], isVariableStatement).declarationList.declarations[0].name)); + assert.equal(program.getProjectReferences(), undefined); + assert.equal(program.getResolvedProjectReferences(), undefined); + }); + + test("Program counters reflect work on its type checker without forcing diagnostics", () => { + using api = spawnAPI({ + "/src/main.ts": `type Box = { value: T }; +export const box: Box = { value: "hello" }; +export const other: { value: string } = box;`, + }); + using program = api.createProgram(["/src/main.ts"], { noLib: true }); + const identifiers = program.getIdentifierCount(); + const nodes = program.getNodeCount(); + const symbols = program.getSymbolCount(); + const types = program.getTypeCount(); + const instantiations = program.getInstantiationCount(); + const relations = program.getRelationCacheSizes(); + assert.ok(identifiers > 0); + assert.ok(nodes > identifiers); + assert.ok(symbols > 0); + assert.ok(types > 0); + assert.equal(instantiations, 0); + assert.deepEqual(relations, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); + + const file = (program.getSourceFile("/src/main.ts"))!; + const checker = program.getTypeChecker(); + const declaration = cast(file.statements[1], isVariableStatement).declarationList.declarations[0]; + const type = checker.getTypeAtLocation(declaration.name); + type.getProperties(); + assert.ok(program.getInstantiationCount() > instantiations); + assert.ok(program.getTypeCount() > types); + assert.ok(program.getSymbolCount() >= symbols); + const other = cast(file.statements[2], isVariableStatement).declarationList.declarations[0]; + checker.isTypeAssignableTo(type, checker.getTypeAtLocation(other.name)); + assert.ok((program.getRelationCacheSizes()).assignable > relations.assignable); + assert.equal(program.getIdentifierCount(), identifiers); + assert.equal(program.getNodeCount(), nodes); + }); + + test("Program accessors handle empty programs and missing root files", () => { + using api = spawnAPI({}); + using empty = api.createProgram([], { noLib: true }); + assert.deepEqual(empty.getRootFileNames(), []); + assert.deepEqual(empty.getSourceFiles(), []); + assert.equal(empty.getNodeCount(), 0); + assert.equal(empty.getIdentifierCount(), 0); + assert.equal(empty.getProjectReferences(), undefined); + assert.equal(empty.getResolvedProjectReferences(), undefined); + assert.deepEqual(empty.getOptionsDiagnostics(), []); + + using noReferences = api.createProgram([], { noLib: true }, { projectReferences: [] }); + assert.deepEqual(noReferences.getProjectReferences(), []); + assert.deepEqual(noReferences.getResolvedProjectReferences(), []); + + using missing = api.createProgram(["/missing.ts"], { noLib: true }); + assert.deepEqual(missing.getRootFileNames(), ["/missing.ts"]); + assert.deepEqual(missing.getSourceFiles(), []); + assert.ok((missing.getOptionsDiagnostics()).some(diagnostic => diagnostic.code === 6053)); + }); + + test("Program options diagnostics exclude source-file and config parsing diagnostics", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ + compilerOptions: { noLib: true, strictNullChecks: false, exactOptionalPropertyTypes: true, unknownOption: true }, + files: ["src/main.ts"], + }), + "/src/main.ts": `/// +const value: string = 1;`, + }); + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; + const diagnostics = program.getOptionsDiagnostics(); + assert.ok(diagnostics.some(diagnostic => diagnostic.code === 5052)); + assert.ok(diagnostics.every(diagnostic => !diagnostic.fileName || diagnostic.fileName === "/tsconfig.json")); + assert.ok(!(diagnostics.some(diagnostic => diagnostic.code === 5023 || diagnostic.code === 6053 || diagnostic.code === 2322))); + assert.ok((program.getConfigFileParsingDiagnostics()).some(diagnostic => diagnostic.code === 5023)); + }); + + test("Program resolved project references preserve missing, nested, shared, and circular references", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ files: [], references: [{ path: "./a" }, { path: "./missing" }, { path: "./b" }, { path: "./a" }] }), + "/a/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, files: ["index.ts"], references: [{ path: "../b" }] }), + "/a/index.ts": "export const a = 1;", + "/b/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, files: ["index.ts"], references: [{ path: "../a", circular: true }, { path: "../missing" }] }), + "/b/index.ts": "export const b = 1;", + }); + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; + const references = program.getProjectReferences(); + assert.equal(references?.length, 4); + const resolved = program.getResolvedProjectReferences(); + assert.equal(resolved?.length, 4); + assert.equal(resolved![1], undefined); + const a = resolved![0]!; + const b = resolved![2]!; + assert.strictEqual(resolved![3], a); + assert.strictEqual(a.references![0], b); + assert.strictEqual(b.references![0], a); + assert.equal(b.references![1], undefined); + assert.equal(a.sourceFile.fileName, "/a/tsconfig.json"); + assert.equal(a.commandLine.options.composite, true); + assert.deepEqual(a.commandLine.fileNames, ["/a/index.ts"]); + assert.strictEqual((program.getResolvedProjectReferences())![0], a); + + using synthetic = api.createProgram([], { noLib: true }, { + projectReferences: [{ path: "/a/tsconfig.json" }, { path: "/missing/tsconfig.json" }], + }); + assert.equal((synthetic.getProjectReferences())!.length, 2); + const syntheticResolved = synthetic.getResolvedProjectReferences(); + assert.equal(syntheticResolved!.length, 2); + assert.equal(syntheticResolved![0]!.sourceFile.fileName, "/a/tsconfig.json"); + assert.equal(syntheticResolved![1], undefined); + }); + test("getSourceFileNames returns all program files, not just root files", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index c03e7ff85ed46..40cc134f6463f 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -111,6 +111,9 @@ const ( MethodGetNonMissingTypeOfSymbol Method = "getNonMissingTypeOfSymbol" MethodGetSourceFile Method = "getSourceFile" MethodGetSourceFileNames Method = "getSourceFileNames" + MethodGetProgramStatistics Method = "getProgramStatistics" + MethodGetResolvedProjectReferences Method = "getResolvedProjectReferences" + MethodGetOptionsDiagnostics Method = "getOptionsDiagnostics" MethodGetSourceFileMetadata Method = "getSourceFileMetadata" MethodGetModeForUsageLocation Method = "getModeForUsageLocation" MethodGetModeForResolutionAtIndex Method = "getModeForResolutionAtIndex" @@ -770,6 +773,9 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetSuggestionDiagnostics: unmarshallerFor[GetDiagnosticsParams], MethodGetDeclarationDiagnostics: unmarshallerFor[GetDiagnosticsParams], MethodGetProgramDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], + MethodGetProgramStatistics: unmarshallerFor[GetProjectDiagnosticsParams], + MethodGetResolvedProjectReferences: unmarshallerFor[GetProjectDiagnosticsParams], + MethodGetOptionsDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetGlobalDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetConfigFileParsingDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], MethodStartCPUProfile: unmarshallerFor[ProfileParams], @@ -1014,13 +1020,34 @@ type ConfigFileResponse struct { FileNames []tspath.RootedFilePath `json:"fileNames" nonnil:"true"` Options *core.CompilerOptions `json:"options" nonnil:"true"` BuildOptions *core.BuildOptions `json:"buildOptions,omitempty"` - ProjectReferences []*core.ProjectReference `json:"projectReferences,omitempty"` + ProjectReferences []*core.ProjectReference `json:"projectReferences,omitzero"` TypeAcquisition *core.TypeAcquisition `json:"typeAcquisition,omitempty"` CompileOnSave *bool `json:"compileOnSave,omitempty"` Raw any `json:"raw,omitempty"` Errors []*DiagnosticResponse `json:"errors" nonnil:"true"` } +type ProgramStatisticsResponse struct { + NodeCount int `json:"nodeCount"` + IdentifierCount int `json:"identifierCount"` + SymbolCount int `json:"symbolCount"` + TypeCount uint32 `json:"typeCount"` + InstantiationCount uint32 `json:"instantiationCount"` + RelationCacheSizes checker.RelationCacheSizes `json:"relationCacheSizes"` +} + +// References use one-based indices into Projects; zero denotes an unresolved reference. +type ResolvedProjectReferencesResponse struct { + References []int `json:"references,omitzero"` + Projects []*ResolvedProjectReferenceResponse `json:"projects" nonnil:"true"` +} + +type ResolvedProjectReferenceResponse struct { + CommandLine *ConfigFileResponse `json:"commandLine" nonnil:"true"` + SourceFile *SourceFileResponse `json:"sourceFile" nonnil:"true"` + References []int `json:"references,omitzero"` +} + type ReadConfigFileResponse struct { Config any `json:"config"` Error *DiagnosticResponse `json:"error,omitempty"` diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index c88ef9a1b3bee..6f001e22a5f88 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -951,6 +951,12 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetSourceFile(ctx, parsed.(*GetSourceFileParams)) case string(MethodGetSourceFileNames): return s.handleGetSourceFileNames(ctx, parsed.(*GetSourceFileNamesParams)) + case string(MethodGetProgramStatistics): + return s.handleGetProgramStatistics(ctx, parsed.(*GetProjectDiagnosticsParams)) + case string(MethodGetResolvedProjectReferences): + return s.handleGetResolvedProjectReferences(ctx, parsed.(*GetProjectDiagnosticsParams)) + case string(MethodGetOptionsDiagnostics): + return s.handleGetOptionsDiagnostics(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodGetSourceFileMetadata): return s.handleGetSourceFileMetadata(ctx, parsed.(*GetSourceFileParams)) case string(MethodGetModeForUsageLocation): @@ -2348,6 +2354,99 @@ func transpileOutput(ctx context.Context, input string, options TranspileOptions }, nil } +func (s *Session) handleGetProgramStatistics(ctx context.Context, params *GetProjectDiagnosticsParams) (*ProgramStatisticsResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + result := &ProgramStatisticsResponse{ + SymbolCount: int(setup.checker.SymbolCount), + TypeCount: setup.checker.TypeCount, + InstantiationCount: setup.checker.TotalInstantiationCount, + RelationCacheSizes: setup.checker.GetRelationCacheSizes(), + } + for _, file := range setup.program.GetSourceFiles() { + result.NodeCount += file.NodeCount + result.IdentifierCount += file.IdentifierCount + result.SymbolCount += file.SymbolCount + } + return result, nil +} + +func (s *Session) handleGetResolvedProjectReferences(ctx context.Context, params *GetProjectDiagnosticsParams) (*ResolvedProjectReferencesResponse, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + program, err := sd.getProgram(params.Project) + if err != nil { + return nil, err + } + result := &ResolvedProjectReferencesResponse{Projects: []*ResolvedProjectReferenceResponse{}} + indices := map[*tsoptions.ParsedCommandLine]int{} + var visit func(config *tsoptions.ParsedCommandLine) (int, error) + visit = func(config *tsoptions.ParsedCommandLine) (int, error) { + if config == nil { + return 0, nil + } + if index, ok := indices[config]; ok { + return index, nil + } + if config.ConfigFile == nil || config.ConfigFile.SourceFile == nil { + return 0, errors.New("resolved project reference has no config source file") + } + file := config.ConfigFile.SourceFile + data, _, encodeErr := encoder.EncodeSourceFile(file) + if encodeErr != nil { + return 0, fmt.Errorf("failed to encode project reference config: %w", encodeErr) + } + encoder.SetSourceFileID(data, sourceFileNodeID(file)) + response := &ResolvedProjectReferenceResponse{ + CommandLine: NewConfigFileResponse(config), + SourceFile: &SourceFileResponse{Data: base64.StdEncoding.EncodeToString(data)}, + } + result.Projects = append(result.Projects, response) + index := len(result.Projects) + indices[config] = index + if config.ProjectReferences() != nil { + response.References = make([]int, len(config.ProjectReferences())) + for i, path := range config.ResolvedProjectReferencePaths() { + child, _ := program.GetResolvedProjectReferenceFor(program.PathKeyForFileName(path)) + response.References[i], err = visit(child) + if err != nil { + return 0, err + } + } + } + return index, nil + } + if program.CommandLine().ProjectReferences() != nil { + references := program.GetResolvedProjectReferences() + result.References = make([]int, len(references)) + for i, config := range references { + result.References[i], err = visit(config) + if err != nil { + return nil, err + } + } + } + return result, nil +} + +// @gen-proto-nullable +func (s *Session) handleGetOptionsDiagnostics(ctx context.Context, params *GetProjectDiagnosticsParams) ([]*DiagnosticResponse, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + program, err := sd.getProgram(params.Project) + if err != nil { + return nil, err + } + return NewDiagnosticResponses(program.GetOptionsDiagnostics()), nil +} + // handleGetSourceFile returns a source file from a project within a snapshot. // @gen-proto-result: SourceFileResponse // @gen-proto-nullable diff --git a/tsc/internal/api/session_program_test.go b/tsc/internal/api/session_program_test.go new file mode 100644 index 0000000000000..5cbd6758e3e4a --- /dev/null +++ b/tsc/internal/api/session_program_test.go @@ -0,0 +1,52 @@ +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 TestProgramStatisticsUseAPIChecker(t *testing.T) { + t.Parallel() + + const fileName = "/home/projects/p/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{ + fileName: "export const value = 1;", + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: fileName}}, + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + }}, + }) + assert.NilError(t, err) + params := &GetProjectDiagnosticsParams{Snapshot: response.Snapshot, Project: response.Projects[0].Handle} + setup, err := session.setupChecker(context.Background(), params.Snapshot, params.Project) + assert.NilError(t, err) + file := setup.program.GetSourceFile(fileName) + setup.checker.GetTypeAtLocation(file.Statements.Nodes[0]) + expected := ProgramStatisticsResponse{ + NodeCount: file.NodeCount, + IdentifierCount: file.IdentifierCount, + SymbolCount: file.SymbolCount + int(setup.checker.SymbolCount), + TypeCount: setup.checker.TypeCount, + InstantiationCount: setup.checker.TotalInstantiationCount, + RelationCacheSizes: setup.checker.GetRelationCacheSizes(), + } + setup.done() + + actual, err := session.handleGetProgramStatistics(context.Background(), params) + assert.NilError(t, err) + assert.DeepEqual(t, *actual, expected) + _, err = session.handleGetProgramStatistics(context.Background(), &GetProjectDiagnosticsParams{ + Snapshot: response.Snapshot, Project: project.Handle(999999), + }) + assert.ErrorContains(t, err, "not found") +} diff --git a/tsc/internal/checker/exports.go b/tsc/internal/checker/exports.go index bce0ba48cfa20..497b2c49850ec 100644 --- a/tsc/internal/checker/exports.go +++ b/tsc/internal/checker/exports.go @@ -8,6 +8,15 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/diagnostics" ) +func (c *Checker) GetRelationCacheSizes() RelationCacheSizes { + return RelationCacheSizes{ + Assignable: c.assignableRelation.size(), + Identity: c.identityRelation.size(), + Subtype: c.subtypeRelation.size(), + StrictSubtype: c.strictSubtypeRelation.size(), + } +} + func (c *Checker) GetStringType() *Type { return c.stringType } diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index f119288a21744..83ac6bd0408de 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -14,6 +14,13 @@ import ( //go:generate npx hereby generate:checker +type RelationCacheSizes struct { + Assignable int `json:"assignable"` + Identity int `json:"identity"` + Subtype int `json:"subtype"` + StrictSubtype int `json:"strictSubtype"` +} + // ParseFlags type ParseFlags uint32 diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 9946362def3ad..96c94196b8065 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -857,6 +857,27 @@ func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic { )) } +func (p *Program) GetOptionsDiagnostics() []*ast.Diagnostic { + configFiles := map[tspath.PathKey]*ast.SourceFile{} + addConfig := func(config *tsoptions.ParsedCommandLine) { + if config != nil && config.ConfigFile != nil { + configFiles[config.ConfigFile.SourceFile.PathKey()] = config.ConfigFile.SourceFile + } + } + addConfig(p.CommandLine()) + p.RangeResolvedProjectReference(func(_ tspath.PathKey, config *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { + addConfig(config) + return true + }) + result := core.Filter(p.GetProgramDiagnostics(), func(d *ast.Diagnostic) bool { + return d.File() == nil || configFiles[d.File().PathKey()] != nil + }) + for _, file := range configFiles { + result = append(result, p.includeProcessor.getDiagnostics(p).GetDiagnosticsForFile(file)...) + } + return SortAndDeduplicateDiagnostics(result) +} + func (p *Program) collectContentMapperOptionDiagnostics() { project := p.ContentMapperProject() if project == nil { From c2817fedf1f27a7396cdef27374e92a87447492c Mon Sep 17 00:00:00 2001 From: isabel duan Date: Tue, 27 Oct 2026 23:27:16 -0700 Subject: [PATCH 02/13] project --- packages/typescript/src/api/async/api.ts | 61 +++++- .../typescript/src/api/proto.generated.ts | 16 ++ packages/typescript/src/api/sync/api.ts | 182 +++++++++++++++++- packages/typescript/test/async/api.test.ts | 88 +++++++++ .../test/sync/api-generators.test.ts | 9 + packages/typescript/test/sync/api.test.ts | 88 +++++++++ tsc/internal/api/proto.go | 13 ++ tsc/internal/api/session.go | 60 ++++++ tsc/internal/api/session_program_test.go | 74 +++++++ tsc/internal/project/projectcollection.go | 8 + 10 files changed, 596 insertions(+), 3 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 820915ccd6f78..13785866a72b6 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1565,11 +1565,13 @@ export class Project { readonly program: Program; readonly checker: Checker; readonly languageService: LanguageService; + private readonly toPath: (fileName: string, basePath?: string) => PathKey; 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.toPath = toPath; this.configFileName = data.configFileName; this.currentDirectory = data.currentDirectory; this.dirty = data.dirty; @@ -1583,6 +1585,60 @@ export class Project { this.languageService = new LanguageService(snapshotId, this, objectRegistry); } + async containsFile(file: DocumentIdentifier, requireOpen?: boolean): Promise { + return this.api.client.apiRequest("containsProjectFile", { + snapshot: this.program.snapshotId, + project: this.handle, + file, + requireOpen, + }); + } + + async getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): Promise { + const files = await this.program.getSourceFileNames(); + const result: RootedFilePath[] = []; + for (const file of files) { + if (excludeFilesFromExternalLibraries) { + const metadata = await this.program.getSourceFileMetadata(file); + if (!metadata) { + throw new Error(`Program source file '${file}' has no metadata`); + } + if (metadata.isFromExternalLibrary) continue; + } + result.push(file); + } + if (!excludeConfigFiles) { + result.push(...await this.program.getConfigFileNames()); + } + return result; + } + + /** Returns existing root files, excluding missing root declarations. */ + async getRootFiles(): Promise { + return this.api.client.apiRequest("getProjectRootFiles", { + snapshot: this.program.snapshotId, + project: this.handle, + }); + } + + async hasRoots(): Promise { + return this.parsedCommandLine.fileNames.length > 0; + } + + async isRoot(file: DocumentIdentifier): Promise { + const path = this.toPath(resolveFileName(file), this.currentDirectory); + return (await this.getRootFiles()).some(root => this.toPath(root, this.currentDirectory) === path); + } + + async hasConfigFile(file: DocumentIdentifier): Promise { + const path = this.toPath(resolveFileName(file), this.currentDirectory); + return (await this.program.getConfigFileNames()).some(config => this.toPath(config, this.currentDirectory) === path); + } + + async getSourceFile(file: DocumentIdentifier): Promise { + return this.program.getSourceFile(file); + } + dispose(): void { this.checker.dispose(); } @@ -1792,7 +1848,10 @@ export class Program implements FormatDiagnost } async getTypeChecker(): Promise { - await this.getStatistics(); + await this.client.apiRequest("ensureTypeChecker", { + snapshot: this.snapshotId, + project: this.project.handle, + }); return this.project.checker; } diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 5b753e044042d..ba62934bb65c3 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -80,6 +80,9 @@ export interface APIMethodInfo { getNonMissingTypeOfSymbol: APIMethod; getSourceFile: APIMethod; getSourceFileNames: APIMethod; + containsProjectFile: APIMethod; + getProjectRootFiles: APIMethod; + ensureTypeChecker: APIMethod; getProgramStatistics: APIMethod; getResolvedProjectReferences: APIMethod; getOptionsDiagnostics: APIMethod; @@ -592,6 +595,13 @@ export interface GetSourceFileNamesParams { project: number; } +export interface ContainsProjectFileParams { + snapshot: number; + project: number; + file: DocumentIdentifier; + requireOpen?: boolean | undefined; +} + /** GetProjectDiagnosticsParams are parameters for project-wide diagnostic methods. */ export interface GetProjectDiagnosticsParams { snapshot: number; @@ -1173,6 +1183,7 @@ export interface BatchRequest { | "buildReferences" | "cleanBuild" | "cleanReferences" + | "containsProjectFile" | "createBuildOrchestrator" | "createModuleResolver" | "createSnapshot" @@ -1181,6 +1192,7 @@ export interface BatchRequest { | "disposeBuildOrchestrator" | "emit" | "emitToString" + | "ensureTypeChecker" | "formatNodeForInsertion" | "getAliasSymbolOfType" | "getAliasTypeArgumentsOfType" @@ -1255,6 +1267,7 @@ export interface BatchRequest { | "getParentOfSymbolForChecker" | "getProgramDiagnostics" | "getProgramStatistics" + | "getProjectRootFiles" | "getPropertiesOfType" | "getPropertyOfType" | "getReducedType" @@ -1358,6 +1371,7 @@ export interface BatchResponse { | "buildReferences" | "cleanBuild" | "cleanReferences" + | "containsProjectFile" | "createBuildOrchestrator" | "createModuleResolver" | "createSnapshot" @@ -1366,6 +1380,7 @@ export interface BatchResponse { | "disposeBuildOrchestrator" | "emit" | "emitToString" + | "ensureTypeChecker" | "formatNodeForInsertion" | "getAliasSymbolOfType" | "getAliasTypeArgumentsOfType" @@ -1440,6 +1455,7 @@ export interface BatchResponse { | "getParentOfSymbolForChecker" | "getProgramDiagnostics" | "getProgramStatistics" + | "getProjectRootFiles" | "getPropertiesOfType" | "getPropertyOfType" | "getReducedType" diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 50ba3adfbe182..46555d8c61136 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -2596,11 +2596,13 @@ export class Project { readonly program: Program; readonly checker: Checker; readonly languageService: LanguageService; + private readonly toPath: (fileName: string, basePath?: string) => PathKey; 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.toPath = toPath; this.configFileName = data.configFileName; this.currentDirectory = data.currentDirectory; this.dirty = data.dirty; @@ -2614,6 +2616,176 @@ export class Project { this.languageService = new LanguageService(snapshotId, this, objectRegistry); } + get containsFile(): { + (file: DocumentIdentifier, requireOpen?: boolean): boolean; + gen(file: DocumentIdentifier, requireOpen?: boolean): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "containsFile", + function (file: DocumentIdentifier, requireOpen?: boolean): boolean { + return owner.api.client.apiRequest("containsProjectFile", { + snapshot: owner.program.snapshotId, + project: owner.handle, + file, + requireOpen, + }); + }, + function* (file: DocumentIdentifier, requireOpen?: boolean): Generator { + return yield* apiRequest("containsProjectFile", { + snapshot: owner.program.snapshotId, + project: owner.handle, + file, + requireOpen, + }); + }, + ); + } + + get getFileNames(): { + (excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): readonly RootedFilePath[]; + gen(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getFileNames", + function (excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): readonly RootedFilePath[] { + const files = owner.program.getSourceFileNames(); + const result: RootedFilePath[] = []; + for (const file of files) { + if (excludeFilesFromExternalLibraries) { + const metadata = owner.program.getSourceFileMetadata(file); + if (!metadata) { + throw new Error(`Program source file '${file}' has no metadata`); + } + if (metadata.isFromExternalLibrary) continue; + } + result.push(file); + } + if (!excludeConfigFiles) { + result.push(...owner.program.getConfigFileNames()); + } + return result; + }, + function* (excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): Generator { + const files = yield* owner.program.getSourceFileNames.gen(); + const result: RootedFilePath[] = []; + for (const file of files) { + if (excludeFilesFromExternalLibraries) { + const metadata = yield* owner.program.getSourceFileMetadata.gen(file); + if (!metadata) { + throw new Error(`Program source file '${file}' has no metadata`); + } + if (metadata.isFromExternalLibrary) continue; + } + result.push(file); + } + if (!excludeConfigFiles) { + result.push(...(yield* owner.program.getConfigFileNames.gen())); + } + return result; + }, + ); + } + + /** Returns existing root files, excluding missing root declarations. */ + get getRootFiles(): { + (): readonly RootedFilePath[]; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getRootFiles", + function (): readonly RootedFilePath[] { + return owner.api.client.apiRequest("getProjectRootFiles", { + snapshot: owner.program.snapshotId, + project: owner.handle, + }); + }, + function* (): Generator { + return yield* apiRequest("getProjectRootFiles", { + snapshot: owner.program.snapshotId, + project: owner.handle, + }); + }, + ); + } + + get hasRoots(): { + (): boolean; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "hasRoots", + function (): boolean { + return owner.parsedCommandLine.fileNames.length > 0; + }, + function* (): Generator { + return owner.parsedCommandLine.fileNames.length > 0; + }, + ); + } + + get isRoot(): { + (file: DocumentIdentifier): boolean; + gen(file: DocumentIdentifier): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "isRoot", + function (file: DocumentIdentifier): boolean { + const path = owner.toPath(resolveFileName(file), owner.currentDirectory); + return (owner.getRootFiles()).some(root => owner.toPath(root, owner.currentDirectory) === path); + }, + function* (file: DocumentIdentifier): Generator { + const path = owner.toPath(resolveFileName(file), owner.currentDirectory); + return (yield* owner.getRootFiles.gen()).some(root => owner.toPath(root, owner.currentDirectory) === path); + }, + ); + } + + get hasConfigFile(): { + (file: DocumentIdentifier): boolean; + gen(file: DocumentIdentifier): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "hasConfigFile", + function (file: DocumentIdentifier): boolean { + const path = owner.toPath(resolveFileName(file), owner.currentDirectory); + return (owner.program.getConfigFileNames()).some(config => owner.toPath(config, owner.currentDirectory) === path); + }, + function* (file: DocumentIdentifier): Generator { + const path = owner.toPath(resolveFileName(file), owner.currentDirectory); + return (yield* owner.program.getConfigFileNames.gen()).some(config => owner.toPath(config, owner.currentDirectory) === path); + }, + ); + } + + get getSourceFile(): { + (file: DocumentIdentifier): SourceFile | undefined; + gen(file: DocumentIdentifier): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getSourceFile", + function (file: DocumentIdentifier): SourceFile | undefined { + return owner.program.getSourceFile(file); + }, + function* (file: DocumentIdentifier): Generator { + return yield* owner.program.getSourceFile.gen(file); + }, + ); + } + dispose(): void { this.checker.dispose(); } @@ -3041,11 +3213,17 @@ export class Program implements FormatDiagnost owner, "getTypeChecker", function (): Checker { - owner.getStatistics(); + owner.client.apiRequest("ensureTypeChecker", { + snapshot: owner.snapshotId, + project: owner.project.handle, + }); return owner.project.checker; }, function* (): Generator { - yield* owner.getStatistics.gen(); + yield* apiRequest("ensureTypeChecker", { + snapshot: owner.snapshotId, + project: owner.project.handle, + }); return owner.project.checker; }, ); diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index a29b6388dfb17..642723ec6d190 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -2777,6 +2777,94 @@ const value: string = 1;`, assert.equal(syntheticResolved![1], undefined); }); + test("Project membership distinguishes roots, dependencies, and open files", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: ["src/main.ts", "missing.ts"] }), + "/src/main.ts": `import { value } from "./dependency"; export const result = value;`, + "/src/dependency.ts": `export const value = 1;`, + "/outside.ts": "export {};", + }); + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"], openFiles: ["/src/main.ts", "/outside.ts"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + assert.equal(await project.hasRoots(), true); + assert.deepEqual(await project.getRootFiles(), ["/src/main.ts"]); + assert.equal(await project.isRoot("src/main.ts"), true); + assert.equal(await project.isRoot({ uri: "file:///src/main.ts" }), true); + assert.equal(await project.isRoot("/missing.ts"), false); + assert.equal(await project.isRoot("/src/dependency.ts"), false); + assert.equal(await project.containsFile("src/main.ts"), true); + assert.equal(await project.containsFile({ uri: "file:///src/main.ts" }, true), true); + assert.equal(await project.containsFile("/src/dependency.ts"), true); + assert.equal(await project.containsFile("/src/dependency.ts", true), false); + assert.equal(await project.containsFile("/outside.ts"), false); + assert.equal(await project.containsFile("/outside.ts", true), false); + assert.equal(await project.containsFile("/missing.ts"), false); + assert.strictEqual(await project.getSourceFile("src/main.ts"), await project.program.getSourceFile("/src/main.ts")); + assert.equal(await project.getSourceFile("/outside.ts"), undefined); + assert.equal(await project.hasConfigFile("tsconfig.json"), true); + assert.equal(await project.hasConfigFile("/src/main.ts"), false); + + const updated = await snapshot.update({ closeFiles: ["/src/main.ts"] }); + assert.equal(await updated.getConfiguredProject("/tsconfig.json")!.containsFile("/src/main.ts", true), false); + assert.equal(await project.containsFile("/src/main.ts", true), true); + }); + + test("Project file lists include extended configs and optionally exclude external libraries", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ extends: "./base.json", files: ["src/main.ts"] }), + "/base.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" } }), + "/src/main.ts": `import { value } from "my-lib"; export const result = value;`, + "/node_modules/my-lib/package.json": JSON.stringify({ types: "./index.d.ts" }), + "/node_modules/my-lib/index.d.ts": "export declare const value: number;", + }); + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + assert.deepEqual(await project.getFileNames(), ["/node_modules/my-lib/index.d.ts", "/src/main.ts", "/tsconfig.json", "/base.json"]); + assert.deepEqual(await project.getFileNames(true), ["/src/main.ts", "/tsconfig.json", "/base.json"]); + assert.deepEqual(await project.getFileNames(false, true), ["/node_modules/my-lib/index.d.ts", "/src/main.ts"]); + assert.deepEqual(await project.getFileNames(true, true), ["/src/main.ts"]); + assert.equal(await project.hasConfigFile("./base.json"), true); + assert.equal(await project.hasConfigFile({ uri: "file:///base.json" }), true); + assert.equal(await project.hasConfigFile("/other.json"), false); + assert.equal(await project.containsFile("/tsconfig.json"), false); + }); + + test("Project roots preserve missing declarations and empty projects", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: [] }), + }); + const snapshot = await api.createSnapshot({ + openProjects: ["/tsconfig.json"], + createPrograms: [{ rootFiles: ["/missing.ts"], compilerOptions: { noLib: true } }], + }); + const empty = snapshot.getConfiguredProject("/tsconfig.json")!; + assert.equal(await empty.hasRoots(), false); + assert.deepEqual(await empty.getRootFiles(), []); + assert.deepEqual(await empty.getFileNames(), ["/tsconfig.json"]); + const missing = snapshot.getProject(snapshot.operation.createdPrograms![0].id)!; + assert.equal(await missing.hasRoots(), true); + assert.deepEqual(await missing.getRootFiles(), []); + assert.equal(await missing.isRoot("/missing.ts"), false); + assert.equal(await missing.containsFile("/missing.ts"), false); + assert.deepEqual(await missing.getFileNames(), []); + assert.equal(await missing.hasConfigFile("/tsconfig.json"), false); + }); + + test("Project membership uses configured path case sensitivity", async () => { + const files = { + "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: ["src/main.ts"] }), + "/src/main.ts": "export {};", + }; + for (const useCaseSensitiveFileNames of [true, false]) { + await using api = new API({ cwd: "/", fs: createVirtualFileSystem(files), useCaseSensitiveFileNames }); + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"], openFiles: ["/src/main.ts"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + assert.equal(await project.isRoot("/SRC/MAIN.TS"), !useCaseSensitiveFileNames); + assert.equal(await project.hasConfigFile("/TSCONFIG.JSON"), !useCaseSensitiveFileNames); + assert.equal(await project.containsFile("/SRC/MAIN.TS", true), !useCaseSensitiveFileNames); + } + }); + test("getSourceFileNames returns all program files, not just root files", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 4c33e43ab3797..7b46d05fd464f 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -1600,6 +1600,15 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Snapshot", "getDefaultProjectForFile", snapshot.getDefaultProjectForFile, assertOptionalProjectsEquivalent, "/src/index.ts"), parityCase("ModuleResolver", "resolveModuleName", moduleResolver.resolveModuleName, assertDeepEquivalent, "models", "/src"), parityCase("Snapshot", "update", snapshot.update, assertSnapshotsEquivalent, {}), + parityCase("Project", "containsFile", project.containsFile, assertDeepEquivalent, "/src/index.ts"), + parityCase("Project", "containsFile", project.containsFile, assertDeepEquivalent, "/src/index.ts", true), + parityCase("Project", "getFileNames", project.getFileNames, assertDeepEquivalent), + parityCase("Project", "getFileNames", project.getFileNames, assertDeepEquivalent, true, true), + parityCase("Project", "getRootFiles", project.getRootFiles, assertDeepEquivalent), + parityCase("Project", "hasRoots", project.hasRoots, assertDeepEquivalent), + parityCase("Project", "isRoot", project.isRoot, assertDeepEquivalent, "/src/index.ts"), + parityCase("Project", "hasConfigFile", project.hasConfigFile, assertDeepEquivalent, "/tsconfig.json"), + parityCase("Project", "getSourceFile", project.getSourceFile, assertOptionalSourceFilesEquivalent, "/src/index.ts"), parityCase("LanguageService", "getImportAdderEdits", languageService.getImportAdderEdits, assertDeepEquivalent, "/src/index.ts", [{ kind: "importSymbol", symbol: unimportedSymbol }]), parityCase("LanguageService", "getImportEditsForSymbols", languageService.getImportEditsForSymbols, assertDeepEquivalent, "/src/index.ts", [unimportedSymbol], { isValidTypeOnlyUseSite: true }), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 2cccd99abedce..dda5185add414 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2635,6 +2635,94 @@ const value: string = 1;`, assert.equal(syntheticResolved![1], undefined); }); + test("Project membership distinguishes roots, dependencies, and open files", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: ["src/main.ts", "missing.ts"] }), + "/src/main.ts": `import { value } from "./dependency"; export const result = value;`, + "/src/dependency.ts": `export const value = 1;`, + "/outside.ts": "export {};", + }); + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"], openFiles: ["/src/main.ts", "/outside.ts"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + assert.equal(project.hasRoots(), true); + assert.deepEqual(project.getRootFiles(), ["/src/main.ts"]); + assert.equal(project.isRoot("src/main.ts"), true); + assert.equal(project.isRoot({ uri: "file:///src/main.ts" }), true); + assert.equal(project.isRoot("/missing.ts"), false); + assert.equal(project.isRoot("/src/dependency.ts"), false); + assert.equal(project.containsFile("src/main.ts"), true); + assert.equal(project.containsFile({ uri: "file:///src/main.ts" }, true), true); + assert.equal(project.containsFile("/src/dependency.ts"), true); + assert.equal(project.containsFile("/src/dependency.ts", true), false); + assert.equal(project.containsFile("/outside.ts"), false); + assert.equal(project.containsFile("/outside.ts", true), false); + assert.equal(project.containsFile("/missing.ts"), false); + assert.strictEqual(project.getSourceFile("src/main.ts"), project.program.getSourceFile("/src/main.ts")); + assert.equal(project.getSourceFile("/outside.ts"), undefined); + assert.equal(project.hasConfigFile("tsconfig.json"), true); + assert.equal(project.hasConfigFile("/src/main.ts"), false); + + const updated = snapshot.update({ closeFiles: ["/src/main.ts"] }); + assert.equal(updated.getConfiguredProject("/tsconfig.json")!.containsFile("/src/main.ts", true), false); + assert.equal(project.containsFile("/src/main.ts", true), true); + }); + + test("Project file lists include extended configs and optionally exclude external libraries", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ extends: "./base.json", files: ["src/main.ts"] }), + "/base.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" } }), + "/src/main.ts": `import { value } from "my-lib"; export const result = value;`, + "/node_modules/my-lib/package.json": JSON.stringify({ types: "./index.d.ts" }), + "/node_modules/my-lib/index.d.ts": "export declare const value: number;", + }); + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + assert.deepEqual(project.getFileNames(), ["/node_modules/my-lib/index.d.ts", "/src/main.ts", "/tsconfig.json", "/base.json"]); + assert.deepEqual(project.getFileNames(true), ["/src/main.ts", "/tsconfig.json", "/base.json"]); + assert.deepEqual(project.getFileNames(false, true), ["/node_modules/my-lib/index.d.ts", "/src/main.ts"]); + assert.deepEqual(project.getFileNames(true, true), ["/src/main.ts"]); + assert.equal(project.hasConfigFile("./base.json"), true); + assert.equal(project.hasConfigFile({ uri: "file:///base.json" }), true); + assert.equal(project.hasConfigFile("/other.json"), false); + assert.equal(project.containsFile("/tsconfig.json"), false); + }); + + test("Project roots preserve missing declarations and empty projects", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: [] }), + }); + const snapshot = api.createSnapshot({ + openProjects: ["/tsconfig.json"], + createPrograms: [{ rootFiles: ["/missing.ts"], compilerOptions: { noLib: true } }], + }); + const empty = snapshot.getConfiguredProject("/tsconfig.json")!; + assert.equal(empty.hasRoots(), false); + assert.deepEqual(empty.getRootFiles(), []); + assert.deepEqual(empty.getFileNames(), ["/tsconfig.json"]); + const missing = snapshot.getProject(snapshot.operation.createdPrograms![0].id)!; + assert.equal(missing.hasRoots(), true); + assert.deepEqual(missing.getRootFiles(), []); + assert.equal(missing.isRoot("/missing.ts"), false); + assert.equal(missing.containsFile("/missing.ts"), false); + assert.deepEqual(missing.getFileNames(), []); + assert.equal(missing.hasConfigFile("/tsconfig.json"), false); + }); + + test("Project membership uses configured path case sensitivity", () => { + const files = { + "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: ["src/main.ts"] }), + "/src/main.ts": "export {};", + }; + for (const useCaseSensitiveFileNames of [true, false]) { + using api = new API({ cwd: "/", fs: createVirtualFileSystem(files), useCaseSensitiveFileNames }); + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"], openFiles: ["/src/main.ts"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + assert.equal(project.isRoot("/SRC/MAIN.TS"), !useCaseSensitiveFileNames); + assert.equal(project.hasConfigFile("/TSCONFIG.JSON"), !useCaseSensitiveFileNames); + assert.equal(project.containsFile("/SRC/MAIN.TS", true), !useCaseSensitiveFileNames); + } + }); + test("getSourceFileNames returns all program files, not just root files", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 40cc134f6463f..d586398bc19b4 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -111,6 +111,9 @@ const ( MethodGetNonMissingTypeOfSymbol Method = "getNonMissingTypeOfSymbol" MethodGetSourceFile Method = "getSourceFile" MethodGetSourceFileNames Method = "getSourceFileNames" + MethodContainsProjectFile Method = "containsProjectFile" + MethodGetProjectRootFiles Method = "getProjectRootFiles" + MethodEnsureTypeChecker Method = "ensureTypeChecker" MethodGetProgramStatistics Method = "getProgramStatistics" MethodGetResolvedProjectReferences Method = "getResolvedProjectReferences" MethodGetOptionsDiagnostics Method = "getOptionsDiagnostics" @@ -629,6 +632,8 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetDefaultProjectForFile: unmarshallerFor[GetDefaultProjectForFileParams], MethodGetSourceFile: unmarshallerFor[GetSourceFileParams], MethodGetSourceFileNames: unmarshallerFor[GetSourceFileNamesParams], + MethodContainsProjectFile: unmarshallerFor[ContainsProjectFileParams], + MethodGetProjectRootFiles: unmarshallerFor[GetSourceFileNamesParams], MethodGetSourceFileMetadata: unmarshallerFor[GetSourceFileParams], MethodGetModeForUsageLocation: unmarshallerFor[GetModeForUsageLocationParams], MethodGetModeForResolutionAtIndex: unmarshallerFor[GetModeForResolutionAtIndexParams], @@ -773,6 +778,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetSuggestionDiagnostics: unmarshallerFor[GetDiagnosticsParams], MethodGetDeclarationDiagnostics: unmarshallerFor[GetDiagnosticsParams], MethodGetProgramDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], + MethodEnsureTypeChecker: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetProgramStatistics: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetResolvedProjectReferences: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetOptionsDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], @@ -1414,6 +1420,13 @@ type GetSourceFileNamesParams struct { Project project.Handle `json:"project"` } +type ContainsProjectFileParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project project.Handle `json:"project"` + File DocumentIdentifier `json:"file"` + RequireOpen bool `json:"requireOpen,omitempty"` +} + type GetModeForUsageLocationParams struct { Snapshot SnapshotID `json:"snapshot"` Project project.Handle `json:"project"` diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 6f001e22a5f88..9f5028519b469 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -951,6 +951,12 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetSourceFile(ctx, parsed.(*GetSourceFileParams)) case string(MethodGetSourceFileNames): return s.handleGetSourceFileNames(ctx, parsed.(*GetSourceFileNamesParams)) + case string(MethodContainsProjectFile): + return s.handleContainsProjectFile(ctx, parsed.(*ContainsProjectFileParams)) + case string(MethodGetProjectRootFiles): + return s.handleGetProjectRootFiles(ctx, parsed.(*GetSourceFileNamesParams)) + case string(MethodEnsureTypeChecker): + return s.handleEnsureTypeChecker(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodGetProgramStatistics): return s.handleGetProgramStatistics(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodGetResolvedProjectReferences): @@ -2354,6 +2360,15 @@ func transpileOutput(ctx context.Context, input string, options TranspileOptions }, nil } +func (s *Session) handleEnsureTypeChecker(ctx context.Context, params *GetProjectDiagnosticsParams) (bool, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return false, err + } + defer setup.done() + return true, nil +} + func (s *Session) handleGetProgramStatistics(ctx context.Context, params *GetProjectDiagnosticsParams) (*ProgramStatisticsResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) if err != nil { @@ -2554,6 +2569,51 @@ func (s *Session) encodeSourceFileResponse(sourceFile *ast.SourceFile) (any, err }, nil } +func (s *Session) handleContainsProjectFile(ctx context.Context, params *ContainsProjectFileParams) (bool, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return false, err + } + proj, err := sd.getProject(params.Project) + if err != nil { + return false, err + } + fileName := params.File.ToFileName(proj.CurrentDirectory()) + file := sd.snapshot.GetFile(fileName) + if file == nil { + return false, nil + } + path := sd.snapshot.CaseSensitivity().PathKey(fileName.AsPath()) + if params.RequireOpen && !sd.snapshot.ProjectCollection.IsFileOpen(path) { + return false, nil + } + for _, root := range proj.CommandLine.FileNames() { + if sd.snapshot.CaseSensitivity().PathKey(root.AsPath()) == path { + return true, nil + } + } + program := proj.GetProgram() + return program != nil && program.GetSourceFileByPath(path) != nil, nil +} + +func (s *Session) handleGetProjectRootFiles(ctx context.Context, params *GetSourceFileNamesParams) ([]tspath.RootedFilePath, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + proj, err := sd.getProject(params.Project) + if err != nil { + return nil, err + } + result := make([]tspath.RootedFilePath, 0, len(proj.CommandLine.FileNames())) + for _, root := range proj.CommandLine.FileNames() { + if sd.snapshot.GetFile(root) != nil { + result = append(result, root) + } + } + return result, nil +} + // handleGetSourceFileNames returns file names of all source files in a project. func (s *Session) handleGetSourceFileNames(ctx context.Context, params *GetSourceFileNamesParams) ([]tspath.RootedFilePath, error) { sd, err := s.getSnapshotData(params.Snapshot) diff --git a/tsc/internal/api/session_program_test.go b/tsc/internal/api/session_program_test.go index 5cbd6758e3e4a..9dceffd8353c2 100644 --- a/tsc/internal/api/session_program_test.go +++ b/tsc/internal/api/session_program_test.go @@ -5,11 +5,45 @@ import ( "testing" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/project" "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" "gotest.tools/v3/assert" ) +func TestProjectContainsEditorOpenFile(t *testing.T) { + t.Parallel() + + const fileName = "/home/projects/p/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{ + "/home/projects/p/tsconfig.json": `{"compilerOptions":{"noLib":true},"files":["index.ts"]}`, + fileName: "export {};", + }) + defer projectSession.Close() + uri := DocumentIdentifier{FileName: fileName}.ToURI(projectSession.GetCurrentDirectory()) + projectSession.DidOpenFile(context.Background(), uri, 1, "export {};", lsproto.LanguageKindTypeScript) + session := NewLSPSession(projectSession, nil) + defer session.Close() + response, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{}) + assert.NilError(t, err) + params := &ContainsProjectFileParams{ + Snapshot: response.Snapshot, + Project: response.Projects[0].Handle, + File: DocumentIdentifier{FileName: fileName}, + RequireOpen: true, + } + contains, err := session.handleContainsProjectFile(context.Background(), params) + assert.NilError(t, err) + assert.Assert(t, contains) + params.Project = project.Handle(999999) + _, err = session.handleContainsProjectFile(context.Background(), params) + assert.ErrorContains(t, err, "not found") + _, err = session.handleGetProjectRootFiles(context.Background(), &GetSourceFileNamesParams{ + Snapshot: response.Snapshot, Project: params.Project, + }) + assert.ErrorContains(t, err, "not found") +} + func TestProgramStatisticsUseAPIChecker(t *testing.T) { t.Parallel() @@ -50,3 +84,43 @@ func TestProgramStatisticsUseAPIChecker(t *testing.T) { }) assert.ErrorContains(t, err, "not found") } + +func TestEnsureTypeChecker(t *testing.T) { + t.Parallel() + + const fileName = "/home/projects/p/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{ + fileName: "export const value = 1;", + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: fileName}}, + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + }}, + }) + assert.NilError(t, err) + params := &GetProjectDiagnosticsParams{Snapshot: response.Snapshot, Project: response.Projects[0].Handle} + initialized, err := session.handleEnsureTypeChecker(context.Background(), params) + assert.NilError(t, err) + assert.Assert(t, initialized) + setup, err := session.setupChecker(context.Background(), params.Snapshot, params.Project) + assert.NilError(t, err) + first := setup.checker + setup.done() + + initialized, err = session.handleEnsureTypeChecker(context.Background(), params) + assert.NilError(t, err) + assert.Assert(t, initialized) + setup, err = session.setupChecker(context.Background(), params.Snapshot, params.Project) + assert.NilError(t, err) + assert.Assert(t, setup.checker == first) + setup.done() + + _, err = session.handleEnsureTypeChecker(context.Background(), &GetProjectDiagnosticsParams{ + Snapshot: response.Snapshot, Project: project.Handle(999999), + }) + assert.ErrorContains(t, err, "not found") +} diff --git a/tsc/internal/project/projectcollection.go b/tsc/internal/project/projectcollection.go index 609985e2c8bd5..222701557ece7 100644 --- a/tsc/internal/project/projectcollection.go +++ b/tsc/internal/project/projectcollection.go @@ -224,6 +224,14 @@ func (c *ProjectCollection) GetOpenConfiguredProjects() *collections.Set[Configu return c.openConfiguredProjects } +func (c *ProjectCollection) IsFileOpen(path tspath.PathKey) bool { + if c.openFiles.Has(path) { + return true + } + _, ok := c.apiState.openFiles[path] + return ok +} + func openFilePaths(overlays map[tspath.PathKey]*Overlay) collections.Set[tspath.PathKey] { openFiles := collections.Set[tspath.PathKey]{M: make(map[tspath.PathKey]struct{}, len(overlays))} for path := range overlays { From 6a6a1d23a09970d2b58d979f1c7e7d31e02f0ec7 Mon Sep 17 00:00:00 2001 From: isabel duan Date: Wed, 28 Oct 2026 19:05:25 -0700 Subject: [PATCH 03/13] address comments --- packages/typescript/test/async/api.test.ts | 63 +++++++++++++++++++++- packages/typescript/test/sync/api.test.ts | 63 +++++++++++++++++++++- tsc/internal/api/session.go | 12 ++--- tsc/internal/api/session_program_test.go | 50 +++++++++++++++++ 4 files changed, 177 insertions(+), 11 deletions(-) diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 642723ec6d190..a7d8fc62ece4e 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -126,8 +126,19 @@ import { toRootedFilePath, } from "@typescript/typescript/path"; import assert from "node:assert"; -import { globSync } from "node:fs"; -import { resolve } from "node:path"; +import { + existsSync, + globSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { + join, + resolve, +} from "node:path"; import { describe, test, @@ -2809,6 +2820,54 @@ const value: string = 1;`, assert.equal(await project.containsFile("/src/main.ts", true), true); }); + test("Project membership includes supplemental files without physical files", async t => { + const directory = mkdtempSync(join(tmpdir(), "ts-project-membership-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + const mapperDirectory = join(directory, "node_modules", "mapper"); + mkdirSync(mapperDirectory, { recursive: true }); + const mapperScript = ` + import { createMessageConnection } from ${JSON.stringify(new URL("../../vendor/vscode-jsonrpc/lib/node/main.js", import.meta.url).href)}; + const connection = createMessageConnection(process.stdin, process.stdout); + connection.onRequest("initialize", () => ({ positionEncoding: "utf-8", diagnosticSource: "mapper" })); + connection.onRequest("openProject", () => ({})); + connection.onRequest("closeProject", () => null); + connection.onRequest("transform", ({ content }) => ({ + text: "export {};", + extension: ".ts", + supplemental: [{ text: content, extension: ".ts" }], + })); + connection.listen(); + `; + writeFileSync( + join(mapperDirectory, "package.json"), + JSON.stringify({ + name: "mapper", + version: "1.0.0", + typescript: { contentMapper: { exec: [process.execPath, "--input-type=module", "--eval", mapperScript] } }, + }), + ); + const configFileName = toRootedFilePath(join(directory, "tsconfig.json"), undefined); + writeFileSync( + configFileName, + JSON.stringify({ + compilerOptions: { noLib: true }, + files: ["app.box", "main.ts", "missing.ts"], + contentMappers: [{ package: "mapper", extensions: [".box"] }], + }), + ); + writeFileSync(join(directory, "app.box"), "declare const supplementalValue: number;"); + writeFileSync(join(directory, "main.ts"), "const value: number = supplementalValue;"); + await using api = new API({ cwd: directory, runExternalCode: true }); + const snapshot = await api.createSnapshot({ openProjects: [configFileName] }); + const project = snapshot.getConfiguredProject(configFileName)!; + const supplementalFileName = toRootedFilePath(join(directory, "app.box.0.ts"), undefined); + assert.equal(existsSync(supplementalFileName), false); + assert.ok(await project.getSourceFile(supplementalFileName)); + assert.equal(await project.containsFile(supplementalFileName), true); + assert.equal(await project.containsFile(supplementalFileName, true), false); + assert.equal(await project.containsFile(join(directory, "missing.ts")), false); + }); + test("Project file lists include extended configs and optionally exclude external libraries", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ extends: "./base.json", files: ["src/main.ts"] }), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index dda5185add414..300361def50e4 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -134,8 +134,19 @@ import { type UserPreferences, } from "@typescript/typescript/sync"; import assert from "node:assert"; -import { globSync } from "node:fs"; -import { resolve } from "node:path"; +import { + existsSync, + globSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { + join, + resolve, +} from "node:path"; import { describe, test, @@ -2667,6 +2678,54 @@ const value: string = 1;`, assert.equal(project.containsFile("/src/main.ts", true), true); }); + test("Project membership includes supplemental files without physical files", t => { + const directory = mkdtempSync(join(tmpdir(), "ts-project-membership-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + const mapperDirectory = join(directory, "node_modules", "mapper"); + mkdirSync(mapperDirectory, { recursive: true }); + const mapperScript = ` + import { createMessageConnection } from ${JSON.stringify(new URL("../../vendor/vscode-jsonrpc/lib/node/main.js", import.meta.url).href)}; + const connection = createMessageConnection(process.stdin, process.stdout); + connection.onRequest("initialize", () => ({ positionEncoding: "utf-8", diagnosticSource: "mapper" })); + connection.onRequest("openProject", () => ({})); + connection.onRequest("closeProject", () => null); + connection.onRequest("transform", ({ content }) => ({ + text: "export {};", + extension: ".ts", + supplemental: [{ text: content, extension: ".ts" }], + })); + connection.listen(); + `; + writeFileSync( + join(mapperDirectory, "package.json"), + JSON.stringify({ + name: "mapper", + version: "1.0.0", + typescript: { contentMapper: { exec: [process.execPath, "--input-type=module", "--eval", mapperScript] } }, + }), + ); + const configFileName = toRootedFilePath(join(directory, "tsconfig.json"), undefined); + writeFileSync( + configFileName, + JSON.stringify({ + compilerOptions: { noLib: true }, + files: ["app.box", "main.ts", "missing.ts"], + contentMappers: [{ package: "mapper", extensions: [".box"] }], + }), + ); + writeFileSync(join(directory, "app.box"), "declare const supplementalValue: number;"); + writeFileSync(join(directory, "main.ts"), "const value: number = supplementalValue;"); + using api = new API({ cwd: directory, runExternalCode: true }); + const snapshot = api.createSnapshot({ openProjects: [configFileName] }); + const project = snapshot.getConfiguredProject(configFileName)!; + const supplementalFileName = toRootedFilePath(join(directory, "app.box.0.ts"), undefined); + assert.equal(existsSync(supplementalFileName), false); + assert.ok(project.getSourceFile(supplementalFileName)); + assert.equal(project.containsFile(supplementalFileName), true); + assert.equal(project.containsFile(supplementalFileName, true), false); + assert.equal(project.containsFile(join(directory, "missing.ts")), false); + }); + test("Project file lists include extended configs and optionally exclude external libraries", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ extends: "./base.json", files: ["src/main.ts"] }), diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 9f5028519b469..2fbbd11179cfe 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -2579,21 +2579,19 @@ func (s *Session) handleContainsProjectFile(ctx context.Context, params *Contain return false, err } fileName := params.File.ToFileName(proj.CurrentDirectory()) - file := sd.snapshot.GetFile(fileName) - if file == nil { - return false, nil - } path := sd.snapshot.CaseSensitivity().PathKey(fileName.AsPath()) if params.RequireOpen && !sd.snapshot.ProjectCollection.IsFileOpen(path) { return false, nil } + if program := proj.GetProgram(); program != nil && program.GetSourceFileByPath(path) != nil { + return true, nil + } for _, root := range proj.CommandLine.FileNames() { if sd.snapshot.CaseSensitivity().PathKey(root.AsPath()) == path { - return true, nil + return sd.snapshot.GetFile(fileName) != nil, nil } } - program := proj.GetProgram() - return program != nil && program.GetSourceFileByPath(path) != nil, nil + return false, nil } func (s *Session) handleGetProjectRootFiles(ctx context.Context, params *GetSourceFileNamesParams) ([]tspath.RootedFilePath, error) { diff --git a/tsc/internal/api/session_program_test.go b/tsc/internal/api/session_program_test.go index 9dceffd8353c2..e31cd1dad8a44 100644 --- a/tsc/internal/api/session_program_test.go +++ b/tsc/internal/api/session_program_test.go @@ -7,10 +7,60 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/project" + "github.com/microsoft/TypeScript/tsc/internal/testutil/contentmappertest" "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" "gotest.tools/v3/assert" ) +func TestProjectContainsSupplementalFile(t *testing.T) { + t.Parallel() + + const supplementalFileName = "/home/project/app.box.0.ts" + files := map[string]any{ + "/home/project/tsconfig.json": `{"compilerOptions":{"noLib":true},"files":["app.box","main.ts","missing.ts"],"contentMappers":[{"package":"mapper","extensions":[".box"]}]}`, + "/home/project/node_modules/mapper/package.json": contentmappertest.PackageJSON(contentmappertest.SupplementalMapper), + "/home/project/app.box": "declare const supplementalValue: number;", + "/home/project/main.ts": "const value: number = supplementalValue;", + } + init, _ := projecttestutil.GetSessionInitOptions(files, &project.SessionOptions{ + CurrentDirectory: "/home/project", + RunExternalCode: true, + }, nil) + init.Spawner = contentmappertest.NewSpawner() + projectSession := project.NewSession(init) + defer projectSession.Close() + projectSession.DidOpenFile(context.Background(), "file:///home/project/main.ts", 1, files["/home/project/main.ts"].(string), lsproto.LanguageKindTypeScript) + session := NewLSPSession(projectSession, nil) + defer session.Close() + response, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{}) + assert.NilError(t, err) + params := &ContainsProjectFileParams{ + Snapshot: response.Snapshot, + Project: response.Projects[0].Handle, + File: DocumentIdentifier{FileName: supplementalFileName}, + } + sd, err := session.getSnapshotData(response.Snapshot) + assert.NilError(t, err) + assert.Assert(t, sd.snapshot.GetFile(supplementalFileName) == nil) + sourceFile, err := session.handleGetSourceFile(context.Background(), &GetSourceFileParams{ + Snapshot: params.Snapshot, Project: params.Project, File: params.File, + }) + assert.NilError(t, err) + assert.Assert(t, sourceFile != nil) + contains, err := session.handleContainsProjectFile(context.Background(), params) + assert.NilError(t, err) + assert.Assert(t, contains) + params.RequireOpen = true + contains, err = session.handleContainsProjectFile(context.Background(), params) + assert.NilError(t, err) + assert.Assert(t, !contains) + params.RequireOpen = false + params.File = DocumentIdentifier{FileName: "/home/project/missing.ts"} + contains, err = session.handleContainsProjectFile(context.Background(), params) + assert.NilError(t, err) + assert.Assert(t, !contains) +} + func TestProjectContainsEditorOpenFile(t *testing.T) { t.Parallel() From 566a8b6a752c3ba3f09fef251363281dd4f1e69e Mon Sep 17 00:00:00 2001 From: isabel duan Date: Sun, 1 Nov 2026 18:33:30 -0800 Subject: [PATCH 04/13] address comments --- packages/typescript/scripts/generateSync.ts | 75 +++++++++++++++++-- packages/typescript/src/api/async/api.ts | 26 ++++--- packages/typescript/src/api/sync/api.ts | 58 ++++++++------ packages/typescript/test/async/api.test.ts | 37 +++++++++ .../test/sync/api-generators.test.ts | 66 +++++++++++++++- 5 files changed, 224 insertions(+), 38 deletions(-) diff --git a/packages/typescript/scripts/generateSync.ts b/packages/typescript/scripts/generateSync.ts index a20dbdc531b4f..4273e8429a2fc 100755 --- a/packages/typescript/scripts/generateSync.ts +++ b/packages/typescript/scripts/generateSync.ts @@ -11,6 +11,7 @@ * // @sync-skip-block-start/end — omit all lines between (inclusive) * // @sync-only-start/end — uncomment lines between (strip "// " prefix) * // @sync: — replace this line with (preserving indent) + * // @generators: - replace this line only in generator output * * AST-based transforms (applied after directives using the TypeScript compiler API): * - Generate synchronous implementations from async methods @@ -60,6 +61,7 @@ function generateSyncFile( transform: SourceTransform, force: boolean, variant: SyncVariant = "sync", + preserveGeneratorOverrides = false, ): GeneratedFile | undefined { const generated = new GeneratedFile(destPath, [import.meta.filename, srcPath]); if (generated.isCurrent(force)) return; @@ -69,7 +71,7 @@ function generateSyncFile( const normalized = source.replace(/\r/g, ""); // Phase 1: Process sync directives (text-based, operates on comments/lines) - const afterDirectives = processDirectives(normalized.split("\n"), variant).join("\n"); + const afterDirectives = processDirectives(normalized.split("\n"), variant, preserveGeneratorOverrides).join("\n"); // Phase 2: AST-based async→sync transforms const fileName = destPath.split("/").pop()!; @@ -88,7 +90,7 @@ function generateSyncFile( // ── Directive processing ───────────────────────────────────────── -function processDirectives(lines: string[], variant: SyncVariant): string[] { +function processDirectives(lines: string[], variant: SyncVariant, preserveGeneratorOverrides = false): string[] { const output: string[] = []; let skipBlock = false; let syncOnlyBlock = false; @@ -163,7 +165,7 @@ function processDirectives(lines: string[], variant: SyncVariant): string[] { continue; } - const lineWithoutGeneratorsDirective = generatorsReplaceMatch + const lineWithoutGeneratorsDirective = generatorsReplaceMatch && !preserveGeneratorOverrides ? line.slice(0, generatorsReplaceMatch.index).trimEnd() : line; const syncReplaceMatch = lineWithoutGeneratorsDirective.match(/\/\/ @sync: (.+)$/); @@ -211,14 +213,35 @@ function transformAsyncSource(source: string, fileName: string, attachGenerators const edits: Edit[] = []; const asyncMethodNames = new Set(); const synchronousMethodNames = new Set(); + const batchedMethodNames = new Set(); + const asyncMethods: ts.MethodDeclaration[] = []; for (const classDeclaration of getNamedClasses(sourceFile).values()) { for (const group of getMethodGroups(classDeclaration).values()) { const method = group.implementation; - if (isGeneratorMethod(method)) asyncMethodNames.add((method.name as ts.Identifier).text); + if (isGeneratorMethod(method)) { + asyncMethodNames.add((method.name as ts.Identifier).text); + asyncMethods.push(method); + } else synchronousMethodNames.add((method.name as ts.Identifier).text); } } + let changed = true; + while (changed) { + changed = false; + for (const method of asyncMethods) { + const name = (method.name as ts.Identifier).text; + if (!batchedMethodNames.has(name) && containsBatchedCall(method.body!)) { + batchedMethodNames.add(name); + changed = true; + } + } + } + + function containsBatchedCall(node: ts.Node): boolean { + if (isPromiseAllCall(node) || ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && batchedMethodNames.has(node.expression.name.text)) return true; + return ts.forEachChild(node, containsBatchedCall) ?? false; + } visit(sourceFile); return applyEdits(source, edits); @@ -321,6 +344,9 @@ function transformAsyncSource(source: string, fileName: string, attachGenerators if (!node.type) return "void"; if (mode === "generator" && isPromiseType(node.type)) { const returnType = getUnwrappedPromiseText(node.type, source, sourceFile); + if (node.name && ts.isIdentifier(node.name) && batchedMethodNames.has(node.name.text)) { + return `APIRequestGenerator<${returnType}>`; + } return `Generator`; } return transformNodeText(node.type, mode, false); @@ -347,7 +373,10 @@ function transformAsyncSource(source: string, fileName: string, attachGenerators } visitNode(node); - return applyEdits(source.slice(start, node.end), localEdits); + const result = applyEdits(source.slice(start, node.end), localEdits); + return ts.isBlock(node) + ? processDirectives(result.split("\n"), mode === "generator" ? "generators" : "sync").join("\n") + : result; } function addGeneratorEdit(node: ts.Node, offset: number, targetEdits: Edit[]): boolean { @@ -387,6 +416,24 @@ function transformAsyncSource(source: string, fileName: string, attachGenerators } function getGeneratorAwaitText(node: ts.AwaitExpression): string { + if (isPromiseAllCall(node.expression)) { + const argument = node.expression.arguments[0]; + if ( + !ts.isCallExpression(argument) || + !ts.isPropertyAccessExpression(argument.expression) || + argument.expression.name.text !== "map" || + argument.arguments.length !== 1 || + !ts.isArrowFunction(argument.arguments[0]) || + !ts.isCallExpression(argument.arguments[0].body) || + !isGeneratorCall(argument.arguments[0].body) + ) { + throw new Error("Promise.all requires a map callback returning an API method call"); + } + const callback = argument.arguments[0]; + const receiver = getTextWithOwner(argument.expression.expression); + const parameters = callback.parameters.map(parameter => getTextWithOwner(parameter)).join(", "); + return `(yield* all(...${receiver}.map((${parameters}) => ${getGeneratorCallText(argument.arguments[0].body)})))`; + } if (isAPIRequestCall(node.expression)) { const receiver = node.expression.expression.expression; if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "client") return getRequestYieldText(node.expression); @@ -487,6 +534,11 @@ function transformAsyncSource(source: string, fileName: string, attachGenerators } function addSyncEdit(node: ts.Node, source: string, sourceFile: ts.SourceFile, edits: Edit[], offset: number = 0): boolean { + if (isPromiseAllCall(node)) { + const argument = node.arguments[0]; + edits.push({ start: node.getStart(sourceFile) - offset, end: argument.getStart(sourceFile) - offset, newText: "" }); + edits.push({ start: argument.end - offset, end: node.end - offset, newText: "" }); + } if ( (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && node.modifiers @@ -540,6 +592,15 @@ function addSyncEdit(node: ts.Node, source: string, sourceFile: ts.SourceFile, e return false; } +function isPromiseAllCall(node: ts.Node): node is ts.CallExpression { + return ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === "Promise" && + node.expression.name.text === "all" && + node.arguments.length === 1; +} + function isPromiseTypeReference(node: ts.Node | undefined): node is ts.TypeReferenceNode { return !!node && ts.isTypeReferenceNode(node) && @@ -641,7 +702,7 @@ export function generateSync(force = false): void { ? (source, fileName) => [ 'import type { APIRequest as ProtocolRequest, APIResponse as ProtocolResponse } from "../proto.ts";', - 'import { apiRequest, cacheGeneratorMethod } from "./generatorSupport.ts";', + 'import { all, apiRequest, cacheGeneratorMethod, type APIRequestGenerator } from "./generatorSupport.ts";', "", transformAsyncSource(source, fileName, true), ].join("\n") @@ -652,6 +713,8 @@ export function generateSync(force = false): void { transformAsyncSource(source, fileName, true), ].join("\n"), force, + "sync", + relPath === "api.ts", )); } diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 8936bc1881ca7..c685495b8bbad 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1610,13 +1610,17 @@ export class Project { async getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): Promise { const files = await this.program.getSourceFileNames(); const result: RootedFilePath[] = []; - for (const file of files) { - if (excludeFilesFromExternalLibraries) { - const metadata = await this.program.getSourceFileMetadata(file); - if (!metadata) { + const metadata = excludeFilesFromExternalLibraries + ? await Promise.all(files.map(file => this.program.getSourceFileMetadata(file))) + : undefined; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (metadata) { + const fileMetadata = metadata[i]; + if (!fileMetadata) { throw new Error(`Program source file '${file}' has no metadata`); } - if (metadata.isFromExternalLibrary) continue; + if (fileMetadata.isFromExternalLibrary) continue; } result.push(file); } @@ -1869,11 +1873,13 @@ export class Program implements FormatDiagnost } async getSourceFiles(): Promise { + const fileNames = await this.getSourceFileNames(); + const sourceFiles = await Promise.all(fileNames.map(fileName => this.getSourceFile(fileName))); const files: SourceFile[] = []; - for (const fileName of await this.getSourceFileNames()) { - const file = await this.getSourceFile(fileName); + for (let i = 0; i < fileNames.length; i++) { + const file = sourceFiles[i]; if (!file) { - throw new Error(`Program source file '${fileName}' could not be loaded`); + throw new Error(`Program source file '${fileNames[i]}' could not be loaded`); } files.push(file); } @@ -1912,7 +1918,9 @@ export class Program implements FormatDiagnost } getResolvedProjectReferences(): Promise { - return this.resolvedProjectReferences ??= this.fetchResolvedProjectReferences(); + // @generators: if (owner.resolvedProjectReferences !== undefined) return owner.resolvedProjectReferences; + // @generators: const references = yield* owner.fetchResolvedProjectReferences.gen(); + return this.resolvedProjectReferences ??= this.fetchResolvedProjectReferences(); // @generators: return owner.resolvedProjectReferences ??= references; } private async fetchResolvedProjectReferences(): Promise { diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 9d7ee5e1e7d75..b86a010bdc82f 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -11,7 +11,9 @@ import type { APIResponse as ProtocolResponse, } from "../proto.ts"; import { + all, apiRequest, + type APIRequestGenerator, cacheGeneratorMethod, } from "./generatorSupport.ts"; @@ -2658,7 +2660,7 @@ export class Project { get getFileNames(): { (excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): readonly RootedFilePath[]; - gen(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): Generator; + gen(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): APIRequestGenerator; } { const owner = this; return cacheGeneratorMethod( @@ -2667,13 +2669,17 @@ export class Project { function (excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): readonly RootedFilePath[] { const files = owner.program.getSourceFileNames(); const result: RootedFilePath[] = []; - for (const file of files) { - if (excludeFilesFromExternalLibraries) { - const metadata = owner.program.getSourceFileMetadata(file); - if (!metadata) { + const metadata = excludeFilesFromExternalLibraries + ? files.map(file => owner.program.getSourceFileMetadata(file)) + : undefined; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (metadata) { + const fileMetadata = metadata[i]; + if (!fileMetadata) { throw new Error(`Program source file '${file}' has no metadata`); } - if (metadata.isFromExternalLibrary) continue; + if (fileMetadata.isFromExternalLibrary) continue; } result.push(file); } @@ -2682,16 +2688,20 @@ export class Project { } return result; }, - function* (excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): Generator { + function* (excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): APIRequestGenerator { const files = yield* owner.program.getSourceFileNames.gen(); const result: RootedFilePath[] = []; - for (const file of files) { - if (excludeFilesFromExternalLibraries) { - const metadata = yield* owner.program.getSourceFileMetadata.gen(file); - if (!metadata) { + const metadata = excludeFilesFromExternalLibraries + ? (yield* all(...files.map(file => owner.program.getSourceFileMetadata.gen(file)))) + : undefined; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (metadata) { + const fileMetadata = metadata[i]; + if (!fileMetadata) { throw new Error(`Program source file '${file}' has no metadata`); } - if (metadata.isFromExternalLibrary) continue; + if (fileMetadata.isFromExternalLibrary) continue; } result.push(file); } @@ -3244,29 +3254,33 @@ export class Program implements FormatDiagnost get getSourceFiles(): { (): readonly SourceFile[]; - gen(): Generator; + gen(): APIRequestGenerator; } { const owner = this; return cacheGeneratorMethod( owner, "getSourceFiles", function (): readonly SourceFile[] { + const fileNames = owner.getSourceFileNames(); + const sourceFiles = fileNames.map(fileName => owner.getSourceFile(fileName)); const files: SourceFile[] = []; - for (const fileName of owner.getSourceFileNames()) { - const file = owner.getSourceFile(fileName); + for (let i = 0; i < fileNames.length; i++) { + const file = sourceFiles[i]; if (!file) { - throw new Error(`Program source file '${fileName}' could not be loaded`); + throw new Error(`Program source file '${fileNames[i]}' could not be loaded`); } files.push(file); } return files; }, - function* (): Generator { + function* (): APIRequestGenerator { + const fileNames = yield* owner.getSourceFileNames.gen(); + const sourceFiles = yield* all(...fileNames.map(fileName => owner.getSourceFile.gen(fileName))); const files: SourceFile[] = []; - for (const fileName of (yield* owner.getSourceFileNames.gen())) { - const file = yield* owner.getSourceFile.gen(fileName); + for (let i = 0; i < fileNames.length; i++) { + const file = sourceFiles[i]; if (!file) { - throw new Error(`Program source file '${fileName}' could not be loaded`); + throw new Error(`Program source file '${fileNames[i]}' could not be loaded`); } files.push(file); } @@ -3412,7 +3426,9 @@ export class Program implements FormatDiagnost return owner.resolvedProjectReferences ??= owner.fetchResolvedProjectReferences(); }, function* (): Generator { - return owner.resolvedProjectReferences ??= yield* owner.fetchResolvedProjectReferences.gen(); + if (owner.resolvedProjectReferences !== undefined) return owner.resolvedProjectReferences; + const references = yield* owner.fetchResolvedProjectReferences.gen(); + return owner.resolvedProjectReferences ??= references; }, ); } diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index a936eb56c18e9..5c8b1bd4ef8cb 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1567,6 +1567,43 @@ describe("API - automatic batching", { concurrency }, () => { assert.equal(parseCommandLine.options.strict, true); assert.deepStrictEqual(readConfigFile.config, {}); }); + + test("batches file-list lookups while preserving order and cached results", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" }, files: ["src/main.ts"] }), + "/src/main.ts": `import { value } from "my-lib"; import { local } from "./local"; export const result = value + local;`, + "/src/local.ts": "export const local = 1;", + "/node_modules/my-lib/package.json": JSON.stringify({ types: "./index.d.ts" }), + "/node_modules/my-lib/index.d.ts": "export declare const value: number;", + }, { collectTiming: true }); + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + const program = project.program; + const expected = ["/src/local.ts", "/src/main.ts", "/tsconfig.json"]; + + await api.resetTimingInfo(); + assert.deepEqual(await project.getFileNames(true), expected); + assert.equal((await api.getTimingInfo()).totals.requestCount, 3); + + await api.resetTimingInfo(); + assert.deepEqual(await project.getFileNames(true), expected); + assert.equal((await api.getTimingInfo()).totals.requestCount, 2); + + const local = await program.getSourceFile("/src/local.ts"); + assert.ok(local); + await api.resetTimingInfo(); + const files = await program.getSourceFiles(); + assert.deepEqual(files.map(file => file.fileName), ["/node_modules/my-lib/index.d.ts", "/src/local.ts", "/src/main.ts"]); + assert.strictEqual(files[1], local); + assert.equal((await api.getTimingInfo()).totals.requestCount, 2); + + await api.resetTimingInfo(); + const cached = await program.getSourceFiles(); + for (let i = 0; i < files.length; i++) { + assert.strictEqual(cached[i], files[i]); + } + assert.equal((await api.getTimingInfo()).totals.requestCount, 1); + }); }); describe("API - batchContext", { concurrency }, () => { diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index b65774ec9b21c..8a2474c2aeb04 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -142,7 +142,7 @@ export const missingValue = absent; }; type GeneratorMethod = ((...args: Args) => Result) & { - gen(...args: Args): Generator; + gen(...args: Args): APIRequestGenerator; }; type EquivalenceAssertion = (actual: Result, expected: Result, message?: string) => void; @@ -150,7 +150,7 @@ type EquivalenceAssertion = (actual: Result, expected: Result, message?: interface ParityCase { readonly ownerName: string; readonly methodName: string; - readonly runGenerator: () => Generator; + readonly runGenerator: () => APIRequestGenerator; readonly assertEquivalent: (message?: string) => void; } @@ -487,6 +487,68 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => ]); }); + test("concurrent resolved project reference generators reuse the first cached graph", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ files: [], references: [{ path: "./a" }, { path: "./b" }, { path: "./a" }] }), + "/a/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, files: ["index.ts"], references: [{ path: "../b" }] }), + "/a/index.ts": "export const a = 1;", + "/b/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, files: ["index.ts"], references: [{ path: "../a", circular: true }] }), + "/b/index.ts": "export const b = 1;", + }); + using snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; + const [first, second] = api.batch(program.getResolvedProjectReferences.gen(), program.getResolvedProjectReferences.gen()); + assert.ok(first); + assert.equal(first.length, 3); + assert.ok(second === first, "Concurrent calls must return the same reference graph"); + assert.strictEqual(first[2], first[0]); + assert.strictEqual(first[0]!.references![0], first[1]); + assert.strictEqual(first[1]!.references![0], first[0]); + assert.strictEqual(api.batch(program.getResolvedProjectReferences.gen())[0], first); + assert.strictEqual(program.getResolvedProjectReferences(), first); + }); + + test("batches file-list lookups while preserving order and cached results", context => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" }, files: ["src/main.ts"] }), + "/src/main.ts": `import { value } from "my-lib"; import { local } from "./local"; export const result = value + local;`, + "/src/local.ts": "export const local = 1;", + "/node_modules/my-lib/package.json": JSON.stringify({ types: "./index.d.ts" }), + "/node_modules/my-lib/index.d.ts": "export declare const value: number;", + }); + using snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + const program = project.program; + const requestBatches: string[][] = []; + observeRequestBatches(api, requestBatches, context); + + const [names] = api.batch(project.getFileNames.gen(true)); + assert.deepEqual(names, ["/src/local.ts", "/src/main.ts", "/tsconfig.json"]); + assert.deepEqual(requestBatches, [ + ["getSourceFileNames"], + ["getSourceFileMetadata", "getSourceFileMetadata", "getSourceFileMetadata"], + ["getConfigFileNames"], + ]); + requestBatches.length = 0; + assert.deepEqual(api.batch(project.getFileNames.gen(true))[0], names); + assert.deepEqual(requestBatches, [["getSourceFileNames"], ["getConfigFileNames"]]); + + const local = program.getSourceFile("/src/local.ts"); + assert.ok(local); + requestBatches.length = 0; + const [files] = api.batch(program.getSourceFiles.gen()); + assert.deepEqual(files.map(file => file.fileName), ["/node_modules/my-lib/index.d.ts", "/src/local.ts", "/src/main.ts"]); + assert.strictEqual(files[1], local); + assert.deepEqual(requestBatches, [["getSourceFileNames"], ["getSourceFile", "getSourceFile"]]); + + requestBatches.length = 0; + const [cached] = api.batch(program.getSourceFiles.gen()); + for (let i = 0; i < files.length; i++) { + assert.strictEqual(cached[i], files[i]); + } + assert.deepEqual(requestBatches, [["getSourceFileNames"]]); + }); + test("all and defer yield discriminated host messages without starting children", () => { let started = false; function* child() { From 47bca17c8aea6e678b87abb454dbc75e0623bf24 Mon Sep 17 00:00:00 2001 From: isabel duan Date: Sun, 1 Nov 2026 19:55:17 -0800 Subject: [PATCH 05/13] test failure --- packages/typescript/test/async/api.test.ts | 7 +++++-- packages/typescript/test/sync/api.test.ts | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index ef28a0e70bd0a..0522f8ba7aa62 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1595,7 +1595,8 @@ describe("API - automatic batching", { concurrency }, () => { const files = await program.getSourceFiles(); assert.deepEqual(files.map(file => file.fileName), ["/node_modules/my-lib/index.d.ts", "/src/local.ts", "/src/main.ts"]); assert.strictEqual(files[1], local); - assert.equal((await api.getTimingInfo()).totals.requestCount, 2); + // The WASM transport sends the two uncached binary source-file requests individually. + assert.equal((await api.getTimingInfo()).totals.requestCount, process.env.TS_API_TEST_WASM ? 3 : 2); await api.resetTimingInfo(); const cached = await program.getSourceFiles(); @@ -2904,7 +2905,9 @@ const value: string = 1;`, assert.equal(await project.containsFile("/src/main.ts", true), true); }); - test("Project membership includes supplemental files without physical files", async t => { + test("Project membership includes supplemental files without physical files", { + skip: process.env.TS_API_TEST_WASM ? "WASM cannot spawn external content mapper processes" : false, + }, async t => { const directory = mkdtempSync(join(tmpdir(), "ts-project-membership-")); t.after(() => rmSync(directory, { recursive: true, force: true })); const mapperDirectory = join(directory, "node_modules", "mapper"); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 8690a10303759..64250ff47d068 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2725,7 +2725,9 @@ const value: string = 1;`, assert.equal(project.containsFile("/src/main.ts", true), true); }); - test("Project membership includes supplemental files without physical files", t => { + test("Project membership includes supplemental files without physical files", { + skip: process.env.TS_API_TEST_WASM ? "WASM cannot spawn external content mapper processes" : false, + }, t => { const directory = mkdtempSync(join(tmpdir(), "ts-project-membership-")); t.after(() => rmSync(directory, { recursive: true, force: true })); const mapperDirectory = join(directory, "node_modules", "mapper"); From 32fb65147fb748529b55b5d02d99a2d1085ea2ea Mon Sep 17 00:00:00 2001 From: isabel duan Date: Wed, 4 Nov 2026 15:13:19 -0800 Subject: [PATCH 06/13] remove getTypeChecker --- packages/typescript/src/api/async/api.ts | 8 ---- .../typescript/src/api/proto.generated.ts | 3 -- packages/typescript/src/api/sync/api.ts | 25 ------------ packages/typescript/test/async/api.test.ts | 14 ++++--- .../test/sync/api-generators.test.ts | 1 - packages/typescript/test/sync/api.test.ts | 14 ++++--- tsc/internal/api/proto.go | 2 - tsc/internal/api/session.go | 11 ----- tsc/internal/api/session_program_test.go | 40 ------------------- 9 files changed, 16 insertions(+), 102 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 9568e8eee678d..c6fc6a6f6d399 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1874,14 +1874,6 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.projectReferences; } - async getTypeChecker(): Promise { - await this.client.apiRequest("ensureTypeChecker", { - snapshot: this.snapshotId, - project: this.project.handle, - }); - return this.project.checker; - } - async getSourceFiles(): Promise { const fileNames = await this.getSourceFileNames(); const sourceFiles = await Promise.all(fileNames.map(fileName => this.getSourceFile(fileName))); diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 9576b7b2b08e0..0178f002675b8 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -82,7 +82,6 @@ export interface APIMethodInfo { getSourceFileNames: APIMethod; containsProjectFile: APIMethod; getProjectRootFiles: APIMethod; - ensureTypeChecker: APIMethod; getProgramStatistics: APIMethod; getResolvedProjectReferences: APIMethod; getOptionsDiagnostics: APIMethod; @@ -1194,7 +1193,6 @@ export interface BatchRequest { | "disposeBuildOrchestrator" | "emit" | "emitToString" - | "ensureTypeChecker" | "formatNodeForInsertion" | "getAliasSymbolOfType" | "getAliasTypeArgumentsOfType" @@ -1382,7 +1380,6 @@ export interface BatchResponse { | "disposeBuildOrchestrator" | "emit" | "emitToString" - | "ensureTypeChecker" | "formatNodeForInsertion" | "getAliasSymbolOfType" | "getAliasTypeArgumentsOfType" diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index d4088e0a82afb..7da4abbe1cf11 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -3227,31 +3227,6 @@ export class Program implements FormatDiagnost ); } - get getTypeChecker(): { - (): Checker; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getTypeChecker", - function (): Checker { - owner.client.apiRequest("ensureTypeChecker", { - snapshot: owner.snapshotId, - project: owner.project.handle, - }); - return owner.project.checker; - }, - function* (): Generator { - yield* apiRequest("ensureTypeChecker", { - snapshot: owner.snapshotId, - project: owner.project.handle, - }); - return owner.project.checker; - }, - ); - } - get getSourceFiles(): { (): readonly SourceFile[]; gen(): APIRequestGenerator; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 8a5bc8b5b39d2..ae4b5e43f53ff 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -2746,19 +2746,17 @@ describe("SourceFile", { concurrency }, () => { ); }); - test("Program accessors distinguish root files from source files and reuse the checker", async () => { + test("Program accessors distinguish root files from source files", async () => { await using api = spawnAPI({ "/src/main.ts": `import { value } from "./dependency"; export const result = value;`, "/src/dependency.ts": `export const value = 1;`, }); await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); + assert.ok(!("getTypeChecker" in program)); assert.deepEqual(await program.getRootFileNames(), ["/src/main.ts"]); const files = await program.getSourceFiles(); assert.deepEqual(files.map(file => file.fileName), ["/src/dependency.ts", "/src/main.ts"]); assert.strictEqual(files[1], await program.getSourceFile("/src/main.ts")); - const checker = await program.getTypeChecker(); - assert.strictEqual(await program.getTypeChecker(), checker); - assert.ok(await checker.getSymbolAtLocation(cast(files[1].statements[1], isVariableStatement).declarationList.declarations[0].name)); assert.equal(await program.getProjectReferences(), undefined); assert.equal(await program.getResolvedProjectReferences(), undefined); }); @@ -2769,7 +2767,11 @@ describe("SourceFile", { concurrency }, () => { export const box: Box = { value: "hello" }; export const other: { value: string } = box;`, }); - await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); + const snapshot = await api.createSnapshot({ + createPrograms: [{ rootFiles: ["/src/main.ts"], compilerOptions: { noLib: true } }], + }); + const project = snapshot.getProjects()[0]; + const program = project.program; const identifiers = await program.getIdentifierCount(); const nodes = await program.getNodeCount(); const symbols = await program.getSymbolCount(); @@ -2784,7 +2786,7 @@ export const other: { value: string } = box;`, assert.deepEqual(relations, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); const file = (await program.getSourceFile("/src/main.ts"))!; - const checker = await program.getTypeChecker(); + const checker = project.checker; const declaration = cast(file.statements[1], isVariableStatement).declarationList.declarations[0]; const type = await checker.getTypeAtLocation(declaration.name); await type.getProperties(); diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index d8d6d99a0ef77..46dd31f431273 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -1695,7 +1695,6 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => }), parityCase("Program", "getProjectReferences", program.getProjectReferences, assertDeepEquivalent), parityCase("Program", "getResolvedProjectReferences", program.getResolvedProjectReferences, assertDeepEquivalent), - parityCase("Program", "getTypeChecker", program.getTypeChecker, assert.strictEqual), parityCase("Program", "getSourceFileMetadata", program.getSourceFileMetadata, assertDeepEquivalent, "/src/index.ts"), parityCase("Program", "getSourceFileMetadataByPath", program.getSourceFileMetadataByPath, assertDeepEquivalent, indexFile.path), parityCase("Program", "isSourceFileFromExternalLibrary", program.isSourceFileFromExternalLibrary, assertDeepEquivalent, indexFile), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index f0f7cfc47f801..eee5aa35caf09 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2566,19 +2566,17 @@ describe("SourceFile", { concurrency }, () => { ); }); - test("Program accessors distinguish root files from source files and reuse the checker", () => { + test("Program accessors distinguish root files from source files", () => { using api = spawnAPI({ "/src/main.ts": `import { value } from "./dependency"; export const result = value;`, "/src/dependency.ts": `export const value = 1;`, }); using program = api.createProgram(["/src/main.ts"], { noLib: true }); + assert.ok(!("getTypeChecker" in program)); assert.deepEqual(program.getRootFileNames(), ["/src/main.ts"]); const files = program.getSourceFiles(); assert.deepEqual(files.map(file => file.fileName), ["/src/dependency.ts", "/src/main.ts"]); assert.strictEqual(files[1], program.getSourceFile("/src/main.ts")); - const checker = program.getTypeChecker(); - assert.strictEqual(program.getTypeChecker(), checker); - assert.ok(checker.getSymbolAtLocation(cast(files[1].statements[1], isVariableStatement).declarationList.declarations[0].name)); assert.equal(program.getProjectReferences(), undefined); assert.equal(program.getResolvedProjectReferences(), undefined); }); @@ -2589,7 +2587,11 @@ describe("SourceFile", { concurrency }, () => { export const box: Box = { value: "hello" }; export const other: { value: string } = box;`, }); - using program = api.createProgram(["/src/main.ts"], { noLib: true }); + const snapshot = api.createSnapshot({ + createPrograms: [{ rootFiles: ["/src/main.ts"], compilerOptions: { noLib: true } }], + }); + const project = snapshot.getProjects()[0]; + const program = project.program; const identifiers = program.getIdentifierCount(); const nodes = program.getNodeCount(); const symbols = program.getSymbolCount(); @@ -2604,7 +2606,7 @@ export const other: { value: string } = box;`, assert.deepEqual(relations, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); const file = (program.getSourceFile("/src/main.ts"))!; - const checker = program.getTypeChecker(); + const checker = project.checker; const declaration = cast(file.statements[1], isVariableStatement).declarationList.declarations[0]; const type = checker.getTypeAtLocation(declaration.name); type.getProperties(); diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 73ad40fa17816..7f89d79cc22e5 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -113,7 +113,6 @@ const ( MethodGetSourceFileNames Method = "getSourceFileNames" MethodContainsProjectFile Method = "containsProjectFile" MethodGetProjectRootFiles Method = "getProjectRootFiles" - MethodEnsureTypeChecker Method = "ensureTypeChecker" MethodGetProgramStatistics Method = "getProgramStatistics" MethodGetResolvedProjectReferences Method = "getResolvedProjectReferences" MethodGetOptionsDiagnostics Method = "getOptionsDiagnostics" @@ -780,7 +779,6 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetSuggestionDiagnostics: unmarshallerFor[GetDiagnosticsParams], MethodGetDeclarationDiagnostics: unmarshallerFor[GetDiagnosticsParams], MethodGetProgramDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], - MethodEnsureTypeChecker: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetProgramStatistics: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetResolvedProjectReferences: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetOptionsDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 75df3961e3ec6..2eb419c3c76bd 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -955,8 +955,6 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleContainsProjectFile(ctx, parsed.(*ContainsProjectFileParams)) case string(MethodGetProjectRootFiles): return s.handleGetProjectRootFiles(ctx, parsed.(*GetSourceFileNamesParams)) - case string(MethodEnsureTypeChecker): - return s.handleEnsureTypeChecker(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodGetProgramStatistics): return s.handleGetProgramStatistics(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodGetResolvedProjectReferences): @@ -2386,15 +2384,6 @@ func transpileOutput(ctx context.Context, input string, options TranspileOptions }, nil } -func (s *Session) handleEnsureTypeChecker(ctx context.Context, params *GetProjectDiagnosticsParams) (bool, error) { - setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) - if err != nil { - return false, err - } - defer setup.done() - return true, nil -} - func (s *Session) handleGetProgramStatistics(ctx context.Context, params *GetProjectDiagnosticsParams) (*ProgramStatisticsResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) if err != nil { diff --git a/tsc/internal/api/session_program_test.go b/tsc/internal/api/session_program_test.go index e31cd1dad8a44..af0a6a5048c98 100644 --- a/tsc/internal/api/session_program_test.go +++ b/tsc/internal/api/session_program_test.go @@ -134,43 +134,3 @@ func TestProgramStatisticsUseAPIChecker(t *testing.T) { }) assert.ErrorContains(t, err, "not found") } - -func TestEnsureTypeChecker(t *testing.T) { - t.Parallel() - - const fileName = "/home/projects/p/index.ts" - projectSession, _ := projecttestutil.Setup(map[string]any{ - fileName: "export const value = 1;", - }) - defer projectSession.Close() - session := NewLSPSession(projectSession, nil) - defer session.Close() - response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ - CreatePrograms: []*CreateSnapshotProgramParams{{ - RootFiles: []DocumentIdentifier{{FileName: fileName}}, - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, - }}, - }) - assert.NilError(t, err) - params := &GetProjectDiagnosticsParams{Snapshot: response.Snapshot, Project: response.Projects[0].Handle} - initialized, err := session.handleEnsureTypeChecker(context.Background(), params) - assert.NilError(t, err) - assert.Assert(t, initialized) - setup, err := session.setupChecker(context.Background(), params.Snapshot, params.Project) - assert.NilError(t, err) - first := setup.checker - setup.done() - - initialized, err = session.handleEnsureTypeChecker(context.Background(), params) - assert.NilError(t, err) - assert.Assert(t, initialized) - setup, err = session.setupChecker(context.Background(), params.Snapshot, params.Project) - assert.NilError(t, err) - assert.Assert(t, setup.checker == first) - setup.done() - - _, err = session.handleEnsureTypeChecker(context.Background(), &GetProjectDiagnosticsParams{ - Snapshot: response.Snapshot, Project: project.Handle(999999), - }) - assert.ErrorContains(t, err, "not found") -} From 18f4bc8a9bda1449efe716352d4328d2c60e1b68 Mon Sep 17 00:00:00 2001 From: isabel duan Date: Wed, 4 Nov 2026 16:20:37 -0800 Subject: [PATCH 07/13] remove extra asyncs --- packages/typescript/src/api/async/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index c6fc6a6f6d399..6c96f304bd4d2 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1866,7 +1866,7 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.options; } - async getRootFileNames(): Promise { + getRootFileNames(): readonly RootedFilePath[] { return this.project.parsedCommandLine.fileNames; } From 97c4ee45f79efb4416f452587fe67ad07c5c4ae3 Mon Sep 17 00:00:00 2001 From: isabel duan Date: Wed, 4 Nov 2026 18:21:25 -0800 Subject: [PATCH 08/13] remove project methods --- packages/typescript/src/api/async/api.ts | 78 ++---- .../typescript/src/api/proto.generated.ts | 13 - packages/typescript/src/api/sync/api.ts | 231 ++---------------- packages/typescript/test/async/api.test.ts | 170 +++++-------- .../test/sync/api-generators.test.ts | 28 +-- packages/typescript/test/sync/api.test.ts | 106 +------- tsc/internal/api/proto.go | 11 - tsc/internal/api/session.go | 47 ---- tsc/internal/api/session_program_test.go | 53 +--- 9 files changed, 120 insertions(+), 617 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 6c96f304bd4d2..dca0e250a1797 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1588,13 +1588,11 @@ export class Project { readonly program: Program; readonly checker: Checker; readonly languageService: LanguageService; - private readonly toPath: (fileName: string, basePath?: string) => PathKey; 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.toPath = toPath; this.configFileName = data.configFileName; this.currentDirectory = data.currentDirectory; this.dirty = data.dirty; @@ -1608,64 +1606,6 @@ export class Project { this.languageService = new LanguageService(snapshotId, this, objectRegistry); } - async containsFile(file: DocumentIdentifier, requireOpen?: boolean): Promise { - return this.api.client.apiRequest("containsProjectFile", { - snapshot: this.program.snapshotId, - project: this.handle, - file, - requireOpen, - }); - } - - async getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): Promise { - const files = await this.program.getSourceFileNames(); - const result: RootedFilePath[] = []; - const metadata = excludeFilesFromExternalLibraries - ? await Promise.all(files.map(file => this.program.getSourceFileMetadata(file))) - : undefined; - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (metadata) { - const fileMetadata = metadata[i]; - if (!fileMetadata) { - throw new Error(`Program source file '${file}' has no metadata`); - } - if (fileMetadata.isFromExternalLibrary) continue; - } - result.push(file); - } - if (!excludeConfigFiles) { - result.push(...await this.program.getConfigFileNames()); - } - return result; - } - - /** Returns existing root files, excluding missing root declarations. */ - async getRootFiles(): Promise { - return this.api.client.apiRequest("getProjectRootFiles", { - snapshot: this.program.snapshotId, - project: this.handle, - }); - } - - async hasRoots(): Promise { - return this.parsedCommandLine.fileNames.length > 0; - } - - async isRoot(file: DocumentIdentifier): Promise { - const path = this.toPath(resolveFileName(file), this.currentDirectory); - return (await this.getRootFiles()).some(root => this.toPath(root, this.currentDirectory) === path); - } - - async hasConfigFile(file: DocumentIdentifier): Promise { - const path = this.toPath(resolveFileName(file), this.currentDirectory); - return (await this.program.getConfigFileNames()).some(config => this.toPath(config, this.currentDirectory) === path); - } - - async getSourceFile(file: DocumentIdentifier): Promise { - return this.program.getSourceFile(file); - } - dispose(): void { this.checker.dispose(); } @@ -1815,6 +1755,7 @@ export class Program implements FormatDiagnost private ownedSnapshot: Snapshot | undefined; private disposePromise: Promise | undefined; private resolvedProjectReferences: Promise | undefined; + private pendingStatistics: Promise | undefined; // @sync-skip private get client(): Client { return this.project.api.client; @@ -1870,7 +1811,7 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.fileNames; } - async getProjectReferences(): Promise { + getProjectReferences(): readonly ProjectReference[] | undefined { return this.project.parsedCommandLine.projectReferences; } @@ -1889,6 +1830,21 @@ export class Program implements FormatDiagnost } private async getStatistics(): Promise { + // @sync-only-start + // return this.fetchStatistics(); + // @sync-only-end + // @sync-skip-block-start + if (this.pendingStatistics) return this.pendingStatistics; + try { + return await (this.pendingStatistics = this.fetchStatistics()); + } + finally { + this.pendingStatistics = undefined; + } + // @sync-skip-block-end + } + + private async fetchStatistics(): Promise { return this.client.apiRequest("getProgramStatistics", { snapshot: this.snapshotId, project: this.project.handle, diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 0178f002675b8..667265ef0786c 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -80,8 +80,6 @@ export interface APIMethodInfo { getNonMissingTypeOfSymbol: APIMethod; getSourceFile: APIMethod; getSourceFileNames: APIMethod; - containsProjectFile: APIMethod; - getProjectRootFiles: APIMethod; getProgramStatistics: APIMethod; getResolvedProjectReferences: APIMethod; getOptionsDiagnostics: APIMethod; @@ -596,13 +594,6 @@ export interface GetSourceFileNamesParams { project: number; } -export interface ContainsProjectFileParams { - snapshot: number; - project: number; - file: DocumentIdentifier; - requireOpen?: boolean | undefined; -} - /** GetProjectDiagnosticsParams are parameters for project-wide diagnostic methods. */ export interface GetProjectDiagnosticsParams { snapshot: number; @@ -1184,7 +1175,6 @@ export interface BatchRequest { | "buildReferences" | "cleanBuild" | "cleanReferences" - | "containsProjectFile" | "createBuildOrchestrator" | "createModuleResolver" | "createSnapshot" @@ -1267,7 +1257,6 @@ export interface BatchRequest { | "getParentOfSymbolForChecker" | "getProgramDiagnostics" | "getProgramStatistics" - | "getProjectRootFiles" | "getPropertiesOfType" | "getPropertyOfType" | "getReducedType" @@ -1371,7 +1360,6 @@ export interface BatchResponse { | "buildReferences" | "cleanBuild" | "cleanReferences" - | "containsProjectFile" | "createBuildOrchestrator" | "createModuleResolver" | "createSnapshot" @@ -1454,7 +1442,6 @@ export interface BatchResponse { | "getParentOfSymbolForChecker" | "getProgramDiagnostics" | "getProgramStatistics" - | "getProjectRootFiles" | "getPropertiesOfType" | "getPropertyOfType" | "getReducedType" diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 7da4abbe1cf11..fdee53ee4d214 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -2611,13 +2611,11 @@ export class Project { readonly program: Program; readonly checker: Checker; readonly languageService: LanguageService; - private readonly toPath: (fileName: string, basePath?: string) => PathKey; 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.toPath = toPath; this.configFileName = data.configFileName; this.currentDirectory = data.currentDirectory; this.dirty = data.dirty; @@ -2631,184 +2629,6 @@ export class Project { this.languageService = new LanguageService(snapshotId, this, objectRegistry); } - get containsFile(): { - (file: DocumentIdentifier, requireOpen?: boolean): boolean; - gen(file: DocumentIdentifier, requireOpen?: boolean): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "containsFile", - function (file: DocumentIdentifier, requireOpen?: boolean): boolean { - return owner.api.client.apiRequest("containsProjectFile", { - snapshot: owner.program.snapshotId, - project: owner.handle, - file, - requireOpen, - }); - }, - function* (file: DocumentIdentifier, requireOpen?: boolean): Generator { - return yield* apiRequest("containsProjectFile", { - snapshot: owner.program.snapshotId, - project: owner.handle, - file, - requireOpen, - }); - }, - ); - } - - get getFileNames(): { - (excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): readonly RootedFilePath[]; - gen(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): APIRequestGenerator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getFileNames", - function (excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): readonly RootedFilePath[] { - const files = owner.program.getSourceFileNames(); - const result: RootedFilePath[] = []; - const metadata = excludeFilesFromExternalLibraries - ? files.map(file => owner.program.getSourceFileMetadata(file)) - : undefined; - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (metadata) { - const fileMetadata = metadata[i]; - if (!fileMetadata) { - throw new Error(`Program source file '${file}' has no metadata`); - } - if (fileMetadata.isFromExternalLibrary) continue; - } - result.push(file); - } - if (!excludeConfigFiles) { - result.push(...owner.program.getConfigFileNames()); - } - return result; - }, - function* (excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): APIRequestGenerator { - const files = yield* owner.program.getSourceFileNames.gen(); - const result: RootedFilePath[] = []; - const metadata = excludeFilesFromExternalLibraries - ? (yield* all(...files.map(file => owner.program.getSourceFileMetadata.gen(file)))) - : undefined; - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (metadata) { - const fileMetadata = metadata[i]; - if (!fileMetadata) { - throw new Error(`Program source file '${file}' has no metadata`); - } - if (fileMetadata.isFromExternalLibrary) continue; - } - result.push(file); - } - if (!excludeConfigFiles) { - result.push(...(yield* owner.program.getConfigFileNames.gen())); - } - return result; - }, - ); - } - - /** Returns existing root files, excluding missing root declarations. */ - get getRootFiles(): { - (): readonly RootedFilePath[]; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getRootFiles", - function (): readonly RootedFilePath[] { - return owner.api.client.apiRequest("getProjectRootFiles", { - snapshot: owner.program.snapshotId, - project: owner.handle, - }); - }, - function* (): Generator { - return yield* apiRequest("getProjectRootFiles", { - snapshot: owner.program.snapshotId, - project: owner.handle, - }); - }, - ); - } - - get hasRoots(): { - (): boolean; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "hasRoots", - function (): boolean { - return owner.parsedCommandLine.fileNames.length > 0; - }, - function* (): Generator { - return owner.parsedCommandLine.fileNames.length > 0; - }, - ); - } - - get isRoot(): { - (file: DocumentIdentifier): boolean; - gen(file: DocumentIdentifier): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "isRoot", - function (file: DocumentIdentifier): boolean { - const path = owner.toPath(resolveFileName(file), owner.currentDirectory); - return (owner.getRootFiles()).some(root => owner.toPath(root, owner.currentDirectory) === path); - }, - function* (file: DocumentIdentifier): Generator { - const path = owner.toPath(resolveFileName(file), owner.currentDirectory); - return (yield* owner.getRootFiles.gen()).some(root => owner.toPath(root, owner.currentDirectory) === path); - }, - ); - } - - get hasConfigFile(): { - (file: DocumentIdentifier): boolean; - gen(file: DocumentIdentifier): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "hasConfigFile", - function (file: DocumentIdentifier): boolean { - const path = owner.toPath(resolveFileName(file), owner.currentDirectory); - return (owner.program.getConfigFileNames()).some(config => owner.toPath(config, owner.currentDirectory) === path); - }, - function* (file: DocumentIdentifier): Generator { - const path = owner.toPath(resolveFileName(file), owner.currentDirectory); - return (yield* owner.program.getConfigFileNames.gen()).some(config => owner.toPath(config, owner.currentDirectory) === path); - }, - ); - } - - get getSourceFile(): { - (file: DocumentIdentifier): SourceFile | undefined; - gen(file: DocumentIdentifier): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getSourceFile", - function (file: DocumentIdentifier): SourceFile | undefined { - return owner.program.getSourceFile(file); - }, - function* (file: DocumentIdentifier): Generator { - return yield* owner.program.getSourceFile.gen(file); - }, - ); - } - dispose(): void { this.checker.dispose(); } @@ -3193,38 +3013,12 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.options; } - get getRootFileNames(): { - (): readonly RootedFilePath[]; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getRootFileNames", - function (): readonly RootedFilePath[] { - return owner.project.parsedCommandLine.fileNames; - }, - function* (): Generator { - return owner.project.parsedCommandLine.fileNames; - }, - ); + getRootFileNames(): readonly RootedFilePath[] { + return this.project.parsedCommandLine.fileNames; } - get getProjectReferences(): { - (): readonly ProjectReference[] | undefined; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getProjectReferences", - function (): readonly ProjectReference[] | undefined { - return owner.project.parsedCommandLine.projectReferences; - }, - function* (): Generator { - return owner.project.parsedCommandLine.projectReferences; - }, - ); + getProjectReferences(): readonly ProjectReference[] | undefined { + return this.project.parsedCommandLine.projectReferences; } get getSourceFiles(): { @@ -3272,6 +3066,23 @@ export class Program implements FormatDiagnost return cacheGeneratorMethod( owner, "getStatistics", + function (): ProgramStatisticsResponse { + return owner.fetchStatistics(); + }, + function* (): Generator { + return yield* owner.fetchStatistics.gen(); + }, + ); + } + + private get fetchStatistics(): { + (): ProgramStatisticsResponse; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "fetchStatistics", function (): ProgramStatisticsResponse { return owner.client.apiRequest("getProgramStatistics", { snapshot: owner.snapshotId, diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index ae4b5e43f53ff..88000f688e9ca 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1527,6 +1527,56 @@ declare module "augmentation" {}`, // @sync-skip-block-start describe("API - automatic batching", { concurrency }, () => { + test("shares pending Program statistics requests without caching settled results", async context => { + await using api = spawnAPI({ "/src/main.ts": "export const value = 1;" }); + await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); + await using empty = await api.createProgram([], { noLib: true }); + // @ts-ignore -- Internal client requests are exercised by source tests. + const client = api["client"]; + const request = context.mock.method(client, "apiRequest"); + + const [identifiers, instantiations, nodes, symbols, types, relations, emptyNodes] = await Promise.all([ + program.getIdentifierCount(), + program.getInstantiationCount(), + program.getNodeCount(), + program.getSymbolCount(), + program.getTypeCount(), + program.getRelationCacheSizes(), + empty.getNodeCount(), + ]); + assert.equal(request.mock.callCount(), 2); + assert.ok(identifiers > 0); + assert.ok(nodes > identifiers); + assert.ok(symbols > 0); + assert.ok(types > 0); + assert.equal(instantiations, 0); + assert.deepEqual(relations, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); + assert.equal(emptyNodes, 0); + + await program.getTypeCount(); + await program.getSymbolCount(); + assert.equal(request.mock.callCount(), 4); + }); + + test("shares rejected Program statistics requests and retries after failure", async context => { + await using api = spawnAPI({ "/src/main.ts": "export const value = 1;" }); + await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); + // @ts-ignore -- Internal client requests are exercised by source tests. + const client = api["client"]; + const request = context.mock.method(client, "apiRequest"); + const error = new Error("Statistics request failed"); + request.mock.mockImplementationOnce(() => Promise.reject(error)); + + const results = await Promise.allSettled([program.getNodeCount(), program.getTypeCount()]); + assert.deepEqual(results, [ + { status: "rejected", reason: error }, + { status: "rejected", reason: error }, + ]); + assert.equal(request.mock.callCount(), 1); + assert.ok(await program.getNodeCount() > 0); + assert.equal(request.mock.callCount(), 2); + }); + test("initializes only once for concurrent first requests", async () => { await using api = spawnAPI(); const client = (api as unknown as { @@ -1568,7 +1618,7 @@ describe("API - automatic batching", { concurrency }, () => { assert.deepStrictEqual(readConfigFile.config, {}); }); - test("batches file-list lookups while preserving order and cached results", async () => { + test("batches source-file lookups while preserving order and cached results", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" }, files: ["src/main.ts"] }), "/src/main.ts": `import { value } from "my-lib"; import { local } from "./local"; export const result = value + local;`, @@ -1577,17 +1627,7 @@ describe("API - automatic batching", { concurrency }, () => { "/node_modules/my-lib/index.d.ts": "export declare const value: number;", }, { collectTiming: true }); const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); - const project = snapshot.getConfiguredProject("/tsconfig.json")!; - const program = project.program; - const expected = ["/src/local.ts", "/src/main.ts", "/tsconfig.json"]; - - await api.resetTimingInfo(); - assert.deepEqual(await project.getFileNames(true), expected); - assert.equal((await api.getTimingInfo()).totals.requestCount, 3); - - await api.resetTimingInfo(); - assert.deepEqual(await project.getFileNames(true), expected); - assert.equal((await api.getTimingInfo()).totals.requestCount, 2); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const local = await program.getSourceFile("/src/local.ts"); assert.ok(local); @@ -2875,39 +2915,16 @@ const value: string = 1;`, assert.equal(syntheticResolved![1], undefined); }); - test("Project membership distinguishes roots, dependencies, and open files", async () => { - await using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: ["src/main.ts", "missing.ts"] }), - "/src/main.ts": `import { value } from "./dependency"; export const result = value;`, - "/src/dependency.ts": `export const value = 1;`, - "/outside.ts": "export {};", - }); - const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"], openFiles: ["/src/main.ts", "/outside.ts"] }); - const project = snapshot.getConfiguredProject("/tsconfig.json")!; - assert.equal(await project.hasRoots(), true); - assert.deepEqual(await project.getRootFiles(), ["/src/main.ts"]); - assert.equal(await project.isRoot("src/main.ts"), true); - assert.equal(await project.isRoot({ uri: "file:///src/main.ts" }), true); - assert.equal(await project.isRoot("/missing.ts"), false); - assert.equal(await project.isRoot("/src/dependency.ts"), false); - assert.equal(await project.containsFile("src/main.ts"), true); - assert.equal(await project.containsFile({ uri: "file:///src/main.ts" }, true), true); - assert.equal(await project.containsFile("/src/dependency.ts"), true); - assert.equal(await project.containsFile("/src/dependency.ts", true), false); - assert.equal(await project.containsFile("/outside.ts"), false); - assert.equal(await project.containsFile("/outside.ts", true), false); - assert.equal(await project.containsFile("/missing.ts"), false); - assert.strictEqual(await project.getSourceFile("src/main.ts"), await project.program.getSourceFile("/src/main.ts")); - assert.equal(await project.getSourceFile("/outside.ts"), undefined); - assert.equal(await project.hasConfigFile("tsconfig.json"), true); - assert.equal(await project.hasConfigFile("/src/main.ts"), false); - - const updated = await snapshot.update({ closeFiles: ["/src/main.ts"] }); - assert.equal(await updated.getConfiguredProject("/tsconfig.json")!.containsFile("/src/main.ts", true), false); - assert.equal(await project.containsFile("/src/main.ts", true), true); - }); - - test("Project membership includes supplemental files without physical files", { + test("Project does not expose membership and file-list methods", async () => { + await using api = spawnAPI(); + const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + for (const method of ["containsFile", "getFileNames", "getRootFiles", "hasRoots", "isRoot", "hasConfigFile", "getSourceFile"]) { + assert.ok(!(method in project), `Project should not expose ${method}`); + } + }); + + test("getSourceFile includes supplemental files without physical files", { skip: process.env.TS_API_TEST_WASM ? "WASM cannot spawn external content mapper processes" : false, }, async t => { const directory = mkdtempSync(join(tmpdir(), "ts-project-membership-")); @@ -2948,69 +2965,10 @@ const value: string = 1;`, writeFileSync(join(directory, "main.ts"), "const value: number = supplementalValue;"); await using api = new API({ cwd: directory, runExternalCode: true }); const snapshot = await api.createSnapshot({ openProjects: [configFileName] }); - const project = snapshot.getConfiguredProject(configFileName)!; + const program = snapshot.getConfiguredProject(configFileName)!.program; const supplementalFileName = toRootedFilePath(join(directory, "app.box.0.ts"), undefined); assert.equal(existsSync(supplementalFileName), false); - assert.ok(await project.getSourceFile(supplementalFileName)); - assert.equal(await project.containsFile(supplementalFileName), true); - assert.equal(await project.containsFile(supplementalFileName, true), false); - assert.equal(await project.containsFile(join(directory, "missing.ts")), false); - }); - - test("Project file lists include extended configs and optionally exclude external libraries", async () => { - await using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ extends: "./base.json", files: ["src/main.ts"] }), - "/base.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" } }), - "/src/main.ts": `import { value } from "my-lib"; export const result = value;`, - "/node_modules/my-lib/package.json": JSON.stringify({ types: "./index.d.ts" }), - "/node_modules/my-lib/index.d.ts": "export declare const value: number;", - }); - const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); - const project = snapshot.getConfiguredProject("/tsconfig.json")!; - assert.deepEqual(await project.getFileNames(), ["/node_modules/my-lib/index.d.ts", "/src/main.ts", "/tsconfig.json", "/base.json"]); - assert.deepEqual(await project.getFileNames(true), ["/src/main.ts", "/tsconfig.json", "/base.json"]); - assert.deepEqual(await project.getFileNames(false, true), ["/node_modules/my-lib/index.d.ts", "/src/main.ts"]); - assert.deepEqual(await project.getFileNames(true, true), ["/src/main.ts"]); - assert.equal(await project.hasConfigFile("./base.json"), true); - assert.equal(await project.hasConfigFile({ uri: "file:///base.json" }), true); - assert.equal(await project.hasConfigFile("/other.json"), false); - assert.equal(await project.containsFile("/tsconfig.json"), false); - }); - - test("Project roots preserve missing declarations and empty projects", async () => { - await using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: [] }), - }); - const snapshot = await api.createSnapshot({ - openProjects: ["/tsconfig.json"], - createPrograms: [{ rootFiles: ["/missing.ts"], compilerOptions: { noLib: true } }], - }); - const empty = snapshot.getConfiguredProject("/tsconfig.json")!; - assert.equal(await empty.hasRoots(), false); - assert.deepEqual(await empty.getRootFiles(), []); - assert.deepEqual(await empty.getFileNames(), ["/tsconfig.json"]); - const missing = snapshot.getProject(snapshot.operation.createdPrograms![0].id)!; - assert.equal(await missing.hasRoots(), true); - assert.deepEqual(await missing.getRootFiles(), []); - assert.equal(await missing.isRoot("/missing.ts"), false); - assert.equal(await missing.containsFile("/missing.ts"), false); - assert.deepEqual(await missing.getFileNames(), []); - assert.equal(await missing.hasConfigFile("/tsconfig.json"), false); - }); - - test("Project membership uses configured path case sensitivity", async () => { - const files = { - "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: ["src/main.ts"] }), - "/src/main.ts": "export {};", - }; - for (const useCaseSensitiveFileNames of [true, false]) { - await using api = new API({ cwd: "/", fs: createVirtualFileSystem(files), useCaseSensitiveFileNames }); - const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"], openFiles: ["/src/main.ts"] }); - const project = snapshot.getConfiguredProject("/tsconfig.json")!; - assert.equal(await project.isRoot("/SRC/MAIN.TS"), !useCaseSensitiveFileNames); - assert.equal(await project.hasConfigFile("/TSCONFIG.JSON"), !useCaseSensitiveFileNames); - assert.equal(await project.containsFile("/SRC/MAIN.TS", true), !useCaseSensitiveFileNames); - } + assert.ok(await program.getSourceFile(supplementalFileName)); }); test("getSourceFileNames returns all program files, not just root files", async () => { diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 46dd31f431273..0f540d901282f 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -176,6 +176,7 @@ const privateGeneratorGetters = new Set([ "Program.getStatistics", "Program.fetchResolvedProjectReferences", "Program.fetchSourceFileMetadata", + "Program.fetchStatistics", "Program.getSourceFileWorker", "Snapshot.disposeWorker", "Symbol.fetchSymbol", @@ -508,7 +509,7 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => assert.strictEqual(program.getResolvedProjectReferences(), first); }); - test("batches file-list lookups while preserving order and cached results", context => { + test("batches source-file lookups while preserving order and cached results", context => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" }, files: ["src/main.ts"] }), "/src/main.ts": `import { value } from "my-lib"; import { local } from "./local"; export const result = value + local;`, @@ -517,22 +518,10 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => "/node_modules/my-lib/index.d.ts": "export declare const value: number;", }); using snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); - const project = snapshot.getConfiguredProject("/tsconfig.json")!; - const program = project.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const requestBatches: string[][] = []; observeRequestBatches(api, requestBatches, context); - const [names] = api.batch(project.getFileNames.gen(true)); - assert.deepEqual(names, ["/src/local.ts", "/src/main.ts", "/tsconfig.json"]); - assert.deepEqual(requestBatches, [ - ["getSourceFileNames"], - ["getSourceFileMetadata", "getSourceFileMetadata", "getSourceFileMetadata"], - ["getConfigFileNames"], - ]); - requestBatches.length = 0; - assert.deepEqual(api.batch(project.getFileNames.gen(true))[0], names); - assert.deepEqual(requestBatches, [["getSourceFileNames"], ["getConfigFileNames"]]); - const local = program.getSourceFile("/src/local.ts"); assert.ok(local); requestBatches.length = 0; @@ -1663,15 +1652,6 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Snapshot", "getDefaultProjectForFile", snapshot.getDefaultProjectForFile, assertOptionalProjectsEquivalent, "/src/index.ts"), parityCase("ModuleResolver", "resolveModuleName", moduleResolver.resolveModuleName, assertDeepEquivalent, "models", "/src"), parityCase("Snapshot", "update", snapshot.update, assertSnapshotsEquivalent, {}), - parityCase("Project", "containsFile", project.containsFile, assertDeepEquivalent, "/src/index.ts"), - parityCase("Project", "containsFile", project.containsFile, assertDeepEquivalent, "/src/index.ts", true), - parityCase("Project", "getFileNames", project.getFileNames, assertDeepEquivalent), - parityCase("Project", "getFileNames", project.getFileNames, assertDeepEquivalent, true, true), - parityCase("Project", "getRootFiles", project.getRootFiles, assertDeepEquivalent), - parityCase("Project", "hasRoots", project.hasRoots, assertDeepEquivalent), - parityCase("Project", "isRoot", project.isRoot, assertDeepEquivalent, "/src/index.ts"), - parityCase("Project", "hasConfigFile", project.hasConfigFile, assertDeepEquivalent, "/tsconfig.json"), - parityCase("Project", "getSourceFile", project.getSourceFile, assertOptionalSourceFilesEquivalent, "/src/index.ts"), parityCase("LanguageService", "getImportAdderEdits", languageService.getImportAdderEdits, assertDeepEquivalent, "/src/index.ts", [{ kind: "importSymbol", symbol: unimportedSymbol }]), parityCase("LanguageService", "getImportEditsForSymbols", languageService.getImportEditsForSymbols, assertDeepEquivalent, "/src/index.ts", [unimportedSymbol], { isValidTypeOnlyUseSite: true }), @@ -1689,11 +1669,9 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Program", "getResolvedTypeReferenceDirectiveFromTypeReferenceDirective", program.getResolvedTypeReferenceDirectiveFromTypeReferenceDirective, assertDeepEquivalent, typeReferenceDirective, "/src/index.ts"), parityCase("Program", "getSourceFileByPath", program.getSourceFileByPath, assertOptionalSourceFilesEquivalent, indexFile.path), parityCase("Program", "getSourceFileNames", program.getSourceFileNames, assertDeepEquivalent), - parityCase("Program", "getRootFileNames", program.getRootFileNames, assertDeepEquivalent), parityCase("Program", "getSourceFiles", program.getSourceFiles, (actual, expected, message) => { assertArrayElementsEquivalent(actual, expected, assertSourceFilesEquivalent, message); }), - parityCase("Program", "getProjectReferences", program.getProjectReferences, assertDeepEquivalent), parityCase("Program", "getResolvedProjectReferences", program.getResolvedProjectReferences, assertDeepEquivalent), parityCase("Program", "getSourceFileMetadata", program.getSourceFileMetadata, assertDeepEquivalent, "/src/index.ts"), parityCase("Program", "getSourceFileMetadataByPath", program.getSourceFileMetadataByPath, assertDeepEquivalent, indexFile.path), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index eee5aa35caf09..6cf2d71beae3b 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2695,39 +2695,16 @@ const value: string = 1;`, assert.equal(syntheticResolved![1], undefined); }); - test("Project membership distinguishes roots, dependencies, and open files", () => { - using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: ["src/main.ts", "missing.ts"] }), - "/src/main.ts": `import { value } from "./dependency"; export const result = value;`, - "/src/dependency.ts": `export const value = 1;`, - "/outside.ts": "export {};", - }); - const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"], openFiles: ["/src/main.ts", "/outside.ts"] }); - const project = snapshot.getConfiguredProject("/tsconfig.json")!; - assert.equal(project.hasRoots(), true); - assert.deepEqual(project.getRootFiles(), ["/src/main.ts"]); - assert.equal(project.isRoot("src/main.ts"), true); - assert.equal(project.isRoot({ uri: "file:///src/main.ts" }), true); - assert.equal(project.isRoot("/missing.ts"), false); - assert.equal(project.isRoot("/src/dependency.ts"), false); - assert.equal(project.containsFile("src/main.ts"), true); - assert.equal(project.containsFile({ uri: "file:///src/main.ts" }, true), true); - assert.equal(project.containsFile("/src/dependency.ts"), true); - assert.equal(project.containsFile("/src/dependency.ts", true), false); - assert.equal(project.containsFile("/outside.ts"), false); - assert.equal(project.containsFile("/outside.ts", true), false); - assert.equal(project.containsFile("/missing.ts"), false); - assert.strictEqual(project.getSourceFile("src/main.ts"), project.program.getSourceFile("/src/main.ts")); - assert.equal(project.getSourceFile("/outside.ts"), undefined); - assert.equal(project.hasConfigFile("tsconfig.json"), true); - assert.equal(project.hasConfigFile("/src/main.ts"), false); - - const updated = snapshot.update({ closeFiles: ["/src/main.ts"] }); - assert.equal(updated.getConfiguredProject("/tsconfig.json")!.containsFile("/src/main.ts", true), false); - assert.equal(project.containsFile("/src/main.ts", true), true); - }); - - test("Project membership includes supplemental files without physical files", { + test("Project does not expose membership and file-list methods", () => { + using api = spawnAPI(); + const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; + for (const method of ["containsFile", "getFileNames", "getRootFiles", "hasRoots", "isRoot", "hasConfigFile", "getSourceFile"]) { + assert.ok(!(method in project), `Project should not expose ${method}`); + } + }); + + test("getSourceFile includes supplemental files without physical files", { skip: process.env.TS_API_TEST_WASM ? "WASM cannot spawn external content mapper processes" : false, }, t => { const directory = mkdtempSync(join(tmpdir(), "ts-project-membership-")); @@ -2768,69 +2745,10 @@ const value: string = 1;`, writeFileSync(join(directory, "main.ts"), "const value: number = supplementalValue;"); using api = new API({ cwd: directory, runExternalCode: true }); const snapshot = api.createSnapshot({ openProjects: [configFileName] }); - const project = snapshot.getConfiguredProject(configFileName)!; + const program = snapshot.getConfiguredProject(configFileName)!.program; const supplementalFileName = toRootedFilePath(join(directory, "app.box.0.ts"), undefined); assert.equal(existsSync(supplementalFileName), false); - assert.ok(project.getSourceFile(supplementalFileName)); - assert.equal(project.containsFile(supplementalFileName), true); - assert.equal(project.containsFile(supplementalFileName, true), false); - assert.equal(project.containsFile(join(directory, "missing.ts")), false); - }); - - test("Project file lists include extended configs and optionally exclude external libraries", () => { - using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ extends: "./base.json", files: ["src/main.ts"] }), - "/base.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" } }), - "/src/main.ts": `import { value } from "my-lib"; export const result = value;`, - "/node_modules/my-lib/package.json": JSON.stringify({ types: "./index.d.ts" }), - "/node_modules/my-lib/index.d.ts": "export declare const value: number;", - }); - const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); - const project = snapshot.getConfiguredProject("/tsconfig.json")!; - assert.deepEqual(project.getFileNames(), ["/node_modules/my-lib/index.d.ts", "/src/main.ts", "/tsconfig.json", "/base.json"]); - assert.deepEqual(project.getFileNames(true), ["/src/main.ts", "/tsconfig.json", "/base.json"]); - assert.deepEqual(project.getFileNames(false, true), ["/node_modules/my-lib/index.d.ts", "/src/main.ts"]); - assert.deepEqual(project.getFileNames(true, true), ["/src/main.ts"]); - assert.equal(project.hasConfigFile("./base.json"), true); - assert.equal(project.hasConfigFile({ uri: "file:///base.json" }), true); - assert.equal(project.hasConfigFile("/other.json"), false); - assert.equal(project.containsFile("/tsconfig.json"), false); - }); - - test("Project roots preserve missing declarations and empty projects", () => { - using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: [] }), - }); - const snapshot = api.createSnapshot({ - openProjects: ["/tsconfig.json"], - createPrograms: [{ rootFiles: ["/missing.ts"], compilerOptions: { noLib: true } }], - }); - const empty = snapshot.getConfiguredProject("/tsconfig.json")!; - assert.equal(empty.hasRoots(), false); - assert.deepEqual(empty.getRootFiles(), []); - assert.deepEqual(empty.getFileNames(), ["/tsconfig.json"]); - const missing = snapshot.getProject(snapshot.operation.createdPrograms![0].id)!; - assert.equal(missing.hasRoots(), true); - assert.deepEqual(missing.getRootFiles(), []); - assert.equal(missing.isRoot("/missing.ts"), false); - assert.equal(missing.containsFile("/missing.ts"), false); - assert.deepEqual(missing.getFileNames(), []); - assert.equal(missing.hasConfigFile("/tsconfig.json"), false); - }); - - test("Project membership uses configured path case sensitivity", () => { - const files = { - "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true }, files: ["src/main.ts"] }), - "/src/main.ts": "export {};", - }; - for (const useCaseSensitiveFileNames of [true, false]) { - using api = new API({ cwd: "/", fs: createVirtualFileSystem(files), useCaseSensitiveFileNames }); - const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"], openFiles: ["/src/main.ts"] }); - const project = snapshot.getConfiguredProject("/tsconfig.json")!; - assert.equal(project.isRoot("/SRC/MAIN.TS"), !useCaseSensitiveFileNames); - assert.equal(project.hasConfigFile("/TSCONFIG.JSON"), !useCaseSensitiveFileNames); - assert.equal(project.containsFile("/SRC/MAIN.TS", true), !useCaseSensitiveFileNames); - } + assert.ok(program.getSourceFile(supplementalFileName)); }); test("getSourceFileNames returns all program files, not just root files", () => { diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 7f89d79cc22e5..26a1d6e2e448d 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -111,8 +111,6 @@ const ( MethodGetNonMissingTypeOfSymbol Method = "getNonMissingTypeOfSymbol" MethodGetSourceFile Method = "getSourceFile" MethodGetSourceFileNames Method = "getSourceFileNames" - MethodContainsProjectFile Method = "containsProjectFile" - MethodGetProjectRootFiles Method = "getProjectRootFiles" MethodGetProgramStatistics Method = "getProgramStatistics" MethodGetResolvedProjectReferences Method = "getResolvedProjectReferences" MethodGetOptionsDiagnostics Method = "getOptionsDiagnostics" @@ -633,8 +631,6 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetDefaultProjectForFile: unmarshallerFor[GetDefaultProjectForFileParams], MethodGetSourceFile: unmarshallerFor[GetSourceFileParams], MethodGetSourceFileNames: unmarshallerFor[GetSourceFileNamesParams], - MethodContainsProjectFile: unmarshallerFor[ContainsProjectFileParams], - MethodGetProjectRootFiles: unmarshallerFor[GetSourceFileNamesParams], MethodGetSourceFileMetadata: unmarshallerFor[GetSourceFileParams], MethodGetModeForUsageLocation: unmarshallerFor[GetModeForUsageLocationParams], MethodGetModeForResolutionAtIndex: unmarshallerFor[GetModeForResolutionAtIndexParams], @@ -1420,13 +1416,6 @@ type GetSourceFileNamesParams struct { Project project.Handle `json:"project"` } -type ContainsProjectFileParams struct { - Snapshot SnapshotID `json:"snapshot"` - Project project.Handle `json:"project"` - File DocumentIdentifier `json:"file"` - RequireOpen bool `json:"requireOpen,omitempty"` -} - type GetModeForUsageLocationParams struct { Snapshot SnapshotID `json:"snapshot"` Project project.Handle `json:"project"` diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 2eb419c3c76bd..63cdd6d7df792 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -951,10 +951,6 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetSourceFile(ctx, parsed.(*GetSourceFileParams)) case string(MethodGetSourceFileNames): return s.handleGetSourceFileNames(ctx, parsed.(*GetSourceFileNamesParams)) - case string(MethodContainsProjectFile): - return s.handleContainsProjectFile(ctx, parsed.(*ContainsProjectFileParams)) - case string(MethodGetProjectRootFiles): - return s.handleGetProjectRootFiles(ctx, parsed.(*GetSourceFileNamesParams)) case string(MethodGetProgramStatistics): return s.handleGetProgramStatistics(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodGetResolvedProjectReferences): @@ -2584,49 +2580,6 @@ func (s *Session) encodeSourceFileResponse(sourceFile *ast.SourceFile) (any, err }, nil } -func (s *Session) handleContainsProjectFile(ctx context.Context, params *ContainsProjectFileParams) (bool, error) { - sd, err := s.getSnapshotData(params.Snapshot) - if err != nil { - return false, err - } - proj, err := sd.getProject(params.Project) - if err != nil { - return false, err - } - fileName := params.File.ToFileName(proj.CurrentDirectory()) - path := sd.snapshot.CaseSensitivity().PathKey(fileName.AsPath()) - if params.RequireOpen && !sd.snapshot.ProjectCollection.IsFileOpen(path) { - return false, nil - } - if program := proj.GetProgram(); program != nil && program.GetSourceFileByPath(path) != nil { - return true, nil - } - for _, root := range proj.CommandLine.FileNames() { - if sd.snapshot.CaseSensitivity().PathKey(root.AsPath()) == path { - return sd.snapshot.GetFile(fileName) != nil, nil - } - } - return false, nil -} - -func (s *Session) handleGetProjectRootFiles(ctx context.Context, params *GetSourceFileNamesParams) ([]tspath.RootedFilePath, error) { - sd, err := s.getSnapshotData(params.Snapshot) - if err != nil { - return nil, err - } - proj, err := sd.getProject(params.Project) - if err != nil { - return nil, err - } - result := make([]tspath.RootedFilePath, 0, len(proj.CommandLine.FileNames())) - for _, root := range proj.CommandLine.FileNames() { - if sd.snapshot.GetFile(root) != nil { - result = append(result, root) - } - } - return result, nil -} - // handleGetSourceFileNames returns file names of all source files in a project. func (s *Session) handleGetSourceFileNames(ctx context.Context, params *GetSourceFileNamesParams) ([]tspath.RootedFilePath, error) { sd, err := s.getSnapshotData(params.Snapshot) diff --git a/tsc/internal/api/session_program_test.go b/tsc/internal/api/session_program_test.go index af0a6a5048c98..6cdd99530fcd4 100644 --- a/tsc/internal/api/session_program_test.go +++ b/tsc/internal/api/session_program_test.go @@ -12,7 +12,7 @@ import ( "gotest.tools/v3/assert" ) -func TestProjectContainsSupplementalFile(t *testing.T) { +func TestProgramSourceFileIncludesSupplementalFile(t *testing.T) { t.Parallel() const supplementalFileName = "/home/project/app.box.0.ts" @@ -34,7 +34,7 @@ func TestProjectContainsSupplementalFile(t *testing.T) { defer session.Close() response, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{}) assert.NilError(t, err) - params := &ContainsProjectFileParams{ + params := &GetSourceFileParams{ Snapshot: response.Snapshot, Project: response.Projects[0].Handle, File: DocumentIdentifier{FileName: supplementalFileName}, @@ -42,56 +42,9 @@ func TestProjectContainsSupplementalFile(t *testing.T) { sd, err := session.getSnapshotData(response.Snapshot) assert.NilError(t, err) assert.Assert(t, sd.snapshot.GetFile(supplementalFileName) == nil) - sourceFile, err := session.handleGetSourceFile(context.Background(), &GetSourceFileParams{ - Snapshot: params.Snapshot, Project: params.Project, File: params.File, - }) + sourceFile, err := session.handleGetSourceFile(context.Background(), params) assert.NilError(t, err) assert.Assert(t, sourceFile != nil) - contains, err := session.handleContainsProjectFile(context.Background(), params) - assert.NilError(t, err) - assert.Assert(t, contains) - params.RequireOpen = true - contains, err = session.handleContainsProjectFile(context.Background(), params) - assert.NilError(t, err) - assert.Assert(t, !contains) - params.RequireOpen = false - params.File = DocumentIdentifier{FileName: "/home/project/missing.ts"} - contains, err = session.handleContainsProjectFile(context.Background(), params) - assert.NilError(t, err) - assert.Assert(t, !contains) -} - -func TestProjectContainsEditorOpenFile(t *testing.T) { - t.Parallel() - - const fileName = "/home/projects/p/index.ts" - projectSession, _ := projecttestutil.Setup(map[string]any{ - "/home/projects/p/tsconfig.json": `{"compilerOptions":{"noLib":true},"files":["index.ts"]}`, - fileName: "export {};", - }) - defer projectSession.Close() - uri := DocumentIdentifier{FileName: fileName}.ToURI(projectSession.GetCurrentDirectory()) - projectSession.DidOpenFile(context.Background(), uri, 1, "export {};", lsproto.LanguageKindTypeScript) - session := NewLSPSession(projectSession, nil) - defer session.Close() - response, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{}) - assert.NilError(t, err) - params := &ContainsProjectFileParams{ - Snapshot: response.Snapshot, - Project: response.Projects[0].Handle, - File: DocumentIdentifier{FileName: fileName}, - RequireOpen: true, - } - contains, err := session.handleContainsProjectFile(context.Background(), params) - assert.NilError(t, err) - assert.Assert(t, contains) - params.Project = project.Handle(999999) - _, err = session.handleContainsProjectFile(context.Background(), params) - assert.ErrorContains(t, err, "not found") - _, err = session.handleGetProjectRootFiles(context.Background(), &GetSourceFileNamesParams{ - Snapshot: response.Snapshot, Project: params.Project, - }) - assert.ErrorContains(t, err, "not found") } func TestProgramStatisticsUseAPIChecker(t *testing.T) { From ab7dfdcce2a0bb94fcac970c65594acddf3f8fd9 Mon Sep 17 00:00:00 2001 From: isabel duan Date: Wed, 4 Nov 2026 19:32:29 -0800 Subject: [PATCH 09/13] remove `GetResolvedProjectReferences` to rethink its implementation --- packages/typescript/src/api/async/api.ts | 29 -------- .../typescript/src/api/proto.generated.ts | 15 ----- packages/typescript/src/api/sync/api.ts | 67 ------------------- packages/typescript/test/async/api.test.ts | 23 +------ .../test/sync/api-generators.test.ts | 23 ------- packages/typescript/test/sync/api.test.ts | 23 +------ tsc/internal/api/proto.go | 14 ---- tsc/internal/api/session.go | 62 ----------------- 8 files changed, 4 insertions(+), 252 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index dca0e250a1797..965d91ab4d641 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1738,12 +1738,6 @@ export class LanguageService { } } -export interface ResolvedProjectReference { - commandLine: ParsedCommandLine; - sourceFile: SourceFile; - references?: readonly (ResolvedProjectReference | undefined)[] | undefined; -} - export class Program implements FormatDiagnosticsHost { /** @internal */ readonly snapshotId: number; @@ -1754,7 +1748,6 @@ export class Program implements FormatDiagnost private readonly sourceFileMetadataCache = new Map>(); private ownedSnapshot: Snapshot | undefined; private disposePromise: Promise | undefined; - private resolvedProjectReferences: Promise | undefined; private pendingStatistics: Promise | undefined; // @sync-skip private get client(): Client { @@ -1875,28 +1868,6 @@ export class Program implements FormatDiagnost return (await this.getStatistics()).relationCacheSizes; } - getResolvedProjectReferences(): Promise { - // @generators: if (owner.resolvedProjectReferences !== undefined) return owner.resolvedProjectReferences; - // @generators: const references = yield* owner.fetchResolvedProjectReferences.gen(); - return this.resolvedProjectReferences ??= this.fetchResolvedProjectReferences(); // @generators: return owner.resolvedProjectReferences ??= references; - } - - private async fetchResolvedProjectReferences(): Promise { - const data = await this.client.apiRequest("getResolvedProjectReferences", { - snapshot: this.snapshotId, - project: this.project.handle, - }); - const projects: ResolvedProjectReference[] = data.projects.map(project => ({ - commandLine: project.commandLine, - sourceFile: new RemoteSourceFile(sourceFileResponseToUint8Array(project.sourceFile)!, this.decoder, undefined, this.project.api) as unknown as SourceFile, - })); - const resolve = (indices: readonly number[] | null | undefined): (ResolvedProjectReference | undefined)[] | undefined => indices?.map(index => index === 0 ? undefined : projects[index - 1]); - for (let i = 0; i < projects.length; i++) { - projects[i].references = resolve(data.projects[i].references); - } - return resolve(data.references); - } - async getSourceFile(file: DocumentIdentifier): Promise { const fileName = resolveFileName(file); const path = this.pathKeyForFileName(fileName); diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 667265ef0786c..42a82c01f6785 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -81,7 +81,6 @@ export interface APIMethodInfo { getSourceFile: APIMethod; getSourceFileNames: APIMethod; getProgramStatistics: APIMethod; - getResolvedProjectReferences: APIMethod; getOptionsDiagnostics: APIMethod; getSourceFileMetadata: APIMethod; getModeForUsageLocation: APIMethod; @@ -609,12 +608,6 @@ export interface ProgramStatisticsResponse { relationCacheSizes: RelationCacheSizes; } -/** References use one-based indices into Projects; zero denotes an unresolved reference. */ -export interface ResolvedProjectReferencesResponse { - references?: number[] | undefined; - projects: ResolvedProjectReferenceResponse[]; -} - /** DiagnosticResponse is the API response for a single diagnostic. */ export interface DiagnosticResponse { /** The file name of the file this diagnostic belongs to, if any. */ @@ -1265,7 +1258,6 @@ export interface BatchRequest { | "getRegularTypeOfType" | "getResolvedModule" | "getResolvedModuleFromModuleSpecifier" - | "getResolvedProjectReferences" | "getResolvedSignature" | "getResolvedTypeReferenceDirective" | "getResolvedTypeReferenceDirectiveFromTypeReferenceDirective" @@ -1450,7 +1442,6 @@ export interface BatchResponse { | "getRegularTypeOfType" | "getResolvedModule" | "getResolvedModuleFromModuleSpecifier" - | "getResolvedProjectReferences" | "getResolvedSignature" | "getResolvedTypeReferenceDirective" | "getResolvedTypeReferenceDirectiveFromTypeReferenceDirective" @@ -1700,12 +1691,6 @@ export interface RelationCacheSizes { strictSubtype: number; } -export interface ResolvedProjectReferenceResponse { - commandLine: ConfigFileResponse; - sourceFile: SourceFileResponse; - references?: number[] | undefined; -} - export interface DiagnosticPositionResponse { line: number; character: number; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index fdee53ee4d214..80eaae80ccbb7 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -2917,12 +2917,6 @@ export class LanguageService { } } -export interface ResolvedProjectReference { - commandLine: ParsedCommandLine; - sourceFile: SourceFile; - references?: readonly (ResolvedProjectReference | undefined)[] | undefined; -} - export class Program implements FormatDiagnosticsHost { /** @internal */ readonly snapshotId: number; @@ -2933,7 +2927,6 @@ export class Program implements FormatDiagnost private readonly sourceFileMetadataCache = new Map(); private ownedSnapshot: Snapshot | undefined; private disposePromise: void | undefined; - private resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined | undefined; private get client(): Client { return this.project.api.client; @@ -3200,66 +3193,6 @@ export class Program implements FormatDiagnost ); } - get getResolvedProjectReferences(): { - (): readonly (ResolvedProjectReference | undefined)[] | undefined; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getResolvedProjectReferences", - function (): readonly (ResolvedProjectReference | undefined)[] | undefined { - return owner.resolvedProjectReferences ??= owner.fetchResolvedProjectReferences(); - }, - function* (): Generator { - if (owner.resolvedProjectReferences !== undefined) return owner.resolvedProjectReferences; - const references = yield* owner.fetchResolvedProjectReferences.gen(); - return owner.resolvedProjectReferences ??= references; - }, - ); - } - - private get fetchResolvedProjectReferences(): { - (): readonly (ResolvedProjectReference | undefined)[] | undefined; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "fetchResolvedProjectReferences", - function (): readonly (ResolvedProjectReference | undefined)[] | undefined { - const data = owner.client.apiRequest("getResolvedProjectReferences", { - snapshot: owner.snapshotId, - project: owner.project.handle, - }); - const projects: ResolvedProjectReference[] = data.projects.map(project => ({ - commandLine: project.commandLine, - sourceFile: new RemoteSourceFile(sourceFileResponseToUint8Array(project.sourceFile)!, owner.decoder, undefined, owner.project.api) as unknown as SourceFile, - })); - const resolve = (indices: readonly number[] | null | undefined): (ResolvedProjectReference | undefined)[] | undefined => indices?.map(index => index === 0 ? undefined : projects[index - 1]); - for (let i = 0; i < projects.length; i++) { - projects[i].references = resolve(data.projects[i].references); - } - return resolve(data.references); - }, - function* (): Generator { - const data = yield* apiRequest("getResolvedProjectReferences", { - snapshot: owner.snapshotId, - project: owner.project.handle, - }); - const projects: ResolvedProjectReference[] = data.projects.map(project => ({ - commandLine: project.commandLine, - sourceFile: new RemoteSourceFile(sourceFileResponseToUint8Array(project.sourceFile)!, owner.decoder, undefined, owner.project.api) as unknown as SourceFile, - })); - const resolve = (indices: readonly number[] | null | undefined): (ResolvedProjectReference | undefined)[] | undefined => indices?.map(index => index === 0 ? undefined : projects[index - 1]); - for (let i = 0; i < projects.length; i++) { - projects[i].references = resolve(data.projects[i].references); - } - return resolve(data.references); - }, - ); - } - get getSourceFile(): { (file: DocumentIdentifier): SourceFile | undefined; gen(file: DocumentIdentifier): Generator; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 88000f688e9ca..d0585275a7e63 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -2793,12 +2793,12 @@ describe("SourceFile", { concurrency }, () => { }); await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); assert.ok(!("getTypeChecker" in program)); + assert.ok(!("getResolvedProjectReferences" in program)); assert.deepEqual(await program.getRootFileNames(), ["/src/main.ts"]); const files = await program.getSourceFiles(); assert.deepEqual(files.map(file => file.fileName), ["/src/dependency.ts", "/src/main.ts"]); assert.strictEqual(files[1], await program.getSourceFile("/src/main.ts")); assert.equal(await program.getProjectReferences(), undefined); - assert.equal(await program.getResolvedProjectReferences(), undefined); }); test("Program counters reflect work on its type checker without forcing diagnostics", async () => { @@ -2848,12 +2848,10 @@ export const other: { value: string } = box;`, assert.equal(await empty.getNodeCount(), 0); assert.equal(await empty.getIdentifierCount(), 0); assert.equal(await empty.getProjectReferences(), undefined); - assert.equal(await empty.getResolvedProjectReferences(), undefined); assert.deepEqual(await empty.getOptionsDiagnostics(), []); await using noReferences = await api.createProgram([], { noLib: true }, { projectReferences: [] }); assert.deepEqual(await noReferences.getProjectReferences(), []); - assert.deepEqual(await noReferences.getResolvedProjectReferences(), []); await using missing = await api.createProgram(["/missing.ts"], { noLib: true }); assert.deepEqual(await missing.getRootFileNames(), ["/missing.ts"]); @@ -2879,7 +2877,7 @@ const value: string = 1;`, assert.ok((await program.getConfigFileParsingDiagnostics()).some(diagnostic => diagnostic.code === 5023)); }); - test("Program resolved project references preserve missing, nested, shared, and circular references", async () => { + test("Program project references are available for configured and synthetic programs", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ files: [], references: [{ path: "./a" }, { path: "./missing" }, { path: "./b" }, { path: "./a" }] }), "/a/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, files: ["index.ts"], references: [{ path: "../b" }] }), @@ -2891,28 +2889,11 @@ const value: string = 1;`, const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const references = await program.getProjectReferences(); assert.equal(references?.length, 4); - const resolved = await program.getResolvedProjectReferences(); - assert.equal(resolved?.length, 4); - assert.equal(resolved![1], undefined); - const a = resolved![0]!; - const b = resolved![2]!; - assert.strictEqual(resolved![3], a); - assert.strictEqual(a.references![0], b); - assert.strictEqual(b.references![0], a); - assert.equal(b.references![1], undefined); - assert.equal(a.sourceFile.fileName, "/a/tsconfig.json"); - assert.equal(a.commandLine.options.composite, true); - assert.deepEqual(a.commandLine.fileNames, ["/a/index.ts"]); - assert.strictEqual((await program.getResolvedProjectReferences())![0], a); await using synthetic = await api.createProgram([], { noLib: true }, { projectReferences: [{ path: "/a/tsconfig.json" }, { path: "/missing/tsconfig.json" }], }); assert.equal((await synthetic.getProjectReferences())!.length, 2); - const syntheticResolved = await synthetic.getResolvedProjectReferences(); - assert.equal(syntheticResolved!.length, 2); - assert.equal(syntheticResolved![0]!.sourceFile.fileName, "/a/tsconfig.json"); - assert.equal(syntheticResolved![1], undefined); }); test("Project does not expose membership and file-list methods", async () => { diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 0f540d901282f..81a47f663c58d 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -174,7 +174,6 @@ const privateGeneratorGetters = new Set([ "NodeHandle.resolveInFile", "Program.disposeWorker", "Program.getStatistics", - "Program.fetchResolvedProjectReferences", "Program.fetchSourceFileMetadata", "Program.fetchStatistics", "Program.getSourceFileWorker", @@ -488,27 +487,6 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => ]); }); - test("concurrent resolved project reference generators reuse the first cached graph", () => { - using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ files: [], references: [{ path: "./a" }, { path: "./b" }, { path: "./a" }] }), - "/a/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, files: ["index.ts"], references: [{ path: "../b" }] }), - "/a/index.ts": "export const a = 1;", - "/b/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, files: ["index.ts"], references: [{ path: "../a", circular: true }] }), - "/b/index.ts": "export const b = 1;", - }); - using snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); - const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; - const [first, second] = api.batch(program.getResolvedProjectReferences.gen(), program.getResolvedProjectReferences.gen()); - assert.ok(first); - assert.equal(first.length, 3); - assert.ok(second === first, "Concurrent calls must return the same reference graph"); - assert.strictEqual(first[2], first[0]); - assert.strictEqual(first[0]!.references![0], first[1]); - assert.strictEqual(first[1]!.references![0], first[0]); - assert.strictEqual(api.batch(program.getResolvedProjectReferences.gen())[0], first); - assert.strictEqual(program.getResolvedProjectReferences(), first); - }); - test("batches source-file lookups while preserving order and cached results", context => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" }, files: ["src/main.ts"] }), @@ -1672,7 +1650,6 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Program", "getSourceFiles", program.getSourceFiles, (actual, expected, message) => { assertArrayElementsEquivalent(actual, expected, assertSourceFilesEquivalent, message); }), - parityCase("Program", "getResolvedProjectReferences", program.getResolvedProjectReferences, assertDeepEquivalent), parityCase("Program", "getSourceFileMetadata", program.getSourceFileMetadata, assertDeepEquivalent, "/src/index.ts"), parityCase("Program", "getSourceFileMetadataByPath", program.getSourceFileMetadataByPath, assertDeepEquivalent, indexFile.path), parityCase("Program", "isSourceFileFromExternalLibrary", program.isSourceFileFromExternalLibrary, assertDeepEquivalent, indexFile), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 6cf2d71beae3b..610fa7c5e118b 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2573,12 +2573,12 @@ describe("SourceFile", { concurrency }, () => { }); using program = api.createProgram(["/src/main.ts"], { noLib: true }); assert.ok(!("getTypeChecker" in program)); + assert.ok(!("getResolvedProjectReferences" in program)); assert.deepEqual(program.getRootFileNames(), ["/src/main.ts"]); const files = program.getSourceFiles(); assert.deepEqual(files.map(file => file.fileName), ["/src/dependency.ts", "/src/main.ts"]); assert.strictEqual(files[1], program.getSourceFile("/src/main.ts")); assert.equal(program.getProjectReferences(), undefined); - assert.equal(program.getResolvedProjectReferences(), undefined); }); test("Program counters reflect work on its type checker without forcing diagnostics", () => { @@ -2628,12 +2628,10 @@ export const other: { value: string } = box;`, assert.equal(empty.getNodeCount(), 0); assert.equal(empty.getIdentifierCount(), 0); assert.equal(empty.getProjectReferences(), undefined); - assert.equal(empty.getResolvedProjectReferences(), undefined); assert.deepEqual(empty.getOptionsDiagnostics(), []); using noReferences = api.createProgram([], { noLib: true }, { projectReferences: [] }); assert.deepEqual(noReferences.getProjectReferences(), []); - assert.deepEqual(noReferences.getResolvedProjectReferences(), []); using missing = api.createProgram(["/missing.ts"], { noLib: true }); assert.deepEqual(missing.getRootFileNames(), ["/missing.ts"]); @@ -2659,7 +2657,7 @@ const value: string = 1;`, assert.ok((program.getConfigFileParsingDiagnostics()).some(diagnostic => diagnostic.code === 5023)); }); - test("Program resolved project references preserve missing, nested, shared, and circular references", () => { + test("Program project references are available for configured and synthetic programs", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ files: [], references: [{ path: "./a" }, { path: "./missing" }, { path: "./b" }, { path: "./a" }] }), "/a/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, files: ["index.ts"], references: [{ path: "../b" }] }), @@ -2671,28 +2669,11 @@ const value: string = 1;`, const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const references = program.getProjectReferences(); assert.equal(references?.length, 4); - const resolved = program.getResolvedProjectReferences(); - assert.equal(resolved?.length, 4); - assert.equal(resolved![1], undefined); - const a = resolved![0]!; - const b = resolved![2]!; - assert.strictEqual(resolved![3], a); - assert.strictEqual(a.references![0], b); - assert.strictEqual(b.references![0], a); - assert.equal(b.references![1], undefined); - assert.equal(a.sourceFile.fileName, "/a/tsconfig.json"); - assert.equal(a.commandLine.options.composite, true); - assert.deepEqual(a.commandLine.fileNames, ["/a/index.ts"]); - assert.strictEqual((program.getResolvedProjectReferences())![0], a); using synthetic = api.createProgram([], { noLib: true }, { projectReferences: [{ path: "/a/tsconfig.json" }, { path: "/missing/tsconfig.json" }], }); assert.equal((synthetic.getProjectReferences())!.length, 2); - const syntheticResolved = synthetic.getResolvedProjectReferences(); - assert.equal(syntheticResolved!.length, 2); - assert.equal(syntheticResolved![0]!.sourceFile.fileName, "/a/tsconfig.json"); - assert.equal(syntheticResolved![1], undefined); }); test("Project does not expose membership and file-list methods", () => { diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 26a1d6e2e448d..9a01d5c1d0b18 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -112,7 +112,6 @@ const ( MethodGetSourceFile Method = "getSourceFile" MethodGetSourceFileNames Method = "getSourceFileNames" MethodGetProgramStatistics Method = "getProgramStatistics" - MethodGetResolvedProjectReferences Method = "getResolvedProjectReferences" MethodGetOptionsDiagnostics Method = "getOptionsDiagnostics" MethodGetSourceFileMetadata Method = "getSourceFileMetadata" MethodGetModeForUsageLocation Method = "getModeForUsageLocation" @@ -776,7 +775,6 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetDeclarationDiagnostics: unmarshallerFor[GetDiagnosticsParams], MethodGetProgramDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetProgramStatistics: unmarshallerFor[GetProjectDiagnosticsParams], - MethodGetResolvedProjectReferences: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetOptionsDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetGlobalDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetConfigFileParsingDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], @@ -1038,18 +1036,6 @@ type ProgramStatisticsResponse struct { RelationCacheSizes checker.RelationCacheSizes `json:"relationCacheSizes"` } -// References use one-based indices into Projects; zero denotes an unresolved reference. -type ResolvedProjectReferencesResponse struct { - References []int `json:"references,omitzero"` - Projects []*ResolvedProjectReferenceResponse `json:"projects" nonnil:"true"` -} - -type ResolvedProjectReferenceResponse struct { - CommandLine *ConfigFileResponse `json:"commandLine" nonnil:"true"` - SourceFile *SourceFileResponse `json:"sourceFile" nonnil:"true"` - References []int `json:"references,omitzero"` -} - type ReadConfigFileResponse struct { Config any `json:"config"` Error *DiagnosticResponse `json:"error,omitempty"` diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 63cdd6d7df792..818f014b363ee 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -953,8 +953,6 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetSourceFileNames(ctx, parsed.(*GetSourceFileNamesParams)) case string(MethodGetProgramStatistics): return s.handleGetProgramStatistics(ctx, parsed.(*GetProjectDiagnosticsParams)) - case string(MethodGetResolvedProjectReferences): - return s.handleGetResolvedProjectReferences(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodGetOptionsDiagnostics): return s.handleGetOptionsDiagnostics(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodGetSourceFileMetadata): @@ -2400,66 +2398,6 @@ func (s *Session) handleGetProgramStatistics(ctx context.Context, params *GetPro return result, nil } -func (s *Session) handleGetResolvedProjectReferences(ctx context.Context, params *GetProjectDiagnosticsParams) (*ResolvedProjectReferencesResponse, error) { - sd, err := s.getSnapshotData(params.Snapshot) - if err != nil { - return nil, err - } - program, err := sd.getProgram(params.Project) - if err != nil { - return nil, err - } - result := &ResolvedProjectReferencesResponse{Projects: []*ResolvedProjectReferenceResponse{}} - indices := map[*tsoptions.ParsedCommandLine]int{} - var visit func(config *tsoptions.ParsedCommandLine) (int, error) - visit = func(config *tsoptions.ParsedCommandLine) (int, error) { - if config == nil { - return 0, nil - } - if index, ok := indices[config]; ok { - return index, nil - } - if config.ConfigFile == nil || config.ConfigFile.SourceFile == nil { - return 0, errors.New("resolved project reference has no config source file") - } - file := config.ConfigFile.SourceFile - data, _, encodeErr := encoder.EncodeSourceFile(file) - if encodeErr != nil { - return 0, fmt.Errorf("failed to encode project reference config: %w", encodeErr) - } - encoder.SetSourceFileID(data, sourceFileNodeID(file)) - response := &ResolvedProjectReferenceResponse{ - CommandLine: NewConfigFileResponse(config), - SourceFile: &SourceFileResponse{Data: base64.StdEncoding.EncodeToString(data)}, - } - result.Projects = append(result.Projects, response) - index := len(result.Projects) - indices[config] = index - if config.ProjectReferences() != nil { - response.References = make([]int, len(config.ProjectReferences())) - for i, path := range config.ResolvedProjectReferencePaths() { - child, _ := program.GetResolvedProjectReferenceFor(program.PathKeyForFileName(path)) - response.References[i], err = visit(child) - if err != nil { - return 0, err - } - } - } - return index, nil - } - if program.CommandLine().ProjectReferences() != nil { - references := program.GetResolvedProjectReferences() - result.References = make([]int, len(references)) - for i, config := range references { - result.References[i], err = visit(config) - if err != nil { - return nil, err - } - } - } - return result, nil -} - // @gen-proto-nullable func (s *Session) handleGetOptionsDiagnostics(ctx context.Context, params *GetProjectDiagnosticsParams) ([]*DiagnosticResponse, error) { sd, err := s.getSnapshotData(params.Snapshot) From 406105ab307b784851b2c381e1952d7dfc44b440 Mon Sep 17 00:00:00 2001 From: isabel duan Date: Thu, 5 Nov 2026 14:04:59 -0800 Subject: [PATCH 10/13] remove getOptionsDiagnostics --- packages/typescript/src/api/async/api.ts | 8 -- .../typescript/src/api/proto.generated.ts | 87 +++++++++---------- packages/typescript/src/api/sync/api.ts | 25 ------ packages/typescript/test/async/api.test.ts | 22 +---- .../test/sync/api-generators.test.ts | 1 - packages/typescript/test/sync/api.test.ts | 22 +---- tsc/internal/api/proto.go | 2 - tsc/internal/api/session.go | 15 ---- 8 files changed, 46 insertions(+), 136 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 6c0756e7d805b..1493b7699e385 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -2167,14 +2167,6 @@ export class Program implements FormatDiagnost return data ?? []; } - async getOptionsDiagnostics(): Promise { - const data = await this.client.apiRequest("getOptionsDiagnostics", { - snapshot: this.snapshotId, - project: this.project.handle, - }); - return data ?? []; - } - /** * Get global (non-file-specific) semantic diagnostics for the project. */ diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 463a72fe8288f..1b1c34c4fdd74 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -81,7 +81,6 @@ export interface APIMethodInfo { getSourceFile: APIMethod; getSourceFileNames: APIMethod; getProgramStatistics: APIMethod; - getOptionsDiagnostics: APIMethod; getSourceFileMetadata: APIMethod; getModeForUsageLocation: APIMethod; getModeForResolutionAtIndex: APIMethod; @@ -619,38 +618,6 @@ export interface ProgramStatisticsResponse { relationCacheSizes: RelationCacheSizes; } -/** DiagnosticResponse is the API response for a single diagnostic. */ -export interface DiagnosticResponse { - /** The file name of the file this diagnostic belongs to, if any. */ - fileName?: RootedFilePath | undefined; - /** Pos is the start position of the diagnostic in the source file. */ - pos: number; - /** End is the end position of the diagnostic in the source file. */ - end: number; - /** StartPosition is the zero-based line and UTF-16 character position of Pos. */ - startPosition?: DiagnosticPositionResponse | undefined; - /** EndPosition is the zero-based line and UTF-16 character position of End. */ - endPosition?: DiagnosticPositionResponse | undefined; - /** SourceLines contains the source lines needed to render this diagnostic with context. */ - sourceLines?: DiagnosticSourceLineResponse[] | undefined; - /** Code is the diagnostic error code. */ - code: number; - /** Category is the diagnostic category (error, warning, suggestion, message). */ - category: number; - /** Source is a custom diagnostic-code prefix. An empty value uses the default "TS". */ - source?: string | undefined; - /** Text is the localized diagnostic message text. */ - text: string; - /** ReportsUnnecessary indicates this diagnostic highlights unnecessary code. */ - reportsUnnecessary?: boolean | undefined; - /** ReportsDeprecated indicates this diagnostic highlights deprecated code. */ - reportsDeprecated?: boolean | undefined; - /** MessageChain contains chained diagnostic messages, if any. */ - messageChain?: DiagnosticResponse[] | undefined; - /** RelatedInformation contains related diagnostic information, if any. */ - relatedInformation?: DiagnosticResponse[] | undefined; -} - /** SourceFileMetadata carries program-stored metadata about a single source file. */ export interface SourceFileMetadata { isDefaultLibrary: boolean; @@ -1116,6 +1083,38 @@ export interface GetDiagnosticsParams { files?: readonly DocumentIdentifier[] | undefined; } +/** DiagnosticResponse is the API response for a single diagnostic. */ +export interface DiagnosticResponse { + /** The file name of the file this diagnostic belongs to, if any. */ + fileName?: RootedFilePath | undefined; + /** Pos is the start position of the diagnostic in the source file. */ + pos: number; + /** End is the end position of the diagnostic in the source file. */ + end: number; + /** StartPosition is the zero-based line and UTF-16 character position of Pos. */ + startPosition?: DiagnosticPositionResponse | undefined; + /** EndPosition is the zero-based line and UTF-16 character position of End. */ + endPosition?: DiagnosticPositionResponse | undefined; + /** SourceLines contains the source lines needed to render this diagnostic with context. */ + sourceLines?: DiagnosticSourceLineResponse[] | undefined; + /** Code is the diagnostic error code. */ + code: number; + /** Category is the diagnostic category (error, warning, suggestion, message). */ + category: number; + /** Source is a custom diagnostic-code prefix. An empty value uses the default "TS". */ + source?: string | undefined; + /** Text is the localized diagnostic message text. */ + text: string; + /** ReportsUnnecessary indicates this diagnostic highlights unnecessary code. */ + reportsUnnecessary?: boolean | undefined; + /** ReportsDeprecated indicates this diagnostic highlights deprecated code. */ + reportsDeprecated?: boolean | undefined; + /** MessageChain contains chained diagnostic messages, if any. */ + messageChain?: DiagnosticResponse[] | undefined; + /** RelatedInformation contains related diagnostic information, if any. */ + relatedInformation?: DiagnosticResponse[] | undefined; +} + /** PrintNodeParams are the parameters for the printNode method. */ export interface PrintNodeParams { /** base64-encoded binary AST data */ @@ -1294,7 +1293,6 @@ export interface BatchRequest { | "getNumberLiteralType" | "getNumberType" | "getObjectTypeOfType" - | "getOptionsDiagnostics" | "getOuterTypeParametersOfType" | "getParameterType" | "getParametersOfSignature" @@ -1489,7 +1487,6 @@ export interface BatchResponse { | "getNumberLiteralType" | "getNumberType" | "getObjectTypeOfType" - | "getOptionsDiagnostics" | "getOuterTypeParametersOfType" | "getParameterType" | "getParametersOfSignature" @@ -1759,16 +1756,6 @@ export interface RelationCacheSizes { strictSubtype: number; } -export interface DiagnosticPositionResponse { - line: number; - character: number; -} - -export interface DiagnosticSourceLineResponse { - line: number; - text: string; -} - export interface PackageId { name: string; subModuleName: string; @@ -1794,6 +1781,16 @@ export interface CompletionEntryResponse { symbol?: SymbolResponse | undefined; } +export interface DiagnosticPositionResponse { + line: number; + character: number; +} + +export interface DiagnosticSourceLineResponse { + line: number; + text: string; +} + export interface EmitOutputFile { fileName: RootedFilePath; text: string; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 5be50afe979df..467ad3354d4d5 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -3915,31 +3915,6 @@ export class Program implements FormatDiagnost ); } - get getOptionsDiagnostics(): { - (): readonly Diagnostic[]; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getOptionsDiagnostics", - function (): readonly Diagnostic[] { - const data = owner.client.apiRequest("getOptionsDiagnostics", { - snapshot: owner.snapshotId, - project: owner.project.handle, - }); - return data ?? []; - }, - function* (): Generator { - const data = yield* apiRequest("getOptionsDiagnostics", { - snapshot: owner.snapshotId, - project: owner.project.handle, - }); - return data ?? []; - }, - ); - } - /** * Get global (non-file-specific) semantic diagnostics for the project. */ diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index b7d395fab3c4d..8e2a004d72297 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -2852,7 +2852,7 @@ export const other: { value: string } = box;`, assert.equal(await empty.getNodeCount(), 0); assert.equal(await empty.getIdentifierCount(), 0); assert.equal(await empty.getProjectReferences(), undefined); - assert.deepEqual(await empty.getOptionsDiagnostics(), []); + assert.deepEqual(await empty.getProgramDiagnostics(), []); await using noReferences = await api.createProgram([], { noLib: true }, { projectReferences: [] }); assert.deepEqual(await noReferences.getProjectReferences(), []); @@ -2860,25 +2860,7 @@ export const other: { value: string } = box;`, await using missing = await api.createProgram(["/missing.ts"], { noLib: true }); assert.deepEqual(await missing.getRootFileNames(), ["/missing.ts"]); assert.deepEqual(await missing.getSourceFiles(), []); - assert.ok((await missing.getOptionsDiagnostics()).some(diagnostic => diagnostic.code === 6053)); - }); - - test("Program options diagnostics exclude source-file and config parsing diagnostics", async () => { - await using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ - compilerOptions: { noLib: true, strictNullChecks: false, exactOptionalPropertyTypes: true, unknownOption: true }, - files: ["src/main.ts"], - }), - "/src/main.ts": `/// -const value: string = 1;`, - }); - const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); - const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; - const diagnostics = await program.getOptionsDiagnostics(); - assert.ok(diagnostics.some(diagnostic => diagnostic.code === 5052)); - assert.ok(diagnostics.every(diagnostic => !diagnostic.fileName || diagnostic.fileName === "/tsconfig.json")); - assert.ok(!(diagnostics.some(diagnostic => diagnostic.code === 5023 || diagnostic.code === 6053 || diagnostic.code === 2322))); - assert.ok((await program.getConfigFileParsingDiagnostics()).some(diagnostic => diagnostic.code === 5023)); + assert.ok((await missing.getProgramDiagnostics()).some(diagnostic => diagnostic.code === 6053)); }); test("Program project references are available for configured and synthetic programs", async () => { diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 71a6f9c905b7a..92c036e39f871 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -1666,7 +1666,6 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Program", "getSuggestionDiagnostics", program.getSuggestionDiagnostics, assertDeepEquivalent, "/src/suggestions.ts"), parityCase("Program", "getDeclarationDiagnostics", program.getDeclarationDiagnostics, assertDeepEquivalent, "/src/index.ts"), parityCase("Program", "getProgramDiagnostics", program.getProgramDiagnostics, assertDeepEquivalent), - parityCase("Program", "getOptionsDiagnostics", program.getOptionsDiagnostics, assertDeepEquivalent), parityCase("Program", "getGlobalDiagnostics", program.getGlobalDiagnostics, assertDeepEquivalent), parityCase("Program", "getConfigFileParsingDiagnostics", program.getConfigFileParsingDiagnostics, assertDeepEquivalent), parityCase("Program", "emit", program.emit, assertDeepEquivalent), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index db82b59e46777..74718bea37975 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2632,7 +2632,7 @@ export const other: { value: string } = box;`, assert.equal(empty.getNodeCount(), 0); assert.equal(empty.getIdentifierCount(), 0); assert.equal(empty.getProjectReferences(), undefined); - assert.deepEqual(empty.getOptionsDiagnostics(), []); + assert.deepEqual(empty.getProgramDiagnostics(), []); using noReferences = api.createProgram([], { noLib: true }, { projectReferences: [] }); assert.deepEqual(noReferences.getProjectReferences(), []); @@ -2640,25 +2640,7 @@ export const other: { value: string } = box;`, using missing = api.createProgram(["/missing.ts"], { noLib: true }); assert.deepEqual(missing.getRootFileNames(), ["/missing.ts"]); assert.deepEqual(missing.getSourceFiles(), []); - assert.ok((missing.getOptionsDiagnostics()).some(diagnostic => diagnostic.code === 6053)); - }); - - test("Program options diagnostics exclude source-file and config parsing diagnostics", () => { - using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ - compilerOptions: { noLib: true, strictNullChecks: false, exactOptionalPropertyTypes: true, unknownOption: true }, - files: ["src/main.ts"], - }), - "/src/main.ts": `/// -const value: string = 1;`, - }); - const snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); - const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; - const diagnostics = program.getOptionsDiagnostics(); - assert.ok(diagnostics.some(diagnostic => diagnostic.code === 5052)); - assert.ok(diagnostics.every(diagnostic => !diagnostic.fileName || diagnostic.fileName === "/tsconfig.json")); - assert.ok(!(diagnostics.some(diagnostic => diagnostic.code === 5023 || diagnostic.code === 6053 || diagnostic.code === 2322))); - assert.ok((program.getConfigFileParsingDiagnostics()).some(diagnostic => diagnostic.code === 5023)); + assert.ok((missing.getProgramDiagnostics()).some(diagnostic => diagnostic.code === 6053)); }); test("Program project references are available for configured and synthetic programs", () => { diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 602bc0c6de97d..23b381abc4a8b 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -112,7 +112,6 @@ const ( MethodGetSourceFile Method = "getSourceFile" MethodGetSourceFileNames Method = "getSourceFileNames" MethodGetProgramStatistics Method = "getProgramStatistics" - MethodGetOptionsDiagnostics Method = "getOptionsDiagnostics" MethodGetSourceFileMetadata Method = "getSourceFileMetadata" MethodGetModeForUsageLocation Method = "getModeForUsageLocation" MethodGetModeForResolutionAtIndex Method = "getModeForResolutionAtIndex" @@ -797,7 +796,6 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetDeclarationDiagnostics: unmarshallerFor[GetDiagnosticsParams], MethodGetProgramDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetProgramStatistics: unmarshallerFor[GetProjectDiagnosticsParams], - MethodGetOptionsDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetGlobalDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], MethodGetConfigFileParsingDiagnostics: unmarshallerFor[GetProjectDiagnosticsParams], MethodStartCPUProfile: unmarshallerFor[ProfileParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index e7d9d49670b51..cfc8b51a112a2 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -953,8 +953,6 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetSourceFileNames(ctx, parsed.(*GetSourceFileNamesParams)) case string(MethodGetProgramStatistics): return s.handleGetProgramStatistics(ctx, parsed.(*GetProjectDiagnosticsParams)) - case string(MethodGetOptionsDiagnostics): - return s.handleGetOptionsDiagnostics(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodGetSourceFileMetadata): return s.handleGetSourceFileMetadata(ctx, parsed.(*GetSourceFileParams)) case string(MethodGetModeForUsageLocation): @@ -2420,19 +2418,6 @@ func (s *Session) handleGetProgramStatistics(ctx context.Context, params *GetPro return result, nil } -// @gen-proto-nullable -func (s *Session) handleGetOptionsDiagnostics(ctx context.Context, params *GetProjectDiagnosticsParams) ([]*DiagnosticResponse, error) { - sd, err := s.getSnapshotData(params.Snapshot) - if err != nil { - return nil, err - } - program, err := sd.getProgram(params.Project) - if err != nil { - return nil, err - } - return NewDiagnosticResponses(program.GetOptionsDiagnostics()), nil -} - // handleGetSourceFile returns a source file from a project within a snapshot. // @gen-proto-result: SourceFileResponse // @gen-proto-nullable From d0f52ea8482314cebe45ff43ba3ad3170871a757 Mon Sep 17 00:00:00 2001 From: isabel duan Date: Thu, 5 Nov 2026 14:35:12 -0800 Subject: [PATCH 11/13] Remove extra functions --- packages/typescript/src/api/async/api.ts | 14 ------- packages/typescript/src/api/sync/api.ts | 37 ------------------- packages/typescript/test/async/api.test.ts | 36 ++---------------- .../test/sync/api-generators.test.ts | 32 ---------------- packages/typescript/test/sync/api.test.ts | 9 ++--- tsc/internal/project/projectcollection.go | 8 ---- 6 files changed, 8 insertions(+), 128 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 1493b7699e385..fa971f31400f4 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1808,20 +1808,6 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.projectReferences; } - async getSourceFiles(): Promise { - const fileNames = await this.getSourceFileNames(); - const sourceFiles = await Promise.all(fileNames.map(fileName => this.getSourceFile(fileName))); - const files: SourceFile[] = []; - for (let i = 0; i < fileNames.length; i++) { - const file = sourceFiles[i]; - if (!file) { - throw new Error(`Program source file '${fileNames[i]}' could not be loaded`); - } - files.push(file); - } - return files; - } - private async getStatistics(): Promise { // @sync-only-start // return this.fetchStatistics(); diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 467ad3354d4d5..81c3de6d362ec 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -3014,43 +3014,6 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.projectReferences; } - get getSourceFiles(): { - (): readonly SourceFile[]; - gen(): APIRequestGenerator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getSourceFiles", - function (): readonly SourceFile[] { - const fileNames = owner.getSourceFileNames(); - const sourceFiles = fileNames.map(fileName => owner.getSourceFile(fileName)); - const files: SourceFile[] = []; - for (let i = 0; i < fileNames.length; i++) { - const file = sourceFiles[i]; - if (!file) { - throw new Error(`Program source file '${fileNames[i]}' could not be loaded`); - } - files.push(file); - } - return files; - }, - function* (): APIRequestGenerator { - const fileNames = yield* owner.getSourceFileNames.gen(); - const sourceFiles = yield* all(...fileNames.map(fileName => owner.getSourceFile.gen(fileName))); - const files: SourceFile[] = []; - for (let i = 0; i < fileNames.length; i++) { - const file = sourceFiles[i]; - if (!file) { - throw new Error(`Program source file '${fileNames[i]}' could not be loaded`); - } - files.push(file); - } - return files; - }, - ); - } - private get getStatistics(): { (): ProgramStatisticsResponse; gen(): Generator; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 8e2a004d72297..78b75a5076cb5 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1622,33 +1622,6 @@ describe("API - automatic batching", { concurrency }, () => { assert.deepStrictEqual(readConfigFile.config, {}); }); - test("batches source-file lookups while preserving order and cached results", async () => { - await using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" }, files: ["src/main.ts"] }), - "/src/main.ts": `import { value } from "my-lib"; import { local } from "./local"; export const result = value + local;`, - "/src/local.ts": "export const local = 1;", - "/node_modules/my-lib/package.json": JSON.stringify({ types: "./index.d.ts" }), - "/node_modules/my-lib/index.d.ts": "export declare const value: number;", - }, { collectTiming: true }); - const snapshot = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); - const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; - - const local = await program.getSourceFile("/src/local.ts"); - assert.ok(local); - await api.resetTimingInfo(); - const files = await program.getSourceFiles(); - assert.deepEqual(files.map(file => file.fileName), ["/node_modules/my-lib/index.d.ts", "/src/local.ts", "/src/main.ts"]); - assert.strictEqual(files[1], local); - // The WASM transport sends the two uncached binary source-file requests individually. - assert.equal((await api.getTimingInfo()).totals.requestCount, process.env.TS_API_TEST_WASM ? 3 : 2); - - await api.resetTimingInfo(); - const cached = await program.getSourceFiles(); - for (let i = 0; i < files.length; i++) { - assert.strictEqual(cached[i], files[i]); - } - assert.equal((await api.getTimingInfo()).totals.requestCount, 1); - }); }); describe("API - batchContext", { concurrency }, () => { @@ -2798,10 +2771,9 @@ describe("SourceFile", { concurrency }, () => { await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); assert.ok(!("getTypeChecker" in program)); assert.ok(!("getResolvedProjectReferences" in program)); + assert.ok(!("getSourceFiles" in program)); assert.deepEqual(await program.getRootFileNames(), ["/src/main.ts"]); - const files = await program.getSourceFiles(); - assert.deepEqual(files.map(file => file.fileName), ["/src/dependency.ts", "/src/main.ts"]); - assert.strictEqual(files[1], await program.getSourceFile("/src/main.ts")); + assert.deepEqual(await program.getSourceFileNames(), ["/src/dependency.ts", "/src/main.ts"]); assert.equal(await program.getProjectReferences(), undefined); }); @@ -2848,7 +2820,7 @@ export const other: { value: string } = box;`, await using api = spawnAPI({}); await using empty = await api.createProgram([], { noLib: true }); assert.deepEqual(await empty.getRootFileNames(), []); - assert.deepEqual(await empty.getSourceFiles(), []); + assert.deepEqual(await empty.getSourceFileNames(), []); assert.equal(await empty.getNodeCount(), 0); assert.equal(await empty.getIdentifierCount(), 0); assert.equal(await empty.getProjectReferences(), undefined); @@ -2859,7 +2831,7 @@ export const other: { value: string } = box;`, await using missing = await api.createProgram(["/missing.ts"], { noLib: true }); assert.deepEqual(await missing.getRootFileNames(), ["/missing.ts"]); - assert.deepEqual(await missing.getSourceFiles(), []); + assert.deepEqual(await missing.getSourceFileNames(), []); assert.ok((await missing.getProgramDiagnostics()).some(diagnostic => diagnostic.code === 6053)); }); diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 92c036e39f871..9cb7b59a17e55 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -487,35 +487,6 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => ]); }); - test("batches source-file lookups while preserving order and cached results", context => { - using api = spawnAPI({ - "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node10" }, files: ["src/main.ts"] }), - "/src/main.ts": `import { value } from "my-lib"; import { local } from "./local"; export const result = value + local;`, - "/src/local.ts": "export const local = 1;", - "/node_modules/my-lib/package.json": JSON.stringify({ types: "./index.d.ts" }), - "/node_modules/my-lib/index.d.ts": "export declare const value: number;", - }); - using snapshot = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); - const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; - const requestBatches: string[][] = []; - observeRequestBatches(api, requestBatches, context); - - const local = program.getSourceFile("/src/local.ts"); - assert.ok(local); - requestBatches.length = 0; - const [files] = api.batch(program.getSourceFiles.gen()); - assert.deepEqual(files.map(file => file.fileName), ["/node_modules/my-lib/index.d.ts", "/src/local.ts", "/src/main.ts"]); - assert.strictEqual(files[1], local); - assert.deepEqual(requestBatches, [["getSourceFileNames"], ["getSourceFile", "getSourceFile"]]); - - requestBatches.length = 0; - const [cached] = api.batch(program.getSourceFiles.gen()); - for (let i = 0; i < files.length; i++) { - assert.strictEqual(cached[i], files[i]); - } - assert.deepEqual(requestBatches, [["getSourceFileNames"]]); - }); - test("all and defer yield discriminated host messages without starting children", () => { let started = false; function* child() { @@ -1647,9 +1618,6 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Program", "getResolvedTypeReferenceDirectiveFromTypeReferenceDirective", program.getResolvedTypeReferenceDirectiveFromTypeReferenceDirective, assertDeepEquivalent, typeReferenceDirective, "/src/index.ts"), parityCase("Program", "getSourceFileByPath", program.getSourceFileByPath, assertOptionalSourceFilesEquivalent, indexFile.path), parityCase("Program", "getSourceFileNames", program.getSourceFileNames, assertDeepEquivalent), - parityCase("Program", "getSourceFiles", program.getSourceFiles, (actual, expected, message) => { - assertArrayElementsEquivalent(actual, expected, assertSourceFilesEquivalent, message); - }), parityCase("Program", "getSourceFileMetadata", program.getSourceFileMetadata, assertDeepEquivalent, "/src/index.ts"), parityCase("Program", "getSourceFileMetadataByPath", program.getSourceFileMetadataByPath, assertDeepEquivalent, indexFile.path), parityCase("Program", "isSourceFileFromExternalLibrary", program.isSourceFileFromExternalLibrary, assertDeepEquivalent, indexFile), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 74718bea37975..d8636b1de3bed 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2578,10 +2578,9 @@ describe("SourceFile", { concurrency }, () => { using program = api.createProgram(["/src/main.ts"], { noLib: true }); assert.ok(!("getTypeChecker" in program)); assert.ok(!("getResolvedProjectReferences" in program)); + assert.ok(!("getSourceFiles" in program)); assert.deepEqual(program.getRootFileNames(), ["/src/main.ts"]); - const files = program.getSourceFiles(); - assert.deepEqual(files.map(file => file.fileName), ["/src/dependency.ts", "/src/main.ts"]); - assert.strictEqual(files[1], program.getSourceFile("/src/main.ts")); + assert.deepEqual(program.getSourceFileNames(), ["/src/dependency.ts", "/src/main.ts"]); assert.equal(program.getProjectReferences(), undefined); }); @@ -2628,7 +2627,7 @@ export const other: { value: string } = box;`, using api = spawnAPI({}); using empty = api.createProgram([], { noLib: true }); assert.deepEqual(empty.getRootFileNames(), []); - assert.deepEqual(empty.getSourceFiles(), []); + assert.deepEqual(empty.getSourceFileNames(), []); assert.equal(empty.getNodeCount(), 0); assert.equal(empty.getIdentifierCount(), 0); assert.equal(empty.getProjectReferences(), undefined); @@ -2639,7 +2638,7 @@ export const other: { value: string } = box;`, using missing = api.createProgram(["/missing.ts"], { noLib: true }); assert.deepEqual(missing.getRootFileNames(), ["/missing.ts"]); - assert.deepEqual(missing.getSourceFiles(), []); + assert.deepEqual(missing.getSourceFileNames(), []); assert.ok((missing.getProgramDiagnostics()).some(diagnostic => diagnostic.code === 6053)); }); diff --git a/tsc/internal/project/projectcollection.go b/tsc/internal/project/projectcollection.go index 222701557ece7..609985e2c8bd5 100644 --- a/tsc/internal/project/projectcollection.go +++ b/tsc/internal/project/projectcollection.go @@ -224,14 +224,6 @@ func (c *ProjectCollection) GetOpenConfiguredProjects() *collections.Set[Configu return c.openConfiguredProjects } -func (c *ProjectCollection) IsFileOpen(path tspath.PathKey) bool { - if c.openFiles.Has(path) { - return true - } - _, ok := c.apiState.openFiles[path] - return ok -} - func openFilePaths(overlays map[tspath.PathKey]*Overlay) collections.Set[tspath.PathKey] { openFiles := collections.Set[tspath.PathKey]{M: make(map[tspath.PathKey]struct{}, len(overlays))} for path := range overlays { From b3646abb3fb5edb602afd55b6272352050b1d0fe Mon Sep 17 00:00:00 2001 From: isabel duan Date: Thu, 5 Nov 2026 16:17:58 -0800 Subject: [PATCH 12/13] cache statistics --- packages/typescript/src/api/async/api.ts | 21 +++------------ packages/typescript/src/api/sync/api.ts | 22 +++------------ packages/typescript/test/async/api.test.ts | 25 ++++++++--------- .../test/sync/api-generators.test.ts | 27 ++++++++++++++++++- packages/typescript/test/sync/api.test.ts | 13 +++++---- 5 files changed, 53 insertions(+), 55 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index fa971f31400f4..59997cb9606e7 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1748,7 +1748,7 @@ export class Program implements FormatDiagnost private readonly sourceFileMetadataCache = new Map>(); private ownedSnapshot: Snapshot | undefined; private disposePromise: Promise | undefined; - private pendingStatistics: Promise | undefined; // @sync-skip + private statistics: Promise | undefined; private get client(): Client { return this.project.api.client; @@ -1808,23 +1808,8 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.projectReferences; } - private async getStatistics(): Promise { - // @sync-only-start - // return this.fetchStatistics(); - // @sync-only-end - // @sync-skip-block-start - if (this.pendingStatistics) return this.pendingStatistics; - try { - return await (this.pendingStatistics = this.fetchStatistics()); - } - finally { - this.pendingStatistics = undefined; - } - // @sync-skip-block-end - } - - private async fetchStatistics(): Promise { - return this.client.apiRequest("getProgramStatistics", { + private getStatistics(): Promise { + return this.statistics ??= this.client.apiRequest("getProgramStatistics", { snapshot: this.snapshotId, project: this.project.handle, }); diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 81c3de6d362ec..c3c9352ec685e 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -2927,6 +2927,7 @@ export class Program implements FormatDiagnost private readonly sourceFileMetadataCache = new Map(); private ownedSnapshot: Snapshot | undefined; private disposePromise: void | undefined; + private statistics: ProgramStatisticsResponse | undefined; private get client(): Client { return this.project.api.client; @@ -3023,30 +3024,13 @@ export class Program implements FormatDiagnost owner, "getStatistics", function (): ProgramStatisticsResponse { - return owner.fetchStatistics(); - }, - function* (): Generator { - return yield* owner.fetchStatistics.gen(); - }, - ); - } - - private get fetchStatistics(): { - (): ProgramStatisticsResponse; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "fetchStatistics", - function (): ProgramStatisticsResponse { - return owner.client.apiRequest("getProgramStatistics", { + return owner.statistics ??= owner.client.apiRequest("getProgramStatistics", { snapshot: owner.snapshotId, project: owner.project.handle, }); }, function* (): Generator { - return yield* apiRequest("getProgramStatistics", { + return owner.statistics ??= yield* apiRequest("getProgramStatistics", { snapshot: owner.snapshotId, project: owner.project.handle, }); diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 78b75a5076cb5..86d471e2a2f64 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1531,7 +1531,7 @@ declare module "augmentation" {}`, // @sync-skip-block-start describe("API - automatic batching", { concurrency }, () => { - test("shares pending Program statistics requests without caching settled results", async context => { + test("shares pending and settled Program statistics requests", async context => { await using api = spawnAPI({ "/src/main.ts": "export const value = 1;" }); await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); await using empty = await api.createProgram([], { noLib: true }); @@ -1559,10 +1559,10 @@ describe("API - automatic batching", { concurrency }, () => { await program.getTypeCount(); await program.getSymbolCount(); - assert.equal(request.mock.callCount(), 4); + assert.equal(request.mock.callCount(), 2); }); - test("shares rejected Program statistics requests and retries after failure", async context => { + test("caches rejected Program statistics requests", async context => { await using api = spawnAPI({ "/src/main.ts": "export const value = 1;" }); await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); // @ts-ignore -- Internal client requests are exercised by source tests. @@ -1577,8 +1577,8 @@ describe("API - automatic batching", { concurrency }, () => { { status: "rejected", reason: error }, ]); assert.equal(request.mock.callCount(), 1); - assert.ok(await program.getNodeCount() > 0); - assert.equal(request.mock.callCount(), 2); + await assert.rejects(program.getNodeCount(), error); + assert.equal(request.mock.callCount(), 1); }); test("initializes only once for concurrent first requests", async () => { @@ -1621,7 +1621,6 @@ describe("API - automatic batching", { concurrency }, () => { assert.equal(parseCommandLine.options.strict, true); assert.deepStrictEqual(readConfigFile.config, {}); }); - }); describe("API - batchContext", { concurrency }, () => { @@ -2777,23 +2776,25 @@ describe("SourceFile", { concurrency }, () => { assert.equal(await program.getProjectReferences(), undefined); }); - test("Program counters reflect work on its type checker without forcing diagnostics", async () => { + test("Program counters cache the first statistics without forcing diagnostics", async () => { await using api = spawnAPI({ "/src/main.ts": `type Box = { value: T }; export const box: Box = { value: "hello" }; export const other: { value: string } = box;`, - }); + }, { collectTiming: true }); const snapshot = await api.createSnapshot({ createPrograms: [{ rootFiles: ["/src/main.ts"], compilerOptions: { noLib: true } }], }); const project = snapshot.getProjects()[0]; const program = project.program; + await api.resetTimingInfo(); const identifiers = await program.getIdentifierCount(); const nodes = await program.getNodeCount(); const symbols = await program.getSymbolCount(); const types = await program.getTypeCount(); const instantiations = await program.getInstantiationCount(); const relations = await program.getRelationCacheSizes(); + assert.equal((await api.getTimingInfo()).totals.requestCount, 1); assert.ok(identifiers > 0); assert.ok(nodes > identifiers); assert.ok(symbols > 0); @@ -2806,12 +2807,12 @@ export const other: { value: string } = box;`, const declaration = cast(file.statements[1], isVariableStatement).declarationList.declarations[0]; const type = await checker.getTypeAtLocation(declaration.name); await type.getProperties(); - assert.ok(await program.getInstantiationCount() > instantiations); - assert.ok(await program.getTypeCount() > types); - assert.ok(await program.getSymbolCount() >= symbols); + assert.equal(await program.getInstantiationCount(), instantiations); + assert.equal(await program.getTypeCount(), types); + assert.equal(await program.getSymbolCount(), symbols); const other = cast(file.statements[2], isVariableStatement).declarationList.declarations[0]; await checker.isTypeAssignableTo(type, await checker.getTypeAtLocation(other.name)); - assert.ok((await program.getRelationCacheSizes()).assignable > relations.assignable); + assert.deepEqual(await program.getRelationCacheSizes(), relations); assert.equal(await program.getIdentifierCount(), identifiers); assert.equal(await program.getNodeCount(), nodes); }); diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 9cb7b59a17e55..0658469387b9e 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -175,7 +175,6 @@ const privateGeneratorGetters = new Set([ "Program.disposeWorker", "Program.getStatistics", "Program.fetchSourceFileMetadata", - "Program.fetchStatistics", "Program.getSourceFileWorker", "Snapshot.disposeWorker", "Symbol.fetchSymbol", @@ -448,6 +447,32 @@ function assertPublicGeneratorCoverage(owners: readonly { readonly name: string; } describe("API - generator batching", { concurrency: areTestsFiltered() }, () => { + test("Program statistics share a cache between sync and generator calls", () => { + using api = spawnAPI({ "/src/main.ts": "export const value = 1;" }, { collectTiming: true }); + for (const generatorFirst of [false, true]) { + using program = api.createProgram(["/src/main.ts"], { noLib: true }); + api.resetTimingInfo(); + const nodes = generatorFirst ? api.batch(program.getNodeCount.gen())[0] : program.getNodeCount(); + assert.ok(nodes > 0); + const [cachedNodes, types, symbols, instantiations, identifiers, relations] = api.batch( + program.getNodeCount.gen(), + program.getTypeCount.gen(), + program.getSymbolCount.gen(), + program.getInstantiationCount.gen(), + program.getIdentifierCount.gen(), + program.getRelationCacheSizes.gen(), + ); + assert.equal(cachedNodes, nodes); + assert.equal(program.getNodeCount(), nodes); + assert.equal(program.getTypeCount(), types); + assert.equal(program.getSymbolCount(), symbols); + assert.equal(program.getInstantiationCount(), instantiations); + assert.equal(program.getIdentifierCount(), identifiers); + assert.strictEqual(program.getRelationCacheSizes(), relations); + assert.equal(api.getTimingInfo().totals.requestCount, 1); + } + }); + test("looks up binder symbols through standalone and API generators", () => { using api = spawnAPI(); using lease = api.createSourceFile("/symbols.ts", "function present() {}"); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index d8636b1de3bed..f591c46501251 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2584,7 +2584,7 @@ describe("SourceFile", { concurrency }, () => { assert.equal(program.getProjectReferences(), undefined); }); - test("Program counters reflect work on its type checker without forcing diagnostics", () => { + test("Program counters cache the first statistics without forcing diagnostics", context => { using api = spawnAPI({ "/src/main.ts": `type Box = { value: T }; export const box: Box = { value: "hello" }; @@ -2595,6 +2595,8 @@ export const other: { value: string } = box;`, }); const project = snapshot.getProjects()[0]; const program = project.program; + // @ts-ignore -- Internal client requests are exercised by source tests. + const request = context.mock.method(api["client"], "apiRequest"); const identifiers = program.getIdentifierCount(); const nodes = program.getNodeCount(); const symbols = program.getSymbolCount(); @@ -2613,14 +2615,15 @@ export const other: { value: string } = box;`, const declaration = cast(file.statements[1], isVariableStatement).declarationList.declarations[0]; const type = checker.getTypeAtLocation(declaration.name); type.getProperties(); - assert.ok(program.getInstantiationCount() > instantiations); - assert.ok(program.getTypeCount() > types); - assert.ok(program.getSymbolCount() >= symbols); + assert.equal(program.getInstantiationCount(), instantiations); + assert.equal(program.getTypeCount(), types); + assert.equal(program.getSymbolCount(), symbols); const other = cast(file.statements[2], isVariableStatement).declarationList.declarations[0]; checker.isTypeAssignableTo(type, checker.getTypeAtLocation(other.name)); - assert.ok((program.getRelationCacheSizes()).assignable > relations.assignable); + assert.deepEqual(program.getRelationCacheSizes(), relations); assert.equal(program.getIdentifierCount(), identifiers); assert.equal(program.getNodeCount(), nodes); + assert.equal(request.mock.calls.filter(call => call.arguments[0] === "getProgramStatistics").length, 1); }); test("Program accessors handle empty programs and missing root files", () => { From f22dc74c17efa6eb4573cf3aaf6067287174ecf1 Mon Sep 17 00:00:00 2001 From: isabel duan Date: Thu, 5 Nov 2026 16:50:30 -0800 Subject: [PATCH 13/13] fix up stuff about statistics --- packages/typescript/src/api/async/api.ts | 30 +---- packages/typescript/src/api/sync/api.ts | 110 +----------------- packages/typescript/test/async/api.test.ts | 92 +++++++-------- .../test/sync/api-generators.test.ts | 36 ++---- packages/typescript/test/sync/api.test.ts | 47 ++++---- 5 files changed, 81 insertions(+), 234 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 59997cb9606e7..7798b13517326 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -241,6 +241,7 @@ export type { ObjectType, PackageId, ParsedCommandLine, + ProgramStatisticsResponse, ProjectId, ProjectReference, RawCompilerOptions, @@ -1748,7 +1749,6 @@ export class Program implements FormatDiagnost private readonly sourceFileMetadataCache = new Map>(); private ownedSnapshot: Snapshot | undefined; private disposePromise: Promise | undefined; - private statistics: Promise | undefined; private get client(): Client { return this.project.api.client; @@ -1808,37 +1808,13 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.projectReferences; } - private getStatistics(): Promise { - return this.statistics ??= this.client.apiRequest("getProgramStatistics", { + getStatistics(): Promise { + return this.client.apiRequest("getProgramStatistics", { snapshot: this.snapshotId, project: this.project.handle, }); } - async getIdentifierCount(): Promise { - return (await this.getStatistics()).identifierCount; - } - - async getInstantiationCount(): Promise { - return (await this.getStatistics()).instantiationCount; - } - - async getNodeCount(): Promise { - return (await this.getStatistics()).nodeCount; - } - - async getSymbolCount(): Promise { - return (await this.getStatistics()).symbolCount; - } - - async getTypeCount(): Promise { - return (await this.getStatistics()).typeCount; - } - - async getRelationCacheSizes(): Promise { - return (await this.getStatistics()).relationCacheSizes; - } - async getSourceFile(file: DocumentIdentifier): Promise { const fileName = resolveFileName(file); const path = this.pathKeyForFileName(fileName); diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index c3c9352ec685e..4c8b08f036901 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -259,6 +259,7 @@ export type { ObjectType, PackageId, ParsedCommandLine, + ProgramStatisticsResponse, ProjectId, ProjectReference, RawCompilerOptions, @@ -2927,7 +2928,6 @@ export class Program implements FormatDiagnost private readonly sourceFileMetadataCache = new Map(); private ownedSnapshot: Snapshot | undefined; private disposePromise: void | undefined; - private statistics: ProgramStatisticsResponse | undefined; private get client(): Client { return this.project.api.client; @@ -3015,7 +3015,7 @@ export class Program implements FormatDiagnost return this.project.parsedCommandLine.projectReferences; } - private get getStatistics(): { + get getStatistics(): { (): ProgramStatisticsResponse; gen(): Generator; } { @@ -3024,13 +3024,13 @@ export class Program implements FormatDiagnost owner, "getStatistics", function (): ProgramStatisticsResponse { - return owner.statistics ??= owner.client.apiRequest("getProgramStatistics", { + return owner.client.apiRequest("getProgramStatistics", { snapshot: owner.snapshotId, project: owner.project.handle, }); }, function* (): Generator { - return owner.statistics ??= yield* apiRequest("getProgramStatistics", { + return yield* apiRequest("getProgramStatistics", { snapshot: owner.snapshotId, project: owner.project.handle, }); @@ -3038,108 +3038,6 @@ export class Program implements FormatDiagnost ); } - get getIdentifierCount(): { - (): number; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getIdentifierCount", - function (): number { - return (owner.getStatistics()).identifierCount; - }, - function* (): Generator { - return (yield* owner.getStatistics.gen()).identifierCount; - }, - ); - } - - get getInstantiationCount(): { - (): number; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getInstantiationCount", - function (): number { - return (owner.getStatistics()).instantiationCount; - }, - function* (): Generator { - return (yield* owner.getStatistics.gen()).instantiationCount; - }, - ); - } - - get getNodeCount(): { - (): number; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getNodeCount", - function (): number { - return (owner.getStatistics()).nodeCount; - }, - function* (): Generator { - return (yield* owner.getStatistics.gen()).nodeCount; - }, - ); - } - - get getSymbolCount(): { - (): number; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getSymbolCount", - function (): number { - return (owner.getStatistics()).symbolCount; - }, - function* (): Generator { - return (yield* owner.getStatistics.gen()).symbolCount; - }, - ); - } - - get getTypeCount(): { - (): number; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getTypeCount", - function (): number { - return (owner.getStatistics()).typeCount; - }, - function* (): Generator { - return (yield* owner.getStatistics.gen()).typeCount; - }, - ); - } - - get getRelationCacheSizes(): { - (): RelationCacheSizes; - gen(): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "getRelationCacheSizes", - function (): RelationCacheSizes { - return (owner.getStatistics()).relationCacheSizes; - }, - function* (): Generator { - return (yield* owner.getStatistics.gen()).relationCacheSizes; - }, - ); - } - get getSourceFile(): { (file: DocumentIdentifier): SourceFile | undefined; gen(file: DocumentIdentifier): Generator; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 86d471e2a2f64..303077ef09504 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1531,7 +1531,7 @@ declare module "augmentation" {}`, // @sync-skip-block-start describe("API - automatic batching", { concurrency }, () => { - test("shares pending and settled Program statistics requests", async context => { + test("fetches Program statistics for every request", async context => { await using api = spawnAPI({ "/src/main.ts": "export const value = 1;" }); await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); await using empty = await api.createProgram([], { noLib: true }); @@ -1539,30 +1539,26 @@ describe("API - automatic batching", { concurrency }, () => { const client = api["client"]; const request = context.mock.method(client, "apiRequest"); - const [identifiers, instantiations, nodes, symbols, types, relations, emptyNodes] = await Promise.all([ - program.getIdentifierCount(), - program.getInstantiationCount(), - program.getNodeCount(), - program.getSymbolCount(), - program.getTypeCount(), - program.getRelationCacheSizes(), - empty.getNodeCount(), + const [statistics, repeated, emptyStatistics] = await Promise.all([ + program.getStatistics(), + program.getStatistics(), + empty.getStatistics(), ]); - assert.equal(request.mock.callCount(), 2); - assert.ok(identifiers > 0); - assert.ok(nodes > identifiers); - assert.ok(symbols > 0); - assert.ok(types > 0); - assert.equal(instantiations, 0); - assert.deepEqual(relations, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); - assert.equal(emptyNodes, 0); - - await program.getTypeCount(); - await program.getSymbolCount(); - assert.equal(request.mock.callCount(), 2); + assert.equal(request.mock.callCount(), 3); + assert.deepEqual(repeated, statistics); + assert.ok(statistics.identifierCount > 0); + assert.ok(statistics.nodeCount > statistics.identifierCount); + assert.ok(statistics.symbolCount > 0); + assert.ok(statistics.typeCount > 0); + assert.equal(statistics.instantiationCount, 0); + assert.deepEqual(statistics.relationCacheSizes, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); + assert.equal(emptyStatistics.nodeCount, 0); + + assert.deepEqual(await program.getStatistics(), statistics); + assert.equal(request.mock.callCount(), 4); }); - test("caches rejected Program statistics requests", async context => { + test("retries Program statistics requests after failure", async context => { await using api = spawnAPI({ "/src/main.ts": "export const value = 1;" }); await using program = await api.createProgram(["/src/main.ts"], { noLib: true }); // @ts-ignore -- Internal client requests are exercised by source tests. @@ -1571,14 +1567,10 @@ describe("API - automatic batching", { concurrency }, () => { const error = new Error("Statistics request failed"); request.mock.mockImplementationOnce(() => Promise.reject(error)); - const results = await Promise.allSettled([program.getNodeCount(), program.getTypeCount()]); - assert.deepEqual(results, [ - { status: "rejected", reason: error }, - { status: "rejected", reason: error }, - ]); - assert.equal(request.mock.callCount(), 1); - await assert.rejects(program.getNodeCount(), error); + await assert.rejects(program.getStatistics(), error); assert.equal(request.mock.callCount(), 1); + assert.ok((await program.getStatistics()).nodeCount > 0); + assert.equal(request.mock.callCount(), 2); }); test("initializes only once for concurrent first requests", async () => { @@ -2776,7 +2768,7 @@ describe("SourceFile", { concurrency }, () => { assert.equal(await program.getProjectReferences(), undefined); }); - test("Program counters cache the first statistics without forcing diagnostics", async () => { + test("Program statistics reflect checker work without forcing diagnostics", async () => { await using api = spawnAPI({ "/src/main.ts": `type Box = { value: T }; export const box: Box = { value: "hello" }; @@ -2788,33 +2780,30 @@ export const other: { value: string } = box;`, const project = snapshot.getProjects()[0]; const program = project.program; await api.resetTimingInfo(); - const identifiers = await program.getIdentifierCount(); - const nodes = await program.getNodeCount(); - const symbols = await program.getSymbolCount(); - const types = await program.getTypeCount(); - const instantiations = await program.getInstantiationCount(); - const relations = await program.getRelationCacheSizes(); - assert.equal((await api.getTimingInfo()).totals.requestCount, 1); - assert.ok(identifiers > 0); - assert.ok(nodes > identifiers); - assert.ok(symbols > 0); - assert.ok(types > 0); - assert.equal(instantiations, 0); - assert.deepEqual(relations, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); + const statistics = await program.getStatistics(); + assert.deepEqual(await program.getStatistics(), statistics); + assert.equal((await api.getTimingInfo()).totals.requestCount, 2); + assert.ok(statistics.identifierCount > 0); + assert.ok(statistics.nodeCount > statistics.identifierCount); + assert.ok(statistics.symbolCount > 0); + assert.ok(statistics.typeCount > 0); + assert.equal(statistics.instantiationCount, 0); + assert.deepEqual(statistics.relationCacheSizes, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); const file = (await program.getSourceFile("/src/main.ts"))!; const checker = project.checker; const declaration = cast(file.statements[1], isVariableStatement).declarationList.declarations[0]; const type = await checker.getTypeAtLocation(declaration.name); await type.getProperties(); - assert.equal(await program.getInstantiationCount(), instantiations); - assert.equal(await program.getTypeCount(), types); - assert.equal(await program.getSymbolCount(), symbols); const other = cast(file.statements[2], isVariableStatement).declarationList.declarations[0]; await checker.isTypeAssignableTo(type, await checker.getTypeAtLocation(other.name)); - assert.deepEqual(await program.getRelationCacheSizes(), relations); - assert.equal(await program.getIdentifierCount(), identifiers); - assert.equal(await program.getNodeCount(), nodes); + const updated = await program.getStatistics(); + assert.ok(updated.instantiationCount > statistics.instantiationCount); + assert.ok(updated.typeCount > statistics.typeCount); + assert.ok(updated.symbolCount >= statistics.symbolCount); + assert.ok(updated.relationCacheSizes.assignable > statistics.relationCacheSizes.assignable); + assert.equal(updated.identifierCount, statistics.identifierCount); + assert.equal(updated.nodeCount, statistics.nodeCount); }); test("Program accessors handle empty programs and missing root files", async () => { @@ -2822,8 +2811,9 @@ export const other: { value: string } = box;`, await using empty = await api.createProgram([], { noLib: true }); assert.deepEqual(await empty.getRootFileNames(), []); assert.deepEqual(await empty.getSourceFileNames(), []); - assert.equal(await empty.getNodeCount(), 0); - assert.equal(await empty.getIdentifierCount(), 0); + const statistics = await empty.getStatistics(); + assert.equal(statistics.nodeCount, 0); + assert.equal(statistics.identifierCount, 0); assert.equal(await empty.getProjectReferences(), undefined); assert.deepEqual(await empty.getProgramDiagnostics(), []); diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 0658469387b9e..b6d9a6ff1cd0b 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -173,7 +173,6 @@ const privateGeneratorGetters = new Set([ "NodeHandle.fetchOwnerFile", "NodeHandle.resolveInFile", "Program.disposeWorker", - "Program.getStatistics", "Program.fetchSourceFileMetadata", "Program.getSourceFileWorker", "Snapshot.disposeWorker", @@ -447,29 +446,21 @@ function assertPublicGeneratorCoverage(owners: readonly { readonly name: string; } describe("API - generator batching", { concurrency: areTestsFiltered() }, () => { - test("Program statistics share a cache between sync and generator calls", () => { + test("Program statistics are fetched for every sync and generator call", () => { using api = spawnAPI({ "/src/main.ts": "export const value = 1;" }, { collectTiming: true }); for (const generatorFirst of [false, true]) { using program = api.createProgram(["/src/main.ts"], { noLib: true }); api.resetTimingInfo(); - const nodes = generatorFirst ? api.batch(program.getNodeCount.gen())[0] : program.getNodeCount(); - assert.ok(nodes > 0); - const [cachedNodes, types, symbols, instantiations, identifiers, relations] = api.batch( - program.getNodeCount.gen(), - program.getTypeCount.gen(), - program.getSymbolCount.gen(), - program.getInstantiationCount.gen(), - program.getIdentifierCount.gen(), - program.getRelationCacheSizes.gen(), + const statistics = generatorFirst ? api.batch(program.getStatistics.gen())[0] : program.getStatistics(); + assert.ok(statistics.nodeCount > 0); + const [next, repeated] = api.batch( + program.getStatistics.gen(), + program.getStatistics.gen(), ); - assert.equal(cachedNodes, nodes); - assert.equal(program.getNodeCount(), nodes); - assert.equal(program.getTypeCount(), types); - assert.equal(program.getSymbolCount(), symbols); - assert.equal(program.getInstantiationCount(), instantiations); - assert.equal(program.getIdentifierCount(), identifiers); - assert.strictEqual(program.getRelationCacheSizes(), relations); - assert.equal(api.getTimingInfo().totals.requestCount, 1); + assert.deepEqual(next, statistics); + assert.deepEqual(repeated, statistics); + assert.deepEqual(program.getStatistics(), statistics); + assert.equal(api.getTimingInfo().totals.requestCount, 3); } }); @@ -1828,12 +1819,7 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => runParityBatch(api, cases); runParityBatch(api, [ - parityCase("Program", "getIdentifierCount", program.getIdentifierCount, assertDeepEquivalent), - parityCase("Program", "getNodeCount", program.getNodeCount, assertDeepEquivalent), - parityCase("Program", "getSymbolCount", program.getSymbolCount, assertDeepEquivalent), - parityCase("Program", "getTypeCount", program.getTypeCount, assertDeepEquivalent), - parityCase("Program", "getInstantiationCount", program.getInstantiationCount, assertDeepEquivalent), - parityCase("Program", "getRelationCacheSizes", program.getRelationCacheSizes, assertDeepEquivalent), + parityCase("Program", "getStatistics", program.getStatistics, assertDeepEquivalent), ]); assert.deepEqual(temporaryProjects, ["/tsconfig.json", "/tsconfig.json"]); diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index f591c46501251..9c82750df0984 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -2584,46 +2584,42 @@ describe("SourceFile", { concurrency }, () => { assert.equal(program.getProjectReferences(), undefined); }); - test("Program counters cache the first statistics without forcing diagnostics", context => { + test("Program statistics reflect checker work without forcing diagnostics", () => { using api = spawnAPI({ "/src/main.ts": `type Box = { value: T }; export const box: Box = { value: "hello" }; export const other: { value: string } = box;`, - }); + }, { collectTiming: true }); const snapshot = api.createSnapshot({ createPrograms: [{ rootFiles: ["/src/main.ts"], compilerOptions: { noLib: true } }], }); const project = snapshot.getProjects()[0]; const program = project.program; - // @ts-ignore -- Internal client requests are exercised by source tests. - const request = context.mock.method(api["client"], "apiRequest"); - const identifiers = program.getIdentifierCount(); - const nodes = program.getNodeCount(); - const symbols = program.getSymbolCount(); - const types = program.getTypeCount(); - const instantiations = program.getInstantiationCount(); - const relations = program.getRelationCacheSizes(); - assert.ok(identifiers > 0); - assert.ok(nodes > identifiers); - assert.ok(symbols > 0); - assert.ok(types > 0); - assert.equal(instantiations, 0); - assert.deepEqual(relations, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); + api.resetTimingInfo(); + const statistics = program.getStatistics(); + assert.deepEqual(program.getStatistics(), statistics); + assert.equal((api.getTimingInfo()).totals.requestCount, 2); + assert.ok(statistics.identifierCount > 0); + assert.ok(statistics.nodeCount > statistics.identifierCount); + assert.ok(statistics.symbolCount > 0); + assert.ok(statistics.typeCount > 0); + assert.equal(statistics.instantiationCount, 0); + assert.deepEqual(statistics.relationCacheSizes, { assignable: 0, identity: 0, subtype: 0, strictSubtype: 0 }); const file = (program.getSourceFile("/src/main.ts"))!; const checker = project.checker; const declaration = cast(file.statements[1], isVariableStatement).declarationList.declarations[0]; const type = checker.getTypeAtLocation(declaration.name); type.getProperties(); - assert.equal(program.getInstantiationCount(), instantiations); - assert.equal(program.getTypeCount(), types); - assert.equal(program.getSymbolCount(), symbols); const other = cast(file.statements[2], isVariableStatement).declarationList.declarations[0]; checker.isTypeAssignableTo(type, checker.getTypeAtLocation(other.name)); - assert.deepEqual(program.getRelationCacheSizes(), relations); - assert.equal(program.getIdentifierCount(), identifiers); - assert.equal(program.getNodeCount(), nodes); - assert.equal(request.mock.calls.filter(call => call.arguments[0] === "getProgramStatistics").length, 1); + const updated = program.getStatistics(); + assert.ok(updated.instantiationCount > statistics.instantiationCount); + assert.ok(updated.typeCount > statistics.typeCount); + assert.ok(updated.symbolCount >= statistics.symbolCount); + assert.ok(updated.relationCacheSizes.assignable > statistics.relationCacheSizes.assignable); + assert.equal(updated.identifierCount, statistics.identifierCount); + assert.equal(updated.nodeCount, statistics.nodeCount); }); test("Program accessors handle empty programs and missing root files", () => { @@ -2631,8 +2627,9 @@ export const other: { value: string } = box;`, using empty = api.createProgram([], { noLib: true }); assert.deepEqual(empty.getRootFileNames(), []); assert.deepEqual(empty.getSourceFileNames(), []); - assert.equal(empty.getNodeCount(), 0); - assert.equal(empty.getIdentifierCount(), 0); + const statistics = empty.getStatistics(); + assert.equal(statistics.nodeCount, 0); + assert.equal(statistics.identifierCount, 0); assert.equal(empty.getProjectReferences(), undefined); assert.deepEqual(empty.getProgramDiagnostics(), []);