mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Talk to TypeScript 7.1's API server beside 7.0's
The TypeScript session reads initialize's answer to tell a 7.1 server
(caseSensitivity) from a 7.0 one (useCaseSensitiveFileNames), and on a
7.1 server sends 7.1's requests and reads its answers:
- snapshots: the first comes from createSnapshot and later ones from
updateSnapshot with the latest snapshot as the base, which the server
keeps until the next update. Each asks for current programs
(ensurePrograms), and a project's root files and options come from its
parsed command line. Projects an update does not list carry over from
the base.
- symbols: a symbol is named by its reference, a source file or the
snapshot and project that owns it plus its id. The semantic model keeps
numeric symbol ids: each reference, and each compact reference other
answers hold, gets a local id, and requests send the reference back.
Symbol property methods take it in `symbol`.
- requests: getApparentType, getConstraintOfTypeParameter,
getNonNullableType and getReturnTypeOfSignature name their type or
signature in `objectId`, the diagnostics methods take `files`, and
signatureToSignatureDeclaration's kind is a 7.1 SyntaxKind.
- answers: isTupleType is read from the type, JsxEmit's React and
ReactNative values are swapped back, and parseConfigFile returns the
options and file names.
- file system callbacks reply with {kind, value}: a value, "missing" for
a hidden path, or "useOS" to delegate to the server.
The session, checker and semantic checker tests run against tsc 7.0.2 and
against tsc 7.1.0-beta, from the typescript-7.1 test dependency.
This commit is contained in:
@@ -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:*",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, string>) {
|
||||
/** 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<string, string>) {
|
||||
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<string, () => 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<string, () => 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[][] = [];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!);
|
||||
});
|
||||
|
||||
@@ -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<string, Ts7Session>();
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<S> {
|
||||
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<S> {
|
||||
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<boolean>("isArrayLikeType", { type: type.id });
|
||||
}
|
||||
isTupleType(type: SemanticType): boolean {
|
||||
if (this.project.ts71()) return this.project.isTupleType71(type.id);
|
||||
return this.project.request<boolean>("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<TypePredicateResponse71 | null>(
|
||||
"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<TypePredicateResponse | null>("getTypePredicateOfSignature", {
|
||||
signature: signature.id,
|
||||
});
|
||||
@@ -403,6 +441,22 @@ export class SemanticChecker {
|
||||
};
|
||||
}
|
||||
getIndexInfosOfType(type: SemanticType): SemanticIndexInfo[] {
|
||||
if (this.project.ts71()) {
|
||||
const data = this.project.request<IndexInfoResponse71[] | null>("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<IndexInfoResponse[] | null>("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<WellKnownSymbols>("getWellKnownSymbols");
|
||||
if (this.wellKnown === undefined) {
|
||||
const ids = this.project.request<WellKnownSymbols>("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<ReferencedSymbolResponse<Ts71SymbolData>[] | 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<ReferencedSymbolResponse<SymbolResponse>[] | 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<CompletionResponse | null>("getCompletionsAtPosition", {
|
||||
const query = {
|
||||
file,
|
||||
position,
|
||||
triggerCharacter: options?.triggerCharacter,
|
||||
includeSymbol: options?.includeSymbol,
|
||||
});
|
||||
};
|
||||
if (this.project.ts71()) {
|
||||
const data = this.project.request<CompletionResponse<Ts71SymbolData> | 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<CompletionResponse<SymbolResponse> | null>(
|
||||
"getCompletionsAtPosition",
|
||||
query,
|
||||
);
|
||||
if (data === null) return undefined;
|
||||
return {
|
||||
isIncomplete: data.isIncomplete,
|
||||
|
||||
@@ -149,7 +149,7 @@ function parseJsonSlice<T>(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<T extends { id: number }, R>(
|
||||
function replyElement<T extends object, R>(
|
||||
bytes: Uint8Array,
|
||||
start: number,
|
||||
end: number,
|
||||
@@ -172,7 +172,7 @@ function replyElement<T extends { id: number }, R>(
|
||||
* 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<T extends { id: number }, R>(
|
||||
export function decodeSemanticObject<T extends object, R>(
|
||||
bytes: Uint8Array,
|
||||
cached: (id: number) => R | undefined,
|
||||
build: (data: T) => R,
|
||||
@@ -187,7 +187,7 @@ export function decodeSemanticObject<T extends { id: number }, R>(
|
||||
/** 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<T extends { id: number }, R>(
|
||||
export function decodeSemanticObjects<T extends object, R>(
|
||||
bytes: Uint8Array,
|
||||
cached: (id: number) => R | undefined,
|
||||
build: (data: T) => R,
|
||||
|
||||
@@ -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<string, unknown>) => unknown) {
|
||||
const requests: { method: string; query: Record<string, unknown> }[] = [];
|
||||
const snapshot = new SemanticSnapshot(
|
||||
1,
|
||||
{
|
||||
text: (method, payload) => {
|
||||
const query = JSON.parse(payload) as Record<string, unknown>;
|
||||
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);
|
||||
});
|
||||
@@ -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<string, "type" | "signature">([
|
||||
["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<string, number>();
|
||||
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<number, SemanticSymbol>();
|
||||
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<number, SemanticType>();
|
||||
/** 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<number, boolean>();
|
||||
private readonly signatures = new Map<number, SemanticSignature>();
|
||||
private readonly cleanups: (() => void)[] = [];
|
||||
private disposed = false;
|
||||
@@ -150,24 +368,90 @@ export class SemanticProject {
|
||||
this.cleanups.push(cleanup);
|
||||
}
|
||||
|
||||
request<T>(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<T>(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<T>(method: string, query: SemanticQuery = {}): T {
|
||||
return parseSemanticJson<T>(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);
|
||||
}
|
||||
|
||||
@@ -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 = <T extends { fileName?: string | undefined }>(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 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<string, Ts7ProjectChangeData | null>;
|
||||
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<T extends { jsx?: number | undefined }>(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<Ts7SessionSnapshot>();
|
||||
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<Ts7InitializeData>(this.text("initialize", "null"));
|
||||
this.paths = new Ts7Paths(data.currentDirectory, data.useCaseSensitiveFileNames);
|
||||
const data = parseSemanticJson<Ts7InitializeReply>(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<Ts7ConfigData>("parseConfigFile", { file });
|
||||
const data = this.request<Ts7ConfigData>("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<Ts7ReportedSnapshotData>(
|
||||
this.text("updateSnapshot", JSON.stringify(params)),
|
||||
);
|
||||
const data = this.ts71
|
||||
? this.snapshot71(params)
|
||||
: parseSemanticJson<Ts7ReportedSnapshotData>(
|
||||
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<string, unknown> = {};
|
||||
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<Ts71SnapshotData>(
|
||||
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<string, Ts7ReportedProjectData>();
|
||||
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<string, Ts7ProjectChangeData> = {};
|
||||
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<string, Ts7SessionProject>();
|
||||
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<Ts7ReportedProjectData | null>("getDefaultProjectForFile", {
|
||||
snapshot: this.id,
|
||||
file,
|
||||
});
|
||||
const query = { snapshot: this.id, file };
|
||||
if (this.session.usesTs71Protocol()) {
|
||||
const data = this.session.request<Ts71ProjectData | null>("getDefaultProjectForFile", query);
|
||||
return data === null ? undefined : this.getProject(project71(data).configFileName);
|
||||
}
|
||||
const data = this.session.request<Ts7ReportedProjectData | null>(
|
||||
"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<Ts7DiagnosticData[] | null>(method, this.query(file)) ??
|
||||
[])
|
||||
for (const data of this.session.request<Ts7DiagnosticData[] | null>(
|
||||
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<Ts7DiagnosticData[] | null>(method, this.query(file)) ?? [];
|
||||
return (
|
||||
this.session.request<Ts7DiagnosticData[] | null>(method, this.diagnosticsQuery(file)) ?? []
|
||||
);
|
||||
}
|
||||
/** The semantic diagnostics of each of `files` and the global diagnostics,
|
||||
* after the server checks only those files (getDiagnosticsOfCheckedFiles),
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
}
|
||||
Generated
+222
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user