mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
feat(compiler): statically compile the TypeScript semantic client
- Replace SDK semantic objects with native type, symbol, and signature registries. - Route production checker queries through the owned client while preserving identity and snapshot lifetimes. - Support scalar BigInt conversion and class union checks needed by native semantic decoding.
This commit is contained in:
@@ -3804,24 +3804,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
lowerer.noLowering(`BigInt with ${expr.arguments.length} arguments`, expr);
|
||||
}
|
||||
const argNode = expr.arguments[0]!;
|
||||
const arg = lowerer.lowerExpr(argNode);
|
||||
if (arg.type.kind === "bigint") return arg;
|
||||
if (arg.type.kind === "string") {
|
||||
return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc };
|
||||
}
|
||||
if (arg.type.kind === "f64") {
|
||||
return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc };
|
||||
}
|
||||
if (arg.type.kind === "bool") {
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: arg,
|
||||
then: { kind: "libCall", fn: "bigint.parse", args: [{ kind: "strLit", value: "1", type: STRING, loc }], type: BIGINT_T, loc },
|
||||
else_: { kind: "libCall", fn: "bigint.parse", args: [{ kind: "strLit", value: "0", type: STRING, loc }], type: BIGINT_T, loc },
|
||||
type: BIGINT_T,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Captured optional storage can change after TypeScript's guard.
|
||||
// BigInt consumes the stored value, including its nullish tag.
|
||||
const arg = optionalCallValue(lowerer, argNode) ?? lowerer.lowerExpr(argNode);
|
||||
const converted = lowerBigIntConstructorValue(lowerer, arg, loc);
|
||||
if (converted) return converted;
|
||||
lowerer.noLowering(`BigInt of ${lowerer.fmt(arg.type)} values`, argNode, "string, number, boolean, and bigint arguments are supported");
|
||||
}
|
||||
|
||||
@@ -5098,6 +5085,56 @@ function lowerNumberConstructorValue(lowerer: Lowerer, argNode: ts.Expression, l
|
||||
);
|
||||
}
|
||||
|
||||
/** A runtime-optional local retains its full tagged storage even after a
|
||||
* checker guard. Dispatch BigInt by the actual value, including nullish
|
||||
* TypeErrors, rather than interpreting that storage as the narrowed arm. */
|
||||
function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc): IrExpr | null {
|
||||
if (arg.type.kind === "bigint") return arg;
|
||||
if (arg.type.kind === "string") return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc };
|
||||
if (arg.type.kind === "f64") return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc };
|
||||
if (arg.type.kind === "bool") {
|
||||
return {
|
||||
kind: "ternary", cond: arg,
|
||||
then: { kind: "libCall", fn: "bigint.parse", args: [{ kind: "strLit", value: "1", type: STRING, loc }], type: BIGINT_T, loc },
|
||||
else_: { kind: "libCall", fn: "bigint.parse", args: [{ kind: "strLit", value: "0", type: STRING, loc }], type: BIGINT_T, loc },
|
||||
type: BIGINT_T, loc,
|
||||
};
|
||||
}
|
||||
if (isUnitType(arg.type)) {
|
||||
const name = arg.type.kind === "nullT" ? "null" : "undefined";
|
||||
return defaultAfterUndefined(arg, nodeThrowExpr(1, "", `Cannot convert ${name} to a BigInt`, BIGINT_T, loc));
|
||||
}
|
||||
if (arg.type.kind !== "union") return null;
|
||||
const unionId = arg.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms;
|
||||
if (!arms || !arms.every((arm) => arm.kind === "bigint" || arm.kind === "string" || arm.kind === "f64" || arm.kind === "bool" || isUnitType(arm))) return null;
|
||||
const key = `bigint.scalar:${unionId}`;
|
||||
let helper = lowerer.widthHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%bigint.scalar.${lowerer.widthHelpers.size}`;
|
||||
lowerer.widthHelpers.set(key, helper);
|
||||
const value = varRef("value.0", arg.type, loc);
|
||||
const body: IrStmt[] = [];
|
||||
arms.forEach((arm, tag) => {
|
||||
const narrowed: IrExpr = isUnitType(arm)
|
||||
? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc }
|
||||
: { kind: "unionNarrow", unionId, tag, value, type: arm, loc };
|
||||
const converted = lowerBigIntConstructorValue(lowerer, narrowed, loc);
|
||||
if (!converted) throw new InternalCompilerError("BigInt scalar union has an unsupported arm");
|
||||
const ret: IrStmt = { kind: "return", value: converted, loc };
|
||||
body.push(tag === arms.length - 1 ? ret : {
|
||||
kind: "if", cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc },
|
||||
then: [ret], else_: null, loc,
|
||||
});
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name: helper, params: [{ localId: "value.0", name: "value", type: arg.type }],
|
||||
returnType: BIGINT_T, locals: [{ id: "value.0", name: "value", type: arg.type, mutable: false }], body, loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [arg], type: BIGINT_T, loc };
|
||||
}
|
||||
|
||||
function immediatePrimitiveWrapperToString(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
const stringValue = stringWrapperToString(lowerer, node);
|
||||
if (stringValue) return stringValue;
|
||||
|
||||
@@ -2502,15 +2502,17 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
}
|
||||
const tag = lowerer.armTag(expr.type.unionId, narrowed);
|
||||
if (tag < 0) {
|
||||
// A proven-present optional base class can narrow further to a
|
||||
// subclass. The payload is still stored under the base class tag;
|
||||
// unwrap it before applying the ordinary class downcast.
|
||||
// A base-class arm can narrow to a subclass after instanceof. The
|
||||
// payload retains its base-class tag even when the union also has
|
||||
// records or other classes. Require one unambiguous containing arm.
|
||||
const arms = lowerer.unions.get(expr.type.unionId)?.arms ?? [];
|
||||
const valueTag = arms.findIndex((arm) => !isUnitType(arm));
|
||||
const valueTag = narrowed.kind === "object"
|
||||
? arms.findIndex((arm) => arm.kind === "object" && lowerer.isSubclassOf(narrowed.className, arm.className))
|
||||
: -1;
|
||||
const valueType = arms[valueTag];
|
||||
if (
|
||||
narrowed.kind === "object" && valueType?.kind === "object" &&
|
||||
arms.every((arm, i) => i === valueTag || isUnitType(arm)) &&
|
||||
arms.every((arm, i) => i === valueTag || arm.kind !== "object" || !lowerer.isSubclassOf(narrowed.className, arm.className)) &&
|
||||
lowerer.isSubclassOf(narrowed.className, valueType.className)
|
||||
) {
|
||||
return {
|
||||
@@ -7987,6 +7989,42 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
`'instanceof ${target.def.name.replace(/^%/, "")}' on 'unknown' values (only the Error classes answer — test 'instanceof Error' and read '.name')`,
|
||||
);
|
||||
}
|
||||
if (left.type.kind === "union") {
|
||||
const unionId = left.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms;
|
||||
// Ordinary records and scalar values do not carry a class prototype.
|
||||
// Dynamic/opaque runtime objects keep their existing instanceof fences.
|
||||
if (arms?.every((arm) => arm.kind === "object" || arm.kind === "record" ||
|
||||
arm.kind === "f64" || arm.kind === "string" || arm.kind === "bool" ||
|
||||
arm.kind === "bigint" || arm.kind === "symbol" || isUnitType(arm))) {
|
||||
const key = `instanceof.union:${unionId}:${target.def.name}`;
|
||||
let helper = lowerer.widthHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%instanceof.union.${lowerer.widthHelpers.size}`;
|
||||
lowerer.widthHelpers.set(key, helper);
|
||||
const value: IrExpr = { kind: "varRef", localId: "value.0", type: left.type, loc };
|
||||
const body: IrStmt[] = [];
|
||||
arms.forEach((arm, tag) => {
|
||||
if (arm.kind !== "object") return;
|
||||
const lhsInfo = lowerer.classes.get(arm.className);
|
||||
if (!lhsInfo) throw new InternalCompilerError(`lowerer bug: unknown class ${arm.className}`);
|
||||
const result: IrExpr = lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(target)
|
||||
? { kind: "instanceOf", value: { kind: "unionNarrow", unionId, tag, value, type: arm, loc }, className: target.def.name, type: BOOL, loc }
|
||||
: { kind: "boolLit", value: arm.className === target.def.name || lowerer.isSubclassOf(arm.className, target.def.name), type: BOOL, loc };
|
||||
body.push({
|
||||
kind: "if", cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc },
|
||||
then: [{ kind: "return", value: result, loc }], else_: null, loc,
|
||||
});
|
||||
});
|
||||
body.push({ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc });
|
||||
lowerer.liftedFns.push({
|
||||
name: helper, params: [{ localId: "value.0", name: "value", type: left.type }], returnType: BOOL,
|
||||
locals: [{ id: "value.0", name: "value", type: left.type, mutable: false }], body, loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [left], type: BOOL, loc };
|
||||
}
|
||||
}
|
||||
if (left.type.kind !== "object") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
/** Transitional SDK boundary. The AST itself is the same concrete model
|
||||
* used by the native client. The SDK still owns Type/Symbol/Signature
|
||||
* registries, diagnostics, and snapshots; node-bearing queries below send
|
||||
* our handles directly, without manufacturing SDK RemoteNode objects. */
|
||||
import { Checker, NodeHandle, Program } from "typescript/unstable/sync";
|
||||
import type { DocumentIdentifier, DocumentPosition, Project, ReferencedSymbolEntry, Signature, SignatureUsage, Symbol as TsSymbol, Type } from "typescript/unstable/sync";
|
||||
* used by the native client. The SDK still owns program metadata and
|
||||
* snapshots. ASTs, checker queries, and semantic identities are owned here;
|
||||
* the casts retain the frontend's existing type-only interface surface. */
|
||||
import { Program } from "typescript/unstable/sync";
|
||||
import type { Checker, DocumentIdentifier, Project, Snapshot } from "typescript/unstable/sync";
|
||||
import { resolveFileName } from "typescript/unstable/proto";
|
||||
import type { SignatureResponse, SymbolResponse, TypeResponse } from "typescript/unstable/proto";
|
||||
import type { Node, Path, SourceFile, TypeNode } from "typescript/unstable/ast";
|
||||
import type { NodeBuilderFlags, SymbolFlags, SyntaxKind } from "./enums.js";
|
||||
import type { Path, SourceFile } from "typescript/unstable/ast";
|
||||
import { AstFile, AstNode } from "./ast-node.js";
|
||||
import { SemanticChecker } from "./semantic-checker.js";
|
||||
import { SemanticSnapshot, type SemanticTransport } from "./semantic-model.js";
|
||||
|
||||
type SdkClient = ConstructorParameters<typeof Checker>[2];
|
||||
type Registry = ConstructorParameters<typeof Checker>[3];
|
||||
type Cache = ConstructorParameters<typeof Program>[3];
|
||||
type CanonicalPath = (fileName: string) => Path;
|
||||
|
||||
export function astNodeId(node: Node): string {
|
||||
if (!(node instanceof AstNode)) throw new Error("TypeScript query requires a scriptc AST node");
|
||||
return node.id;
|
||||
}
|
||||
|
||||
function decode(bytes: Uint8Array, timing?: ReturnType<SdkClient["getTimingCollector"]>): AstFile {
|
||||
return new AstFile(bytes, (nodes, pos, end) => {
|
||||
// The SDK's NodeArray interface adds properties to an ordinary array.
|
||||
@@ -52,98 +46,26 @@ class NativeAstProgram extends Program {
|
||||
}
|
||||
}
|
||||
|
||||
class NativeAstChecker extends Checker {
|
||||
constructor(
|
||||
private readonly astSnapshot: number,
|
||||
private readonly astProject: Project,
|
||||
private readonly astClient: SdkClient,
|
||||
private readonly astRegistry: Registry,
|
||||
) { super(astSnapshot, astProject, astClient, astRegistry); }
|
||||
|
||||
private request<T>(method: string, params: object): T {
|
||||
return this.astClient.apiRequest<T>(method, { snapshot: this.astSnapshot, project: this.astProject.id, ...params });
|
||||
/** Install every project before publishing a snapshot: shared symbols may
|
||||
* name another project's canonical context on their very first response. */
|
||||
export function installNativeAst(snapshot: Snapshot, client: SdkClient, cache: Cache, canonical: CanonicalPath, transport: SemanticTransport): () => void {
|
||||
const semantic = new SemanticSnapshot(snapshot.id, transport);
|
||||
const projects = snapshot.getProjects();
|
||||
for (const project of projects) {
|
||||
const target = project as unknown as { program: Program; checker: Checker };
|
||||
target.program = new NativeAstProgram(snapshot.id, project, client, cache, canonical);
|
||||
semantic.addProject(project.id, (path) => {
|
||||
const file = project.program.getSourceFile(path);
|
||||
if (file === undefined) return undefined;
|
||||
if (!(file instanceof AstNode)) throw new Error("TypeScript semantic query requires a scriptc AST");
|
||||
return file;
|
||||
});
|
||||
}
|
||||
private nodeType(method: string, node: Node): Type | undefined {
|
||||
const data = this.request<TypeResponse | null>(method, { location: astNodeId(node) });
|
||||
return data === null ? undefined : this.astRegistry.getOrCreateType(data);
|
||||
}
|
||||
private nodeSymbol(method: string, node: Node): TsSymbol | undefined {
|
||||
const data = this.request<SymbolResponse | null>(method, { location: astNodeId(node) });
|
||||
return data === null ? undefined : this.astRegistry.getOrCreateSymbol(data);
|
||||
}
|
||||
private nodeSignature(method: string, node: Node): Signature | undefined {
|
||||
const data = this.request<SignatureResponse | null>(method, { location: astNodeId(node) });
|
||||
return data === null ? undefined : this.astRegistry.getOrCreateSignature(data);
|
||||
}
|
||||
|
||||
override getSymbolAtLocation(node: Node): TsSymbol | undefined;
|
||||
override getSymbolAtLocation(nodes: readonly Node[]): (TsSymbol | undefined)[];
|
||||
override getSymbolAtLocation(input: Node | readonly Node[]): TsSymbol | undefined | (TsSymbol | undefined)[] {
|
||||
if (!Array.isArray(input)) return this.nodeSymbol("getSymbolAtLocation", input as Node);
|
||||
const data = this.request<(SymbolResponse | null)[]>("getSymbolsAtLocations", { locations: input.map(astNodeId) });
|
||||
return data.map((value) => value === null ? undefined : this.astRegistry.getOrCreateSymbol(value));
|
||||
}
|
||||
override getTypeAtLocation(node: Node): Type | undefined;
|
||||
override getTypeAtLocation(nodes: readonly Node[]): (Type | undefined)[];
|
||||
override getTypeAtLocation(input: Node | readonly Node[]): Type | undefined | (Type | undefined)[] {
|
||||
if (!Array.isArray(input)) return this.nodeType("getTypeAtLocation", input as Node);
|
||||
const data = this.request<(TypeResponse | null)[]>("getTypeAtLocations", { locations: input.map(astNodeId) });
|
||||
return data.map((value) => value === null ? undefined : this.astRegistry.getOrCreateType(value));
|
||||
}
|
||||
override getResolvedSignature(node: Node): Signature | undefined { return this.nodeSignature("getResolvedSignature", node); }
|
||||
override getSignatureFromDeclaration(node: Node): Signature | undefined { return this.nodeSignature("getSignatureFromDeclaration", node); }
|
||||
override getContextualType(node: Node): Type | undefined { return this.nodeType("getContextualType", node); }
|
||||
override getTypeFromTypeNode(node: Node): Type | undefined { return this.nodeType("getTypeFromTypeNode", node); }
|
||||
override getShorthandAssignmentValueSymbol(node: Node): TsSymbol | undefined { return this.nodeSymbol("getShorthandAssignmentValueSymbol", node); }
|
||||
override getExportSpecifierLocalTargetSymbol(node: Node): TsSymbol | undefined { return this.nodeSymbol("getExportSpecifierLocalTargetSymbol", node); }
|
||||
override getConstantValue(node: Node): string | number | undefined {
|
||||
return this.request<string | number | null>("getConstantValue", { location: astNodeId(node) }) ?? undefined;
|
||||
}
|
||||
override isContextSensitive(node: Node): boolean {
|
||||
return this.request<boolean>("isContextSensitive", { location: astNodeId(node) });
|
||||
}
|
||||
override getTypeOfSymbolAtLocation(symbol: TsSymbol, location: Node): Type {
|
||||
const data = this.request<TypeResponse | null>("getTypeOfSymbolAtLocation", { symbol: symbol.id, location: astNodeId(location) });
|
||||
if (data === null) throw new Error(`getTypeOfSymbolAtLocation returned no type for symbol ${symbol.id}`);
|
||||
return this.astRegistry.getOrCreateType(data);
|
||||
}
|
||||
override typeToString(type: Type, enclosingDeclaration?: Node, flags?: number): string {
|
||||
return this.request<string>("typeToString", { type: type.id, location: enclosingDeclaration === undefined ? undefined : astNodeId(enclosingDeclaration), flags });
|
||||
}
|
||||
override resolveName(name: string, meaning: SymbolFlags, location?: Node | DocumentPosition, excludeGlobals?: boolean): TsSymbol | undefined {
|
||||
const node = location !== undefined && "kind" in location ? location : undefined;
|
||||
const position = location !== undefined && "document" in location ? location : undefined;
|
||||
const data = this.request<SymbolResponse | null>("resolveName", { name, meaning, location: node === undefined ? undefined : astNodeId(node), file: position?.document, position: position?.position, excludeGlobals });
|
||||
return data === null ? undefined : this.astRegistry.getOrCreateSymbol(data);
|
||||
}
|
||||
override getReferencedSymbolsForNode(node: Node, position: number): ReferencedSymbolEntry[] {
|
||||
const data = this.request<{ definition: string; symbol?: SymbolResponse; references?: string[] }[] | null>("getReferencedSymbolsForNode", { node: astNodeId(node), position });
|
||||
return (data ?? []).map((entry) => ({
|
||||
definition: new NodeHandle(entry.definition, this.astProject),
|
||||
symbol: entry.symbol === undefined ? undefined : this.astRegistry.getOrCreateSymbol(entry.symbol),
|
||||
references: (entry.references ?? []).map((handle) => new NodeHandle(handle, this.astProject)),
|
||||
}));
|
||||
}
|
||||
override getSignatureUsage(signatureDecl: Node): SignatureUsage[] {
|
||||
const data = this.request<{ name: string; call?: string }[] | null>("getSignatureUsages", { signatureDecl: astNodeId(signatureDecl) });
|
||||
return (data ?? []).map((entry) => ({ name: new NodeHandle(entry.name, this.astProject), call: entry.call === undefined ? undefined : new NodeHandle(entry.call, this.astProject) }));
|
||||
}
|
||||
override typeToTypeNode(type: Type, enclosingDeclaration?: Node, flags?: number): TypeNode | undefined {
|
||||
const bytes = this.astClient.apiRequestBinary("typeToTypeNode", { snapshot: this.astSnapshot, project: this.astProject.id, type: type.id, location: enclosingDeclaration === undefined ? undefined : astNodeId(enclosingDeclaration), flags });
|
||||
return bytes === undefined ? undefined : decode(bytes).root as unknown as TypeNode;
|
||||
}
|
||||
override signatureToSignatureDeclaration(signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): Node | undefined {
|
||||
const bytes = this.astClient.apiRequestBinary("signatureToSignatureDeclaration", { snapshot: this.astSnapshot, project: this.astProject.id, signature: signature.id, kind, location: enclosingDeclaration === undefined ? undefined : astNodeId(enclosingDeclaration), flags });
|
||||
return bytes === undefined ? undefined : decode(bytes).root as unknown as Node;
|
||||
for (const project of projects) {
|
||||
// The frontend's structural Type/Node interfaces are a type-only bridge.
|
||||
// No native object is copied, adapted, or inserted in an SDK registry.
|
||||
const target = project as unknown as { checker: Checker };
|
||||
target.checker = new SemanticChecker(semantic.getProject(project.id)) as unknown as Checker;
|
||||
}
|
||||
}
|
||||
|
||||
/** Wire the AST boundary before publishing a new snapshot. The registry's
|
||||
* canonical Project object remains the same, so declaration handles resolve
|
||||
* through this program and return the exact nodes used by frontend walks. */
|
||||
export function installNativeAst(snapshot: number, project: Project, client: SdkClient, cache: Cache, canonical: CanonicalPath): void {
|
||||
const registry = (project.checker as unknown as { objectRegistry: Registry }).objectRegistry;
|
||||
const target = project as unknown as { program: Program; checker: Checker };
|
||||
target.program = new NativeAstProgram(snapshot, project, client, cache, canonical);
|
||||
target.checker = new NativeAstChecker(snapshot, project, client, registry);
|
||||
return () => { semantic.dispose(); };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { registerTs7FileSystem, TS7_FILE_SYSTEM_CALLBACKS, type Ts7FileSystem }
|
||||
import { spawnTs7Wire } from "./rpc-process.js";
|
||||
import { installNativeAst } from "./ast-sdk.js";
|
||||
|
||||
// The pinned SDK still owns snapshots and checker object identity.
|
||||
// The pinned SDK still owns snapshots and program metadata.
|
||||
// These hidden helpers have no public package exports. Derive their types
|
||||
// from Snapshot's constructor rather than duplicating the SDK's contracts.
|
||||
// This bridge can go away when those object models also compile statically;
|
||||
@@ -51,11 +51,16 @@ class SnapshotClient {
|
||||
}
|
||||
|
||||
apiRequestBinary(method: string, params: unknown): Uint8Array | undefined {
|
||||
const payload = Buffer.from(JSON.stringify(params), "utf8");
|
||||
const result = this.requestEncoded(method, JSON.stringify(params));
|
||||
return result.length === 0 ? undefined : result;
|
||||
}
|
||||
|
||||
requestEncoded(method: string, json: string): Uint8Array {
|
||||
const payload = Buffer.from(json, "utf8");
|
||||
const start = performance.now();
|
||||
const result = this.rpc.requestBytes(method, payload);
|
||||
this.timing?.record({ method, roundTripMs: performance.now() - start, bytesSent: payload.length, bytesReceived: result.length });
|
||||
return result.length === 0 ? undefined : result;
|
||||
return result;
|
||||
}
|
||||
|
||||
getTimingCollector(): TimingCollector | undefined {
|
||||
@@ -71,8 +76,8 @@ class SnapshotClient {
|
||||
}
|
||||
|
||||
/** The API operations scriptc uses, backed by its own synchronous client.
|
||||
* Keep upstream Snapshot/Project objects until their checker registries
|
||||
* have a native replacement. ASTs use the owned model. No upstream API
|
||||
* Keep upstream Snapshot/Project objects for lifecycle and metadata. ASTs
|
||||
* and checker registries use concrete native models. No upstream API
|
||||
* or Client instance is constructed, and all requests use Ts7RpcClient. */
|
||||
export class Ts7Api {
|
||||
private readonly client: SnapshotClient;
|
||||
@@ -116,10 +121,17 @@ export class Ts7Api {
|
||||
// The upstream class's private fields make its Client nominal. Only
|
||||
// these three public operations are used by Snapshot and its children.
|
||||
const snapshot = new Snapshot(data, this.client as unknown as SdkClient, this.cache, canonical, () => {
|
||||
releaseSemantic();
|
||||
this.snapshots.delete(snapshot);
|
||||
if (snapshot !== this.latest) this.cache.releaseSnapshot(snapshot.id);
|
||||
});
|
||||
for (const project of snapshot.getProjects()) installNativeAst(snapshot.id, project, this.client as unknown as SdkClient, this.cache, canonical);
|
||||
const releaseSemantic = installNativeAst(snapshot, this.client as unknown as SdkClient, this.cache, canonical, {
|
||||
text: (method, payload) => {
|
||||
const bytes = this.client.requestEncoded(method, payload);
|
||||
return bytes.length === 0 ? "null" : Buffer.from(bytes).toString("utf8");
|
||||
},
|
||||
binary: (method, payload) => this.client.requestEncoded(method, payload),
|
||||
});
|
||||
this.latest = snapshot;
|
||||
this.snapshots.add(snapshot);
|
||||
return snapshot;
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
import type { SemanticIndexInfoData as IndexInfoResponse, SemanticSignatureData as SignatureResponse, SemanticSymbolData as SymbolResponse, SemanticTypePredicateData as TypePredicateResponse, SemanticTypeData as TypeResponse } from "./semantic-schema.generated.js";
|
||||
import { AstFile, AstNode } from "./ast-node.js";
|
||||
import { SemanticNodeHandle, SemanticProject, SemanticSignature, SemanticSymbol, SemanticType, type SemanticDocument, type SemanticQuery } from "./semantic-model.js";
|
||||
import { SemanticSymbolFlags } from "./semantic-schema.generated.js";
|
||||
|
||||
export interface SemanticIndexInfo {
|
||||
keyType: SemanticType;
|
||||
valueType: SemanticType;
|
||||
isReadonly: boolean;
|
||||
declaration: SemanticNodeHandle | undefined;
|
||||
}
|
||||
|
||||
export interface SemanticTypePredicate {
|
||||
kind: number;
|
||||
parameterIndex: number;
|
||||
parameterName: string | undefined;
|
||||
type: SemanticType | undefined;
|
||||
}
|
||||
|
||||
export interface SemanticReferencedSymbol {
|
||||
definition: SemanticNodeHandle;
|
||||
symbol: SemanticSymbol | undefined;
|
||||
references: SemanticNodeHandle[];
|
||||
}
|
||||
|
||||
export interface SemanticSignatureUsage {
|
||||
name: SemanticNodeHandle;
|
||||
call: SemanticNodeHandle | undefined;
|
||||
}
|
||||
|
||||
export interface SemanticJsDocTag {
|
||||
name: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface WellKnownSymbols { unknown: number; undefined: number; arguments: number; }
|
||||
|
||||
export interface SemanticCompletionOptions {
|
||||
triggerCharacter?: string;
|
||||
includeSymbol?: boolean;
|
||||
}
|
||||
interface CompletionResponse {
|
||||
isIncomplete: boolean;
|
||||
entries: {
|
||||
name: string;
|
||||
kind?: number;
|
||||
sortText?: string;
|
||||
insertText?: string;
|
||||
filterText?: string;
|
||||
detail?: string;
|
||||
labelDetails?: { detail?: string; description?: string };
|
||||
symbol?: SymbolResponse;
|
||||
}[];
|
||||
}
|
||||
export interface SemanticCompletion {
|
||||
name: string;
|
||||
kind: number | undefined;
|
||||
sortText: string | undefined;
|
||||
insertText: string | undefined;
|
||||
filterText: string | undefined;
|
||||
detail: string | undefined;
|
||||
labelDetails: { detail?: string; description?: string } | undefined;
|
||||
symbol: SemanticSymbol | undefined;
|
||||
}
|
||||
|
||||
/** The pinned checker's request surface over native objects. Every query
|
||||
* converges on the same snapshot/project registries, including batch and
|
||||
* lazy-object queries. No SDK object is constructed or consulted here. */
|
||||
export class SemanticChecker {
|
||||
private wellKnown: WellKnownSymbols | undefined;
|
||||
|
||||
constructor(readonly project: SemanticProject) {}
|
||||
|
||||
private type(method: string, query: SemanticQuery): SemanticType | undefined {
|
||||
const data = this.project.request<TypeResponse | null>(method, query);
|
||||
return data === null ? undefined : this.project.type(data);
|
||||
}
|
||||
private symbol(method: string, query: SemanticQuery): SemanticSymbol | undefined {
|
||||
const data = this.project.request<SymbolResponse | null>(method, query);
|
||||
return data === null ? undefined : this.project.symbol(data);
|
||||
}
|
||||
private signature(method: string, query: SemanticQuery): SemanticSignature | undefined {
|
||||
const data = this.project.request<SignatureResponse | null>(method, query);
|
||||
return data === null ? undefined : this.project.signature(data);
|
||||
}
|
||||
private types(method: string, query: SemanticQuery): SemanticType[] {
|
||||
const data = this.project.request<TypeResponse[] | null>(method, query);
|
||||
return (data ?? []).map((item) => this.project.type(item));
|
||||
}
|
||||
private symbols(method: string, query: SemanticQuery): SemanticSymbol[] {
|
||||
const data = this.project.request<SymbolResponse[] | null>(method, query);
|
||||
return (data ?? []).map((item) => this.project.symbol(item));
|
||||
}
|
||||
private optionalTypes(method: string, query: SemanticQuery): (SemanticType | undefined)[] {
|
||||
const data = this.project.request<(TypeResponse | null)[]>(method, query);
|
||||
return data.map((item) => item === null ? undefined : this.project.type(item));
|
||||
}
|
||||
private optionalSymbols(method: string, query: SemanticQuery): (SemanticSymbol | undefined)[] {
|
||||
const data = this.project.request<(SymbolResponse | null)[]>(method, query);
|
||||
return data.map((item) => item === null ? undefined : this.project.symbol(item));
|
||||
}
|
||||
|
||||
getTypeAtLocation(node: AstNode): SemanticType | undefined;
|
||||
getTypeAtLocation(nodes: readonly AstNode[]): (SemanticType | undefined)[];
|
||||
getTypeAtLocation(input: AstNode | readonly AstNode[]): SemanticType | undefined | (SemanticType | undefined)[] {
|
||||
if (Array.isArray(input)) return this.optionalTypes("getTypeAtLocations", { locations: (input as readonly AstNode[]).map((node) => node.id) });
|
||||
return this.type("getTypeAtLocation", { location: (input as AstNode).id });
|
||||
}
|
||||
getSymbolAtLocation(node: AstNode): SemanticSymbol | undefined;
|
||||
getSymbolAtLocation(nodes: readonly AstNode[]): (SemanticSymbol | undefined)[];
|
||||
getSymbolAtLocation(input: AstNode | readonly AstNode[]): SemanticSymbol | undefined | (SemanticSymbol | undefined)[] {
|
||||
if (Array.isArray(input)) return this.optionalSymbols("getSymbolsAtLocations", { locations: (input as readonly AstNode[]).map((node) => node.id) });
|
||||
return this.symbol("getSymbolAtLocation", { location: (input as AstNode).id });
|
||||
}
|
||||
getTypeAtPosition(file: SemanticDocument, position: number): SemanticType | undefined;
|
||||
getTypeAtPosition(file: SemanticDocument, positions: readonly number[]): (SemanticType | undefined)[];
|
||||
getTypeAtPosition(file: SemanticDocument, positions: number | readonly number[]): SemanticType | undefined | (SemanticType | undefined)[] {
|
||||
return typeof positions === "number"
|
||||
? this.type("getTypeAtPosition", { file, position: positions })
|
||||
: this.optionalTypes("getTypesAtPositions", { file, positions });
|
||||
}
|
||||
getSymbolAtPosition(file: SemanticDocument, position: number): SemanticSymbol | undefined;
|
||||
getSymbolAtPosition(file: SemanticDocument, positions: readonly number[]): (SemanticSymbol | undefined)[];
|
||||
getSymbolAtPosition(file: SemanticDocument, positions: number | readonly number[]): SemanticSymbol | undefined | (SemanticSymbol | undefined)[] {
|
||||
return typeof positions === "number"
|
||||
? this.symbol("getSymbolAtPosition", { file, position: positions })
|
||||
: this.optionalSymbols("getSymbolsAtPositions", { file, positions });
|
||||
}
|
||||
getTypeOfSymbol(symbol: SemanticSymbol): SemanticType | undefined;
|
||||
getTypeOfSymbol(symbols: readonly SemanticSymbol[]): (SemanticType | undefined)[];
|
||||
getTypeOfSymbol(input: SemanticSymbol | readonly SemanticSymbol[]): SemanticType | undefined | (SemanticType | undefined)[] {
|
||||
if (Array.isArray(input)) return this.optionalTypes("getTypesOfSymbols", { symbols: (input as readonly SemanticSymbol[]).map((symbol) => symbol.id) });
|
||||
return this.type("getTypeOfSymbol", { symbol: (input as SemanticSymbol).id });
|
||||
}
|
||||
|
||||
getDeclaredTypeOfSymbol(symbol: SemanticSymbol): SemanticType {
|
||||
const type = this.type("getDeclaredTypeOfSymbol", { symbol: symbol.id });
|
||||
if (type === undefined) throw new Error(`getDeclaredTypeOfSymbol returned no type for symbol ${symbol.id}`);
|
||||
return type;
|
||||
}
|
||||
getTypeOfSymbolAtLocation(symbol: SemanticSymbol, node: AstNode): SemanticType {
|
||||
const type = this.type("getTypeOfSymbolAtLocation", { symbol: symbol.id, location: node.id });
|
||||
if (type === undefined) throw new Error(`getTypeOfSymbolAtLocation returned no type for symbol ${symbol.id}`);
|
||||
return type;
|
||||
}
|
||||
getContextualType(node: AstNode): SemanticType | undefined { return this.type("getContextualType", { location: node.id }); }
|
||||
getTypeFromTypeNode(node: AstNode): SemanticType | undefined { return this.type("getTypeFromTypeNode", { location: node.id }); }
|
||||
getShorthandAssignmentValueSymbol(node: AstNode): SemanticSymbol | undefined { return this.symbol("getShorthandAssignmentValueSymbol", { location: node.id }); }
|
||||
getExportSpecifierLocalTargetSymbol(node: AstNode): SemanticSymbol | undefined { return this.symbol("getExportSpecifierLocalTargetSymbol", { location: node.id }); }
|
||||
getResolvedSignature(node: AstNode): SemanticSignature | undefined { return this.signature("getResolvedSignature", { location: node.id }); }
|
||||
getSignatureFromDeclaration(node: AstNode): SemanticSignature | undefined { return this.signature("getSignatureFromDeclaration", { location: node.id }); }
|
||||
getReturnTypeOfSignature(signature: SemanticSignature): SemanticType | undefined { return this.type("getReturnTypeOfSignature", { signature: signature.id }); }
|
||||
getRestTypeOfSignature(signature: SemanticSignature): SemanticType | undefined { return this.type("getRestTypeOfSignature", { signature: signature.id }); }
|
||||
getParameterType(signature: SemanticSignature, index: number): SemanticType | undefined { return this.type("getParameterType", { signature: signature.id, index }); }
|
||||
getBaseTypeOfLiteralType(type: SemanticType): SemanticType | undefined { return this.type("getBaseTypeOfLiteralType", { type: type.id }); }
|
||||
getNonNullableType(type: SemanticType): SemanticType | undefined { return this.type("getNonNullableType", { type: type.id }); }
|
||||
getWidenedType(type: SemanticType): SemanticType | undefined { return this.type("getWidenedType", { type: type.id }); }
|
||||
getApparentType(type: SemanticType): SemanticType | undefined { return this.type("getApparentType", { type: type.id }); }
|
||||
getConstraintOfTypeParameter(type: SemanticType): SemanticType | undefined { return this.type("getConstraintOfTypeParameter", { type: type.id }); }
|
||||
getBaseConstraintOfType(type: SemanticType): SemanticType | undefined { return this.type("getBaseConstraintOfType", { type: type.id }); }
|
||||
getBaseTypes(type: SemanticType): SemanticType[] { return this.types("getBaseTypes", { type: type.id }); }
|
||||
getTypeArguments(type: SemanticType): SemanticType[] { return this.types("getTypeArguments", { type: type.id }); }
|
||||
getPropertiesOfType(type: SemanticType): SemanticSymbol[] { return this.symbols("getPropertiesOfType", { type: type.id }); }
|
||||
getPropertyOfType(type: SemanticType, name: string): SemanticSymbol | undefined { return this.symbol("getPropertyOfType", { type: type.id, name }); }
|
||||
getExportsOfModule(symbol: SemanticSymbol): SemanticSymbol[] { return this.symbols("getExportsOfModule", { symbol: symbol.id }); }
|
||||
getMemberInModuleExports(symbol: SemanticSymbol, name: string): SemanticSymbol | undefined { return this.symbol("getMemberInModuleExports", { symbol: symbol.id, name }); }
|
||||
getImmediateAliasedSymbol(symbol: SemanticSymbol): SemanticSymbol | undefined { return this.symbol("getImmediateAliasedSymbol", { symbol: symbol.id }); }
|
||||
|
||||
getAliasedSymbol(symbol: SemanticSymbol): SemanticSymbol {
|
||||
const result = this.symbol("getAliasedSymbol", { symbol: symbol.id });
|
||||
if (result === undefined) throw new Error(`getAliasedSymbol returned no symbol for symbol ${symbol.id}`);
|
||||
return result;
|
||||
}
|
||||
resolveName(name: string, meaning: number, location?: AstNode | { document: SemanticDocument; position: number }, excludeGlobals?: boolean): SemanticSymbol | undefined {
|
||||
const node = location instanceof AstNode ? location : undefined;
|
||||
const position = location !== undefined && !(location instanceof AstNode) ? location : undefined;
|
||||
return this.symbol("resolveName", { name, meaning, location: node?.id, file: position?.document, position: position?.position, excludeGlobals });
|
||||
}
|
||||
getResolvedSymbol(node: AstNode): SemanticSymbol | undefined {
|
||||
const text = node.text;
|
||||
return text ? this.resolveName(text, SemanticSymbolFlags.Value | SemanticSymbolFlags.ExportValue, node) : undefined;
|
||||
}
|
||||
|
||||
private intrinsic(method: string): SemanticType {
|
||||
const result = this.type(method, {});
|
||||
if (result === undefined) throw new Error(`${method} returned no intrinsic type`);
|
||||
return result;
|
||||
}
|
||||
getAnyType(): SemanticType { return this.intrinsic("getAnyType"); }
|
||||
getStringType(): SemanticType { return this.intrinsic("getStringType"); }
|
||||
getNumberType(): SemanticType { return this.intrinsic("getNumberType"); }
|
||||
getBooleanType(): SemanticType { return this.intrinsic("getBooleanType"); }
|
||||
getVoidType(): SemanticType { return this.intrinsic("getVoidType"); }
|
||||
getUndefinedType(): SemanticType { return this.intrinsic("getUndefinedType"); }
|
||||
getNullType(): SemanticType { return this.intrinsic("getNullType"); }
|
||||
getNeverType(): SemanticType { return this.intrinsic("getNeverType"); }
|
||||
getUnknownType(): SemanticType { return this.intrinsic("getUnknownType"); }
|
||||
getBigIntType(): SemanticType { return this.intrinsic("getBigIntType"); }
|
||||
getESSymbolType(): SemanticType { return this.intrinsic("getESSymbolType"); }
|
||||
|
||||
typeToString(type: SemanticType, enclosingDeclaration?: AstNode, flags?: number): string {
|
||||
return this.project.request<string>("typeToString", { type: type.id, location: enclosingDeclaration?.id, flags });
|
||||
}
|
||||
isContextSensitive(node: AstNode): boolean { return this.project.request<boolean>("isContextSensitive", { location: node.id }); }
|
||||
isArrayType(type: SemanticType): boolean { return this.project.request<boolean>("isArrayType", { type: type.id }); }
|
||||
isArrayLikeType(type: SemanticType): boolean { return this.project.request<boolean>("isArrayLikeType", { type: type.id }); }
|
||||
isTupleType(type: SemanticType): boolean { return this.project.request<boolean>("isTupleType", { type: type.id }); }
|
||||
isTypeAssignableTo(source: SemanticType, target: SemanticType): boolean {
|
||||
return this.project.request<boolean>("isTypeAssignableTo", { source: source.id, target: target.id });
|
||||
}
|
||||
getConstantValue(node: AstNode): string | number | undefined {
|
||||
return this.project.request<string | number | null>("getConstantValue", { location: node.id }) ?? undefined;
|
||||
}
|
||||
getSignaturesOfType(type: SemanticType, kind: number): SemanticSignature[] {
|
||||
const data = this.project.request<SignatureResponse[]>("getSignaturesOfType", { type: type.id, kind });
|
||||
return data.map((item) => this.project.signature(item));
|
||||
}
|
||||
getTypePredicateOfSignature(signature: SemanticSignature): SemanticTypePredicate | undefined {
|
||||
const data = this.project.request<TypePredicateResponse | 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.type(data.type),
|
||||
};
|
||||
}
|
||||
getIndexInfosOfType(type: SemanticType): SemanticIndexInfo[] {
|
||||
const data = this.project.request<IndexInfoResponse[] | null>("getIndexInfosOfType", { type: type.id });
|
||||
return (data ?? []).map((item) => ({
|
||||
keyType: this.project.type(item.keyType), valueType: this.project.type(item.valueType),
|
||||
isReadonly: item.isReadonly ?? false,
|
||||
declaration: item.declaration === undefined ? undefined : new SemanticNodeHandle(item.declaration, this.project),
|
||||
}));
|
||||
}
|
||||
private getWellKnownSymbols(): WellKnownSymbols {
|
||||
this.project.ensureActive();
|
||||
if (this.wellKnown === undefined) this.wellKnown = this.project.request<WellKnownSymbols>("getWellKnownSymbols");
|
||||
return this.wellKnown;
|
||||
}
|
||||
isUnknownSymbol(symbol: SemanticSymbol): boolean { return symbol.id === this.getWellKnownSymbols().unknown; }
|
||||
isUndefinedSymbol(symbol: SemanticSymbol): boolean { return symbol.id === this.getWellKnownSymbols().undefined; }
|
||||
isArgumentsSymbol(symbol: SemanticSymbol): boolean { return symbol.id === this.getWellKnownSymbols().arguments; }
|
||||
|
||||
getReferencesToSymbolInFile(file: SemanticDocument, symbol: SemanticSymbol): SemanticNodeHandle[] {
|
||||
const data = this.project.request<string[] | null>("getReferencesToSymbolInFile", { file, symbol: symbol.id });
|
||||
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 });
|
||||
return (data ?? []).map((item) => ({
|
||||
definition: new SemanticNodeHandle(item.definition, this.project),
|
||||
symbol: item.symbol === undefined ? undefined : this.project.symbol(item.symbol),
|
||||
references: (item.references ?? []).map((handle) => new SemanticNodeHandle(handle, this.project)),
|
||||
}));
|
||||
}
|
||||
getSignatureUsage(node: AstNode): SemanticSignatureUsage[] {
|
||||
const data = this.project.request<{ name: string; call?: string }[] | null>("getSignatureUsages", { signatureDecl: node.id });
|
||||
return (data ?? []).map((item) => ({
|
||||
name: new SemanticNodeHandle(item.name, this.project),
|
||||
call: item.call === undefined ? undefined : new SemanticNodeHandle(item.call, this.project),
|
||||
}));
|
||||
}
|
||||
getJsDocTagsOfSymbol(symbol: SemanticSymbol): SemanticJsDocTag[] {
|
||||
return this.project.request<SemanticJsDocTag[] | null>("getJsDocTags", { symbol: symbol.id }) ?? [];
|
||||
}
|
||||
getDocumentationCommentOfSymbol(symbol: SemanticSymbol): string {
|
||||
return this.project.request<string>("getDocumentationComment", { symbol: symbol.id });
|
||||
}
|
||||
getCompletionsAtPosition(file: SemanticDocument, position: number, options?: SemanticCompletionOptions): { isIncomplete: boolean; entries: SemanticCompletion[] } | undefined {
|
||||
const data = this.project.request<CompletionResponse | null>("getCompletionsAtPosition", { file, position, triggerCharacter: options?.triggerCharacter, includeSymbol: options?.includeSymbol });
|
||||
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.symbol(item.symbol),
|
||||
})),
|
||||
};
|
||||
}
|
||||
typeToTypeNode(type: SemanticType, enclosingDeclaration?: AstNode, flags?: number): AstNode | undefined {
|
||||
const bytes = this.project.requestBinary("typeToTypeNode", { type: type.id, location: enclosingDeclaration?.id, flags });
|
||||
return bytes.length === 0 ? undefined : new AstFile(bytes).root;
|
||||
}
|
||||
signatureToSignatureDeclaration(signature: SemanticSignature, kind: number, enclosingDeclaration?: AstNode, flags?: number): AstNode | undefined {
|
||||
const bytes = this.project.requestBinary("signatureToSignatureDeclaration", { signature: signature.id, kind, location: enclosingDeclaration?.id, flags });
|
||||
return bytes.length === 0 ? undefined : new AstFile(bytes).root;
|
||||
}
|
||||
dispose(): void { this.project.dispose(); }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { checkSemanticJsonStrings, parseSemanticJson } from "./semantic-json.js";
|
||||
|
||||
test("semantic JSON preserves ordinary strings, escaped slashes, quotes, pairs and nulls", () => {
|
||||
const values = ["", "\uFEFFhéllo 🌍", "\\ud800", 'quoted "\\ud800"', "\ud800\udc00", "\udbff\udfff", "\\", null];
|
||||
for (const value of values) {
|
||||
const json = JSON.stringify({ value });
|
||||
expect(parseSemanticJson(json)).toEqual({ value });
|
||||
expect(() => checkSemanticJsonStrings(json, false)).not.toThrow();
|
||||
}
|
||||
for (const json of ['"\\ud800\\udc00"', '"\\uDBFF\\uDFFF"', '{"\\uD800\\uDC00":"ok"}']) {
|
||||
expect(() => checkSemanticJsonStrings(json, false)).not.toThrow();
|
||||
expect(parseSemanticJson(json)).toEqual(JSON.parse(json));
|
||||
}
|
||||
});
|
||||
|
||||
test("all lone code units remain exact on Node and are refused at the native boundary", () => {
|
||||
for (let unit = 0xd800; unit <= 0xdfff; unit++) {
|
||||
const text = String.fromCharCode(unit);
|
||||
for (const value of [text, { [text]: "key", value: text }, [text, "after"]]) {
|
||||
const json = JSON.stringify(value);
|
||||
expect(parseSemanticJson(json)).toEqual(value);
|
||||
expect(() => checkSemanticJsonStrings(json, false)).toThrow("cannot preserve lone UTF-16 surrogates");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("backslash parity and separated surrogate escapes cannot bypass the boundary", () => {
|
||||
for (let count = 1; count <= 8; count++) {
|
||||
const json = '"' + "\\".repeat(count) + 'ud800"';
|
||||
if (count % 2 === 0) expect(() => checkSemanticJsonStrings(json, false)).not.toThrow();
|
||||
else expect(() => checkSemanticJsonStrings(json, false)).toThrow("cannot preserve");
|
||||
}
|
||||
for (const json of ['"\\ud800x\\udc00"', '["\\ud800","\\udc00"]', '"\\ud800\\ud800"', '"\\udc00\\ud800"']) {
|
||||
expect(() => checkSemanticJsonStrings(json, false)).toThrow("cannot preserve");
|
||||
}
|
||||
});
|
||||
|
||||
test("malformed JSON remains a parse error rather than an accepted response", () => {
|
||||
for (const json of ['"\\uZZZZ"', '"\\uD8"', '"unclosed', '{"value":}', 'undefined']) {
|
||||
expect(() => parseSemanticJson(json)).toThrow(SyntaxError);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/** Check the only JSON string form the native runtime cannot yet retain.
|
||||
* JSON.parse itself validates grammar. A valid surrogate pair is preserved;
|
||||
* a lone escape must not silently change a type literal or symbol name. */
|
||||
export function checkSemanticJsonStrings(json: string, preservesSurrogates: boolean): void {
|
||||
if (preservesSurrogates) return;
|
||||
let inString = false;
|
||||
for (let index = 0; index < json.length; index++) {
|
||||
const code = json.charCodeAt(index);
|
||||
if (code === 34) { inString = !inString; continue; }
|
||||
if (!inString || code !== 92) continue;
|
||||
if (json.charCodeAt(++index) !== 117) continue;
|
||||
const unit = jsonHexUnit(json, index + 1);
|
||||
index += 4;
|
||||
if (unit < 0xd800 || unit > 0xdfff) continue;
|
||||
if (unit <= 0xdbff && json.charCodeAt(index + 1) === 92 && json.charCodeAt(index + 2) === 117) {
|
||||
const next = jsonHexUnit(json, index + 3);
|
||||
if (next >= 0xdc00 && next <= 0xdfff) { index += 6; continue; }
|
||||
}
|
||||
throw new Error("TypeScript semantic response: runtime cannot preserve lone UTF-16 surrogates");
|
||||
}
|
||||
}
|
||||
|
||||
function jsonHexUnit(json: string, offset: number): number {
|
||||
if (offset + 4 > json.length) return -1;
|
||||
let value = 0;
|
||||
for (let index = offset; index < offset + 4; index++) {
|
||||
const code = json.charCodeAt(index);
|
||||
const digit = code >= 48 && code <= 57 ? code - 48 : code >= 65 && code <= 70 ? code - 55 : code >= 97 && code <= 102 ? code - 87 : -1;
|
||||
if (digit < 0) return -1;
|
||||
value = value * 16 + digit;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Capability-based so the boundary automatically disappears once native
|
||||
// strings and JSON parsing support lossless UTF-16. Probe once per process.
|
||||
const preservesJsonSurrogates = (JSON.parse('"\\ud800"') as string).charCodeAt(0) === 0xd800;
|
||||
|
||||
export function parseSemanticJson<T>(json: string): T {
|
||||
checkSemanticJsonStrings(json, preservesJsonSurrogates);
|
||||
return JSON.parse(json) as T;
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { SymbolResponse, TypeResponse } from "typescript/unstable/proto";
|
||||
import { isBigIntLiteralType, isBooleanLiteralType, isClassOrInterfaceType, isConditionalType, isErrorType, isIndexType, isIndexedAccessType, isIntersectionType, isIntrinsicType, isLiteralType, isNumberLiteralType, isObjectType, isStringLiteralType, isStringMappingType, isSubstitutionType, isTemplateLiteralType, isTupleType, isTypeParameter, isTypeReference, isUnionType, type Type } from "typescript/unstable/sync";
|
||||
import { SemanticNodeHandle, SemanticSnapshot, SemanticType } from "./semantic-model.js";
|
||||
import { SemanticObjectFlags as ObjectFlags, SemanticSignatureFlags as SignatureFlags, SemanticTypeFlags as TypeFlags } from "./semantic-schema.generated.js";
|
||||
|
||||
function harness() {
|
||||
const requests: { method: string; query: Record<string, unknown> }[] = [];
|
||||
const answers = new Map<string, unknown>();
|
||||
const snapshot = new SemanticSnapshot(17, {
|
||||
text(method, payload) {
|
||||
requests.push({ method, query: JSON.parse(payload) });
|
||||
if (!answers.has(method)) throw new Error(`Unexpected request ${method}`);
|
||||
return JSON.stringify(answers.get(method));
|
||||
},
|
||||
binary() { throw new Error("Unexpected binary request"); },
|
||||
});
|
||||
const first = snapshot.addProject("/first.json", () => undefined);
|
||||
const second = snapshot.addProject("/second.json", () => undefined);
|
||||
const symbol = (id: number, extra: Partial<SymbolResponse> = {}): SymbolResponse => ({
|
||||
id, flags: 2, checkFlags: 0, name: `name${id}`, project: first.id, ...extra,
|
||||
} as SymbolResponse);
|
||||
return { snapshot, first, second, requests, answers, symbol };
|
||||
}
|
||||
|
||||
test("types/signatures are project-local and symbols are snapshot-wide", () => {
|
||||
const { snapshot, first, second, symbol } = harness();
|
||||
const data = { id: 1, flags: TypeFlags.Number };
|
||||
expect(first.type(data)).toBe(first.type({ ...data }));
|
||||
expect(second.type(data)).not.toBe(first.type(data));
|
||||
expect(first.signature({ id: 1, flags: 0 })).toBe(first.signature({ id: 1, flags: 0 }));
|
||||
expect(second.signature({ id: 1, flags: 0 })).not.toBe(first.signature({ id: 1, flags: 0 }));
|
||||
expect(second.symbol(symbol(1))).toBe(first.symbol(symbol(1)));
|
||||
expect(second.symbol(symbol(1)).canonicalProject).toBe(first);
|
||||
const separate = new SemanticSnapshot(snapshot.id, snapshot.transport);
|
||||
const third = separate.addProject(first.id, () => undefined);
|
||||
expect(third.symbol(symbol(1))).not.toBe(first.symbol(symbol(1)));
|
||||
expect(third.type(data)).not.toBe(first.type(data));
|
||||
});
|
||||
|
||||
test("canonical projects must exist before a symbol is registered", () => {
|
||||
const { first, symbol, snapshot } = harness();
|
||||
const missing = symbol(2, { project: "/later.json" as SymbolResponse["project"] });
|
||||
expect(() => first.symbol(missing)).toThrow("Unknown TypeScript canonical project");
|
||||
const later = snapshot.addProject("/later.json", () => undefined);
|
||||
expect(first.symbol(missing).canonicalProject).toBe(later);
|
||||
expect(() => snapshot.addProject(later.id, () => undefined)).toThrow("Duplicate TypeScript project");
|
||||
});
|
||||
|
||||
test("absent handles and empty known lists do not issue requests", () => {
|
||||
const { first, requests, symbol } = harness();
|
||||
const type = first.type({ id: 1, flags: TypeFlags.Number });
|
||||
expect(type.getSymbol()).toBeUndefined();
|
||||
expect(type.getAliasSymbol()).toBeUndefined();
|
||||
expect(type.getTarget()).toBeUndefined();
|
||||
expect(type.getFreshType()).toBeUndefined();
|
||||
expect(type.getRegularType()).toBeUndefined();
|
||||
expect(type.getObjectType()).toBeUndefined();
|
||||
expect(type.getIndexType()).toBeUndefined();
|
||||
expect(type.getCheckType()).toBeUndefined();
|
||||
expect(type.getExtendsType()).toBeUndefined();
|
||||
expect(type.getBaseType()).toBeUndefined();
|
||||
expect(type.getConstraint()).toBeUndefined();
|
||||
expect(type.getTypes()).toBeUndefined();
|
||||
expect(type.getBaseTypes()).toBeUndefined();
|
||||
expect(type.getTypeParameters()).toEqual([]);
|
||||
expect(type.getOuterTypeParameters()).toEqual([]);
|
||||
expect(type.getLocalTypeParameters()).toEqual([]);
|
||||
expect(type.getAliasTypeArguments()).toEqual([]);
|
||||
const signature = first.signature({ id: 1, flags: 0 });
|
||||
expect(signature.getTypeParameters()).toEqual([]);
|
||||
expect(signature.getParameters()).toEqual([]);
|
||||
expect(signature.getThisParameter()).toBeUndefined();
|
||||
expect(signature.getTarget()).toBeUndefined();
|
||||
const sym = first.symbol(symbol(1));
|
||||
expect(sym.getParent()).toBeUndefined();
|
||||
expect(sym.getExportSymbol()).toBe(sym);
|
||||
expect(requests).toEqual([]);
|
||||
});
|
||||
|
||||
describe("lazy type handles", () => {
|
||||
const cases = [
|
||||
["symbol", "getSymbolOfType", "getSymbol"],
|
||||
["aliasSymbol", "getAliasSymbolOfType", "getAliasSymbol"],
|
||||
["target", "getTargetOfType", "getTarget"],
|
||||
["freshType", "getFreshTypeOfType", "getFreshType"],
|
||||
["regularType", "getRegularTypeOfType", "getRegularType"],
|
||||
["objectType", "getObjectTypeOfType", "getObjectType"],
|
||||
["indexType", "getIndexTypeOfType", "getIndexType"],
|
||||
["checkType", "getCheckTypeOfType", "getCheckType"],
|
||||
["extendsType", "getExtendsTypeOfType", "getExtendsType"],
|
||||
["baseType", "getBaseTypeOfType", "getBaseType"],
|
||||
["substConstraint", "getConstraintOfType", "getConstraint"],
|
||||
] as const;
|
||||
for (const [field, method, accessor] of cases) {
|
||||
test(`${accessor} resolves once and reuses the correct identity registry`, () => {
|
||||
const { first, second, answers, requests, symbol } = harness();
|
||||
const isSymbol = field === "symbol" || field === "aliasSymbol";
|
||||
const data = isSymbol ? symbol(2) : { id: 2, flags: TypeFlags.String };
|
||||
answers.set(method, data);
|
||||
const type = first.type({ id: 1, flags: 0, [field]: 2 });
|
||||
const result = type[accessor]();
|
||||
expect(type[accessor]()).toBe(result);
|
||||
expect(requests).toEqual([{ method, query: { snapshot: 17, project: first.id, objectId: 1 } }]);
|
||||
expect(result).toBe(isSymbol ? second.symbol(data as SymbolResponse) : first.type(data as TypeResponse));
|
||||
if (!isSymbol) expect(second.type(data as TypeResponse)).not.toBe(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("a partially cached type list fetches once, preserves order and duplicates", () => {
|
||||
const { first, answers, requests } = harness();
|
||||
const one = first.type({ id: 1, flags: TypeFlags.String });
|
||||
const two = { id: 2, flags: TypeFlags.Number };
|
||||
const data = { id: 3, flags: TypeFlags.Object, typeParameters: [2, 1, 2] };
|
||||
answers.set("getTypeParametersOfType", [two, { id: one.id, flags: one.flags }, two]);
|
||||
const type = first.type(data);
|
||||
const parameters = type.getTypeParameters();
|
||||
expect(parameters).toEqual([first.type(two), one, first.type(two)]);
|
||||
expect(type.getTypeParameters()).toEqual(parameters);
|
||||
expect(requests).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("unknown constituent lists fetch while non-constituent types stay local", () => {
|
||||
const { first, answers, requests } = harness();
|
||||
answers.set("getTypesOfType", [{ id: 2, flags: TypeFlags.String }]);
|
||||
for (const flags of [TypeFlags.Union, TypeFlags.Intersection, TypeFlags.TemplateLiteral]) {
|
||||
const type = first.type({ id: flags, flags });
|
||||
expect(type.getTypes()?.[0]).toBe(first.type({ id: 2, flags: TypeFlags.String }));
|
||||
}
|
||||
expect(requests).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("conditional branches have independent lazy state, including a retry after refusal", () => {
|
||||
const { first, answers, requests } = harness();
|
||||
const type = first.type({ id: 3, flags: TypeFlags.Conditional });
|
||||
answers.set("getTrueTypeOfConditionalType", null);
|
||||
expect(() => type.getTrueType()).toThrow("returned null type");
|
||||
answers.set("getTrueTypeOfConditionalType", { id: 1, flags: TypeFlags.String });
|
||||
answers.set("getFalseTypeOfConditionalType", { id: 2, flags: TypeFlags.Number });
|
||||
const yes = type.getTrueType();
|
||||
const no = type.getFalseType();
|
||||
expect(yes).not.toBe(no);
|
||||
expect(type.getTrueType()).toBe(yes);
|
||||
expect(type.getFalseType()).toBe(no);
|
||||
expect(requests.map((request) => request.method)).toEqual(["getTrueTypeOfConditionalType", "getTrueTypeOfConditionalType", "getFalseTypeOfConditionalType"]);
|
||||
});
|
||||
|
||||
test("base types use the checker type field, not an objectId request", () => {
|
||||
const { first, answers, requests } = harness();
|
||||
const type = first.type({ id: 1, flags: TypeFlags.Object, objectFlags: ObjectFlags.Interface });
|
||||
answers.set("getBaseTypes", [{ id: 2, flags: TypeFlags.Object, objectFlags: ObjectFlags.Class }]);
|
||||
expect(type.getBaseTypes()?.[0]?.isClassOrInterface()).toBe(true);
|
||||
expect(requests).toEqual([{ method: "getBaseTypes", query: { snapshot: 17, project: first.id, type: 1 } }]);
|
||||
});
|
||||
|
||||
test("symbol tables keep escaped keys, display names, canonical context and cache identity", () => {
|
||||
const { first, second, answers, requests, symbol } = harness();
|
||||
const parent = second.symbol(symbol(1, { parent: 2, exportSymbol: 3 }));
|
||||
answers.set("getMembersOfSymbol", [symbol(4, { name: "___x" as SymbolResponse["name"] })]);
|
||||
answers.set("getExportsOfSymbol", [symbol(5, { name: "__x" as SymbolResponse["name"] })]);
|
||||
answers.set("getParentOfSymbol", symbol(2));
|
||||
answers.set("getExportSymbolOfSymbol", symbol(3));
|
||||
const members = parent.getMembers();
|
||||
const exports = parent.getExports();
|
||||
expect(members.get("___x")?.name).toBe("__x");
|
||||
expect(exports.get("__x")?.name).toBe("__x");
|
||||
expect(parent.getMembers()).toBe(members);
|
||||
expect(parent.getExports()).toBe(exports);
|
||||
expect(parent.getParent()).toBe(first.symbol(symbol(2)));
|
||||
expect(parent.getExportSymbol()).toBe(first.symbol(symbol(3)));
|
||||
expect(requests.every((request) => request.query["project"] === first.id)).toBe(true);
|
||||
expect(requests).toHaveLength(4);
|
||||
});
|
||||
|
||||
test("signature handles use type, symbol and signature registries independently", () => {
|
||||
const { first, answers, requests, symbol } = harness();
|
||||
const signature = first.signature({ id: 1, flags: SignatureFlags.HasRestParameter | SignatureFlags.Construct | SignatureFlags.Abstract, typeParameters: [2], parameters: [2, 3], thisParameter: 3, target: 2 });
|
||||
answers.set("getTypeParametersOfSignature", [{ id: 2, flags: TypeFlags.TypeParameter }]);
|
||||
answers.set("getParametersOfSignature", [symbol(2), symbol(3)]);
|
||||
answers.set("getTargetOfSignature", { id: 2, flags: 0 });
|
||||
expect(signature.getTypeParameters()[0]?.isTypeParameter()).toBe(true);
|
||||
const parameters = signature.getParameters();
|
||||
expect(signature.getThisParameter()).toBe(parameters[1]);
|
||||
expect(signature.getParameters()).toEqual(parameters);
|
||||
expect(signature.getTypeParameters()[0]).toBe(first.type({ id: 2, flags: TypeFlags.TypeParameter }));
|
||||
expect(signature.getTarget()).toBe(first.signature({ id: 2, flags: 0 }));
|
||||
expect(signature.getTarget()).not.toBe(signature);
|
||||
expect(signature.hasRestParameter && signature.isConstruct && signature.isAbstract).toBe(true);
|
||||
expect(requests).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("literal metadata preserves zero, false, empty strings and arbitrary precision integers", () => {
|
||||
const { first } = harness();
|
||||
const values = [
|
||||
{ flags: TypeFlags.StringLiteral, input: "", expected: "" },
|
||||
{ flags: TypeFlags.NumberLiteral, input: 0, expected: 0 },
|
||||
{ flags: TypeFlags.BooleanLiteral, input: false, expected: false },
|
||||
{ flags: TypeFlags.BigIntLiteral, input: "-123456789012345678901234567890", expected: -123456789012345678901234567890n },
|
||||
];
|
||||
for (const [index, { flags, input, expected }] of values.entries()) {
|
||||
expect(first.type({ id: index + 1, flags, value: input }).value).toBe(expected);
|
||||
}
|
||||
expect(first.type({ id: 6, flags: TypeFlags.String, value: null }).value).toBeUndefined();
|
||||
});
|
||||
|
||||
test("native semantic predicates match all pinned TypeScript flag classifications", () => {
|
||||
const { first } = harness();
|
||||
const predicates = [
|
||||
["isUnionType", isUnionType], ["isIntersectionType", isIntersectionType], ["isObjectType", isObjectType],
|
||||
["isClassOrInterface", isClassOrInterfaceType], ["isIntrinsicType", isIntrinsicType], ["isErrorType", isErrorType],
|
||||
["isLiteralType", isLiteralType], ["isStringLiteralType", isStringLiteralType], ["isNumberLiteralType", isNumberLiteralType],
|
||||
["isBigIntLiteralType", isBigIntLiteralType], ["isBooleanLiteralType", isBooleanLiteralType],
|
||||
["isTypeReference", isTypeReference], ["isTupleType", isTupleType], ["isIndexType", isIndexType],
|
||||
["isIndexedAccessType", isIndexedAccessType], ["isConditionalType", isConditionalType], ["isSubstitutionType", isSubstitutionType],
|
||||
["isTemplateLiteralType", isTemplateLiteralType], ["isStringMappingType", isStringMappingType], ["isTypeParameter", isTypeParameter],
|
||||
] as const;
|
||||
let id = 0;
|
||||
for (const flags of Object.values(TypeFlags)) for (const objectFlags of [0, ObjectFlags.Class, ObjectFlags.Interface, ObjectFlags.Reference, ObjectFlags.Tuple]) {
|
||||
const type = first.type({ id: ++id, flags, objectFlags, intrinsicName: "error" });
|
||||
for (const [name, oracle] of predicates) expect(type[name](), `${name} flags=${flags} objectFlags=${objectFlags}`).toBe(oracle(type as unknown as Type));
|
||||
}
|
||||
});
|
||||
|
||||
test("project disposal cannot poison another project, and snapshot disposal seals every cache", () => {
|
||||
const { snapshot, first, second, symbol, requests } = harness();
|
||||
const old = first.type({ id: 1, flags: TypeFlags.Number });
|
||||
const shared = first.symbol(symbol(1));
|
||||
first.dispose();
|
||||
first.dispose();
|
||||
expect(() => old.getTarget()).toThrow("project is disposed");
|
||||
expect(() => first.type({ id: 1, flags: TypeFlags.Number })).toThrow("project is disposed");
|
||||
expect(second.type({ id: 1, flags: TypeFlags.String })).toBeInstanceOf(SemanticType);
|
||||
expect(second.symbol(symbol(1))).toBe(shared);
|
||||
snapshot.dispose();
|
||||
snapshot.dispose();
|
||||
expect(() => second.type({ id: 1, flags: TypeFlags.Number })).toThrow("snapshot is disposed");
|
||||
expect(() => shared.getMembers()).toThrow("snapshot is disposed");
|
||||
expect(() => snapshot.addProject("later", () => undefined)).toThrow("snapshot is disposed");
|
||||
expect(requests).toEqual([]);
|
||||
});
|
||||
|
||||
test("node handles validate their serialized identity and retain canonical context", () => {
|
||||
const { first } = harness();
|
||||
const handle = new SemanticNodeHandle("7.79./file.with.dots.ts", first);
|
||||
expect([handle.index, handle.kind, handle.path]).toEqual([7, 79, "/file.with.dots.ts"]);
|
||||
expect(handle.resolve()).toBeUndefined();
|
||||
expect(() => new SemanticNodeHandle("bad", first)).toThrow();
|
||||
first.dispose();
|
||||
expect(() => handle.resolve()).toThrow("disposed");
|
||||
});
|
||||
@@ -0,0 +1,413 @@
|
||||
import type { SemanticSignatureData as SignatureResponse, SemanticSymbolData as SymbolResponse, SemanticTypeData as TypeResponse } from "./semantic-schema.generated.js";
|
||||
import { AstNode } from "./ast-node.js";
|
||||
import { parseAstNodeHandle } from "./ast-wire.js";
|
||||
import type { SemanticChecker, SemanticJsDocTag } from "./semantic-checker.js";
|
||||
import { parseSemanticJson } from "./semantic-json.js";
|
||||
import { SemanticObjectFlags as ObjectFlags, SemanticSignatureFlags as SignatureFlags, SemanticTypeFlags as TypeFlags } from "./semantic-schema.generated.js";
|
||||
|
||||
/** JSON and binary operations stay at the transport boundary. The concrete
|
||||
* semantic objects below need neither the SDK nor a JavaScript engine. */
|
||||
export interface SemanticTransport {
|
||||
text: (method: string, payload: string) => string;
|
||||
binary: (method: string, payload: string) => Uint8Array;
|
||||
}
|
||||
|
||||
export type SemanticDocument = string | { uri: string };
|
||||
|
||||
/** The pinned checker protocol's request fields. A single concrete record
|
||||
* keeps generic response decoding independent of structural object spread. */
|
||||
export interface SemanticQuery {
|
||||
snapshot?: number | undefined;
|
||||
project?: string | undefined;
|
||||
objectId?: number | undefined;
|
||||
location?: string | undefined;
|
||||
locations?: string[] | undefined;
|
||||
file?: SemanticDocument | undefined;
|
||||
position?: number | undefined;
|
||||
positions?: readonly number[] | undefined;
|
||||
type?: number | undefined;
|
||||
symbol?: number | undefined;
|
||||
symbols?: number[] | undefined;
|
||||
signature?: number | undefined;
|
||||
source?: number | undefined;
|
||||
target?: number | undefined;
|
||||
kind?: number | undefined;
|
||||
name?: string | undefined;
|
||||
flags?: number | undefined;
|
||||
meaning?: number | undefined;
|
||||
excludeGlobals?: boolean | undefined;
|
||||
index?: number | undefined;
|
||||
node?: string | undefined;
|
||||
signatureDecl?: string | undefined;
|
||||
triggerCharacter?: string | undefined;
|
||||
includeSymbol?: boolean | undefined;
|
||||
}
|
||||
|
||||
export class SemanticProgram {
|
||||
constructor(private readonly sourceFile: (path: string) => AstNode | undefined) {}
|
||||
getSourceFile(path: string): AstNode | undefined { return this.sourceFile(path); }
|
||||
}
|
||||
|
||||
/** Symbols are snapshot-wide; TypeScript type and signature handles are
|
||||
* project-local. Preserve both scopes even when numeric handles coincide. */
|
||||
export class SemanticSnapshot {
|
||||
private readonly projects = new Map<string, SemanticProject>();
|
||||
private readonly symbols = new Map<number, SemanticSymbol>();
|
||||
private disposed = false;
|
||||
|
||||
constructor(readonly id: number, readonly transport: SemanticTransport) {}
|
||||
|
||||
addProject(id: string, sourceFile: (path: string) => AstNode | undefined): SemanticProject {
|
||||
this.ensureActive();
|
||||
if (this.projects.has(id)) throw new Error(`Duplicate TypeScript project ${id}`);
|
||||
const project = new SemanticProject(id, this, new SemanticProgram(sourceFile));
|
||||
this.projects.set(id, project);
|
||||
return project;
|
||||
}
|
||||
|
||||
getProject(id: string): SemanticProject {
|
||||
this.ensureActive();
|
||||
const project = this.projects.get(id);
|
||||
if (project === undefined) throw new Error(`Unknown TypeScript canonical project ${id}`);
|
||||
return project;
|
||||
}
|
||||
|
||||
symbol(data: SymbolResponse): SemanticSymbol {
|
||||
this.ensureActive();
|
||||
let symbol = this.symbols.get(data.id);
|
||||
if (symbol === undefined) {
|
||||
symbol = new SemanticSymbol(data, this.getProject(data.project));
|
||||
this.symbols.set(data.id, symbol);
|
||||
}
|
||||
return symbol;
|
||||
}
|
||||
|
||||
cachedSymbol(id: number): SemanticSymbol | undefined {
|
||||
this.ensureActive();
|
||||
return this.symbols.get(id);
|
||||
}
|
||||
|
||||
ensureActive(): void {
|
||||
if (this.disposed) throw new Error("TypeScript semantic snapshot is disposed");
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.disposed) return;
|
||||
this.disposed = true;
|
||||
for (const project of this.projects.values()) project.dispose();
|
||||
this.projects.clear();
|
||||
this.symbols.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class SemanticProject {
|
||||
private readonly types = new Map<number, SemanticType>();
|
||||
private readonly signatures = new Map<number, SemanticSignature>();
|
||||
private disposed = false;
|
||||
|
||||
constructor(readonly id: string, readonly snapshot: SemanticSnapshot, readonly program: SemanticProgram) {}
|
||||
|
||||
ensureActive(): void {
|
||||
this.snapshot.ensureActive();
|
||||
if (this.disposed) throw new Error("TypeScript semantic project is disposed");
|
||||
}
|
||||
|
||||
request<T>(method: string, query: SemanticQuery = {}): T {
|
||||
this.ensureActive();
|
||||
query.snapshot = this.snapshot.id;
|
||||
query.project = this.id;
|
||||
return parseSemanticJson<T>(this.snapshot.transport.text(method, JSON.stringify(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));
|
||||
}
|
||||
|
||||
type(data: TypeResponse): SemanticType {
|
||||
this.ensureActive();
|
||||
let type = this.types.get(data.id);
|
||||
if (type === undefined) {
|
||||
type = new SemanticType(data, this);
|
||||
this.types.set(data.id, type);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
signature(data: SignatureResponse): SemanticSignature {
|
||||
this.ensureActive();
|
||||
let signature = this.signatures.get(data.id);
|
||||
if (signature === undefined) {
|
||||
signature = new SemanticSignature(data, this);
|
||||
this.signatures.set(data.id, signature);
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
|
||||
symbol(data: SymbolResponse): SemanticSymbol {
|
||||
this.ensureActive();
|
||||
return this.snapshot.symbol(data);
|
||||
}
|
||||
|
||||
fetchType(source: number, method: string, handle: number | undefined | false): SemanticType | undefined {
|
||||
this.ensureActive();
|
||||
if (handle !== false) {
|
||||
if (handle === undefined || handle === 0) return undefined;
|
||||
const cached = this.types.get(handle);
|
||||
if (cached !== undefined) return cached;
|
||||
}
|
||||
const data = this.request<TypeResponse | null>(method, { objectId: source });
|
||||
if (data === null) throw new Error(`${method} returned null type for ${source}`);
|
||||
return this.type(data);
|
||||
}
|
||||
|
||||
fetchSymbol(source: number, method: string, handle: number | undefined): SemanticSymbol | undefined {
|
||||
this.ensureActive();
|
||||
if (handle === undefined || handle === 0) return undefined;
|
||||
const cached = this.snapshot.cachedSymbol(handle);
|
||||
if (cached !== undefined) return cached;
|
||||
const data = this.request<SymbolResponse | null>(method, { objectId: source });
|
||||
if (data === null) throw new Error(`${method} returned null symbol for ${source}`);
|
||||
return this.symbol(data);
|
||||
}
|
||||
|
||||
fetchSignature(source: number, method: string, handle: number | undefined): SemanticSignature | undefined {
|
||||
this.ensureActive();
|
||||
if (handle === undefined || handle === 0) return undefined;
|
||||
const cached = this.signatures.get(handle);
|
||||
if (cached !== undefined) return cached;
|
||||
const data = this.request<SignatureResponse | null>(method, { objectId: source });
|
||||
if (data === null) throw new Error(`${method} returned null signature for ${source}`);
|
||||
return this.signature(data);
|
||||
}
|
||||
|
||||
fetchTypes(source: number, method: string, handles?: readonly number[]): SemanticType[] {
|
||||
this.ensureActive();
|
||||
if (handles !== undefined) {
|
||||
const cached: SemanticType[] = [];
|
||||
for (const id of handles) {
|
||||
const type = this.types.get(id);
|
||||
if (type === undefined) break;
|
||||
cached.push(type);
|
||||
}
|
||||
if (cached.length === handles.length) return cached;
|
||||
}
|
||||
const data = this.request<TypeResponse[] | null>(method, { objectId: source });
|
||||
return (data ?? []).map((item) => this.type(item));
|
||||
}
|
||||
|
||||
fetchSymbols(source: number, method: string, handles?: readonly number[]): SemanticSymbol[] {
|
||||
this.ensureActive();
|
||||
if (handles !== undefined) {
|
||||
const cached: SemanticSymbol[] = [];
|
||||
for (const id of handles) {
|
||||
const symbol = this.snapshot.cachedSymbol(id);
|
||||
if (symbol === undefined) break;
|
||||
cached.push(symbol);
|
||||
}
|
||||
if (cached.length === handles.length) return cached;
|
||||
}
|
||||
const data = this.request<SymbolResponse[] | null>(method, { objectId: source });
|
||||
return (data ?? []).map((item) => this.symbol(item));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.disposed) return;
|
||||
this.disposed = true;
|
||||
this.types.clear();
|
||||
this.signatures.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/** A handle records its producing project, so declaration resolution is
|
||||
* unambiguous even when a symbol is reused by a second project's checker. */
|
||||
export class SemanticNodeHandle {
|
||||
readonly index: number;
|
||||
readonly kind: number;
|
||||
readonly path: string;
|
||||
|
||||
constructor(readonly handle: string, readonly canonicalProject: SemanticProject) {
|
||||
const parsed = parseAstNodeHandle(handle);
|
||||
this.index = parsed.index;
|
||||
this.kind = parsed.kind;
|
||||
this.path = parsed.path;
|
||||
}
|
||||
|
||||
resolve(project: SemanticProject = this.canonicalProject): AstNode | undefined {
|
||||
this.canonicalProject.ensureActive();
|
||||
const file = project.program.getSourceFile(this.path);
|
||||
return file?.file.resolve(this.handle);
|
||||
}
|
||||
}
|
||||
|
||||
export class SemanticSymbol {
|
||||
readonly id: number;
|
||||
readonly escapedName: string;
|
||||
readonly name: string;
|
||||
readonly flags: number;
|
||||
readonly checkFlags: number;
|
||||
readonly declarations: SemanticNodeHandle[];
|
||||
readonly valueDeclaration: SemanticNodeHandle | undefined;
|
||||
private readonly parent: number | undefined;
|
||||
private readonly exportSymbol: number | undefined;
|
||||
private membersCache: Map<string, SemanticSymbol> | undefined;
|
||||
private exportsCache: Map<string, SemanticSymbol> | undefined;
|
||||
|
||||
constructor(data: SymbolResponse, readonly canonicalProject: SemanticProject) {
|
||||
this.id = data.id;
|
||||
this.escapedName = data.name;
|
||||
this.name = data.name.startsWith("___") ? data.name.slice(1) : data.name;
|
||||
this.flags = data.flags;
|
||||
this.checkFlags = data.checkFlags;
|
||||
this.declarations = (data.declarations ?? []).map((handle) => new SemanticNodeHandle(handle, canonicalProject));
|
||||
this.valueDeclaration = data.valueDeclaration ? new SemanticNodeHandle(data.valueDeclaration, canonicalProject) : undefined;
|
||||
this.parent = data.parent;
|
||||
this.exportSymbol = data.exportSymbol;
|
||||
}
|
||||
|
||||
getParent(): SemanticSymbol | undefined {
|
||||
return this.canonicalProject.fetchSymbol(this.id, "getParentOfSymbol", this.parent);
|
||||
}
|
||||
getExportSymbol(): SemanticSymbol {
|
||||
this.canonicalProject.ensureActive();
|
||||
if (this.exportSymbol === undefined || this.exportSymbol === 0) return this;
|
||||
const result = this.canonicalProject.fetchSymbol(this.id, "getExportSymbolOfSymbol", this.exportSymbol);
|
||||
if (result === undefined) throw new Error(`Missing export symbol for ${this.id}`);
|
||||
return result;
|
||||
}
|
||||
private table(method: string): Map<string, SemanticSymbol> {
|
||||
const table = new Map<string, SemanticSymbol>();
|
||||
for (const symbol of this.canonicalProject.fetchSymbols(this.id, method)) table.set(symbol.escapedName, symbol);
|
||||
return table;
|
||||
}
|
||||
getMembers(): Map<string, SemanticSymbol> {
|
||||
this.canonicalProject.ensureActive();
|
||||
if (this.membersCache === undefined) this.membersCache = this.table("getMembersOfSymbol");
|
||||
return this.membersCache;
|
||||
}
|
||||
getExports(): Map<string, SemanticSymbol> {
|
||||
this.canonicalProject.ensureActive();
|
||||
if (this.exportsCache === undefined) this.exportsCache = this.table("getExportsOfSymbol");
|
||||
return this.exportsCache;
|
||||
}
|
||||
getJsDocTags(checker: SemanticChecker): SemanticJsDocTag[] { return checker.getJsDocTagsOfSymbol(this); }
|
||||
getDocumentationComment(checker: SemanticChecker): string { return checker.getDocumentationCommentOfSymbol(this); }
|
||||
}
|
||||
|
||||
/** Kind-specific accessors share one nominal layout. The legacy frontend
|
||||
* still narrows its type-only SDK interfaces at the boundary; native code
|
||||
* uses this concrete class and tests the same flag predicates. */
|
||||
export class SemanticType {
|
||||
readonly id: number;
|
||||
readonly flags: number;
|
||||
readonly objectFlags: number | undefined;
|
||||
readonly value: string | number | boolean | bigint | undefined;
|
||||
readonly intrinsicName: string | undefined;
|
||||
readonly isThisType: boolean | undefined;
|
||||
readonly elementFlags: number[] | undefined;
|
||||
readonly fixedLength: number | undefined;
|
||||
readonly readonly: boolean | undefined;
|
||||
readonly texts: string[] | undefined;
|
||||
private trueType: number | false = false;
|
||||
private falseType: number | false = false;
|
||||
|
||||
constructor(private readonly data: TypeResponse, readonly project: SemanticProject) {
|
||||
this.id = data.id;
|
||||
this.flags = data.flags;
|
||||
this.objectFlags = data.objectFlags;
|
||||
const value = data.value;
|
||||
if ((data.flags & TypeFlags.BigIntLiteral) !== 0 && value != null) {
|
||||
if (typeof value !== "string") throw new Error("TypeScript bigint literal is not a decimal string");
|
||||
this.value = BigInt(value);
|
||||
} else {
|
||||
this.value = value ?? undefined;
|
||||
}
|
||||
this.intrinsicName = data.intrinsicName;
|
||||
this.isThisType = data.isThisType;
|
||||
this.elementFlags = data.elementFlags;
|
||||
this.fixedLength = data.fixedLength;
|
||||
this.readonly = data.readonly;
|
||||
this.texts = data.texts;
|
||||
}
|
||||
|
||||
getSymbol(): SemanticSymbol | undefined { return this.project.fetchSymbol(this.id, "getSymbolOfType", this.data.symbol); }
|
||||
getAliasSymbol(): SemanticSymbol | undefined { return this.project.fetchSymbol(this.id, "getAliasSymbolOfType", this.data.aliasSymbol); }
|
||||
getTarget(): SemanticType | undefined { return this.project.fetchType(this.id, "getTargetOfType", this.data.target); }
|
||||
getFreshType(): SemanticType | undefined { return this.project.fetchType(this.id, "getFreshTypeOfType", this.data.freshType); }
|
||||
getRegularType(): SemanticType | undefined { return this.project.fetchType(this.id, "getRegularTypeOfType", this.data.regularType); }
|
||||
getObjectType(): SemanticType | undefined { return this.project.fetchType(this.id, "getObjectTypeOfType", this.data.objectType); }
|
||||
getIndexType(): SemanticType | undefined { return this.project.fetchType(this.id, "getIndexTypeOfType", this.data.indexType); }
|
||||
getCheckType(): SemanticType | undefined { return this.project.fetchType(this.id, "getCheckTypeOfType", this.data.checkType); }
|
||||
getExtendsType(): SemanticType | undefined { return this.project.fetchType(this.id, "getExtendsTypeOfType", this.data.extendsType); }
|
||||
getBaseType(): SemanticType | undefined { return this.project.fetchType(this.id, "getBaseTypeOfType", this.data.baseType); }
|
||||
getConstraint(): SemanticType | undefined { return this.project.fetchType(this.id, "getConstraintOfType", this.data.substConstraint); }
|
||||
getTypeParameters(): SemanticType[] { return this.project.fetchTypes(this.id, "getTypeParametersOfType", this.data.typeParameters ?? []); }
|
||||
getOuterTypeParameters(): SemanticType[] { return this.project.fetchTypes(this.id, "getOuterTypeParametersOfType", this.data.outerTypeParameters ?? []); }
|
||||
getLocalTypeParameters(): SemanticType[] { return this.project.fetchTypes(this.id, "getLocalTypeParametersOfType", this.data.localTypeParameters ?? []); }
|
||||
getAliasTypeArguments(): SemanticType[] { return this.project.fetchTypes(this.id, "getAliasTypeArgumentsOfType", this.data.aliasTypeArguments ?? []); }
|
||||
getTypes(): SemanticType[] | undefined {
|
||||
this.project.ensureActive();
|
||||
if ((this.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral)) === 0) return undefined;
|
||||
return this.project.fetchTypes(this.id, "getTypesOfType");
|
||||
}
|
||||
getBaseTypes(): SemanticType[] | undefined {
|
||||
this.project.ensureActive();
|
||||
if (!this.isClassOrInterface()) return undefined;
|
||||
const data = this.project.request<TypeResponse[] | null>("getBaseTypes", { type: this.id });
|
||||
return (data ?? []).map((item) => this.project.type(item));
|
||||
}
|
||||
getTrueType(): SemanticType {
|
||||
const result = this.project.fetchType(this.id, "getTrueTypeOfConditionalType", this.trueType);
|
||||
if (result === undefined) throw new Error(`Missing conditional true type for ${this.id}`);
|
||||
this.trueType = result.id;
|
||||
return result;
|
||||
}
|
||||
getFalseType(): SemanticType {
|
||||
const result = this.project.fetchType(this.id, "getFalseTypeOfConditionalType", this.falseType);
|
||||
if (result === undefined) throw new Error(`Missing conditional false type for ${this.id}`);
|
||||
this.falseType = result.id;
|
||||
return result;
|
||||
}
|
||||
isClassOrInterface(): boolean { return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.ClassOrInterface) !== 0; }
|
||||
isUnionType(): boolean { return (this.flags & TypeFlags.Union) !== 0; }
|
||||
isIntersectionType(): boolean { return (this.flags & TypeFlags.Intersection) !== 0; }
|
||||
isObjectType(): boolean { return (this.flags & TypeFlags.Object) !== 0; }
|
||||
isIntrinsicType(): boolean { return (this.flags & TypeFlags.Intrinsic) !== 0; }
|
||||
isErrorType(): boolean { return this.isIntrinsicType() && this.intrinsicName === "error"; }
|
||||
isLiteralType(): boolean { return (this.flags & TypeFlags.Literal) !== 0; }
|
||||
isStringLiteralType(): boolean { return (this.flags & TypeFlags.StringLiteral) !== 0; }
|
||||
isNumberLiteralType(): boolean { return (this.flags & TypeFlags.NumberLiteral) !== 0; }
|
||||
isBigIntLiteralType(): boolean { return (this.flags & TypeFlags.BigIntLiteral) !== 0; }
|
||||
isBooleanLiteralType(): boolean { return (this.flags & TypeFlags.BooleanLiteral) !== 0; }
|
||||
isTypeReference(): boolean { return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.Reference) !== 0; }
|
||||
isTupleType(): boolean { return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.Tuple) !== 0; }
|
||||
isIndexType(): boolean { return (this.flags & TypeFlags.Index) !== 0; }
|
||||
isIndexedAccessType(): boolean { return (this.flags & TypeFlags.IndexedAccess) !== 0; }
|
||||
isConditionalType(): boolean { return (this.flags & TypeFlags.Conditional) !== 0; }
|
||||
isSubstitutionType(): boolean { return (this.flags & TypeFlags.Substitution) !== 0; }
|
||||
isTemplateLiteralType(): boolean { return (this.flags & TypeFlags.TemplateLiteral) !== 0; }
|
||||
isStringMappingType(): boolean { return (this.flags & TypeFlags.StringMapping) !== 0; }
|
||||
isTypeParameter(): boolean { return (this.flags & TypeFlags.TypeParameter) !== 0; }
|
||||
}
|
||||
|
||||
export class SemanticSignature {
|
||||
readonly id: number;
|
||||
readonly flags: number;
|
||||
readonly declaration: SemanticNodeHandle | undefined;
|
||||
|
||||
constructor(private readonly data: SignatureResponse, readonly project: SemanticProject) {
|
||||
this.id = data.id;
|
||||
this.flags = data.flags;
|
||||
this.declaration = data.declaration ? new SemanticNodeHandle(data.declaration, project) : undefined;
|
||||
}
|
||||
|
||||
getTypeParameters(): SemanticType[] { return this.project.fetchTypes(this.id, "getTypeParametersOfSignature", this.data.typeParameters ?? []); }
|
||||
getParameters(): SemanticSymbol[] { return this.project.fetchSymbols(this.id, "getParametersOfSignature", this.data.parameters ?? []); }
|
||||
getThisParameter(): SemanticSymbol | undefined { return this.project.fetchSymbol(this.id, "getThisParameterOfSignature", this.data.thisParameter); }
|
||||
getTarget(): SemanticSignature | undefined { return this.project.fetchSignature(this.id, "getTargetOfSignature", this.data.target); }
|
||||
get hasRestParameter(): boolean { return (this.flags & SignatureFlags.HasRestParameter) !== 0; }
|
||||
get isConstruct(): boolean { return (this.flags & SignatureFlags.Construct) !== 0; }
|
||||
get isAbstract(): boolean { return (this.flags & SignatureFlags.Abstract) !== 0; }
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
// Generated from typescript@7.0.2 by scripts/generate-ts7-ast-schema.mjs.
|
||||
// TypeScript is Copyright Microsoft Corporation, licensed under Apache-2.0.
|
||||
// Regenerate when changing the TypeScript pin; do not edit by hand.
|
||||
|
||||
export const SemanticTypeFlags = {
|
||||
None: 0,
|
||||
Any: 1,
|
||||
Unknown: 2,
|
||||
Undefined: 4,
|
||||
Null: 8,
|
||||
Void: 16,
|
||||
String: 32,
|
||||
Number: 64,
|
||||
BigInt: 128,
|
||||
Boolean: 256,
|
||||
ESSymbol: 512,
|
||||
StringLiteral: 1024,
|
||||
NumberLiteral: 2048,
|
||||
BigIntLiteral: 4096,
|
||||
BooleanLiteral: 8192,
|
||||
UniqueESSymbol: 16384,
|
||||
EnumLiteral: 32768,
|
||||
Enum: 65536,
|
||||
NonPrimitive: 131072,
|
||||
Never: 262144,
|
||||
TypeParameter: 524288,
|
||||
Object: 1048576,
|
||||
Index: 2097152,
|
||||
TemplateLiteral: 4194304,
|
||||
StringMapping: 8388608,
|
||||
Substitution: 16777216,
|
||||
IndexedAccess: 33554432,
|
||||
Conditional: 67108864,
|
||||
Union: 134217728,
|
||||
Intersection: 268435456,
|
||||
Reserved1: 536870912,
|
||||
Reserved2: 1073741824,
|
||||
Reserved3: -2147483648,
|
||||
AnyOrUnknown: 3,
|
||||
Nullable: 12,
|
||||
Literal: 15360,
|
||||
Unit: 97292,
|
||||
Freshable: 80896,
|
||||
StringOrNumberLiteral: 3072,
|
||||
StringOrNumberLiteralOrUnique: 19456,
|
||||
DefinitelyFalsy: 15388,
|
||||
PossiblyFalsy: 15868,
|
||||
Intrinsic: 393983,
|
||||
StringLike: 12583968,
|
||||
NumberLike: 67648,
|
||||
BigIntLike: 4224,
|
||||
BooleanLike: 8448,
|
||||
EnumLike: 98304,
|
||||
ESSymbolLike: 16896,
|
||||
VoidLike: 20,
|
||||
Primitive: 12713980,
|
||||
DefinitelyNonNullable: 13893600,
|
||||
DisjointDomains: 12812284,
|
||||
UnionOrIntersection: 402653184,
|
||||
StructuredType: 403701760,
|
||||
TypeVariable: 34078720,
|
||||
InstantiableNonPrimitive: 117964800,
|
||||
InstantiablePrimitive: 14680064,
|
||||
Instantiable: 132644864,
|
||||
StructuredOrInstantiable: 536346624,
|
||||
ObjectFlagsType: 403963917,
|
||||
Simplifiable: 102760448,
|
||||
Singleton: 394239,
|
||||
Narrowable: 536575971,
|
||||
IncludesMask: 416808959,
|
||||
IncludesMissingType: 524288,
|
||||
IncludesNonWideningType: 2097152,
|
||||
IncludesWildcard: 33554432,
|
||||
IncludesEmptyObject: 67108864,
|
||||
IncludesInstantiable: 16777216,
|
||||
IncludesConstrainedTypeVariable: 536870912,
|
||||
IncludesError: 1073741824,
|
||||
NotPrimitiveUnion: 286523411,
|
||||
} as const;
|
||||
|
||||
export const SemanticObjectFlags = {
|
||||
None: 0,
|
||||
Class: 1,
|
||||
Interface: 2,
|
||||
Reference: 4,
|
||||
Tuple: 8,
|
||||
Anonymous: 16,
|
||||
Mapped: 32,
|
||||
Instantiated: 64,
|
||||
ObjectLiteral: 128,
|
||||
EvolvingArray: 256,
|
||||
ObjectLiteralPatternWithComputedProperties: 512,
|
||||
ReverseMapped: 1024,
|
||||
JsxAttributes: 2048,
|
||||
JSLiteral: 4096,
|
||||
FreshLiteral: 8192,
|
||||
ArrayLiteral: 16384,
|
||||
PrimitiveUnion: 32768,
|
||||
ContainsWideningType: 65536,
|
||||
ContainsObjectOrArrayLiteral: 131072,
|
||||
NonInferrableType: 262144,
|
||||
CouldContainTypeVariablesComputed: 524288,
|
||||
CouldContainTypeVariables: 1048576,
|
||||
MembersResolved: 2097152,
|
||||
ClassOrInterface: 3,
|
||||
RequiresWidening: 196608,
|
||||
PropagatingFlags: 458752,
|
||||
InstantiatedMapped: 96,
|
||||
InstantiationExpressionType: 16777216,
|
||||
SingleSignatureType: 33554432,
|
||||
ObjectTypeKindMask: 50332991,
|
||||
ContainsSpread: 4194304,
|
||||
ObjectRestType: 8388608,
|
||||
IsClassInstanceClone: 67108864,
|
||||
IdenticalBaseTypeCalculated: 134217728,
|
||||
IdenticalBaseTypeExists: 268435456,
|
||||
UnresolvedMembers: 536870912,
|
||||
FromTypeNode: 1073741824,
|
||||
IsGenericTypeComputed: 4194304,
|
||||
IsGenericObjectType: 8388608,
|
||||
IsGenericIndexType: 16777216,
|
||||
IsGenericType: 25165824,
|
||||
ContainsIntersections: 33554432,
|
||||
IsUnknownLikeUnionComputed: 67108864,
|
||||
IsUnknownLikeUnion: 134217728,
|
||||
IsNeverIntersectionComputed: 33554432,
|
||||
IsNeverIntersection: 67108864,
|
||||
IsConstrainedTypeVariable: 134217728,
|
||||
} as const;
|
||||
|
||||
export const SemanticSymbolFlags = {
|
||||
None: 0,
|
||||
FunctionScopedVariable: 1,
|
||||
BlockScopedVariable: 2,
|
||||
Property: 4,
|
||||
EnumMember: 8,
|
||||
Function: 16,
|
||||
Class: 32,
|
||||
Interface: 64,
|
||||
ConstEnum: 128,
|
||||
RegularEnum: 256,
|
||||
ValueModule: 512,
|
||||
NamespaceModule: 1024,
|
||||
TypeLiteral: 2048,
|
||||
ObjectLiteral: 4096,
|
||||
Method: 8192,
|
||||
Constructor: 16384,
|
||||
GetAccessor: 32768,
|
||||
SetAccessor: 65536,
|
||||
Signature: 131072,
|
||||
TypeParameter: 262144,
|
||||
TypeAlias: 524288,
|
||||
ExportValue: 1048576,
|
||||
Alias: 2097152,
|
||||
Prototype: 4194304,
|
||||
ExportStar: 8388608,
|
||||
Optional: 16777216,
|
||||
Transient: 33554432,
|
||||
Assignment: 67108864,
|
||||
ModuleExports: 134217728,
|
||||
ConstEnumOnlyModule: 268435456,
|
||||
ReplaceableByMethod: 536870912,
|
||||
GlobalLookup: 1073741824,
|
||||
All: 536870912,
|
||||
Enum: 384,
|
||||
Variable: 3,
|
||||
Value: 111551,
|
||||
Type: 788968,
|
||||
Namespace: 1920,
|
||||
Module: 1536,
|
||||
Accessor: 98304,
|
||||
FunctionScopedVariableExcludes: 111550,
|
||||
BlockScopedVariableExcludes: 111551,
|
||||
ParameterExcludes: 111551,
|
||||
PropertyExcludes: 13243,
|
||||
EnumMemberExcludes: 900095,
|
||||
FunctionExcludes: 110991,
|
||||
ClassExcludes: 899503,
|
||||
InterfaceExcludes: 788872,
|
||||
RegularEnumExcludes: 899327,
|
||||
ConstEnumExcludes: 899967,
|
||||
ValueModuleExcludes: 110735,
|
||||
NamespaceModuleExcludes: 0,
|
||||
MethodExcludes: 103359,
|
||||
GetAccessorExcludes: 46011,
|
||||
SetAccessorExcludes: 78779,
|
||||
AccessorExcludes: 111547,
|
||||
TypeParameterExcludes: 526824,
|
||||
TypeAliasExcludes: 788968,
|
||||
AliasExcludes: 2097152,
|
||||
ModuleMember: 2623475,
|
||||
ExportHasLocal: 944,
|
||||
BlockScoped: 418,
|
||||
PropertyOrAccessor: 98308,
|
||||
ClassMember: 106500,
|
||||
ExportSupportsDefaultModifier: 112,
|
||||
ExportDoesNotSupportDefaultModifier: -113,
|
||||
Classifiable: 2885600,
|
||||
LateBindingContainer: 6256,
|
||||
} as const;
|
||||
|
||||
export const SemanticSignatureFlags = {
|
||||
None: 0,
|
||||
HasRestParameter: 1,
|
||||
HasLiteralTypes: 2,
|
||||
Construct: 4,
|
||||
Abstract: 8,
|
||||
IsInnerCallChain: 16,
|
||||
IsOuterCallChain: 32,
|
||||
IsUntypedSignatureInJSFile: 64,
|
||||
IsNonInferrable: 128,
|
||||
IsSignatureCandidateForOverloadFailure: 256,
|
||||
PropagatingFlags: 335,
|
||||
CallChainFlags: 48,
|
||||
} as const;
|
||||
|
||||
export const SemanticSignatureKind = {
|
||||
Call: 0,
|
||||
Construct: 1,
|
||||
} as const;
|
||||
|
||||
export const SemanticTypePredicateKind = {
|
||||
This: 0,
|
||||
Identifier: 1,
|
||||
AssertsThis: 2,
|
||||
AssertsIdentifier: 3,
|
||||
} as const;
|
||||
|
||||
export interface SemanticSymbolData {
|
||||
id: number;
|
||||
/**
|
||||
* The project the symbol was first observed in. Used as the default project for
|
||||
* follow-up lookups that need a project context (e.g. members/exports), since symbols
|
||||
* are shared snapshot-wide and such lookups can vary by project.
|
||||
*/
|
||||
project: string;
|
||||
name: string;
|
||||
flags: number;
|
||||
checkFlags: number;
|
||||
declarations?: string[];
|
||||
valueDeclaration?: string;
|
||||
parent?: number;
|
||||
exportSymbol?: number;
|
||||
}
|
||||
|
||||
export interface SemanticTypeData {
|
||||
id: number;
|
||||
flags: number;
|
||||
objectFlags?: number;
|
||||
/** Literal value. BigInt literals are encoded as a decimal string (e.g. "-123") since JSON cannot represent bigint. Absent values are serialized as null. */
|
||||
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;
|
||||
aliasTypeArguments?: number[];
|
||||
aliasSymbol?: number;
|
||||
symbol?: number;
|
||||
}
|
||||
|
||||
export interface SemanticSignatureData {
|
||||
id: number;
|
||||
flags: number;
|
||||
declaration?: string;
|
||||
typeParameters?: number[];
|
||||
parameters?: number[];
|
||||
thisParameter?: number;
|
||||
target?: number;
|
||||
}
|
||||
|
||||
export interface SemanticTypePredicateData {
|
||||
kind: number;
|
||||
parameterIndex: number;
|
||||
parameterName?: string;
|
||||
type?: SemanticTypeData;
|
||||
}
|
||||
|
||||
export interface SemanticIndexInfoData {
|
||||
keyType: SemanticTypeData;
|
||||
valueType: SemanticTypeData;
|
||||
isReadonly?: boolean;
|
||||
declaration?: string;
|
||||
}
|
||||
@@ -8578,6 +8578,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/bigint-union-conversion.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/bigint-union-conversion.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/console-imports.cjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/console-imports.cjs"
|
||||
@@ -8590,6 +8596,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/instanceof-class-record-union.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/instanceof-class-record-union.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-declared-fields.cjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-declared-fields.cjs"
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Ts7RpcClient } from "../../src/frontend/ts7/rpc-client.js";
|
||||
import type { Ts7FileSystem } from "../../src/frontend/ts7/rpc-filesystem.js";
|
||||
import { spawnTs7Wire } from "../../src/frontend/ts7/rpc-process.js";
|
||||
import { AstNode } from "../../src/frontend/ts7/ast-node.js";
|
||||
import { SemanticChecker } from "../../src/frontend/ts7/semantic-checker.js";
|
||||
import { SemanticNodeHandle, SemanticSignature, SemanticSymbol, SemanticType } from "../../src/frontend/ts7/semantic-model.js";
|
||||
|
||||
function tsgoPath(path: string): string {
|
||||
return process.platform === "win32" ? path.replaceAll("\\", "/") : path;
|
||||
@@ -68,8 +70,12 @@ test("production RPC bridge preserves pinned SDK parser/checker behavior", () =>
|
||||
expect(source).toBeInstanceOf(AstNode);
|
||||
expect(project.program.getSourceFile(path)).toBe(source);
|
||||
const first = project.checker.getTypeAtPosition(path, source.text.indexOf("answer"));
|
||||
expect(project.checker).toBeInstanceOf(SemanticChecker);
|
||||
expect(first).toBeInstanceOf(SemanticType);
|
||||
expect(project.checker.getTypeAtPosition(path, source.text.indexOf("answer"))).toBe(first);
|
||||
const symbol = project.checker.getSymbolAtPosition(path, source.text.indexOf("answer"));
|
||||
expect(symbol).toBeInstanceOf(SemanticSymbol);
|
||||
expect(symbol!.declarations[0]).toBeInstanceOf(SemanticNodeHandle);
|
||||
expect(project.checker.getSymbolAtPosition(path, source.text.indexOf("answer"))).toBe(symbol);
|
||||
expect(symbol!.declarations[0]!.resolve()).toBe(symbol!.declarations[0]!.resolve());
|
||||
const declaration = symbol!.declarations[0]!.resolve()!;
|
||||
@@ -123,6 +129,65 @@ test("snapshots share unchanged source files and release independently", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("native semantic registries preserve project scope and snapshot lifetimes", () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-semantic-scopes-"));
|
||||
const firstConfig = tsgoPath(join(dir, "first.json"));
|
||||
const secondConfig = tsgoPath(join(dir, "second.json"));
|
||||
const path = tsgoPath(join(dir, "shared.ts"));
|
||||
for (const config of [firstConfig, secondConfig]) writeFileSync(config, JSON.stringify({ compilerOptions: { strict: true, target: "esnext", types: [] }, files: [path] }));
|
||||
const source = "export function identity<T>(value: T): T { return value; }\nexport const answer = 42;\n";
|
||||
writeFileSync(path, source);
|
||||
const api = new Ts7Api({ cwd: dir, fs: fallback });
|
||||
const sdk = new API({ cwd: dir });
|
||||
function facts(snapshot: Snapshot) {
|
||||
const a = snapshot.getProject(firstConfig)!;
|
||||
const b = snapshot.getProject(secondConfig)!;
|
||||
const answer = source.indexOf("answer");
|
||||
const symbolA = a.checker.getSymbolAtPosition(path, answer)!;
|
||||
const symbolB = b.checker.getSymbolAtPosition(path, answer)!;
|
||||
const typeA = a.checker.getTypeAtPosition(path, answer)!;
|
||||
const typeB = b.checker.getTypeAtPosition(path, answer)!;
|
||||
const fileA = a.program.getSourceFile(path)!;
|
||||
const fileB = b.program.getSourceFile(path)!;
|
||||
const signatureA = a.checker.getSignatureFromDeclaration(fileA.statements[0]!)!;
|
||||
const signatureB = b.checker.getSignatureFromDeclaration(fileB.statements[0]!)!;
|
||||
return {
|
||||
shape: {
|
||||
sharedSymbol: symbolA === symbolB, sharedType: typeA === typeB, sharedSignature: signatureA === signatureB,
|
||||
sameDeclaration: symbolA.declarations[0]!.resolve() === symbolB.declarations[0]!.resolve(),
|
||||
explicitDeclaration: symbolB.declarations[0]!.resolve(b) === (fileB.statements[1] as import("typescript/unstable/ast").VariableStatement).declarationList.declarations[0],
|
||||
texts: [a.checker.typeToString(typeA), b.checker.typeToString(typeB)],
|
||||
parameters: signatureA.getParameters().map((symbol) => symbol.name),
|
||||
},
|
||||
symbolA, typeA, signatureA, fileA,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const first = api.updateSnapshot({ openProjects: [firstConfig, secondConfig] });
|
||||
const actual = facts(first);
|
||||
expect(actual.shape).toEqual(facts(sdk.updateSnapshot({ openProjects: [firstConfig, secondConfig] })).shape);
|
||||
expect(actual.signatureA).toBeInstanceOf(SemanticSignature);
|
||||
expect(actual.typeA).toBeInstanceOf(SemanticType);
|
||||
const second = api.updateSnapshot({ openProjects: [] });
|
||||
const next = facts(second);
|
||||
expect(next.fileA).toBe(actual.fileA);
|
||||
expect(next.typeA).not.toBe(actual.typeA);
|
||||
expect(next.symbolA).not.toBe(actual.symbolA);
|
||||
expect(next.signatureA).not.toBe(actual.signatureA);
|
||||
first.dispose();
|
||||
expect(() => actual.typeA.getSymbol()).toThrow("disposed");
|
||||
expect(() => actual.symbolA.declarations[0]!.resolve()).toThrow("disposed");
|
||||
expect(next.signatureA.getParameters()[0]!.name).toBe("value");
|
||||
expect(second.getProject(firstConfig)!.checker.typeToString(next.typeA)).toBe("42");
|
||||
second.dispose();
|
||||
expect(() => next.signatureA.getParameters()).toThrow("disposed");
|
||||
} finally {
|
||||
api.close();
|
||||
sdk.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("virtual filesystem callbacks retain empty, hidden, and fallback files", () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-rpc-fs-"));
|
||||
const config = tsgoPath(join(dir, "virtual.json"));
|
||||
|
||||
@@ -45,6 +45,45 @@ if (process.argv.includes("--check")) {
|
||||
writeFileSync(target, output);
|
||||
}
|
||||
|
||||
// The semantic client consumes the same pinned numeric discriminants as
|
||||
// tsgo. Keep these independent of the SDK's runtime module graph too.
|
||||
const semanticLines = [
|
||||
`// Generated from typescript@${version} by scripts/generate-ts7-ast-schema.mjs.`,
|
||||
"// TypeScript is Copyright Microsoft Corporation, licensed under Apache-2.0.",
|
||||
"// Regenerate when changing the TypeScript pin; do not edit by hand.",
|
||||
];
|
||||
for (const name of ["TypeFlags", "ObjectFlags", "SymbolFlags", "SignatureFlags", "SignatureKind", "TypePredicateKind"]) {
|
||||
const basename = name[0].toLowerCase() + name.slice(1);
|
||||
const values = require(join(packageRoot, "dist/enums", `${basename}.js`))[name];
|
||||
semanticLines.push("", `export const Semantic${name} = {`);
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (typeof value === "number") semanticLines.push(` ${key}: ${value},`);
|
||||
}
|
||||
semanticLines.push("} as const;");
|
||||
}
|
||||
const protoDeclarations = readFileSync(join(packageRoot, "dist/api/proto.d.ts"), "utf8");
|
||||
const semanticTypes = new Map([
|
||||
["SymbolResponse", "SemanticSymbolData"],
|
||||
["TypeResponse", "SemanticTypeData"],
|
||||
["SignatureResponse", "SemanticSignatureData"],
|
||||
["TypePredicateResponse", "SemanticTypePredicateData"],
|
||||
["IndexInfoResponse", "SemanticIndexInfoData"],
|
||||
]);
|
||||
for (const [name] of semanticTypes) {
|
||||
const declaration = protoDeclarations.match(new RegExp(`export interface ${name} \\{[\\s\\S]*?\\n\\}`));
|
||||
if (!declaration) throw new Error(`Missing TypeScript semantic response: ${name}`);
|
||||
let text = declaration[0].replace(/\b(Path|__String)\b/g, "string");
|
||||
for (const [original, generated] of semanticTypes) text = text.replaceAll(original, generated);
|
||||
semanticLines.push("", text);
|
||||
}
|
||||
const semanticOutput = semanticLines.join("\n") + "\n";
|
||||
const semanticTarget = join(root, "packages/compiler/src/frontend/ts7/semantic-schema.generated.ts");
|
||||
if (process.argv.includes("--check")) {
|
||||
if (readFileSync(semanticTarget, "utf8") !== semanticOutput) throw new Error("TypeScript semantic schema is stale; run node scripts/generate-ts7-ast-schema.mjs");
|
||||
} else {
|
||||
writeFileSync(semanticTarget, semanticOutput);
|
||||
}
|
||||
|
||||
// Child names alone cannot distinguish arrays from nodes (attributes and
|
||||
// children can be either). Read that distinction from the pinned client's
|
||||
// declarations, and require every wire property to have a declared getter.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
type Input = string | number | boolean | bigint | null | undefined;
|
||||
|
||||
let evaluations = 0;
|
||||
function source(value: Input): Input { evaluations++; return value; }
|
||||
function convert(value: Input): void {
|
||||
try {
|
||||
// @ts-expect-error Nullish inputs intentionally exercise BigInt's TypeError.
|
||||
console.log("value", BigInt(source(value)).toString());
|
||||
}
|
||||
catch (error) { if (error instanceof Error) console.log("error", error.name, error.message); }
|
||||
}
|
||||
|
||||
for (const value of ["123456789012345678901234567890", "0xFF", "", "invalid", 42, -0, 1.5, NaN, Infinity, true, false, 123456789012345678901234567890n, null, undefined] as Input[]) convert(value);
|
||||
console.log("evaluations", evaluations);
|
||||
|
||||
class Literal {
|
||||
readonly value: Input;
|
||||
constructor(data: { value?: string | number | boolean | null; flags: number }) {
|
||||
const value = data.value;
|
||||
if ((data.flags & 1) !== 0 && value != null) {
|
||||
if (typeof value !== "string") throw new TypeError("expected decimal string");
|
||||
this.value = BigInt(value);
|
||||
} else this.value = value ?? undefined;
|
||||
}
|
||||
}
|
||||
console.log("constructor", new Literal({ value: "9007199254740993", flags: 1 }).value);
|
||||
|
||||
function captured(): void {
|
||||
let value: string | undefined = "12";
|
||||
const mutate = () => { value = undefined; };
|
||||
if (typeof value === "string") {
|
||||
mutate();
|
||||
try { console.log(BigInt(value)); }
|
||||
catch (error) { if (error instanceof Error) console.log("capture", error.name, error.message); }
|
||||
}
|
||||
}
|
||||
captured();
|
||||
@@ -0,0 +1,28 @@
|
||||
// @transform-types
|
||||
class Base { constructor(readonly value: number) {} }
|
||||
class Derived extends Base { read(): number { return this.value; } }
|
||||
class Other { constructor(readonly label: string) {} }
|
||||
type Input = Base | Other | { value: number; label: string } | number | string | boolean | bigint | symbol | null | undefined;
|
||||
|
||||
let reads = 0;
|
||||
function source(value: Input): Input { reads++; return value; }
|
||||
function inspect(value: Input): void {
|
||||
console.log("kinds", source(value) instanceof Base, source(value) instanceof Derived, source(value) instanceof Other);
|
||||
if (value instanceof Base) console.log("base", value.value);
|
||||
if (value instanceof Derived) console.log("derived", value.read());
|
||||
if (value instanceof Other) console.log("other", value.label);
|
||||
}
|
||||
const inputs: Input[] = [new Base(1), new Derived(2), new Other("separate"), { value: 3, label: "record" }, 3, "text", false, 2n, Symbol("s"), null, undefined];
|
||||
for (const input of inputs) inspect(input);
|
||||
console.log("reads", reads);
|
||||
|
||||
class Node { constructor(readonly kind: number) {} get text(): string { return "node"; } }
|
||||
function location(value?: Node | { document: string; position: number }): string {
|
||||
const node = value instanceof Node ? value : undefined;
|
||||
const position = value !== undefined && !(value instanceof Node) ? value : undefined;
|
||||
return node === undefined ? position?.document ?? "absent" : node.text;
|
||||
}
|
||||
console.log(location(new Node(1)), location({ document: "file.ts", position: 0 }), location());
|
||||
|
||||
const errors: (Error | { message: string } | undefined)[] = [new Error("base"), new TypeError("type"), { message: "record" }, undefined];
|
||||
for (const error of errors) console.log("error", error instanceof Error, error instanceof TypeError);
|
||||
+47
-7
@@ -6,6 +6,10 @@ import { Ts7Wire } from "../../../packages/compiler/src/frontend/ts7/rpc-wire.js
|
||||
import { AstFile, AstNode } from "../../../packages/compiler/src/frontend/ts7/ast-node.js";
|
||||
import { AstKind, KIND_NODE_LIST, astChildNames } from "../../../packages/compiler/src/frontend/ts7/ast-schema.generated.js";
|
||||
import { decodeAstString } from "../../../packages/compiler/src/frontend/ts7/ast-bytes.js";
|
||||
import { SemanticSnapshot } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js";
|
||||
import { SemanticChecker } from "../../../packages/compiler/src/frontend/ts7/semantic-checker.js";
|
||||
import { parseSemanticJson } from "../../../packages/compiler/src/frontend/ts7/semantic-json.js";
|
||||
import { checkSemanticModel, semanticSource } from "./ts7-semantic-cases.js";
|
||||
|
||||
// The harness connects these inherited descriptors straight to native tsgo.
|
||||
// No JavaScript helper reads, interprets, or relays protocol messages.
|
||||
@@ -58,6 +62,25 @@ function checkSurrogateBoundary(): string {
|
||||
}
|
||||
}
|
||||
|
||||
function checkSemanticSurrogateBoundary(): string {
|
||||
check(parseSemanticJson<string>('"\\ud83c\\udf0d"') === "🌍", "semantic JSON surrogate pair");
|
||||
check(parseSemanticJson<string>('"\\\\ud800"') === "\\ud800", "semantic JSON escaped backslash");
|
||||
let refused = 0;
|
||||
const hex = "0123456789abcdef";
|
||||
for (let unit = 0xd800; unit <= 0xdfff; unit++) {
|
||||
try {
|
||||
const escape = hex.charAt((unit >> 12) & 15) + hex.charAt((unit >> 8) & 15) + hex.charAt((unit >> 4) & 15) + hex.charAt(unit & 15);
|
||||
const value = parseSemanticJson<string>('"\\u' + escape + '"');
|
||||
check(value.length === 1 && value.charCodeAt(0) === unit, "lossless semantic JSON code unit");
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error) || error.message !== "TypeScript semantic response: runtime cannot preserve lone UTF-16 surrogates") throw error;
|
||||
refused++;
|
||||
}
|
||||
}
|
||||
check(refused === 0 || refused === 2048, "consistent semantic JSON surrogate boundary");
|
||||
return refused === 0 ? "preserved" : "refused";
|
||||
}
|
||||
|
||||
function checkAst(ast: AstFile, source: string): void {
|
||||
check(ast.root.text === source, "decoded source text");
|
||||
check(ast.root.getSourceFile() === ast.root, "source identity");
|
||||
@@ -109,7 +132,7 @@ function checkFailures(): void {
|
||||
let offset = 0;
|
||||
let closes = 0;
|
||||
const wire = new Ts7Wire({
|
||||
read: (buffer, start, _length) => {
|
||||
read: (buffer, start) => {
|
||||
if (offset === bytes.length) return 0;
|
||||
buffer[start] = bytes[offset++]!;
|
||||
return 1;
|
||||
@@ -134,15 +157,15 @@ function checkFailures(): void {
|
||||
let offset = 0;
|
||||
let closes = 0;
|
||||
const local = new Ts7RpcClient(new Ts7Wire({
|
||||
read: (buffer, start, _length) => {
|
||||
read: (buffer, start) => {
|
||||
if (offset === bytes.length) return 0;
|
||||
buffer[start] = bytes[offset++]!;
|
||||
return 1;
|
||||
},
|
||||
write: (buffer, start, _length) => { output.push(buffer[start]!); return 1; },
|
||||
write: (buffer, start) => { output.push(buffer[start]!); return 1; },
|
||||
close: () => { closes++; },
|
||||
}));
|
||||
local.registerCallback("x", (_payload) => {
|
||||
local.registerCallback("x", () => {
|
||||
if (throws) throw new Error("callback failed");
|
||||
// Reject a nested request before it can put bytes on this stream.
|
||||
let nestedFailed = false;
|
||||
@@ -176,6 +199,7 @@ content += '/** Box documentation. */\nexport class Box { readonly value = 2; me
|
||||
content += 'export const many = [';
|
||||
for (let index = 0; index < 40; index++) content += `${index},`;
|
||||
content += '];\nexport const template = `head\\n${answer}tail`;\n';
|
||||
content += semanticSource();
|
||||
let reads = 0;
|
||||
registerTs7FileSystem(client, {
|
||||
readFile: (path) => {
|
||||
@@ -190,9 +214,9 @@ registerTs7FileSystem(client, {
|
||||
return undefined;
|
||||
},
|
||||
fileExists: (path) => path === file || path === empty || path === configPath ? true : path === hidden ? false : undefined,
|
||||
directoryExists: (_path) => undefined,
|
||||
realpath: (_path) => undefined,
|
||||
getAccessibleEntries: (_path) => undefined,
|
||||
directoryExists: () => undefined,
|
||||
realpath: () => undefined,
|
||||
getAccessibleEntries: () => undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -245,6 +269,21 @@ try {
|
||||
check(nodeType.id === type.id && nodeSymbol.id === symbol.id, "native AST checker query");
|
||||
check(tree.resolve(nodeSymbol.declarations![0]!) === declaration, "checker declaration identity");
|
||||
|
||||
const semanticSnapshot = new SemanticSnapshot(snapshot.snapshot, {
|
||||
text: (method, payload) => client.requestText(method, payload),
|
||||
binary: (method, payload) => client.requestBytes(method, Buffer.from(payload)),
|
||||
});
|
||||
const context = semanticSnapshot.addProject(project.id, (path) => path === file || path === tree.root.path ? tree.root : undefined);
|
||||
const checker = new SemanticChecker(context);
|
||||
const ownedType = checker.getTypeAtLocation(identifier)!;
|
||||
const ownedSymbol = checker.getSymbolAtLocation(identifier)!;
|
||||
check(ownedType.id === type.id && ownedSymbol.id === symbol.id, "semantic model handles");
|
||||
check(checker.getTypeAtPosition(file, content.indexOf("answer")) === ownedType, "semantic type identity");
|
||||
check(checker.getSymbolAtPosition(file, content.indexOf("answer")) === ownedSymbol, "semantic symbol identity");
|
||||
check(ownedSymbol.declarations[0]!.resolve() === declaration, "semantic declaration identity");
|
||||
check(checker.typeToString(ownedType) === "42" && ownedType.isNumberLiteralType() && ownedType.value === 42, "semantic literal metadata");
|
||||
checkSemanticModel(semanticSnapshot, checker, tree);
|
||||
|
||||
// A server-side refusal completes its request. It must not poison the
|
||||
// channel: the frontend's checker panic fence relies on this recovery.
|
||||
let refused = false;
|
||||
@@ -268,6 +307,7 @@ try {
|
||||
writeFileSync(report, JSON.stringify({
|
||||
typeText, symbol: symbol.name, diagnostics: diagnostics.map((diagnostic) => diagnostic.code),
|
||||
surrogateBoundary: checkSurrogateBoundary(),
|
||||
semanticSurrogateBoundary: checkSemanticSurrogateBoundary(), semanticModel: true,
|
||||
echo: true, binaryAst: true, astIdentity: true, virtualFiles: true, retainedSnapshot: true, serverErrorRecovery: true, protocolFailures: true,
|
||||
}));
|
||||
} finally {
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
import { AstFile, AstNode } from "../../../packages/compiler/src/frontend/ts7/ast-node.js";
|
||||
import { AstKind } from "../../../packages/compiler/src/frontend/ts7/ast-schema.generated.js";
|
||||
import { SemanticChecker } from "../../../packages/compiler/src/frontend/ts7/semantic-checker.js";
|
||||
import { SemanticSignature, SemanticSnapshot, SemanticSymbol, SemanticType } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js";
|
||||
import { SemanticSignatureKind, SemanticSymbolFlags, SemanticTypeFlags } from "../../../packages/compiler/src/frontend/ts7/semantic-schema.generated.js";
|
||||
|
||||
export function semanticSource(): string {
|
||||
return '\nexport type Choice = string | number | null;\n'
|
||||
+ 'export type Pair<T> = readonly [T, string];\n'
|
||||
+ 'export const pair: Pair<number> = [1, "a"];\n'
|
||||
+ 'export interface Dict { readonly [key: string]: number; }\n'
|
||||
+ 'export interface Generic<T> { item: T; }\n'
|
||||
+ 'export type GenericAlias = Generic<number>;\n'
|
||||
+ 'export class Child extends Box {}\n'
|
||||
+ '/** Keep the input.\n * @param value input value\n */\n'
|
||||
+ 'export function identity<T extends { n: number }>(value: T): T { return value; }\n'
|
||||
+ 'export const called = identity({ n: 1 });\n'
|
||||
+ 'export function isBox(value: unknown): value is Box { return value instanceof Box; }\n'
|
||||
+ 'export function withThis(this: Box, ...items: number[]): number { return items.length; }\n'
|
||||
+ 'export const huge = -123456789012345678901234567890n;\n'
|
||||
+ 'export const flag = false;\n'
|
||||
+ 'export const emptyString = "";\n'
|
||||
+ 'export const ref = { answer };\n'
|
||||
+ 'export { answer as renamed };\n'
|
||||
+ 'export enum Mode { Ready = 3 }\n'
|
||||
+ 'export const enumValue = Mode.Ready;\n'
|
||||
+ 'export type Nullable = Box | undefined;\n'
|
||||
+ 'export type Conditional<T> = T extends string ? number : boolean;\n'
|
||||
+ 'export type Indexed<T extends Generic<number>> = T["item"];\n'
|
||||
+ 'export type Keys<T> = keyof T;\n'
|
||||
+ 'export type Template<T extends string> = `prefix${T}suffix`;\n'
|
||||
+ 'export type Mapped<T extends string> = Uppercase<T>;\n';
|
||||
}
|
||||
|
||||
function check(value: boolean, message: string): void {
|
||||
if (!value) throw new Error(`Semantic client: ${message}`);
|
||||
}
|
||||
|
||||
function declaration(root: AstNode, name: string): AstNode {
|
||||
for (const statement of root.statements ?? []) {
|
||||
if (statement.name?.text === name) return statement;
|
||||
for (const variable of statement.declarationList?.declarations ?? []) {
|
||||
if (variable.name?.text === name) return variable;
|
||||
}
|
||||
}
|
||||
throw new Error(`Missing semantic fixture declaration ${name}`);
|
||||
}
|
||||
|
||||
function typeAt(checker: SemanticChecker, node: AstNode): SemanticType {
|
||||
const type = checker.getTypeAtLocation(node);
|
||||
if (type === undefined) throw new Error("Missing semantic fixture type");
|
||||
return type;
|
||||
}
|
||||
|
||||
function symbolAt(checker: SemanticChecker, node: AstNode): SemanticSymbol {
|
||||
const symbol = checker.getSymbolAtLocation(node);
|
||||
if (symbol === undefined) throw new Error("Missing semantic fixture symbol");
|
||||
return symbol;
|
||||
}
|
||||
|
||||
function signatureAt(checker: SemanticChecker, node: AstNode): SemanticSignature {
|
||||
const signature = checker.getSignatureFromDeclaration(node);
|
||||
if (signature === undefined) throw new Error("Missing semantic fixture signature");
|
||||
return signature;
|
||||
}
|
||||
|
||||
/** This runs both in Node and in the C/LLVM native executables. Each fact
|
||||
* checks relationships within the live native parser/checker's response;
|
||||
* server-assigned numeric handles never become hard-coded test answers. */
|
||||
export function checkSemanticModel(snapshot: SemanticSnapshot, checker: SemanticChecker, file: AstFile): void {
|
||||
const root = file.root;
|
||||
const answer = declaration(root, "answer");
|
||||
const answerName = answer.name!;
|
||||
const answerSymbol = symbolAt(checker, answerName);
|
||||
const answerType = typeAt(checker, answerName);
|
||||
const missing = root.endOfFileToken!;
|
||||
const nodeTypes = checker.getTypeAtLocation([answerName, missing, answerName]);
|
||||
const nodeSymbols = checker.getSymbolAtLocation([answerName, missing, answerName]);
|
||||
check(nodeTypes[0] === answerType && nodeTypes[2] === answerType, "batched type identity/order");
|
||||
check(nodeSymbols[0] === answerSymbol && nodeSymbols[2] === answerSymbol && nodeSymbols[1] === undefined, "batched symbol null/identity/order");
|
||||
check(checker.getTypeOfSymbol(answerSymbol) === answerType, "symbol type identity");
|
||||
check(checker.getTypeOfSymbol([answerSymbol, answerSymbol])[1] === answerType, "batch symbol types");
|
||||
check(checker.getTypeOfSymbolAtLocation(answerSymbol, answerName) === answerType, "location-dependent symbol type");
|
||||
check(answerSymbol.valueDeclaration?.resolve() === answer && answerSymbol.declarations[0]!.resolve() === answer, "default declaration resolution");
|
||||
check(answerSymbol.declarations[0]!.resolve(checker.project) === answer, "explicit declaration project");
|
||||
check(checker.getTypeAtPosition(root.fileName, [answerName.getStart(), answerName.getStart()])[1] === answerType, "position type batch");
|
||||
check(checker.getSymbolAtPosition(root.fileName, [answerName.getStart(), answerName.getStart()])[1] === answerSymbol, "position symbol batch");
|
||||
check(checker.getResolvedSymbol(answerName)!.getExportSymbol() === answerSymbol, "resolved local/export symbol identity");
|
||||
check(checker.resolveName("answer", SemanticSymbolFlags.Value, { document: root.fileName, position: answerName.getStart() }) === answerSymbol, "position name resolution");
|
||||
check(checker.resolveName("scriptcMissing", SemanticSymbolFlags.Value, answerName) === undefined, "missing name");
|
||||
|
||||
const intrinsicNumber = checker.getNumberType();
|
||||
const intrinsicString = checker.getStringType();
|
||||
check(checker.getBaseTypeOfLiteralType(answerType) === intrinsicNumber, "literal base type");
|
||||
check(answerType.getFreshType()!.getRegularType() === answerType.getRegularType(), "fresh/regular literal identity");
|
||||
const huge = typeAt(checker, declaration(root, "huge").name!);
|
||||
check(huge.isBigIntLiteralType() && huge.value === -123456789012345678901234567890n, "arbitrary precision bigint response");
|
||||
check(typeAt(checker, declaration(root, "flag").name!).value === false, "false literal response");
|
||||
check(typeAt(checker, declaration(root, "emptyString").name!).value === "", "empty string literal response");
|
||||
const intrinsics = [checker.getAnyType(), intrinsicString, intrinsicNumber, checker.getVoidType(), checker.getUndefinedType(), checker.getNullType(), checker.getNeverType(), checker.getUnknownType(), checker.getBigIntType(), checker.getESSymbolType()];
|
||||
for (const type of intrinsics) check(type.isIntrinsicType() && !type.isErrorType() && type.intrinsicName !== undefined, "intrinsic metadata");
|
||||
const boolean = checker.getBooleanType();
|
||||
check(boolean.isUnionType() && boolean.getTypes()!.every((type) => type.isBooleanLiteralType()), "boolean literal union");
|
||||
check(checker.isTypeAssignableTo(answerType, intrinsicNumber) && !checker.isTypeAssignableTo(intrinsicNumber, answerType), "assignability");
|
||||
check(checker.getWidenedType(answerType) !== undefined && checker.getApparentType(intrinsicString)!.isObjectType(), "widened and apparent types");
|
||||
check(checker.getBaseConstraintOfType(answerType) === undefined, "absent base constraint");
|
||||
|
||||
const choice = typeAt(checker, declaration(root, "Choice").type!);
|
||||
check(choice.isUnionType() && !choice.isIntersectionType() && choice.getTypes()!.length === 3, "union constituents");
|
||||
check(choice.getTypes()!.some((type) => type === intrinsicString) && choice.getTypes()!.some((type) => type === intrinsicNumber), "constituent identity");
|
||||
const pair = typeAt(checker, declaration(root, "pair").name!);
|
||||
check(pair.isTypeReference() && checker.isTupleType(pair) && !checker.isArrayType(pair), "tuple reference classification");
|
||||
const pairTarget = pair.getTarget()!;
|
||||
check(pairTarget.isTupleType() && pairTarget.fixedLength === 2 && pairTarget.readonly === true && pairTarget.elementFlags!.length === 2, "tuple target metadata");
|
||||
check(checker.getTypeArguments(pair)[0] === intrinsicNumber && checker.getTypeArguments(pair)[1] === intrinsicString, "tuple argument identities");
|
||||
check(pair.getAliasSymbol()!.name === "Pair" && pair.getAliasTypeArguments()[0] === intrinsicNumber, "alias metadata");
|
||||
|
||||
const box = declaration(root, "Box");
|
||||
const boxSymbol = symbolAt(checker, box.name!);
|
||||
const boxType = checker.getDeclaredTypeOfSymbol(boxSymbol);
|
||||
check(boxType.isClassOrInterface() && boxType.getSymbol() === boxSymbol, "declared class type");
|
||||
// The checker instantiates a transient property symbol for a class type;
|
||||
// the binder's member symbol is distinct but refers to the same AST node.
|
||||
const boundMember = boxSymbol.getMembers().get("value")!;
|
||||
const checkedMember = checker.getPropertyOfType(boxType, "value")!;
|
||||
check(boundMember !== checkedMember && boundMember.declarations[0]!.resolve() === checkedMember.declarations[0]!.resolve(), "bound and instantiated member declarations");
|
||||
check(boxSymbol.getMembers() === boxSymbol.getMembers() && boxSymbol.getExports() === boxSymbol.getExports(), "symbol table cache");
|
||||
check(boxType.getBaseTypes()!.length === 0 && checker.getBaseTypes(boxType).length === 0, "empty base types");
|
||||
const child = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Child").name!));
|
||||
check(child.getBaseTypes()![0] === boxType && checker.getBaseTypes(child)[0] === boxType, "base type request identity");
|
||||
check(checker.getPropertiesOfType(boxType).some((symbol) => symbol.name === "method"), "property query");
|
||||
check(checker.getPropertyOfType(boxType, "missing") === undefined, "absent property");
|
||||
const nullable = typeAt(checker, declaration(root, "Nullable").type!);
|
||||
check(checker.getNonNullableType(nullable) === boxType, "non-nullable type");
|
||||
const dict = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Dict").name!));
|
||||
const index = checker.getIndexInfosOfType(dict)[0]!;
|
||||
check(index.keyType === intrinsicString && index.valueType === intrinsicNumber && index.isReadonly, "index signature types");
|
||||
check(index.declaration!.resolve() === declaration(root, "Dict").members![0], "index signature declaration");
|
||||
const generic = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Generic").name!));
|
||||
const genericParameter = generic.getTypeParameters()[0]!;
|
||||
check(genericParameter.isTypeParameter() && generic.getLocalTypeParameters()[0] === genericParameter && generic.getOuterTypeParameters().length === 0, "interface type parameters");
|
||||
const genericAlias = typeAt(checker, declaration(root, "GenericAlias").type!);
|
||||
check(genericAlias.getTarget() === generic && checker.getTypeArguments(genericAlias)[0] === intrinsicNumber, "generic target");
|
||||
|
||||
const identity = declaration(root, "identity");
|
||||
const identitySymbol = symbolAt(checker, identity.name!);
|
||||
const signature = signatureAt(checker, identity);
|
||||
const signatureTypes = signature.getTypeParameters();
|
||||
check(signatureTypes.length === 1 && signatureTypes[0]!.isTypeParameter(), "signature type parameters");
|
||||
check(signature.getParameters()[0]!.name === "value" && signature.getParameters()[0]!.declarations[0]!.resolve() === identity.parameters![0], "signature parameters");
|
||||
check(signature.declaration!.resolve() === identity, "signature declaration");
|
||||
check(checker.getReturnTypeOfSignature(signature) === signatureTypes[0], "generic return identity");
|
||||
check(checker.getParameterType(signature, 0) === signatureTypes[0], "parameter type query");
|
||||
const constraint = checker.getConstraintOfTypeParameter(signatureTypes[0]!)!;
|
||||
check(checker.getPropertyOfType(constraint, "n") !== undefined && checker.getBaseConstraintOfType(signatureTypes[0]!) === constraint, "generic constraints");
|
||||
check(checker.getSignaturesOfType(typeAt(checker, identity.name!), SemanticSignatureKind.Call)[0] === signature, "signature registry identity");
|
||||
const resolved = checker.getResolvedSignature(declaration(root, "called").initializer!)!;
|
||||
check(resolved.getTarget() === signature && resolved.getTypeParameters().length === 0, "instantiated signature target");
|
||||
const ctor = checker.getSignaturesOfType(checker.getTypeOfSymbol(boxSymbol)!, SemanticSignatureKind.Construct)[0]!;
|
||||
check(ctor.isConstruct && !ctor.isAbstract && checker.getReturnTypeOfSignature(ctor) === boxType, "construct signature");
|
||||
const thisSignature = signatureAt(checker, declaration(root, "withThis"));
|
||||
check(thisSignature.hasRestParameter && !thisSignature.isConstruct && thisSignature.getThisParameter()!.name === "this", "signature this/rest flags");
|
||||
check(checker.getRestTypeOfSignature(thisSignature) === intrinsicNumber && checker.isArrayLikeType(pair), "array predicates and rest element type");
|
||||
const predicate = checker.getTypePredicateOfSignature(signatureAt(checker, declaration(root, "isBox")))!;
|
||||
check(predicate.type === boxType && predicate.parameterIndex === 0 && predicate.parameterName === "value", "type predicate");
|
||||
check(checker.getTypePredicateOfSignature(signature) === undefined, "absent predicate");
|
||||
check(identitySymbol.getJsDocTags(checker).some((tag) => tag.name === "param"), "JSDoc tags");
|
||||
check(identitySymbol.getDocumentationComment(checker).includes("Keep the input"), "JSDoc documentation");
|
||||
|
||||
const moduleSymbol = symbolAt(checker, root);
|
||||
const exports = moduleSymbol.getExports();
|
||||
const renamed = exports.get("renamed")!;
|
||||
check(checker.getAliasedSymbol(renamed) === answerSymbol && checker.getImmediateAliasedSymbol(renamed) === answerSymbol, "symbol alias targets");
|
||||
check(checker.getMemberInModuleExports(moduleSymbol, "answer") === answerSymbol, "module export lookup");
|
||||
check(checker.getExportsOfModule(moduleSymbol).some((symbol) => symbol === answerSymbol), "module exports identity");
|
||||
check(answerSymbol.getExportSymbol() === answerSymbol && answerSymbol.getParent() === moduleSymbol, "symbol parent/export handles");
|
||||
check(!checker.isUnknownSymbol(answerSymbol) && !checker.isUndefinedSymbol(answerSymbol) && !checker.isArgumentsSymbol(answerSymbol), "well-known symbol exclusion");
|
||||
const undefinedSymbol = checker.resolveName("undefined", SemanticSymbolFlags.Value, answerName)!;
|
||||
check(checker.isUndefinedSymbol(undefinedSymbol), "well-known undefined symbol");
|
||||
const shorthand = declaration(root, "ref").initializer!.properties![0]!;
|
||||
check(checker.getShorthandAssignmentValueSymbol(shorthand) === answerSymbol, "shorthand symbol");
|
||||
const exportSpecifier = renamed.declarations[0]!.resolve()!;
|
||||
check(checker.getExportSpecifierLocalTargetSymbol(exportSpecifier) === answerSymbol, "export specifier symbol");
|
||||
check(checker.getConstantValue(declaration(root, "Mode").members![0]!) === 3, "enum constant");
|
||||
check(checker.getConstantValue(answerName) === undefined, "missing constant");
|
||||
check(checker.getContextualType(declaration(root, "pair").initializer!) === pair, "contextual tuple type");
|
||||
check(checker.getTypeFromTypeNode(declaration(root, "Choice").type!) === choice, "type-node query identity");
|
||||
check(!checker.isContextSensitive(answerName), "context sensitivity query");
|
||||
|
||||
const conditional = typeAt(checker, declaration(root, "Conditional").type!);
|
||||
check(conditional.isConditionalType() && conditional.getCheckType()!.isTypeParameter() && conditional.getExtendsType() === intrinsicString, "conditional metadata");
|
||||
check(conditional.getTrueType() === intrinsicNumber && conditional.getFalseType() === checker.getBooleanType(), "conditional lazy branches");
|
||||
const indexed = typeAt(checker, declaration(root, "Indexed").type!);
|
||||
check(indexed.isIndexedAccessType() && indexed.getObjectType()!.isTypeParameter() && indexed.getIndexType()!.value === "item", "indexed access type");
|
||||
const keys = typeAt(checker, declaration(root, "Keys").type!);
|
||||
check(keys.isIndexType() && keys.getTarget()!.isTypeParameter(), "index type target");
|
||||
const template = typeAt(checker, declaration(root, "Template").type!);
|
||||
check(template.isTemplateLiteralType() && template.texts!.join("|") === "prefix|suffix" && template.getTypes()![0]!.isTypeParameter(), "template literal type metadata");
|
||||
const mapped = typeAt(checker, declaration(root, "Mapped").type!);
|
||||
check(mapped.isStringMappingType() && mapped.getTarget()!.isTypeParameter(), "string mapping type");
|
||||
check(checker.typeToTypeNode(intrinsicNumber, answer)!.kind === AstKind.NumberKeyword, "binary type node");
|
||||
check(checker.signatureToSignatureDeclaration(signature, AstKind.FunctionType, identity)!.kind === AstKind.FunctionType, "binary signature node");
|
||||
const references = checker.getReferencesToSymbolInFile(root.fileName, answerSymbol);
|
||||
check(references.length > 0 && references.every((handle) => handle.resolve() !== undefined), "reference node handles");
|
||||
check(checker.getReferencedSymbolsForNode(root, answerName.getStart()).length === 0, "empty referenced-symbol query");
|
||||
const usages = checker.getSignatureUsage(identity);
|
||||
check(usages.length === 1 && usages[0]!.name.resolve()!.text === "identity" && usages[0]!.call!.resolve() === declaration(root, "called").initializer, "signature usage handles");
|
||||
const completions = checker.getCompletionsAtPosition(root.fileName, root.text!.indexOf("Mode.Ready") + 5, { includeSymbol: true })!;
|
||||
const ready = completions.entries.find((entry) => entry.name === "Ready")!;
|
||||
check(ready.symbol === symbolAt(checker, declaration(root, "Mode").members![0]!.name!), "completion symbol identity");
|
||||
check(checker.getCompletionsAtPosition(root.fileName, answerName.getStart()) === undefined, "absent completions");
|
||||
check(answerType.flags === SemanticTypeFlags.NumberLiteral, "literal flags pin");
|
||||
|
||||
// Disposed object graphs must not fetch a replacement under recycled ids.
|
||||
snapshot.dispose();
|
||||
snapshot.dispose();
|
||||
let refused = false;
|
||||
try { answerType.getRegularType(); } catch { refused = true; }
|
||||
check(refused, "type access after disposal");
|
||||
refused = false;
|
||||
try { answerSymbol.declarations[0]!.resolve(); } catch { refused = true; }
|
||||
check(refused, "declaration access after disposal");
|
||||
}
|
||||
@@ -92,15 +92,17 @@ for (const backend of ["c", "llvm"] as const) {
|
||||
expect(built.backend).toBe(backend);
|
||||
const oracle = await runClient(process.execPath, ["--import", "tsx", entry], dir, join(dir, "oracle.json"));
|
||||
const native = await runClient(built.binaryPath, [], dir, join(dir, "native.json"));
|
||||
const { surrogateBoundary: oracleSurrogates, ...oracleFacts } = JSON.parse(oracle);
|
||||
const { surrogateBoundary: nativeSurrogates, ...nativeFacts } = JSON.parse(native);
|
||||
const { surrogateBoundary: oracleSurrogates, semanticSurrogateBoundary: oracleSemanticSurrogates, ...oracleFacts } = JSON.parse(oracle);
|
||||
const { surrogateBoundary: nativeSurrogates, semanticSurrogateBoundary: nativeSemanticSurrogates, ...nativeFacts } = JSON.parse(native);
|
||||
// The native runtime's documented UTF-16 limit must surface as an
|
||||
// explicit AST refusal; replacing a checker name would be corruption.
|
||||
expect(oracleSurrogates).toBe("preserved");
|
||||
expect(nativeSurrogates).toBe("refused");
|
||||
expect(oracleSemanticSurrogates).toBe("preserved");
|
||||
expect(nativeSemanticSurrogates).toBe("refused");
|
||||
expect(nativeFacts).toEqual(oracleFacts);
|
||||
expect(nativeFacts).toEqual({
|
||||
typeText: "42", symbol: "answer", diagnostics: [2322], echo: true, binaryAst: true, astIdentity: true,
|
||||
typeText: "42", symbol: "answer", diagnostics: [2322], echo: true, binaryAst: true, astIdentity: true, semanticModel: true,
|
||||
virtualFiles: true, retainedSnapshot: true, serverErrorRecovery: true, protocolFailures: true,
|
||||
});
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user