diff --git a/packages/compiler/package.json b/packages/compiler/package.json index d1c39d8ac..3237def0a 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -44,6 +44,9 @@ "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, + "devDependencies": { + "typescript-7.1": "npm:typescript@7.1.0-beta" + }, "optionalDependencies": { "@scriptc/llvm-darwin-arm64": "workspace:*", "@scriptc/llvm-darwin-x64": "workspace:*", diff --git a/packages/compiler/src/frontend/ts7/api.ts b/packages/compiler/src/frontend/ts7/api.ts index f18675d0e..4e8ef8291 100644 --- a/packages/compiler/src/frontend/ts7/api.ts +++ b/packages/compiler/src/frontend/ts7/api.ts @@ -12,8 +12,10 @@ export class Ts7Api { constructor(rpc: Ts7RpcClient, fs: Ts7FileSystem, collectTiming = false) { try { - registerTs7FileSystem(rpc, fs); - this.session = new Ts7Session(rpc, collectTiming); + const session = new Ts7Session(rpc, collectTiming); + // Callbacks run inside requests, after initialize chose the protocol. + registerTs7FileSystem(rpc, fs, () => session.speaksTs71()); + this.session = session; } catch (error) { rpc.close(); throw error; diff --git a/packages/compiler/src/frontend/ts7/ast-wire.ts b/packages/compiler/src/frontend/ts7/ast-wire.ts index 4cba5923a..14b1c4548 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.ts @@ -135,6 +135,11 @@ const HEADER_SIZE_7_1 = 64; /** SourceKeyword in TypeScript 7.1's kinds. */ const SOURCE_KEYWORD_7_1 = 167; +/** A TypeScript 7.0 SyntaxKind as TypeScript 7.1 numbers it. */ +export function syntaxKind71(kind: number): number { + return kind >= SOURCE_KEYWORD_7_1 ? kind + 1 : kind; +} + /** The node table of a protocol 7.1 response in TypeScript 7.0's terms: each * kind above SourceKeyword one lower, and each module declaration's child * mask without the attributes slot. Throws for the nodes 7.0 has no form @@ -248,7 +253,7 @@ export class AstWireFile { /** `kind` (TypeScript 7.0's) as this response's protocol numbers it, the * kind a node handle names. */ kindOnWire(kind: number): number { - return this.protocol === PROTOCOL_VERSION_7_1 && kind >= SOURCE_KEYWORD_7_1 ? kind + 1 : kind; + return this.protocol === PROTOCOL_VERSION_7_1 ? syntaxKind71(kind) : kind; } /** A kind as this response's protocol numbers it (a node handle's) in diff --git a/packages/compiler/src/frontend/ts7/checker.test.ts b/packages/compiler/src/frontend/ts7/checker.test.ts index cb4a2e5fb..7e7227437 100644 --- a/packages/compiler/src/frontend/ts7/checker.test.ts +++ b/packages/compiler/src/frontend/ts7/checker.test.ts @@ -7,6 +7,7 @@ import { Ts7RpcClient } from "./rpc-client.js"; import { registerTs7FileSystem, TS7_FILE_SYSTEM_CALLBACKS } from "./rpc-filesystem.js"; import { spawnTs7Wire } from "./rpc-process.js"; import { ts7Executable } from "./rpc-api.js"; +import { ts7TestServers, type Ts7TestServer } from "./test-servers.js"; import { Ts7Session } from "./session.js"; import { SemanticChecker } from "./semantic-checker.js"; import { SemanticSnapshot } from "./semantic-model.js"; @@ -18,26 +19,36 @@ import type { Identifier, SourceFile } from "./ast-types.js"; const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const protocolPath = (path: string) => path.replaceAll("\\", "/"); -function connect(files: Record) { +/** The default checker (tsc-ts) and tsc 7.1. */ +const servers: Ts7TestServer[] = [ + { name: "tsc 7.0", executable: ts7Executable(), ts71: false }, + ...ts7TestServers().filter((server) => server.ts71), +]; + +function connect(server: Ts7TestServer, files: Record) { const dir = mkdtempSync(join(tempRoot, "scriptc-checker-lifetime-")); const paths = new Map( Object.entries(files).map(([name, text]) => [protocolPath(join(dir, name)), text]), ); const rpc = new Ts7RpcClient( - spawnTs7Wire(ts7Executable(), [ + spawnTs7Wire(server.executable, [ "--api", "--cwd", dir, `--callbacks=${TS7_FILE_SYSTEM_CALLBACKS}`, ]), ); - registerTs7FileSystem(rpc, { - readFile: (path) => paths.get(path), - fileExists: (path) => (paths.has(path) ? true : undefined), - directoryExists: () => undefined, - realpath: () => undefined, - getAccessibleEntries: () => undefined, - }); + registerTs7FileSystem( + rpc, + { + readFile: (path) => paths.get(path), + fileExists: (path) => (paths.has(path) ? true : undefined), + directoryExists: () => undefined, + realpath: () => undefined, + getAccessibleEntries: () => undefined, + }, + () => server.ts71, + ); const session = new Ts7Session(rpc); return { session, @@ -65,184 +76,188 @@ const config = JSON.stringify({ files: ["main.ts"], }); -test("unchanged AST identities receive independent semantic answers after a dependency update", () => { - const h = connect({ - "tsconfig.json": config, - "main.ts": 'import { value } from "./dependency.js"; export const result = value;', - "dependency.ts": "export const value: number = 1;", - }); - try { - const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); - const oldProject = first.getProjects()[0]!; - const root = oldProject.program.getSourceFile(h.path("main.ts"))!; - const name = declaration(root, "result").name!; - const old = new CheckerFacade(oldProject.checker, { project: oldProject.checker.project }); - const oldType = old.getTypeAtLocation(name); - const oldSymbol = old.getSymbolAtLocation(name)!; - expect(old.typeToString(oldType)).toBe("number"); - expect(oldSymbol.valueDeclaration).toBe(declaration(root, "result")); - - h.paths.set(h.path("dependency.ts"), 'export const value: string = "changed";'); - const second = h.session.updateSnapshot({ - fileChanges: { changed: [h.path("dependency.ts")] }, +for (const server of servers) + test(`unchanged AST identities receive independent semantic answers after a dependency update (${server.name})`, () => { + const h = connect(server, { + "tsconfig.json": config, + "main.ts": 'import { value } from "./dependency.js"; export const result = value;', + "dependency.ts": "export const value: number = 1;", }); - const newProject = second.getProjects()[0]!; - const nextRoot = newProject.program.getSourceFile(h.path("main.ts"))!; - expect(nextRoot).toBe(root); - expect(declaration(nextRoot, "result").name).toBe(name); - const next = new CheckerFacade(newProject.checker, { project: newProject.checker.project }); - const newType = next.getTypeAtLocation(name); - expect(next.typeToString(newType)).toBe("string"); - expect(newType).not.toBe(oldType); - expect(next.getSymbolAtLocation(name)).not.toBe(oldSymbol); - expect(old.typeToString(old.getTypeAtLocation(name))).toBe("number"); - first.dispose(); - expect(() => old.getTypeAtLocation(name)).toThrow("disposed"); - expect(next.getTypeAtLocation(name)).toBe(newType); - expect(next.typeToString(newType)).toBe("string"); - second.dispose(); - expect(() => next.getTypeAtLocation(name)).toThrow("disposed"); - } finally { - h.close(); - } -}); + try { + const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); + const oldProject = first.getProjects()[0]!; + const root = oldProject.program.getSourceFile(h.path("main.ts"))!; + const name = declaration(root, "result").name!; + const old = new CheckerFacade(oldProject.checker, { project: oldProject.checker.project }); + const oldType = old.getTypeAtLocation(name); + const oldSymbol = old.getSymbolAtLocation(name)!; + expect(old.typeToString(oldType)).toBe("number"); + expect(oldSymbol.valueDeclaration).toBe(declaration(root, "result")); -test("project-local type handles never alias across simultaneous checker caches", () => { - const h = connect({ - "first.json": config, - "second.json": config, - "main.ts": "export type Choice = string | number; export const value = 42;", + h.paths.set(h.path("dependency.ts"), 'export const value: string = "changed";'); + const second = h.session.updateSnapshot({ + fileChanges: { changed: [h.path("dependency.ts")] }, + }); + const newProject = second.getProjects()[0]!; + const nextRoot = newProject.program.getSourceFile(h.path("main.ts"))!; + expect(nextRoot).toBe(root); + expect(declaration(nextRoot, "result").name).toBe(name); + const next = new CheckerFacade(newProject.checker, { project: newProject.checker.project }); + const newType = next.getTypeAtLocation(name); + expect(next.typeToString(newType)).toBe("string"); + expect(newType).not.toBe(oldType); + expect(next.getSymbolAtLocation(name)).not.toBe(oldSymbol); + expect(old.typeToString(old.getTypeAtLocation(name))).toBe("number"); + first.dispose(); + expect(() => old.getTypeAtLocation(name)).toThrow("disposed"); + expect(next.getTypeAtLocation(name)).toBe(newType); + expect(next.typeToString(newType)).toBe("string"); + second.dispose(); + expect(() => next.getTypeAtLocation(name)).toThrow("disposed"); + } finally { + h.close(); + } }); - try { - const snapshot = h.session.updateSnapshot({ - openProjects: [h.path("first.json"), h.path("second.json")], + +for (const server of servers) + test(`project-local type handles never alias across simultaneous checker caches (${server.name})`, () => { + const h = connect(server, { + "first.json": config, + "second.json": config, + "main.ts": "export type Choice = string | number; export const value = 42;", }); - const first = snapshot.getProject(h.path("first.json"))!; - const second = snapshot.getProject(h.path("second.json"))!; - const root = first.program.getSourceFile(h.path("main.ts"))!; - expect(second.program.getSourceFile(h.path("main.ts"))).toBe(root); - const a = new CheckerFacade(first.checker, { project: first.checker.project }); - const b = new CheckerFacade(second.checker, { project: second.checker.project }); - const name = declaration(root, "value").name!; - const firstType = a.getTypeAtLocation(name); - const secondType = b.getTypeAtLocation(name); - expect(firstType.project).not.toBe(secondType.project); - expect(firstType).not.toBe(secondType); - expect(a.typeToString(firstType)).toBe(b.typeToString(secondType)); - const alias = declaration(root, "Choice").type!; - const firstArms = constituentTypes(a.getTypeFromTypeNode(alias)); - const secondArms = constituentTypes(b.getTypeFromTypeNode(alias)); - expect(firstArms).not.toBe(secondArms); - expect(firstArms.map((arm) => a.typeToString(arm))).toEqual( - secondArms.map((arm) => b.typeToString(arm)), - ); - first.checker.project.dispose(); - expect(() => a.getTypeAtLocation(name)).toThrow("disposed"); - expect(b.getTypeAtLocation(name)).toBe(secondType); - expect(b.getTypeFromTypeNode(alias).getTypes()).toBe(secondArms); - } finally { - h.close(); - } -}); - -test("all public query and prefetch entry points reject a disposed facade before returning cached or local answers", () => { - const h = connect({ - "tsconfig.json": config, - "main.ts": "export const value = 42; export function fn(v: number): number { return v; }", + try { + const snapshot = h.session.updateSnapshot({ + openProjects: [h.path("first.json"), h.path("second.json")], + }); + const first = snapshot.getProject(h.path("first.json"))!; + const second = snapshot.getProject(h.path("second.json"))!; + const root = first.program.getSourceFile(h.path("main.ts"))!; + expect(second.program.getSourceFile(h.path("main.ts"))).toBe(root); + const a = new CheckerFacade(first.checker, { project: first.checker.project }); + const b = new CheckerFacade(second.checker, { project: second.checker.project }); + const name = declaration(root, "value").name!; + const firstType = a.getTypeAtLocation(name); + const secondType = b.getTypeAtLocation(name); + expect(firstType.project).not.toBe(secondType.project); + expect(firstType).not.toBe(secondType); + expect(a.typeToString(firstType)).toBe(b.typeToString(secondType)); + const alias = declaration(root, "Choice").type!; + const firstArms = constituentTypes(a.getTypeFromTypeNode(alias)); + const secondArms = constituentTypes(b.getTypeFromTypeNode(alias)); + expect(firstArms).not.toBe(secondArms); + expect(firstArms.map((arm) => a.typeToString(arm))).toEqual( + secondArms.map((arm) => b.typeToString(arm)), + ); + first.checker.project.dispose(); + expect(() => a.getTypeAtLocation(name)).toThrow("disposed"); + expect(b.getTypeAtLocation(name)).toBe(secondType); + expect(b.getTypeFromTypeNode(alias).getTypes()).toBe(secondArms); + } finally { + h.close(); + } }); - try { - const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); - const project = snapshot.getProjects()[0]!; - const root = project.program.getSourceFile(h.path("main.ts"))!; - const facade = new CheckerFacade(project.checker, { project: project.checker.project }); - const name = declaration(root, "value").name!; - const fn = declaration(root, "fn"); - const type = facade.getTypeAtLocation(name); - const symbol = facade.getSymbolAtLocation(name)!; - const signature = facade.getSignatureFromDeclaration(fn)!; - const never = project.checker.getNeverType(); - facade.getUnknownType(); - facade.typeToString(type); - facade.prefetchSourceFile(root); - const calls: Record unknown> = { - getCallSignatures: () => facade.getCallSignatures(type), - getConstructSignatures: () => facade.getConstructSignatures(type), - getSignaturesOfType: () => facade.getSignaturesOfType(type, SignatureKind.Call), - resolveName: () => facade.resolveName("value", SymbolFlags.Value, name), - getResolvedSymbol: () => facade.getResolvedSymbol(name as Identifier), - getAnyType: () => facade.getAnyType(), - getNeverType: () => facade.getNeverType(), - getBigIntType: () => facade.getBigIntType(), - getPropertyOfType: () => facade.getPropertyOfType(type, "missing"), - prefetchSourceFile: () => facade.prefetchSourceFile(root), - prefetchSourceFileStructures: () => facade.prefetchSourceFileStructures([]), - prefetchRoots: () => facade.prefetchRoots([]), - prefetchSymbolRoots: () => facade.prefetchSymbolRoots([]), - prefetchSymbolNodesExact: () => facade.prefetchSymbolNodesExact([]), - prefetchCollectionTypes: () => facade.prefetchCollectionTypes([]), - prefetchClassCollection: () => facade.prefetchClassCollection([], []), - getTypeAtLocation: () => facade.getTypeAtLocation(name), - getSymbolAtLocation: () => facade.getSymbolAtLocation(name), - getTypeOfSymbol: () => facade.getTypeOfSymbol(symbol), - prefetchMemberTypes: () => facade.prefetchMemberTypes([symbol], 0), - getAliasedSymbol: () => facade.getAliasedSymbol(symbol), - getDeclaredTypeOfSymbol: () => facade.getDeclaredTypeOfSymbol(symbol), - getContextualType: () => facade.getContextualType(name), - getTypeFromTypeNode: () => facade.getTypeFromTypeNode(fn.type!), - getShorthandAssignmentValueSymbol: () => facade.getShorthandAssignmentValueSymbol(name), - getResolvedSignature: () => facade.getResolvedSignature(name), - getSignatureFromDeclaration: () => facade.getSignatureFromDeclaration(fn), - getReturnTypeOfSignature: () => facade.getReturnTypeOfSignature(signature), - getTypePredicateOfSignature: () => facade.getTypePredicateOfSignature(signature), - getBaseTypeOfLiteralType: () => facade.getBaseTypeOfLiteralType(type), - getConstantValue: () => facade.getConstantValue(name), - getNonNullableType: () => facade.getNonNullableType(type), - getPropertiesOfType: () => facade.getPropertiesOfType(type), - getBaseTypes: () => facade.getBaseTypes(type), - isNeverType: () => facade.isNeverType(never), - isTypeAssignableTo: () => facade.isTypeAssignableTo(type, type), - getIndexInfosOfType: () => facade.getIndexInfosOfType(type), - getTypeArguments: () => facade.getTypeArguments(type), - isArrayType: () => facade.isArrayType(type), - isTupleType: () => facade.isTupleType(type), - isArrayLikeType: () => facade.isArrayLikeType(type), - typeToString: () => facade.typeToString(type), - getTypeOfSymbolAtLocation: () => facade.getTypeOfSymbolAtLocation(symbol, name), - getUnknownType: () => facade.getUnknownType(), - getStringType: () => facade.getStringType(), - getNumberType: () => facade.getNumberType(), - getBooleanType: () => facade.getBooleanType(), - getAwaitedType: () => facade.getAwaitedType(type), - }; - const before = h.rpc.timing().requests; - facade.dispose(); - facade.dispose(); - for (const [name, call] of Object.entries(calls)) - expect(call, name).toThrow("checker facade is disposed"); - expect(h.rpc.timing().requests).toBe(before); - expect(project.checker.typeToString(type)).toBe("42"); - } finally { - h.close(); - } -}); -test("session shutdown invalidates every retained facade without explicit program disposal", () => { - const h = connect({ "tsconfig.json": config, "main.ts": "export const value = 1;" }); - try { - const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); - const project = snapshot.getProjects()[0]!; - const facade = new CheckerFacade(project.checker); - const name = declaration(project.program.getSourceFile(h.path("main.ts"))!, "value").name!; - facade.getTypeAtLocation(name); - facade.getNumberType(); - h.session.close(); - expect(() => facade.getTypeAtLocation(name)).toThrow("disposed"); - expect(() => facade.getNumberType()).toThrow("disposed"); - expect(() => new CheckerFacade(project.checker)).toThrow("disposed"); - } finally { - h.close(); - } -}); +for (const server of servers) + test(`all public query and prefetch entry points reject a disposed facade before returning cached or local answers (${server.name})`, () => { + const h = connect(server, { + "tsconfig.json": config, + "main.ts": "export const value = 42; export function fn(v: number): number { return v; }", + }); + try { + const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); + const project = snapshot.getProjects()[0]!; + const root = project.program.getSourceFile(h.path("main.ts"))!; + const facade = new CheckerFacade(project.checker, { project: project.checker.project }); + const name = declaration(root, "value").name!; + const fn = declaration(root, "fn"); + const type = facade.getTypeAtLocation(name); + const symbol = facade.getSymbolAtLocation(name)!; + const signature = facade.getSignatureFromDeclaration(fn)!; + const never = project.checker.getNeverType(); + facade.getUnknownType(); + facade.typeToString(type); + facade.prefetchSourceFile(root); + const calls: Record unknown> = { + getCallSignatures: () => facade.getCallSignatures(type), + getConstructSignatures: () => facade.getConstructSignatures(type), + getSignaturesOfType: () => facade.getSignaturesOfType(type, SignatureKind.Call), + resolveName: () => facade.resolveName("value", SymbolFlags.Value, name), + getResolvedSymbol: () => facade.getResolvedSymbol(name as Identifier), + getAnyType: () => facade.getAnyType(), + getNeverType: () => facade.getNeverType(), + getBigIntType: () => facade.getBigIntType(), + getPropertyOfType: () => facade.getPropertyOfType(type, "missing"), + prefetchSourceFile: () => facade.prefetchSourceFile(root), + prefetchSourceFileStructures: () => facade.prefetchSourceFileStructures([]), + prefetchRoots: () => facade.prefetchRoots([]), + prefetchSymbolRoots: () => facade.prefetchSymbolRoots([]), + prefetchSymbolNodesExact: () => facade.prefetchSymbolNodesExact([]), + prefetchCollectionTypes: () => facade.prefetchCollectionTypes([]), + prefetchClassCollection: () => facade.prefetchClassCollection([], []), + getTypeAtLocation: () => facade.getTypeAtLocation(name), + getSymbolAtLocation: () => facade.getSymbolAtLocation(name), + getTypeOfSymbol: () => facade.getTypeOfSymbol(symbol), + prefetchMemberTypes: () => facade.prefetchMemberTypes([symbol], 0), + getAliasedSymbol: () => facade.getAliasedSymbol(symbol), + getDeclaredTypeOfSymbol: () => facade.getDeclaredTypeOfSymbol(symbol), + getContextualType: () => facade.getContextualType(name), + getTypeFromTypeNode: () => facade.getTypeFromTypeNode(fn.type!), + getShorthandAssignmentValueSymbol: () => facade.getShorthandAssignmentValueSymbol(name), + getResolvedSignature: () => facade.getResolvedSignature(name), + getSignatureFromDeclaration: () => facade.getSignatureFromDeclaration(fn), + getReturnTypeOfSignature: () => facade.getReturnTypeOfSignature(signature), + getTypePredicateOfSignature: () => facade.getTypePredicateOfSignature(signature), + getBaseTypeOfLiteralType: () => facade.getBaseTypeOfLiteralType(type), + getConstantValue: () => facade.getConstantValue(name), + getNonNullableType: () => facade.getNonNullableType(type), + getPropertiesOfType: () => facade.getPropertiesOfType(type), + getBaseTypes: () => facade.getBaseTypes(type), + isNeverType: () => facade.isNeverType(never), + isTypeAssignableTo: () => facade.isTypeAssignableTo(type, type), + getIndexInfosOfType: () => facade.getIndexInfosOfType(type), + getTypeArguments: () => facade.getTypeArguments(type), + isArrayType: () => facade.isArrayType(type), + isTupleType: () => facade.isTupleType(type), + isArrayLikeType: () => facade.isArrayLikeType(type), + typeToString: () => facade.typeToString(type), + getTypeOfSymbolAtLocation: () => facade.getTypeOfSymbolAtLocation(symbol, name), + getUnknownType: () => facade.getUnknownType(), + getStringType: () => facade.getStringType(), + getNumberType: () => facade.getNumberType(), + getBooleanType: () => facade.getBooleanType(), + getAwaitedType: () => facade.getAwaitedType(type), + }; + const before = h.rpc.timing().requests; + facade.dispose(); + facade.dispose(); + for (const [name, call] of Object.entries(calls)) + expect(call, name).toThrow("checker facade is disposed"); + expect(h.rpc.timing().requests).toBe(before); + expect(project.checker.typeToString(type)).toBe("42"); + } finally { + h.close(); + } + }); + +for (const server of servers) + test(`session shutdown invalidates every retained facade without explicit program disposal (${server.name})`, () => { + const h = connect(server, { "tsconfig.json": config, "main.ts": "export const value = 1;" }); + try { + const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); + const project = snapshot.getProjects()[0]!; + const facade = new CheckerFacade(project.checker); + const name = declaration(project.program.getSourceFile(h.path("main.ts"))!, "value").name!; + facade.getTypeAtLocation(name); + facade.getNumberType(); + h.session.close(); + expect(() => facade.getTypeAtLocation(name)).toThrow("disposed"); + expect(() => facade.getNumberType()).toThrow("disposed"); + expect(() => new CheckerFacade(project.checker)).toThrow("disposed"); + } finally { + h.close(); + } + }); test("program disposal and host close seal cached source and checker access", () => { const dir = mkdtempSync(join(tempRoot, "scriptc-program-lifetime-")); @@ -356,37 +371,38 @@ test("property lookup caches hits and misses by type and name within each projec snapshot.dispose(); }); -test("dependency edits invalidate positive and negative property answers", () => { - const h = connect({ - "tsconfig.json": config, - "main.ts": 'import { value } from "./dependency.js"; export const result = value;', - "dependency.ts": "export const value = { before: 1 };", - }); - try { - const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); - const project = first.getProjects()[0]!; - const root = project.program.getSourceFile(h.path("main.ts"))!; - const old = new CheckerFacade(project.checker); - const type = old.getTypeAtLocation(declaration(root, "result").name!); - expect(old.getPropertyOfType(type, "before")).toBeDefined(); - expect(old.getPropertyOfType(type, "after")).toBeUndefined(); - h.paths.set(h.path("dependency.ts"), "export const value = { after: 2 };"); - const second = h.session.updateSnapshot({ - fileChanges: { changed: [h.path("dependency.ts")] }, +for (const server of servers) + test(`dependency edits invalidate positive and negative property answers (${server.name})`, () => { + const h = connect(server, { + "tsconfig.json": config, + "main.ts": 'import { value } from "./dependency.js"; export const result = value;', + "dependency.ts": "export const value = { before: 1 };", }); - const nextProject = second.getProjects()[0]!; - const nextRoot = nextProject.program.getSourceFile(h.path("main.ts"))!; - expect(nextRoot).toBe(root); - const next = new CheckerFacade(nextProject.checker); - const nextType = next.getTypeAtLocation(declaration(nextRoot, "result").name!); - expect(next.getPropertyOfType(nextType, "before")).toBeUndefined(); - expect(next.getPropertyOfType(nextType, "after")).toBeDefined(); - expect(old.getPropertyOfType(type, "before")).toBeDefined(); - expect(old.getPropertyOfType(type, "after")).toBeUndefined(); - } finally { - h.close(); - } -}); + try { + const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); + const project = first.getProjects()[0]!; + const root = project.program.getSourceFile(h.path("main.ts"))!; + const old = new CheckerFacade(project.checker); + const type = old.getTypeAtLocation(declaration(root, "result").name!); + expect(old.getPropertyOfType(type, "before")).toBeDefined(); + expect(old.getPropertyOfType(type, "after")).toBeUndefined(); + h.paths.set(h.path("dependency.ts"), "export const value = { after: 2 };"); + const second = h.session.updateSnapshot({ + fileChanges: { changed: [h.path("dependency.ts")] }, + }); + const nextProject = second.getProjects()[0]!; + const nextRoot = nextProject.program.getSourceFile(h.path("main.ts"))!; + expect(nextRoot).toBe(root); + const next = new CheckerFacade(nextProject.checker); + const nextType = next.getTypeAtLocation(declaration(nextRoot, "result").name!); + expect(next.getPropertyOfType(nextType, "before")).toBeUndefined(); + expect(next.getPropertyOfType(nextType, "after")).toBeDefined(); + expect(old.getPropertyOfType(type, "before")).toBeDefined(); + expect(old.getPropertyOfType(type, "after")).toBeUndefined(); + } finally { + h.close(); + } + }); test("member type batches are bounded, reuse warm entries, and isolate absent or panicking fields", () => { const calls: number[][] = []; diff --git a/packages/compiler/src/frontend/ts7/rpc-filesystem.test.ts b/packages/compiler/src/frontend/ts7/rpc-filesystem.test.ts index 858a5a9e5..b63a66cb1 100644 --- a/packages/compiler/src/frontend/ts7/rpc-filesystem.test.ts +++ b/packages/compiler/src/frontend/ts7/rpc-filesystem.test.ts @@ -15,7 +15,12 @@ const fallback: Ts7FileSystem = { getAccessibleEntries: () => undefined, }; -function callback(name: string, fs: Ts7FileSystem, payload = JSON.stringify("/file.ts")) { +function callback( + name: string, + fs: Ts7FileSystem, + payload = JSON.stringify("/file.ts"), + ts71 = false, +) { const incoming: number[] = []; const encoded = new Ts7Wire({ read: () => 0, @@ -47,7 +52,7 @@ function callback(name: string, fs: Ts7FileSystem, payload = JSON.stringify("/fi }, }), ); - registerTs7FileSystem(client, fs); + registerTs7FileSystem(client, fs, () => ts71); let error: unknown; try { client.requestText("query", "null"); @@ -88,6 +93,36 @@ test.each(TS7_FILE_SYSTEM_CALLBACKS.split(","))("%s delegates undefined to the s expect(callback(name, fallback)).toEqual({ kind: 2, text: "", error: undefined, closed: false }); }); +test("TypeScript 7.1 replies carry the same answers as value, missing and useOS", () => { + const reply = (name: string, fs: Ts7FileSystem) => + JSON.parse(callback(name, fs, JSON.stringify("/file.ts"), true).text) as unknown; + expect(reply("readFile", { ...fallback, readFile: () => "text" })).toEqual({ + kind: "value", + value: "text", + }); + expect(reply("readFile", { ...fallback, readFile: () => "" })).toEqual({ + kind: "value", + value: "", + }); + expect(reply("readFile", { ...fallback, readFile: () => null })).toEqual({ kind: "missing" }); + for (const name of TS7_FILE_SYSTEM_CALLBACKS.split(",")) + expect(reply(name, fallback), name).toEqual({ kind: "useOS" }); + expect(reply("fileExists", { ...fallback, fileExists: () => false })).toEqual({ + kind: "value", + value: false, + }); + expect(reply("realpath", { ...fallback, realpath: () => "/real.ts" })).toEqual({ + kind: "value", + value: "/real.ts", + }); + expect( + reply("getAccessibleEntries", { + ...fallback, + getAccessibleEntries: () => ({ files: ["a.ts"], directories: [] }), + }), + ).toEqual({ kind: "value", value: { files: ["a.ts"], directories: [] } }); +}); + test("boolean false and empty directory entries do not fall through", () => { const fs = { ...fallback, diff --git a/packages/compiler/src/frontend/ts7/rpc-filesystem.ts b/packages/compiler/src/frontend/ts7/rpc-filesystem.ts index 601e4473c..f1fcf2265 100644 --- a/packages/compiler/src/frontend/ts7/rpc-filesystem.ts +++ b/packages/compiler/src/frontend/ts7/rpc-filesystem.ts @@ -18,31 +18,50 @@ function pathArgument(payload: string): string { return value; } +/** A TypeScript 7.1 callback reply: the value, `missing` for a path the + * client hides, or `useOS` to delegate to the server's file system. */ +function reply71(result: unknown): string { + if (result === undefined) return '{"kind":"useOS"}'; + if (result === null) return '{"kind":"missing"}'; + return JSON.stringify({ kind: "value", value: result }); +} + /** Preserve all three readFile answers. An empty reply delegates to the * native server's filesystem; {content:null} hides a path; {content:""} - * supplies a present empty file. Conflating these changes module resolution. */ -export function registerTs7FileSystem(client: Ts7RpcClient, fs: Ts7FileSystem): void { + * supplies a present empty file. Conflating these changes module resolution. + * `ts71` tells whether the server speaks TypeScript 7.1's protocol, whose + * replies carry the same three answers as reply71 encodes them. */ +export function registerTs7FileSystem( + client: Ts7RpcClient, + fs: Ts7FileSystem, + ts71: () => boolean = () => false, +): void { client.registerCallback("readFile", (payload) => { const content = fs.readFile(pathArgument(payload)); + if (ts71()) return reply71(content); return content === undefined ? "" : JSON.stringify({ content }); }); client.registerCallback("fileExists", (payload) => { const result = fs.fileExists(pathArgument(payload)); + if (ts71()) return reply71(result); if (result === undefined) return ""; return result ? "true" : "false"; }); client.registerCallback("directoryExists", (payload) => { const result = fs.directoryExists(pathArgument(payload)); + if (ts71()) return reply71(result); if (result === undefined) return ""; return result ? "true" : "false"; }); client.registerCallback("realpath", (payload) => { const result = fs.realpath(pathArgument(payload)); + if (ts71()) return reply71(result); if (result === undefined) return ""; return JSON.stringify(result); }); client.registerCallback("getAccessibleEntries", (payload) => { const result = fs.getAccessibleEntries(pathArgument(payload)); + if (ts71()) return reply71(result); if (result === undefined) return ""; return JSON.stringify(result!); }); diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts index 90a7a25cf..d6c9b18da 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts @@ -6,9 +6,9 @@ import { API } from "typescript/unstable/sync"; import type { Node as SdkNode } from "typescript/unstable/ast"; import { AstNode } from "./ast-node.js"; import { Ts7RpcClient } from "./rpc-client.js"; -import { tsgoExecutable } from "./rpc-api.js"; import { spawnTs7Wire } from "./rpc-process.js"; import { Ts7Session } from "./session.js"; +import { ts7TestServers } from "./test-servers.js"; import { SyntaxKind } from "./enums.js"; import { getSourceFileOfNode } from "./node-functions.js"; import { forEachChild } from "./ast.js"; @@ -30,7 +30,8 @@ const source = [ let directory: string; let file: string; let config: string; -let session: Ts7Session; +const servers = ts7TestServers(); +const sessions = new Map(); let oracle: API; beforeAll(() => { @@ -47,15 +48,19 @@ beforeAll(() => { files: [file], }), ); - // Compared with the pinned SDK, which runs tsgo. - session = new Ts7Session( - new Ts7RpcClient(spawnTs7Wire(tsgoExecutable(), ["--api", "--cwd", directory])), - ); + // Compared with the pinned SDK, which runs tsgo 7.0; tsc 7.1 answers these cases the same. + for (const server of servers) + sessions.set( + server.name, + new Ts7Session( + new Ts7RpcClient(spawnTs7Wire(server.executable, ["--api", "--cwd", directory])), + ), + ); oracle = new API({ cwd: directory }); }); afterAll(() => { - session?.close(); + for (const session of sessions.values()) session.close(); oracle?.close(); if (directory) rmSync(directory, { recursive: true, force: true }); }); @@ -75,82 +80,86 @@ function shape(node: AstNode | SdkNode): unknown { }; } -test("synthetic type roots and descendants match the pinned SDK without claiming SourceFile fields", () => { - const snapshot = session.updateSnapshot({ openProjects: [config] }); - const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] }); - try { - const project = snapshot.getProject(config)!; - const sdk = sdkSnapshot.getProject(config)!; - const root = project.program.getSourceFile(file)!; - const expectedRoot = sdk.program.getSourceFile(file)!; - let checked = 0; - for (let index = 1; index < 7; index++) { - const statement = root.statements.nodes[index]!; - const expectedStatement = expectedRoot.statements[index]!; - const node = statement.declarationList!.declarations!.nodes[0]!.name!; - const expectedNode = Reflect.get(expectedStatement, "declarationList").declarations[0] - .name as SdkNode; - const type = project.checker.getTypeAtLocation(node)!; - const expectedType = sdk.checker.getTypeAtLocation(expectedNode)!; - const actual = project.checker.typeToTypeNode(type, node)!; - const expected = sdk.checker.typeToTypeNode(expectedType, expectedNode)!; - expect(shape(actual)).toEqual(shape(expected)); - expect(actual.kind).not.toBe(SyntaxKind.SourceFile); - expect(actual.file.root).toBe(actual); - expect(actual.file.node(1)).toBe(actual); - expect(actual.parent).toBeUndefined(); - expect(() => actual.file.sourceFile).toThrow("expected a source file root"); - expect(() => getSourceFileOfNode(actual)).toThrow("expected a source file root"); - forEachChild(actual, (child) => { - expect(child.parent).toBe(actual); - }); - checked++; - } - expect(checked).toBe(6); - expect(root.file.sourceFile).toBe(root); - expect(getSourceFileOfNode(root)).toBe(root); - } finally { - snapshot.dispose(); - sdkSnapshot.dispose(); - } -}); - -test("generic, predicate, rest and this-parameter signature fragments match the pinned SDK", () => { - const snapshot = session.updateSnapshot({ openProjects: [config] }); - const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] }); - try { - const project = snapshot.getProject(config)!; - const sdk = sdkSnapshot.getProject(config)!; - const root = project.program.getSourceFile(file)!; - const expectedRoot = sdk.program.getSourceFile(file)!; - for (let index = 7; index < 11; index++) { - const node = root.statements.nodes[index]!; - const expectedNode = expectedRoot.statements[index]!; - const signature = project.checker.getSignatureFromDeclaration(node)!; - const expectedSignature = sdk.checker.getSignatureFromDeclaration(expectedNode)!; - const actual = project.checker.signatureToSignatureDeclaration( - signature, - SyntaxKind.FunctionType, - node, - )!; - const expected = sdk.checker.signatureToSignatureDeclaration( - expectedSignature, - SyntaxKind.FunctionType, - expectedNode, - )!; - expect(actual.kind).toBe(SyntaxKind.FunctionType); - expect(shape(actual)).toEqual(shape(expected)); - expect(actual.file.root).toBe(actual); - const parameters = actual.parameters!.nodes; - expect(parameters.length).toBeGreaterThan(0); - for (const parameter of parameters) { - expect(parameter.parent).toBe(actual); - expect(actual.file.node(parameter.index)).toBe(parameter); +for (const server of servers) + test(`synthetic type roots and descendants match the pinned SDK without claiming SourceFile fields (${server.name})`, () => { + const session = sessions.get(server.name)!; + const snapshot = session.updateSnapshot({ openProjects: [config] }); + const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] }); + try { + const project = snapshot.getProject(config)!; + const sdk = sdkSnapshot.getProject(config)!; + const root = project.program.getSourceFile(file)!; + const expectedRoot = sdk.program.getSourceFile(file)!; + let checked = 0; + for (let index = 1; index < 7; index++) { + const statement = root.statements.nodes[index]!; + const expectedStatement = expectedRoot.statements[index]!; + const node = statement.declarationList!.declarations!.nodes[0]!.name!; + const expectedNode = Reflect.get(expectedStatement, "declarationList").declarations[0] + .name as SdkNode; + const type = project.checker.getTypeAtLocation(node)!; + const expectedType = sdk.checker.getTypeAtLocation(expectedNode)!; + const actual = project.checker.typeToTypeNode(type, node)!; + const expected = sdk.checker.typeToTypeNode(expectedType, expectedNode)!; + expect(shape(actual)).toEqual(shape(expected)); + expect(actual.kind).not.toBe(SyntaxKind.SourceFile); + expect(actual.file.root).toBe(actual); + expect(actual.file.node(1)).toBe(actual); + expect(actual.parent).toBeUndefined(); + expect(() => actual.file.sourceFile).toThrow("expected a source file root"); + expect(() => getSourceFileOfNode(actual)).toThrow("expected a source file root"); + forEachChild(actual, (child) => { + expect(child.parent).toBe(actual); + }); + checked++; } - expect(() => actual.file.sourceFile).toThrow("expected a source file root"); + expect(checked).toBe(6); + expect(root.file.sourceFile).toBe(root); + expect(getSourceFileOfNode(root)).toBe(root); + } finally { + snapshot.dispose(); + sdkSnapshot.dispose(); } - } finally { - snapshot.dispose(); - sdkSnapshot.dispose(); - } -}); + }); + +for (const server of servers) + test(`generic, predicate, rest and this-parameter signature fragments match the pinned SDK (${server.name})`, () => { + const session = sessions.get(server.name)!; + const snapshot = session.updateSnapshot({ openProjects: [config] }); + const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] }); + try { + const project = snapshot.getProject(config)!; + const sdk = sdkSnapshot.getProject(config)!; + const root = project.program.getSourceFile(file)!; + const expectedRoot = sdk.program.getSourceFile(file)!; + for (let index = 7; index < 11; index++) { + const node = root.statements.nodes[index]!; + const expectedNode = expectedRoot.statements[index]!; + const signature = project.checker.getSignatureFromDeclaration(node)!; + const expectedSignature = sdk.checker.getSignatureFromDeclaration(expectedNode)!; + const actual = project.checker.signatureToSignatureDeclaration( + signature, + SyntaxKind.FunctionType, + node, + )!; + const expected = sdk.checker.signatureToSignatureDeclaration( + expectedSignature, + SyntaxKind.FunctionType, + expectedNode, + )!; + expect(actual.kind).toBe(SyntaxKind.FunctionType); + expect(shape(actual)).toEqual(shape(expected)); + expect(actual.file.root).toBe(actual); + const parameters = actual.parameters!.nodes; + expect(parameters.length).toBeGreaterThan(0); + for (const parameter of parameters) { + expect(parameter.parent).toBe(actual); + expect(actual.file.node(parameter.index)).toBe(parameter); + } + expect(() => actual.file.sourceFile).toThrow("expected a source file root"); + } + } finally { + snapshot.dispose(); + sdkSnapshot.dispose(); + } + }); diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.ts b/packages/compiler/src/frontend/ts7/semantic-checker.ts index c19f93598..e60f9a5a3 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.ts @@ -12,6 +12,8 @@ import { SemanticType, type SemanticDocument, type SemanticQuery, + type Ts71SymbolData, + type Ts71TypeData, present, } from "./semantic-model.js"; import { SemanticSymbolFlags } from "./semantic-schema.generated.js"; @@ -72,7 +74,8 @@ export interface SemanticCompletionOptions { triggerCharacter?: string; includeSymbol?: boolean; } -interface CompletionResponse { +/** `S` is TypeScript 7.0's SymbolResponse or 7.1's Ts71SymbolData. */ +interface CompletionResponse { isIncomplete: boolean; entries: { name: string; @@ -82,9 +85,29 @@ interface CompletionResponse { filterText?: string; detail?: string; labelDetails?: { detail?: string; description?: string }; - symbol?: SymbolResponse; + symbol?: S; }[]; } + +/** TypeScript 7.1's type predicate and index info replies. */ +interface TypePredicateResponse71 { + kind: number; + parameterIndex: number; + parameterName?: string; + type?: Ts71TypeData; +} +interface IndexInfoResponse71 { + keyType: Ts71TypeData; + valueType: Ts71TypeData; + isReadonly?: boolean; + declaration?: string; +} + +interface ReferencedSymbolResponse { + definition: string; + symbol?: S; + references?: string[]; +} export interface SemanticCompletion { name: string; kind: number | undefined; @@ -369,6 +392,7 @@ export class SemanticChecker { return this.project.request("isArrayLikeType", { type: type.id }); } isTupleType(type: SemanticType): boolean { + if (this.project.ts71()) return this.project.isTupleType71(type.id); return this.project.request("isTupleType", { type: type.id }); } isTypeAssignableTo(source: SemanticType, target: SemanticType): boolean { @@ -390,6 +414,20 @@ export class SemanticChecker { ); } getTypePredicateOfSignature(signature: SemanticSignature): SemanticTypePredicate | undefined { + if (this.project.ts71()) { + const data = this.project.request( + "getTypePredicateOfSignature", + { signature: signature.id }, + ); + return data === null + ? undefined + : { + kind: data.kind, + parameterIndex: data.parameterIndex, + parameterName: data.parameterName, + type: data.type === undefined ? undefined : this.project.type71(data.type), + }; + } const data = this.project.request("getTypePredicateOfSignature", { signature: signature.id, }); @@ -403,6 +441,22 @@ export class SemanticChecker { }; } getIndexInfosOfType(type: SemanticType): SemanticIndexInfo[] { + if (this.project.ts71()) { + const data = this.project.request("getIndexInfosOfType", { + type: type.id, + }); + return (data ?? []).map( + (item) => + new SemanticIndexInfo( + this.project.type71(item.keyType), + this.project.type71(item.valueType), + item.isReadonly ?? false, + item.declaration === undefined + ? undefined + : new SemanticNodeHandle(item.declaration, this.project), + ), + ); + } const data = this.project.request("getIndexInfosOfType", { type: type.id, }); @@ -420,8 +474,14 @@ export class SemanticChecker { } private getWellKnownSymbols(): WellKnownSymbols { this.project.ensureActive(); - if (this.wellKnown === undefined) - this.wellKnown = this.project.request("getWellKnownSymbols"); + if (this.wellKnown === undefined) { + const ids = this.project.request("getWellKnownSymbols"); + this.wellKnown = { + unknown: this.project.snapshotSymbolId(ids.unknown), + undefined: this.project.snapshotSymbolId(ids.undefined), + arguments: this.project.snapshotSymbolId(ids.arguments), + }; + } return this.wellKnown; } isUnknownSymbol(symbol: SemanticSymbol): boolean { @@ -445,9 +505,24 @@ export class SemanticChecker { return (data ?? []).map((handle) => new SemanticNodeHandle(handle, this.project)); } getReferencedSymbolsForNode(node: AstNode, position: number): SemanticReferencedSymbol[] { - const data = this.project.request< - { definition: string; symbol?: SymbolResponse; references?: string[] }[] | null - >("getReferencedSymbolsForNode", { node: node.id, position }); + const query = { node: node.id, position }; + if (this.project.ts71()) { + const data = this.project.request[] | null>( + "getReferencedSymbolsForNode", + query, + ); + return (data ?? []).map((item) => ({ + definition: new SemanticNodeHandle(item.definition, this.project), + symbol: item.symbol === undefined ? undefined : this.project.symbol71(item.symbol), + references: (item.references ?? []).map( + (handle) => new SemanticNodeHandle(handle, this.project), + ), + })); + } + const data = this.project.request[] | null>( + "getReferencedSymbolsForNode", + query, + ); return (data ?? []).map((item) => ({ definition: new SemanticNodeHandle(item.definition, this.project), symbol: item.symbol === undefined ? undefined : this.project.symbol(item.symbol), @@ -479,12 +554,36 @@ export class SemanticChecker { position: number, options?: SemanticCompletionOptions, ): { isIncomplete: boolean; entries: SemanticCompletion[] } | undefined { - const data = this.project.request("getCompletionsAtPosition", { + const query = { file, position, triggerCharacter: options?.triggerCharacter, includeSymbol: options?.includeSymbol, - }); + }; + if (this.project.ts71()) { + const data = this.project.request | null>( + "getCompletionsAtPosition", + query, + ); + if (data === null) return undefined; + return { + isIncomplete: data.isIncomplete, + entries: data.entries.map((item) => ({ + name: item.name, + kind: item.kind, + sortText: item.sortText, + insertText: item.insertText, + filterText: item.filterText, + detail: item.detail, + labelDetails: item.labelDetails, + symbol: item.symbol === undefined ? undefined : this.project.symbol71(item.symbol), + })), + }; + } + const data = this.project.request | null>( + "getCompletionsAtPosition", + query, + ); if (data === null) return undefined; return { isIncomplete: data.isIncomplete, diff --git a/packages/compiler/src/frontend/ts7/semantic-json.ts b/packages/compiler/src/frontend/ts7/semantic-json.ts index 8913007d3..ea924a039 100644 --- a/packages/compiler/src/frontend/ts7/semantic-json.ts +++ b/packages/compiler/src/frontend/ts7/semantic-json.ts @@ -149,7 +149,7 @@ function parseJsonSlice(bytes: Uint8Array, start: number, end: number): T { /** One element of a reply: null, or the object with leading id `id`. Objects * the caller already holds under that id are not decoded. */ -function replyElement( +function replyElement( bytes: Uint8Array, start: number, end: number, @@ -172,7 +172,7 @@ function replyElement( * reply is null. Objects the caller already holds under their id, which a * reply repeats whenever it names them, are returned from `cached` without * decoding their text; the rest are decoded and handed to `build`. */ -export function decodeSemanticObject( +export function decodeSemanticObject( bytes: Uint8Array, cached: (id: number) => R | undefined, build: (data: T) => R, @@ -187,7 +187,7 @@ export function decodeSemanticObject( /** A checker reply that is an array of objects (each with a numeric `id`) and * nulls, or null. An empty reply is null. Elements are resolved as in * decodeSemanticObject. */ -export function decodeSemanticObjects( +export function decodeSemanticObjects( bytes: Uint8Array, cached: (id: number) => R | undefined, build: (data: T) => R, diff --git a/packages/compiler/src/frontend/ts7/semantic-model-71.test.ts b/packages/compiler/src/frontend/ts7/semantic-model-71.test.ts new file mode 100644 index 000000000..11d2437eb --- /dev/null +++ b/packages/compiler/src/frontend/ts7/semantic-model-71.test.ts @@ -0,0 +1,114 @@ +import { expect, test } from "vitest"; +import { SemanticChecker } from "./semantic-checker.js"; +import { SemanticSnapshot } from "./semantic-model.js"; +import { SyntaxKind, TypeFlags } from "./enums.js"; + +/** A snapshot on a fake TypeScript 7.1 server: `answer` gets each request's + * method and parsed payload and returns the reply's JSON. */ +function server(answer: (method: string, query: Record) => unknown) { + const requests: { method: string; query: Record }[] = []; + const snapshot = new SemanticSnapshot( + 1, + { + text: (method, payload) => { + const query = JSON.parse(payload) as Record; + requests.push({ method, query }); + return JSON.stringify(answer(method, query) ?? null); + }, + binary(method, payload) { + const text = this.text(method, payload); + // AST replies (typeToTypeNode, signatureToSignatureDeclaration) are empty for none. + return text === "null" ? new Uint8Array() : Buffer.from(text, "utf8"); + }, + }, + true, + ); + const project = snapshot.addProject("project", () => undefined); + return { snapshot, project, checker: new SemanticChecker(project), requests }; +} + +const file = { + fileName: "/main.ts", + path: "/main.ts", + contentHash: "0", + parseOptionsKey: "0", + scriptKind: 3, + nodeId: "77", +}; + +test("TypeScript 7.1 symbol references round-trip through local ids", () => { + const box = { kind: 0, file, id: 1 }; + const parent = { kind: 0, file, id: 2 }; + const { project, checker, requests } = server((method, query) => { + if (method === "getTypeAtPosition") + return { id: 10, flags: TypeFlags.Object, objectFlags: 1, symbol: { id: 1, file: "77" } }; + if (method === "getSymbolOfType") + return { + reference: box, + name: "Box", + flags: 32, + checkFlags: 0, + parent: { id: 2, file: "77" }, + }; + if (method === "getParentOfSymbol") { + expect(query["symbol"]).toEqual(box); + expect(query["objectId"]).toBeUndefined(); + return { reference: parent, name: '"/main"', flags: 512, checkFlags: 0 }; + } + if (method === "getDeclaredTypeOfSymbol") { + expect(query["symbol"]).toEqual(box); + return { id: 11, flags: TypeFlags.Object, isTupleType: true }; + } + if (method === "getExportsOfModule") { + expect(query["symbol"]).toEqual(parent); + return [{ reference: box, name: "Box", flags: 32, checkFlags: 0 }]; + } + throw new Error(`unexpected ${method}`); + }); + const type = checker.getTypeAtPosition("/main.ts", 0)!; + const symbol = type.getSymbol()!; + expect(symbol.escapedName).toBe("Box"); + // The compact reference in the type and the full one in the symbol name one symbol. + expect(project.snapshot.cachedSymbol(symbol.id)).toBe(symbol); + const owner = symbol.getParent()!; + expect(owner.escapedName).toBe('"/main"'); + expect(checker.getExportsOfModule(owner)).toEqual([symbol]); + const declared = checker.getDeclaredTypeOfSymbol(symbol); + expect(checker.isTupleType(declared)).toBe(true); + expect(checker.isTupleType(type)).toBe(false); + expect(requests.map((request) => request.method)).not.toContain("isTupleType"); +}); + +test("TypeScript 7.1 requests name types and signatures as 7.1 does", () => { + const { project, checker, requests } = server((method) => { + if (method === "getApparentType" || method === "getReturnTypeOfSignature") + return { id: 12, flags: TypeFlags.Number }; + if (method === "signatureToSignatureDeclaration") return null; + if (method === "getWellKnownSymbols") return { unknown: 5, undefined: 6, arguments: 7 }; + if (method === "getSymbolAtPosition") + return { + reference: { kind: 1, snapshot: 1, project: "project", id: 6 }, + name: "undefined", + flags: 3, + checkFlags: 0, + }; + throw new Error(`unexpected ${method}`); + }); + const type = project.type({ id: 1, flags: TypeFlags.Object }); + const signature = project.signature({ id: 3, flags: 0 }); + checker.getApparentType(type); + checker.getReturnTypeOfSignature(signature); + checker.signatureToSignatureDeclaration(signature, SyntaxKind.FunctionType); + const queries = new Map(requests.map((request) => [request.method, request.query])); + expect(queries.get("getApparentType")).toMatchObject({ objectId: 1 }); + expect(queries.get("getApparentType")!["type"]).toBeUndefined(); + expect(queries.get("getReturnTypeOfSignature")).toMatchObject({ objectId: 3 }); + // 7.1 numbers every kind from SourceKeyword up one higher. + expect(queries.get("signatureToSignatureDeclaration")).toMatchObject({ + kind: SyntaxKind.FunctionType + 1, + }); + // A well-known symbol id names a symbol of the snapshot. + const undefinedSymbol = checker.getSymbolAtPosition("/main.ts", 0)!; + expect(checker.isUndefinedSymbol(undefinedSymbol)).toBe(true); + expect(checker.isUnknownSymbol(undefinedSymbol)).toBe(false); +}); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index 04fa8e637..fb438a661 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -6,7 +6,7 @@ import type { import { AstNode } from "./ast-node.js"; import type { __String } from "./ast-types.js"; import type * as enums from "./enums.generated.js"; -import { parseAstNodeHandle } from "./ast-wire.js"; +import { parseAstNodeHandle, syntaxKind71 } from "./ast-wire.js"; import type { SemanticChecker, SemanticJsDocTag } from "./semantic-checker.js"; import { decodeSemanticObject, decodeSemanticObjects, parseSemanticJson } from "./semantic-json.js"; import { @@ -44,8 +44,9 @@ export interface SemanticQuery { position?: number | undefined; positions?: readonly number[] | undefined; type?: number | undefined; - symbol?: number | undefined; - symbols?: number[] | undefined; + /** A symbol's id; on a TypeScript 7.1 server, its reference (SemanticProject.encode). */ + symbol?: number | Ts71SymbolReference | undefined; + symbols?: (number | Ts71SymbolReference)[] | undefined; signature?: number | undefined; source?: number | undefined; target?: number | undefined; @@ -61,6 +62,214 @@ export interface SemanticQuery { includeSymbol?: boolean | undefined; } +/** A symbol in TypeScript 7.1's protocol: its owner, a source file (kind 0) + * or the snapshot and project (kind 1), and its id within that owner. */ +export interface Ts71SymbolReference { + kind: number; + file?: Ts71SourceFileDescriptor; + snapshot?: number; + project?: string; + id: number; +} + +export interface Ts71SourceFileDescriptor { + fileName: string; + path: string; + contentHash: string; + parseOptionsKey: string; + scriptKind: number; + nodeId: string; +} + +/** A symbol another TypeScript 7.1 response names: its id and, for a symbol + * a source file owns, the file's node ID. Without a file, the response's + * snapshot and project own it. */ +export interface Ts71CompactReference { + id: number; + file?: string; +} + +/** A symbol as TypeScript 7.1 reports it. */ +export interface Ts71SymbolData { + reference: Ts71SymbolReference; + name: string; + flags: number; + checkFlags: number; + declarations?: string[]; + valueDeclaration?: string; + parent?: Ts71CompactReference; + exportSymbol?: Ts71CompactReference; +} + +/** A type as TypeScript 7.1 reports it: TypeResponse with compact symbol + * references. */ +export interface Ts71TypeData { + id: number; + flags: number; + objectFlags?: number; + value?: string | number | boolean | null; + freshType?: number; + regularType?: number; + target?: number; + typeParameters?: number[]; + outerTypeParameters?: number[]; + localTypeParameters?: number[]; + elementFlags?: number[]; + fixedLength?: number; + readonly?: boolean; + objectType?: number; + indexType?: number; + checkType?: number; + extendsType?: number; + baseType?: number; + substConstraint?: number; + texts?: string[]; + intrinsicName?: string; + isThisType?: boolean; + /** Whether the type is a tuple type, which 7.0 answers with isTupleType. */ + isTupleType?: boolean; + aliasTypeArguments?: number[]; + aliasSymbol?: Ts71CompactReference; + symbol?: Ts71CompactReference; +} + +/** A signature as TypeScript 7.1 reports it. */ +export interface Ts71SignatureData { + id: number; + flags: number; + declaration?: string; + typeParameters?: number[]; + parameters?: Ts71CompactReference[]; + thisParameter?: Ts71CompactReference; + target?: number; +} + +/** The methods whose source is a symbol: TypeScript 7.0 names it by + * `objectId`, 7.1 by its reference in `symbol`. */ +const SYMBOL_PROPERTY_METHODS = new Set([ + "getParentOfSymbol", + "getExportSymbolOfSymbol", + "getMembersOfSymbol", + "getExportsOfSymbol", +]); + +/** The methods TypeScript 7.1 gives their type or signature in `objectId`, + * where 7.0 takes it in `type` or `signature`. */ +const OBJECT_ID_METHODS_7_1 = new Map([ + ["getApparentType", "type"], + ["getConstraintOfTypeParameter", "type"], + ["getNonNullableType", "type"], + ["getReturnTypeOfSignature", "signature"], +]); + +/** TypeScript 7.1's symbol references by local id. The semantic model keeps + * TypeScript 7.0's numeric symbol ids; on a 7.1 server, each symbol owner + * and id gets a local id, and requests send the reference back. A compact + * reference gets the local id of the symbol it names, so a later response + * with that symbol's full reference finds the same id. */ +export class Ts71SymbolTable { + private readonly locals = new Map(); + private readonly references: (Ts71SymbolReference | undefined)[] = [undefined]; + + private allocate(key: string): number { + let local = this.locals.get(key); + if (local === undefined) { + local = this.references.length; + this.references.push(undefined); + this.locals.set(key, local); + } + return local; + } + + /** The local id of `reference`, which requests then name it by. */ + fromReference(reference: Ts71SymbolReference): number { + const key = + reference.kind === 0 + ? `f${reference.file?.nodeId ?? ""}:${reference.id}` + : `s${reference.project ?? ""}:${reference.id}`; + const local = this.allocate(key); + this.references[local] = reference; + return local; + } + + /** The local id of a compact reference in a response for `project`. */ + fromCompact(reference: Ts71CompactReference, project: string): number { + return this.allocate( + reference.file !== undefined + ? `f${reference.file}:${reference.id}` + : `s${project}:${reference.id}`, + ); + } + + /** The reference of local id `local`. Throws for a symbol no response has + * described yet. */ + reference(local: number): Ts71SymbolReference { + const reference = this.references[local]; + if (reference === undefined) throw new Error(`Unknown TypeScript 7.1 symbol ${local}`); + return reference; + } + + /** `data` in TypeScript 7.0's shape, with local ids. */ + symbolData(data: Ts71SymbolData, project: string): SymbolResponse { + const result: SymbolResponse = { + id: this.fromReference(data.reference), + project, + name: data.name, + flags: data.flags, + checkFlags: data.checkFlags, + }; + if (data.declarations !== undefined) result.declarations = data.declarations; + if (data.valueDeclaration !== undefined) result.valueDeclaration = data.valueDeclaration; + if (data.parent !== undefined) result.parent = this.fromCompact(data.parent, project); + if (data.exportSymbol !== undefined) + result.exportSymbol = this.fromCompact(data.exportSymbol, project); + return result; + } + + typeData(data: Ts71TypeData, project: string): TypeResponse { + const result: TypeResponse = { id: data.id, flags: data.flags }; + if (data.objectFlags !== undefined) result.objectFlags = data.objectFlags; + if (data.value !== undefined) result.value = data.value; + if (data.freshType !== undefined) result.freshType = data.freshType; + if (data.regularType !== undefined) result.regularType = data.regularType; + if (data.target !== undefined) result.target = data.target; + if (data.typeParameters !== undefined) result.typeParameters = data.typeParameters; + if (data.outerTypeParameters !== undefined) + result.outerTypeParameters = data.outerTypeParameters; + if (data.localTypeParameters !== undefined) + result.localTypeParameters = data.localTypeParameters; + if (data.elementFlags !== undefined) result.elementFlags = data.elementFlags; + if (data.fixedLength !== undefined) result.fixedLength = data.fixedLength; + if (data.readonly !== undefined) result.readonly = data.readonly; + if (data.objectType !== undefined) result.objectType = data.objectType; + if (data.indexType !== undefined) result.indexType = data.indexType; + if (data.checkType !== undefined) result.checkType = data.checkType; + if (data.extendsType !== undefined) result.extendsType = data.extendsType; + if (data.baseType !== undefined) result.baseType = data.baseType; + if (data.substConstraint !== undefined) result.substConstraint = data.substConstraint; + if (data.texts !== undefined) result.texts = data.texts; + if (data.intrinsicName !== undefined) result.intrinsicName = data.intrinsicName; + if (data.isThisType !== undefined) result.isThisType = data.isThisType; + if (data.aliasTypeArguments !== undefined) result.aliasTypeArguments = data.aliasTypeArguments; + if (data.aliasSymbol !== undefined) + result.aliasSymbol = this.fromCompact(data.aliasSymbol, project); + if (data.symbol !== undefined) result.symbol = this.fromCompact(data.symbol, project); + return result; + } + + signatureData(data: Ts71SignatureData, project: string): SignatureResponse { + const result: SignatureResponse = { id: data.id, flags: data.flags }; + if (data.declaration !== undefined) result.declaration = data.declaration; + if (data.typeParameters !== undefined) result.typeParameters = data.typeParameters; + if (data.parameters !== undefined) + result.parameters = data.parameters.map((parameter) => this.fromCompact(parameter, project)); + if (data.thisParameter !== undefined) + result.thisParameter = this.fromCompact(data.thisParameter, project); + if (data.target !== undefined) result.target = data.target; + return result; + } +} + export class SemanticProgram { constructor(private readonly sourceFile: (path: string) => AstNode | undefined) {} getSourceFile(path: string): AstNode | undefined { @@ -77,10 +286,16 @@ export class SemanticSnapshot { private readonly symbols = new Map(); private disposed = false; + /** TypeScript 7.1's symbol references, on a server that speaks 7.1. */ + readonly references: Ts71SymbolTable | undefined; + constructor( readonly id: number, readonly transport: SemanticTransport, - ) {} + ts71 = false, + ) { + this.references = ts71 ? new Ts71SymbolTable() : undefined; + } addProject(id: string, sourceFile: (path: string) => AstNode | undefined): SemanticProject { this.ensureActive(); @@ -128,6 +343,9 @@ export class SemanticSnapshot { export class SemanticProject { private readonly types = new Map(); + /** TypeScript 7.1's isTupleType answers, by type id: 7.1 reports them on + * the type instead of answering an isTupleType request. */ + private readonly tupleTypes = new Map(); private readonly signatures = new Map(); private readonly cleanups: (() => void)[] = []; private disposed = false; @@ -150,24 +368,90 @@ export class SemanticProject { this.cleanups.push(cleanup); } - request(method: string, query: SemanticQuery = {}): T { + /** `query` for `method` in this project's snapshot. On a TypeScript 7.1 + * server, symbol ids become their references, and a symbol property + * method takes its source symbol's reference in `symbol`. */ + private encode(method: string, query: SemanticQuery): string { this.ensureActive(); query.snapshot = this.snapshot.id; query.project = this.id; - return parseSemanticJson(this.snapshot.transport.text(method, JSON.stringify(query))); + const references = this.snapshot.references; + if (references !== undefined) { + if (typeof query.symbol === "number") query.symbol = references.reference(query.symbol); + if (query.symbols !== undefined) + query.symbols = query.symbols.map((symbol) => + typeof symbol === "number" ? references.reference(symbol) : symbol, + ); + if (SYMBOL_PROPERTY_METHODS.has(method) && query.objectId !== undefined) { + query.symbol = references.reference(query.objectId); + query.objectId = undefined; + } + // signatureToSignatureDeclaration's kind is a SyntaxKind. + if (method === "signatureToSignatureDeclaration" && query.kind !== undefined) + query.kind = syntaxKind71(query.kind); + const field = OBJECT_ID_METHODS_7_1.get(method); + if (field === "type") { + query.objectId = query.type; + query.type = undefined; + } else if (field === "signature") { + query.objectId = query.signature; + query.signature = undefined; + } + } + return JSON.stringify(query); + } + + request(method: string, query: SemanticQuery = {}): T { + return parseSemanticJson(this.snapshot.transport.text(method, this.encode(method, query))); } requestBinary(method: string, query: SemanticQuery): Uint8Array { - this.ensureActive(); - query.snapshot = this.snapshot.id; - query.project = this.id; - return this.snapshot.transport.binary(method, JSON.stringify(query)); + return this.snapshot.transport.binary(method, this.encode(method, query)); + } + + /** A type a TypeScript 7.1 reply holds. */ + type71(data: Ts71TypeData): SemanticType { + const references = this.snapshot.references; + if (references === undefined) throw new Error("TypeScript 7.1 type from a 7.0 server"); + this.tupleTypes.set(data.id, data.isTupleType === true); + return this.type(references.typeData(data, this.id)); + } + + /** Whether a type a TypeScript 7.1 server reported is a tuple type. */ + isTupleType71(type: number): boolean { + const result = this.tupleTypes.get(type); + if (result === undefined) throw new Error(`Unknown TypeScript 7.1 type ${type}`); + return result; + } + + /** Whether the server speaks TypeScript 7.1's protocol. */ + ts71(): boolean { + return this.snapshot.references !== undefined; + } + + /** A symbol a TypeScript 7.1 reply holds. */ + symbol71(data: Ts71SymbolData): SemanticSymbol { + const references = this.snapshot.references; + if (references === undefined) throw new Error("TypeScript 7.1 symbol from a 7.0 server"); + return this.symbol(references.symbolData(data, this.id)); + } + + /** The local id of a symbol id a TypeScript 7.1 server reports as a plain + * number (getWellKnownSymbols): a symbol of this project's snapshot. */ + snapshotSymbolId(id: number): number { + const references = this.snapshot.references; + return references === undefined ? id : references.fromCompact({ id }, this.id); } /** A reply of one type, or undefined for null. */ requestType(method: string, query: SemanticQuery): SemanticType | undefined { const bytes = this.requestBinary(method, query); const cached = (id: number): SemanticType | undefined => this.types.get(id); + const references = this.snapshot.references; + if (references !== undefined) { + const build71 = (data: Ts71TypeData): SemanticType => this.type71(data); + return decodeSemanticObject(bytes, cached, build71) ?? undefined; + } const build = (data: TypeResponse): SemanticType => this.type(data); return decodeSemanticObject(bytes, cached, build) ?? undefined; } @@ -176,6 +460,11 @@ export class SemanticProject { requestTypes(method: string, query: SemanticQuery): (SemanticType | undefined)[] { const bytes = this.requestBinary(method, query); const cached = (id: number): SemanticType | undefined => this.types.get(id); + const references = this.snapshot.references; + if (references !== undefined) { + const build71 = (data: Ts71TypeData): SemanticType => this.type71(data); + return (decodeSemanticObjects(bytes, cached, build71) ?? []).map((item) => item ?? undefined); + } const build = (data: TypeResponse): SemanticType => this.type(data); return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined); } @@ -183,6 +472,14 @@ export class SemanticProject { /** A reply of one symbol, or undefined for null. */ requestSymbol(method: string, query: SemanticQuery): SemanticSymbol | undefined { const bytes = this.requestBinary(method, query); + const references = this.snapshot.references; + if (references !== undefined) { + // TypeScript 7.1's symbols start with their reference, not an id. + const none = (): SemanticSymbol | undefined => undefined; + const build71 = (data: Ts71SymbolData): SemanticSymbol => + this.symbol(references.symbolData(data, this.id)); + return decodeSemanticObject(bytes, none, build71) ?? undefined; + } const cached = (id: number): SemanticSymbol | undefined => this.snapshot.cachedSymbol(id); const build = (data: SymbolResponse): SemanticSymbol => this.symbol(data); return decodeSemanticObject(bytes, cached, build) ?? undefined; @@ -191,6 +488,13 @@ export class SemanticProject { /** A reply of symbols and nulls (undefined here), or [] for null. */ requestSymbols(method: string, query: SemanticQuery): (SemanticSymbol | undefined)[] { const bytes = this.requestBinary(method, query); + const references = this.snapshot.references; + if (references !== undefined) { + const none = (): SemanticSymbol | undefined => undefined; + const build71 = (data: Ts71SymbolData): SemanticSymbol => + this.symbol(references.symbolData(data, this.id)); + return (decodeSemanticObjects(bytes, none, build71) ?? []).map((item) => item ?? undefined); + } const cached = (id: number): SemanticSymbol | undefined => this.snapshot.cachedSymbol(id); const build = (data: SymbolResponse): SemanticSymbol => this.symbol(data); return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined); @@ -200,6 +504,12 @@ export class SemanticProject { requestSignature(method: string, query: SemanticQuery): SemanticSignature | undefined { const bytes = this.requestBinary(method, query); const cached = (id: number): SemanticSignature | undefined => this.signatures.get(id); + const references = this.snapshot.references; + if (references !== undefined) { + const build71 = (data: Ts71SignatureData): SemanticSignature => + this.signature(references.signatureData(data, this.id)); + return decodeSemanticObject(bytes, cached, build71) ?? undefined; + } const build = (data: SignatureResponse): SemanticSignature => this.signature(data); return decodeSemanticObject(bytes, cached, build) ?? undefined; } @@ -208,6 +518,12 @@ export class SemanticProject { requestSignatures(method: string, query: SemanticQuery): (SemanticSignature | undefined)[] { const bytes = this.requestBinary(method, query); const cached = (id: number): SemanticSignature | undefined => this.signatures.get(id); + const references = this.snapshot.references; + if (references !== undefined) { + const build71 = (data: Ts71SignatureData): SemanticSignature => + this.signature(references.signatureData(data, this.id)); + return (decodeSemanticObjects(bytes, cached, build71) ?? []).map((item) => item ?? undefined); + } const build = (data: SignatureResponse): SemanticSignature => this.signature(data); return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined); } diff --git a/packages/compiler/src/frontend/ts7/session.test.ts b/packages/compiler/src/frontend/ts7/session.test.ts index d957fe0dd..5bf50a087 100644 --- a/packages/compiler/src/frontend/ts7/session.test.ts +++ b/packages/compiler/src/frontend/ts7/session.test.ts @@ -11,7 +11,7 @@ import { type Ts7FileSystem, } from "./rpc-filesystem.js"; import { spawnTs7Wire } from "./rpc-process.js"; -import { tsgoExecutable } from "./rpc-api.js"; +import { ts7TestServers, type Ts7TestServer } from "./test-servers.js"; import { Ts7Session, Ts7SessionProject, Ts7SessionSnapshot } from "./session.js"; import { AstNode } from "./ast-node.js"; import { ts7Diagnostic } from "./diagnostic.js"; @@ -27,10 +27,16 @@ const fallback: Ts7FileSystem = { getAccessibleEntries: () => undefined, }; -// These tests compare the session with the pinned SDK, which runs tsgo, so the session runs tsgo too. -function transport(cwd: string, fs: Ts7FileSystem = fallback, timing = false) { +// These tests compare the session with the pinned SDK, which runs tsgo 7.0. The session runs +// tsc 7.0 and tsc 7.1, whose answers to these cases are the same. +function transport( + server: Ts7TestServer, + cwd: string, + fs: Ts7FileSystem = fallback, + timing = false, +) { const rpc = new Ts7RpcClient( - spawnTs7Wire(tsgoExecutable(), [ + spawnTs7Wire(server.executable, [ "--api", "--cwd", cwd, @@ -38,265 +44,291 @@ function transport(cwd: string, fs: Ts7FileSystem = fallback, timing = false) { ...(timing ? ["--timing"] : []), ]), ); - registerTs7FileSystem(rpc, fs); + registerTs7FileSystem(rpc, fs, () => server.ts71); return rpc; } -function connect(cwd: string, fs: Ts7FileSystem = fallback, timing = false) { - return new Ts7Session(transport(cwd, fs, timing), timing); +function connect(server: Ts7TestServer, cwd: string, fs: Ts7FileSystem = fallback, timing = false) { + return new Ts7Session(transport(server, cwd, fs, timing), timing); } -test("owned snapshots preserve SDK options, diagnostics, metadata and default-project lookup", () => { - const dir = mkdtempSync(join(tempRoot, "scriptc-session-parity-")); - const file = protocolPath(join(dir, "main.ts")); - const config = protocolPath(join(dir, "tsconfig.json")); - writeFileSync(file, 'export const answer: number = "wrong";\n'); - writeFileSync( - config, - JSON.stringify({ - compilerOptions: { - strict: true, - target: "esnext", - types: [], - declaration: true, - sourceMap: true, - paths: { "named/*": ["./*", "../*"] }, - }, - files: [file], - }), - ); - const session = connect(dir, fallback, true); - const sdk = new API({ cwd: dir }); - try { - expect(session.parseConfigFile(config)).toEqual(sdk.parseConfigFile(config)); - const snapshot = session.updateSnapshot({ openProjects: [config], openFiles: [file] }); - const oracle = sdk.updateSnapshot({ openProjects: [config], openFiles: [file] }); - expect(snapshot).toBeInstanceOf(Ts7SessionSnapshot); - const project = snapshot.getProject(config)!; - const expected = oracle.getProject(config)!; - expect(project).toBeInstanceOf(Ts7SessionProject); - expect(project.program.getCompilerOptions()).toEqual(expected.program.getCompilerOptions()); - expect(project.rootFiles).toEqual(expected.rootFiles); - expect(snapshot.getDefaultProjectForFile(file)).toBe(project); - expect(snapshot.getProject(protocolPath(join(dir, "./tsconfig.json")))).toBe(project); - expect(project.program.getSourceFileNames()).toEqual(expected.program.getSourceFileNames()); - const source = project.program.getSourceFile(file)!; - expect(source).toBeInstanceOf(AstNode); - expect(source.text).toBe(expected.program.getSourceFile(file)!.text); - const metadata = project.program.getSourceFileMetadata(file)!; - expect(metadata).toEqual(expected.program.getSourceFileMetadata(file)); - expect(project.program.getSourceFileMetadata(file)).toBe(metadata); - const missing = protocolPath(join(dir, "missing.ts")); - expect(project.program.getSourceFile(missing)).toBeUndefined(); - const sourceRequests = session.getTimingInfo().totals.requestCount; - expect(project.program.getSourceFile(file)).toBe(source); - expect(project.program.getSourceFile({ uri: pathToFileURL(join(dir, "main.ts")).href })).toBe( - source, +for (const server of ts7TestServers()) + test(`owned snapshots preserve SDK options, diagnostics, metadata and default-project lookup (${server.name})`, () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-session-parity-")); + const file = protocolPath(join(dir, "main.ts")); + const config = protocolPath(join(dir, "tsconfig.json")); + writeFileSync(file, 'export const answer: number = "wrong";\n'); + writeFileSync( + config, + JSON.stringify({ + compilerOptions: { + strict: true, + target: "esnext", + types: [], + declaration: true, + sourceMap: true, + paths: { "named/*": ["./*", "../*"] }, + }, + files: [file], + }), ); - expect(project.program.getSourceFile(missing)).toBeUndefined(); - expect(session.getTimingInfo().totals.requestCount).toBe(sourceRequests); - expect(project.program.getSourceFileMetadata(missing)).toBeUndefined(); - const before = session.getTimingInfo().totals.requestCount; - expect(project.program.getSourceFileMetadata(missing)).toBeUndefined(); - expect(session.getTimingInfo().totals.requestCount).toBe(before); - for (const name of [ - "getSyntacticDiagnostics", - "getBindDiagnostics", - "getSemanticDiagnostics", - "getSuggestionDiagnostics", - "getDeclarationDiagnostics", - ] as const) { - // scriptc's diagnostics have every member set (ts7Diagnostic); the SDK's leave absent ones out. - expect(project.program[name](file), name).toEqual( - expected.program[name](file).map(ts7Diagnostic), + const session = connect(server, dir, fallback, true); + const sdk = new API({ cwd: dir }); + try { + expect(session.parseConfigFile(config)).toEqual(sdk.parseConfigFile(config)); + const snapshot = session.updateSnapshot({ openProjects: [config], openFiles: [file] }); + const oracle = sdk.updateSnapshot({ openProjects: [config], openFiles: [file] }); + expect(snapshot).toBeInstanceOf(Ts7SessionSnapshot); + const project = snapshot.getProject(config)!; + const expected = oracle.getProject(config)!; + expect(project).toBeInstanceOf(Ts7SessionProject); + expect(project.program.getCompilerOptions()).toEqual(expected.program.getCompilerOptions()); + expect(project.rootFiles).toEqual(expected.rootFiles); + expect(snapshot.getDefaultProjectForFile(file)).toBe(project); + expect(snapshot.getProject(protocolPath(join(dir, "./tsconfig.json")))).toBe(project); + const names = project.program.getSourceFileNames(); + const expectedNames = expected.program.getSourceFileNames(); + if (server.ts71) { + // tsc 7.1 reads its own lib files, which split some esnext libs into es2026 ones. + const isLib = (name: string) => /\/lib\.[^/]*\.d\.ts$/.test(name); + expect(names.filter((name) => !isLib(name))).toEqual( + expectedNames.filter((name) => !isLib(name)), + ); + for (const lib of ["lib.es5.d.ts", "lib.esnext.d.ts"]) + expect( + names.some((name) => name.endsWith(`/${lib}`)), + lib, + ).toBe(true); + } else { + expect(names).toEqual(expectedNames); + } + const source = project.program.getSourceFile(file)!; + expect(source).toBeInstanceOf(AstNode); + expect(source.text).toBe(expected.program.getSourceFile(file)!.text); + const metadata = project.program.getSourceFileMetadata(file)!; + expect(metadata).toEqual(expected.program.getSourceFileMetadata(file)); + expect(project.program.getSourceFileMetadata(file)).toBe(metadata); + const missing = protocolPath(join(dir, "missing.ts")); + expect(project.program.getSourceFile(missing)).toBeUndefined(); + const sourceRequests = session.getTimingInfo().totals.requestCount; + expect(project.program.getSourceFile(file)).toBe(source); + expect(project.program.getSourceFile({ uri: pathToFileURL(join(dir, "main.ts")).href })).toBe( + source, ); - expect(project.program[name](), name).toEqual(expected.program[name]().map(ts7Diagnostic)); - } - for (const name of [ - "getProgramDiagnostics", - "getGlobalDiagnostics", - "getConfigFileParsingDiagnostics", - ] as const) - expect(project.program[name](), name).toEqual(expected.program[name]()); - const library = project.program - .getSourceFileNames() - .find((name) => name.endsWith("lib.esnext.d.ts"))!; - expect(library).toBeDefined(); - expect( - project.program.isSourceFileDefaultLibrary(project.program.getSourceFile(library)!), - ).toBe(true); - expect(project.program.isSourceFileFromExternalLibrary(source)).toBe(false); - snapshot.dispose(); - expect(snapshot.isDisposed()).toBe(true); - expect(() => snapshot.getProjects()).toThrow("disposed"); - expect(() => project.program.getSourceFileNames()).toThrow("disposed"); - } finally { - session.close(); - sdk.close(); - rmSync(dir, { recursive: true, force: true }); - } -}); - -test("virtual updates share active ASTs and refetch after releasing their baseline", () => { - const dir = mkdtempSync(join(tempRoot, "scriptc-session-updates-")); - const a = protocolPath(join(dir, "a.json")), - b = protocolPath(join(dir, "b.json")); - const changed = protocolPath(join(dir, "changed.ts")), - stable = protocolPath(join(dir, "stable.ts")); - let content = "export const value = 1;"; - let option = "esnext"; - const overlay: Ts7FileSystem = { - ...fallback, - readFile: (file) => { - if (file === a || file === b) - return JSON.stringify({ - compilerOptions: { strict: true, target: option, types: [] }, - files: [changed, stable], - }); - if (file === changed) return content; - if (file === stable) return "export const stable = true;"; - return undefined; - }, - fileExists: (file) => ([a, b, changed, stable].includes(file) ? true : undefined), - }; - const session = connect(dir, overlay, true); - const sdk = new API({ cwd: dir, fs: overlay }); - try { - const first = session.updateSnapshot({ openProjects: [a, b] }); - const oracleFirst = sdk.updateSnapshot({ openProjects: [a, b] }); - const oracleStable = oracleFirst.getProject(a)!.program.getSourceFile(stable)!; - const old = first.getProject(a)!.program.getSourceFile(changed)!; - const retained = first.getProject(a)!.program.getSourceFile(stable)!; - expect(first.getProject(b)!.program.getSourceFile(changed)).toBe(old); - content = 'export const value: number = "wrong";'; - const second = session.updateSnapshot({ fileChanges: { changed: [changed] } }); - const oracleSecond = sdk.updateSnapshot({ fileChanges: { changed: [changed] } }); - const next = second.getProject(a)!.program.getSourceFile(changed)!; - expect(next).not.toBe(old); - expect(old.text).toContain("= 1"); - expect(next.text).toBe(content); - expect( - second - .getProject(a)! - .program.getSemanticDiagnostics(changed) - .map((d) => d.code), - ).toEqual([2322]); - expect(first.getProject(a)!.program.getSemanticDiagnostics(changed)).toEqual([]); - expect(second.getProject(a)!.program.getSourceFile(stable)).toBe(retained); - first.dispose(); - second.dispose(); - oracleFirst.dispose(); - oracleSecond.dispose(); - const before = session.getTimingInfo().totals.sourceFilesFetched; - const third = session.updateSnapshot(); - const oracleThird = sdk.updateSnapshot(); - const refreshed = third.getProject(a)!.program.getSourceFile(stable)!; - expect(refreshed.text).toBe(retained.text); - expect(session.getTimingInfo().totals.sourceFilesFetched).toBe(before + 1); - option = "es2020"; - const fourth = session.updateSnapshot({ fileChanges: { changed: [a, b] } }); - const oracleFourth = sdk.updateSnapshot({ fileChanges: { changed: [a, b] } }); - const reparsed = fourth.getProject(a)!.program.getSourceFile(stable)!; - expect(reparsed === refreshed).toBe( - oracleFourth.getProject(a)!.program.getSourceFile(stable) === oracleStable, - ); - expect(fourth.getProject(a)!.compilerOptions.target).toBe( - oracleFourth.getProject(a)!.compilerOptions.target, - ); - expect(fourth.getProject(a)!.compilerOptions.target).not.toBe( - third.getProject(a)!.compilerOptions.target, - ); - expect(oracleThird.getProject(a)!.program.getSourceFile(stable)).toBe(oracleStable); - const closed = session.updateSnapshot({ closeProjects: [b] }); - expect(closed.getProject(b)).toBeUndefined(); - expect(closed.getProject(a)).toBeDefined(); - session.close(); - expect(third.isDisposed() && fourth.isDisposed() && closed.isDisposed()).toBe(true); - expect(session.cache.size).toBe(0); - expect(() => session.updateSnapshot()).toThrow("closed"); - expect(() => session.parseConfigFile(a)).toThrow("closed"); - expect(() => session.getTimingInfo()).toThrow("closed"); - } finally { - session.close(); - sdk.close(); - rmSync(dir, { recursive: true, force: true }); - } -}); - -test("transport failure during close seals every snapshot and clears retained ASTs", () => { - const dir = mkdtempSync(join(tempRoot, "scriptc-session-close-")); - const file = protocolPath(join(dir, "main.ts")); - const config = protocolPath(join(dir, "tsconfig.json")); - writeFileSync(file, "export const answer = 42;"); - writeFileSync( - config, - JSON.stringify({ compilerOptions: { strict: true, types: [] }, files: [file] }), - ); - const rpc = transport(dir); - const session = new Ts7Session(rpc); - try { - const first = session.updateSnapshot({ openProjects: [config] }); - const project = first.getProject(config)!; - const source = project.program.getSourceFile(file)!; - const type = project.checker.getTypeAtPosition(file, source.text!.indexOf("answer"))!; - const second = session.updateSnapshot(); - const secondProject = second.getProject(config)!; - expect(secondProject.program.getSourceFile(file)).toBe(source); - const third = session.updateSnapshot(); - expect(session.cache.size).toBe(1); - rpc.close(); - expect(() => session.close()).toThrow("closed"); - for (const snapshot of [first, second, third]) { + expect(project.program.getSourceFile(missing)).toBeUndefined(); + expect(session.getTimingInfo().totals.requestCount).toBe(sourceRequests); + expect(project.program.getSourceFileMetadata(missing)).toBeUndefined(); + const before = session.getTimingInfo().totals.requestCount; + expect(project.program.getSourceFileMetadata(missing)).toBeUndefined(); + expect(session.getTimingInfo().totals.requestCount).toBe(before); + for (const name of [ + "getSyntacticDiagnostics", + "getBindDiagnostics", + "getSemanticDiagnostics", + "getSuggestionDiagnostics", + "getDeclarationDiagnostics", + ] as const) { + // scriptc's diagnostics have every member set (ts7Diagnostic); the SDK's leave absent ones out. + expect(project.program[name](file), name).toEqual( + expected.program[name](file).map(ts7Diagnostic), + ); + // tsc 7.1's lib files differ from 7.0's, and so do their diagnostics. + const own = (list: T[]) => + server.ts71 + ? list.filter((item) => !/\/lib\.[^/]*\.d\.ts$/.test(item.fileName ?? "")) + : list; + expect(own(project.program[name]()), name).toEqual( + own(expected.program[name]().map(ts7Diagnostic)), + ); + } + for (const name of [ + "getProgramDiagnostics", + "getGlobalDiagnostics", + "getConfigFileParsingDiagnostics", + ] as const) + expect(project.program[name](), name).toEqual(expected.program[name]()); + const library = project.program + .getSourceFileNames() + .find((name) => name.endsWith("lib.esnext.d.ts"))!; + expect(library).toBeDefined(); + expect( + project.program.isSourceFileDefaultLibrary(project.program.getSourceFile(library)!), + ).toBe(true); + expect(project.program.isSourceFileFromExternalLibrary(source)).toBe(false); + snapshot.dispose(); expect(snapshot.isDisposed()).toBe(true); - expect(() => snapshot.getProjects()).toThrow("closed"); - expect(() => snapshot.dispose()).not.toThrow(); + expect(() => snapshot.getProjects()).toThrow("disposed"); + expect(() => project.program.getSourceFileNames()).toThrow("disposed"); + } finally { + session.close(); + sdk.close(); + rmSync(dir, { recursive: true, force: true }); } - expect(() => secondProject.checker.getNumberType()).toThrow("disposed"); - expect(() => secondProject.program.getSourceFile(file)).toThrow("closed"); - expect(() => type.getRegularType()).toThrow("disposed"); - expect(session.cache.size).toBe(0); - expect(() => session.close()).not.toThrow(); - } finally { - session.close(); - rmSync(dir, { recursive: true, force: true }); - } -}); + }); -test("a released latest snapshot cannot lend stale ASTs to the same project", () => { - const dir = mkdtempSync(join(tempRoot, "scriptc-session-released-")); - const file = protocolPath(join(dir, "main.ts")); - const config = protocolPath(join(dir, "tsconfig.json")); - let source = "export const value = 1;"; - const overlay: Ts7FileSystem = { - ...fallback, - readFile: (path) => - path === file - ? source - : path === config - ? JSON.stringify({ compilerOptions: { noLib: true, types: [] }, files: [file] }) - : null, - fileExists: (path) => path === file || path === config, - }; - const session = connect(dir, overlay); - try { - const first = session.updateSnapshot({ openProjects: [config] }); - const old = first.getProject(config)!.program.getSourceFile(file)!; - first.dispose(); - source = 'export const replacement = "new";'; - const second = session.updateSnapshot({ fileChanges: { changed: [file] } }); - const current = second.getProject(config)!.program.getSourceFile(file)!; - expect(current.text).toBe(source); - expect(current).not.toBe(old); - expect(old.text).toBe("export const value = 1;"); - second.dispose(); - const deleted = session.updateSnapshot({ fileChanges: { deleted: [file] } }); - deleted.dispose(); - source = "export const third = true;"; - const third = session.updateSnapshot({ fileChanges: { created: [file] } }); - expect(third.getProject(config)!.program.getSourceFile(file)!.text).toBe(source); - expect(getTextOfNode(old.statements.nodes[0]!)).toBe("export const value = 1;"); - } finally { - session.close(); - rmSync(dir, { recursive: true, force: true }); - } -}); +for (const server of ts7TestServers()) + test(`virtual updates share active ASTs and refetch after releasing their baseline (${server.name})`, () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-session-updates-")); + const a = protocolPath(join(dir, "a.json")), + b = protocolPath(join(dir, "b.json")); + const changed = protocolPath(join(dir, "changed.ts")), + stable = protocolPath(join(dir, "stable.ts")); + let content = "export const value = 1;"; + let option = "esnext"; + const overlay: Ts7FileSystem = { + ...fallback, + readFile: (file) => { + if (file === a || file === b) + return JSON.stringify({ + compilerOptions: { strict: true, target: option, types: [] }, + files: [changed, stable], + }); + if (file === changed) return content; + if (file === stable) return "export const stable = true;"; + return undefined; + }, + fileExists: (file) => ([a, b, changed, stable].includes(file) ? true : undefined), + }; + const session = connect(server, dir, overlay, true); + const sdk = new API({ cwd: dir, fs: overlay }); + try { + const first = session.updateSnapshot({ openProjects: [a, b] }); + const oracleFirst = sdk.updateSnapshot({ openProjects: [a, b] }); + const oracleStable = oracleFirst.getProject(a)!.program.getSourceFile(stable)!; + const old = first.getProject(a)!.program.getSourceFile(changed)!; + const retained = first.getProject(a)!.program.getSourceFile(stable)!; + expect(first.getProject(b)!.program.getSourceFile(changed)).toBe(old); + content = 'export const value: number = "wrong";'; + const second = session.updateSnapshot({ fileChanges: { changed: [changed] } }); + const oracleSecond = sdk.updateSnapshot({ fileChanges: { changed: [changed] } }); + const next = second.getProject(a)!.program.getSourceFile(changed)!; + expect(next).not.toBe(old); + expect(old.text).toContain("= 1"); + expect(next.text).toBe(content); + expect( + second + .getProject(a)! + .program.getSemanticDiagnostics(changed) + .map((d) => d.code), + ).toEqual([2322]); + expect(first.getProject(a)!.program.getSemanticDiagnostics(changed)).toEqual([]); + expect(second.getProject(a)!.program.getSourceFile(stable)).toBe(retained); + first.dispose(); + second.dispose(); + oracleFirst.dispose(); + oracleSecond.dispose(); + const before = session.getTimingInfo().totals.sourceFilesFetched; + const third = session.updateSnapshot(); + const oracleThird = sdk.updateSnapshot(); + const refreshed = third.getProject(a)!.program.getSourceFile(stable)!; + expect(refreshed.text).toBe(retained.text); + expect(session.getTimingInfo().totals.sourceFilesFetched).toBe(before + 1); + option = "es2020"; + const fourth = session.updateSnapshot({ fileChanges: { changed: [a, b] } }); + const oracleFourth = sdk.updateSnapshot({ fileChanges: { changed: [a, b] } }); + const reparsed = fourth.getProject(a)!.program.getSourceFile(stable)!; + expect(reparsed === refreshed).toBe( + oracleFourth.getProject(a)!.program.getSourceFile(stable) === oracleStable, + ); + expect(fourth.getProject(a)!.compilerOptions.target).toBe( + oracleFourth.getProject(a)!.compilerOptions.target, + ); + expect(fourth.getProject(a)!.compilerOptions.target).not.toBe( + third.getProject(a)!.compilerOptions.target, + ); + expect(oracleThird.getProject(a)!.program.getSourceFile(stable)).toBe(oracleStable); + const closed = session.updateSnapshot({ closeProjects: [b] }); + expect(closed.getProject(b)).toBeUndefined(); + expect(closed.getProject(a)).toBeDefined(); + session.close(); + expect(third.isDisposed() && fourth.isDisposed() && closed.isDisposed()).toBe(true); + expect(session.cache.size).toBe(0); + expect(() => session.updateSnapshot()).toThrow("closed"); + expect(() => session.parseConfigFile(a)).toThrow("closed"); + expect(() => session.getTimingInfo()).toThrow("closed"); + } finally { + session.close(); + sdk.close(); + rmSync(dir, { recursive: true, force: true }); + } + }); + +for (const server of ts7TestServers()) + test(`transport failure during close seals every snapshot and clears retained ASTs (${server.name})`, () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-session-close-")); + const file = protocolPath(join(dir, "main.ts")); + const config = protocolPath(join(dir, "tsconfig.json")); + writeFileSync(file, "export const answer = 42;"); + writeFileSync( + config, + JSON.stringify({ compilerOptions: { strict: true, types: [] }, files: [file] }), + ); + const rpc = transport(server, dir); + const session = new Ts7Session(rpc); + try { + const first = session.updateSnapshot({ openProjects: [config] }); + const project = first.getProject(config)!; + const source = project.program.getSourceFile(file)!; + const type = project.checker.getTypeAtPosition(file, source.text!.indexOf("answer"))!; + const second = session.updateSnapshot(); + const secondProject = second.getProject(config)!; + expect(secondProject.program.getSourceFile(file)).toBe(source); + const third = session.updateSnapshot(); + expect(session.cache.size).toBe(1); + rpc.close(); + expect(() => session.close()).toThrow("closed"); + for (const snapshot of [first, second, third]) { + expect(snapshot.isDisposed()).toBe(true); + expect(() => snapshot.getProjects()).toThrow("closed"); + expect(() => snapshot.dispose()).not.toThrow(); + } + expect(() => secondProject.checker.getNumberType()).toThrow("disposed"); + expect(() => secondProject.program.getSourceFile(file)).toThrow("closed"); + expect(() => type.getRegularType()).toThrow("disposed"); + expect(session.cache.size).toBe(0); + expect(() => session.close()).not.toThrow(); + } finally { + session.close(); + rmSync(dir, { recursive: true, force: true }); + } + }); + +for (const server of ts7TestServers()) + test(`a released latest snapshot cannot lend stale ASTs to the same project (${server.name})`, () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-session-released-")); + const file = protocolPath(join(dir, "main.ts")); + const config = protocolPath(join(dir, "tsconfig.json")); + let source = "export const value = 1;"; + const overlay: Ts7FileSystem = { + ...fallback, + readFile: (path) => + path === file + ? source + : path === config + ? JSON.stringify({ compilerOptions: { noLib: true, types: [] }, files: [file] }) + : null, + fileExists: (path) => path === file || path === config, + }; + const session = connect(server, dir, overlay); + try { + const first = session.updateSnapshot({ openProjects: [config] }); + const old = first.getProject(config)!.program.getSourceFile(file)!; + first.dispose(); + source = 'export const replacement = "new";'; + const second = session.updateSnapshot({ fileChanges: { changed: [file] } }); + const current = second.getProject(config)!.program.getSourceFile(file)!; + expect(current.text).toBe(source); + expect(current).not.toBe(old); + expect(old.text).toBe("export const value = 1;"); + second.dispose(); + const deleted = session.updateSnapshot({ fileChanges: { deleted: [file] } }); + deleted.dispose(); + source = "export const third = true;"; + const third = session.updateSnapshot({ fileChanges: { created: [file] } }); + expect(third.getProject(config)!.program.getSourceFile(file)!.text).toBe(source); + expect(getTextOfNode(old.statements.nodes[0]!)).toBe("export const value = 1;"); + } finally { + session.close(); + rmSync(dir, { recursive: true, force: true }); + } + }); diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index 082bb79a6..9223c897f 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -12,7 +12,7 @@ import type { Ts7CompilerOptionsData, Ts7ConfigData, Ts7DiagnosticData, - Ts7InitializeData, + Ts7ProjectChangeData, Ts7ProjectData, Ts7SnapshotData, Ts7SourceMetadata, @@ -36,6 +36,52 @@ interface Ts7ReportedSnapshotData extends Ts7SnapshotData { projects: Ts7ReportedProjectData[]; } +/** initialize's answer. TypeScript 7.0 reports useCaseSensitiveFileNames; + * 7.1 reports caseSensitivity (1 when case-sensitive) instead, which is how + * a session tells the two protocols apart. */ +interface Ts7InitializeReply { + currentDirectory: string; + useCaseSensitiveFileNames?: boolean; + caseSensitivity?: number; +} + +/** A project as TypeScript 7.1 reports it: the parsed command line carries + * the root files and options, and a project need not have a config file. */ +interface Ts71ProjectData { + id: string; + configFileName?: string; + parsedCommandLine: { fileNames: string[]; options: Ts7ProjectCompilerOptions }; +} + +/** createSnapshot's and updateSnapshot's answer in TypeScript 7.1. An update + * lists only the projects added or replaced since its base snapshot, and a + * changed project's entry is null when its files are not listed. */ +interface Ts71SnapshotData { + snapshot: number; + projects: Ts71ProjectData[]; + changes?: { + changedProjects?: Record; + removedProjects?: string[]; + }; +} + +/** TypeScript 7.1's compiler options in 7.0's values: 7.1 swaps JsxEmit's + * React (2) and ReactNative (3). */ +function options71(options: T): T { + if (options.jsx === 2) options.jsx = 3; + else if (options.jsx === 3) options.jsx = 2; + return options; +} + +function project71(data: Ts71ProjectData): Ts7ReportedProjectData { + return { + id: data.id, + configFileName: data.configFileName ?? data.id, + compilerOptions: options71(data.parsedCommandLine.options), + rootFiles: data.parsedCommandLine.fileNames, + }; +} + export interface Ts7FileChanges { changed?: SemanticDocument[]; created?: SemanticDocument[]; @@ -55,7 +101,7 @@ interface ProgramQuery { snapshot?: number; project?: string; file?: SemanticDocument; - files?: string[]; + files?: SemanticDocument[]; } /** The answer of getDiagnosticsOfCheckedFiles, as the server sends it. */ @@ -73,6 +119,8 @@ export class Ts7Session { private readonly snapshots = new Set(); private latest: Ts7SessionSnapshot | undefined; private paths: Ts7Paths | undefined; + /** Whether the server speaks TypeScript 7.1's protocol (initialize). */ + private ts71 = false; private closed = false; constructor( @@ -116,22 +164,41 @@ export class Ts7Session { initialize(): Ts7Paths { this.ensureOpen(); if (this.paths === undefined) { - const data = parseSemanticJson(this.text("initialize", "null")); - this.paths = new Ts7Paths(data.currentDirectory, data.useCaseSensitiveFileNames); + const data = parseSemanticJson(this.text("initialize", "null")); + this.ts71 = data.caseSensitivity !== undefined; + this.paths = new Ts7Paths( + data.currentDirectory, + this.ts71 ? data.caseSensitivity === 1 : data.useCaseSensitiveFileNames === true, + ); } return this.paths; } parseConfigFile(file: SemanticDocument): Ts7ConfigData { this.initialize(); - return this.request("parseConfigFile", { file }); + const data = this.request("parseConfigFile", { file }); + // TypeScript 7.1 also reports the raw config, errors and compileOnSave. + return this.ts71 ? { options: options71(data.options), fileNames: data.fileNames } : data; + } + + /** Whether initialize found a TypeScript 7.1 server; false before it. */ + speaksTs71(): boolean { + return this.ts71; + } + + /** Whether the server speaks TypeScript 7.1's protocol. */ + usesTs71Protocol(): boolean { + this.initialize(); + return this.ts71; } updateSnapshot(params: Ts7Update = {}): Ts7SessionSnapshot { const paths = this.initialize(); - const data = parseSemanticJson( - this.text("updateSnapshot", JSON.stringify(params)), - ); + const data = this.ts71 + ? this.snapshot71(params) + : parseSemanticJson( + this.text("updateSnapshot", JSON.stringify(params)), + ); const snapshot = new Ts7SessionSnapshot(data, this, paths); const previous = this.latest; if (previous !== undefined) { @@ -147,9 +214,56 @@ export class Ts7Session { return snapshot; } + /** TypeScript 7.1 derives a snapshot from a base snapshot the client + * names (updateSnapshot), or creates the first one (createSnapshot). The + * base is the latest snapshot: the server keeps it until the next update, + * even after the client disposes it (release), so open projects and files + * carry over as they do on a 7.0 server. An update lists only the projects + * that changed; the others carry over from the base. */ + private snapshot71(params: Ts7Update): Ts7ReportedSnapshotData { + const changes: Record = {}; + if (params.openProjects !== undefined) changes["openProjects"] = params.openProjects; + if (params.closeProjects !== undefined) changes["closeProjects"] = params.closeProjects; + if (params.openFiles !== undefined) changes["openFiles"] = params.openFiles; + if (params.closeFiles !== undefined) changes["closeFiles"] = params.closeFiles; + if (params.fileChanges !== undefined) changes["fileNotifications"] = params.fileChanges; + // A 7.1 snapshot leaves a project whose files changed dirty until asked + // to update its program; a 7.0 snapshot's programs are always current. + changes["ensurePrograms"] = true; + const base = this.latest; + const data = parseSemanticJson( + base === undefined + ? this.text("createSnapshot", JSON.stringify(changes)) + : this.text("updateSnapshot", JSON.stringify({ snapshot: base.id, changes })), + ); + if (base !== undefined && base.isDisposed()) + this.binary("release", JSON.stringify({ snapshot: base.id })); + const projects = new Map(); + const removed = new Set(data.changes?.removedProjects ?? []); + if (base !== undefined) + for (const project of base.projectData) + if (!removed.has(project.id)) projects.set(project.id, project); + for (const project of data.projects) projects.set(project.id, project71(project)); + // A changed project without a file list keeps none of its cached files. + const changedProjects: Record = {}; + const removedProjects = [...removed]; + for (const [id, change] of Object.entries(data.changes?.changedProjects ?? {})) { + if (change === null) removedProjects.push(id); + else changedProjects[id] = change; + } + return { + snapshot: data.snapshot, + projects: [...projects.values()], + changes: { changedProjects, removedProjects }, + }; + } + release(snapshot: Ts7SessionSnapshot): void { this.snapshots.delete(snapshot); if (snapshot !== this.latest) this.cache.release(snapshot.id); + // A 7.1 server keeps the latest snapshot as the next update's base + // (snapshot71), which releases it after deriving from it. + if (this.ts71 && snapshot === this.latest) return; this.binary("release", JSON.stringify({ snapshot: snapshot.id })); } @@ -180,6 +294,8 @@ export class Ts7Session { export class Ts7SessionSnapshot { readonly id: number; + /** The projects as the server reported them (TypeScript 7.0's shape). */ + readonly projectData: readonly Ts7ReportedProjectData[]; readonly semantic: SemanticSnapshot; private readonly projects = new Map(); private disposed = false; @@ -190,10 +306,15 @@ export class Ts7SessionSnapshot { readonly paths: Ts7Paths, ) { this.id = data.snapshot; - this.semantic = new SemanticSnapshot(this.id, { - text: (method, payload) => this.session.text(method, payload), - binary: (method, payload) => this.session.binary(method, payload), - }); + this.projectData = data.projects; + this.semantic = new SemanticSnapshot( + this.id, + { + text: (method, payload) => this.session.text(method, payload), + binary: (method, payload) => this.session.binary(method, payload), + }, + session.speaksTs71(), + ); for (const item of data.projects) { const project = new Ts7SessionProject(item, this, session); this.projects.set(paths.canonical(item.configFileName), project); @@ -214,10 +335,15 @@ export class Ts7SessionSnapshot { } getDefaultProjectForFile(file: SemanticDocument): Ts7SessionProject | undefined { this.ensureActive(); - const data = this.session.request("getDefaultProjectForFile", { - snapshot: this.id, - file, - }); + const query = { snapshot: this.id, file }; + if (this.session.usesTs71Protocol()) { + const data = this.session.request("getDefaultProjectForFile", query); + return data === null ? undefined : this.getProject(project71(data).configFileName); + } + const data = this.session.request( + "getDefaultProjectForFile", + query, + ); return data === null ? undefined : this.getProject(data.configFileName); } isDisposed(): boolean { @@ -344,17 +470,29 @@ export class Ts7SessionProgram { isSourceFileDefaultLibrary(file: AstNode): boolean { return this.getSourceFileMetadataByPath(file.path)?.isDefaultLibrary ?? false; } + /** A diagnostics request for `file`, or for the whole program. TypeScript + * 7.1 takes the file in a list (`files`). */ + private diagnosticsQuery(file?: SemanticDocument): ProgramQuery { + if (file === undefined || !this.session.usesTs71Protocol()) return this.query(file); + const query = this.query(); + query.files = [file]; + return query; + } private diagnostics(method: string, file?: SemanticDocument): Ts7Diagnostic[] { const result: Ts7Diagnostic[] = []; - for (const data of this.session.request(method, this.query(file)) ?? - []) + for (const data of this.session.request( + method, + this.diagnosticsQuery(file), + ) ?? []) result.push(ts7Diagnostic(data)); return result; } /** The diagnostics of `method` for `file`, or for the whole program, as * the server sends them. */ diagnosticData(method: string, file?: SemanticDocument): Ts7DiagnosticData[] { - return this.session.request(method, this.query(file)) ?? []; + return ( + this.session.request(method, this.diagnosticsQuery(file)) ?? [] + ); } /** The semantic diagnostics of each of `files` and the global diagnostics, * after the server checks only those files (getDiagnosticsOfCheckedFiles), diff --git a/packages/compiler/src/frontend/ts7/test-servers.ts b/packages/compiler/src/frontend/ts7/test-servers.ts new file mode 100644 index 000000000..68e1e4f0c --- /dev/null +++ b/packages/compiler/src/frontend/ts7/test-servers.ts @@ -0,0 +1,28 @@ +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import { tsgoExecutable } from "./rpc-api.js"; + +/** An API server the TypeScript client tests run against. */ +export interface Ts7TestServer { + /** The label tests append to their names. */ + name: string; + executable: string; + /** Whether the server speaks TypeScript 7.1's protocol. */ + ts71: boolean; +} + +const require = createRequire(import.meta.url); + +/** tsc 7.0.2 from the pinned `typescript` package and tsc 7.1.0-beta from + * the `typescript-7.1` test dependency, so the client's protocol 5 and + * protocol 9 paths answer the same cases. For tests only. */ +export function ts7TestServers(): Ts7TestServer[] { + const root = dirname(require.resolve("typescript-7.1/package.json")); + const { default: getExePath } = require(join(root, "lib/getExePath.js")) as { + default: () => string; + }; + return [ + { name: "tsc 7.0", executable: tsgoExecutable(), ts71: false }, + { name: "tsc 7.1", executable: getExePath(), ts71: true }, + ]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64671b921..fe90bc447 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,10 @@ importers: typescript5: specifier: npm:typescript@5.9.3 version: typescript@5.9.3 + devDependencies: + typescript-7.1: + specifier: npm:typescript@7.1.0-beta + version: typescript@7.1.0-beta optionalDependencies: '@scriptc/llvm-darwin-arm64': specifier: workspace:* @@ -1495,120 +1499,246 @@ packages: cpu: [ppc64] os: [aix] + '@typescript/typescript-aix-ppc64@7.1.0-beta': + resolution: {integrity: sha512-DXDG+jIWxBHGdoO70tcHON33rCnSzsEZj9P13oyf/r/KEGIo6awsdSNVNjAw0uMwEpqRu75IrgCcW58YcVkZHg==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-android-arm64@7.1.0-beta': + resolution: {integrity: sha512-UZx6PxoQavuvaVTrSf3aw0Oy/J/uJpRylipA0kzc038jMvZXh0UtE8vnQp20FqtdgD5bhXZ0aJrRJyoTSUO58Q==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [android] + '@typescript/typescript-darwin-arm64@7.0.2': resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] + '@typescript/typescript-darwin-arm64@7.1.0-beta': + resolution: {integrity: sha512-q7mxblcyX0E7hXsY4k+Q6KWKK5mGmfVq+BDesdTgRVgjodk27aIxER77Zyj9+TgSTlCje8F7+JM6f6hl2BuNdw==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + '@typescript/typescript-darwin-x64@7.0.2': resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] + '@typescript/typescript-darwin-x64@7.1.0-beta': + resolution: {integrity: sha512-D5EYqG5Wm/guw9+0j8+Mn1Jbr/gfS/VtFTuoH8PJtFbcqeDaQGMulc7SW8q51QG6Y3wGOGX3nbsTOw6fIKWWAA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + '@typescript/typescript-freebsd-arm64@7.0.2': resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [freebsd] + '@typescript/typescript-freebsd-arm64@7.1.0-beta': + resolution: {integrity: sha512-9A0XX0tcnH9qPdolSKKzSjTb93dlB0UEcAJbHYEsrFX2fkFNSum9+dvkQQJ9vLc+7vxHetDPujq9Qe6wx+Xhow==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + '@typescript/typescript-freebsd-x64@7.0.2': resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} engines: {node: '>=16.20.0'} cpu: [x64] os: [freebsd] + '@typescript/typescript-freebsd-x64@7.1.0-beta': + resolution: {integrity: sha512-EDf/dBHfY0TxX3NmUTek0hbffeyZlh3+ls6WQW3ICB8vYxwYLFnzg7wkgEUvl9eRlpiJ/nmc2S8arPMzINJ7rg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + '@typescript/typescript-linux-arm64@7.0.2': resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] + '@typescript/typescript-linux-arm64@7.1.0-beta': + resolution: {integrity: sha512-hNqQBgQ5b5oGqN76QqbAU6CwYhJZ6iOcWc2kPTUs2OentgpxZktETziH6KIyRFJPaiBncognwn2ZLu3DNhHiWw==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + '@typescript/typescript-linux-arm@7.0.2': resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] + '@typescript/typescript-linux-arm@7.1.0-beta': + resolution: {integrity: sha512-AvN9HmzOQh46G4312ooSgMHhbB7DtnLwg8D9mo324X49B7weMwtJrBG1MxT9IyI8kzDd7YRtW+1hWGun7WG7Qw==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + '@typescript/typescript-linux-loong64@7.0.2': resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} engines: {node: '>=16.20.0'} cpu: [loong64] os: [linux] + '@typescript/typescript-linux-loong64@7.1.0-beta': + resolution: {integrity: sha512-xbUustlBrFflt2pNEyG5ye+d4WGPc/rfr2xK29MXeobPSw297JqwuKRrvbKM5o3fE/eUM/c+urVm6jQ3IaMVTw==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + '@typescript/typescript-linux-mips64el@7.0.2': resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} engines: {node: '>=16.20.0'} cpu: [mips64el] os: [linux] + '@typescript/typescript-linux-mips64el@7.1.0-beta': + resolution: {integrity: sha512-fS4/G7/0bXZ3NSq3nBMAXyg65I3tO4S7j08VNQ2g1aNC/qq79gE7Qne1BNjVsjMFfHBVBFjQYoTQCjmVU2iFlg==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + '@typescript/typescript-linux-ppc64@7.0.2': resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} engines: {node: '>=16.20.0'} cpu: [ppc64] os: [linux] + '@typescript/typescript-linux-ppc64@7.1.0-beta': + resolution: {integrity: sha512-0upCCxYPX8X6gOwkvfv+Ee7/ufyy7k97mTrGE0e5BxLlnUEa+w+rtgnv2dsKcxFdwdeZYVlZKCqgN/xCBV08+A==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + '@typescript/typescript-linux-riscv64@7.0.2': resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} engines: {node: '>=16.20.0'} cpu: [riscv64] os: [linux] + '@typescript/typescript-linux-riscv64@7.1.0-beta': + resolution: {integrity: sha512-0jKU9YRgOPRgiy5V/UWv37kB53PaTpIbwGCbbOMUPPAkrjVRNOIAJaz7Wa7s3H7mdDebX1LopUEPEJR+7Xa7zg==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + '@typescript/typescript-linux-s390x@7.0.2': resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} engines: {node: '>=16.20.0'} cpu: [s390x] os: [linux] + '@typescript/typescript-linux-s390x@7.1.0-beta': + resolution: {integrity: sha512-j+d04s2kxaPQh6DLPb01sknAQfzsrdSZy15jBRHyLEZoaaQ19dVjbFRlkNVvYwUG7ZdP8+y5b/7RmPxdtHUKdQ==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + '@typescript/typescript-linux-x64@7.0.2': resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] + '@typescript/typescript-linux-x64@7.1.0-beta': + resolution: {integrity: sha512-kcD51ihf+pnI5KT4uQQYG04OpwznaZYBlIQhDhl8fflFd27NktxXsUeguxZyvUMyWfum7IF2bBS6Jv7ox0AgFQ==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + '@typescript/typescript-netbsd-arm64@7.0.2': resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [netbsd] + '@typescript/typescript-netbsd-arm64@7.1.0-beta': + resolution: {integrity: sha512-8W85YyoGK+rgGoSU9jJQUpELbrKcZcbdJk2jPNQ6UKDKI7ryPlPFTrV2jCE5RQuv4Tt96hMDTc33s4YfRrLFBA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + '@typescript/typescript-netbsd-x64@7.0.2': resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [netbsd] + '@typescript/typescript-netbsd-x64@7.1.0-beta': + resolution: {integrity: sha512-iGsTE0ve2PG0AcyZinwAR0LnGEZhu/y4dceS0hZsYCaKdYETvQtvtlNWlmYKempsBr6C7Il7dMXHVk+VFm9u0g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + '@typescript/typescript-openbsd-arm64@7.0.2': resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [openbsd] + '@typescript/typescript-openbsd-arm64@7.1.0-beta': + resolution: {integrity: sha512-Z2FERdHB9fAKI+KGqdWn04O7uNlnB+VYiEm6INE1aZuzzLNd3KBqQ6IpKUJF2ny4GigGU6nVu75XD9tAUODPng==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + '@typescript/typescript-openbsd-x64@7.0.2': resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} engines: {node: '>=16.20.0'} cpu: [x64] os: [openbsd] + '@typescript/typescript-openbsd-x64@7.1.0-beta': + resolution: {integrity: sha512-NVG3qeyCq/rJEOhaRjR3LsWgjrjo5vJW25omVLeHAVnpKIG4pE4cenwwKGR06DiJ1s0QTOymcJeKXMEpsiNBVg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + '@typescript/typescript-sunos-x64@7.0.2': resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} engines: {node: '>=16.20.0'} cpu: [x64] os: [sunos] + '@typescript/typescript-sunos-x64@7.1.0-beta': + resolution: {integrity: sha512-ovAdoWXEn2B5/fyWG69pLU3FWmQwPWOjQV5TFbcxtpnOv+R3g8ySGHl1NYRq7Lb/GYdFDvaGcZ+SiU7iKuiMsA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + '@typescript/typescript-win32-arm64@7.0.2': resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] + '@typescript/typescript-win32-arm64@7.1.0-beta': + resolution: {integrity: sha512-lJbo71IEOfL7ZZIZnArRaOLqjn9BAfoSzyUQn0aSjfSpDZL4ru0afUsdBfo6PQEjD6ssdmi0n/RwaPADX6bsvQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + '@typescript/typescript-win32-x64@7.0.2': resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] + '@typescript/typescript-win32-x64@7.1.0-beta': + resolution: {integrity: sha512-UKcHvbybY2tDTvFW2b1vwiAvDlIRooo69lh8CXWVhOQejcvpM5+IgbiFW9EIwR5Wi5X/v+BdN7gdWbigBuJ3rw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + '@vercel/backends@0.8.27': resolution: {integrity: sha512-WW9hfF0mWZDQr23RcjLgVUIoo3V07aq25t6f3RSbq0yIQ2zKZ41x8+0nhNKjzAP7GB7yFmwX4UOv4wV2fWqDDQ==} @@ -2654,6 +2784,11 @@ packages: engines: {node: '>=16.20.0'} hasBin: true + typescript@7.1.0-beta: + resolution: {integrity: sha512-cjWjZAJDsmZzsNC5GwHNlW0YPM65jZa5oR0KyQcYX3KSVIfTvtBq//vewISyatKk4OLi5grZjHhIVE9fdrw1UQ==} + engines: {node: '>=16.20.0'} + hasBin: true + uid-promise@1.0.0: resolution: {integrity: sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig==} @@ -3555,63 +3690,126 @@ snapshots: '@typescript/typescript-aix-ppc64@7.0.2': optional: true + '@typescript/typescript-aix-ppc64@7.1.0-beta': + optional: true + + '@typescript/typescript-android-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-darwin-arm64@7.0.2': optional: true + '@typescript/typescript-darwin-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-darwin-x64@7.0.2': optional: true + '@typescript/typescript-darwin-x64@7.1.0-beta': + optional: true + '@typescript/typescript-freebsd-arm64@7.0.2': optional: true + '@typescript/typescript-freebsd-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-freebsd-x64@7.0.2': optional: true + '@typescript/typescript-freebsd-x64@7.1.0-beta': + optional: true + '@typescript/typescript-linux-arm64@7.0.2': optional: true + '@typescript/typescript-linux-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-linux-arm@7.0.2': optional: true + '@typescript/typescript-linux-arm@7.1.0-beta': + optional: true + '@typescript/typescript-linux-loong64@7.0.2': optional: true + '@typescript/typescript-linux-loong64@7.1.0-beta': + optional: true + '@typescript/typescript-linux-mips64el@7.0.2': optional: true + '@typescript/typescript-linux-mips64el@7.1.0-beta': + optional: true + '@typescript/typescript-linux-ppc64@7.0.2': optional: true + '@typescript/typescript-linux-ppc64@7.1.0-beta': + optional: true + '@typescript/typescript-linux-riscv64@7.0.2': optional: true + '@typescript/typescript-linux-riscv64@7.1.0-beta': + optional: true + '@typescript/typescript-linux-s390x@7.0.2': optional: true + '@typescript/typescript-linux-s390x@7.1.0-beta': + optional: true + '@typescript/typescript-linux-x64@7.0.2': optional: true + '@typescript/typescript-linux-x64@7.1.0-beta': + optional: true + '@typescript/typescript-netbsd-arm64@7.0.2': optional: true + '@typescript/typescript-netbsd-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-netbsd-x64@7.0.2': optional: true + '@typescript/typescript-netbsd-x64@7.1.0-beta': + optional: true + '@typescript/typescript-openbsd-arm64@7.0.2': optional: true + '@typescript/typescript-openbsd-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-openbsd-x64@7.0.2': optional: true + '@typescript/typescript-openbsd-x64@7.1.0-beta': + optional: true + '@typescript/typescript-sunos-x64@7.0.2': optional: true + '@typescript/typescript-sunos-x64@7.1.0-beta': + optional: true + '@typescript/typescript-win32-arm64@7.0.2': optional: true + '@typescript/typescript-win32-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-win32-x64@7.0.2': optional: true + '@typescript/typescript-win32-x64@7.1.0-beta': + optional: true + '@vercel/backends@0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: '@vercel/build-utils': 13.36.0 @@ -4915,6 +5113,30 @@ snapshots: '@typescript/typescript-win32-arm64': 7.0.2 '@typescript/typescript-win32-x64': 7.0.2 + typescript@7.1.0-beta: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.1.0-beta + '@typescript/typescript-android-arm64': 7.1.0-beta + '@typescript/typescript-darwin-arm64': 7.1.0-beta + '@typescript/typescript-darwin-x64': 7.1.0-beta + '@typescript/typescript-freebsd-arm64': 7.1.0-beta + '@typescript/typescript-freebsd-x64': 7.1.0-beta + '@typescript/typescript-linux-arm': 7.1.0-beta + '@typescript/typescript-linux-arm64': 7.1.0-beta + '@typescript/typescript-linux-loong64': 7.1.0-beta + '@typescript/typescript-linux-mips64el': 7.1.0-beta + '@typescript/typescript-linux-ppc64': 7.1.0-beta + '@typescript/typescript-linux-riscv64': 7.1.0-beta + '@typescript/typescript-linux-s390x': 7.1.0-beta + '@typescript/typescript-linux-x64': 7.1.0-beta + '@typescript/typescript-netbsd-arm64': 7.1.0-beta + '@typescript/typescript-netbsd-x64': 7.1.0-beta + '@typescript/typescript-openbsd-arm64': 7.1.0-beta + '@typescript/typescript-openbsd-x64': 7.1.0-beta + '@typescript/typescript-sunos-x64': 7.1.0-beta + '@typescript/typescript-win32-arm64': 7.1.0-beta + '@typescript/typescript-win32-x64': 7.1.0-beta + uid-promise@1.0.0: {} undici-types@5.26.5: {}