From 37551e41b485d475ad4bf8696932db6a17b98025 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 19 Oct 2026 22:17:12 -0700 Subject: [PATCH 01/22] Compile `any` crossings: destructuring, namespace patterns, `any` parameters and async callbacks - Destructuring an `any` value in TypeScript (a function typed `any`, an awaited `any`, `JSON.parse`, an `any` table read) now goes through the checked-dynamic value, as JavaScript sources and #1407's JS-declared results already did. Object sources get V8's RequireObjectCoercible TypeError for the spellings the assignment form reproduces. - A method call or member read on a checker-`any` receiver whose value is a fresh typed call result (`out.split("\n").map(...)`, `.pop()`, `.length`) boxes that temporary and dispatches on the runtime kind, only after every other lowering declined. - `const { a, b: c } = ns` and `= await import("./m")` over a program module bind each export the way `ns.a` reads it. - A defaulted parameter of a program-source function, arrow or function expression that the checker types `any` (a default under an `(...args: any[]) => any` wrapper slot, where the contextual `any` wins) is a dyn ABI slot in the callable's type. - An async lambda whose signature answered a bare dyn result (a library slot `A | Promise` with A = any) produces Promise. - `761-any-objects` compiles and moves back to the corpus; the retired-engine 2083/2104 snapshots shrink to their remaining fences. - JavaScript `Promise.resolve(value)` (in a JavaScript file) over a dyn value holding a promise answers that promise instead of wrapping it, so a TypeScript async callback's result is adopted by JavaScript retry helpers. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/builtins/promises.ts | 27 ++++ .../lowering/expressions/property-access.ts | 28 ++++ .../src/frontend/lowering/lower-calls.ts | 28 ++++ .../src/frontend/lowering/lower-functions.ts | 13 ++ .../src/frontend/lowering/lower-stmts.ts | 139 ++++++++++++------ packages/compiler/src/frontend/type-mapper.ts | 21 +++ .../corpus/6470-any-destructuring.ts.json | 6 + .../corpus/6471-any-method-chains.ts.json | 6 + .../6472-namespace-destructuring/main.ts.json | 7 + .../corpus/6473-any-parameter-values.ts.json | 6 + .../6474-async-any-callbacks/main.ts.json | 6 + .../main.ts.json | 6 + .../tests/corpus/761-any-objects.ts.json | 6 + .../retired-engine-761-any-objects.ts.json | 6 - tests/corpus/6470-any-destructuring.ts | 55 +++++++ tests/corpus/6471-any-method-chains.ts | 24 +++ .../6472-namespace-destructuring/lib.ts | 21 +++ .../6472-namespace-destructuring/main.ts | 19 +++ tests/corpus/6473-any-parameter-values.ts | 23 +++ tests/corpus/6474-async-any-callbacks/main.ts | 41 ++++++ .../node_modules/retry-lite/index.d.ts | 15 ++ .../node_modules/retry-lite/index.js | 36 +++++ .../node_modules/retry-lite/package.json | 6 + .../6474-async-any-callbacks/tsconfig.json | 3 + .../6475-js-promise-resolve-adoption/main.ts | 23 +++ .../node_modules/calls/index.d.ts | 4 + .../node_modules/calls/index.js | 21 +++ .../node_modules/calls/package.json | 6 + .../761-any-objects.ts} | 6 +- ...gine-2083-destructuring-island-decl.ts.txt | 63 -------- ...ine-2104-computed-key-destructuring.ts.txt | 21 +-- .../retired-engine-761-any-objects.ts.txt | 6 - 32 files changed, 559 insertions(+), 139 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-any-destructuring.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-any-method-chains.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-namespace-destructuring/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-any-parameter-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-async-any-callbacks/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-js-promise-resolve-adoption/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/761-any-objects.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-761-any-objects.ts.json create mode 100644 tests/corpus/6470-any-destructuring.ts create mode 100644 tests/corpus/6471-any-method-chains.ts create mode 100644 tests/corpus/6472-namespace-destructuring/lib.ts create mode 100644 tests/corpus/6472-namespace-destructuring/main.ts create mode 100644 tests/corpus/6473-any-parameter-values.ts create mode 100644 tests/corpus/6474-async-any-callbacks/main.ts create mode 100644 tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.d.ts create mode 100644 tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.js create mode 100644 tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/package.json create mode 100644 tests/corpus/6474-async-any-callbacks/tsconfig.json create mode 100644 tests/corpus/6475-js-promise-resolve-adoption/main.ts create mode 100644 tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.d.ts create mode 100644 tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.js create mode 100644 tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/package.json rename tests/{diagnostics/retired-engine-761-any-objects.ts => corpus/761-any-objects.ts} (67%) delete mode 100644 tests/harness/__snapshots__/retired-engine-761-any-objects.ts.txt diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 2656da18b..70e2ee954 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -5,6 +5,7 @@ import { type Lowerer } from "../lowerer.js"; import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { + BOOL, DYN, type IrExpr, type IrFunction, @@ -488,6 +489,32 @@ export function lowerPromiseStaticCall( lowerer.noLowering("Promise.resolve at a unit-typed promise", call); } const value = lowerer.lowerExprExpecting(argNode, resultT.inner); + // In JavaScript, a checked-dynamic argument may itself hold a promise + // (a TypeScript async callback's result: `Promise.resolve(fn())`): + // Promise.resolve answers THAT promise — the same object, adopted — + // and wraps every other value. + if ( + isJsSourceFile(call.getSourceFile()) && + value.type.kind === "dyn" && + resultT.inner.kind === "dyn" + ) { + const local = lowerer.declareHiddenLocal("%resolveValue", DYN); + const ref: IrExpr = { kind: "varRef", localId: local.id, type: DYN, loc }; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], + result: { + kind: "ternary", + cond: { kind: "dynTest", test: "promise", value: ref, type: BOOL, loc }, + then: { kind: "dynCheck", value: ref, type: resultT, loc }, + else_: { kind: "intrinsic", name: "promise.resolve", args: [ref], type: resultT, loc }, + type: resultT, + loc, + }, + type: resultT, + loc, + }; + } return { kind: "intrinsic", name: "promise.resolve", args: [value], type: resultT, loc }; } return null; // reject lands on lowerPromiseRejectCall / the member fence diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index a3154c45a..f564cd333 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1255,6 +1255,27 @@ export function lowerPropertyAccessExpr( const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); return mapped && mapped.kind !== "void" ? lowerer.coerceInto(expr, read, mapped) : read; } + // A checker-`any` receiver whose value is a FRESH typed result of an + // earlier step in the chain (`out.split(" ").length`, where split + // extracted the string and answered a native string[]): the member read + // goes through the boxed checked-dynamic value, as the method-call twin + // does. The box copies; a call result has no other holder to observe it. + if ( + (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Any) !== 0 && + isFreshCallResult(expr.expression) && + lowerer.dynConvertible(recvLowered.type) + ) { + return lowerer.maybeNarrow( + { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(recvLowered, DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + }, + expr, + ); + } lowerer.unsupported( "SC1090", expr, @@ -1262,6 +1283,13 @@ export function lowerPropertyAccessExpr( ); } +/** A call expression, through parentheses: its value has no other holder. */ +function isFreshCallResult(node: ts.Expression): boolean { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + return ts.isCallExpression(e); +} + function importMetaProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null { return isImportMeta(expr.expression) ? importMetaField(lowerer, expr, expr.name.text) : null; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index ed549e4ff..01788c6d5 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1570,6 +1570,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // monomorphized against the defining literal's declaration. lowerObjLitGenericMethodCall(lowerer, expr, expr.expression); if (intrinsic) return intrinsic; + { + const boxed = lowerDynReceiverMethodCall(lowerer, expr, expr.expression, true); + if (boxed) return boxed; + } if (isJsSourceFile(expr.getSourceFile())) { const staticField = lowerStaticFieldRead(lowerer, expr.expression); if (staticField?.type.kind === "dyn" && !expr.arguments.nodes.some(ts.isSpreadElement)) { @@ -3277,6 +3281,13 @@ const ITERATOR_HELPER_METHODS = new Set([ "find", ]); +/** A call expression, through parentheses: its value has no other holder. */ +function isFreshCallResult(node: ts.Expression): boolean { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + return ts.isCallExpression(e); +} + /** METHOD calls on dyn receivers (`pkg.name.replace(...)`, `rawName.split`, * `ws.packages.filter(...)` — JSON.parse-derived values): validate the * receiver's dyn kind, extract, and ride the STATIC method machinery — the @@ -3294,6 +3305,7 @@ function lowerDynReceiverMethodCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, + boxTypedTemporary = false, ): IrExpr | null { if (lowerer.chainBlocked(call, access)) return null; if (stdlibGlobalNameOf(lowerer, access.expression) !== null) { @@ -3476,6 +3488,22 @@ function lowerDynReceiverMethodCall( // 'unknown' slot does. if (checkerUntyped && DYN_HANDLE_KINDS.has(recv.type.kind)) recv = lowerer.coerceToExpected(recv, DYN); + // The last-resort retry (boxTypedTemporary — every other lowering + // already declined): a checker-`any` receiver whose value is a FRESH + // typed result of an earlier step in the chain (`out.split("\n")` + // extracted the string and answered a native string[]) has no typed + // home under its `any` spelling, so it boxes into the checked-dynamic + // tree and the call dispatches on the runtime kind — `.map`, `.pop`, + // `.filter` on that array run JS-exact. Only call results qualify: + // the box copies, and a temporary has no other holder to observe it. + if ( + boxTypedTemporary && + checkerUntyped && + recv.type.kind !== "dyn" && + isFreshCallResult(access.expression) && + lowerer.dynConvertible(recv.type) + ) + recv = lowerer.coerceToExpected(recv, DYN); if (recv.type.kind !== "dyn") return null; if ( access.name.text === "normalize" && diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 0ef601672..6394daaa1 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -214,6 +214,19 @@ export function lowerLambda( ret: funcType.ret.kind === "promise" ? { ...funcType.ret, inner: DYN } : DYN, }; } + // An ASYNC lambda whose signature answered a bare checked-dynamic result + // (`retry(async () => loose())` against a library slot typed + // `(...) => A | Promise` with A = any — the union collapses to `any`): + // calling it still produces a promise, so the value's result is + // Promise, the payload the dyn the body returns. + if ( + funcType.ret.kind === "dyn" && + !ts.isAccessor(node) && + node.asteriskToken === undefined && + node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true + ) { + funcType = { ...funcType, ret: { kind: "promise", inner: DYN } }; + } // A lambda IS a value: the completed-ABI rule applies at birth. The // contextual (target) type decides — `(x?: number) => void` may flow // into a slot annotated `(x: number | undefined) => void` (same ABI diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e530a57f7..c1aaceded 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2087,43 +2087,6 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool .some((p) => p.declarations.some(declarationBelongs)); } -/** True when an `any` value comes from a JavaScript function's result: - * a call (possibly awaited) whose callee is declared in a JavaScript file, - * or a const bound to one. A TypeScript program importing a JavaScript - * package sees the package's inferred types (`jwtVerify` from jose - * answers `any`), and its object destructuring reads the members of that - * checked-dynamic value as JavaScript does — member reads on the same - * value already compile. Other `any` sources keep the fence. */ -function javaScriptAnyOrigin(lowerer: Lowerer, node: ts.Expression, depth: number): boolean { - let expr = node; - while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression; - if (ts.isCallExpression(expr)) { - const callee = ts.isPropertyAccessExpression(expr.expression) - ? expr.expression.name - : expr.expression; - if (!ts.isIdentifier(callee)) return false; - const symbol = lowerer.resolveValueSymbol(callee); - const declarations = symbol ? symbol.declarations : []; - return ( - declarations.length > 0 && - declarations.every((d) => isJsSourceFile(d.getSourceFile()) && ts.isFunctionLike(d)) - ); - } - if (ts.isIdentifier(expr) && depth === 0) { - const symbol = lowerer.resolveValueSymbol(expr); - const declaration = symbol ? symbol.valueDeclaration : undefined; - return ( - declaration !== undefined && - ts.isVariableDeclaration(declaration) && - ts.isIdentifier(declaration.name) && - declaration.initializer !== undefined && - (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0 && - javaScriptAnyOrigin(lowerer, declaration.initializer, depth + 1) - ); - } - return false; -} - /** Destructuring declarations, desugared as sugar over indexed/field * reads: the initializer evaluates ONCE into a hidden temp, then each * bound name declares (or assigns its pre-registered module global) from @@ -2162,18 +2125,18 @@ export function lowerDestructuringDecl( const parseArgsDynObject = init.type.kind === "dyn" && isParseArgsDynCheckerType(lowerer, lowerer.typeOf(decl.initializer)); - // An `any`-origin source needs a represented record or array shape for - // destructuring. JS files retain their per-site runtime fences. + // An `any`-origin source destructures through a represented record or + // array shape, or through the checked-dynamic value it lowered to: the + // pattern reads members (or iterates) the dyn value exactly as a + // JavaScript file's pattern does, with the same per-site runtime fences + // (null/undefined sources throw Node's TypeError). Primitive and unit + // sources keep their own fences below; other carriers keep this one. if ( lowerer.anyOrigin(decl.initializer) && !isJsSourceFile(decl.getSourceFile()) && init.type.kind !== "record" && init.type.kind !== "array" && - !( - init.type.kind === "dyn" && - ts.isObjectBindingPattern(decl.name) && - javaScriptAnyOrigin(lowerer, decl.initializer, 0) - ) + init.type.kind !== "dyn" ) { lowerer.anyOpFence("destructuring", decl); } @@ -2278,6 +2241,38 @@ export function lowerDestructuringDecl( if (groupsSource !== null) init = groupsSource; const tmp = lowerer.declareHiddenLocal("%destr", init.type); out.push({ kind: "varDecl", localId: tmp.id, init, loc }); + // A TypeScript `any` source destructured through its checked-dynamic + // value: RequireObjectCoercible with V8's own TypeError ("Cannot + // destructure property 'a' of 'f(...)' as it is null.") for the source + // spellings the assignment form already reproduces. JavaScript files + // keep their existing per-read errors. + if ( + init.type.kind === "dyn" && + ts.isObjectBindingPattern(decl.name) && + !isJsSourceFile(decl.getSourceFile()) && + lowerer.anyOrigin(decl.initializer) + ) { + const spelling = destrSpellingOf(decl.initializer); + const first = decl.name.elements.nodes[0]; + const firstKey = + first === undefined || first.dotDotDotToken !== undefined + ? undefined + : (first.propertyName ?? first.name); + if (spelling !== null && (firstKey === undefined || ts.isIdentifier(firstKey))) { + out.push({ + kind: "exprStmt", + expr: { + kind: "dynDestrCheck", + value: { kind: "varRef", localId: tmp.id, type: init.type, loc }, + spelling, + ...(firstKey !== undefined ? { firstProp: firstKey.text } : {}), + type: init.type, + loc, + }, + loc, + }); + } + } // V8's destructuring TypeError spells STATIC sources: an identifier // source reads " is not iterable", an identifier-callee call // " is not a function or its return value is not iterable"; @@ -3099,6 +3094,10 @@ export function lowerBindingPattern( lowerArrayPositionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); return; } + if (srcType.kind === "moduleNs" && lowerer.sourceFileOfModuleNamespace(srcType) !== null) { + lowerModuleNamespacePattern(lowerer, pattern, isLet, out); + return; + } if (srcType.kind !== "record") { lowerer.unsupported( "SC1031", @@ -3356,6 +3355,58 @@ function patternBindingType(lowerer: Lowerer, name: ts.BindingName): IrType | nu ); } +/** `const { a, b: c } = ns` / `const { f } = await import("./m")` over + * a program module's namespace: each element reads the export exactly + * like `ns.a` does — the member resolves to the exporter's own binding + * (nsMemberIdentOf's rule), read once at the pattern's position, so the + * bound name holds the export's value at that moment (destructuring + * copies; it does not alias the live binding). The namespace value + * itself was already evaluated into the pattern's temp, so its side + * effects (the dynamic import) keep their order. Rest elements would + * materialize the namespace object and keep their fence. */ +function lowerModuleNamespacePattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + isLet: boolean, + out: IrStmt[], +): void { + const nsType = lowerer.typeOf(pattern); + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + if (el.dotDotDotToken) { + lowerer.unsupported( + "SC1031", + el, + "rest bindings over module namespaces (the namespace object is not materialized — bind the exports you need by name)", + ); + } + const prop = el.propertyName ?? el.name; + const propName = + ts.isIdentifier(prop) || + ts.isComputedPropertyName(prop) || + ts.isStringLiteralLike(prop) || + ts.isNumericLiteral(prop) + ? patternKeyNameOf(lowerer, prop as ts.PropertyName) + : null; + let exported = + propName === null ? undefined : lowerer.checker.getPropertyOfType(nsType, propName); + if (exported !== undefined && exported.flags & ts.SymbolFlags.Alias) { + exported = lowerer.checker.getAliasedSymbol(exported); + } + const declared = exported?.valueDeclaration?.name as ts.Node | undefined; + if (declared === undefined || !ts.isIdentifier(declared)) { + lowerer.unsupported( + "SC1031", + el, + "destructuring module namespace members that are not named value exports", + ); + } + let value = lowerer.lowerExpr(declared); + if (el.initializer) value = applyBindingDefault(lowerer, el, value); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } +} + function patternKeyNameOf(lowerer: Lowerer, prop: ts.PropertyName): string | null { if (ts.isIdentifier(prop) || ts.isPrivateIdentifier(prop)) return prop.text; if (ts.isComputedPropertyName(prop)) return lowerer.foldedStringKeyOf(prop.expression); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index ea706992a..ab29f08ff 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -4309,6 +4309,27 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null ) ) pt = DYN; + // A DEFAULTED program-source parameter of a function, arrow or function + // expression that the checker types `any` — a default under an + // `(...args: any[]) => any` wrapper slot (`wrap((limit = 5) => ...)`), + // where the contextual `any` wins over the default's type: its binding + // is already the checked-dynamic kind inside the body + // (dynFallbackType), so the callable's ABI slot is the same dyn value. + // Other `any` parameters keep their existing paths (an object literal + // holding such a callback stays a checked-dynamic object). + if ( + pt === null && + !rest && + decl !== undefined && + ts.isParameter(decl) && + decl.initializer !== undefined && + (ts.isArrowFunction(decl.parent) || + ts.isFunctionExpression(decl.parent) || + ts.isFunctionDeclaration(decl.parent)) && + !decl.getSourceFile().isDeclarationFile && + (checker.getTypeOfSymbol(p).flags & ts.TypeFlags.Any) !== 0 + ) + pt = DYN; if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions); // Belt and braces for non-strict type worlds: an optional param's // ABI slot is always the undefined-armed union (strictNullChecks diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-any-destructuring.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-any-destructuring.ts.json new file mode 100644 index 000000000..eb1120cae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-any-destructuring.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6470-any-destructuring.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-any-method-chains.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-any-method-chains.ts.json new file mode 100644 index 000000000..463bc3a67 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-any-method-chains.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6471-any-method-chains.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-namespace-destructuring/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-namespace-destructuring/main.ts.json new file mode 100644 index 000000000..ec90234c8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-namespace-destructuring/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6472-namespace-destructuring/lib.ts", + "/tests/corpus/6472-namespace-destructuring/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-any-parameter-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-any-parameter-values.ts.json new file mode 100644 index 000000000..1747655f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-any-parameter-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6473-any-parameter-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-async-any-callbacks/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-async-any-callbacks/main.ts.json new file mode 100644 index 000000000..6a7e7a966 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-async-any-callbacks/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6474-async-any-callbacks/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-js-promise-resolve-adoption/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-js-promise-resolve-adoption/main.ts.json new file mode 100644 index 000000000..d27bf5fd6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-js-promise-resolve-adoption/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6475-js-promise-resolve-adoption/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/761-any-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/761-any-objects.ts.json new file mode 100644 index 000000000..aa907b2ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/761-any-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/761-any-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-761-any-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-761-any-objects.ts.json deleted file mode 100644 index ed04bcd16..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-761-any-objects.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/retired-engine-761-any-objects.ts" - ], - "diags": [] -} diff --git a/tests/corpus/6470-any-destructuring.ts b/tests/corpus/6470-any-destructuring.ts new file mode 100644 index 000000000..3d615472d --- /dev/null +++ b/tests/corpus/6470-any-destructuring.ts @@ -0,0 +1,55 @@ +// Destructuring `any` values in TypeScript: a function typed to return +// `any` (an untyped command runner), an awaited `any` promise, JSON.parse, +// and an `any` table read by key all destructure through the +// checked-dynamic value, with renames, defaults, nesting, rest, holes and +// Node's TypeError for a null source. +function run(command: string): any { + return { stdout: ` ${command} output \n`, stderr: "", exitCode: 0, meta: { pid: 42 } }; +} + +const { stdout, stderr } = run("echo"); +console.log(JSON.stringify(stdout.toString().trim()), stderr.length); + +const { exitCode: code, missing, meta: { pid }, ...rest } = run("ls"); +console.log(code, missing, pid, Object.keys(rest).join(",")); + +const { signal = "none", exitCode = 9 } = run("true"); +console.log(signal, exitCode); + +const [first, , third = "default", ...others] = JSON.parse('["a", "b", null, "d", "e"]'); +console.log(first, third, others.length); + +const { list: [head, ...tail] } = JSON.parse('{"list":[1,2,3]}'); +console.log(head, tail); + +async function fetchAny(): Promise { + return { body: "payload", status: 200 }; +} + +async function main(): Promise { + const { body, status } = await fetchAny(); + console.log(body, status); + const pending = fetchAny(); + const { status: again } = await pending; + console.log(again + 1); +} + +const table: any = { a: { mode: 0o755, path: "/bin" }, b: { mode: 0o644, path: "/etc" } }; +for (const key in table) { + const { mode, path } = table[key]; + console.log(key, mode.toString(8), path); +} + +function lookup(found: boolean): any { + return found ? { anything: 1 } : null; +} +for (const found of [true, false]) { + try { + const { anything } = lookup(found); + console.log(anything); + } catch (e) { + console.log(e instanceof TypeError, (e as Error).message); + } +} + +main(); diff --git a/tests/corpus/6471-any-method-chains.ts b/tests/corpus/6471-any-method-chains.ts new file mode 100644 index 000000000..3bfce5fba --- /dev/null +++ b/tests/corpus/6471-any-method-chains.ts @@ -0,0 +1,24 @@ +// Method chains over `any`-derived strings: each step that answers a fresh +// native value (split's string[]) keeps chaining through the +// checked-dynamic value — map, filter(Boolean), pop, join — and the result +// checks back into typed slots. +function exec(): any { + return { stdout: " 3f2a\n 9c1b \n\n77de\n", stderr: "warn: shallow\n" }; +} + +const { stdout, stderr } = exec(); +let ids: string[] = []; +ids = stdout + .split("\n") + .map((s: string) => s.trim()) + .filter(Boolean); +console.log(ids.length, ids.join(", ")); + +const last: string = stdout.trim().split("\n").pop() ?? ""; +console.log(JSON.stringify(last)); + +const firstWord: string = stderr.trim().split(":")[0]; +console.log(firstWord, stderr.trim().split(" ").length); + +const words: string[] = stdout.trim().split(/\s+/); +console.log(words.length, stdout.split("\n").length, stdout.trim().split("\n").join("|")); diff --git a/tests/corpus/6472-namespace-destructuring/lib.ts b/tests/corpus/6472-namespace-destructuring/lib.ts new file mode 100644 index 000000000..d25d53d74 --- /dev/null +++ b/tests/corpus/6472-namespace-destructuring/lib.ts @@ -0,0 +1,21 @@ +export function add(a: number, b: number): number { + return a + b; +} + +export const label = "lib"; + +export let counter = 1; + +export function bump(): void { + counter++; +} + +export class Shape { + side: number; + constructor(side: number) { + this.side = side; + } + area(): number { + return this.side * this.side; + } +} diff --git a/tests/corpus/6472-namespace-destructuring/main.ts b/tests/corpus/6472-namespace-destructuring/main.ts new file mode 100644 index 000000000..dee614272 --- /dev/null +++ b/tests/corpus/6472-namespace-destructuring/main.ts @@ -0,0 +1,19 @@ +// Destructuring a program module's namespace, statically +// (`import * as ns`) and from a dynamic import: each element reads the +// export at the pattern's position — renames and defaults included — and +// binds a copy of its value (a later reassignment of an exported `let` is +// visible through the namespace, not through the binding). +import * as ns from "./lib.ts"; + +const { add, label, counter, bump, Shape: Ctor } = ns; +console.log(add(2, 3), label, counter); +bump(); +console.log(counter, ns.counter); +console.log(new Ctor(4).area()); + +async function main(): Promise { + const { add: plus, label: name = "unused" } = await import("./lib.ts"); + console.log(plus(10, 20), name); +} + +main(); diff --git a/tests/corpus/6473-any-parameter-values.ts b/tests/corpus/6473-any-parameter-values.ts new file mode 100644 index 000000000..399332551 --- /dev/null +++ b/tests/corpus/6473-any-parameter-values.ts @@ -0,0 +1,23 @@ +// Function values whose defaulted parameter the checker types `any`: under +// an `(...args: any[]) => any` wrapper slot the contextual `any` wins over +// the default's type. The callable's slot is the checked-dynamic value the +// body already reads. +function traced any>(name: string, fn: F): F { + console.log("wrap", name); + return fn; +} + +function configured(raw = process.env["SCRIPTC_TEST_UNSET_WIDTH"]): number | undefined { + const width = Number.parseInt(raw ?? "", 10); + return Number.isSafeInteger(width) && width > 0 ? width : undefined; +} + +const layout = traced("layout", (width = configured() ?? 80) => { + return { width, gutter: 2 }; +}); +console.log(JSON.stringify(layout()), JSON.stringify(layout(120))); + +const scaled = traced("scaled", function (factor = 2, base = 10) { + return base * factor; +}); +console.log(scaled(), scaled(3), scaled(undefined, 1)); diff --git a/tests/corpus/6474-async-any-callbacks/main.ts b/tests/corpus/6474-async-any-callbacks/main.ts new file mode 100644 index 000000000..c84ce37ba --- /dev/null +++ b/tests/corpus/6474-async-any-callbacks/main.ts @@ -0,0 +1,41 @@ +// Async callbacks whose result the checker types `Promise` handed to +// a JavaScript library typed `(...) => A | Promise` (A = any collapses +// the slot to `any`): the callback value still returns a promise, and its +// payload is the checked-dynamic value the body produced. +import retry from "retry-lite"; + +function loose(attempt: number): any { + if (attempt < 3) throw new Error(`attempt ${attempt} failed`); + return { text: `ok after ${attempt}`, size: attempt * 10 }; +} + +async function main(): Promise { + const got = await retry(async (_bail, attempt) => loose(attempt), { + retries: 5, + onRetry: (e: Error) => console.log("retry:", e.message), + }); + console.log(got.text, got.size); + + const parsed = await retry( + async () => { + const value = JSON.parse('{"items":[1,2,3]}'); + return value; + }, + { retries: 1 }, + ); + console.log(parsed.items.length); + + try { + await retry( + async (bail) => { + bail(new Error("gave up")); + throw new Error("ignored"); + }, + { retries: 2 }, + ); + } catch (e) { + console.log("caught:", (e as Error).message); + } +} + +main(); diff --git a/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.d.ts b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.d.ts new file mode 100644 index 000000000..896f81f9d --- /dev/null +++ b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.d.ts @@ -0,0 +1,15 @@ +declare function retryLite( + fn: retryLite.RetryFunction, + opts: retryLite.Options +): Promise; + +declare namespace retryLite { + interface Options { + retries?: number; + onRetry?: (e: Error) => any; + } + + type RetryFunction = (bail: (e: Error) => void, attempt: number) => A | Promise; +} + +export = retryLite; diff --git a/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.js b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.js new file mode 100644 index 000000000..a1ae718ab --- /dev/null +++ b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.js @@ -0,0 +1,36 @@ +"use strict"; + +function retryLite(fn, opts) { + const retries = opts.retries === undefined ? 3 : opts.retries; + return new Promise((resolve, reject) => { + let attempt = 0; + let bailed = false; + const bail = (err) => { + bailed = true; + reject(err); + }; + const onError = (err) => { + if (bailed) return; + if (attempt > retries) { + reject(err); + return; + } + if (opts.onRetry) opts.onRetry(err); + runAttempt(); + }; + const runAttempt = () => { + attempt++; + let val; + try { + val = fn(bail, attempt); + } catch (err) { + onError(err); + return; + } + Promise.resolve(val).then(resolve).catch(onError); + }; + runAttempt(); + }); +} + +module.exports = retryLite; diff --git a/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/package.json b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/package.json new file mode 100644 index 000000000..251988634 --- /dev/null +++ b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/package.json @@ -0,0 +1,6 @@ +{ + "name": "retry-lite", + "version": "1.0.0", + "main": "./index.js", + "types": "./index.d.ts" +} diff --git a/tests/corpus/6474-async-any-callbacks/tsconfig.json b/tests/corpus/6474-async-any-callbacks/tsconfig.json new file mode 100644 index 000000000..9297bdb5b --- /dev/null +++ b/tests/corpus/6474-async-any-callbacks/tsconfig.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": { "strict": true, "types": ["node"], "esModuleInterop": true } +} diff --git a/tests/corpus/6475-js-promise-resolve-adoption/main.ts b/tests/corpus/6475-js-promise-resolve-adoption/main.ts new file mode 100644 index 000000000..61a58dbe2 --- /dev/null +++ b/tests/corpus/6475-js-promise-resolve-adoption/main.ts @@ -0,0 +1,23 @@ +// JavaScript's Promise.resolve over a checked-dynamic value that holds a +// promise (a TypeScript async callback's result handed back into a +// JavaScript library) answers that same promise — adopted, not wrapped — +// and wraps plain values. +import { sameAfterResolve, viaExecutor, viaResolve, viaThen } from "calls"; + +async function main(): Promise { + console.log(await viaResolve(async (): Promise => "resolved")); + console.log(await viaThen(async (): Promise => 42)); + console.log(await viaExecutor(async (): Promise => "executor")); + console.log(await viaExecutor((): string => "plain")); + try { + await viaExecutor(async (): Promise => { + throw new Error("callback failed"); + }); + } catch (e) { + console.log("caught:", (e as Error).message); + } + const pending = Promise.resolve("p"); + console.log(sameAfterResolve(pending), sameAfterResolve("p")); +} + +main(); diff --git a/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.d.ts b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.d.ts new file mode 100644 index 000000000..d1040a227 --- /dev/null +++ b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.d.ts @@ -0,0 +1,4 @@ +export declare function viaResolve(fn: () => A | Promise): Promise; +export declare function viaThen(fn: () => A | Promise): Promise; +export declare function viaExecutor(fn: () => A | Promise): Promise; +export declare function sameAfterResolve(value: unknown): boolean; diff --git a/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.js b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.js new file mode 100644 index 000000000..fb55d867c --- /dev/null +++ b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.js @@ -0,0 +1,21 @@ +"use strict"; + +function viaResolve(fn) { + return Promise.resolve(fn()); +} + +function viaThen(fn) { + return Promise.resolve(fn()).then((value) => value); +} + +function viaExecutor(fn) { + return new Promise((resolve, reject) => { + Promise.resolve(fn()).then(resolve).catch(reject); + }); +} + +function sameAfterResolve(value) { + return Promise.resolve(value) === value; +} + +module.exports = { viaResolve, viaThen, viaExecutor, sameAfterResolve }; diff --git a/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/package.json b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/package.json new file mode 100644 index 000000000..c85cd1f4b --- /dev/null +++ b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/package.json @@ -0,0 +1,6 @@ +{ + "name": "calls", + "version": "1.0.0", + "main": "./index.js", + "types": "./index.d.ts" +} diff --git a/tests/diagnostics/retired-engine-761-any-objects.ts b/tests/corpus/761-any-objects.ts similarity index 67% rename from tests/diagnostics/retired-engine-761-any-objects.ts rename to tests/corpus/761-any-objects.ts index 59796020b..45e6e6f82 100644 --- a/tests/diagnostics/retired-engine-761-any-objects.ts +++ b/tests/corpus/761-any-objects.ts @@ -1,6 +1,6 @@ -// Object/array literals in `any` slots build natively in the island; -// property reads/writes, computed keys, and method calls go through the -// engine (this-binding and coercions JS-exact). +// Object/array literals in `any` slots: property reads/writes, computed +// keys, and method calls go through the checked-dynamic value, including a +// member read on a fresh typed call result (`s.split(" ").length`). const o: any = { a: 1, b: "x", nested: { deep: true } }; o.a = o.a + 10; o["c"] = 2.5; diff --git a/tests/harness/__snapshots__/retired-engine-2083-destructuring-island-decl.ts.txt b/tests/harness/__snapshots__/retired-engine-2083-destructuring-island-decl.ts.txt index 531002a4b..4bbff5a33 100644 --- a/tests/harness/__snapshots__/retired-engine-2083-destructuring-island-decl.ts.txt +++ b/tests/harness/__snapshots__/retired-engine-2083-destructuring-island-decl.ts.txt @@ -1,66 +1,3 @@ -retired-engine-2083-destructuring-island-decl.ts:9:5 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 8 | const a: any = [1, "two", true, 4, 5]; - 9 | var [x, , y, ...rest] = a; - | ^~~~~~~~~~~~~~~~~~~~~ - 10 | console.log(`${x} ${y} ${rest.length} ${rest[0]} ${rest[1]}`); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - -retired-engine-2083-destructuring-island-decl.ts:12:7 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 11 | const o: any = { p: 1, q: { r: "deep" } }; - 12 | const { p, q: { r } } = o; - | ^~~~~~~~~~~~~~~~~~~ - 13 | console.log(`${p} ${r}`); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - -retired-engine-2083-destructuring-island-decl.ts:14:7 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 13 | console.log(`${p} ${r}`); - 14 | const { missing = 42 } = o; - | ^~~~~~~~~~~~~~~~~~~~ - 15 | console.log(`${missing}`); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - -retired-engine-2083-destructuring-island-decl.ts:16:7 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 15 | console.log(`${missing}`); - 16 | const { p: p2 = 9, ...others } = o; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 17 | console.log(`${p2} ${others.q.r}`); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - -retired-engine-2083-destructuring-island-decl.ts:17:22 - error SC2004: uses of 'others' inherit the blocker on its declaration - - 16 | const { p: p2 = 9, ...others } = o; - 17 | console.log(`${p2} ${others.q.r}`); - | ^~~~~~~~~~ - 18 | var [] = [1, 2] as any; - - hint: the declaration of 'others' did not compile — fix the diagnostic reported there and these sites clear with it - -retired-engine-2083-destructuring-island-decl.ts:19:5 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 18 | var [] = [1, 2] as any; - 19 | var {} = o; - | ^~~~~~ - 20 | console.log("empties ok"); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - -retired-engine-2083-destructuring-island-decl.ts:22:11 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 21 | const u: any = undefined; - 22 | try { var {} = u; } catch (e) { console.log("obj-undef throws"); } - | ^~~~~~ - 23 | try { var [] = 0 as any; } catch (e) { console.log("arr-num throws"); } - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - retired-engine-2083-destructuring-island-decl.ts:23:11 - error SC2011: destructuring on 'any'-typed values cannot compile statically 22 | try { var {} = u; } catch (e) { console.log("obj-undef throws"); } diff --git a/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt b/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt index b41cd4d10..415eb5895 100644 --- a/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt +++ b/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt @@ -1,12 +1,10 @@ -retired-engine-2104-computed-key-destructuring.ts:12:9 - error SC2011: destructuring on 'any'-typed values cannot compile statically +retired-engine-2104-computed-key-destructuring.ts:12:11 - error SC1031: destructuring with computed keys that do not fold to one property name is not supported yet 11 | function pick(key: string) { 12 | const { [key]: got } = src; - | ^~~~~~~~~~~~~~~~~~~~ + | ^~~~~~~~~~ 13 | return got; - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - retired-engine-2104-computed-key-destructuring.ts:13:10 - error SC2004: uses of 'got' inherit the blocker on its declaration 12 | const { [key]: got } = src; @@ -16,15 +14,13 @@ retired-engine-2104-computed-key-destructuring.ts:13:10 - error SC2004: uses of hint: the declaration of 'got' did not compile — fix the diagnostic reported there and these sites clear with it -retired-engine-2104-computed-key-destructuring.ts:17:9 - error SC2011: destructuring on 'any'-typed values cannot compile statically +retired-engine-2104-computed-key-destructuring.ts:17:11 - error SC1031: destructuring with computed keys that do not fold to one property name is not supported yet 16 | function pick2(key: string) { 17 | const { [key]: got = "dflt" } = src; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~ 18 | return got; - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - retired-engine-2104-computed-key-destructuring.ts:18:10 - error SC2004: uses of 'got' inherit the blocker on its declaration 17 | const { [key]: got = "dflt" } = src; @@ -34,15 +30,6 @@ retired-engine-2104-computed-key-destructuring.ts:18:10 - error SC2004: uses of hint: the declaration of 'got' did not compile — fix the diagnostic reported there and these sites clear with it -retired-engine-2104-computed-key-destructuring.ts:23:7 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 22 | const key2 = "y"; - 23 | const { x: sx, [key2]: sy } = src; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ - 24 | console.log(`${sx} ${sy}`); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - retired-engine-2104-computed-key-destructuring.ts:30:38 - error SC2009: values of type '[any?]' cannot be compiled: the tuple shape is supported, but its element type 'any' does not compile 29 | let order = (n: any): any => { traceS += `${n},`; return n; }; diff --git a/tests/harness/__snapshots__/retired-engine-761-any-objects.ts.txt b/tests/harness/__snapshots__/retired-engine-761-any-objects.ts.txt deleted file mode 100644 index 008901f2d..000000000 --- a/tests/harness/__snapshots__/retired-engine-761-any-objects.ts.txt +++ /dev/null @@ -1,6 +0,0 @@ -retired-engine-761-any-objects.ts:13:38 - error SC1090: reading 'length' from a value of type 'any' is not supported yet - - 12 | const s: any = "hello world"; - 13 | console.log(`${s.toUpperCase()}`, `${s.split(" ").length}`, `${s.charAt(4)}`); - | ^~~~~~~~~~~~~~~~~~~ - 14 | const num: any = 3.14159; \ No newline at end of file From a64d7f8ce12c36e39c405525098c94a59a6fc18d Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 00:39:10 -0700 Subject: [PATCH 02/22] Run JavaScript promise executors passed by name, and read `catch (e: any)` bindings dynamically - `new Promise(run)` where `run` is a JavaScript function value whose parameters are untyped (the retry-helper shape `function run(resolve, reject) {...}; return new Promise(run)`) gets an adapter executor: it receives the native resolve/reject pair and calls the original with both boxed as checked-dynamic function values. This was a deferred runtime refusal inside such packages. - A catch binding annotated `any` reads the caught value through its checked-dynamic face, as an unannotated `unknown` one does: member reads, expando writes, `delete` and `in` act on the thrown object, and a rethrow keeps its identity. It was refused (SC1063). Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 5 +- .../src/frontend/lowering/lower-exprs.ts | 9 +- .../lowering/promise-executor-adapter.ts | 102 ++++++++++++++++++ .../main.ts.json | 6 ++ .../corpus/6491-catch-any-bindings.ts.json | 6 ++ .../6490-js-named-promise-executor/main.ts | 22 ++++ .../node_modules/later/index.d.ts | 1 + .../node_modules/later/index.js | 31 ++++++ .../node_modules/later/package.json | 6 ++ .../tsconfig.json | 3 + tests/corpus/6491-catch-any-bindings.ts | 34 ++++++ 11 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/promise-executor-adapter.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-js-named-promise-executor/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-catch-any-bindings.ts.json create mode 100644 tests/corpus/6490-js-named-promise-executor/main.ts create mode 100644 tests/corpus/6490-js-named-promise-executor/node_modules/later/index.d.ts create mode 100644 tests/corpus/6490-js-named-promise-executor/node_modules/later/index.js create mode 100644 tests/corpus/6490-js-named-promise-executor/node_modules/later/package.json create mode 100644 tests/corpus/6490-js-named-promise-executor/tsconfig.json create mode 100644 tests/corpus/6491-catch-any-bindings.ts diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 6589d7c04..afac089f8 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,4 +1,5 @@ import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; +import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; import { constructedFromValue, @@ -9856,7 +9857,9 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // catch-side instanceof and the uncaught printer working). First // settle wins, exactly JS: reject-after-resolve and double-reject // are no-ops, and an executor throw after any settle is swallowed. - const executor = lowerer.lowerExpr(args[0]!); + let executor = lowerer.lowerExpr(args[0]!); + if (executor.type.kind !== "func") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!)); + executor = adaptPromiseExecutor(lowerer, executor, type.inner, loc) ?? executor; if (executor.type.kind !== "func") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!)); if (executor.type.params.length > 1) { const rj = executor.type.params[1]!; diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 6cbe5d7a4..49f49987d 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -2132,8 +2132,13 @@ export function caughtRead( } // Unnarrowed reads cross into unknown. The caught-value dispatch pass // retains known native class payloads through live identity capsules; - // other payload kinds use the runtime's ordinary conversion. - if (narrowed?.kind === "dyn") { + // other payload kinds use the runtime's ordinary conversion. A binding + // annotated `any` (`catch (error: any)`) reads the same checked-dynamic + // value: its member reads, writes and deletes go through it. + if ( + narrowed?.kind === "dyn" || + (narrowed === null && (lowerer.typeOf(node).flags & ts.TypeFlags.Any) !== 0) + ) { return { kind: "caughtToDyn", value: ref, type: DYN, loc }; } lowerer.unsupported("SC1063", node); diff --git a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts new file mode 100644 index 000000000..09d228b05 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts @@ -0,0 +1,102 @@ +import { varRef } from "../../ir/build.js"; +import { + type IrExpr, + type IrLocal, + type IrParam, + type IrType, + type SrcLoc, + VOID, + funcOf, + typeKey, +} from "../../ir/ir.js"; +import { newFnCtx, type Lowerer } from "./lowerer.js"; + +/** An executor VALUE whose parameters are checked-dynamic (a JavaScript + * function declaration: `function run(resolve, reject) {...}; return new + * Promise(run)`): the promise's native executor is an adapter closure that + * receives the native (resolve, reject) pair and calls the original with + * both boxed into the checked-dynamic tree, so the JavaScript body calls + * them like any function value. Null when the executor's shape is not + * adaptable (more than two parameters, a rest parameter, or a parameter + * that neither is dyn nor already the native closure). */ +export function adaptPromiseExecutor( + lowerer: Lowerer, + executor: IrExpr, + inner: IrType, + loc: SrcLoc, +): IrExpr | null { + const execT = executor.type; + if (execT.kind !== "func" || execT.rest || execT.params.length > 2) return null; + if (!execT.params.some((p) => p.kind === "dyn")) return null; + const resolveT = funcOf(inner.kind === "void" ? [] : [inner], VOID); + const rejectT = funcOf([{ kind: "object", className: "%Error" }], VOID); + const native = [resolveT, rejectT]; + if ( + !execT.params.every((p, i) => p.kind === "dyn" || typeKey(p) === typeKey(native[i]!)) || + !native.slice(0, execT.params.length).every((t) => lowerer.dynConvertible(t)) + ) + return null; + const adaptT = funcOf(native, VOID); + const key = `promise.executor:${typeKey(execT)}:${typeKey(inner)}`; + let name = lowerer.arrHofHelpers.get(key); + if (name === undefined) { + name = `%promise.executor.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + const impl = `${name}.impl`; + const captured: IrLocal = { id: "f.0", name: "f", type: execT, mutable: false, boxed: true }; + const params: IrParam[] = [ + { localId: "resolve.0", name: "resolve", type: resolveT }, + { localId: "reject.0", name: "reject", type: rejectT }, + ]; + const ctx = newFnCtx(false, null, null, VOID); + lowerer.fnStack.push(ctx); + let args: IrExpr[]; + try { + args = execT.params.map((p, i) => + lowerer.coerceToExpected(varRef(params[i]!.localId, native[i]!, loc), p), + ); + } finally { + lowerer.fnStack.pop(); + } + lowerer.liftedFns.push({ + name: impl, + params, + returnType: VOID, + captures: [{ localId: captured.id, name: captured.name, type: execT }], + locals: [ + captured, + ...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + ...ctx.locals, + ], + body: [ + { + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef(captured.id, execT, loc), + args, + type: execT.ret, + loc, + }, + loc, + }, + ], + loc, + }); + lowerer.liftedFns.push({ + name, + params: [{ localId: captured.id, name: captured.name, type: execT }], + returnType: adaptT, + locals: [captured], + body: [ + { + kind: "return", + value: { kind: "closure", fnName: impl, captures: [captured.id], type: adaptT, loc }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: name, args: [executor], type: adaptT, loc }; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-js-named-promise-executor/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-js-named-promise-executor/main.ts.json new file mode 100644 index 000000000..dd95018f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-js-named-promise-executor/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6490-js-named-promise-executor/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-catch-any-bindings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-catch-any-bindings.ts.json new file mode 100644 index 000000000..bc69a66e3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-catch-any-bindings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6491-catch-any-bindings.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6490-js-named-promise-executor/main.ts b/tests/corpus/6490-js-named-promise-executor/main.ts new file mode 100644 index 000000000..79e2a17d7 --- /dev/null +++ b/tests/corpus/6490-js-named-promise-executor/main.ts @@ -0,0 +1,22 @@ +// A JavaScript library that builds its promise from a named executor +// function (`function run(resolve, reject) {...}; return new Promise(run)`, +// the classic retry helper shape): the executor's parameters are untyped, +// so it receives the native resolve and reject as checked-dynamic function +// values and calls them like any JavaScript function. +import { attempt } from "later"; +async function main(): Promise { + const v = await attempt(async (_bail: (e: Error) => void, n: number): Promise => { + if (n < 3) throw new Error("try " + n); + return "done after " + n; + }, 5); + console.log(v); + try { + await attempt((bail: (e: Error) => void): string => { + bail(new Error("stop")); + return "ignored"; + }, 5); + } catch (e) { + console.log("caught", (e as Error).message); + } +} +main(); diff --git a/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.d.ts b/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.d.ts new file mode 100644 index 000000000..55956f01d --- /dev/null +++ b/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.d.ts @@ -0,0 +1 @@ +export declare function attempt(fn: (bail: (e: Error) => void, n: number) => A | Promise, retries: number): Promise; diff --git a/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.js b/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.js new file mode 100644 index 000000000..deefa6aee --- /dev/null +++ b/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.js @@ -0,0 +1,31 @@ +"use strict"; +function attempt(fn, retries) { + function run(resolve, reject) { + let n = 0; + function bail(err) { + reject(err || new Error("Aborted")); + } + function onError(err) { + if (err.bail) { + bail(err); + return; + } + if (n > retries) reject(err); + else runAttempt(); + } + function runAttempt() { + n++; + let val; + try { + val = fn(bail, n); + } catch (err) { + onError(err); + return; + } + Promise.resolve(val).then(resolve).catch(onError); + } + runAttempt(); + } + return new Promise(run); +} +module.exports = { attempt }; diff --git a/tests/corpus/6490-js-named-promise-executor/node_modules/later/package.json b/tests/corpus/6490-js-named-promise-executor/node_modules/later/package.json new file mode 100644 index 000000000..ce1f29bac --- /dev/null +++ b/tests/corpus/6490-js-named-promise-executor/node_modules/later/package.json @@ -0,0 +1,6 @@ +{ + "name": "later", + "version": "1.0.0", + "main": "./index.js", + "types": "./index.d.ts" +} diff --git a/tests/corpus/6490-js-named-promise-executor/tsconfig.json b/tests/corpus/6490-js-named-promise-executor/tsconfig.json new file mode 100644 index 000000000..27d492dd2 --- /dev/null +++ b/tests/corpus/6490-js-named-promise-executor/tsconfig.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": { "strict": true, "types": ["node"] } +} diff --git a/tests/corpus/6491-catch-any-bindings.ts b/tests/corpus/6491-catch-any-bindings.ts new file mode 100644 index 000000000..9979637b4 --- /dev/null +++ b/tests/corpus/6491-catch-any-bindings.ts @@ -0,0 +1,34 @@ +// A catch binding annotated `any` reads the caught value through its +// checked-dynamic face: member reads, expando writes, `delete` and `in` +// act on the thrown object itself, and a rethrow keeps its identity. +const thrown: Error[] = []; + +function fail(n: number): never { + const e = new Error(`failure ${n}`); + thrown.push(e); + throw e; +} + +for (const n of [1, 2]) { + try { + try { + fail(n); + } catch (error: any) { + error.bail = n === 1; + console.log("marked:", error.bail, "bail" in error, error.message); + if (error.bail) { + delete error.bail; + console.log("deleted:", error.bail, "bail" in error); + } + throw error; + } + } catch (outer) { + console.log("outer:", outer === thrown[n - 1], (outer as Error).message, "bail" in (outer as object)); + } +} + +try { + JSON.parse("{"); +} catch (error: any) { + console.log(error.name, typeof error.message, error instanceof SyntaxError); +} From 51b001444fee6bb48dc466eac166d22285af6659 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 05:15:24 -0500 Subject: [PATCH 03/22] Count a computed key as a stack read only when it can be "stack" A program that spelled "stack" anywhere treated every computed-key read on a checked-dynamic value as a possible read of an error's stack, so it kept a source frame in every function. The key of such a read now counts only when it can be "stack": a string literal answers by its value; a number, bigint or boolean key, or one converted from such a value, cannot be "stack"; an immutable local answers with its one initializer, and a parameter of a function the program only calls by name answers with the arguments its call sites pass. Any other key counts when the program spells "stack", as before. --- .../src/backend/stack-observations.test.ts | 49 ++++++ .../src/backend/stack-observations.ts | 142 +++++++++++++++--- 2 files changed, 169 insertions(+), 22 deletions(-) diff --git a/packages/compiler/src/backend/stack-observations.test.ts b/packages/compiler/src/backend/stack-observations.test.ts index 0e1178ce9..4a5a5de13 100644 --- a/packages/compiler/src/backend/stack-observations.test.ts +++ b/packages/compiler/src/backend/stack-observations.test.ts @@ -84,6 +84,55 @@ test("a computed key reads the stack when the program spells it", () => { expect(observesStacks(mod([fn("%main", [computed])]))).toBe(false); }); +test('a computed key that cannot be "stack" reads no stack', () => { + const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN }; + const spelled: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "strLit", value: "stack", type: STRING }, + }; + const read = (key: IrExpr): IrStmt => ({ + loc, + kind: "exprStmt", + expr: { loc, kind: "dynKeyGet", value, key, type: DYN }, + }); + const index: IrExpr = { loc, kind: "varRef", localId: "i", type: F64 }; + const indexText: IrExpr = { loc, kind: "toString", operand: index, type: STRING }; + // A number key, and a key converted from one. + expect(observesStacks(mod([fn("%main", [read(index), spelled])]))).toBe(false); + expect(observesStacks(mod([fn("%main", [read(indexText), spelled])]))).toBe(false); + // An immutable local answers with its initializer (the shape of the + // dispatch helpers' formatted index keys). + const key: IrExpr = { loc, kind: "varRef", localId: "key", type: STRING }; + const local = (init: IrExpr, mutable = false): IrFunction => ({ + ...fn("%main", [{ loc, kind: "varDecl", localId: "key", init }, read(key), spelled]), + locals: [{ id: "key", name: "key", type: STRING, mutable }], + }); + expect(observesStacks(mod([local(indexText)]))).toBe(false); + expect(observesStacks(mod([local(indexText, true)]))).toBe(true); + // A parameter of a function called by name answers with its arguments. + const helper: IrFunction = { + ...fn("readKey", [read(key)]), + params: [{ localId: "key", name: "key", type: STRING }], + locals: [{ id: "key", name: "key", type: STRING, mutable: false }], + }; + const callWith = (arg: IrExpr): IrStmt => ({ + loc, + kind: "exprStmt", + expr: { loc, kind: "call", callee: "readKey", args: [arg], type: DYN }, + }); + const numbers = mod([fn("%main", [callWith(indexText), spelled]), helper]); + expect(observesStacks(numbers)).toBe(false); + const stack = mod([ + fn("%main", [ + callWith(indexText), + callWith({ loc, kind: "strLit", value: "stack", type: STRING }), + ]), + helper, + ]); + expect(observesStacks(stack)).toBe(true); +}); + test("programs without stack reads are not", () => { expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false); }); diff --git a/packages/compiler/src/backend/stack-observations.ts b/packages/compiler/src/backend/stack-observations.ts index cc6d109e4..fbb1c23cf 100644 --- a/packages/compiler/src/backend/stack-observations.ts +++ b/packages/compiler/src/backend/stack-observations.ts @@ -3,7 +3,7 @@ * it answers true. */ import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js"; import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js"; -import type { IrExpr, IrFunction, IrModule, IrStmt } from "../ir/ir.js"; +import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js"; import { MAY_THROW_ARR_METHODS } from "../ir/ir.js"; import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js"; import { unreachableStatements } from "./may-throw.js"; @@ -13,16 +13,122 @@ import { writesPublished } from "./publication.js"; /** Lib calls that read a captured stack trace. */ const STACK_READS: ReadonlySet = new Set(["error.stack"]); +/** Key types whose property names are never `stack`: a number, bigint or + * boolean key names its decimal or `true`/`false` property. */ +const NON_STACK_KEY_TYPES: ReadonlySet = new Set(["f64", "bigint", "bool"]); + +/** Whether a computed key can be the string "stack", the question a read + * on a checked-dynamic value asks before it counts as a stack read. + * - A string literal answers by its value; a key of a number, bigint or + * boolean type, or one converted from such a value, cannot be "stack". + * - An immutable local answers with its one initializer, and a parameter of + * a function the program only calls by name answers with the arguments + * its call sites pass. + * - Any other key can be "stack" when the program spells the literal + * "stack" somewhere, since the literal can reach the key through fields, + * containers or calls; a key the program assembles from other strings or + * from data it reads keeps the static answer. */ +class StackKeys { + private readonly spelled: boolean; + private readonly calls = new Map(); + private readonly named: Set; + private readonly inits = new Map>(); + private readonly visiting = new Set(); + + constructor(mod: IrModule) { + this.spelled = spellsStack(mod); + this.named = new Set(moduleFunctionReferences(mod)); + // Class methods, accessors and constructors are called through dispatch. + const classNames = new Set((mod.classes ?? []).map((cls) => cls.name)); + for (const fn of mod.functions) { + if (isClassMember(fn.name, classNames)) this.named.add(fn.name); + everyStmtList(fn.body, { + stmt: () => true, + expr: (expr) => { + if (expr.kind === "call") { + const sites = this.calls.get(expr.callee) ?? []; + sites.push({ caller: fn, args: expr.args }); + this.calls.set(expr.callee, sites); + } + const referenced = exprFunctionReference(expr); + if (referenced !== undefined) this.named.add(referenced); + return true; + }, + }); + } + } + + mayBeStack(fn: IrFunction, key: IrExpr): boolean { + if (key.kind === "strLit") return key.value === "stack"; + if (!this.spelled) return false; + if (NON_STACK_KEY_TYPES.has(key.type.kind)) return false; + if (key.kind === "toString" && NON_STACK_KEY_TYPES.has(key.operand.type.kind)) return false; + if (key.kind === "dynFrom" && NON_STACK_KEY_TYPES.has(key.value.type.kind)) return false; + if (key.kind !== "varRef") return true; + const visit = `${fn.name}\0${key.localId}`; + // A cycle adds no value of its own: the answer comes from its entries. + if (this.visiting.has(visit)) return false; + this.visiting.add(visit); + try { + return this.localMayBeStack(fn, key.localId); + } finally { + this.visiting.delete(visit); + } + } + + private localMayBeStack(fn: IrFunction, localId: string): boolean { + const local = fn.locals.find((candidate) => candidate.id === localId); + if (local === undefined || local.mutable) return true; + const index = fn.params.findIndex((param) => param.localId === localId); + if (index !== -1) { + const sites = this.calls.get(fn.name); + if (this.named.has(fn.name) || sites === undefined) return true; + return sites.some( + (site) => + site.args.length !== fn.params.length || this.mayBeStack(site.caller, site.args[index]!), + ); + } + const init = this.initializerOf(fn, localId); + return init === null || this.mayBeStack(fn, init); + } + + /** The one initializer of an immutable local, or null when it has none + * or more than one declaration. */ + private initializerOf(fn: IrFunction, localId: string): IrExpr | null { + let inits = this.inits.get(fn); + if (inits === undefined) { + inits = new Map(); + everyStmtList(fn.body, { + stmt: (stmt) => { + if (stmt.kind === "varDecl") + inits!.set(stmt.localId, inits!.has(stmt.localId) ? null : (stmt.init ?? null)); + return true; + }, + expr: () => true, + }); + this.inits.set(fn, inits); + } + return inits.get(localId) ?? null; + } +} + +/** Whether `name` follows the `%.` convention of class + * methods, accessors and constructors for one of `classNames`. */ +function isClassMember(name: string, classNames: ReadonlySet): boolean { + if (!name.startsWith("%")) return false; + for (let dot = name.indexOf("."); dot !== -1; dot = name.indexOf(".", dot + 1)) { + if (classNames.has(name.slice(1, dot))) return true; + } + return false; +} + /** A typed `error.stack` read, or a read on a checked-dynamic value that - * can reach an error's own `stack` accessor: the constant key `stack`, or - * a computed key when `computedKeys` (the program spells "stack", so a - * computed key can name it). A key the program assembles from other - * strings or from data it reads keeps the static answer. */ -function readsStack(expr: IrExpr, computedKeys: boolean): boolean { + * can reach an error's own `stack` accessor: one whose key can be "stack" + * (StackKeys). */ +function readsStack(fn: IrFunction, expr: IrExpr, keys: StackKeys): boolean { if (expr.kind === "libCall") return STACK_READS.has(expr.fn); if (expr.kind !== "dynKeyGet") return false; - if (expr.key.kind === "strLit") return expr.key.value === "stack"; - return computedKeys; + return keys.mayBeStack(fn, expr.key); } /** Whether the module contains the string literal "stack". */ @@ -36,10 +142,10 @@ function spellsStack(mod: IrModule): boolean { ); } -function containsStackRead(fn: IrFunction, computedKeys: boolean): boolean { +function containsStackRead(fn: IrFunction, keys: StackKeys): boolean { return !everyStmtList(fn.body, { stmt: () => true, - expr: (expr) => !readsStack(expr, computedKeys), + expr: (expr) => !readsStack(fn, expr, keys), }); } @@ -65,22 +171,14 @@ function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void { * and those reads must not cost every call of the program a frame. * Programs without any stack read answer false without the walk. */ export function observesStacks(mod: IrModule): boolean { - const computedKeys = spellsStack(mod); - if (!mod.functions.some((fn) => containsStackRead(fn, computedKeys))) return false; + const keys = new StackKeys(mod); + if (!mod.functions.some((fn) => containsStackRead(fn, keys))) return false; const byName = new Map(mod.functions.map((fn) => [fn.name, fn])); const classNames = new Set((mod.classes ?? []).map((cls) => cls.name)); const pending: string[] = [...moduleFunctionReferences(mod)]; // Class methods, accessors and constructors are reachable through // dispatch the IR does not name per site: all of them are roots. - for (const fn of mod.functions) { - if (!fn.name.startsWith("%")) continue; - for (let dot = fn.name.indexOf("."); dot !== -1; dot = fn.name.indexOf(".", dot + 1)) { - if (classNames.has(fn.name.slice(1, dot))) { - pending.push(fn.name); - break; - } - } - } + for (const fn of mod.functions) if (isClassMember(fn.name, classNames)) pending.push(fn.name); const reached = new Set(); let observed = false; while (pending.length > 0 && !observed) { @@ -90,7 +188,7 @@ export function observesStacks(mod: IrModule): boolean { const fn = byName.get(name); if (fn === undefined) continue; visitLiveExprs(fn.body, (expr) => { - if (readsStack(expr, computedKeys)) observed = true; + if (readsStack(fn, expr, keys)) observed = true; if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee); const referenced = exprFunctionReference(expr); if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced); From c7b2620e5cf6dd94dbe198352fc99a0b33e39073 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 08:24:54 -0700 Subject: [PATCH 04/22] Compile events.once with an AbortSignal option and over stream receivers events.once(emitter, name, { signal }) lowers to events.onceSignal, served by a new runtime unit (scr_events_once_signal.c, linked only through the eventsOnceSignal feature). It follows Node 24's lib/events.js: an undefined signal is the plain form, a value that is not an AbortSignal rejects with ERR_INVALID_ARG_TYPE, an aborted signal rejects with an AbortError whose cause is signal.reason, and otherwise a once, resist-stop-propagation 'abort' listener removes the event and 'error' listeners and rejects. The event or the error removes the abort listener. Options other than an object literal with one signal stay refused. events.once now accepts stream receivers: a stream's runtime-emitted events use their fixed tuples, and its 'data' and 'keypress' events stay refused. A NodeJS stream interface slot that holds a stdio handle requests the general stream representation. Node's once is an async function that returns the executor's promise, so its result adopts that promise through a thenable job. The plain form settled its result two turns early; both forms now settle on Node's turn. Co-Authored-By: Claude Opus 5.5 --- .../src/backend/executable-features.ts | 1 + packages/compiler/src/backend/llvm/emitter.ts | 7 +- .../src/backend/llvm/expr-callbacks.ts | 12 +- .../compiler/src/backend/llvm/lib-dispatch.ts | 3 +- .../compiler/src/backend/llvm/lib-process.ts | 31 +- .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 3 + .../compiler/src/backend/native/executable.ts | 3 + .../compiler/src/backend/runtime-pack-core.ts | 2 + .../src/executable/executable-cache.ts | 1 + .../frontend/lowering/lower-event-emitter.ts | 92 ++++-- packages/compiler/src/index.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 4 + packages/compiler/src/ir/runtime-features.ts | 4 + packages/compiler/src/ir/validate.ts | 4 +- .../compiler/src/native/sanitizer.test.ts | 1 + .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_events_emitter.c | 33 +- packages/runtime/src/scr_events_emitter.h | 14 + packages/runtime/src/scr_events_once_signal.c | 303 ++++++++++++++++++ 20 files changed, 479 insertions(+), 45 deletions(-) create mode 100644 packages/runtime/src/scr_events_once_signal.c diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 74d5de278..df2dc8446 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -26,6 +26,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { dynAsync: features.dynAsync, ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), + ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), events: features.processEvents, emitter: features.emitter, symbol: features.symbol, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index f7b9f2517..56ec6c1a3 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -5568,8 +5568,11 @@ export class LlEmitter { return emitterFixedAdapter(this, cbT); } - emitterOnceAdapter(boxerT: IrType & { kind: "func" }): { fn: string; shim: string } { - return emitterOnceAdapter(this, boxerT); + emitterOnceAdapter( + boxerT: IrType & { kind: "func" }, + fire?: string, + ): { fn: string; shim: string } { + return emitterOnceAdapter(this, boxerT, fire); } wrapEmitterListener(target: string, adapterFn: string): string { diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index db1dca1af..e805760d8 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -822,23 +822,27 @@ export function emitterFixedAdapter( * (the event's tuple to any[]): the runtime-built listener closure holds * the boxer in caps[0], so the adapter calls it with the event's typed * arguments, as emitterFixedAdapter calls a listener, and hands the array - * to scr_events_once_fire. */ + * to scr_events_once_fire (or `fire`, the signal form's entry). */ export function emitterOnceAdapter( host: LlvmEmitterContext, boxerT: IrType & { kind: "func" }, + fire = "scr_events_once_fire", ): { fn: string; shim: string } { const cursor = boxerT.params.length > 4; const shim = cursor ? "scr_ee_inv_args" : `scr_ee_inv_fixed${boxerT.params.length}`; if (cursor) host.declare("declare ptr @scr_ee_arg_next(ptr)"); host.declare(`declare void @${shim}(ptr, ptr)`); - const key = `eeOnce:${typeKey(boxerT)}`; + const key = + fire === "scr_events_once_fire" + ? `eeOnce:${typeKey(boxerT)}` + : `eeOnce:${fire}:${typeKey(boxerT)}`; let sym = host.resolveThunks.get(key); if (sym) return { fn: sym, shim }; sym = `sc_ee_once_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_closure_release(ptr)`); - host.declare(`declare void @scr_events_once_fire(ptr, ptr)`); + host.declare(`declare void @${fire}(ptr, ptr)`); const params = cursor ? "ptr %args" : boxerT.params.map((_, i) => `ptr %a${i}`).join(", "); const d: string[] = [ `define internal void @${sym}(ptr %cb${params ? ", " + params : ""}) ${FN_ATTRS} { ; events.once adapter ${typeKey(boxerT)}`, @@ -869,7 +873,7 @@ export function emitterOnceAdapter( ` %fn = load ptr, ptr %fnp`, ` %arr = call ptr %fn(${passed.join(", ")}) ; the boxed arguments, +1`, ` call void @scr_closure_release(ptr %boxer)`, - ` call void @scr_events_once_fire(ptr %cb, ptr %arr) ; moves %arr`, + ` call void @${fire}(ptr %cb, ptr %arr) ; moves %arr`, ` ret void`, `}`, ``, diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 16759a592..8dab775e5 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -755,7 +755,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { } const prefix = e.fn.slice(0, e.fn.indexOf(".")) as LibCallPrefix; // events.once builds typed listener adapters beside the emitter calls. - if (e.fn === "events.once") return host.emitErrorsEventsLibCall(e); + if (e.fn === "events.once" || e.fn === "events.onceSignal") + return host.emitErrorsEventsLibCall(e); switch (prefix) { case "fetch": case "abort": diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 4a7be1a35..097fcb4de 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -731,27 +731,34 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr host.emitAlwaysPendingUnwind(); return out; } - if (e.fn === "events.once") { - // (recv, name, boxer, errorBoxer, listenForError): scr_events_once - // registers one-shot typed listeners behind adapters that box the - // event's tuple (and the 'error' tuple) into the fulfilled array. The - // runtime retains the boxers; the frame's temporaries release as usual. - // May-throw ('newListener' meta listeners run inside). + if (e.fn === "events.once" || e.fn === "events.onceSignal") { + // (recv, name, boxer, errorBoxer, listenForError[, signal]): + // scr_events_once registers one-shot typed listeners behind adapters + // that box the event's tuple (and the 'error' tuple) into the fulfilled + // array. The runtime retains the boxers; the frame's temporaries + // release as usual. May-throw ('newListener' meta listeners run + // inside). The signal form's adapters end in its own fire entry. const boxerT = e.args[2]!.type; const errorT = e.args[3]!.type; if (boxerT.kind !== "func" || errorT.kind !== "func") - throw new InternalCompilerError("llvm emitter bug: events.once boxer not a func"); + throw new InternalCompilerError(`llvm emitter bug: ${e.fn} boxer not a func`); + const signal = e.fn === "events.onceSignal"; const args = e.args.map((a) => host.emitExpr(a)); - const event = host.emitterOnceAdapter(boxerT); - const error = host.emitterOnceAdapter(errorT); - host.declare(`declare ptr @scr_events_once(ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr)`); + const fire = signal ? "scr_events_once_signal_fire" : undefined; + const event = host.emitterOnceAdapter(boxerT, fire); + const error = host.emitterOnceAdapter(errorT, fire); + const entry = signal ? "scr_events_once_signal" : "scr_events_once"; + host.declare( + `declare ptr @${entry}(ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr${signal ? ", ptr" : ""})`, + ); const errorBoxer = B.tmp(); B.line(`${errorBoxer} = select i1 ${args[4]!.name}, ptr ${args[3]!.name}, ptr null`); const t = B.tmp(); B.line( - `${t} = call ptr @scr_events_once(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` + + `${t} = call ptr @${entry}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` + `ptr ${args[2]!.name}, ptr @${event.fn}, ptr @${event.shim}, ` + - `ptr ${errorBoxer}, ptr @${error.fn}, ptr @${error.shim})`, + `ptr ${errorBoxer}, ptr @${error.fn}, ptr @${error.shim}` + + `${signal ? `, ptr ${args[5]!.name}` : ""})`, ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index df03d528a..38531267e 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -43,6 +43,9 @@ export interface NativeLinkFeatures { /** Checked-dynamic stream slots: the scr_stream_dyn.c unit. Absent (not * false) for programs without them. */ streamDyn?: boolean; + /** events.once with options: scr_events_once_signal.c. Absent (not + * false) for programs without it. */ + eventsOnceSignal?: boolean; events: boolean; emitter: boolean; symbol: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 3b700ea69..31b223fa6 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -116,6 +116,9 @@ export interface CcOptions { /** Checked-dynamic stream slots (stream.dynNative / stream.pipeDyn): * compiles scr_stream_dyn.c. */ streamDyn?: boolean; + /** events.once with options (events.onceSignal): compiles + * scr_events_once_signal.c. */ + eventsOnceSignal?: boolean; /** The program constructs EventTarget/Event/CustomEvent values or * calls events.addAbortListener (the event.* libCalls), or uses the * native fetch AbortSignal built on the same engine: compiles diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index d2616151b..f8266cc38 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -217,6 +217,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ // measures with. unit((u) => u.opts.events && u.regex, "scr_readline_tty.c"), unit((u) => u.opts.emitter || u.opts.workers, "scr_events_emitter.c"), + // events.once with options registers through the emitter and + // event-target units. + unit((u) => u.opts.eventsOnceSignal === true, "scr_events_once_signal.c"), // The checked-dynamic HANDLE support unit (listener gate + runtime // adapter closures): every referencing unit is one of the emitter or // net families (http implies net), so handle-free binaries keep diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 41fa1442a..dc986d7d1 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -84,6 +84,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { eventTarget: boolean; dynTier: boolean; streamDyn: boolean; + eventsOnceSignal: boolean; warningReplace: boolean; stdioExtended: boolean; nativeFetch: boolean; @@ -275,6 +276,7 @@ export function effectiveRuntimeFeatures( eventTarget: features.eventTarget === true, dynTier: features.dynTier === true, streamDyn: features.streamDyn === true, + eventsOnceSignal: features.eventsOnceSignal === true, warningReplace: features.warningReplace === true, stdioExtended: features.stdioExtended === true, nativeFetch, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 7e3b427e5..45a186afa 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -66,6 +66,7 @@ export interface EarlyExecutableNativeFeatures { dynAsync: boolean; dynTier?: boolean; streamDyn?: boolean; + eventsOnceSignal?: boolean; events: boolean; emitter: boolean; symbol: boolean; diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index 4b8b9ea14..4f08ecc0d 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -72,6 +72,7 @@ import { STREAM_FORCED_EVENT_NAMES, streamForcedTuple, streamSidesOf } from "./l import { boolLit, strLit, varRef } from "../../ir/build.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerAddAbortListener } from "./builtin-calls.js"; +import { requestGeneralStreamSurface } from "./general-streams.js"; /** True when the class descends from (or is) the runtime emitter. */ export function emitterRooted(lowerer: Lowerer, info: ClassInfo | undefined | null): boolean { @@ -2204,24 +2205,54 @@ function eventsOnceBoxer( return { kind: "closure", fnName: name, captures: [], type: fnType, loc }; } -/** events.once(emitter, name): a promise of the next `name` event's - * arguments as an array, rejected by an earlier 'error' event (unless the - * awaited event is 'error'). The emitter's listeners are typed by the - * program-wide event table, so the runtime registers one-shot typed - * listeners whose compiler-built boxers turn the event's tuple into the - * array. EventTarget and stream receivers, computed names and options are - * refused. */ +/** The `signal` of an events.once options object literal (a checked- + * dynamic AbortSignal or undefined), or null when the literal names none. + * Other options forms are refused. */ +function eventsOnceSignal(lowerer: Lowerer, node: ts.Expression): IrExpr | null { + if (!ts.isObjectLiteralExpression(node)) { + lowerer.noLowering( + "events.once options that are not an object literal", + node, + "pass the options as an object literal: { signal }", + ); + return null; + } + let signal: IrExpr | null = null; + for (const prop of node.properties.nodes) { + const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null; + if ( + name === "signal" && + signal === null && + (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) + ) { + signal = lowerer.lowerExprExpecting( + ts.isPropertyAssignment(prop) ? prop.initializer : (prop.name as ts.Identifier), + DYN, + ); + } else { + lowerer.noLowering( + "events.once options other than one signal", + prop, + "the lowered options object takes signal: AbortSignal", + ); + } + } + return signal; +} + +/** events.once(emitter, name[, { signal }]): a promise of the next `name` + * event's arguments as an array, rejected by an earlier 'error' event + * (unless the awaited event is 'error') or by the signal's abort with an + * AbortError. The emitter's listeners are typed by the program-wide event + * table (a stream's runtime-emitted events by their fixed tuples), so the + * runtime registers one-shot typed listeners whose compiler-built boxers + * turn the event's tuple into the array. EventTarget receivers, a stream's + * 'data' event, computed names and options other than a literal { signal } + * are refused. */ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { const args = call.arguments.nodes; - if (args.length < 2 || args.some(ts.isSpreadElement)) { - lowerer.noLowering("events.once without an emitter and an event name", call); - } - if (args.length > 2) { - lowerer.noLowering( - "events.once with options", - args[2]!, - "the lowered form is events.once(emitter, name); AbortSignal options are not supported yet", - ); + if (args.length < 2 || args.length > 3 || args.some(ts.isSpreadElement)) { + lowerer.noLowering("events.once without an emitter, an event name and options", call); } // Node's Promise: the arguments array of checked-dynamic values. const arrayT = arrayOf(DYN); @@ -2229,11 +2260,14 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: const receiver = lowerer.lowerExpr(args[0]!); const info = receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined; - if (!emitterRooted(lowerer, info) || streamSidesOf(lowerer, info) !== null) { + if (!emitterRooted(lowerer, info)) { + // A NodeJS stream interface slot that holds a stdio handle here: the + // program lowers again with the runtime stream classes. + requestGeneralStreamSurface(lowerer, args[0]!); lowerer.noLowering( "events.once over this value", args[0]!, - "the emitter must be an EventEmitter instance; EventTarget and stream receivers are not supported yet", + "the emitter must be an EventEmitter or stream instance; EventTarget receivers are not supported yet", ); } const name = literalEventName(lowerer, args[1]!); @@ -2245,6 +2279,14 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: ); } const event = name ?? ""; + const sides = streamSidesOf(lowerer, info); + if ((sides === "r" || sides === "rw") && (event === "data" || event === "keypress")) { + lowerer.noLowering( + `events.once of a stream's '${event}' event`, + args[1]!, + `the chunk has no checked-dynamic boxing here; register stream.once('${event}', listener) instead`, + ); + } const table = emitterEvents(lowerer); if (event !== "error" && flexibleJsEvent(event, table.get(event))) { lowerer.noLowering( @@ -2252,14 +2294,22 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: args[1]!, ); } - const tuple = tupleOf(lowerer, table, event, call); + const tuple = streamForcedTuple(lowerer, info, event) ?? tupleOf(lowerer, table, event, call); const errorTuple: IrType[] = [{ kind: "object", className: "%Error" }]; const boxer = eventsOnceBoxer(lowerer, call, tuple, arrayT, loc); const errorBoxer = eventsOnceBoxer(lowerer, call, errorTuple, arrayT, loc); + const signal = args.length > 2 ? eventsOnceSignal(lowerer, args[2]!) : null; + const common = [ + receiver, + eventNameExpr(event, loc), + boxer, + errorBoxer, + boolLit(event !== "error", loc), + ]; return { kind: "libCall", - fn: "events.once", - args: [receiver, eventNameExpr(event, loc), boxer, errorBoxer, boolLit(event !== "error", loc)], + fn: signal === null ? "events.once" : "events.onceSignal", + args: signal === null ? common : [...common, signal], type: resultT, loc, }; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 12b84654b..4b70ec7b0 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -538,6 +538,7 @@ async function compileExecutableNative( dynAsync: features.dynAsync, ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), + ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), events: features.events, emitter: features.emitter, symbol: features.symbol, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 2d70a7254..efd721557 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4174,6 +4174,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * errorBoxer, listenForError] → a pending promise of any[]; the boxers * turn the event's and the 'error' tuple into that array. */ "events.once": { argTypes: [null, STRING, null, null, BOOL], result: VOID }, + /** events.once(emitter, name, { signal }) (scr_events_once_signal.c): + * events.once's arguments plus the checked-dynamic signal (an + * AbortSignal or undefined). */ + "events.onceSignal": { argTypes: [null, STRING, null, null, BOOL, DYN], result: VOID }, /** The resolve-value forms: [ms?, value, ref] → a pending promise of the * value's type that copies the borrowed already-resolved `value` promise * when the timer (or immediate) fires; ref false unrefs it. The result diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 9b45fad1a..7b1b759b6 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -129,6 +129,7 @@ const LEGACY_TEXT_DECODER_LIB_FNS: ReadonlySet = new Set([ /** Signal-taking readline, stream and timers/promises entry points, which register abort listeners. */ const ABORT_SIGNAL_LIB_FNS: ReadonlySet = new Set([ + "events.onceSignal", "http.clientSignal", "rl.createSignal", "rl.questionAsyncSignal", @@ -527,6 +528,8 @@ const RUNTIME_FEATURE_RULES = { fns: ["events.addAbortListener", ...ABORT_SIGNAL_LIB_FNS], prefixes: ["event."], }), + /** events.once(emitter, name, { signal }) (scr_events_once_signal.c). */ + eventsOnceSignal: rule({ fns: ["events.onceSignal"] }), fetch: rule({ prefixes: ["fetch."] }), fileHandle: rule({ types: ["fileHandle"] }), fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }), @@ -652,6 +655,7 @@ function noRuntimeFeatures(): RuntimeFeatures { dynTier: false, emitter: false, eventTarget: false, + eventsOnceSignal: false, fetch: false, fileHandle: false, fsWatch: false, diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 68997c295..e652eeb52 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4154,7 +4154,7 @@ function validateFunction( if (!ok) err(`libCall ${e.fn} must be a promise of its ${cryptoPromise} value`, e.loc); return true; } - if (e.fn === "events.once") { + if (e.fn === "events.once" || e.fn === "events.onceSignal") { const [recv, , boxer, errorBoxer] = e.args; const arrayT = e.type.kind === "promise" ? e.type.inner : null; const boxes = (b: IrExpr | undefined) => @@ -4165,7 +4165,7 @@ function validateFunction( !boxes(boxer) || !boxes(errorBoxer) ) { - err(`libCall events.once must box its event tuples into the promised array`, e.loc); + err(`libCall ${e.fn} must box its event tuples into the promised array`, e.loc); } return true; } diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index d9efb7cd8..28bb80169 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -77,6 +77,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( eventTarget: false, dynTier: false, streamDyn: false, + eventsOnceSignal: false, warningReplace: false, stdioExtended: false, regex: true, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 790864677..aa8eb880b 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -76,6 +76,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_readline.c", "events"], ["scr_readline_tty.c", all("events", "regex")], ["scr_events_emitter.c", "emitter"], + // events.once with options registers through the emitter and event-target units. + ["scr_events_once_signal.c", "eventsOnceSignal"], ["scr_dyn_handle.c", any("emitter", "netEffective")], ["scr_symbol.c", "symbol"], ["scr_bigint_assert.c", all("assert", "bigint")], diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index e0e305bde..4ac7abdf1 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -518,8 +518,25 @@ static ScrClosure *scr_ee_once_closure(void *adapter, ScrClosure *boxer, ScrEeOn return c; } +static ScrBox *scr_ee_once_promise_box(ScrPromise *p) { + ScrBox *box = scr_box_new_obj(&scr_promise_retain_v, &scr_promise_release_v, &scr_promise_trace_v); + scr_box_set_ref(box, scr_promise_retain(p)); + return box; +} + +static void scr_ee_once_adopt_job(ScrClosure *job) { + ScrPromise *outer = scr_box_get_ref(job->caps[0]); + ScrPromise *inner = scr_box_get_ref(job->caps[1]); + scr_promise_race_join(outer, inner, &scr_promise_adapt_copy); + scr_promise_release(inner); + scr_promise_release(outer); +} + /* boxer and err_boxer are borrowed; the result is +1. err_boxer is NULL - * when the awaited event is 'error' itself. */ + * when the awaited event is 'error' itself. Node's once is an async + * function returning the executor's promise, so the result adopts that + * promise through a thenable job queued here, whose reaction settles the + * result one turn after the executor's promise settles. */ ScrPromise *scr_events_once(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, void *adapter, ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter, ScrEeInvoke err_inv) { @@ -558,9 +575,13 @@ ScrPromise *scr_events_once(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, voi scr_caught_release(thrown); scr_promise_reject_pending(st->promise); } - ScrPromise *p = scr_promise_retain(st->promise); + ScrPromise *outer = scr_promise_new(); + ScrClosure *job = scr_closure_new((void *)&scr_ee_once_adopt_job, 2); + job->caps[0] = scr_ee_once_promise_box(outer); + job->caps[1] = scr_ee_once_promise_box(st->promise); + scr_queue_microtask(job); scr_ee_once_release(st); - return p; + return outer; } /* The adapter's tail: `cb` is the firing listener closure, `args` the @@ -1130,3 +1151,9 @@ double scr_emitter_get_default_max(void) { return scr_emitter_default_max; } static SCR_TL double scr_emitter_esm_default_max = 10; double scr_emitter_esm_default_max_get(void) { return scr_emitter_esm_default_max; } void scr_emitter_esm_sync(void) { scr_emitter_esm_default_max = scr_emitter_default_max; } + +/* events.once with a signal (scr_events_once_signal.c) undoes an add whose + * 'newListener' announcement threw. */ +void scr_emitter_unadd_quiet(ScrEmitter *em, ScrStr *name, ScrClosure *cb) { + scr_ee_unadd(em, name, cb); +} diff --git a/packages/runtime/src/scr_events_emitter.h b/packages/runtime/src/scr_events_emitter.h index a92ae1864..39a7f4b5b 100644 --- a/packages/runtime/src/scr_events_emitter.h +++ b/packages/runtime/src/scr_events_emitter.h @@ -64,6 +64,20 @@ ScrPromise *scr_events_once(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, voi ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter, ScrEeInvoke err_inv); void scr_events_once_fire(ScrClosure *cb, ScrArr *args); +/* events.once(emitter, name, { signal }) (scr_events_once_signal.c): the + * scr_events_once arguments plus the checked-dynamic signal (borrowed; + * undefined for the plain form). Never throws: a non-AbortSignal value, + * an aborted signal or a throwing 'newListener' listener rejects the +1 + * result. Its adapters end in scr_events_once_signal_fire. */ +typedef struct ScrDyn ScrDyn; /* full definition below (C11 repeat) */ +ScrPromise *scr_events_once_signal(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, void *adapter, + ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter, + ScrEeInvoke err_inv, ScrDyn *signal); +void scr_events_once_signal_fire(ScrClosure *cb, ScrArr *args); +/* Removes the listener entry whose closure is `cb` (borrowed) without a + * 'removeListener' event: the undo of an add whose 'newListener' + * announcement threw. */ +void scr_emitter_unadd_quiet(ScrEmitter *em, ScrStr *name, ScrClosure *cb); ScrEmitter *scr_emitter_on(ScrEmitter *em, ScrStr *name, ScrClosure *cb /*moves*/, ScrEeInvoke inv, bool once, bool prepend); /* The dyn-adapted registration family (JS-lane checked-dynamic diff --git a/packages/runtime/src/scr_events_once_signal.c b/packages/runtime/src/scr_events_once_signal.c new file mode 100644 index 000000000..c3c2db37d --- /dev/null +++ b/packages/runtime/src/scr_events_once_signal.c @@ -0,0 +1,303 @@ +/* events.once(emitter, name, { signal }) over typed listeners — Node 24's + * lib/events.js `once` with an options object. Linked only by programs + * that pass the options (the emitter and event-target units together). + * + * The signal is validated first: undefined is the plain form, a value that + * is not an AbortSignal rejects with ERR_INVALID_ARG_TYPE, and an aborted + * signal rejects with an AbortError whose cause is signal.reason, all + * without registering a listener. Otherwise the event's one-shot listener, + * the 'error' one (unless the event is 'error') and a once, + * resist-stop-propagation 'abort' listener on the signal share the state + * below. The event or the error removes the other emitter listener and the + * abort listener, then settles; the abort removes both emitter listeners + * and rejects with the AbortError. + * + * Ownership: the state owns the promise, the name and the signal. The + * emitter is weak in the state; the abort listener holds it strongly (the + * abort is the one settlement that does not come from the emitter itself, + * like the closure Node's abortListener keeps). The three listener closures + * reach the state through link objects; a link clears the state's pointer + * to its closure when that closure is freed, so removals never compare a + * stale identity. The cycle through the signal's listener ends when the + * promise settles or the signal aborts (or at the exit teardown of the + * signal's listeners). */ +#include "scr_runtime.h" + +#include + +typedef struct ScrEosState { + size_t rc; + ScrPromise *promise; /* owned */ + ScrEmitter *em; /* weak: alive whenever a listener fires */ + ScrStr *name; /* owned */ + ScrDyn *signal; /* owned; undefined without an abort listener */ + ScrClosure *event_cb; /* weak, NULL once freed */ + ScrClosure *error_cb; /* weak, NULL once freed or for 'error' */ + ScrDyn *listener; /* weak abort listener, NULL once freed */ + bool settled; +} ScrEosState; + +enum { SCR_EOS_EVENT, SCR_EOS_ERROR, SCR_EOS_ABORT }; + +typedef struct ScrEosLink { + size_t rc; + ScrEosState *st; /* owned */ + int slot; +} ScrEosLink; + +static void scr_eos_oom(void) { scr_trap("scriptc: out of memory in events.once\n"); } + +static void scr_eos_state_release(ScrEosState *st) { + if (--st->rc > 0) return; + scr_promise_release(st->promise); + scr_str_release(st->name); + scr_dyn_release(st->signal); + free(st); +} + +static void *scr_eos_link_retain(void *p) { + ((ScrEosLink *)p)->rc++; + return p; +} + +static void scr_eos_link_release(void *p) { + ScrEosLink *link = p; + if (--link->rc > 0) return; + ScrEosState *st = link->st; + if (link->slot == SCR_EOS_EVENT) st->event_cb = NULL; + else if (link->slot == SCR_EOS_ERROR) st->error_cb = NULL; + else st->listener = NULL; + scr_eos_state_release(st); + free(link); +} + +static ScrBox *scr_eos_link_box(ScrEosState *st, int slot) { + ScrEosLink *link = calloc(1, sizeof *link); + if (!link) scr_eos_oom(); + link->rc = 1; + link->st = st; + link->slot = slot; + st->rc++; + ScrBox *box = scr_box_new_obj(&scr_eos_link_retain, &scr_eos_link_release, NULL); + scr_box_set_ref(box, link); + return box; +} + +/* An emitter listener: caps[0] its boxer (the compiled adapter calls it), + * caps[1] its link. */ +static ScrClosure *scr_eos_closure(void *adapter, ScrClosure *boxer, ScrEosState *st, int slot) { + ScrClosure *c = scr_closure_new(adapter, 2); + ScrBox *bx = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(bx, scr_closure_retain(boxer)); + c->caps[0] = bx; + c->caps[1] = scr_eos_link_box(st, slot); + return c; +} + +/* new AbortError(undefined, { cause: signal.reason }), thrown. */ +static void scr_eos_throw_abort(ScrDyn *signal) { + ScrStr *message = scr_str_new("The operation was aborted", 25); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_str_release(e->name); + e->name = scr_str_new("AbortError", 10); + /* AbortError assigns code, then name: own enumerable properties */ + scr_error_set_code(e, "ABORT_ERR"); + e->name_present = true; + e->name_enumerable = true; + ScrStr *k = scr_str_new("reason", 6); + ScrDyn *reason = scr_dyn_handle_key_get(signal, k); + scr_str_release(k); + scr_error_define_cause(e, reason ? reason : scr_dyn_undefined()); + if (reason) scr_dyn_release(reason); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +/* removeEventListener('abort', listener) on the signal, when the listener + * is still registered. */ +static void scr_eos_remove_abort(ScrEosState *st) { + if (st->listener == NULL) return; + bool aborted; + ScrEventTargetCore *core = scr_et_signal_core_fn ? scr_et_signal_core_fn(st->signal, &aborted) : NULL; + if (!core) return; + ScrStr *abort = scr_str_new("abort", 5); + ScrDyn *type = scr_dyn_new_str(abort); + scr_str_release(abort); + ScrDyn *listener = scr_dyn_retain(st->listener); + ScrDyn *remove_args[2] = {type, listener}; + ScrDyn *r = scr_et_remove_listener(core, remove_args, 2); + if (r) scr_dyn_release(r); + scr_dyn_release(type); + scr_dyn_release(listener); +} + +/* emitter.removeListener(name, cb) for a listener closure still alive. */ +static bool scr_eos_off(ScrEmitter *em, ScrStr *name, ScrClosure *cb) { + if (cb == NULL) return true; + scr_emitter_release(scr_emitter_off(em, name, cb)); + return !scr_exc_pending(); +} + +/* The compiled adapter's tail: `cb` is the firing emitter listener, `args` + * the boxed event arguments (moves in). */ +void scr_events_once_signal_fire(ScrClosure *cb, ScrArr *args) { + ScrEosLink *link = scr_box_get_ref(cb->caps[1]); + ScrEosState *st = link->st; + if (!st->settled) { + st->settled = true; + bool error = link->slot == SCR_EOS_ERROR; + /* Node's errorListener removes the event's listener and resolver the + * error's, each through removeListener; then the abort listener. */ + bool ok; + if (error) { + ok = scr_eos_off(st->em, st->name, st->event_cb); + } else { + ScrStr *error_name = scr_str_new("error", 5); + ok = scr_eos_off(st->em, error_name, st->error_cb); + scr_str_release(error_name); + } + if (ok) { + scr_eos_remove_abort(st); + if (error) { + ScrDyn *reason = (ScrDyn *)scr_arr_get_ref(args, 0); + scr_dyn_throw(reason); /* moves reason */ + scr_promise_reject_pending(st->promise); + } else { + scr_promise_fulfill_ref(st->promise, scr_arr_retain(args), scr_arr_retain_v, + scr_arr_release_v, args->cyc ? scr_arr_trace_v : NULL); + } + } + } + scr_arr_release(args); + scr_eos_link_release(link); +} + +/* The signal's 'abort' listener: caps[0] its link, caps[1] the emitter. */ +static ScrDyn *scr_eos_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrEosLink *link = scr_box_get_ref(clo->caps[0]); + ScrEmitter *em = scr_box_get_ref(clo->caps[1]); + ScrEosState *st = link->st; + bool ok = true; + if (!st->settled) { + st->settled = true; + ok = scr_eos_off(em, st->name, st->event_cb); + if (ok) { + ScrStr *error_name = scr_str_new("error", 5); + ok = scr_eos_off(em, error_name, st->error_cb); + scr_str_release(error_name); + } + if (ok) { + scr_eos_throw_abort(st->signal); + scr_promise_reject_pending(st->promise); + } + } + scr_emitter_release(em); + scr_eos_link_release(link); + return ok ? scr_dyn_retain(scr_dyn_undefined()) : NULL; +} + +static ScrBox *scr_eos_promise_box(ScrPromise *p) { + ScrBox *box = scr_box_new_obj(&scr_promise_retain_v, &scr_promise_release_v, &scr_promise_trace_v); + scr_box_set_ref(box, scr_promise_retain(p)); + return box; +} + +static void scr_eos_adopt_job(ScrClosure *job) { + ScrPromise *outer = scr_box_get_ref(job->caps[0]); + ScrPromise *inner = scr_box_get_ref(job->caps[1]); + scr_promise_race_join(outer, inner, &scr_promise_adapt_copy); + scr_promise_release(inner); + scr_promise_release(outer); +} + +/* Node's once is an async function returning the executor's promise: its + * result adopts that promise through a thenable job queued now, whose + * reaction settles the result one turn after the inner promise settles. + * Moves `inner` in; the result is +1. */ +static ScrPromise *scr_eos_adopt(ScrPromise *inner) { + ScrPromise *outer = scr_promise_new(); + ScrClosure *job = scr_closure_new((void *)&scr_eos_adopt_job, 2); + job->caps[0] = scr_eos_promise_box(outer); + job->caps[1] = scr_eos_promise_box(inner); + scr_promise_release(inner); + scr_queue_microtask(job); + return outer; +} + +/* boxer and err_boxer are borrowed (err_boxer is NULL when the awaited + * event is 'error'); signal is borrowed. The result is +1 and never throws: + * every failure rejects it, as Node's async function does. */ +ScrPromise *scr_events_once_signal(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, void *adapter, + ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter, + ScrEeInvoke err_inv, ScrDyn *signal) { + ScrPromise *result = scr_promise_new(); + bool with_signal = signal->kind != SCR_DYN_UNDEF; + if (with_signal) { + bool aborted = false; + ScrEventTargetCore *core = + scr_et_signal_core_fn ? scr_et_signal_core_fn(signal, &aborted) : NULL; + if (!core) { + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal); + scr_promise_reject_pending(result); + return result; + } + if (aborted) { + scr_eos_throw_abort(signal); + scr_promise_reject_pending(result); + return result; + } + } + ScrEosState *st = calloc(1, sizeof *st); + if (!st) scr_eos_oom(); + st->rc = 1; + st->promise = result; + st->em = em; + st->name = scr_str_retain(name); + st->signal = scr_dyn_retain(signal); + ScrClosure *event_cb = scr_eos_closure(adapter, boxer, st, SCR_EOS_EVENT); + ScrClosure *error_cb = + err_boxer != NULL ? scr_eos_closure(err_adapter, err_boxer, st, SCR_EOS_ERROR) : NULL; + st->event_cb = event_cb; + st->error_cb = error_cb; + /* Node registers inside the promise's executor: a throwing 'newListener' + * listener rejects the promise, and the listener it announced is never + * added (the event's listener stays when only the 'error' announcement + * throws). No abort listener follows a throw. */ + ScrCaught *thrown = NULL; + scr_emitter_release(scr_emitter_on(em, name, event_cb, inv, true, false)); + if (scr_exc_pending()) { + thrown = scr_exc_take(); + scr_emitter_unadd_quiet(em, name, event_cb); + if (error_cb != NULL) scr_closure_release(error_cb); /* never registered */ + } else if (error_cb != NULL) { + ScrStr *error = scr_str_new("error", 5); + scr_emitter_release(scr_emitter_on(em, error, error_cb, err_inv, true, false)); + if (scr_exc_pending()) { + thrown = scr_exc_take(); + scr_emitter_unadd_quiet(em, error, error_cb); + } + scr_str_release(error); + } + if (thrown != NULL) { + st->settled = true; + scr_rethrow(thrown); + scr_caught_release(thrown); + scr_promise_reject_pending(st->promise); + } else if (with_signal) { + ScrClosure *clo = scr_closure_new((void *)&scr_eos_abort_thunk, 2); + clo->caps[0] = scr_eos_link_box(st, SCR_EOS_ABORT); + clo->caps[1] = scr_box_new_obj(&scr_emitter_retain_v, &scr_emitter_release_v, NULL); + scr_box_set_ref(clo->caps[1], scr_emitter_retain(em)); + ScrDyn *listener = scr_dyn_new_func(clo, &scr_eos_abort_thunk, 0, "()", "abortListener"); + st->listener = listener; + ScrDyn *dispose = scr_events_add_abort_listener(signal, listener); + if (dispose) scr_dyn_release(dispose); + /* The signal's listener entry keeps it; the state's pointer is weak. */ + scr_dyn_release(listener); + } + ScrPromise *inner = scr_promise_retain(st->promise); + scr_eos_state_release(st); + return scr_eos_adopt(inner); +} From 2429eaf5568584032c5de7082da01d0e0c068761 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 08:25:46 -0700 Subject: [PATCH 05/22] Admit Promise race entries (the race part of main's 465abda81) The Promise.race portion of 465abda81 on main, applied unchanged so the race changes that follow build on it and merge with main cleanly: a Promise entry can only reject, so it races under any result type and its fulfillment adapter traps; a Promise executor's one-parameter resolve validates against the void inner. Includes that commit's corpus program 6450. Co-Authored-By: Claude Opus 5.5 --- .../src/backend/llvm/expr-callbacks.ts | 17 ++++++++++++++ .../frontend/lowering/builtins/promises.ts | 13 ++++++++++- packages/compiler/src/ir/validate.ts | 5 ++++- .../6450-promise-race-never-entry.ts.json | 6 +++++ tests/corpus/6450-promise-race-never-entry.ts | 22 +++++++++++++++++++ 5 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-promise-race-never-entry.ts.json create mode 100644 tests/corpus/6450-promise-race-never-entry.ts diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index e805760d8..2e62d1731 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -55,6 +55,23 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp if (sym) return sym; sym = `sc_race_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : []; + if (from.kind === "void" && !toArms.some((a) => a.kind === "undefinedT")) { + // A Promise entry (the frontend admits void entries without an + // undefined result arm only for those): it can only reject, so its + // fulfillment adapter never runs. + host.declare(`declare void @scr_trap(ptr)`); + const msg = host.cstr("scriptc: a Promise race entry fulfilled\n"); + host.resolveThunkDefs.push( + `define internal void @${sym}(ptr %dst, ptr %src) ${FN_ATTRS} { ; race ${key}`, + `entry:`, + ` call void @scr_trap(ptr ${msg})`, + ` unreachable`, + `}`, + ``, + ); + return sym; + } if (to.kind !== "union") throw new InternalCompilerError("llvm emitter bug: race adapter to a non-union"); const toDef = host.unionsById.get(to.unionId); diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 48106d082..03e9c7497 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -74,6 +74,17 @@ export function lowerPromiseStaticCall( } return resultArms.some((a) => typeEquals(a, inner)); }; + // A Promise entry (the deadline idiom: `new Promise((_, + // reject) => setTimeout(reject, ms))`) can only reject, so it races + // under any result type; its inner type maps to void. + const neverEntry = (el: ts.Expression): boolean => { + const t = lowerer.typeOf(el); + const arg = + t.getSymbol()?.name === "Promise" + ? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0] + : undefined; + return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0; + }; const entries = argNode.elements.nodes.map((el) => { const entry = lowerer.lowerExpr(el); if (entry.type.kind !== "promise") { @@ -83,7 +94,7 @@ export function lowerPromiseStaticCall( "JS would resolve plain values — wrap them: new Promise((r) => r(v))", ); } - if (!compatible(entry.type.inner)) { + if (!compatible(entry.type.inner) && !(entry.type.inner.kind === "void" && neverEntry(el))) { lowerer.unsupported( "SC1090", el, diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index e652eeb52..b46cf33de 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -6229,7 +6229,10 @@ function validateFunction( if (expectsArg && (rp.params.length !== 1 || !typeEquals(rp.params[0]!, inner))) { err(`newPromise resolve param must be (${inner.kind}) => void`, e.loc); } - if (!expectsArg && rp.params.length !== 0) { + // A Promise executor types resolve as `(value: never) => + // void` (one parameter over a void inner): the void resolve never + // reads an argument, and a never value cannot be produced. + if (!expectsArg && rp.params.length > 1) { err("newPromise resolve takes no argument", e.loc); } } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-promise-race-never-entry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-promise-race-never-entry.ts.json new file mode 100644 index 000000000..bb709e7f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-promise-race-never-entry.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6450-promise-race-never-entry.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6450-promise-race-never-entry.ts b/tests/corpus/6450-promise-race-never-entry.ts new file mode 100644 index 000000000..412c5f817 --- /dev/null +++ b/tests/corpus/6450-promise-race-never-entry.ts @@ -0,0 +1,22 @@ +// Promise.race with a Promise deadline entry under a typed result: +// the never entry can only reject, so it races with any result type. A +// read that settles first wins; a deadline that fires first rejects. +class Deadline extends Error {} +async function readWithDeadline(read: () => Promise<{ done: boolean; value?: Uint8Array }>, ms: number): Promise<{ done: boolean; value?: Uint8Array }> { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + read(), + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Deadline()), ms); + }), + ]); + } catch (error) { + if (error instanceof Deadline) return { done: true }; + throw error; + } finally { + if (timer !== undefined) clearTimeout(timer); + } +} +readWithDeadline(async () => ({ done: false, value: new Uint8Array([1, 2]) }), 1000).then((r) => console.log(r.done, r.value?.length)); +readWithDeadline(() => new Promise((res) => setTimeout(() => res({ done: false }), 200)), 20).then((r) => console.log("deadline", r.done)); From 2630e8438ae738c000e4c86651003652ea4ea71a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 08:26:04 -0700 Subject: [PATCH 06/22] Combine Promise.race entries whose combined type has no representation When the checker's combined type has no representation (events.once's any[] arrays, or 'void | any[]'), the race result takes the one type the entries that can fulfill share, or 'unknown' when each of them widens into it; Promise entries do not take part. An entry whose payload widens into the result type converts through the checked promise payload widening, which keeps its settlement turn. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/builtins/promises.ts | 72 +++++++++++++------ 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 03e9c7497..aa701f53f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -23,7 +23,11 @@ import { * type is the checker's combined promise, and each entry's inner type * must be that inner type, one of its union arms, or a sub-union of it * (the backend's interned adapters wrap/re-tag fulfillments; a wider - * entry would need machinery that doesn't exist and fences). + * entry would need machinery that doesn't exist and fences). A + * Promise entry fits any result, and an entry whose payload widens + * into the result's converts. When the checker's combined type has no + * representation, the result takes the type the entries that can fulfill + * share, or 'unknown'. * Promise.all/allSettled/any fence with the sequential-await hint; * resolve/reject and the rest fall to the member fence. Null for * non-Promise receivers. */ @@ -50,14 +54,47 @@ export function lowerPromiseStaticCall( "a non-empty array LITERAL of promises is the supported form: Promise.race([p, q])", ); } - const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); - if (resultT?.kind !== "promise") { - lowerer.noLowering( - "Promise.race with this combined result type", - call, - "the entries' value types must combine into a representable union", - ); + // A Promise entry (the deadline idiom: `new Promise((_, + // reject) => setTimeout(reject, ms))`) can only reject, so it races + // under any result type; its inner type maps to void. + const neverEntry = (el: ts.Expression): boolean => { + const t = lowerer.typeOf(el); + const arg = + t.getSymbol()?.name === "Promise" + ? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0] + : undefined; + return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0; + }; + const elements = argNode.elements.nodes; + let combined = lowerer.mapTypeOf(lowerer.typeOf(call)); + let lowered: IrExpr[] | null = null; + if (combined?.kind !== "promise") { + // The checker's combined type has no representation (an 'any' + // element array such as events.once's any[], or 'void | any[]'): the + // result takes the one type the entries that can fulfill share, or + // else 'unknown' when each of them widens into it. + lowered = elements.map((el) => lowerer.lowerExpr(el)); + const fulfilling = lowered.filter((_, i) => !neverEntry(elements[i]!)); + const first = fulfilling[0]?.type; + const dynResult: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; + if (first?.kind === "promise" && fulfilling.every((e) => typeEquals(e.type, first))) { + combined = first; + } else if ( + fulfilling.length > 0 && + fulfilling.every( + (e) => e.type.kind === "promise" && lowerer.promisePayloadWidens(e.type, dynResult), + ) + ) { + combined = dynResult; + } else { + lowerer.noLowering( + "Promise.race with this combined result type", + call, + "the entries' value types must combine into a representable union", + ); + } } + const resultT = combined; const resultArms = resultT.inner.kind === "union" ? (lowerer.unions.get(resultT.inner.unionId)?.arms ?? []) @@ -74,19 +111,8 @@ export function lowerPromiseStaticCall( } return resultArms.some((a) => typeEquals(a, inner)); }; - // A Promise entry (the deadline idiom: `new Promise((_, - // reject) => setTimeout(reject, ms))`) can only reject, so it races - // under any result type; its inner type maps to void. - const neverEntry = (el: ts.Expression): boolean => { - const t = lowerer.typeOf(el); - const arg = - t.getSymbol()?.name === "Promise" - ? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0] - : undefined; - return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0; - }; - const entries = argNode.elements.nodes.map((el) => { - const entry = lowerer.lowerExpr(el); + const entries = elements.map((el, i) => { + const entry = lowered?.[i] ?? lowerer.lowerExpr(el); if (entry.type.kind !== "promise") { lowerer.noLowering( "Promise.race over non-promise entries", @@ -95,6 +121,10 @@ export function lowerPromiseStaticCall( ); } if (!compatible(entry.type.inner) && !(entry.type.inner.kind === "void" && neverEntry(el))) { + // A payload that widens into the result's (into 'unknown', or a + // checked conversion) keeps its promise and settlement turn. + if (lowerer.promisePayloadWidens(entry.type, resultT)) + return lowerer.coerceToExpected(entry, resultT); lowerer.unsupported( "SC1090", el, From 09f427c4fe2e1e6e7401220139366d613f6f9f9c Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 08:25:00 -0700 Subject: [PATCH 07/22] Accept Function-typed completion callbacks in stream underscore methods A stream subclass that declares `_transform(chunk, encoding, cb: Function)` (or _write, _final, _destroy, _flush with a broad `Function` callback) was rejected because 'Function' has no lowering, which also blocked every construction of the class. The callback parameter now takes the checked-dynamic callable the runtime already passes implicitly typed callbacks, and calls through it are dynamic calls. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 19 +++++++++++++++++++ .../src/frontend/lowering/lower-stream.ts | 10 ++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index a31a62e61..1d6b80535 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -112,6 +112,7 @@ import { import { STREAM_API_MEMBERS, STREAM_PROP_MEMBERS, + STREAM_COMPLETION_CALLBACK_INDEX, UNDERSCORE_METHODS, lowerStreamNew, lowerStreamSuperCall, @@ -2690,6 +2691,24 @@ export function collectClassShapeInner( ); } } + // A completion callback declared with the broad `Function` type + // (`cb: Function`) takes the checked-dynamic callable the runtime + // passes implicitly typed callbacks; calls through it are + // dynamic calls of that callable. + const callbackIndex = STREAM_COMPLETION_CALLBACK_INDEX.get(name); + const callbackParam = + ts.isMethodDeclaration(member) && callbackIndex !== undefined + ? member.parameters.nodes[callbackIndex] + : undefined; + if ( + callbackParam !== undefined && + ts.isIdentifier(callbackParam.name) && + !callbackParam.dotDotDotToken && + !callbackParam.questionToken && + !callbackParam.initializer && + lowerer.checker.typeToString(lowerer.typeOf(callbackParam.name)) === "Function" + ) + lowerer.checkedCallbackParams.add(callbackParam); } } if (ts.isPropertyDeclaration(member)) { diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 53e2927d5..f31862f07 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -171,6 +171,16 @@ export const UNDERSCORE_METHODS: ReadonlyMap = new Map([ ["flush", "_flush"], ]); +/** The completion callback's parameter position in each underscore + * method that takes one (cbTuples' "fn" positions). */ +export const STREAM_COMPLETION_CALLBACK_INDEX: ReadonlyMap = new Map([ + ["_write", 2], + ["_final", 0], + ["_destroy", 1], + ["_transform", 2], + ["_flush", 0], +]); + /** The accepted option-callback names (canonical flag order) and the * duplex-shaped-head answer for one runtime stream class. */ export function streamCtorShape(cls: string): { From 67ace26573d8e88740b0bd608cb4518b151b2cf3 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 08:37:28 -0700 Subject: [PATCH 08/22] Unbox native streams for events.once over checked-dynamic stream slots Under checked-dynamic NodeJS stream slots, events.once over a NodeJS.ReadableStream or NodeJS.WritableStream slot unboxes a native stream by identity and takes the stream path; any other value throws the not-supported Error that for await and pipeline stages already throw for JavaScript stream objects. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/lower-event-emitter.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index 4f08ecc0d..f2968cebd 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -73,6 +73,7 @@ import { boolLit, strLit, varRef } from "../../ir/build.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerAddAbortListener } from "./builtin-calls.js"; import { requestGeneralStreamSurface } from "./general-streams.js"; +import { nodeStreamInterfaceSide } from "./dyn-streams.js"; /** True when the class descends from (or is) the runtime emitter. */ export function emitterRooted(lowerer: Lowerer, info: ClassInfo | undefined | null): boolean { @@ -2257,7 +2258,28 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: // Node's Promise: the arguments array of checked-dynamic values. const arrayT = arrayOf(DYN); const resultT: IrType = { kind: "promise", inner: arrayT }; - const receiver = lowerer.lowerExpr(args[0]!); + let receiver = lowerer.lowerExpr(args[0]!); + // A checked-dynamic NodeJS stream slot (dyn-streams.ts): a native stream + // unboxes by identity; any other value throws the not-supported Error. + if ( + receiver.type.kind === "dyn" && + lowerer.dynNodeStreams && + nodeStreamInterfaceSide(lowerer, lowerer.typeOf(args[0]!)) !== null + ) { + receiver = { + kind: "libCall", + fn: "stream.unboxDyn", + args: [ + receiver, + strLit( + "events.once over stream objects other than native streams is not supported yet", + loc, + ), + ], + type: { kind: "object", className: "%Duplex" }, + loc, + }; + } const info = receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined; if (!emitterRooted(lowerer, info)) { From e865594089ea9da215b0b2c9f0b48d5e4957f0b0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 08:47:59 -0700 Subject: [PATCH 09/22] Accept NodeJS stream interfaces in the fallback events.once and document its boundary The fallback declaration of events.once takes NodeJS.ReadableStream and NodeJS.WritableStream emitters like @types/node does. The diagnostics fixture now pins the forms that stay refused (options that are not an object literal, a computed name, a stream's 'data' event), and the limitations page states the supported events.once forms. Co-Authored-By: Claude Opus 5.5 --- .../limitations/builtin-apis/eventemitter.mdx | 1 + .../ambient/scriptc-node-fallback.d.ts | 2 +- tests/diagnostics/events-once-fences.ts | 9 ++++-- .../__snapshots__/events-once-fences.ts.txt | 31 ++++++++++++------- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/docs/content/limitations/builtin-apis/eventemitter.mdx b/docs/content/limitations/builtin-apis/eventemitter.mdx index 4f7142094..a9f03b857 100644 --- a/docs/content/limitations/builtin-apis/eventemitter.mdx +++ b/docs/content/limitations/builtin-apis/eventemitter.mdx @@ -1,4 +1,5 @@ - **EventEmitter:** Literal event names use fixed argument tuples. Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion. Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported. + `events.once` accepts EventEmitter and stream instances with a literal event name and an optional object-literal `{ signal }`; a stream's `data` event, other options values, and EventTarget emitters are unsupported. Reading `defaultMaxListeners` through a CommonJS destructured or member binding (`const { defaultMaxListeners } = require("events")`) is unsupported. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 3885e0fef..2886a730a 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -6443,7 +6443,7 @@ declare module "events" { export { internal as EventEmitter }; // The event's arguments, as checked-dynamic values. export function once( - emitter: EventEmitter, + emitter: EventEmitter | NodeJS.ReadableStream | NodeJS.WritableStream, eventName: string, options?: { signal?: AbortSignal }, ): Promise; diff --git a/tests/diagnostics/events-once-fences.ts b/tests/diagnostics/events-once-fences.ts index 2e5613f70..97bc76254 100644 --- a/tests/diagnostics/events-once-fences.ts +++ b/tests/diagnostics/events-once-fences.ts @@ -1,11 +1,14 @@ -// events.once lowers for EventEmitter instances and literal event names; -// these forms stay refused. +// events.once lowers for EventEmitter and stream instances, literal event +// names and a literal { signal } options object; these forms stay refused. import { EventEmitter, once } from "node:events"; +import { PassThrough } from "node:stream"; const ee = new EventEmitter(); const name: string = process.argv[2] ?? "ready"; +const options = {}; async function main(): Promise { - await once(ee, "ready", { signal: AbortSignal.timeout(5) }); // options + await once(ee, "ready", options); // options that are not an object literal await once(ee, name); // computed event name + await once(new PassThrough(), "data"); // a stream's 'data' event } void main(); diff --git a/tests/harness/__snapshots__/events-once-fences.ts.txt b/tests/harness/__snapshots__/events-once-fences.ts.txt index ffab538b0..60c499cfe 100644 --- a/tests/harness/__snapshots__/events-once-fences.ts.txt +++ b/tests/harness/__snapshots__/events-once-fences.ts.txt @@ -1,17 +1,26 @@ -events-once-fences.ts:8:27 - error SC2020: 'events.once with options' is part of the standard library types but has no scriptc lowering yet +events-once-fences.ts:10:27 - error SC2020: 'events.once options that are not an object literal' is part of the standard library types but has no scriptc lowering yet - 7 | async function main(): Promise { - 8 | await once(ee, "ready", { signal: AbortSignal.timeout(5) }); // options - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 | await once(ee, name); // computed event name + 9 | async function main(): Promise { + 10 | await once(ee, "ready", options); // options that are not an object literal + | ^~~~~~~ + 11 | await once(ee, name); // computed event name - hint: the lowered form is events.once(emitter, name); AbortSignal options are not supported yet + hint: pass the options as an object literal: { signal } -events-once-fences.ts:9:18 - error SC2020: 'events.once with a computed or meta event name' is part of the standard library types but has no scriptc lowering yet +events-once-fences.ts:11:18 - error SC2020: 'events.once with a computed or meta event name' is part of the standard library types but has no scriptc lowering yet - 8 | await once(ee, "ready", { signal: AbortSignal.timeout(5) }); // options - 9 | await once(ee, name); // computed event name + 10 | await once(ee, "ready", options); // options that are not an object literal + 11 | await once(ee, name); // computed event name | ^~~~ - 10 | } + 12 | await once(new PassThrough(), "data"); // a stream's 'data' event - hint: name the awaited event with a string literal \ No newline at end of file + hint: name the awaited event with a string literal + +events-once-fences.ts:12:33 - error SC2020: 'events.once of a stream's 'data' event' is part of the standard library types but has no scriptc lowering yet + + 11 | await once(ee, name); // computed event name + 12 | await once(new PassThrough(), "data"); // a stream's 'data' event + | ^~~~~~ + 13 | } + + hint: the chunk has no checked-dynamic boxing here; register stream.once('data', listener) instead \ No newline at end of file From d883c84752e0911d0e599273630b8acd04d02f45 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 08:48:00 -0700 Subject: [PATCH 10/22] Add corpus programs for events.once with a signal, over streams, and in races 6490 covers events.once with an AbortSignal: abort, an aborted signal, an undefined signal, abort-listener order, 'removeListener' events, a throwing 'newListener' listener and the settlement turns. 6491 covers stream receivers (file streams, Readable.from, a destroyed PassThrough, a Writable) and a NodeJS.ReadableStream part raced against its destination. 6492 covers races over events.once results with entries that only reject and entries of different value types. 6493 covers stream subclasses whose completion callbacks are typed `Function`. 6494 covers events.once over checked-dynamic stream slots. The node-types fixture runs the race shapes under @types/node, where events.once returns Promise. Co-Authored-By: Claude Opus 5.5 --- .../corpus/6490-events-once-signal.ts.json | 6 + .../corpus/6491-events-once-streams.ts.json | 6 + .../corpus/6492-promise-race-combined.ts.json | 6 + .../6493-stream-function-callbacks.ts.json | 6 + .../main.ts.json | 7 + .../events-once-signal-race.ts.json | 6 + tests/corpus/6490-events-once-signal.ts | 120 ++++++++++++++++++ tests/corpus/6491-events-once-streams.ts | 87 +++++++++++++ tests/corpus/6492-promise-race-combined.ts | 72 +++++++++++ .../corpus/6493-stream-function-callbacks.ts | 66 ++++++++++ .../6494-events-once-dyn-stream-slot/main.ts | 59 +++++++++ .../sources.js | 21 +++ .../node-types/events-once-signal-race.ts | 86 +++++++++++++ tests/harness/project-config.test.ts | 18 +++ 14 files changed, 566 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-events-once-signal.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-events-once-streams.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6492-promise-race-combined.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6493-stream-function-callbacks.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-events-once-dyn-stream-slot/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/events-once-signal-race.ts.json create mode 100644 tests/corpus/6490-events-once-signal.ts create mode 100644 tests/corpus/6491-events-once-streams.ts create mode 100644 tests/corpus/6492-promise-race-combined.ts create mode 100644 tests/corpus/6493-stream-function-callbacks.ts create mode 100644 tests/corpus/6494-events-once-dyn-stream-slot/main.ts create mode 100644 tests/corpus/6494-events-once-dyn-stream-slot/sources.js create mode 100644 tests/fixtures/node-types/events-once-signal-race.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-events-once-signal.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-events-once-signal.ts.json new file mode 100644 index 000000000..9c99e2ab0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-events-once-signal.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6490-events-once-signal.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-events-once-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-events-once-streams.ts.json new file mode 100644 index 000000000..4fdbe7a1d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-events-once-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6491-events-once-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6492-promise-race-combined.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6492-promise-race-combined.ts.json new file mode 100644 index 000000000..8fa2ceb9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6492-promise-race-combined.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6492-promise-race-combined.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6493-stream-function-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6493-stream-function-callbacks.ts.json new file mode 100644 index 000000000..bb755b9a7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6493-stream-function-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6493-stream-function-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-events-once-dyn-stream-slot/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-events-once-dyn-stream-slot/main.ts.json new file mode 100644 index 000000000..77f43443e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-events-once-dyn-stream-slot/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6494-events-once-dyn-stream-slot/sources.js", + "/tests/corpus/6494-events-once-dyn-stream-slot/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/events-once-signal-race.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/events-once-signal-race.ts.json new file mode 100644 index 000000000..e5c444b78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/events-once-signal-race.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/events-once-signal-race.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6490-events-once-signal.ts b/tests/corpus/6490-events-once-signal.ts new file mode 100644 index 000000000..8535e1fec --- /dev/null +++ b/tests/corpus/6490-events-once-signal.ts @@ -0,0 +1,120 @@ +// events.once(emitter, name, { signal }) follows Node's lib/events.js: an +// abort removes the event's and the 'error' listener and rejects with an +// AbortError whose cause is signal.reason, an aborted signal rejects +// without registering anything, and a settled once removes its abort +// listener. The result settles on Node's microtask turn: once is an async +// function returning the executor's promise. +import { EventEmitter, once } from "node:events"; + +async function main(): Promise { + const ee = new EventEmitter(); + const ac = new AbortController(); + setTimeout(() => ee.emit("ready", 1, "two"), 1); + const args = await once(ee, "ready", { signal: ac.signal }); + console.log("fulfilled", JSON.stringify(args), ee.listenerCount("ready"), ee.listenerCount("error")); + + const ac2 = new AbortController(); + const pending = once(ee, "ready", { signal: ac2.signal }); + console.log("before abort", ee.listenerCount("ready"), ee.listenerCount("error")); + ac2.abort(); + console.log("after abort", ee.listenerCount("ready"), ee.listenerCount("error")); + try { + await pending; + } catch (err) { + const e = err as NodeJS.ErrnoException; + console.log("rejected", e.name, e.message, e.code, (e.cause as Error).name); + } + + const ac3 = new AbortController(); + ac3.abort(new Error("already")); + try { + await once(ee, "ready", { signal: ac3.signal }); + } catch (err) { + const e = err as Error; + console.log("pre-aborted", e.name, (e.cause as Error).message, ee.listenerCount("ready")); + } + + const ac4 = new AbortController(); + setTimeout(() => ee.emit("error", new Error("boom")), 1); + try { + await once(ee, "ready", { signal: ac4.signal }); + } catch (err) { + console.log("error event", (err as Error).message, ee.listenerCount("ready")); + } + ac4.abort(); + console.log("late abort changes nothing"); + + const errs = once(ee, "error", { signal: ac.signal }); + ee.emit("error", new Error("as value")); + const [first] = await errs; + console.log("error as value", (first as Error).message); + + // An undefined signal is the plain form. + const maybe: AbortSignal | undefined = undefined; + setTimeout(() => ee.emit("ready", 3, "four"), 1); + console.log("undefined signal", JSON.stringify(await once(ee, "ready", { signal: maybe }))); + + // Listeners registered before and after the once's abort listener. + const ac5 = new AbortController(); + const ee5 = new EventEmitter(); + ac5.signal.addEventListener("abort", () => { + console.log("first abort listener", ee5.listenerCount("z")); + }); + const p5 = once(ee5, "z", { signal: ac5.signal }); + ac5.signal.addEventListener("abort", () => { + console.log("second abort listener", ee5.listenerCount("z")); + }); + ac5.abort("why"); + await p5.catch((err: Error) => console.log("p5", err.name, String(err.cause))); + + // The abort's removals emit 'removeListener'. + const ee6 = new EventEmitter(); + ee6.on("removeListener", (name: string) => console.log("removed", name)); + const ac6 = new AbortController(); + const p6 = once(ee6, "w", { signal: ac6.signal }); + ac6.abort(); + await p6.catch(() => console.log("p6 rejected")); + + // A 'newListener' listener that throws rejects the promise. + const ee7 = new EventEmitter(); + ee7.on("newListener", (name: string) => { + if (name === "go") throw new Error("no " + name); + }); + await once(ee7, "go", { signal: new AbortController().signal }).then( + () => console.log("fulfilled"), + (err: Error) => console.log("newListener rejected", err.message, ee7.listenerCount("go")), + ); + + // Settlement turns against plain promise chains. + const ee8 = new EventEmitter(); + const ac8 = new AbortController(); + void once(ee8, "x").then(() => console.log("plain")); + void once(ee8, "x", { signal: ac8.signal }).then(() => console.log("signal")); + void Promise.resolve() + .then(() => console.log("t1")) + .then(() => console.log("t2")) + .then(() => console.log("t3")) + .then(() => console.log("t4")); + ee8.emit("x"); + const ac9 = new AbortController(); + void once(ee8, "y", { signal: ac9.signal }).then( + () => console.log("fulfilled"), + () => console.log("aborted"), + ); + ac9.abort(); + const ac10 = new AbortController(); + ac10.abort(); + void once(ee8, "y", { signal: ac10.signal }).then( + () => console.log("fulfilled"), + () => console.log("pre-aborted turn"), + ); + await new Promise((resolve) => setTimeout(resolve, 5)); + + // Pending onces whose signal never aborts keep their listeners. + const idle = new AbortController(); + void once(ee, "never", { signal: idle.signal }); + void once(new EventEmitter(), "never", { signal: idle.signal }); + console.log("pending", ee.listenerCount("never"), ee.listenerCount("error")); +} + +void main(); diff --git a/tests/corpus/6491-events-once-streams.ts b/tests/corpus/6491-events-once-streams.ts new file mode 100644 index 000000000..efe60c737 --- /dev/null +++ b/tests/corpus/6491-events-once-streams.ts @@ -0,0 +1,87 @@ +// events.once over streams: file streams, Readable.from, a PassThrough +// destroyed with an error, a Writable, and a NodeJS.ReadableStream slot +// raced against its destination's 'close' with an AbortSignal that the +// finally block aborts. +import { once } from "node:events"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { PassThrough, Readable, Writable } from "node:stream"; + +function copyPart(src: NodeJS.ReadableStream, out: PassThrough): Promise { + const ac = new AbortController(); + src.pipe(out, { end: false }); + return Promise.race([ + once(src, "end", { signal: ac.signal }), + once(src, "error", { signal: ac.signal }).then(([error]) => { + throw error; + }), + once(out, "close", { signal: ac.signal }).then(() => { + throw new Error("output closed before the part ended"); + }), + ]).finally(() => { + ac.abort(); + src.unpipe(out); + console.log("part done", out.listenerCount("close"), out.listenerCount("error")); + }); +} + +async function main(): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-once-streams-")); + const file = path.join(dir, "a.txt"); + fs.writeFileSync(file, "part one\n"); + + const rs = fs.createReadStream(file); + rs.resume(); + console.log("file end", JSON.stringify(await once(rs, "end"))); + const ws = fs.createWriteStream(path.join(dir, "b.txt")); + ws.end("x"); + await once(ws, "finish"); + console.log("write stream finish"); + await once(ws, "close"); + console.log("write stream close"); + + const pt = new PassThrough(); + setTimeout(() => pt.destroy(new Error("pt broke")), 1); + try { + await once(pt, "end"); + } catch (err) { + console.log("passthrough error", (err as Error).message, pt.listenerCount("end")); + } + const r = Readable.from(["a", "b"]); + r.resume(); + await once(r, "close"); + console.log("readable close"); + const w = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + w.end("z"); + await once(w, "finish"); + console.log("writable finish"); + + const out = new PassThrough(); + let text = ""; + out.on("data", (chunk: Buffer) => { + text += chunk.toString(); + }); + await copyPart(fs.createReadStream(file), out); + await copyPart(fs.createReadStream(file), out); + console.log(JSON.stringify(text)); + try { + await copyPart(fs.createReadStream(path.join(dir, "missing.txt")), out); + } catch (err) { + console.log("missing", (err as NodeJS.ErrnoException).code); + } + const closing = new PassThrough(); + setTimeout(() => closing.destroy(), 1); + try { + await copyPart(new Readable({ read() {} }), closing); + } catch (err) { + console.log("closed", (err as Error).message); + } + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/corpus/6492-promise-race-combined.ts b/tests/corpus/6492-promise-race-combined.ts new file mode 100644 index 000000000..70f5399af --- /dev/null +++ b/tests/corpus/6492-promise-race-combined.ts @@ -0,0 +1,72 @@ +// Promise.race over events.once results: entries that only reject (a +// handler that throws) fit any result, and entries of different value +// types combine into 'unknown'. +import { EventEmitter, once } from "node:events"; + +function waitFor(a: EventEmitter, b: EventEmitter) { + let failed = false; + return Promise.race([ + once(a, "done"), + once(a, "error").then(([error]) => { + failed = true; + throw error; + }), + once(b, "close").then(() => { + failed = true; + throw new Error("closed early"); + }), + ]).finally(() => { + console.log("settled", failed); + }); +} + +function waitVoid(a: EventEmitter): Promise { + return Promise.race([ + once(a, "done").then((): void => undefined), + once(a, "error").then(([error]) => { + throw error; + }), + ]); +} + +function waitMixed(a: EventEmitter): Promise { + return Promise.race([once(a, "done").then((): void => undefined), once(a, "other")]); +} + +async function main(): Promise { + const a = new EventEmitter(); + const b = new EventEmitter(); + setTimeout(() => a.emit("done", 7), 1); + console.log("value", JSON.stringify(await waitFor(a, b))); + setTimeout(() => b.emit("close"), 1); + try { + await waitFor(a, b); + } catch (err) { + console.log("caught", (err as Error).message); + } + setTimeout(() => a.emit("error", new Error("bad")), 1); + try { + await waitFor(a, b); + } catch (err) { + console.log("caught", (err as Error).message); + } + setTimeout(() => a.emit("done", 8), 1); + await waitVoid(a); + console.log("void done"); + setTimeout(() => a.emit("other", 1, "x"), 1); + console.log("mixed", JSON.stringify(await waitMixed(a))); + setTimeout(() => a.emit("done", 9), 1); + console.log("mixed undefined", await waitMixed(a)); + // Turn order: the race settles one turn after its first entry. + const c = new EventEmitter(); + void waitMixed(c).then(() => console.log("race settled")); + void Promise.resolve() + .then(() => console.log("u1")) + .then(() => console.log("u2")) + .then(() => console.log("u3")) + .then(() => console.log("u4")) + .then(() => console.log("u5")); + c.emit("other", 2, "y"); +} + +void main(); diff --git a/tests/corpus/6493-stream-function-callbacks.ts b/tests/corpus/6493-stream-function-callbacks.ts new file mode 100644 index 000000000..7d5ee45e1 --- /dev/null +++ b/tests/corpus/6493-stream-function-callbacks.ts @@ -0,0 +1,66 @@ +// Stream subclasses that declare their completion callbacks with the broad +// `Function` type: the runtime's callable arrives and calls through it +// complete the chunk, pass data, or fail the stream. +import { Transform, Writable } from "node:stream"; + +class Prefixer extends Transform { + _s: string; + constructor() { + super(); + this._s = ""; + } + _transform(c: Buffer, _: string, cb: Function) { + const text = c.toString(); + const lines = text.split("\n").map((line, index, all) => { + if (index === 0) return this._s === "" || this._s.endsWith("\n") ? "> " + line : line; + if (index === all.length - 1) return line !== "" ? "> " + line : line; + return "> " + line; + }); + this._s += text; + this.push(lines.join("\n")); + cb(); + } +} + +class Upper extends Transform { + _transform(chunk: Buffer, _encoding: string, cb: Function) { + if (chunk.toString() === "bad") { + cb(new Error("bad chunk")); + return; + } + cb(null, chunk.toString().toUpperCase()); + } + _flush(cb: Function) { + this.push("!"); + cb(); + } +} + +class Sink extends Writable { + seen: string[] = []; + _write(chunk: Buffer, _encoding: string, cb: Function) { + this.seen.push(chunk.toString()); + cb(); + } + _final(cb: Function) { + this.seen.push(""); + cb(); + } +} + +const prefixer = new Prefixer(); +prefixer.pipe(process.stdout); +prefixer.write("a\nb"); +prefixer.write("c\n"); +prefixer.end("d\n"); + +const sink = new Sink(); +const upper = new Upper(); +upper.pipe(sink); +sink.on("finish", () => console.log("sink", JSON.stringify(sink.seen))); +upper.write("a"); +upper.end("b"); + +const bad = new Upper(); +bad.on("error", (err: Error) => console.log("error", err.message)); +bad.write("bad"); diff --git a/tests/corpus/6494-events-once-dyn-stream-slot/main.ts b/tests/corpus/6494-events-once-dyn-stream-slot/main.ts new file mode 100644 index 000000000..501944ae3 --- /dev/null +++ b/tests/corpus/6494-events-once-dyn-stream-slot/main.ts @@ -0,0 +1,59 @@ +// events.once over a checked-dynamic NodeJS.ReadableStream slot: the +// program also moves a JavaScript stream object into such a slot, so the +// slots hold native streams through their typed capsules. +import { once } from "node:events"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { PassThrough } from "node:stream"; +import { part } from "./sources.js"; + +function collect(src: NodeJS.ReadableStream): Promise { + return new Promise((resolve) => { + let text = ""; + src.on("data", (chunk: Buffer | string) => { + text += chunk.toString(); + }); + src.on("end", () => resolve(text)); + }); +} + +function copyPart(src: NodeJS.ReadableStream, out: PassThrough) { + const ac = new AbortController(); + src.pipe(out, { end: false }); + return Promise.race([ + once(src, "end", { signal: ac.signal }), + once(src, "error", { signal: ac.signal }).then(([error]) => { + throw error; + }), + once(out, "close", { signal: ac.signal }).then(() => { + throw new Error("output closed before the part ended"); + }), + ]).finally(() => { + ac.abort(); + src.unpipe(out); + }); +} + +async function main(): Promise { + console.log("js part", await collect(part(["a", "b"]))); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-once-dyn-")); + const file = path.join(dir, "a.txt"); + fs.writeFileSync(file, "native part\n"); + const out = new PassThrough(); + let text = ""; + out.on("data", (chunk: Buffer) => { + text += chunk.toString(); + }); + await copyPart(fs.createReadStream(file), out); + await copyPart(fs.createReadStream(file), out); + console.log(JSON.stringify(text)); + try { + await copyPart(fs.createReadStream(path.join(dir, "missing.txt")), out); + } catch (err) { + console.log("missing", (err as NodeJS.ErrnoException).code); + } + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/corpus/6494-events-once-dyn-stream-slot/sources.js b/tests/corpus/6494-events-once-dyn-stream-slot/sources.js new file mode 100644 index 000000000..e76e88785 --- /dev/null +++ b/tests/corpus/6494-events-once-dyn-stream-slot/sources.js @@ -0,0 +1,21 @@ +// A constructor-function stream with its own listener table. +function Part(chunks) { + this.chunks = chunks; + this.listeners = {}; + setImmediate(() => { + for (const chunk of this.chunks) this.emit("data", chunk); + this.emit("end"); + }); +} +Part.prototype.on = function (event, listener) { + (this.listeners[event] = this.listeners[event] || []).push(listener); + return this; +}; +Part.prototype.emit = function (event, value) { + for (const listener of this.listeners[event] || []) listener(value); + return true; +}; + +export function part(chunks) { + return new Part(chunks); +} diff --git a/tests/fixtures/node-types/events-once-signal-race.ts b/tests/fixtures/node-types/events-once-signal-race.ts new file mode 100644 index 000000000..4bc1d2d36 --- /dev/null +++ b/tests/fixtures/node-types/events-once-signal-race.ts @@ -0,0 +1,86 @@ +// events.once under @types/node, which types the result Promise: +// a NodeJS.ReadableStream slot and a PassThrough raced with an AbortSignal +// that the finally block aborts (the entries that throw are +// Promise, so the race result is the any[] of the 'end' entry), an +// EventEmitter race, and a Transform whose completion callback is declared +// with the broad `Function` type. +import { EventEmitter, once } from "node:events"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { PassThrough, Transform } from "node:stream"; + +function copyPart(src: NodeJS.ReadableStream, out: PassThrough) { + const ac = new AbortController(); + let failed = false; + src.pipe(out, { end: false }); + return Promise.race([ + once(src, "end", { signal: ac.signal }), + once(src, "error", { signal: ac.signal }).then(([error]) => { + failed = true; + throw error; + }), + once(out, "error", { signal: ac.signal }).then(([error]) => { + failed = true; + throw error; + }), + once(out, "close", { signal: ac.signal }).then(() => { + failed = true; + throw new Error("output closed before the part ended"); + }), + ]).finally(() => { + ac.abort(); + src.unpipe(out); + console.log("part done", failed); + }); +} + +function waitFor(a: EventEmitter) { + const ac = new AbortController(); + return Promise.race([ + once(a, "done", { signal: ac.signal }), + once(a, "error", { signal: ac.signal }).then(([error]) => { + throw error; + }), + ]).finally(() => ac.abort()); +} + +class Prefixer extends Transform { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + _transform(chunk: Buffer, _encoding: string, cb: Function) { + cb(null, "> " + chunk.toString()); + } +} + +async function main(): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-once-race-")); + const file = path.join(dir, "a.txt"); + fs.writeFileSync(file, "part\n"); + const out = new PassThrough(); + let text = ""; + out.on("data", (chunk: Buffer) => { + text += chunk.toString(); + }); + const ended = await copyPart(fs.createReadStream(file), out); + console.log("ended", ended.length); + await copyPart(fs.createReadStream(file).pipe(new Prefixer()), out); + console.log(JSON.stringify(text)); + try { + await copyPart(fs.createReadStream(path.join(dir, "missing.txt")), out); + } catch (err) { + console.log("missing", (err as NodeJS.ErrnoException).code); + } + const a = new EventEmitter(); + setTimeout(() => a.emit("done", 1, "two"), 1); + const args = (await waitFor(a)) as unknown[]; + console.log("done", JSON.stringify(args), a.listenerCount("done"), a.listenerCount("error")); + setTimeout(() => a.emit("error", new Error("bad")), 1); + try { + await waitFor(a); + } catch (err) { + console.log("caught", (err as Error).message, a.listenerCount("done")); + } + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 3635c88ed..927959440 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -179,6 +179,24 @@ test("node-types: events.once over typed EventEmitter listeners under @types/nod expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: events.once with a signal over streams, raced, under @types/node", async () => { + const outDir = outDirFor("events-once-signal-race"); + const entry = join(nodeTypesDir, "events-once-signal-race.ts"); + const result = await compile(entry, { + outPath: join(outDir, "events-once-signal-race"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: socket.address and resetAndDestroy under @types/node", async () => { const outDir = outDirFor("net-reset-address"); const entry = join(nodeTypesDir, "net-reset-address.ts"); From 507610b90d1fe9761d9daf4d31ab079105f3dcd7 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 09:46:05 -0700 Subject: [PATCH 11/22] Give constructor functions native stream state over the stream classes Bundled npm code (from2, bl, tar-stream, yazl, multistream) builds its streams as ES5 constructor functions: util.inherits(Ctor, Readable) and Readable.call(this, opts), with _read/_write/_transform on the prototype. Readable.call(this) threw SC2020. A new runtime unit, scr_dyn_builtin_stream.c (feature dynBuiltinStream), gives the untyped stream constructors their stream half. Base.call(this, opts) creates a runtime stream over the receiver and keeps it in a hidden slot; the runtime calls the receiver's underscore methods through its property lookup; the stream prototypes carry Node's methods and accessors over the hidden stream; and EventEmitter's methods register listeners with the runtime stream's emitter, so runtime events and pipes reach JavaScript listeners with the receiver as this. _readableState/_writableState are live views. Stream class values now come from dyn.builtinStreamClass, so only programs that use them link the stream half. Typed code sees these objects as their runtime streams: checked casts to the stream classes take them, readable.pipe() into an untyped destination pipes into them, and runtime streams crossing into JavaScript answer the constructor object. Co-Authored-By: Claude Opus 5.5 --- .../limitations/builtin-function-values.mdx | 6 +- .../src/backend/executable-features.ts | 1 + packages/compiler/src/backend/library-pack.ts | 1 + packages/compiler/src/backend/llvm/dyn.ts | 33 + packages/compiler/src/backend/llvm/emitter.ts | 12 +- .../src/backend/llvm/expr-stream-bridges.ts | 9 +- .../compiler/src/backend/llvm/lib-shared.ts | 5 + .../src/backend/native-link-info-core.ts | 1 + .../compiler/src/backend/native/contracts.ts | 4 + .../compiler/src/backend/native/executable.ts | 10 + .../src/backend/runtime-pack-targets.test.ts | 1 + .../compiler/src/backend/runtime-pack.test.ts | 1 + packages/compiler/src/backend/warm-cache.ts | 1 + .../src/executable/executable-cache.test.ts | 1 + .../src/executable/executable-cache.ts | 2 + .../lowering/builtins/builtin-class-dyn.ts | 12 +- .../src/frontend/lowering/lower-stream.ts | 49 + packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 8 + packages/compiler/src/ir/runtime-features.ts | 37 +- packages/compiler/src/native/cli.ts | 1 + packages/compiler/src/native/prepare.ts | 1 + .../compiler/src/native/sanitizer.test.ts | 1 + ...560-constructor-function-readable.cjs.json | 6 + ...561-constructor-function-writable.cjs.json | 6 + ...62-constructor-function-transform.cjs.json | 6 + ...onstructor-function-stream-errors.cjs.json | 6 + .../runtime-pack-matrix.mjs | 5 + packages/runtime/src/scr_dyn.h | 32 + packages/runtime/src/scr_dyn_builtin_class.c | 100 +- packages/runtime/src/scr_dyn_builtin_stream.c | 1672 +++++++++++++++++ packages/runtime/src/scr_stream.c | 2 + packages/runtime/src/scr_stream.h | 3 + .../6560-constructor-function-readable.cjs | 121 ++ .../6561-constructor-function-writable.cjs | 141 ++ .../6562-constructor-function-transform.cjs | 137 ++ ...563-constructor-function-stream-errors.cjs | 137 ++ .../self-hosting-native-toolchain.test.ts | 1 + .../self-hosting-runtime-features.test.ts | 1 + .../harness/self-hosting-runtime-pack.test.ts | 1 + 41 files changed, 2561 insertions(+), 16 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-constructor-function-readable.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6561-constructor-function-writable.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6562-constructor-function-transform.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6563-constructor-function-stream-errors.cjs.json create mode 100644 packages/runtime/src/scr_dyn_builtin_stream.c create mode 100644 tests/corpus/6560-constructor-function-readable.cjs create mode 100644 tests/corpus/6561-constructor-function-writable.cjs create mode 100644 tests/corpus/6562-constructor-function-transform.cjs create mode 100644 tests/corpus/6563-constructor-function-stream-errors.cjs diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index f3902e75b..420ac89aa 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -15,7 +15,11 @@ Native calls accept standard process streams; custom native streams and mutation In JavaScript, a constructor function can inherit from `EventEmitter`, the legacy `Stream` constructor, or a stream class through `util.inherits` or `Object.setPrototypeOf`. `EventEmitter.call(this)` and `Stream.call(this)` initialize the object as Node does, and its listener methods keep their state in the object's own `_events`, `_eventsCount`, and `_maxListeners` properties. The `captureRejections` option and `events.errorMonitor` listeners are not supported on such objects. -Calling `Readable`, `Writable`, `Duplex`, `Transform`, or `PassThrough` with a constructor function's receiver throws `SC2020` where the call runs. +`Readable.call(this, options)` and the same call on `Writable`, `Duplex`, `Transform`, or `PassThrough` give the object a native stream. +The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it. +Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields. +Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works. +The options the native streams refuse (`objectMode`, `decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`. Other class constructors passed to `util.inherits` or `util.deprecate` are refused. Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported. A stored `util.deprecate` reports a `length` of 4, where Node reports 3. diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index a6e228b2e..9418631d0 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -32,6 +32,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { searchParams: features.searchParams, qs: features.qs, dynBuiltinClass: features.dynBuiltinClass, + dynBuiltinStream: features.dynBuiltinStream, parseArgs: features.parseArgs, stream: features.stream, net: features.net, diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index efabdc9d4..0a7618905 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -43,6 +43,7 @@ export async function compilePackedLibrary( searchParams: options.searchParams ?? false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 86b4f5be8..8b35c84c5 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -129,6 +129,8 @@ export interface DynHost extends WalkerHost { /** The shared adapter record: live array capsules read its `arrayOps`, so a * narrower return type would hand the native compiler a copy without it. */ liveDynRefAdapter(t: IrType): LlStreamTypedRefAdapter; + /** See moduleUsesDynBuiltinStream. */ + usesDynBuiltinStream?(): boolean; liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string; dynPromiseAdapter(t: IrType): string; isErrorClass(className: string): boolean; @@ -139,6 +141,19 @@ export interface DynHost extends WalkerHost { const FN_ATTRS = "#0"; +/** scr_dyn_builtin_stream_check's kind for a runtime stream class (the + * numbering of scr_dyn_builtin_class.c), or null. */ +function builtinStreamKind(className: string): number | null { + const kinds: Record = { + "%Readable": 2, + "%Writable": 3, + "%Duplex": 4, + "%Transform": 5, + "%PassThrough": 6, + }; + return kinds[className] ?? null; +} + export class LlDyn { private readonly dynMatchers = new Map(); private readonly dynBuilders = new Map(); @@ -1478,6 +1493,24 @@ export class LlDyn { B.line(`${ref} = call ptr @scr_dyn_typed_ref_unbox(ptr %d)`); B.terminate(`ret ptr ${ref}`); B.startBlock(lNext); + // A constructor-function stream (scr_dyn_builtin_stream.c) carries + // its runtime stream. + const streamKind = t.kind === "object" ? builtinStreamKind(t.className) : null; + if (streamKind !== null && this.host.usesDynBuiltinStream?.() === true) { + host.declare(`declare ptr @scr_dyn_builtin_stream_check(ptr, double)`); + const own = B.tmp(), + hit = B.tmp(); + B.line( + `${own} = call ptr @scr_dyn_builtin_stream_check(ptr %d, double ${f64Lit(streamKind)})`, + ); + B.line(`${hit} = icmp ne ptr ${own}, null`); + const lOwn = B.newLabel("dc.ctor"), + lNotOwn = B.newLabel("dc.notctor"); + B.condBr(hit, lOwn, lNotOwn); + B.startBlock(lOwn); + B.terminate(`ret ptr ${own}`); + B.startBlock(lNotOwn); + } if (t.kind !== "union" && preserveRefs) { const kind = this.kindOf(B, "%d"); const capsule = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 1d745b981..5b138544e 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -52,7 +52,11 @@ import { VOID, classJsName, } from "../../ir/ir.js"; -import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js"; +import { + moduleRuntimeFeatures, + moduleUsesDynBuiltinStream, + moduleUsesMockTimers, +} from "../../ir/runtime-features.js"; import { captureFreeFunctions, observesStacks } from "../stack-observations.js"; import { type DenseArrayLoop } from "./dense-array-loops.js"; import { findInitializedByteLoopBindings } from "./byte-windows.js"; @@ -890,6 +894,11 @@ export class LlEmitter { /** Identity-preserving static→dyn capsules for program classes and Web * APIs whose values remain directly observable. */ readonly liveDynRefAdapters = new Map(); + private dynBuiltinStreamUse: boolean | undefined; + /** See moduleUsesDynBuiltinStream. */ + get usesDynBuiltinStream(): boolean { + return (this.dynBuiltinStreamUse ??= moduleUsesDynBuiltinStream(this.mod)); + } /** Runtime-arm dispatchers for live union values. */ readonly liveDynUnionRefAdapters = new Map(); readonly dynPromiseAdapters = new Map(); @@ -1375,6 +1384,7 @@ export class LlEmitter { fieldAbsentTestIn: (B, value, unionId) => this.fieldAbsentTestIn(B, value, unionId), absentInstanceRef: (unionId) => this.absentInstanceRef(unionId), liveDynRefAdapter: (type) => this.liveDynRefAdapter(type), + usesDynBuiltinStream: () => this.usesDynBuiltinStream, liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type), dynPromiseAdapter: (type) => this.dynPromiseAdapter(type), isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error", diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 8a53b5fa8..a80974975 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -763,8 +763,13 @@ export function streamTypedRefMaterializeAdapter( !meta.def.instancePrototypeHelper && !meta.def.prototypeDataHelper ) { - host.declare(`declare ptr @scr_stream_dyn_view(ptr)`); - B.line(`${out} = call ptr @scr_stream_dyn_view(ptr %p)`); + // Programs with constructor-function streams view them through + // scr_dyn_builtin_stream.c, which answers their own objects. + const view = host.usesDynBuiltinStream + ? "scr_dyn_builtin_stream_view" + : "scr_stream_dyn_view"; + host.declare(`declare ptr @${view}(ptr)`); + B.line(`${out} = call ptr @${view}(ptr %p)`); } else B.line(`${out} = call ptr @scr_dyn_new_obj()`); if (meta.def.tracksOwnFields) { host.declare(`declare ptr @scr_dyn_copy_property_descriptors(ptr, ptr)`); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 2b25cc92e..09b3dd07a 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1405,6 +1405,8 @@ export const LIB_FN_SYMS: Record = { "dyn.classBasePrototype": "scr_dyn_class_base_prototype", "dyn.classInherit": "scr_dyn_class_inherit", "dyn.builtinClass": "scr_dyn_builtin_class", + "dyn.builtinStreamClass": "scr_dyn_builtin_stream_class", + "dyn.builtinStreamPipe": "scr_dyn_builtin_stream_pipe", "dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof", "dyn.functionInstanceOf": "scr_dyn_function_instanceof", "dyn.classSuper": "scr_dyn_class_super", @@ -1483,6 +1485,9 @@ export function mockTimerSym( export const USES_TIMERS_LIB_FNS = new Set([ "process.emitWarning", + // Constructor-function streams defer their events on the stream ticks. + "dyn.builtinStreamClass", + "dyn.builtinStreamPipe", // Decipheriv.setAuthTag reports DEP0182 on the next tick. "crypto.cipherNew", // WebCrypto results settle through the deferred crypto queue. diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index d7cf78327..fcb0822f9 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -47,6 +47,7 @@ export interface NativeLinkFeatures { searchParams: boolean; qs: boolean; dynBuiltinClass: boolean; + dynBuiltinStream: boolean; parseArgs: boolean; stream: boolean; net: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index d978d4387..0a75ad9a6 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -155,6 +155,10 @@ export interface CcOptions { * scr_dyn_builtin_class.c, which also references the inspection, * dyn-invoke and symbol units. */ dynBuiltinClass?: boolean; + /** The program uses a stream class as an untyped constructor value: + * compiles scr_dyn_builtin_stream.c, the constructors' stream half, which + * also references the stream unit. */ + dynBuiltinStream?: boolean; /** The program uses native util parsing, comparison, or styling. Optional * utility units also require their symbol, inspection, and warning features. */ parseArgs?: boolean; diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index bbb720cc7..f904a63b1 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -239,6 +239,16 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ (u) => u.opts.dynBuiltinClass && u.opts.inspect && u.opts.dynInvoke && u.opts.symbol, "scr_dyn_builtin_class.c", ), + unit( + (u) => + u.opts.dynBuiltinStream && + u.opts.dynBuiltinClass && + u.opts.stream && + u.opts.inspect && + u.opts.dynInvoke && + u.opts.symbol, + "scr_dyn_builtin_stream.c", + ), unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"), // The zlib stream classes reference both the zlib engine and the // stream unit. diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 8b0689c51..2253b3e94 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -108,6 +108,7 @@ const features = { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 5af267d96..bda9984ec 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -45,6 +45,7 @@ const BASE: NativeLinkFeatures = { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index b0736f102..32bfb264e 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -51,6 +51,7 @@ export async function warmNativeCaches( searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index cdd24ff7b..b6adee539 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -46,6 +46,7 @@ const native: EarlyExecutableNativeFeatures = { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 349c07d42..ff395246c 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -72,6 +72,7 @@ export interface EarlyExecutableNativeFeatures { searchParams: boolean; qs: boolean; dynBuiltinClass: boolean; + dynBuiltinStream: boolean; parseArgs: boolean; stream: boolean; net: boolean; @@ -187,6 +188,7 @@ const BOOLEAN_NATIVE_KEYS = [ "searchParams", "qs", "dynBuiltinClass", + "dynBuiltinStream", "parseArgs", "stream", "net", diff --git a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts index ac73924ad..9508aa651 100644 --- a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts +++ b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts @@ -111,8 +111,18 @@ export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): I }; } +/** EventEmitter is dyn.builtinClass; the stream classes, which carry + * native stream state for constructor functions, are + * dyn.builtinStreamClass, so only programs that use them link the stream + * half. */ export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr { - return { kind: "libCall", fn: "dyn.builtinClass", args: [numLit(kind, loc)], type: DYN, loc }; + return { + kind: "libCall", + fn: kind === 0 ? "dyn.builtinClass" : "dyn.builtinStreamClass", + args: [numLit(kind, loc)], + type: DYN, + loc, + }; } /** `Base.call(this, …)` / `Base.apply(this, args)` on one of those class diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 64d2e60e8..1a3c93f4b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -3276,6 +3276,54 @@ function pipeEndOption(lowerer: Lowerer, options: ts.Expression | undefined, loc return end; } +/** `readable.pipe(untyped)`: the destination is a constructor-function + * stream or a boxed runtime stream at run time (dyn.builtinStreamPipe; + * other values throw the not-supported Error there), and pipe() answers + * the destination value itself. */ +function pipeIntoUntyped( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + target: IrExpr, + options: ts.Expression | undefined, + loc: SrcLoc, +): IrExpr { + const receiverValue = lowerer.lowerExpr(access.expression); + const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); + const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type); + const end = pipeEndOption(lowerer, options, loc); + const destinationRef: IrExpr = { + kind: "varRef", + localId: destination.id, + type: target.type, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: source.id, init: receiverValue, loc }, + { kind: "varDecl", localId: destination.id, init: target, loc }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.builtinStreamPipe", + args: [ + { kind: "varRef", localId: source.id, type: receiverValue.type, loc }, + destinationRef, + end, + ], + type: VOID, + loc, + }, + loc, + }, + ], + result: destinationRef, + type: target.type, + loc, + }; +} + export function lowerStreamMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -3526,6 +3574,7 @@ export function lowerStreamMethodCall( }; } const dst = lowerer.lowerExpr(args[0]!); + if (dst.type.kind === "dyn") return pipeIntoUntyped(lowerer, access, dst, args[1], loc); const dstInfo = dst.type.kind === "object" ? lowerer.classes.get(dst.type.className) : undefined; const dstSides = streamSidesOf(lowerer, dstInfo); diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 05a6f7423..f34d4dc23 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -544,6 +544,7 @@ async function compileExecutableNative( searchParams: features.searchParams, qs: features.qs, dynBuiltinClass: features.dynBuiltinClass, + dynBuiltinStream: features.dynBuiltinStream, parseArgs: features.parseArgs, stream: features.stream, net: features.net, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 1b84c5888..514d26317 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -559,6 +559,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // a proxy in the prototype chain can throw "dyn.builtinInstanceOf", "dyn.functionInstanceOf", + // a destination that is not a stream throws the not-supported Error + "dyn.builtinStreamPipe", "dyn.getOwnPropertyNames", "dyn.ownKeys", "dyn.getOwnPropertySymbols", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index a3480eb2c..d67bd5c37 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3413,6 +3413,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * Transform (5) and PassThrough (6) as untyped constructor values, one * per thread (scr_dyn_builtin_class.c). */ "dyn.builtinClass": { argTypes: [F64], result: DYN }, + /** The stream classes (1-6 above) with their stream half: constructor + * functions over them get native stream state + * (scr_dyn_builtin_stream.c). */ + "dyn.builtinStreamClass": { argTypes: [F64], result: DYN }, + /** readable.pipe(untyped): a constructor-function stream or a boxed + * runtime stream as the destination; other values throw the not- + * supported Error (scr_dyn_builtin_stream.c). */ + "dyn.builtinStreamPipe": { argTypes: [null, DYN, BOOL], result: VOID }, /** `value instanceof C` for those constructors, over the value's * prototype chain. */ "dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index de91b4b78..79428a02f 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -224,6 +224,8 @@ const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRe const SYMBOL_LIB_FNS: ReadonlySet = new Set([ // EventEmitter's kShapeMode and kCapture properties "dyn.builtinClass", + "dyn.builtinStreamClass", + "dyn.builtinStreamPipe", "dyn.usingDispose", "dyn.usingMethod", "util.isDeepStrictEqual", @@ -497,11 +499,27 @@ const RUNTIME_FEATURE_RULES = { * unit, render unhandled 'error' payloads through the inspect unit and * create EventEmitter's private symbols through the symbol unit. */ dynBuiltinClass: rule({ - fns: ["dyn.builtinClass", "dyn.builtinInstanceOf", "dyn.functionInstanceOf"], + fns: [ + "dyn.builtinClass", + "dyn.builtinStreamClass", + "dyn.builtinStreamPipe", + "dyn.builtinInstanceOf", + "dyn.functionInstanceOf", + ], }), + /** The stream classes as untyped constructor values with their stream + * half (scr_dyn_builtin_stream.c): native stream state for constructor + * functions, over the stream unit. */ + dynBuiltinStream: rule({ fns: ["dyn.builtinStreamClass", "dyn.builtinStreamPipe"] }), dynInvoke: rule({ initial: (mod) => mod.workers === true, - fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, "dyn.builtinClass"], + fns: [ + ...DYN_INVOKE_LIB_FNS, + ...UTIL_CALLABLE_LIB_FNS, + "dyn.builtinClass", + "dyn.builtinStreamClass", + "dyn.builtinStreamPipe", + ], expr: (node, _tables) => node.kind === "dynInvoke", }), /** The program has dynamic-tier jsval operations (js.* builtins, the @@ -512,7 +530,7 @@ const RUNTIME_FEATURE_RULES = { emitter: rule({ initial: (mod) => mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), - fns: [...STREAM_LIB_FNS], + fns: [...STREAM_LIB_FNS, "dyn.builtinStreamClass", "dyn.builtinStreamPipe"], prefixes: ["emitter.", ...STREAM_LIB_PREFIXES], }), /** Subclasses of the runtime EventTarget/Event bases tear down and box @@ -548,6 +566,8 @@ const RUNTIME_FEATURE_RULES = { "util.styleText", "crypto.cipherNew", "dyn.builtinClass", + "dyn.builtinStreamClass", + "dyn.builtinStreamPipe", ...UTIL_CALLABLE_LIB_FNS, ], prefixes: ["insp.", "console.", "test."], @@ -611,7 +631,7 @@ const RUNTIME_FEATURE_RULES = { }), stream: rule({ initial: (mod) => (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)), - fns: [...STREAM_LIB_FNS], + fns: [...STREAM_LIB_FNS, "dyn.builtinStreamClass", "dyn.builtinStreamPipe"], prefixes: STREAM_LIB_PREFIXES, }), /** A Dirent row converted to dyn carries Node's Symbol(type) key @@ -653,6 +673,7 @@ function noRuntimeFeatures(): RuntimeFeatures { dgram: false, dynAsync: false, dynBuiltinClass: false, + dynBuiltinStream: false, dynInvoke: false, dynTier: false, emitter: false, @@ -815,6 +836,14 @@ export function moduleUsesStream(mod: IrModule): boolean { return scanRuntimeFeatures(mod, "stream").stream; } +/** True when the program links scr_dyn_builtin_stream.c (stream classes + * as untyped constructor values): its runtime streams box through that + * unit's view and checked casts to the stream classes accept constructor- + * function streams. Programs without it keep their exact code. */ +export function moduleUsesDynBuiltinStream(mod: IrModule): boolean { + return scanRuntimeFeatures(mod, "dynBuiltinStream").dynBuiltinStream; +} + export const NPM_COMPRESS_MIN = 1024; export function moduleUsesZlib(mod: IrModule): boolean { diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index ce11aadf6..d1e1a664f 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -90,6 +90,7 @@ async function main(): Promise { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index b95b978ff..cc0f1bb9a 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -144,6 +144,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "searchParams", "qs", "dynBuiltinClass", + "dynBuiltinStream", "parseArgs", "stream", "net", diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index bbd07a6cd..52fe1af41 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -96,6 +96,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-constructor-function-readable.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-constructor-function-readable.cjs.json new file mode 100644 index 000000000..48964b0a6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-constructor-function-readable.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6560-constructor-function-readable.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6561-constructor-function-writable.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6561-constructor-function-writable.cjs.json new file mode 100644 index 000000000..603b083d1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6561-constructor-function-writable.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6561-constructor-function-writable.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6562-constructor-function-transform.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6562-constructor-function-transform.cjs.json new file mode 100644 index 000000000..c5d3d1a62 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6562-constructor-function-transform.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6562-constructor-function-transform.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6563-constructor-function-stream-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6563-constructor-function-stream-errors.cjs.json new file mode 100644 index 000000000..6e7f693c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6563-constructor-function-stream-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6563-constructor-function-stream-errors.cjs" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index a6b11f53e..364369366 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -92,6 +92,11 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // through the dyn-invoke unit, inspect unhandled 'error' payloads and // create EventEmitter's private symbols. ["scr_dyn_builtin_class.c", all("dynBuiltinClass", "inspect", "dynInvoke", "symbol")], + // Their stream half also references the stream unit. + [ + "scr_dyn_builtin_stream.c", + all("dynBuiltinStream", "dynBuiltinClass", "stream", "inspect", "dynInvoke", "symbol"), + ], ["scr_stream.c", "stream"], // The zlib stream classes reference both the zlib engine and the stream unit. ["scr_zlib_stream.c", all("zlib", "stream")], diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 91919b641..5dacae1ef 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1122,6 +1122,38 @@ ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key); ScrDyn *scr_dyn_builtin_class(double kind); bool scr_dyn_builtin_instanceof(ScrDyn *value, double kind); bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee); +/* The stream half of those constructors (scr_dyn_builtin_stream.c, linked + * with the dynBuiltinStream feature), registered with + * scr_dyn_builtin_class_streams before the first stream class value is + * read: construct runs `Readable.call(this, opts)` and the other stream + * kinds (+1 or NULL with an exception pending), prototype installs a stream + * kind's methods, and event answers EventEmitter.prototype's methods for a + * receiver with stream state (true when it handled the call; *result +1 or + * NULL with an exception pending). */ +enum { + SCR_EE_M_ON, SCR_EE_M_PREPEND, SCR_EE_M_ONCE, SCR_EE_M_PREPEND_ONCE, SCR_EE_M_OFF, + SCR_EE_M_REMOVE_ALL, SCR_EE_M_EMIT, SCR_EE_M_LISTENERS, SCR_EE_M_RAW_LISTENERS, + SCR_EE_M_LISTENER_COUNT, SCR_EE_M_EVENT_NAMES, SCR_EE_M_SET_MAX, SCR_EE_M_GET_MAX, +}; +typedef struct ScrDynStreamOps { + ScrDyn *(*construct)(int kind, ScrDyn *self, ScrDyn *const *args, size_t argc); + void (*prototype)(int kind, ScrDyn *prototype); + bool (*event)(int method, ScrDyn *self, ScrDyn *const *args, size_t argc, ScrDyn **result); +} ScrDynStreamOps; +void scr_dyn_builtin_class_streams(const ScrDynStreamOps *ops); +/* A stream class value with its stream half (scr_dyn_builtin_stream.c), + * +1; the stream a JavaScript value stands for (a constructor-function + * stream or a boxed runtime stream), +1 or NULL; and the constructor- + * function object of a runtime stream, +1 or NULL. */ +ScrDyn *scr_dyn_builtin_stream_class(double kind); +struct ScrStream *scr_dyn_builtin_stream_of(const ScrDyn *value); +ScrDyn *scr_dyn_builtin_stream_object(struct ScrStream *s); +/* The untyped view of a runtime stream in programs that link that unit + * (+1), a checked cast to a runtime stream class kind (+1 or NULL), and + * readable.pipe() into an untyped destination (src and dest borrowed). */ +ScrDyn *scr_dyn_builtin_stream_view(struct ScrStream *s); +struct ScrStream *scr_dyn_builtin_stream_check(const ScrDyn *value, double kind); +void scr_dyn_builtin_stream_pipe(struct ScrStream *src, ScrDyn *dest, bool end); bool scr_dyn_class_is_key(const ScrDyn *value, const ScrStr *type_key); bool scr_dyn_class_is(const ScrDyn *value, const char *type_key); /* Calls a dyn value: a non-function kind throws the catchable TypeError diff --git a/packages/runtime/src/scr_dyn_builtin_class.c b/packages/runtime/src/scr_dyn_builtin_class.c index 76a7ab15e..a9b50d218 100644 --- a/packages/runtime/src/scr_dyn_builtin_class.c +++ b/packages/runtime/src/scr_dyn_builtin_class.c @@ -15,9 +15,13 @@ * Stream is Node's legacy constructor: EventEmitter.call(this, opts) and a * prototype that inherits EventEmitter's. Readable, Writable, Duplex, * Transform and PassThrough have prototypes chained as in Node, so - * util.inherits onto them and instanceof work, but their native state has - * no dyn object form: calling one with a receiver throws the "not - * supported yet" Error where the construction runs. + * util.inherits onto them and instanceof work. Their stream half (the + * prototypes' methods and `Readable.call(this, opts)`) lives in + * scr_dyn_builtin_stream.c, which registers itself through + * scr_dyn_builtin_class_streams in programs that link it; without it, + * calling one with a receiver throws the "not supported yet" Error where + * the construction runs. EventEmitter's methods hand a receiver with + * stream state to that unit, whose listeners live in the runtime stream. * * One constructor per kind and thread, so every boxing of the same class * value is the same function. All arguments are borrowed. */ @@ -59,6 +63,14 @@ static SCR_TL ScrDyn *bc_symbols[BC_SYMBOLS]; static ScrDyn *bc_class(int kind); +/* The stream half, once registered (scr_dyn_builtin_stream.c). */ +static SCR_TL const ScrDynStreamOps *bc_stream_ops; + +/* EventEmitter.prototype's method on a receiver with stream state. */ +static bool bc_stream_event(int method, ScrDyn *self, ScrDyn *const *args, size_t argc, ScrDyn **result) { + return bc_stream_ops != NULL && bc_stream_ops->event(method, self, args, argc, result); +} + /* ── property helpers ───────────────────────────────────────────────── */ static ScrDyn *bc_key(const char *name) { @@ -587,7 +599,12 @@ static ScrDyn *bc_once_wrap(ScrDyn *target, ScrDyn *type, ScrDyn *listener) { static ScrDyn *bc_on(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); - ScrDyn *result = bc_require_object(self, "_events") + ScrDyn *result; + if (bc_stream_event(SCR_EE_M_ON, self, args, argc, &result)) { + scr_dyn_release(self); + return result; + } + result = bc_require_object(self, "_events") ? bc_add_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1), false) : NULL; scr_dyn_release(self); return result; @@ -596,7 +613,12 @@ static ScrDyn *bc_on(ScrClosure *closure, ScrDyn *const *args, size_t argc) { static ScrDyn *bc_prepend(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); - ScrDyn *result = bc_require_object(self, "_events") + ScrDyn *result; + if (bc_stream_event(SCR_EE_M_PREPEND, self, args, argc, &result)) { + scr_dyn_release(self); + return result; + } + result = bc_require_object(self, "_events") ? bc_add_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1), true) : NULL; scr_dyn_release(self); return result; @@ -607,6 +629,10 @@ static ScrDyn *bc_once_common(ScrDyn *const *args, size_t argc, bool prepend) { ScrDyn *type = bc_arg(args, argc, 0); ScrDyn *listener = bc_arg(args, argc, 1); ScrDyn *result = NULL; + if (bc_stream_event(prepend ? SCR_EE_M_PREPEND_ONCE : SCR_EE_M_ONCE, self, args, argc, &result)) { + scr_dyn_release(self); + return result; + } if (bc_check_listener(listener) && bc_require_object(self, "_events")) { ScrDyn *wrapper = bc_once_wrap(self, type, listener); if (wrapper) { @@ -631,7 +657,12 @@ static ScrDyn *bc_prepend_once(ScrClosure *closure, ScrDyn *const *args, size_t static ScrDyn *bc_off(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); - ScrDyn *result = bc_require_object(self, "_events") + ScrDyn *result; + if (bc_stream_event(SCR_EE_M_OFF, self, args, argc, &result)) { + scr_dyn_release(self); + return result; + } + result = bc_require_object(self, "_events") ? bc_remove_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1)) : NULL; scr_dyn_release(self); return result; @@ -643,6 +674,11 @@ static ScrDyn *bc_off(ScrClosure *closure, ScrDyn *const *args, size_t argc) { static ScrDyn *bc_remove_all(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); + ScrDyn *handled; + if (bc_stream_event(SCR_EE_M_REMOVE_ALL, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } if (!bc_require_object(self, "_events")) { scr_dyn_release(self); return NULL; @@ -761,6 +797,11 @@ fail: static ScrDyn *bc_emit(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); + ScrDyn *handled; + if (bc_stream_event(SCR_EE_M_EMIT, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } if (!bc_require_object(self, "_events")) { scr_dyn_release(self); return NULL; @@ -846,6 +887,11 @@ fail_self: /* _listeners(target, type, unwrap): a fresh array. */ static ScrDyn *bc_listeners_of(bool unwrap, ScrDyn *const *args, size_t argc) { ScrDyn *self = bc_this(); + ScrDyn *handled; + if (bc_stream_event(unwrap ? SCR_EE_M_LISTENERS : SCR_EE_M_RAW_LISTENERS, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } if (!bc_require_object(self, "_events")) { scr_dyn_release(self); return NULL; @@ -939,7 +985,12 @@ static ScrDyn *bc_count_listeners(ScrDyn *self, ScrDyn *type, ScrDyn *only) { static ScrDyn *bc_listener_count(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); - ScrDyn *result = bc_require_object(self, "_events") + ScrDyn *result; + if (bc_stream_event(SCR_EE_M_LISTENER_COUNT, self, args, argc, &result)) { + scr_dyn_release(self); + return result; + } + result = bc_require_object(self, "_events") ? bc_count_listeners(self, bc_arg(args, argc, 0), argc > 1 && !bc_undefined(args[1]) && args[1]->kind != SCR_DYN_NULL ? args[1] : NULL) : NULL; @@ -966,6 +1017,11 @@ static ScrDyn *bc_event_names(ScrClosure *closure, ScrDyn *const *args, size_t a (void)args; (void)argc; ScrDyn *self = bc_this(); + ScrDyn *handled; + if (bc_stream_event(SCR_EE_M_EVENT_NAMES, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } if (!bc_require_object(self, "_eventsCount")) { scr_dyn_release(self); return NULL; @@ -1010,6 +1066,11 @@ static ScrDyn *bc_set_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) scr_dyn_release(self); return NULL; } + ScrDyn *handled; + if (bc_stream_event(SCR_EE_M_SET_MAX, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } if (!bc_require_object(self, "_maxListeners") || !bc_set(self, "_maxListeners", scr_dyn_retain(n))) { scr_dyn_release(self); return NULL; @@ -1022,6 +1083,11 @@ static ScrDyn *bc_get_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) (void)args; (void)argc; ScrDyn *self = bc_this(); + ScrDyn *handled; + if (bc_stream_event(SCR_EE_M_GET_MAX, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } double max = bc_require_object(self, "_maxListeners") ? bc_max_listeners(self) : -1; scr_dyn_release(self); if (scr_exc_pending()) return NULL; @@ -1280,6 +1346,8 @@ static ScrDyn *bc_construct(ScrClosure *closure, ScrDyn *const *args, size_t arg ScrDyn *result = NULL; if (kind == BC_EMITTER || kind == BC_STREAM) { if (bc_emitter_init(self, bc_arg(args, argc, 0))) result = scr_dyn_retain(scr_dyn_undefined()); + } else if (bc_stream_ops) { + result = bc_stream_ops->construct(kind, self, args, argc); } else { char message[160]; snprintf(message, sizeof message, @@ -1373,6 +1441,8 @@ static ScrDyn *bc_new_class(int kind, ScrDyn *parent) { } else if (kind == BC_STREAM) { ScrDyn *pipe = scr_dyn_new_func(scr_closure_new(NULL, 0), bc_pipe, 2, "native:stream", "pipe"); bc_set(prototype, "pipe", pipe); + } else if (bc_stream_ops) { + bc_stream_ops->prototype(kind, prototype); } scr_dyn_release(prototype); if (scr_exc_pending()) scr_exc_clear(); @@ -1408,6 +1478,22 @@ static ScrDyn *bc_class(int kind) { return bc_ctors[kind]; } +/* Registers the stream half; constructors that already exist get their + * stream prototypes' methods now. */ +void scr_dyn_builtin_class_streams(const ScrDynStreamOps *ops) { + if (bc_stream_ops) return; + bc_stream_ops = ops; + for (int kind = BC_READABLE; kind < BC_COUNT; kind++) { + if (!bc_ctors[kind]) continue; + ScrDyn *prototype = bc_get(bc_ctors[kind], "prototype"); + if (prototype) { + ops->prototype(kind, prototype); + scr_dyn_release(prototype); + } + if (scr_exc_pending()) scr_exc_clear(); + } +} + /* The constructor value for kind (the BC_* order), +1. */ ScrDyn *scr_dyn_builtin_class(double kind) { int index = (int)kind; diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c new file mode 100644 index 000000000..4e42d219b --- /dev/null +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -0,0 +1,1672 @@ +/* Native stream state for JavaScript constructor functions that inherit + * from the node:stream classes the pre-class way: + * + * function Source(opts) { Readable.call(this, opts); } + * util.inherits(Source, Readable); + * Source.prototype._read = function (n) { this.push(...); }; + * + * scr_dyn_builtin_class.c provides Readable, Writable, Duplex, Transform + * and PassThrough as untyped constructors with Node's prototype chain. + * This unit (the dynBuiltinStream feature) gives them their stream half: + * + * - `Readable.call(this, opts)` (and the other four) creates a runtime + * stream (scr_stream.c) over the receiver. The receiver keeps it in a + * hidden slot (an entry of its symbol table that no symbol can name), + * so JavaScript sees only the properties Node's constructor adds: + * `_readableState` / `_writableState` (live read-only views of the + * runtime state), `allowHalfOpen` on the duplex kinds, and the option + * callbacks copied to `_read`, `_write`, `_final`, `_destroy`, + * `_transform` and `_flush`. + * - The runtime stream calls the receiver's `_read`, `_write`, + * `_transform`, `_flush`, `_final` and `_destroy` through its property + * lookup, with the receiver as `this`, as Node's machinery does. + * - The stream prototypes carry Node's methods and accessors (push, read, + * write, end, pipe, destroy, readable, destroyed, ...) over the hidden + * stream, and EventEmitter's methods register listeners with the runtime + * stream's own emitter (scr_dyn_builtin_class.c forwards to them), so the + * runtime's events and pipes reach JavaScript listeners with the + * receiver as `this`. + * + * Lifetime: the receiver holds its stream through a traced capsule, and + * the stream's hook closures hold the receiver through traced boxes, so + * the pair lives while either is reachable and the cycle collector + * reclaims it afterwards. The stream's native record points back at the + * receiver without a reference; the hook closures outlive the record. + * + * Not supported here (each throws the "not supported yet" Error where it + * runs): objectMode and the other options scr_stream.c's dyn option walk + * refuses, `_writev` and `_construct`, symbol event names, listeners() / + * rawListeners(), `wrap()`, and pipes into values that are neither runtime + * streams nor constructor-function streams. */ +#include "scr_runtime.h" + +#include +#include +#include + +enum { BCS_READABLE = 2, BCS_WRITABLE, BCS_DUPLEX, BCS_TRANSFORM, BCS_PASSTHROUGH }; + +static const char bcs_hidden_key[] = "scriptc:stream"; +#define BCS_HIDDEN_LEN (sizeof bcs_hidden_key - 1) + +/* The runtime stream's native record. */ +struct BcsRec; +typedef struct BcsRec { + ScrDyn *object; /* borrowed: the hook closures keep it alive */ + ScrDyn **destroy_cbs; /* destroy(err, cb) callbacks waiting for _destroy */ + size_t destroy_n, destroy_cap; + char cls[64]; /* the constructor's name, for the leak warning */ +} BcsRec; + +static void bcs_rec_release(void *p) { + BcsRec *rec = p; + for (size_t i = 0; i < rec->destroy_n; i++) scr_dyn_release(rec->destroy_cbs[i]); + free(rec->destroy_cbs); + free(rec); +} + +static BcsRec *bcs_rec(ScrStream *s) { + return scr_stream_native(s, bcs_rec_release); +} + +/* ── small helpers ──────────────────────────────────────────────────── */ + +static ScrDyn *bcs_key(const char *name) { + ScrStr *text = scr_str_new(name, strlen(name)); + ScrDyn *key = scr_dyn_new_str(text); + scr_str_release(text); + return key; +} + +/* object[name] with object as the receiver: +1, or NULL with an exception + * pending. */ +static ScrDyn *bcs_get(ScrDyn *object, const char *name) { + ScrDyn *key = bcs_key(name); + ScrDyn *value = scr_dyn_reflect_get(object, key, object); + scr_dyn_release(key); + if (!value && !scr_exc_pending()) value = scr_dyn_retain(scr_dyn_undefined()); + return value; +} + +/* object[name] = value; the value moves in. */ +static bool bcs_set(ScrDyn *object, const char *name, ScrDyn *value) { + ScrDyn *key = bcs_key(name); + scr_dyn_key_set_computed(object, key, value); + scr_dyn_release(key); + scr_dyn_release(value); + return !scr_exc_pending(); +} + +static ScrDyn *bcs_arg(ScrDyn *const *args, size_t argc, size_t index) { + return index < argc ? args[index] : scr_dyn_undefined(); +} + +static bool bcs_nullish(const ScrDyn *value) { + return value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL; +} + +static ScrDyn *bcs_undefined(void) { + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *bcs_str(const char *text) { + return bcs_key(text); +} + +static void bcs_unsupported(const char *message) { + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); +} + +/* fn.call(self, ...args): +1 or NULL. */ +static ScrDyn *bcs_apply(ScrDyn *fn, ScrDyn *self, ScrDyn *const *args, size_t argc, const char *what) { + scr_dyn_this_push_dyn(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, what); + scr_dyn_this_pop(); + return result; +} + +/* An error argument of a completion callback or destroy(): NULL for a + * falsy value (Node tests `if (err)`), else the runtime error that boxes + * back to the same JavaScript value (+1). */ +static ScrError *bcs_error_of(const ScrDyn *value) { + if (!scr_dyn_truthy(value)) return NULL; + if (value->kind == SCR_DYN_STR) return scr_error_new(SCR_ERR_ERROR, value->v.str); + return scr_error_from_dyn(value); +} + +/* ── the hidden slot ────────────────────────────────────────────────── */ + +static void *bcs_capsule_retain(void *p) { return scr_stream_retain(p); } +/* Also the capsule's tag. */ +static void bcs_capsule_release(void *p) { scr_stream_release(p); } + +static bool bcs_stream_vt(const ScrVt *vt) { + return (vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post); +} + +static void bcs_rec_release(void *p); + +/* The stream of a constructor-function stream or one of its state views, + * or of a typed capsule of such a stream (a typed stream value that + * crossed back into JavaScript), borrowed; NULL for other values. */ +static ScrStream *bcs_of(const ScrDyn *value) { + if (value != NULL && value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && + memcmp(value->v.typed_ref.type_key, "object:", 7) == 0) { + ScrStream *stream = value->v.typed_ref.ptr; + return bcs_stream_vt(stream->vt) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL; + } + if (value == NULL || value->kind != SCR_DYN_OBJ || value->symbol_properties == NULL) return NULL; + ScrDyn *capsule = scr_dyn_obj_get(value->symbol_properties, bcs_hidden_key, BCS_HIDDEN_LEN); + if (capsule == NULL || capsule->kind != SCR_DYN_TYPED_REF || + capsule->v.typed_ref.release != bcs_capsule_release) + return NULL; + return capsule->v.typed_ref.ptr; +} + +/* Stores the stream in the object's symbol table under a storage name no + * symbol produces (symbols store under their address), outside the key + * list, so no reflection reaches it. */ +static void bcs_hide(ScrDyn *object, ScrStream *s) { + scr_dyn_region_store(object, NULL); + if (!object->symbol_properties) object->symbol_properties = scr_dyn_new_obj_null_proto(); + if (!object->symbol_keys) object->symbol_keys = scr_dyn_new_arr(); + ScrDyn *capsule = scr_dyn_new_typed_ref_traced(s, bcs_capsule_retain, bcs_capsule_release, + bcs_hidden_key, BCS_HIDDEN_LEN, + scr_dyn_class_view_unavailable, NULL); + scr_dyn_obj_set(object->symbol_properties, bcs_hidden_key, BCS_HIDDEN_LEN, capsule); +} + +/* A runtime stream boxed as a typed capsule (a typed stream value that + * crossed into JavaScript): +1, or NULL. */ +static ScrStream *bcs_native_of(const ScrDyn *value) { + if (value->kind != SCR_DYN_TYPED_REF || value->v.typed_ref.type_key_len <= 7 || + memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) + return NULL; + ScrStream *stream = scr_dyn_typed_ref_unbox(value); + if (bcs_stream_vt(stream->vt)) return stream; + value->v.typed_ref.release(stream); + return NULL; +} + +/* Any stream a JavaScript value stands for (+1), or NULL. */ +ScrStream *scr_dyn_builtin_stream_of(const ScrDyn *value) { + ScrStream *own = bcs_of(value); + if (own) return scr_stream_retain(own); + return bcs_native_of(value); +} + +/* The JavaScript value of a runtime stream: the constructor-function + * object it belongs to (+1), or NULL for other streams. */ +ScrDyn *scr_dyn_builtin_stream_object(ScrStream *s) { + BcsRec *rec = bcs_rec(s); + return rec ? scr_dyn_retain(rec->object) : NULL; +} + +/* A stream as a listener argument: its constructor-function object, or a + * live view of another runtime stream. +1. */ +static ScrDyn *bcs_stream_value(ScrStream *s) { + ScrDyn *object = scr_dyn_builtin_stream_object(s); + return object ? object : scr_stream_dyn_view(s); +} + +/* The receiver's stream for a prototype method, borrowed; throws Node's + * TypeError (reading the state of a non-stream) and answers NULL. */ +static ScrStream *bcs_need(ScrDyn *self, const char *reading) { + ScrStream *s = bcs_of(self); + if (s) return s; + char message[128]; + if (bcs_nullish(self)) { + snprintf(message, sizeof message, "Cannot read properties of %s (reading '%s')", + self->kind == SCR_DYN_NULL ? "null" : "undefined", reading); + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); + } else { + snprintf(message, sizeof message, + "stream method '%s' on an object without stream state is not supported yet", reading); + bcs_unsupported(message); + } + return NULL; +} + +/* ── completion callbacks ───────────────────────────────────────────── */ + +enum { BCS_DONE_WRITE, BCS_DONE_FINAL, BCS_DONE_DESTROY, BCS_DONE_TRANSFORM, BCS_DONE_FLUSH }; + +static void bcs_run_destroy_cbs(ScrStream *s, ScrError *err) { + BcsRec *rec = bcs_rec(s); + if (!rec || rec->destroy_n == 0) return; + ScrDyn **cbs = rec->destroy_cbs; + size_t n = rec->destroy_n; + rec->destroy_cbs = NULL; + rec->destroy_n = rec->destroy_cap = 0; + ScrDyn *arg = err ? scr_dyn_from_error(err) : scr_dyn_new_null(); + ScrDyn *object = rec->object; + scr_dyn_retain(object); + for (size_t i = 0; i < n; i++) { + if (!scr_exc_pending()) { + ScrDyn *result = bcs_apply(cbs[i], object, &arg, 1, "callback"); + if (result) scr_dyn_release(result); + } + scr_dyn_release(cbs[i]); + } + free(cbs); + scr_dyn_release(object); + scr_dyn_release(arg); +} + +static ScrDyn *bcs_done_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrStream *s = scr_box_get_ref(closure->caps[0]); + int which = (int)scr_box_get_f64(closure->caps[1]); + ScrError *err = bcs_error_of(bcs_arg(args, argc, 0)); + switch (which) { + case BCS_DONE_WRITE: + scr_stream_write_done(s, err); + break; + case BCS_DONE_FINAL: + scr_stream_final_done(s, err); + break; + case BCS_DONE_DESTROY: { + ScrError *kept = err ? scr_error_retain(err) : NULL; + scr_stream_destroy_done(s, err); + if (!scr_exc_pending()) bcs_run_destroy_cbs(s, kept); + if (kept) scr_error_release(kept); + break; + } + default: { + ScrDyn *data = bcs_arg(args, argc, 1); + ScrBytes *bytes = data->kind == SCR_DYN_BYTES ? scr_bytes_raw_view(data->v.bytes) : NULL; + ScrStr *text = data->kind == SCR_DYN_STR ? scr_str_retain(data->v.str) : NULL; + if (which == BCS_DONE_TRANSFORM) scr_stream_transform_done(s, err, bytes, text); + else scr_stream_flush_done(s, err, bytes, text); + break; + } + } + scr_stream_release(s); + if (scr_exc_pending()) return NULL; + return bcs_undefined(); +} + +static ScrDyn *bcs_done(ScrStream *s, int which) { + ScrClosure *closure = scr_closure_new((void *)bcs_done_call, 2); + closure->caps[0] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); + scr_box_set_ref(closure->caps[0], scr_stream_retain(s)); + closure->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[1], which); + uint32_t arity = which == BCS_DONE_TRANSFORM || which == BCS_DONE_FLUSH ? 2 : 1; + return scr_dyn_new_func(closure, bcs_done_call, arity, "native:stream", ""); +} + +/* ── the hooks: the runtime calls the receiver's underscore methods ─── */ + +static ScrBox *bcs_box(ScrDyn *value /* moves in */) { + ScrBox *box = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(box, value); + return box; +} + +static ScrClosure *bcs_hook(ScrDyn *object, void *inv) { + ScrClosure *closure = scr_closure_new(inv, 1); + closure->caps[0] = bcs_box(scr_dyn_retain(object)); + return closure; +} + +/* receiver[method](...args) with the receiver as `this`: +1 or NULL. */ +static ScrDyn *bcs_invoke(ScrDyn *object, const char *method, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = bcs_get(object, method); + if (!fn) return NULL; + ScrDyn *result = NULL; + if (scr_dyn_is_callable(fn)) { + result = bcs_apply(fn, object, args, argc, method); + } else { + char message[96]; + snprintf(message, sizeof message, "this.%s is not a function", method); + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); + } + scr_dyn_release(fn); + return result; +} + +/* The thrown value of a hook Node calls inside try/catch, as the runtime + * error its catch hands on (+1); clears the exception. */ +static ScrError *bcs_take_error(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *value = scr_caught_to_dyn(caught); + scr_caught_release(caught); + ScrError *err = NULL; + if (value) { + err = scr_dyn_truthy(value) ? bcs_error_of(value) : NULL; + scr_dyn_release(value); + } + if (!err) { + ScrStr *empty = scr_str_new("", 0); + err = scr_error_new(SCR_ERR_ERROR, empty); + scr_str_release(empty); + } + return err; +} + +/* Readable.read: `try { this._read(hwm) } catch (err) { errorOrDestroy }` */ +static void bcs_read_inv(ScrClosure *cb, ScrStream *s, double size) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *n = scr_dyn_new_num(size); + ScrDyn *result = bcs_invoke(object, "_read", &n, 1); + scr_dyn_release(n); + if (result) scr_dyn_release(result); + else if (scr_exc_pending()) { + ScrError *err = bcs_take_error(); + scr_stream_fail(s, err); + scr_error_release(err); + } + scr_dyn_release(object); +} + +/* _write(chunk, 'buffer', cb) and _transform(chunk, 'buffer', cb): a + * throw propagates to the write() caller, as in Node. */ +static void bcs_chunk_call(ScrClosure *cb, ScrStream *s, ScrBytes *chunk, const char *method, int done) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *args[3]; + args[0] = scr_dyn_new_buffer(chunk); + args[1] = bcs_str("buffer"); + args[2] = bcs_done(s, done); + ScrDyn *result = bcs_invoke(object, method, args, 3); + for (int i = 0; i < 3; i++) scr_dyn_release(args[i]); + if (result) scr_dyn_release(result); + scr_dyn_release(object); +} + +static void bcs_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + bcs_chunk_call(cb, s, chunk, "_write", BCS_DONE_WRITE); +} + +static void bcs_transform_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + bcs_chunk_call(cb, s, chunk, "_transform", BCS_DONE_TRANSFORM); +} + +/* callFinal: `try { stream._final(onFinish) } catch (err) { onFinish(err) }`; + * a Transform's _flush runs inside the same try. */ +static void bcs_plain_call(ScrClosure *cb, ScrStream *s, const char *method, int done) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *callback = bcs_done(s, done); + ScrDyn *result = bcs_invoke(object, method, &callback, 1); + scr_dyn_release(callback); + if (result) scr_dyn_release(result); + else if (scr_exc_pending()) { + ScrError *err = bcs_take_error(); + if (done == BCS_DONE_FINAL) scr_stream_final_done(s, err); + else scr_stream_flush_done(s, err, NULL, NULL); + } + scr_dyn_release(object); +} + +static void bcs_final_inv(ScrClosure *cb, ScrStream *s) { + bcs_plain_call(cb, s, "_final", BCS_DONE_FINAL); +} + +static void bcs_flush_inv(ScrClosure *cb, ScrStream *s) { + bcs_plain_call(cb, s, "_flush", BCS_DONE_FLUSH); +} + +static ScrDyn *bcs_default_method(int kind, const char *name); + +/* _destroy(err || null, onDestroy) inside try/catch. The prototype's + * default (cb(err)) completes without a call. */ +static void bcs_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *fn = bcs_get(object, "_destroy"); + ScrDyn *fallback = bcs_default_method(BCS_READABLE, "_destroy"); + if (fn && fallback && scr_dyn_strict_eq(fn, fallback)) { + ScrError *kept = err ? scr_error_retain(err) : NULL; + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); + if (!scr_exc_pending()) bcs_run_destroy_cbs(s, kept); + if (kept) scr_error_release(kept); + } else if (fn) { + ScrDyn *args[2]; + args[0] = err ? scr_dyn_from_error(err) : scr_dyn_new_null(); + args[1] = bcs_done(s, BCS_DONE_DESTROY); + ScrDyn *result = NULL; + if (scr_dyn_is_callable(fn)) { + result = bcs_apply(fn, object, args, 2, "_destroy"); + } else { + static const char message[] = "this._destroy is not a function"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); + if (result) scr_dyn_release(result); + else if (scr_exc_pending()) { + ScrError *thrown = bcs_take_error(); + ScrError *kept = scr_error_retain(thrown); + scr_stream_destroy_done(s, thrown); + if (!scr_exc_pending()) bcs_run_destroy_cbs(s, kept); + scr_error_release(kept); + } + } + if (fallback) scr_dyn_release(fallback); + if (fn) scr_dyn_release(fn); + scr_dyn_release(object); +} + +/* ── listeners: the runtime emitter, the receiver as `this` ─────────── */ + +enum { BCS_L_FN, BCS_L_SELF, BCS_L_CAPS }; + +static void bcs_listener_call(ScrClosure *adapter, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = scr_box_get_ref(adapter->caps[BCS_L_FN]); + ScrDyn *self = scr_box_get_ref(adapter->caps[BCS_L_SELF]); + ScrDyn *result = bcs_apply(fn, self, args, argc, "listener"); + if (result) scr_dyn_release(result); + scr_dyn_release(self); + scr_dyn_release(fn); +} + +static void bcs_listener0(ScrClosure *adapter) { + bcs_listener_call(adapter, NULL, 0); +} + +static void bcs_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *text) { + ScrDyn *arg = text ? scr_dyn_new_str(text) : scr_dyn_new_buffer(bytes); + bcs_listener_call(adapter, &arg, 1); + scr_dyn_release(arg); +} + +static void bcs_listener_error(ScrClosure *adapter, ScrError *error) { + ScrDyn *arg = scr_dyn_from_error(error); + bcs_listener_call(adapter, &arg, 1); + scr_dyn_release(arg); +} + +static void bcs_listener_stream(ScrClosure *adapter, ScrStream *stream) { + ScrDyn *arg = bcs_stream_value(stream); + bcs_listener_call(adapter, &arg, 1); + scr_dyn_release(arg); +} + +static void bcs_listener_name(ScrClosure *adapter, ScrStr *name) { + ScrDyn *arg = scr_dyn_new_str(name); + bcs_listener_call(adapter, &arg, 1); + scr_dyn_release(arg); +} + +/* Computed events: the listener's checked-dynamic call, with every + * argument emit passed. */ +static ScrDyn *bcs_listener_flex(ScrClosure *adapter, ScrDyn *const *args, size_t argc) { + bcs_listener_call(adapter, args, argc); + if (scr_exc_pending()) return NULL; + return bcs_undefined(); +} + +static ScrClosure *bcs_adapter(ScrDyn *fn, ScrDyn *self, void *call) { + ScrClosure *adapter = scr_closure_new(call, BCS_L_CAPS); + adapter->caps[BCS_L_FN] = bcs_box(scr_dyn_retain(fn)); + adapter->caps[BCS_L_SELF] = bcs_box(scr_dyn_retain(self)); + scr_dyn_adopt_identity(adapter, fn); + return adapter; +} + +static bool bcs_is(const ScrStr *name, const char *text) { + size_t length = strlen(text); + return name->len == length && memcmp(name->data, text, length) == 0; +} + +/* The runtime's fixed event tuples: 0 none, 1 data, 2 error, 3 a stream + * (pipe/unpipe), 4 an event name (the meta events); -1 computed. */ +static int bcs_event_shape(const ScrStr *name) { + if (bcs_is(name, "data")) return 1; + if (bcs_is(name, "error")) return 2; + if (bcs_is(name, "pipe") || bcs_is(name, "unpipe")) return 3; + if (bcs_is(name, "newListener") || bcs_is(name, "removeListener")) return 4; + static const char *const empty[] = { + "end", "close", "finish", "drain", "readable", "resume", "pause", "prefinish", + }; + for (size_t i = 0; i < sizeof empty / sizeof *empty; i++) + if (bcs_is(name, empty[i])) return 0; + return -1; +} + +/* The event name argument as a string, or NULL with an exception + * pending. */ +static ScrStr *bcs_event_name(const ScrDyn *type) { + if (type->kind == SCR_DYN_STR) return scr_str_retain(type->v.str); + if (type->kind == SCR_DYN_SYMBOL) { + bcs_unsupported("symbol event names on a constructor-function stream are not supported yet"); + return NULL; + } + return scr_dyn_string_coerce_js(type); +} + +static bool bcs_check_listener(const ScrDyn *listener) { + if (listener->kind == SCR_DYN_FUNC) return true; + if (scr_dyn_is_callable(listener)) { + bcs_unsupported("listeners other than functions on a constructor-function stream are not supported yet"); + return false; + } + scr_dyn_arg_type_fail("listener", "of type function", listener); + return false; +} + +static bool bcs_add_listener(ScrStream *s, ScrDyn *self, ScrDyn *type, ScrDyn *listener, bool once, + bool prepend) { + if (!bcs_check_listener(listener)) return false; + ScrStr *name = bcs_event_name(type); + if (!name) return false; + ScrEmitter *em = (ScrEmitter *)s; + ScrEmitter *chained = NULL; + switch (bcs_event_shape(name)) { + case 0: + chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener0), + scr_ee_inv_fixed0, once, prepend); + break; + case 1: + chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_data), + scr_ee_inv_fixed2, once, prepend); + break; + case 2: + chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_error), + scr_ee_inv_fixed1, once, prepend); + break; + case 3: + chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_stream), + scr_ee_inv_fixed1, once, prepend); + break; + case 4: + chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_name), + scr_ee_inv_fixed1, once, prepend); + break; + default: { + ScrClosure *adapter = bcs_adapter(listener, self, (void *)bcs_listener_flex); + ScrDyn *wrapper = scr_dyn_new_func(adapter, bcs_listener_flex, 0, "native:events", "listener"); + chained = scr_emitter_on_flex(em, name, wrapper, once, prepend); + scr_dyn_release(wrapper); + break; + } + } + if (chained) scr_emitter_release(chained); + scr_str_release(name); + return !scr_exc_pending(); +} + +/* this.emit(type, ...args) on the runtime emitter: the fixed tuples the + * runtime's own listeners read, computed events with every argument. */ +static ScrDyn *bcs_emit(ScrStream *s, ScrDyn *const *args, size_t argc) { + ScrStr *name = bcs_event_name(bcs_arg(args, argc, 0)); + if (!name) return NULL; + ScrEmitter *em = (ScrEmitter *)s; + ScrDyn *const *rest = argc > 1 ? args + 1 : NULL; + size_t restc = argc > 1 ? argc - 1 : 0; + ScrDyn *first = restc ? rest[0] : scr_dyn_undefined(); + bool had = false; + switch (bcs_event_shape(name)) { + case 0: + had = scr_emitter_emit(em, name); + break; + case 1: + if (first->kind == SCR_DYN_BYTES) { + ScrBytes *bytes = scr_bytes_raw_view(first->v.bytes); + had = scr_emitter_emit(em, name, bytes, (ScrStr *)NULL); + scr_bytes_release(bytes); + } else if (first->kind == SCR_DYN_STR) { + had = scr_emitter_emit(em, name, (ScrBytes *)NULL, first->v.str); + } else { + bcs_unsupported("emit('data') with a chunk other than a Buffer or string on a constructor-function stream is not supported yet"); + } + break; + case 2: + if (!scr_emitter_has(em, "error")) { + /* Node's unhandled 'error': an Error throws itself, anything else + * ERR_UNHANDLED_ERROR. */ + if (scr_dyn_err_instanceof(first, SCR_ERR_ERROR)) { + scr_dyn_throw(scr_dyn_retain(first)); + } else { + ScrStr *shown = scr_insp_dyn(first, 0, 2); + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "Unhandled error. ("); + scr_jb_put_str(&text, shown); + scr_jb_putc(&text, ')'); + scr_str_release(shown); + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "ERR_UNHANDLED_ERROR"); + scr_str_release(message); + } + } else { + ScrError *err = bcs_error_of(first); + if (!err) { + ScrStr *empty = scr_str_new("", 0); + err = scr_error_new(SCR_ERR_ERROR, empty); + scr_str_release(empty); + } + had = scr_emitter_emit_error(em, name, err); + scr_error_release(err); + } + break; + case 3: { + ScrStream *stream = scr_dyn_builtin_stream_of(first); + if (stream) { + had = scr_emitter_emit(em, name, stream); + scr_stream_release(stream); + } else { + bcs_unsupported("emit('pipe') or emit('unpipe') with a value other than a stream on a constructor-function stream is not supported yet"); + } + break; + } + case 4: + if (first->kind == SCR_DYN_STR) had = scr_emitter_emit(em, name, first->v.str); + else bcs_unsupported("emit('newListener') or emit('removeListener') with a name other than a string on a constructor-function stream is not supported yet"); + break; + default: + had = scr_emitter_emit_flex(em, name, rest, restc); + break; + } + scr_str_release(name); + if (scr_exc_pending()) return NULL; + return scr_dyn_new_bool(had); +} + +static ScrDyn *bcs_event_names(ScrStream *s) { + ScrArr *names = scr_emitter_event_names((ScrEmitter *)s); + ScrDyn *result = scr_dyn_new_arr(); + double length = scr_arr_len(names); + for (double i = 0; i < length; i++) { + ScrStr *name = scr_arr_get_ref(names, i); + scr_dyn_arr_push(result, scr_dyn_new_str(name)); + scr_str_release(name); + } + scr_arr_release(names); + return result; +} + +/* EventEmitter.prototype's methods on a constructor-function stream + * (called by scr_dyn_builtin_class.c): true when self has a stream. */ +static bool bcs_event(int method, ScrDyn *self, ScrDyn *const *args, size_t argc, ScrDyn **result) { + ScrStream *s = bcs_of(self); + if (!s) return false; + ScrDyn *type = bcs_arg(args, argc, 0); + ScrEmitter *em = (ScrEmitter *)s; + ScrDyn *out = NULL; + switch (method) { + case SCR_EE_M_ON: + case SCR_EE_M_PREPEND: + case SCR_EE_M_ONCE: + case SCR_EE_M_PREPEND_ONCE: + if (bcs_add_listener(s, self, type, bcs_arg(args, argc, 1), + method == SCR_EE_M_ONCE || method == SCR_EE_M_PREPEND_ONCE, + method == SCR_EE_M_PREPEND || method == SCR_EE_M_PREPEND_ONCE)) + out = scr_dyn_retain(self); + break; + case SCR_EE_M_OFF: { + ScrDyn *listener = bcs_arg(args, argc, 1); + if (!bcs_check_listener(listener)) break; + ScrStr *name = bcs_event_name(type); + if (!name) break; + scr_emitter_release(scr_emitter_off_dyn(em, name, listener)); + scr_str_release(name); + if (!scr_exc_pending()) out = scr_dyn_retain(self); + break; + } + case SCR_EE_M_REMOVE_ALL: { + ScrStr *name = argc == 0 || type->kind == SCR_DYN_UNDEF ? NULL : bcs_event_name(type); + if (argc > 0 && type->kind != SCR_DYN_UNDEF && !name) break; + if (!name) name = scr_str_new("", 0); + scr_emitter_release(scr_emitter_remove_all(em, name, argc == 0 || type->kind == SCR_DYN_UNDEF)); + scr_str_release(name); + if (!scr_exc_pending()) out = scr_dyn_retain(self); + break; + } + case SCR_EE_M_EMIT: + out = bcs_emit(s, args, argc); + break; + case SCR_EE_M_LISTENER_COUNT: { + ScrStr *name = bcs_event_name(type); + if (!name) break; + ScrDyn *only = bcs_arg(args, argc, 1); + double count = bcs_nullish(only) ? scr_emitter_listener_count(em, name) + : only->kind == SCR_DYN_FUNC ? scr_emitter_listener_count_dyn(em, name, only) : 0; + scr_str_release(name); + if (!scr_exc_pending()) out = scr_dyn_new_num(count); + break; + } + case SCR_EE_M_EVENT_NAMES: + out = bcs_event_names(s); + break; + case SCR_EE_M_SET_MAX: + scr_emitter_release(scr_emitter_set_max(em, bcs_arg(args, argc, 0)->v.num)); + out = scr_dyn_retain(self); + break; + case SCR_EE_M_GET_MAX: + out = scr_dyn_new_num(scr_emitter_get_max(em)); + break; + default: + bcs_unsupported("listeners() and rawListeners() on a constructor-function stream are not supported yet"); + break; + } + *result = scr_exc_pending() ? (out ? (scr_dyn_release(out), NULL) : NULL) : out; + return true; +} + +/* ── construction ───────────────────────────────────────────────────── */ + +static bool bcs_is_function(ScrDyn *object, const char *name, bool *failed) { + ScrDyn *value = bcs_get(object, name); + if (!value) { + *failed = true; + return false; + } + bool callable = scr_dyn_is_callable(value); + scr_dyn_release(value); + return callable; +} + +/* Node's constructors copy these options onto the instance. */ +static bool bcs_copy_option(ScrDyn *self, ScrDyn *opts, const char *option, const char *property) { + ScrDyn *value = bcs_get(opts, option); + if (!value) return false; + if (scr_dyn_is_callable(value)) return bcs_set(self, property, value); + scr_dyn_release(value); + return true; +} + +static void bcs_class_name(ScrDyn *self, char *out, size_t size) { + snprintf(out, size, "%s", "Object"); + ScrDyn *ctor = bcs_get(self, "constructor"); + if (!ctor) { + scr_exc_clear(); + return; + } + if (scr_dyn_is_callable(ctor)) { + ScrDyn *name = bcs_get(ctor, "name"); + if (name && name->kind == SCR_DYN_STR && name->v.str->len > 0) + snprintf(out, size, "%.*s", (int)name->v.str->len, name->v.str->data); + if (name) scr_dyn_release(name); + else scr_exc_clear(); + } + scr_dyn_release(ctor); +} + +static ScrDyn *bcs_state_proto(bool readable); + +/* Listeners added before the constructor ran (bl registers its 'pipe' + * listener first) sit in the instance's own _events, where Node's + * constructor keeps them: they move to the runtime emitter, in order. */ +static bool bcs_adopt_listeners(ScrStream *s, ScrDyn *self) { + ScrDyn *events = bcs_get(self, "_events"); + if (!events) return false; + if (events->kind != SCR_DYN_OBJ) { + scr_dyn_release(events); + return true; + } + ScrDyn *keys = scr_dyn_own_keys(events); + bool ok = keys != NULL; + size_t count = ok ? (size_t)scr_dyn_arr_len(keys) : 0; + for (size_t i = 0; ok && i < count; i++) { + ScrDyn *key = scr_dyn_index_get(keys, (double)i); + ScrDyn *list = key ? scr_dyn_reflect_get(events, key, events) : NULL; + if (!list) { + if (key) scr_dyn_release(key); + ok = false; + break; + } + if (list->kind == SCR_DYN_ARR) { + size_t length = (size_t)scr_dyn_arr_len(list); + for (size_t j = 0; ok && j < length; j++) { + ScrDyn *listener = scr_dyn_index_get(list, (double)j); + ok = listener && bcs_add_listener(s, self, key, listener, false, false); + if (listener) scr_dyn_release(listener); + } + } else if (scr_dyn_is_callable(list)) { + ok = bcs_add_listener(s, self, key, list, false, false); + } + scr_dyn_release(list); + scr_dyn_release(key); + } + if (keys) scr_dyn_release(keys); + scr_dyn_release(events); + if (ok && count > 0) + ok = bcs_set(self, "_events", scr_dyn_new_obj_null_proto()) && bcs_set(self, "_eventsCount", scr_dyn_new_num(0)); + return ok; +} + +static bool bcs_state_view(ScrDyn *self, ScrStream *s, bool readable) { + ScrDyn *view = scr_dyn_obj_create(bcs_state_proto(readable)); + bcs_hide(view, s); + return bcs_set(self, readable ? "_readableState" : "_writableState", view); +} + +static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t argc) { + static const char *const names[] = { "", "", "Readable", "Writable", "Duplex", "Transform", "PassThrough" }; + char message[192]; + if (self->kind != SCR_DYN_OBJ || !scr_dyn_builtin_instanceof(self, kind)) { + if (scr_exc_pending()) return NULL; + snprintf(message, sizeof message, + "%s.call() on a receiver that does not inherit %s.prototype is not supported yet", + names[kind], names[kind]); + bcs_unsupported(message); + return NULL; + } + if (bcs_of(self)) { + snprintf(message, sizeof message, + "%s.call() on a receiver that already has stream state is not supported yet", names[kind]); + bcs_unsupported(message); + return NULL; + } + bool failed = false; + bool writes = kind != BCS_READABLE; + if (writes && bcs_is_function(self, "_writev", &failed)) { + bcs_unsupported("_writev on a constructor-function stream is not supported yet"); + return NULL; + } + if (!failed && bcs_is_function(self, "_construct", &failed)) { + bcs_unsupported("_construct on a constructor-function stream is not supported yet"); + return NULL; + } + if (failed) return NULL; + ScrDyn *opts = bcs_arg(args, argc, 0); + ScrStream *s = kind == BCS_READABLE ? scr_stream_new_readable_dyn(opts) + : kind == BCS_WRITABLE ? scr_stream_new_writable_dyn(opts) + : kind == BCS_DUPLEX ? scr_stream_new_duplex_dyn(opts) + : kind == BCS_TRANSFORM ? scr_stream_new_transform_dyn(opts) + : scr_stream_new_passthrough_dyn(opts); + if (!s) return NULL; + bool ok = true; + if (opts->kind == SCR_DYN_OBJ) { + bool transformish = kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH; + if (!transformish && kind != BCS_WRITABLE) ok = ok && bcs_copy_option(self, opts, "read", "_read"); + if (!transformish && kind != BCS_READABLE) { + ok = ok && bcs_copy_option(self, opts, "write", "_write"); + ok = ok && bcs_copy_option(self, opts, "final", "_final"); + } + ok = ok && bcs_copy_option(self, opts, "destroy", "_destroy"); + if (transformish) { + ok = ok && bcs_copy_option(self, opts, "transform", "_transform"); + ok = ok && bcs_copy_option(self, opts, "flush", "_flush"); + } + } + if (ok && kind >= BCS_DUPLEX) + ok = bcs_set(self, "allowHalfOpen", scr_dyn_new_bool(scr_stream_prop(s, "allowHalfOpen") != 0)); + if (!ok) { + scr_stream_release(s); + return NULL; + } + BcsRec *rec = calloc(1, sizeof *rec); + if (!rec) scr_trap("out of memory"); + rec->object = self; + bcs_class_name(self, rec->cls, sizeof rec->cls); + scr_stream_set_native(s, rec, bcs_rec_release); + s->cls = rec->cls; + /* The runtime calls the instance's underscore methods. */ + if (kind == BCS_READABLE || kind == BCS_DUPLEX) + scr_stream_set_read(s, bcs_hook(self, (void *)bcs_read_inv), bcs_read_inv); + if (kind == BCS_WRITABLE || kind == BCS_DUPLEX) { + scr_stream_set_write(s, bcs_hook(self, (void *)bcs_write_inv), bcs_write_inv); + if (bcs_is_function(self, "_final", &failed)) scr_stream_set_final(s, bcs_hook(self, (void *)bcs_final_inv), bcs_final_inv); + else scr_stream_set_final(s, NULL, NULL); + } + if (kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH) { + bool identity = false; + if (kind == BCS_PASSTHROUGH) { + ScrDyn *own = bcs_get(self, "_transform"); + ScrDyn *fallback = bcs_default_method(BCS_PASSTHROUGH, "_transform"); + identity = own && fallback && scr_dyn_strict_eq(own, fallback); + if (own) scr_dyn_release(own); + else failed = true; + if (fallback) scr_dyn_release(fallback); + } + if (identity) scr_stream_set_transform(s, NULL, NULL); + else scr_stream_set_transform(s, bcs_hook(self, (void *)bcs_transform_inv), bcs_transform_inv); + if (bcs_is_function(self, "_flush", &failed)) scr_stream_set_flush(s, bcs_hook(self, (void *)bcs_flush_inv), bcs_flush_inv); + else scr_stream_set_flush(s, NULL, NULL); + if (!failed && bcs_is_function(self, "_final", &failed)) { + bcs_unsupported("_final on a constructor-function Transform is not supported yet"); + failed = true; + } + } + scr_stream_set_destroy(s, bcs_hook(self, (void *)bcs_destroy_inv), bcs_destroy_inv); + bcs_hide(self, s); + if (!failed) failed = !bcs_adopt_listeners(s, self); + if (!failed && kind != BCS_WRITABLE) failed = !bcs_state_view(self, s, true); + if (!failed && kind != BCS_READABLE) failed = !bcs_state_view(self, s, false); + scr_stream_release(s); /* the capsule holds it */ + if (failed || scr_exc_pending()) return NULL; + return bcs_undefined(); +} + +/* ── prototype methods ──────────────────────────────────────────────── */ + +/* Node's encoding names, canonical; NULL with ERR_UNKNOWN_ENCODING. */ +static ScrStr *bcs_encoding(const ScrDyn *value) { + static const struct { const char *from, *to; } map[] = { + {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, {"base64", "base64"}, + {"base64url", "base64url"}, {"latin1", "latin1"}, {"binary", "latin1"}, + {"ascii", "ascii"}, {"utf16le", "utf16le"}, {"utf-16le", "utf16le"}, + {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, + }; + ScrStr *raw = value->kind == SCR_DYN_STR ? scr_str_retain(value->v.str) : scr_dyn_string_coerce_js(value); + if (!raw) return NULL; + for (size_t i = 0; i < sizeof map / sizeof *map; i++) { + if (bcs_is(raw, map[i].from)) { + scr_str_release(raw); + return scr_str_new(map[i].to, strlen(map[i].to)); + } + } + char message[128]; + snprintf(message, sizeof message, "Unknown encoding: %.*s", (int)(raw->len < 64 ? raw->len : 64), raw->data); + scr_str_release(raw); + scr_throw_error_msg_code(SCR_ERR_TYPE, message, strlen(message), "ERR_UNKNOWN_ENCODING"); + return NULL; +} + +enum { + BCS_M_PUSH, BCS_M_UNSHIFT, BCS_M_READ, BCS_M_SET_ENCODING, BCS_M_PAUSE, BCS_M_RESUME, + BCS_M_IS_PAUSED, BCS_M_PIPE, BCS_M_UNPIPE, BCS_M_DESTROY, BCS_M_WRITE, BCS_M_END, + BCS_M_CORK, BCS_M_UNCORK, BCS_M_SET_DEFAULT_ENCODING, BCS_M_WRITABLE_PIPE, BCS_M_WRAP, + BCS_M_READ_DEFAULT, BCS_M_WRITE_DEFAULT, BCS_M_TRANSFORM_DEFAULT, BCS_M_DESTROY_DEFAULT, + BCS_M_PASSTHROUGH_TRANSFORM, BCS_M_ON, BCS_M_OFF, BCS_M_REMOVE_ALL, +}; + +enum { BCS_CB_STREAM = BCS_L_CAPS, BCS_CB_CAPS }; + +/* A write or end callback: null after a successful write and at 'finish'; + * once the stream failed, its error, or Node's ERR_STREAM_DESTROYED for a + * write a destroy cut off. */ +static void bcs_callback_fire(ScrClosure *closure) { + ScrDyn *fn = scr_box_get_ref(closure->caps[BCS_L_FN]); + ScrDyn *self = scr_box_get_ref(closure->caps[BCS_L_SELF]); + ScrStream *s = scr_box_get_ref(closure->caps[BCS_CB_STREAM]); + ScrError *err = scr_stream_errored(s); + ScrDyn *arg; + if (err) { + arg = scr_dyn_from_error(err); + scr_error_release(err); + } else if (scr_stream_prop(s, "destroyed") != 0) { + ScrStr *message = scr_str_new("Cannot call write after a stream was destroyed", 46); + ScrError *destroyed = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_error_set_code(destroyed, "ERR_STREAM_DESTROYED"); + arg = scr_dyn_from_error(destroyed); + scr_error_release(destroyed); + } else { + arg = scr_dyn_new_null(); + } + scr_stream_release(s); + ScrDyn *result = bcs_apply(fn, self, &arg, 1, "callback"); + if (result) scr_dyn_release(result); + scr_dyn_release(arg); + scr_dyn_release(self); + scr_dyn_release(fn); +} + +static ScrClosure *bcs_callback(ScrDyn *fn, ScrDyn *self, ScrStream *s) { + if (!scr_dyn_is_callable(fn)) return NULL; + ScrClosure *closure = scr_closure_new((void *)bcs_callback_fire, BCS_CB_CAPS); + closure->caps[BCS_L_FN] = bcs_box(scr_dyn_retain(fn)); + closure->caps[BCS_L_SELF] = bcs_box(scr_dyn_retain(self)); + closure->caps[BCS_CB_STREAM] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); + scr_box_set_ref(closure->caps[BCS_CB_STREAM], scr_stream_retain(s)); + return closure; +} + +/* Strings in another encoding than utf8 have no write path here. */ +static bool bcs_utf8_only(const ScrDyn *encoding) { + if (encoding->kind != SCR_DYN_STR) return true; + ScrStr *canonical = bcs_encoding(encoding); + if (!canonical) return false; + bool utf8 = bcs_is(canonical, "utf8"); + scr_str_release(canonical); + if (!utf8) bcs_unsupported("writing strings in an encoding other than utf8 to a constructor-function stream is not supported yet"); + return utf8; +} + +static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t argc) { + ScrDyn *chunk = bcs_arg(args, argc, 0); + ScrDyn *encoding = bcs_arg(args, argc, 1); + ScrDyn *cb = bcs_arg(args, argc, 2); + if (scr_dyn_is_callable(encoding)) { + cb = encoding; + encoding = scr_dyn_undefined(); + } + if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; + bool below = scr_stream_write_dyn(s, chunk, bcs_callback(cb, self, s)); + if (scr_exc_pending()) return NULL; + return scr_dyn_new_bool(below); +} + +static ScrDyn *bcs_end(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t argc) { + ScrDyn *chunk = bcs_arg(args, argc, 0); + ScrDyn *encoding = bcs_arg(args, argc, 1); + ScrDyn *cb = bcs_arg(args, argc, 2); + if (scr_dyn_is_callable(chunk)) { + cb = chunk; + chunk = scr_dyn_undefined(); + encoding = scr_dyn_undefined(); + } else if (scr_dyn_is_callable(encoding)) { + cb = encoding; + encoding = scr_dyn_undefined(); + } + if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; + ScrStream *chained = NULL; + if (bcs_nullish(chunk)) { + chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); + } else if (chunk->kind == SCR_DYN_STR) { + chained = scr_stream_end(s, NULL, chunk->v.str, bcs_callback(cb, self, s)); + } else if (chunk->kind == SCR_DYN_BYTES) { + ScrBytes *bytes = scr_bytes_raw_view(chunk->v.bytes); + chained = scr_stream_end(s, bytes, NULL, bcs_callback(cb, self, s)); + scr_bytes_release(bytes); + } else { + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", chunk); + } + if (chained) scr_stream_release(chained); + if (scr_exc_pending()) return NULL; + return scr_dyn_retain(self); +} + +static ScrDyn *bcs_destroy(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t argc) { + ScrDyn *errv = bcs_arg(args, argc, 0); + ScrDyn *cb = bcs_arg(args, argc, 1); + if (scr_stream_prop(s, "destroyed") != 0) { + if (scr_dyn_is_callable(cb)) { + ScrDyn *result = bcs_apply(cb, self, NULL, 0, "callback"); + if (!result) return NULL; + scr_dyn_release(result); + } + return scr_dyn_retain(self); + } + if (scr_dyn_is_callable(cb)) { + BcsRec *rec = bcs_rec(s); + if (rec->destroy_n == rec->destroy_cap) { + rec->destroy_cap = rec->destroy_cap ? rec->destroy_cap * 2 : 2; + rec->destroy_cbs = realloc(rec->destroy_cbs, rec->destroy_cap * sizeof *rec->destroy_cbs); + if (!rec->destroy_cbs) scr_trap("out of memory"); + } + rec->destroy_cbs[rec->destroy_n++] = scr_dyn_retain(cb); + } + ScrError *err = bcs_error_of(errv); + ScrStream *chained = scr_stream_destroy(s, err); + if (err) scr_error_release(err); + scr_stream_release(chained); + if (scr_exc_pending()) return NULL; + return scr_dyn_retain(self); +} + +/* pipe(dest, { end }): runtime streams and constructor-function streams + * pipe through the runtime's pipe. */ +static bool bcs_overrides(ScrDyn *dest, const char *name); + +static ScrDyn *bcs_pipe(ScrStream *s, ScrDyn *const *args, size_t argc) { + ScrDyn *dest = bcs_arg(args, argc, 0); + ScrDyn *options = bcs_arg(args, argc, 1); + ScrStream *d = scr_dyn_builtin_stream_of(dest); + if (!d) { + if (!scr_exc_pending()) bcs_unsupported("pipe destinations other than streams are not supported yet"); + return NULL; + } + /* The runtime's pipe writes and ends the destination's stream itself; + * Node calls the destination's write() and end(). */ + if (bcs_of(dest) && (bcs_overrides(dest, "write") || bcs_overrides(dest, "end"))) { + scr_stream_release(d); + if (!scr_exc_pending()) + bcs_unsupported("pipe into a constructor-function stream with its own write() or end() is not supported yet"); + return NULL; + } + bool end = true; + if (options->kind == SCR_DYN_OBJ) { + ScrDyn *option = bcs_get(options, "end"); + if (!option) { + scr_stream_release(d); + return NULL; + } + end = !(option->kind == SCR_DYN_BOOL && !option->v.b); + scr_dyn_release(option); + } + ScrStream *chained = scr_stream_pipe(s, d, end); + if (chained) scr_stream_release(chained); + scr_stream_release(d); + if (scr_exc_pending()) return NULL; + return scr_dyn_retain(dest); +} + +static ScrDyn *bcs_not_implemented(const char *method) { + char message[64]; + snprintf(message, sizeof message, "The %s method is not implemented", method); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "ERR_METHOD_NOT_IMPLEMENTED"); + return NULL; +} + +static ScrDyn *bcs_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + int method = (int)scr_box_get_f64(closure->caps[0]); + switch (method) { + case BCS_M_READ_DEFAULT: + return bcs_not_implemented("_read()"); + case BCS_M_WRITE_DEFAULT: + return bcs_not_implemented("_write()"); + case BCS_M_TRANSFORM_DEFAULT: + return bcs_not_implemented("_transform()"); + case BCS_M_DESTROY_DEFAULT: + case BCS_M_PASSTHROUGH_TRANSFORM: { + /* _destroy(err, cb) => cb(err); _transform(chunk, enc, cb) => cb(null, chunk) */ + ScrDyn *cb = bcs_arg(args, argc, method == BCS_M_DESTROY_DEFAULT ? 1 : 2); + ScrDyn *pass[2]; + size_t passc = 1; + if (method == BCS_M_DESTROY_DEFAULT) { + pass[0] = bcs_arg(args, argc, 0); + } else { + pass[0] = scr_dyn_new_null(); + pass[1] = bcs_arg(args, argc, 0); + passc = 2; + } + ScrDyn *answer = scr_dyn_call(cb, pass, passc, "callback"); + if (method != BCS_M_DESTROY_DEFAULT) scr_dyn_release(pass[0]); + return answer; + } + default: + break; + } + ScrDyn *self = scr_dyn_this_get(); + static const char *const reading[] = { + "push", "unshift", "read", "setEncoding", "pause", "resume", "isPaused", "pipe", "unpipe", + "destroy", "write", "end", "cork", "uncork", "setDefaultEncoding", "pipe", "wrap", + "", "", "", "", "", "on", "removeListener", "removeAllListeners", + }; + ScrDyn *result = NULL; + if (method == BCS_M_WRITABLE_PIPE) { + ScrStream *s = bcs_need(self, "pipe"); + if (s) { + ScrStr *message = scr_str_new("Cannot pipe, not readable", 25); + ScrError *err = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_error_set_code(err, "ERR_STREAM_CANNOT_PIPE"); + scr_stream_fail(s, err); + scr_error_release(err); + if (!scr_exc_pending()) result = scr_dyn_retain(bcs_arg(args, argc, 0)); + } + scr_dyn_release(self); + return result; + } + if (method == BCS_M_ON || method == BCS_M_OFF || method == BCS_M_REMOVE_ALL) { + int ee = method == BCS_M_ON ? SCR_EE_M_ON : method == BCS_M_OFF ? SCR_EE_M_OFF : SCR_EE_M_REMOVE_ALL; + if (!bcs_event(ee, self, args, argc, &result)) { + /* Before the constructor ran (bl adds listeners first), Node's + * Readable.prototype.on is Stream.prototype.on over _events. */ + ScrDyn *emitter = scr_dyn_builtin_class(0); + ScrDyn *prototype = bcs_get(emitter, "prototype"); + scr_dyn_release(emitter); + ScrDyn *fn = prototype ? bcs_get(prototype, reading[method]) : NULL; + result = fn ? bcs_apply(fn, self, args, argc, reading[method]) : NULL; + if (fn) scr_dyn_release(fn); + if (prototype) scr_dyn_release(prototype); + } + scr_dyn_release(self); + return result; + } + ScrStream *s = bcs_need(self, reading[method]); + if (!s) { + scr_dyn_release(self); + return NULL; + } + ScrDyn *first = bcs_arg(args, argc, 0); + switch (method) { + case BCS_M_PUSH: + case BCS_M_UNSHIFT: { + ScrDyn *encoding = bcs_arg(args, argc, 1); + if (method == BCS_M_PUSH && first->kind == SCR_DYN_STR && encoding->kind == SCR_DYN_STR) { + ScrStr *canonical = bcs_encoding(encoding); + if (canonical) { + result = scr_dyn_new_bool(scr_stream_push_str_enc(s, first->v.str, canonical)); + scr_str_release(canonical); + } + } else if (method == BCS_M_PUSH) { + if (first->kind == SCR_DYN_UNDEF) { + ScrStr *empty = scr_str_new("", 0); + result = scr_dyn_new_bool(scr_stream_push_str(s, empty)); + scr_str_release(empty); + } else { + result = scr_dyn_new_bool(scr_stream_push_dyn(s, first)); + } + } else if (first->kind == SCR_DYN_STR) { + scr_stream_unshift_str(s, first->v.str); + result = bcs_undefined(); + } else if (first->kind == SCR_DYN_BYTES) { + ScrBytes *bytes = scr_bytes_raw_view(first->v.bytes); + scr_stream_unshift(s, bytes); + scr_bytes_release(bytes); + result = bcs_undefined(); + } else if (first->kind == SCR_DYN_NULL) { + scr_stream_push_null(s); + result = bcs_undefined(); + } else { + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", first); + } + break; + } + case BCS_M_READ: + result = scr_stream_read_dyn(s, first); + break; + case BCS_M_SET_ENCODING: { + ScrStr *canonical = bcs_encoding(first->kind == SCR_DYN_UNDEF ? scr_dyn_undefined() : first); + if (canonical) { + scr_stream_release(scr_stream_set_encoding(s, canonical)); + scr_str_release(canonical); + result = scr_dyn_retain(self); + } + break; + } + case BCS_M_PAUSE: + scr_stream_release(scr_stream_pause(s)); + result = scr_dyn_retain(self); + break; + case BCS_M_RESUME: + scr_stream_release(scr_stream_resume(s)); + result = scr_dyn_retain(self); + break; + case BCS_M_IS_PAUSED: + result = scr_dyn_new_bool(scr_stream_is_paused(s)); + break; + case BCS_M_PIPE: + result = bcs_pipe(s, args, argc); + break; + case BCS_M_UNPIPE: { + ScrStream *d = first->kind == SCR_DYN_UNDEF ? NULL : scr_dyn_builtin_stream_of(first); + if (first->kind == SCR_DYN_UNDEF || d) { + scr_stream_release(scr_stream_unpipe(s, d)); + if (d) scr_stream_release(d); + } + if (!scr_exc_pending()) result = scr_dyn_retain(self); + break; + } + case BCS_M_DESTROY: + result = bcs_destroy(s, self, args, argc); + break; + case BCS_M_WRITE: + result = bcs_write(s, self, args, argc); + break; + case BCS_M_END: + result = bcs_end(s, self, args, argc); + break; + case BCS_M_CORK: + scr_stream_cork(s); + result = bcs_undefined(); + break; + case BCS_M_UNCORK: + scr_stream_uncork(s); + result = bcs_undefined(); + break; + case BCS_M_SET_DEFAULT_ENCODING: + if (bcs_utf8_only(first)) result = scr_dyn_retain(self); + break; + default: + bcs_unsupported("Readable.prototype.wrap on a constructor-function stream is not supported yet"); + break; + } + scr_dyn_release(self); + if (scr_exc_pending()) { + if (result) scr_dyn_release(result); + return NULL; + } + return result; +} + +static ScrDyn *bcs_function(int method, const char *name, uint32_t arity) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], method); + return scr_dyn_new_func(closure, bcs_method_call, arity, "native:stream", name); +} + +static void bcs_method(ScrDyn *prototype, const char *name, int method, uint32_t arity) { + bcs_set(prototype, name, bcs_function(method, name, arity)); +} + +/* ── accessors ──────────────────────────────────────────────────────── */ + +enum { + BCS_P_READABLE, BCS_P_READABLE_ENDED, BCS_P_READABLE_LENGTH, BCS_P_READABLE_HWM, + BCS_P_READABLE_OBJECT_MODE, BCS_P_READABLE_FLOWING, BCS_P_WRITABLE, BCS_P_WRITABLE_ENDED, + BCS_P_WRITABLE_FINISHED, BCS_P_WRITABLE_LENGTH, BCS_P_WRITABLE_HWM, BCS_P_WRITABLE_NEED_DRAIN, + BCS_P_WRITABLE_CORKED, BCS_P_WRITABLE_OBJECT_MODE, BCS_P_DESTROYED, BCS_P_CLOSED, BCS_P_ERRORED, + BCS_P_COUNT +}; + +static const char *const bcs_property_names[BCS_P_COUNT] = { + "readable", "readableEnded", "readableLength", "readableHighWaterMark", "readableObjectMode", + "readableFlowing", "writable", "writableEnded", "writableFinished", "writableLength", + "writableHighWaterMark", "writableNeedDrain", "writableCorked", "writableObjectMode", + "destroyed", "closed", "errored", +}; + +static ScrDyn *bcs_flowing(ScrStream *s) { + double flowing = scr_stream_flowing(s); + return flowing < 0 ? scr_dyn_new_null() : scr_dyn_new_bool(flowing != 0); +} + +static ScrDyn *bcs_errored(ScrStream *s) { + ScrError *err = scr_stream_errored(s); + if (!err) return scr_dyn_new_null(); + ScrDyn *value = scr_dyn_from_error(err); + scr_error_release(err); + return value; +} + +static ScrDyn *bcs_getter_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + int property = (int)scr_box_get_f64(closure->caps[0]); + ScrDyn *self = scr_dyn_this_get(); + ScrStream *s = bcs_of(self); + scr_dyn_release(self); + /* Node's getters answer undefined (or false) before the constructor ran. */ + if (!s) return property == BCS_P_DESTROYED ? scr_dyn_new_bool(false) : bcs_undefined(); + const char *name = bcs_property_names[property]; + switch (property) { + case BCS_P_READABLE_FLOWING: + return bcs_flowing(s); + case BCS_P_ERRORED: + return bcs_errored(s); + case BCS_P_WRITABLE_OBJECT_MODE: + return scr_dyn_new_bool(false); + case BCS_P_READABLE_LENGTH: + case BCS_P_READABLE_HWM: + case BCS_P_WRITABLE_LENGTH: + case BCS_P_WRITABLE_HWM: + case BCS_P_WRITABLE_CORKED: + return scr_dyn_new_num(scr_stream_prop(s, name)); + default: + return scr_dyn_new_bool(scr_stream_prop(s, name) != 0); + } +} + +/* The destroyed setter: Node sets the states' flag without destroying. + * The readable and writable setters store a flag the runtime state does + * not keep (legacy code assigns `this.readable = true`), so they are + * accepted and have no effect. */ +static ScrDyn *bcs_setter_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + int property = (int)scr_box_get_f64(closure->caps[0]); + ScrDyn *self = scr_dyn_this_get(); + ScrStream *s = bcs_of(self); + scr_dyn_release(self); + if (s && property == BCS_P_DESTROYED) scr_stream_set_destroyed(s, scr_dyn_truthy(bcs_arg(args, argc, 0))); + return bcs_undefined(); +} + +static void bcs_define(ScrDyn *target, const char *name, ScrDyn *getter, ScrDyn *setter) { + ScrDyn *descriptor = scr_dyn_new_obj(); + bcs_set(descriptor, "get", getter); + if (setter) bcs_set(descriptor, "set", setter); + bcs_set(descriptor, "enumerable", scr_dyn_new_bool(false)); + bcs_set(descriptor, "configurable", scr_dyn_new_bool(true)); + ScrDyn *key = bcs_key(name); + ScrDyn *defined = scr_dyn_define_property(target, key, descriptor); + if (defined) scr_dyn_release(defined); + scr_dyn_release(key); + scr_dyn_release(descriptor); +} + +static void bcs_accessor(ScrDyn *prototype, int property) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], property); + const char *name = bcs_property_names[property]; + ScrDyn *getter = scr_dyn_new_func(closure, bcs_getter_call, 0, "native:stream", name); + ScrDyn *setter = NULL; + if (property == BCS_P_DESTROYED || property == BCS_P_READABLE || property == BCS_P_WRITABLE) { + ScrClosure *set = scr_closure_new(NULL, 1); + set->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(set->caps[0], property); + setter = scr_dyn_new_func(set, bcs_setter_call, 1, "native:stream", name); + } + bcs_define(prototype, name, getter, setter); +} + +/* ── _readableState / _writableState views ──────────────────────────── */ + +static const char *const bcs_rs_names[] = { + "highWaterMark", "length", "ended", "endEmitted", "reading", "destroyed", "objectMode", + "needReadable", "emittedReadable", "readableListening", "resumeScheduled", "errorEmitted", + "closed", "constructed", "flowing", "errored", +}; +static const char *const bcs_ws_names[] = { + "highWaterMark", "length", "ended", "ending", "finished", "prefinished", "destroyed", + "objectMode", "needDrain", "corked", "closed", "constructed", "errorEmitted", + "bufferedRequestCount", "errored", +}; +#define BCS_RS_COUNT (sizeof bcs_rs_names / sizeof *bcs_rs_names) +#define BCS_WS_COUNT (sizeof bcs_ws_names / sizeof *bcs_ws_names) + +static SCR_TL ScrDyn *bcs_state_protos[2]; +static SCR_TL bool bcs_teardown_registered; + +static ScrDyn *bcs_state_get(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + bool readable = scr_box_get_f64(closure->caps[0]) != 0; + size_t index = (size_t)scr_box_get_f64(closure->caps[1]); + const char *name = readable ? bcs_rs_names[index] : bcs_ws_names[index]; + ScrDyn *self = scr_dyn_this_get(); + ScrStream *s = bcs_of(self); + scr_dyn_release(self); + if (!s) return bcs_undefined(); + if (!strcmp(name, "flowing")) return bcs_flowing(s); + if (!strcmp(name, "errored")) return bcs_errored(s); + char key[48]; + snprintf(key, sizeof key, "%s:%s", readable ? "rs" : "ws", name); + double value = scr_stream_prop(s, key); + bool number = !strcmp(name, "highWaterMark") || !strcmp(name, "length") || !strcmp(name, "corked") || + !strcmp(name, "bufferedRequestCount"); + return number ? scr_dyn_new_num(value) : scr_dyn_new_bool(value != 0); +} + +static void bcs_teardown(void) { + for (int i = 0; i < 2; i++) { + if (bcs_state_protos[i]) scr_dyn_release(bcs_state_protos[i]); + bcs_state_protos[i] = NULL; + } + if (scr_exc_pending()) scr_exc_clear(); +} + +static ScrDyn *bcs_state_proto(bool readable) { + ScrDyn **slot = &bcs_state_protos[readable ? 0 : 1]; + if (!*slot) { + if (!bcs_teardown_registered) { + bcs_teardown_registered = true; + scr_atexit(bcs_teardown); + } + ScrDyn *proto = scr_dyn_new_obj(); + size_t count = readable ? BCS_RS_COUNT : BCS_WS_COUNT; + for (size_t i = 0; i < count; i++) { + ScrClosure *closure = scr_closure_new(NULL, 2); + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], readable ? 1 : 0); + closure->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[1], (double)i); + const char *name = readable ? bcs_rs_names[i] : bcs_ws_names[i]; + bcs_define(proto, name, scr_dyn_new_func(closure, bcs_state_get, 0, "native:stream", name), NULL); + } + *slot = proto; + } + return *slot; +} + +/* ── the prototypes ─────────────────────────────────────────────────── */ + +static ScrDyn *bcs_default_method(int kind, const char *name) { + ScrDyn *ctor = scr_dyn_builtin_class(kind); + ScrDyn *prototype = bcs_get(ctor, "prototype"); + scr_dyn_release(ctor); + if (!prototype) return NULL; + ScrDyn *method = bcs_get(prototype, name); + scr_dyn_release(prototype); + return method; +} + +static void bcs_writable_members(ScrDyn *prototype) { + bcs_method(prototype, "write", BCS_M_WRITE, 3); + bcs_method(prototype, "cork", BCS_M_CORK, 0); + bcs_method(prototype, "uncork", BCS_M_UNCORK, 0); + bcs_method(prototype, "setDefaultEncoding", BCS_M_SET_DEFAULT_ENCODING, 1); + bcs_method(prototype, "_write", BCS_M_WRITE_DEFAULT, 3); + bcs_set(prototype, "_writev", scr_dyn_new_null()); + bcs_method(prototype, "end", BCS_M_END, 3); + for (int p = BCS_P_WRITABLE; p <= BCS_P_WRITABLE_OBJECT_MODE; p++) bcs_accessor(prototype, p); +} + +/* Node's stream prototypes: Readable and Writable carry their methods, + * Duplex adds Writable's, Transform and PassThrough their _transform. */ +static void bcs_prototype(int kind, ScrDyn *prototype) { + switch (kind) { + case BCS_READABLE: + bcs_method(prototype, "destroy", BCS_M_DESTROY, 2); + bcs_method(prototype, "_destroy", BCS_M_DESTROY_DEFAULT, 2); + bcs_method(prototype, "push", BCS_M_PUSH, 2); + bcs_method(prototype, "unshift", BCS_M_UNSHIFT, 2); + bcs_method(prototype, "isPaused", BCS_M_IS_PAUSED, 0); + bcs_method(prototype, "setEncoding", BCS_M_SET_ENCODING, 1); + bcs_method(prototype, "read", BCS_M_READ, 1); + bcs_method(prototype, "_read", BCS_M_READ_DEFAULT, 1); + bcs_method(prototype, "pipe", BCS_M_PIPE, 2); + bcs_method(prototype, "on", BCS_M_ON, 2); + { + ScrDyn *on = bcs_get(prototype, "on"); + if (on) bcs_set(prototype, "addListener", on); + } + bcs_method(prototype, "removeListener", BCS_M_OFF, 2); + { + ScrDyn *off = bcs_get(prototype, "removeListener"); + if (off) bcs_set(prototype, "off", off); + } + bcs_method(prototype, "removeAllListeners", BCS_M_REMOVE_ALL, 1); + bcs_method(prototype, "resume", BCS_M_RESUME, 0); + bcs_method(prototype, "pause", BCS_M_PAUSE, 0); + bcs_method(prototype, "unpipe", BCS_M_UNPIPE, 1); + bcs_method(prototype, "wrap", BCS_M_WRAP, 1); + for (int p = BCS_P_READABLE; p <= BCS_P_READABLE_FLOWING; p++) bcs_accessor(prototype, p); + bcs_accessor(prototype, BCS_P_DESTROYED); + bcs_accessor(prototype, BCS_P_CLOSED); + bcs_accessor(prototype, BCS_P_ERRORED); + break; + case BCS_WRITABLE: + bcs_method(prototype, "pipe", BCS_M_WRITABLE_PIPE, 0); + bcs_writable_members(prototype); + bcs_method(prototype, "destroy", BCS_M_DESTROY, 2); + bcs_method(prototype, "_destroy", BCS_M_DESTROY_DEFAULT, 2); + bcs_accessor(prototype, BCS_P_DESTROYED); + bcs_accessor(prototype, BCS_P_CLOSED); + bcs_accessor(prototype, BCS_P_ERRORED); + break; + case BCS_DUPLEX: + bcs_writable_members(prototype); + bcs_accessor(prototype, BCS_P_DESTROYED); + break; + case BCS_TRANSFORM: + bcs_method(prototype, "_transform", BCS_M_TRANSFORM_DEFAULT, 3); + break; + case BCS_PASSTHROUGH: + bcs_method(prototype, "_transform", BCS_M_PASSTHROUGH_TRANSFORM, 3); + break; + default: + break; + } + if (scr_exc_pending()) scr_exc_clear(); +} + +static const ScrDynStreamOps bcs_ops = { bcs_construct, bcs_prototype, bcs_event }; + +/* A stream class value (the BC_* kinds of scr_dyn_builtin_class.c) with + * its stream half attached, +1. */ +ScrDyn *scr_dyn_builtin_stream_class(double kind) { + scr_dyn_builtin_class_streams(&bcs_ops); + return scr_dyn_builtin_class(kind); +} + +/* Whether dest's method is its own rather than the stream prototypes'. */ +static bool bcs_overrides(ScrDyn *dest, const char *name) { + ScrDyn *own = bcs_get(dest, name); + if (!own) return true; + ScrDyn *fallback = bcs_default_method(BCS_WRITABLE, name); + ScrDyn *duplex = bcs_default_method(BCS_DUPLEX, name); + bool overridden = !(fallback && scr_dyn_strict_eq(own, fallback)) && !(duplex && scr_dyn_strict_eq(own, duplex)); + if (fallback) scr_dyn_release(fallback); + if (duplex) scr_dyn_release(duplex); + scr_dyn_release(own); + return overridden; +} + +/* pipe and unpipe on a runtime stream's untyped view, which also take + * constructor-function streams. */ +static ScrDyn *bcs_view_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrStream *s = scr_box_get_ref(closure->caps[0]); + bool pipe = scr_box_get_f64(closure->caps[1]) != 0; + ScrDyn *result = NULL; + if (pipe) { + result = bcs_pipe(s, args, argc); + } else { + ScrDyn *first = bcs_arg(args, argc, 0); + ScrStream *d = first->kind == SCR_DYN_UNDEF ? NULL : scr_dyn_builtin_stream_of(first); + if (first->kind == SCR_DYN_UNDEF || d) { + scr_stream_release(scr_stream_unpipe(s, d)); + if (d) scr_stream_release(d); + } else { + scr_dyn_arg_type_fail("destination", "an instance of Stream", first); + } + if (!scr_exc_pending()) result = scr_dyn_this_get(); + } + scr_stream_release(s); + return result; +} + +/* The untyped view of a runtime stream crossing into JavaScript, used in + * place of scr_stream_dyn_view by programs that link this unit: a + * constructor-function stream's own object, or the runtime view with pipe + * and unpipe that take those objects. +1. */ +ScrDyn *scr_dyn_builtin_stream_view(ScrStream *s) { + ScrDyn *object = scr_dyn_builtin_stream_object(s); + if (object) return object; + ScrDyn *view = scr_stream_dyn_view(s); + ScrDyn *prototype = scr_dyn_get_prototype(view); + if (prototype && prototype->kind == SCR_DYN_OBJ) { + for (int pipe = 0; pipe < 2; pipe++) { + ScrClosure *closure = scr_closure_new((void *)bcs_view_call, 2); + closure->caps[0] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); + scr_box_set_ref(closure->caps[0], scr_stream_retain(s)); + closure->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[1], pipe); + const char *name = pipe ? "pipe" : "unpipe"; + bcs_set(prototype, name, scr_dyn_new_func(closure, bcs_view_call, 0, "native stream method", name)); + } + } + if (prototype) scr_dyn_release(prototype); + if (scr_exc_pending()) scr_exc_clear(); + return view; +} + +/* A checked cast of an untyped value to a runtime stream class (kind: + * the BC_* numbering): the stream of a constructor-function stream of a + * matching class, +1, or NULL. */ +ScrStream *scr_dyn_builtin_stream_check(const ScrDyn *value, double kind) { + if (value->kind != SCR_DYN_OBJ) return NULL; + ScrStream *s = bcs_of(value); + if (!s) return NULL; + const ScrVt *vt = s->vt; + bool readable = vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post; + bool writable = vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post; + bool duplex = vt->pre >= scr_duplex_vt.pre && vt->pre <= scr_duplex_vt.post; + bool ok = false; + switch ((int)kind) { + case BCS_READABLE: ok = readable; break; + case BCS_WRITABLE: ok = writable || duplex; break; + case BCS_DUPLEX: ok = duplex; break; + case BCS_TRANSFORM: ok = vt->pre >= scr_transform_vt.pre && vt->pre <= scr_transform_vt.post; break; + case BCS_PASSTHROUGH: ok = vt->pre >= scr_passthrough_vt.pre && vt->pre <= scr_passthrough_vt.post; break; + default: break; + } + return ok ? scr_stream_retain(s) : NULL; +} + +/* readable.pipe(untyped) from typed code: a constructor-function stream + * or a boxed runtime stream as the destination. src borrowed; answers + * nothing (the expression's value is the destination). */ +void scr_dyn_builtin_stream_pipe(ScrStream *src, ScrDyn *dest, bool end) { + ScrDyn *options = scr_dyn_new_obj(); + bcs_set(options, "end", scr_dyn_new_bool(end)); + ScrDyn *args[2] = { dest, options }; + ScrDyn *result = bcs_pipe(src, args, 2); + if (result) scr_dyn_release(result); + scr_dyn_release(options); +} diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 5f0bb493d..9fbf7a970 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -2254,6 +2254,8 @@ void scr_stream_set_flush(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamPlain void scr_stream_set_flush_on_prefinish(ScrStream *s) { s->st->flush_on_prefinish = true; } +void scr_stream_set_destroyed(ScrStream *s, bool destroyed) { s->st->destroyed = destroyed; } + void scr_stream_set_native(ScrStream *s, void *state /*moves*/, void (*release)(void *)) { ScrStreamState *st = s->st; if (st->native) st->native_release(st->native); diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 82727c1dc..61993dba4 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -431,6 +431,9 @@ void scr_stream_set_flush(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); * released when the stream is freed; scr_stream_native answers it * (borrowed) when it was set with `release`, else NULL. */ void scr_stream_set_flush_on_prefinish(ScrStream *s); +/* `stream.destroyed = value` (scr_dyn_builtin_stream.c): Node's setter + * stores the states' flag without destroying the stream. */ +void scr_stream_set_destroyed(ScrStream *s, bool destroyed); void scr_stream_set_native(ScrStream *s, void *state, void (*release)(void *)); void *scr_stream_native(ScrStream *s, void (*release)(void *)); diff --git a/tests/corpus/6560-constructor-function-readable.cjs b/tests/corpus/6560-constructor-function-readable.cjs new file mode 100644 index 000000000..5319aada1 --- /dev/null +++ b/tests/corpus/6560-constructor-function-readable.cjs @@ -0,0 +1,121 @@ +// Readable streams built by constructor functions, the from2 pattern: +// util.inherits(Ctor, Readable), Readable.call(this, opts) and _read on +// the prototype. +const { Readable } = require("stream"); +const util = require("util"); + +function Source(list, opts) { + if (!(this instanceof Source)) return new Source(list, opts); + this._list = list.slice(); + // Before Readable.call there is no state: Node's setter ignores this. + this.destroyed = false; + Readable.call(this, opts || {}); + this._reads = 0; + console.log("hwm", this._readableState.highWaterMark, "objectMode", this._readableState.objectMode); +} +util.inherits(Source, Readable); +Source.prototype._read = function (size) { + this._reads++; + const item = this._list.shift(); + this.push(item === undefined ? null : Buffer.from(item)); +}; + +const s = Source(["alpha", "beta", "gamma"]); +console.log("instanceof", s instanceof Source, s instanceof Readable, Source.super_ === Readable); +console.log("readable", s.readable, "ended", s.readableEnded, "destroyed", s.destroyed); +console.log("flowing", s.readableFlowing, s._readableState.flowing); +s.on("data", function (chunk) { + console.log("data", this === s, Buffer.isBuffer(chunk), chunk.toString()); +}); +s.on("end", function () { + console.log("end", this === s, s.readableEnded, s._readableState.ended, s._reads); +}); +s.on("close", () => { + console.log("close", s.destroyed, s.closed); + paused(); +}); +console.log("sync after on('data')", s.readableFlowing); + +// from2's callback-driven _read, asynchronous pushes and a small +// highWaterMark. +function From(read, opts) { + if (!(this instanceof From)) return new From(read, opts); + this._reading = false; + this._callback = check; + this.destroyed = false; + Readable.call(this, opts); + const self = this; + const hwm = this._readableState.highWaterMark; + function check(err, data) { + if (self.destroyed) return; + if (err) return self.destroy(err); + if (data === null) return self.push(null); + self._reading = false; + if (self.push(data)) self._read(hwm); + } + this._from = read; +} +util.inherits(From, Readable); +From.prototype._read = function (size) { + if (this._reading || this.destroyed) return; + this._reading = true; + this._from(size, this._callback); +}; +From.prototype.destroy = function (err) { + if (this.destroyed) return; + this.destroyed = true; + const self = this; + process.nextTick(function () { + if (err) self.emit("error", err); + self.emit("close"); + }); +}; + +function paused() { + const parts = ["one ", "two ", "three"]; + const r = new From(function (size, next) { + setImmediate(() => next(null, parts.length ? parts.shift() : null)); + }, { highWaterMark: 4 }); + console.log("from hwm", r._readableState.highWaterMark, r.readableHighWaterMark); + r.setEncoding("utf8"); + let text = ""; + r.on("readable", () => { + let chunk; + while ((chunk = r.read()) !== null) text += chunk; + }); + r.on("end", () => { + console.log("paused text", JSON.stringify(text), typeof text); + // From() without new runs outside the listener, where `this` is unset. + setImmediate(failing); + }); +} + +function failing() { + let calls = 0; + const r = From(function (size, next) { + calls++; + if (calls === 2) return next(new Error("source failed")); + next(null, "chunk" + calls); + }); + r.on("data", (d) => console.log("failing data", String(d))); + r.on("error", (e) => console.log("failing error", e.message, r.destroyed)); + r.on("close", () => { + console.log("failing close", calls); + unshifted(); + }); +} + +function unshifted() { + const r = new Source(["xyz"]); + r.once("readable", () => { + const first = r.read(1); + console.log("read(1)", String(first), r.readableLength); + r.unshift(first); + console.log("after unshift", r.readableLength); + r.pause(); + console.log("isPaused", r.isPaused()); + r.on("data", (d) => console.log("rest", String(d))); + r.resume(); + }); + r.on("end", () => console.log("unshifted end")); +} diff --git a/tests/corpus/6561-constructor-function-writable.cjs b/tests/corpus/6561-constructor-function-writable.cjs new file mode 100644 index 000000000..ee25b7595 --- /dev/null +++ b/tests/corpus/6561-constructor-function-writable.cjs @@ -0,0 +1,141 @@ +// Writable and Duplex streams built by constructor functions, the bl and +// tar-stream patterns: Writable.call(this, opts), Duplex.call(this), +// _write/_final/_read on the prototype, listeners added before the base +// constructor runs, and an end() override that calls the base's end. +const stream = require("stream"); +const util = require("util"); + +function Sink(opts) { + if (!(this instanceof Sink)) return new Sink(opts); + stream.Writable.call(this, opts); + this.chunks = []; + this.finalized = false; +} +util.inherits(Sink, stream.Writable); +Sink.prototype._write = function (chunk, enc, cb) { + console.log("_write", this instanceof Sink, enc, Buffer.isBuffer(chunk), chunk.toString()); + this.chunks.push(chunk); + if (this.chunks.length === 2) setImmediate(cb); + else cb(); +}; +Sink.prototype._final = function (cb) { + console.log("_final", this.chunks.length); + this.finalized = true; + cb(); +}; + +const sink = new Sink({ highWaterMark: 8 }); +console.log("writable", sink.writable, sink.writableHighWaterMark, sink._writableState.highWaterMark); +console.log("write a", sink.write("first")); +console.log("write b", sink.write(Buffer.from("second"), () => console.log("write b callback"))); +console.log("needDrain", sink.writableNeedDrain, sink.writableLength); +sink.on("drain", () => console.log("drain", sink.writableLength)); +sink.on("finish", function () { + console.log("finish", this === sink, sink.finalized, sink.writableFinished, Buffer.concat(sink.chunks).toString()); +}); +sink.on("close", () => { + console.log("close", sink.destroyed, sink.closed); + duplex(); +}); +sink.end("third", (err) => console.log("end callback", err, sink.writableEnded, sink._writableState.ended)); +console.log("after end", sink.writableEnded, sink.writable); + +// A bl-style list: a Duplex that collects writes and reads them back. +function BufferListStream(callback) { + if (!(this instanceof BufferListStream)) return new BufferListStream(callback); + if (typeof callback === "function") { + this._callback = callback; + const piper = function piper(err) { + if (this._callback) { + this._callback(err); + this._callback = null; + } + }.bind(this); + this.on("pipe", function onPipe(src) { + console.log("pipe event", src instanceof stream.Readable); + src.on("error", piper); + }); + this.on("unpipe", function onUnpipe(src) { + console.log("unpipe event"); + src.removeListener("error", piper); + }); + callback = null; + } + this._bufs = []; + this.length = 0; + stream.Duplex.call(this); +} +util.inherits(BufferListStream, stream.Duplex); +BufferListStream.prototype._write = function (buf, encoding, callback) { + this._bufs.push(buf); + this.length += buf.length; + callback(); +}; +BufferListStream.prototype._read = function (size) { + if (!this.length) return this.push(null); + const all = Buffer.concat(this._bufs); + size = Math.min(size, this.length, 4); + this.push(all.subarray(0, size)); + this._bufs = [all.subarray(size)]; + this.length -= size; +}; +BufferListStream.prototype.end = function end(chunk) { + stream.Duplex.prototype.end.call(this, chunk); + if (this._callback) { + this._callback(null, Buffer.concat(this._bufs)); + this._callback = null; + } +}; +BufferListStream.prototype.slice = function () { + return Buffer.concat(this._bufs); +}; + +function Letters(text) { + stream.Readable.call(this, { highWaterMark: 2 }); + this._text = text; +} +util.inherits(Letters, stream.Readable); +Letters.prototype._read = function () { + const next = this._text.slice(0, 3); + this._text = this._text.slice(3); + this.push(next.length ? next : null); +}; + +function duplex() { + const list = new BufferListStream((err, data) => { + console.log("bl callback", err, data.toString(), data.length); + }); + console.log("duplex", list.readable, list.writable, list.allowHalfOpen, list instanceof stream.Duplex); + console.log("pre-constructor listeners", list.listenerCount("pipe"), list.listenerCount("unpipe")); + list.write("hello "); + list.write("constructor "); + list.end("functions"); + list.on("finish", () => { + const out = []; + list.on("data", (d) => out.push(d.toString())); + list.on("end", () => { + console.log("read back", JSON.stringify(out)); + piping(); + }); + }); +} + +function piping() { + const src = new Letters("abcdefghij"); + const dst = new Sink({ highWaterMark: 1 }); + dst.on("pipe", (from) => console.log("dst pipe", from === src)); + const returned = src.pipe(dst); + console.log("pipe returns dest", returned === dst); + dst.on("finish", () => { + console.log("piped", Buffer.concat(dst.chunks).toString(), src.readableEnded); + const keep = new Sink(); + const from = new Letters("xyz"); + from.pipe(keep, { end: false }); + from.on("end", () => { + setImmediate(() => { + console.log("end:false keeps the destination open", keep.writable, keep.writableEnded); + keep.end(); + }); + }); + }); +} diff --git a/tests/corpus/6562-constructor-function-transform.cjs b/tests/corpus/6562-constructor-function-transform.cjs new file mode 100644 index 000000000..c112d8ccc --- /dev/null +++ b/tests/corpus/6562-constructor-function-transform.cjs @@ -0,0 +1,137 @@ +// Transform and PassThrough streams built by constructor functions (the +// yazl and tar-stream patterns), piped together with runtime streams: +// zlib and fs. +const { Readable, Writable, Transform, PassThrough } = require("stream"); +const util = require("util"); +const zlib = require("zlib"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +// yazl's ByteCounter +function ByteCounter(options) { + Transform.call(this, options); + this.byteCount = 0; +} +util.inherits(ByteCounter, Transform); +ByteCounter.prototype._transform = function (chunk, encoding, cb) { + this.byteCount += chunk.length; + cb(null, chunk); +}; + +// A transform with a flush that appends a trailer. +function Upper() { + if (!(this instanceof Upper)) return new Upper(); + Transform.call(this); + this.seen = 0; +} +util.inherits(Upper, Transform); +Upper.prototype._transform = function (chunk, encoding, cb) { + this.seen++; + this.push(chunk.toString().toUpperCase()); + cb(); +}; +Upper.prototype._flush = function (cb) { + cb(null, "[" + this.seen + " chunks]"); +}; + +// tar-stream's entry stream: a PassThrough with its own destroy. +function Source(parent, offset) { + this._parent = parent; + this.offset = offset; + PassThrough.call(this, { autoDestroy: false }); +} +util.inherits(Source, PassThrough); +Source.prototype.destroy = function (err) { + console.log("Source.destroy forwarded", err && err.message); + this._parent.destroyed = true; +}; + +// A PassThrough without its own _transform passes chunks through. +function Plain() { + PassThrough.call(this); +} +util.inherits(Plain, PassThrough); + +function Words(words) { + Readable.call(this); + this._words = words.slice(); +} +util.inherits(Words, Readable); +Words.prototype._read = function () { + this.push(this._words.length ? this._words.shift() : null); +}; + +function Collect() { + Writable.call(this); + this.parts = []; +} +util.inherits(Collect, Writable); +Collect.prototype._write = function (chunk, enc, cb) { + this.parts.push(chunk); + cb(); +}; +Collect.prototype.text = function () { + return Buffer.concat(this.parts).toString(); +}; + +const upper = Upper(); +const words = []; +upper.on("data", (d) => words.push(d.toString())); +upper.on("end", () => { + console.log("upper", JSON.stringify(words), upper.seen, upper._readableState.ended, upper._writableState.finished); + counter(); +}); +upper.write("one "); +upper.write(Buffer.from("two ")); +upper.end("three"); + +function counter() { + const counterStream = new ByteCounter(); + const sink = new Collect(); + new Words(["hello ", "byte ", "counter"]).pipe(counterStream).pipe(sink); + sink.on("finish", () => { + console.log("counted", counterStream.byteCount, JSON.stringify(sink.text())); + entry(); + }); +} + +function entry() { + const parent = { destroyed: false }; + const source = new Source(parent, 512); + const plain = new Plain(); + console.log("source", source.offset, source instanceof PassThrough, source instanceof Transform); + let body = ""; + source.pipe(plain); + plain.on("data", (d) => (body += d)); + plain.on("end", () => { + console.log("entry body", JSON.stringify(body), source.destroyed); + source.destroy(new Error("stop")); + console.log("parent destroyed", parent.destroyed); + gzip(); + }); + source.write("file "); + source.end("contents"); +} + +function gzip() { + const counted = new ByteCounter(); + const out = new Collect(); + new Words(["compressed ", "through ", "zlib"]).pipe(counted).pipe(zlib.createGzip()).pipe(out); + out.on("finish", () => { + const back = zlib.gunzipSync(Buffer.concat(out.parts)).toString(); + console.log("gzip roundtrip", JSON.stringify(back), counted.byteCount); + toFile(); + }); +} + +function toFile() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ctor-stream-")); + const file = path.join(dir, "out.txt"); + const ws = fs.createWriteStream(file); + new Words(["written ", "by ", "a ", "constructor"]).pipe(Upper()).pipe(ws); + ws.on("finish", () => { + console.log("file", JSON.stringify(fs.readFileSync(file, "utf8"))); + fs.rmSync(dir, { recursive: true }); + }); +} diff --git a/tests/corpus/6563-constructor-function-stream-errors.cjs b/tests/corpus/6563-constructor-function-stream-errors.cjs new file mode 100644 index 000000000..cf2fb3e7c --- /dev/null +++ b/tests/corpus/6563-constructor-function-stream-errors.cjs @@ -0,0 +1,137 @@ +// Errors, destroy and custom events on constructor-function streams. +const { Readable, Writable, Duplex } = require("stream"); +const util = require("util"); + +function Failing() { + Writable.call(this); +} +util.inherits(Failing, Writable); +Failing.prototype._write = function (chunk, enc, cb) { + const err = new Error("cannot store " + chunk); + err.code = "E_STORE"; + this.lastError = err; + cb(err); +}; + +const failing = new Failing(); +failing.on("error", function (err) { + console.log("write error", this === failing, err === failing.lastError, err.message, err.code); + console.log("errored", failing.errored === err, failing.destroyed, failing.writable); +}); +failing.on("close", () => { + console.log("write close"); + destroying(); +}); +failing.write("x", (err) => console.log("write callback", err && err.code)); + +function Ticker() { + Readable.call(this); + this.n = 0; +} +util.inherits(Ticker, Readable); +Ticker.prototype._read = function () { + this.push(String(this.n++)); +}; +Ticker.prototype._destroy = function (err, cb) { + console.log("_destroy", err && err.message, this.n > 0); + setImmediate(() => cb(err && new Error("wrapped: " + err.message))); +}; + +function destroying() { + const t = new Ticker(); + const order = []; + t.on("data", (d) => { + if (d.toString() === "3") t.destroy(new Error("enough"), (e) => order.push("destroy callback " + (e && e.message))); + }); + t.on("error", (e) => order.push("error " + e.message)); + t.on("close", () => { + order.push("close"); + console.log(order.join(" | "), t.destroyed, t.readable); + t.destroy(null, () => console.log("destroy after close calls back")); + notImplemented(); + }); +} + +function Bare() { + Readable.call(this); +} +util.inherits(Bare, Readable); + +function Throws() { + Readable.call(this); +} +util.inherits(Throws, Readable); +Throws.prototype._read = function () { + throw new TypeError("read threw"); +}; + +function notImplemented() { + const bare = new Bare(); + bare.on("error", (e) => { + console.log("bare", e.code, e.message); + const thrower = new Throws(); + thrower.on("error", (err) => { + console.log("throws", err instanceof TypeError, err.message, thrower.destroyed); + afterEnd(); + }); + thrower.resume(); + }); + bare.resume(); +} + +function Quiet() { + Writable.call(this); +} +util.inherits(Quiet, Writable); +Quiet.prototype._write = function (chunk, enc, cb) { + cb(); +}; + +function afterEnd() { + const q = new Quiet(); + q.end("done"); + q.on("error", (e) => { + console.log("after end", e.code, e.message); + customEvents(); + }); + q.write("late"); +} + +// tar-stream's Extract emits 'entry' with three arguments. +function Extract() { + if (!(this instanceof Extract)) return new Extract(); + Writable.call(this); +} +util.inherits(Extract, Writable); +Extract.prototype._write = function (data, enc, cb) { + const header = { name: data.toString(), size: data.length }; + this.emit("entry", header, "stream-for-" + header.name, cb); +}; + +function customEvents() { + const ex = Extract(); + const seen = []; + function onEntry(header, s, next) { + seen.push(header.name + ":" + header.size + ":" + s + ":" + (this === ex)); + next(); + } + ex.on("entry", onEntry); + ex.once("entry", (header) => seen.push("once " + header.name)); + ex.prependListener("entry", () => seen.push("first")); + console.log("listenerCount", ex.listenerCount("entry"), ex.listenerCount("entry", onEntry)); + ex.write("a.txt"); + ex.removeListener("entry", onEntry); + ex.write("b.txt"); + console.log(seen.join(" | ")); + console.log("emit returns", ex.emit("nothing"), ex.emit("entry", { name: "c", size: 0 }, "", () => {})); + try { + ex.emit("error", new RangeError("nobody listens")); + } catch (e) { + console.log("unhandled error rethrown", e instanceof RangeError, e.message); + } + ex.setMaxListeners(1); + console.log("max listeners", ex.getMaxListeners()); + ex.removeAllListeners("entry"); + console.log("after removeAll", ex.listenerCount("entry")); + console.log("Duplex prototype", typeof Duplex.prototype.write, typeof Duplex.prototype.push); +} diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 1de960368..1a172ca0c 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -31,6 +31,7 @@ const features: NativeLinkFeatures = { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/tests/harness/self-hosting-runtime-features.test.ts b/tests/harness/self-hosting-runtime-features.test.ts index 8d65d72cd..b2f7e3062 100644 --- a/tests/harness/self-hosting-runtime-features.test.ts +++ b/tests/harness/self-hosting-runtime-features.test.ts @@ -51,6 +51,7 @@ const featureCalls: readonly [string, IrLibFn][] = [ ["searchParams", "sp.new"], ["qs", "qs.parse"], ["dynBuiltinClass", "dyn.builtinClass"], + ["dynBuiltinStream", "dyn.builtinStreamClass"], ["parseArgs", "util.parseArgs"], ["parseArgs", "util.isDeepStrictEqual"], ["parseArgs", "util.styleText"], diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index 42cf8a729..a0de981b6 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -32,6 +32,7 @@ const base: NativeLinkFeatures = { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, From 2a9328cc5c80972cfa871388197040a54e7d17fc Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 09:54:08 -0700 Subject: [PATCH 12/22] Pipe a runtime stream into constructor-function streams in the transform corpus program Co-Authored-By: Claude Opus 5.5 --- tests/corpus/6562-constructor-function-transform.cjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/corpus/6562-constructor-function-transform.cjs b/tests/corpus/6562-constructor-function-transform.cjs index c112d8ccc..144a746bc 100644 --- a/tests/corpus/6562-constructor-function-transform.cjs +++ b/tests/corpus/6562-constructor-function-transform.cjs @@ -132,6 +132,12 @@ function toFile() { new Words(["written ", "by ", "a ", "constructor"]).pipe(Upper()).pipe(ws); ws.on("finish", () => { console.log("file", JSON.stringify(fs.readFileSync(file, "utf8"))); - fs.rmSync(dir, { recursive: true }); + // A runtime stream piped into a constructor-function stream. + const back = new Collect(); + fs.createReadStream(file).pipe(new ByteCounter()).pipe(back); + back.on("finish", () => { + console.log("read back", JSON.stringify(back.text())); + fs.rmSync(dir, { recursive: true }); + }); }); } From b89594c276658571eadf72b0032e73372588df70 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 10:31:10 -0700 Subject: [PATCH 13/22] Keep the legacy Stream constructor on the EventEmitter unit Programs whose only stream value is the legacy Stream do not need the stream half. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/builtins/builtin-class-dyn.ts | 6 +++--- packages/compiler/src/ir/builtin-signatures.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts index 9508aa651..07113eee7 100644 --- a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts +++ b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts @@ -111,14 +111,14 @@ export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): I }; } -/** EventEmitter is dyn.builtinClass; the stream classes, which carry - * native stream state for constructor functions, are +/** EventEmitter and the legacy Stream are dyn.builtinClass; the stream + * classes, which carry native stream state for constructor functions, are * dyn.builtinStreamClass, so only programs that use them link the stream * half. */ export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr { return { kind: "libCall", - fn: kind === 0 ? "dyn.builtinClass" : "dyn.builtinStreamClass", + fn: kind <= 1 ? "dyn.builtinClass" : "dyn.builtinStreamClass", args: [numLit(kind, loc)], type: DYN, loc, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index c3d4eeba7..e645738a9 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3540,7 +3540,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * Transform (5) and PassThrough (6) as untyped constructor values, one * per thread (scr_dyn_builtin_class.c). */ "dyn.builtinClass": { argTypes: [F64], result: DYN }, - /** The stream classes (1-6 above) with their stream half: constructor + /** The stream classes (2-6 above) with their stream half: constructor * functions over them get native stream state * (scr_dyn_builtin_stream.c). */ "dyn.builtinStreamClass": { argTypes: [F64], result: DYN }, From 5f3efe314c10e1ac0b555e0e1ec5b0e4bd9c26a7 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 10:35:49 -0700 Subject: [PATCH 14/22] Format the runtime-pack matrix entry Co-Authored-By: Claude Opus 5.5 --- packages/runtime-pack-common/runtime-pack-matrix.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 8baef6698..945192de6 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -95,7 +95,15 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // Their stream half also references the stream unit. [ "scr_dyn_builtin_stream.c", - all("dynBuiltinStream", "dynBuiltinClass", "stream", "inspect", "dynInvoke", "symbol", "emitter"), + all( + "dynBuiltinStream", + "dynBuiltinClass", + "stream", + "inspect", + "dynInvoke", + "symbol", + "emitter", + ), ], ["scr_stream.c", "stream"], // The zlib stream classes reference both the zlib engine and the stream unit. From 97bf98cd892ba7cf37a7f08577c8680746dd9896 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 12:50:19 -0500 Subject: [PATCH 15/22] Declare the WebAssembly W5 and stream-slot entry points in the runtime headers The backend can emit scr_wasm_memory_construct, scr_wasm_table_construct, scr_wasm_global_construct, scr_wasm_construct_without_new, scr_wasm_module_imports, scr_wasm_module_exports, scr_wasm_module_custom_sections and scr_wasm_value_of (#1410), and scr_stream_dyn_native, scr_stream_pipe_dyn and scr_stream_unpipe_dyn (#1524). Their prototypes were in scr_wasm.c or nowhere, so the runtime ABI guard failed. They are now in scr_wasm.h and scr_stream.h, and scr_wasm.c no longer repeats the header's declarations. The speculative-hoisting test relied on `v instanceof Num` in a JavaScript function failing, which constructor-function instanceof now compiles (#1512). Its hoisted function reads the prototype through Reflect.getPrototypeOf instead, which still fences. --- .../speculative-hoisted-functions.test.ts | 8 +++++--- packages/runtime/src/scr_stream.h | 7 +++++++ packages/runtime/src/scr_wasm.c | 19 ------------------- packages/runtime/src/scr_wasm.h | 8 ++++++++ 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/compiler/test/speculative-hoisted-functions.test.ts b/packages/compiler/test/speculative-hoisted-functions.test.ts index a58310b2a..79a56dcde 100644 --- a/packages/compiler/test/speculative-hoisted-functions.test.ts +++ b/packages/compiler/test/speculative-hoisted-functions.test.ts @@ -9,7 +9,9 @@ import type { IrModule } from "../src/ir/ir.js"; // list above the constructor. The initializer lowers speculatively first, // which hoists the constructor and the helper it calls. Those bodies are // kept, so their failing statements must keep runtime fences; dropping -// them left `isNum` without its return (an SC9002 trap at load). +// them left `isNum` without its return (an SC9002 trap at load). isNum's +// return reads the prototype through Reflect.getPrototypeOf, which has no +// lowering, so it fences. const source = ` function clone(config) { var P = (Num.prototype = { constructor: Num, toString: null }), @@ -20,7 +22,7 @@ function clone(config) { x.v = isNum(v) ? 0 : v * unit; } function isNum(v) { - return v instanceof Num || (!!v && v._isNum === true); + return Reflect.getPrototypeOf(v) === P || (!!v && v._isNum === true); } P._isNum = true; return Num; @@ -59,6 +61,6 @@ test("speculatively hoisted JavaScript functions keep their statement fences", a const mod = await lower(); const isNum = mod.functions.find((fn) => fn.name.endsWith("_isNum")); expect(isNum).toBeDefined(); - expect(fenceCodes(isNum!.body)).toEqual(["SC1090"]); + expect(fenceCodes(isNum!.body)).toEqual(["SC2020"]); expect(mod.functions.flatMap((fn) => fenceCodes(fn.body))).not.toContain("SC9002"); }); diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 69da3dc0e..5587b58d7 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -136,6 +136,13 @@ ScrStream *scr_stream_pipe_general(ScrStream *src, ScrStream *dst, bool end); /* A boxed native stream out of a checked-dynamic pipeline stage (+1), or * NULL with `message` thrown as the not-supported Error. */ ScrStream *scr_stream_unbox_dyn(const struct ScrDyn *value, const ScrStr *message); +/* Checked-dynamic stream slots (scr_stream_dyn.c): the native stream a + * value boxes with the requested side (1 readable, 2 writable; +1, or + * NULL), and readable.pipe()/unpipe() for a destination that is not a + * native stream. */ +ScrStream *scr_stream_dyn_native(const struct ScrDyn *value, double side); +void scr_stream_pipe_dyn(ScrStream *src, struct ScrDyn *src_box, struct ScrDyn *dest, bool end); +void scr_stream_unpipe_dyn(ScrStream *src, struct ScrDyn *dest); ScrStream *scr_stream_new_duplex_dyn(struct ScrDyn *opts); ScrStream *scr_stream_new_transform_dyn(struct ScrDyn *opts); ScrStream *scr_stream_new_passthrough_dyn(struct ScrDyn *opts); diff --git a/packages/runtime/src/scr_wasm.c b/packages/runtime/src/scr_wasm.c index 8f9244c28..92d556272 100644 --- a/packages/runtime/src/scr_wasm.c +++ b/packages/runtime/src/scr_wasm.c @@ -90,25 +90,6 @@ #define SCR_WASM_ASAN 1 #endif -ScrPromise *scr_wasm_compile(ScrDyn *bytes); -bool scr_wasm_validate(ScrDyn *bytes); -ScrPromise *scr_wasm_instantiate(ScrDyn *source, ScrDyn *imports); -ScrDyn *scr_wasm_module_new(ScrDyn *bytes); -ScrDyn *scr_wasm_instance_new(ScrDyn *module, ScrDyn *imports); -bool scr_wasm_is(const ScrDyn *value, double kind); -ScrDyn *scr_wasm_error_new(double kind, ScrDyn *message, ScrDyn *options); -ScrDyn *scr_wasm_function(double kind); -ScrDyn *scr_wasm_namespace(void); -ScrDyn *scr_wasm_global_view(void); -ScrDyn *scr_wasm_memory_construct(ScrDyn *descriptor); -ScrDyn *scr_wasm_table_construct(ScrDyn *descriptor, ScrDyn *value); -ScrDyn *scr_wasm_global_construct(ScrDyn *descriptor, ScrDyn *value); -ScrDyn *scr_wasm_construct_without_new(double kind, ScrDyn *arg0, ScrDyn *arg1); -ScrDyn *scr_wasm_module_imports(ScrDyn *module); -ScrDyn *scr_wasm_module_exports(ScrDyn *module); -ScrDyn *scr_wasm_module_custom_sections(ScrDyn *module, ScrDyn *name); -ScrDyn *scr_wasm_value_of(ScrDyn *value); - /* Entry points of translated code. */ /* Sanitized builds do not declare it noreturn: AddressSanitizer would * call __asan_handle_no_return before every call, which warns on the diff --git a/packages/runtime/src/scr_wasm.h b/packages/runtime/src/scr_wasm.h index 7b0a29689..f7a791a26 100644 --- a/packages/runtime/src/scr_wasm.h +++ b/packages/runtime/src/scr_wasm.h @@ -19,5 +19,13 @@ ScrDyn *scr_wasm_error_new(double kind, ScrDyn *message, ScrDyn *options); ScrDyn *scr_wasm_function(double kind); ScrDyn *scr_wasm_namespace(void); ScrDyn *scr_wasm_global_view(void); +ScrDyn *scr_wasm_memory_construct(ScrDyn *descriptor); +ScrDyn *scr_wasm_table_construct(ScrDyn *descriptor, ScrDyn *value); +ScrDyn *scr_wasm_global_construct(ScrDyn *descriptor, ScrDyn *value); +ScrDyn *scr_wasm_construct_without_new(double kind, ScrDyn *arg0, ScrDyn *arg1); +ScrDyn *scr_wasm_module_imports(ScrDyn *module); +ScrDyn *scr_wasm_module_exports(ScrDyn *module); +ScrDyn *scr_wasm_module_custom_sections(ScrDyn *module, ScrDyn *name); +ScrDyn *scr_wasm_value_of(ScrDyn *value); #endif /* SCR_WASM_H */ From 53443672194673c36446afd481114332e1650430 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 13:16:29 -0500 Subject: [PATCH 16/22] Read constructor-function stream hooks when they run A constructor-function stream now reads `_final` and `_flush` when its writable side finishes, and `_transform` for each chunk, as Node does. A hook assigned after `Writable.call(this)` or `Transform.call(this)` runs, and an override of a PassThrough's `_transform` replaces the pass-through. Without `_final` or `_flush` the finish continues as Node's does. A runtime stream passed to a constructor-function stream's listeners, such as the source of a 'pipe' event, is the view whose pipe and unpipe accept constructor-function streams, so `src.unpipe(this)` in a 'pipe' listener unpipes instead of throwing. --- ...ructor-function-stream-late-hooks.cjs.json | 6 + packages/runtime/src/scr_dyn_builtin_stream.c | 54 +++++--- ...constructor-function-stream-late-hooks.cjs | 117 ++++++++++++++++++ ...ructor-function-stream-late-hooks.cjs.json | 1 + 4 files changed, 158 insertions(+), 20 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6564-constructor-function-stream-late-hooks.cjs.json create mode 100644 tests/corpus/6564-constructor-function-stream-late-hooks.cjs create mode 100644 tests/harness/corpus-costs/6564-constructor-function-stream-late-hooks.cjs.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6564-constructor-function-stream-late-hooks.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6564-constructor-function-stream-late-hooks.cjs.json new file mode 100644 index 000000000..52d8e55ff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6564-constructor-function-stream-late-hooks.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6564-constructor-function-stream-late-hooks.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index d9ac6bc50..1926b0622 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -204,10 +204,10 @@ ScrDyn *scr_dyn_builtin_stream_object(ScrStream *s) { } /* A stream as a listener argument: its constructor-function object, or a - * live view of another runtime stream. +1. */ + * live view of another runtime stream whose pipe and unpipe take those + * objects (scr_dyn_builtin_stream_view). +1. */ static ScrDyn *bcs_stream_value(ScrStream *s) { - ScrDyn *object = scr_dyn_builtin_stream_object(s); - return object ? object : scr_stream_dyn_view(s); + return scr_dyn_builtin_stream_view(s); } /* The receiver's stream for a prototype method, borrowed; throws Node's @@ -398,12 +398,35 @@ static void bcs_plain_call(ScrClosure *cb, ScrStream *s, const char *method, int scr_dyn_release(object); } +static bool bcs_is_function(ScrDyn *object, const char *name, bool *failed); + +/* prefinish reads `stream._final` when the writable side finishes, so a + * method assigned after the constructor runs; without one the finish goes + * on as Node's does when `typeof stream._final !== 'function'`. */ static void bcs_final_inv(ScrClosure *cb, ScrStream *s) { - bcs_plain_call(cb, s, "_final", BCS_DONE_FINAL); + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + bool failed = false; + bool present = bcs_is_function(object, "_final", &failed); + scr_dyn_release(object); + if (failed) return; + if (present) bcs_plain_call(cb, s, "_final", BCS_DONE_FINAL); + else scr_stream_final_done(s, NULL); } +/* Transform's final reads `this._flush` the same way: without one it ends + * the readable side. */ static void bcs_flush_inv(ScrClosure *cb, ScrStream *s) { - bcs_plain_call(cb, s, "_flush", BCS_DONE_FLUSH); + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + bool failed = false; + bool present = bcs_is_function(object, "_flush", &failed); + if (!failed && bcs_is_function(object, "_final", &failed)) { + bcs_unsupported("_final on a constructor-function Transform is not supported yet"); + failed = true; + } + scr_dyn_release(object); + if (failed) return; + if (present) bcs_plain_call(cb, s, "_flush", BCS_DONE_FLUSH); + else scr_stream_flush_done(s, NULL, NULL, NULL); } static ScrDyn *bcs_default_method(int kind, const char *name); @@ -897,23 +920,14 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t scr_stream_set_read(s, bcs_hook(self, (void *)bcs_read_inv), bcs_read_inv); if (kind == BCS_WRITABLE || kind == BCS_DUPLEX) { scr_stream_set_write(s, bcs_hook(self, (void *)bcs_write_inv), bcs_write_inv); - if (bcs_is_function(self, "_final", &failed)) scr_stream_set_final(s, bcs_hook(self, (void *)bcs_final_inv), bcs_final_inv); - else scr_stream_set_final(s, NULL, NULL); + /* _final is read when the writable side finishes (bcs_final_inv). */ + scr_stream_set_final(s, bcs_hook(self, (void *)bcs_final_inv), bcs_final_inv); } if (kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH) { - bool identity = false; - if (kind == BCS_PASSTHROUGH) { - ScrDyn *own = bcs_get(self, "_transform"); - ScrDyn *fallback = bcs_default_method(BCS_PASSTHROUGH, "_transform"); - identity = own && fallback && scr_dyn_strict_eq(own, fallback); - if (own) scr_dyn_release(own); - else failed = true; - if (fallback) scr_dyn_release(fallback); - } - if (identity) scr_stream_set_transform(s, NULL, NULL); - else scr_stream_set_transform(s, bcs_hook(self, (void *)bcs_transform_inv), bcs_transform_inv); - if (bcs_is_function(self, "_flush", &failed)) scr_stream_set_flush(s, bcs_hook(self, (void *)bcs_flush_inv), bcs_flush_inv); - else scr_stream_set_flush(s, NULL, NULL); + /* _transform is read per chunk (a PassThrough's default passes the + * chunk on) and _flush when the writable side finishes. */ + scr_stream_set_transform(s, bcs_hook(self, (void *)bcs_transform_inv), bcs_transform_inv); + scr_stream_set_flush(s, bcs_hook(self, (void *)bcs_flush_inv), bcs_flush_inv); if (!failed && bcs_is_function(self, "_final", &failed)) { bcs_unsupported("_final on a constructor-function Transform is not supported yet"); failed = true; diff --git a/tests/corpus/6564-constructor-function-stream-late-hooks.cjs b/tests/corpus/6564-constructor-function-stream-late-hooks.cjs new file mode 100644 index 000000000..22a8ec2f4 --- /dev/null +++ b/tests/corpus/6564-constructor-function-stream-late-hooks.cjs @@ -0,0 +1,117 @@ +// Stream hooks a constructor function assigns after the base constructor +// runs, and unpipe from the destination's 'pipe' listener. +const { Writable, Transform, PassThrough } = require("stream"); +const util = require("util"); + +function Sink() { + Writable.call(this); + this.chunks = []; + this._final = function (cb) { + console.log("_final", this instanceof Sink, this.chunks.join(",")); + setImmediate(cb); + }; +} +util.inherits(Sink, Writable); +Sink.prototype._write = function (chunk, enc, cb) { + this.chunks.push(String(chunk)); + cb(); +}; + +const sink = new Sink(); +sink.on("prefinish", () => console.log("sink prefinish")); +sink.on("finish", () => { + console.log("sink finish"); + flushing(); +}); +sink.write("a"); +sink.end("b"); + +function Summer() { + Transform.call(this); + this.total = 0; + this._flush = function (cb) { + console.log("_flush", this.total); + cb(null, "total " + this.total); + }; +} +util.inherits(Summer, Transform); +Summer.prototype._transform = function (chunk, enc, cb) { + this.total += Number(String(chunk)); + cb(null, "seen " + chunk + "\n"); +}; + +function flushing() { + const summer = new Summer(); + const out = []; + summer.on("data", (chunk) => out.push(String(chunk))); + summer.on("end", () => { + console.log("summer end", JSON.stringify(out)); + passing(); + }); + summer.write("2"); + summer.write("3"); + summer.end(); +} + +function Upper() { + PassThrough.call(this); + this._transform = function (chunk, enc, cb) { + cb(null, String(chunk).toUpperCase()); + }; +} +util.inherits(Upper, PassThrough); + +function passing() { + const upper = new Upper(); + const out = []; + upper.on("data", (chunk) => out.push(String(chunk))); + upper.on("end", () => { + console.log("upper end", JSON.stringify(out)); + plain(); + }); + upper.write("x"); + upper.end("y"); +} + +// Without late hooks the defaults still apply. +function Plain() { + PassThrough.call(this); +} +util.inherits(Plain, PassThrough); + +function plain() { + const p = new Plain(); + const out = []; + p.on("data", (chunk) => out.push(String(chunk))); + p.on("finish", () => console.log("plain finish")); + p.on("end", () => { + console.log("plain end", JSON.stringify(out)); + unpiping(); + }); + p.end("same"); +} + +function Collector() { + Writable.call(this); + this.chunks = []; +} +util.inherits(Collector, Writable); +Collector.prototype._write = function (chunk, enc, cb) { + this.chunks.push(String(chunk)); + cb(); +}; + +function unpiping() { + const source = new PassThrough(); + const collector = new Collector(); + collector.on("pipe", function (src) { + console.log("pipe", typeof src.unpipe, this === collector); + src.unpipe(this); + }); + collector.on("unpipe", (src) => { + console.log("unpipe", typeof src.pipe); + source.write("after"); + setImmediate(() => console.log("collected", JSON.stringify(collector.chunks), source.readableFlowing)); + }); + source.pipe(collector); +} diff --git a/tests/harness/corpus-costs/6564-constructor-function-stream-late-hooks.cjs.json b/tests/harness/corpus-costs/6564-constructor-function-stream-late-hooks.cjs.json new file mode 100644 index 000000000..e873baf78 --- /dev/null +++ b/tests/harness/corpus-costs/6564-constructor-function-stream-late-hooks.cjs.json @@ -0,0 +1 @@ +{ "plain": 2750, "san": 3200 } From bb1452ca5aa935030e3b8876ec581a3fe5d4fc35 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 13:28:32 -0500 Subject: [PATCH 17/22] Expect SC2020 from three web-stream npm programs The fixture's launch() helper calls run().then, which used to be the first statement these programs fenced at (SC1090). It compiles since #1503, so node24-web-stream-pipe-abort and node24-web-stream-readable-lifecycle now stop at 'new ReadableStream' and node24-web-stream-tee at 'ReadableStream.tee', which have no lowering (SC2020). --- tests/harness/npm.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 168fdb6bd..f5f3fdeeb 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -91,13 +91,13 @@ const KNOWN: Record = { "node24-web-stream-encoding": { run: "SC1090" }, "node24-web-stream-errors": { run: "SC1090" }, "node24-web-stream-lifecycle": { run: "SC1090" }, - "node24-web-stream-pipe-abort": { run: "SC1090" }, + "node24-web-stream-pipe-abort": { run: "SC2020" }, "node24-web-stream-pipe-errors": { run: "SC1090" }, "node24-web-stream-pipes": { run: "SC1090" }, "node24-web-stream-queues": { run: "SC1090" }, - "node24-web-stream-readable-lifecycle": { run: "SC1090" }, + "node24-web-stream-readable-lifecycle": { run: "SC2020" }, "node24-web-stream-readers": { run: "SC1090" }, - "node24-web-stream-tee": { run: "SC1090" }, + "node24-web-stream-tee": { run: "SC2020" }, "node24-web-stream-transforms": { run: "SC1090" }, "package-imports": { build: "SC2013" }, "promise-bridge": { build: "SC1090" }, From cad421b35add1b409801c9883cbdbfd81babd206 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 13:39:27 -0500 Subject: [PATCH 18/22] Search each stack-key local once per question A key forwarded through functions with several call sites made the search visit every call path. Each search now visits a local once, and a local keeps its answer: true once a search reached a key that can be "stack" from it, false once a search that found none passed it. --- .../src/backend/stack-observations.test.ts | 42 +++++++++++++++++++ .../src/backend/stack-observations.ts | 37 ++++++++++------ 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/backend/stack-observations.test.ts b/packages/compiler/src/backend/stack-observations.test.ts index 4a5a5de13..d381c5307 100644 --- a/packages/compiler/src/backend/stack-observations.test.ts +++ b/packages/compiler/src/backend/stack-observations.test.ts @@ -133,6 +133,48 @@ test('a computed key that cannot be "stack" reads no stack', () => { expect(observesStacks(stack)).toBe(true); }); +test("a key forwarded through functions with several call sites is searched once", () => { + // f0 calls f1 twice, f1 calls f2 twice, and so on, each passing its + // parameter on: 2^40 call paths reach the read in f40. + const depth = 40; + const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN }; + const key: IrExpr = { loc, kind: "varRef", localId: "key", type: STRING }; + const forward = (callee: string): IrStmt => ({ + loc, + kind: "exprStmt", + expr: { loc, kind: "call", callee, args: [key], type: VOID }, + }); + const chain = (root: IrExpr): IrModule => { + const functions: IrFunction[] = []; + for (let i = 0; i <= depth; i++) { + const body: IrStmt[] = + i < depth + ? [forward(`f${i + 1}`), forward(`f${i + 1}`)] + : [{ loc, kind: "exprStmt", expr: { loc, kind: "dynKeyGet", value, key, type: DYN } }]; + functions.push({ + ...fn(`f${i}`, body), + params: [{ localId: "key", name: "key", type: STRING }], + locals: [{ id: "key", name: "key", type: STRING, mutable: false }], + }); + } + const spelled: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "strLit", value: "stack", type: STRING }, + }; + const start: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "call", callee: "f0", args: [root], type: VOID }, + }; + return mod([fn("%main", [start, spelled]), ...functions]); + }; + expect(observesStacks(chain({ loc, kind: "strLit", value: "message", type: STRING }))).toBe( + false, + ); + expect(observesStacks(chain({ loc, kind: "strLit", value: "stack", type: STRING }))).toBe(true); +}); + test("programs without stack reads are not", () => { expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false); }); diff --git a/packages/compiler/src/backend/stack-observations.ts b/packages/compiler/src/backend/stack-observations.ts index fbb1c23cf..fa67c071f 100644 --- a/packages/compiler/src/backend/stack-observations.ts +++ b/packages/compiler/src/backend/stack-observations.ts @@ -33,7 +33,9 @@ class StackKeys { private readonly calls = new Map(); private readonly named: Set; private readonly inits = new Map>(); - private readonly visiting = new Set(); + /** Settled answers by function and local: true where a search reached a + * key that can be "stack", false where a search that found none passed. */ + private readonly answers = new Map(); constructor(mod: IrModule) { this.spelled = spellsStack(mod); @@ -59,6 +61,15 @@ class StackKeys { } mayBeStack(fn: IrFunction, key: IrExpr): boolean { + const visited = new Set(); + const answer = this.keyMayBeStack(fn, key, visited); + // A search that found no way to "stack" explored everything it visited + // to the end, so none of it can be "stack" either. + if (!answer) for (const visit of visited) this.answers.set(visit, false); + return answer; + } + + private keyMayBeStack(fn: IrFunction, key: IrExpr, visited: Set): boolean { if (key.kind === "strLit") return key.value === "stack"; if (!this.spelled) return false; if (NON_STACK_KEY_TYPES.has(key.type.kind)) return false; @@ -66,17 +77,18 @@ class StackKeys { if (key.kind === "dynFrom" && NON_STACK_KEY_TYPES.has(key.value.type.kind)) return false; if (key.kind !== "varRef") return true; const visit = `${fn.name}\0${key.localId}`; - // A cycle adds no value of its own: the answer comes from its entries. - if (this.visiting.has(visit)) return false; - this.visiting.add(visit); - try { - return this.localMayBeStack(fn, key.localId); - } finally { - this.visiting.delete(visit); - } + const known = this.answers.get(visit); + if (known !== undefined) return known; + // Each local is searched once per question: a cycle, or a second path + // to a local already searched, adds no value of its own. + if (visited.has(visit)) return false; + visited.add(visit); + const answer = this.localMayBeStack(fn, key.localId, visited); + if (answer) this.answers.set(visit, true); + return answer; } - private localMayBeStack(fn: IrFunction, localId: string): boolean { + private localMayBeStack(fn: IrFunction, localId: string, visited: Set): boolean { const local = fn.locals.find((candidate) => candidate.id === localId); if (local === undefined || local.mutable) return true; const index = fn.params.findIndex((param) => param.localId === localId); @@ -85,11 +97,12 @@ class StackKeys { if (this.named.has(fn.name) || sites === undefined) return true; return sites.some( (site) => - site.args.length !== fn.params.length || this.mayBeStack(site.caller, site.args[index]!), + site.args.length !== fn.params.length || + this.keyMayBeStack(site.caller, site.args[index]!, visited), ); } const init = this.initializerOf(fn, localId); - return init === null || this.mayBeStack(fn, init); + return init === null || this.keyMayBeStack(fn, init, visited); } /** The one initializer of an immutable local, or null when it has none From 5765d8922784bac7a859ac8ca124ef89fe64e537 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 13:41:39 -0500 Subject: [PATCH 19/22] Check the frontend's types against tsc-ts/api The frontend calls tokenStartOfNode, getTextOfNode, getSourceFileOfNode and forEachChild instead of AstNode's position and traversal methods. Node lists answer hasTrailingComma(), and a declaration's modifiers are a modifier list with their flags. Symbol, Type, Signature, IndexInfo, TypePredicate, Program, TypeChecker and Diagnostic have tsc-ts/api's members and types. Compiler code reads symbol names with symbolName(symbol). tsc-ts goes to 0.0.5. api-conformance.ts asserts that AstNode, AstNodeList, SourceFile and each node interface are assignable to tsc-ts/api's interface of the same name, and names the members where the wire format differs. --- packages/compiler/package.json | 2 +- packages/compiler/src/frontend/cjs-syntax.ts | 43 +- .../compiler/src/frontend/contract-types.ts | 2 +- .../src/frontend/experimental-import.ts | 6 +- .../compiler/src/frontend/fork-target.test.ts | 3 +- packages/compiler/src/frontend/fork-target.ts | 9 +- .../src/frontend/inferred-js-diagnostics.ts | 14 +- .../compiler/src/frontend/lib-contract.ts | 20 +- packages/compiler/src/frontend/lib-exports.ts | 2 +- .../src/frontend/lowering/assert-instances.ts | 2 +- .../frontend/lowering/assertion-members.ts | 2 +- .../src/frontend/lowering/binding-analysis.ts | 34 +- .../lowering/builtin-function-fences.ts | 2 +- .../lowering/builtins/async-context.ts | 2 +- .../src/frontend/lowering/builtins/blob.ts | 8 +- .../lowering/builtins/builtin-class-dyn.ts | 2 +- .../lowering/builtins/callable-aliases.ts | 2 +- .../builtins/child-process-methods.ts | 12 +- .../lowering/builtins/class-values.ts | 7 +- .../frontend/lowering/builtins/compression.ts | 4 +- .../frontend/lowering/builtins/constants.ts | 2 +- .../src/frontend/lowering/builtins/crypto.ts | 12 +- .../lowering/builtins/disposable-stack.ts | 7 +- .../src/frontend/lowering/builtins/errors.ts | 5 +- .../frontend/lowering/builtins/filesystem.ts | 4 +- .../frontend/lowering/builtins/fs-streams.ts | 8 +- .../src/frontend/lowering/builtins/json.ts | 2 +- .../frontend/lowering/builtins/key-object.ts | 4 +- .../lowering/builtins/module-bindings.ts | 8 +- .../lowering/builtins/module-calls.ts | 2 +- .../lowering/builtins/module-resolution.ts | 6 +- .../frontend/lowering/builtins/navigator.ts | 3 +- .../frontend/lowering/builtins/net-address.ts | 8 +- .../frontend/lowering/builtins/performance.ts | 8 +- .../src/frontend/lowering/builtins/process.ts | 25 +- .../frontend/lowering/builtins/promises.ts | 12 +- .../frontend/lowering/builtins/readline.ts | 10 +- .../lowering/builtins/receiver-types.ts | 4 +- .../lowering/builtins/runtime-constructor.ts | 2 +- .../frontend/lowering/builtins/text-codecs.ts | 13 +- .../src/frontend/lowering/builtins/timers.ts | 4 +- .../src/frontend/lowering/builtins/url.ts | 2 +- .../frontend/lowering/builtins/webcrypto.ts | 6 +- .../src/frontend/lowering/builtins/workers.ts | 4 +- .../src/frontend/lowering/call-arguments.ts | 12 +- .../src/frontend/lowering/call-signatures.ts | 61 +- .../frontend/lowering/cjs-exports-object.ts | 18 +- .../frontend/lowering/cjs-inline-require.ts | 6 +- .../src/frontend/lowering/cjs-tsc-interop.ts | 8 +- .../src/frontend/lowering/class-callbacks.ts | 6 +- .../lowering/class-dynamic-dispatch.ts | 4 +- .../lowering/class-method-mutations.ts | 7 +- .../frontend/lowering/class-method-values.ts | 5 +- .../lowering/class-protocol-dispatch.ts | 18 +- .../src/frontend/lowering/class-protocols.ts | 22 +- .../frontend/lowering/class-static-data.ts | 2 +- .../src/frontend/lowering/code-generation.ts | 8 +- .../frontend/lowering/collection-ingestion.ts | 2 +- .../frontend/lowering/collection-narrowing.ts | 7 +- .../lowering/construction-escapes.test.ts | 4 +- .../frontend/lowering/construction-escapes.ts | 8 +- .../frontend/lowering/constructor-returns.ts | 19 +- .../lowering/containers/array-callbacks.ts | 12 +- .../lowering/containers/array-construction.ts | 6 +- .../lowering/containers/array-methods.ts | 6 +- .../src/frontend/lowering/containers/bytes.ts | 26 +- .../lowering/containers/callback-arguments.ts | 2 +- .../containers/collection-grouping.ts | 2 +- .../lowering/containers/collection-methods.ts | 10 +- .../lowering/containers/dynamic-receivers.ts | 2 +- .../lowering/containers/exec-result-uses.ts | 2 +- .../lowering/containers/iterator-helpers.ts | 2 +- .../lowering/containers/match-row-uses.ts | 2 +- .../lowering/deferred-module-initializers.ts | 53 +- .../src/frontend/lowering/divergences.ts | 24 +- .../src/frontend/lowering/dyn-streams.ts | 12 +- .../lowering/expressions/array-literals.ts | 14 +- .../frontend/lowering/expressions/binary.ts | 14 +- .../lowering/expressions/element-access.ts | 28 +- .../lowering/expressions/identifiers.ts | 20 +- .../expressions/keyed-field-targets.ts | 2 +- .../lowering/expressions/narrowing.ts | 28 +- .../lowering/expressions/native-proxy.ts | 2 +- .../lowering/expressions/object-literals.ts | 51 +- .../lowering/expressions/optional-chains.ts | 6 +- .../lowering/expressions/property-access.ts | 89 +- .../lowering/expressions/relational.ts | 12 +- .../frontend/lowering/expressions/typeof.ts | 4 +- .../lowering/expressions/union-fields.ts | 2 +- .../frontend/lowering/expressions/updates.ts | 19 +- .../src/frontend/lowering/function-returns.ts | 40 +- .../frontend/lowering/generic-functions.ts | 53 +- .../src/frontend/lowering/import-meta.ts | 2 +- .../src/frontend/lowering/intern-tables.ts | 2 +- .../frontend/lowering/js-dynamic-objects.ts | 14 +- .../src/frontend/lowering/json-require.ts | 2 +- .../src/frontend/lowering/lower-assert.ts | 6 +- .../src/frontend/lowering/lower-calls.ts | 167 ++-- .../src/frontend/lowering/lower-classes.ts | 294 ++++--- .../src/frontend/lowering/lower-comptime.ts | 6 +- .../src/frontend/lowering/lower-console.ts | 2 +- .../src/frontend/lowering/lower-dgram.ts | 4 +- .../src/frontend/lowering/lower-enums.ts | 10 +- .../frontend/lowering/lower-event-emitter.ts | 12 +- .../src/frontend/lowering/lower-expando.ts | 24 +- .../src/frontend/lowering/lower-exprs.ts | 23 +- .../src/frontend/lowering/lower-functions.ts | 18 +- .../src/frontend/lowering/lower-generators.ts | 10 +- .../frontend/lowering/lower-global-value.ts | 2 +- .../src/frontend/lowering/lower-inspect.ts | 8 +- .../src/frontend/lowering/lower-mixins.ts | 11 +- .../src/frontend/lowering/lower-mock.ts | 2 +- .../src/frontend/lowering/lower-modules.ts | 29 +- .../src/frontend/lowering/lower-namespaces.ts | 42 +- .../frontend/lowering/lower-node-module.ts | 20 +- .../src/frontend/lowering/lower-server.ts | 22 +- .../src/frontend/lowering/lower-stdlib.ts | 53 +- .../src/frontend/lowering/lower-stmts.ts | 161 ++-- .../src/frontend/lowering/lower-stream.ts | 8 +- .../src/frontend/lowering/lower-test.ts | 4 +- .../src/frontend/lowering/lower-timers.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 158 ++-- .../frontend/lowering/module-member-this.ts | 10 +- .../src/frontend/lowering/nan-coded-reads.ts | 30 +- .../frontend/lowering/narrowing-stability.ts | 21 +- .../src/frontend/lowering/native-ffi.ts | 6 +- .../frontend/lowering/object-factory-new.ts | 2 +- .../src/frontend/lowering/private-brands.ts | 2 +- .../lowering/require-initialization.ts | 4 +- .../lowering/runtime-optional-bounds.ts | 6 +- .../lowering/runtime-optional-elements.ts | 2 +- .../lowering/runtime-optional-guards.ts | 2 +- .../lowering/runtime-optional-ownership.ts | 4 +- .../lowering/runtime-optional-provenance.ts | 4 +- .../lowering/runtime-optional-records.test.ts | 2 +- .../lowering/runtime-optional-records.ts | 14 +- .../src/frontend/lowering/split-consumers.ts | 2 +- .../lowering/static-function-names.ts | 2 +- .../src/frontend/lowering/static-this.ts | 12 +- .../frontend/lowering/string-index-bounds.ts | 16 +- .../src/frontend/lowering/surfaces.ts | 14 +- .../src/frontend/lowering/symbol-fields.ts | 14 +- .../src/frontend/lowering/typed-arguments.ts | 6 +- .../src/frontend/lowering/var-hoisting.ts | 4 +- .../frontend/module-source-candidates.test.ts | 3 +- .../src/frontend/npm-static-bundled-chunks.ts | 29 +- .../src/frontend/npm-static-bundled-cjs.ts | 19 +- .../src/frontend/npm-static-bundled-esm.ts | 53 +- .../frontend/npm-static-cjs-exports-alias.ts | 13 +- .../frontend/npm-static-cjs-exports-var.ts | 5 +- .../frontend/npm-static-declaration-syntax.ts | 70 +- .../src/frontend/npm-static-rewrite-syntax.ts | 59 +- .../src/frontend/npm-static-subtyping.ts | 8 +- packages/compiler/src/frontend/program.ts | 105 +-- .../compiler/src/frontend/services.test.ts | 3 +- .../compiler/src/frontend/tier-analysis.ts | 25 +- .../src/frontend/ts7/api-conformance.ts | 297 +++++++ .../src/frontend/ts7/ast-guards.generated.ts | 3 +- .../src/frontend/ts7/ast-node.test.ts | 30 +- .../compiler/src/frontend/ts7/ast-node.ts | 35 +- .../src/frontend/ts7/ast-types.generated.ts | 76 +- .../compiler/src/frontend/ts7/ast-types.ts | 7 +- packages/compiler/src/frontend/ts7/ast.ts | 1 + .../compiler/src/frontend/ts7/checker.test.ts | 10 +- packages/compiler/src/frontend/ts7/checker.ts | 53 +- .../compiler/src/frontend/ts7/diagnostic.ts | 38 + .../src/frontend/ts7/node-functions.ts | 23 + .../compiler/src/frontend/ts7/program-host.ts | 18 +- .../src/frontend/ts7/semantic-checker.test.ts | 8 +- .../src/frontend/ts7/semantic-checker.ts | 48 +- .../src/frontend/ts7/semantic-model.test.ts | 14 +- .../src/frontend/ts7/semantic-model.ts | 97 ++- .../src/frontend/ts7/semantic-types.ts | 29 +- .../compiler/src/frontend/ts7/session.test.ts | 11 +- packages/compiler/src/frontend/ts7/session.ts | 25 +- .../src/frontend/ts7/source-parser.test.ts | 21 +- packages/compiler/src/frontend/type-mapper.ts | 796 ++++++++++-------- .../compiler/src/frontend/worker-target.ts | 3 +- packages/compiler/src/ir/traverse.test.ts | 27 +- packages/compiler/src/wasm/discover.ts | 2 +- packages/compiler/test/ts7/parity.test.ts | 7 +- packages/compiler/test/ts7/rpc-api.test.ts | 28 +- pnpm-lock.yaml | 75 +- pnpm-workspace.yaml | 16 +- scripts/generate-ts7-ast-schema.mjs | 7 +- scripts/generate-ts7-ast-views.mjs | 15 +- .../self-hosting/lowering-context-cases.ts | 2 +- .../self-hosting/ts7-checker-cases.ts | 8 +- .../fixtures/self-hosting/ts7-client-cases.ts | 3 +- .../self-hosting/ts7-semantic-cases.ts | 19 +- 190 files changed, 2781 insertions(+), 1861 deletions(-) create mode 100644 packages/compiler/src/frontend/ts7/api-conformance.ts create mode 100644 packages/compiler/src/frontend/ts7/diagnostic.ts create mode 100644 packages/compiler/src/frontend/ts7/node-functions.ts diff --git a/packages/compiler/package.json b/packages/compiler/package.json index b6a18db06..97c7688f5 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "@scriptc/runtime": "workspace:*", - "tsc-ts": "0.0.3", + "tsc-ts": "0.0.5", "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, diff --git a/packages/compiler/src/frontend/cjs-syntax.ts b/packages/compiler/src/frontend/cjs-syntax.ts index 41191432a..99195d994 100644 --- a/packages/compiler/src/frontend/cjs-syntax.ts +++ b/packages/compiler/src/frontend/cjs-syntax.ts @@ -1,4 +1,5 @@ import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; export interface CjsLexedExports { /** Names Node's lexer detects in THIS file (no reexport resolution). */ @@ -30,7 +31,7 @@ function identRunEnd(text: string, pos: number): number { * a `a`-escaped name parses to the same AST as its plain spelling, * but merve reads bytes and stops at the backslash. */ function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean { - const ch = sf.text[node.getStart(sf)]; + const ch = sf.text[tokenStartOfNode(node)]; return ch !== undefined && isIdentStartChar(ch); } @@ -39,16 +40,16 @@ function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean { * defineProperty getter's `return IDENT.IDENT` because `this` lexes as an * identifier run — probed), escapes and private names do not. */ function isIdentTokenNode(node: ts.Node, sf: ts.SourceFile): boolean { - const start = node.getStart(sf); + const start = tokenStartOfNode(node); const ch = sf.text[start]; - return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.getEnd(); + return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.end; } /** The identifier run at the node's START — the whole name when its * spelling is plain, the byte prefix merve consumed when a unicode escape * interrupts the run mid-name. */ function identPrefixOf(node: ts.Node, sf: ts.SourceFile): string { - const start = node.getStart(sf); + const start = tokenStartOfNode(node); return sf.text.slice(start, identRunEnd(sf.text, start)); } @@ -84,20 +85,17 @@ export function isModuleExports(e: ts.Expression): boolean { * follows — so the recognizer descends the leftmost-child chain while it * still starts where `e` starts. A ParenthesizedExpression breaks the * chain naturally (its child starts after the `(`), matching merve. */ -function leadingRequireOf( - e: ts.Expression, - sf: ts.SourceFile, -): { spec: string; end: number } | null { - const start = e.getStart(sf); +function leadingRequireOf(e: ts.Expression): { spec: string; end: number } | null { + const start = tokenStartOfNode(e); let cur: ts.Node = e; for (;;) { const spec = bareRequireSpecOf(cur); - if (spec !== null && cur.getStart(sf) === start) return { spec, end: cur.getEnd() }; + if (spec !== null && tokenStartOfNode(cur) === start) return { spec, end: cur.end }; let first: ts.Node | undefined; ts.forEachChild(cur, (c) => { first ??= c; }); - if (first === undefined || first.getStart(sf) !== start) return null; + if (first === undefined || tokenStartOfNode(first) !== start) return null; cur = first; } } @@ -179,13 +177,13 @@ function scanTableLiteral( continue; // whitespace before `,` is skipped on the shorthand path } if (ts.isSpreadAssignment(prop)) { - const req = leadingRequireOf(prop.expression, sf); + const req = leadingRequireOf(prop.expression); let consumedEnd: number; if (req !== null) { reexports.push(req.spec); consumedEnd = req.end; } else if (sourceSpellsIdentifier(prop.expression, sf)) { - consumedEnd = identRunEnd(text, prop.expression.getStart(sf)); + consumedEnd = identRunEnd(text, tokenStartOfNode(prop.expression)); } else { return; // `...{}` and friends stop the scan } @@ -213,7 +211,7 @@ function scanTableLiteral( } if (key === null) return; // numeric / computed / template keys stop the scan const value = prop.initializer; - const req = leadingRequireOf(value, sf); + const req = leadingRequireOf(value); if (req !== null) { // A require VALUE adds the key, records the reexport, and stops // the scan UNCONDITIONALLY — probed: `{ a: require('x'), b: v }` @@ -223,7 +221,7 @@ function scanTableLiteral( reexports.push(req.spec); return; } - const start = value.getStart(sf); + const start = tokenStartOfNode(value); const ch0 = text[start]; if (ch0 === undefined || !isIdentStartChar(ch0)) return; // literal values stop WITHOUT adding the key const consumedEnd = identRunEnd(text, start); @@ -351,8 +349,8 @@ function starExportSpecOf(call: ts.CallExpression, sf: ts.SourceFile): string | if (!sourceSpellsIdentifier(nameNode, sf)) return null; const arg0 = call.arguments.nodes[0]; if (arg0 === undefined) return null; - if (!byteAdjacentParen(sf, nameNode.getEnd(), arg0.getStart(sf))) return null; - return leadingRequireOf(arg0, sf)?.spec ?? null; + if (!byteAdjacentParen(sf, nameNode.end, tokenStartOfNode(arg0))) return null; + return leadingRequireOf(arg0)?.spec ?? null; } /** True when the bytes between a callee name ending at `nameEnd` and its @@ -384,7 +382,7 @@ function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, s init.arguments.nodes.length >= 1 ) { const arg0 = init.arguments.nodes[0]!; - if (!byteAdjacentParen(sf, init.expression.getEnd(), arg0.getStart(sf))) return null; + if (!byteAdjacentParen(sf, init.expression.end, tokenStartOfNode(arg0))) return null; const spec = bareRequireSpecOf(arg0); if (spec !== null) return [decl.name.text, spec]; } @@ -717,15 +715,16 @@ export function cjsLexedExportsOfFile(sf: ts.SourceFile): CjsLexedExports { ) { exports.add(lhs.argumentExpression.text); } else if (isModuleExports(lhs)) { - events.push({ pos: n.getStart(sf), kind: "assign", rhs: n.right }); + events.push({ pos: tokenStartOfNode(n), kind: "assign", rhs: n.right }); } } else if (ts.isCallExpression(n)) { scanDefineProperty(n, sf, exports); if (braceDepth === 0) { const starSpec = starExportSpecOf(n, sf); - if (starSpec !== null) events.push({ pos: n.getStart(sf), kind: "spec", spec: starSpec }); + if (starSpec !== null) + events.push({ pos: tokenStartOfNode(n), kind: "spec", spec: starSpec }); const loopId = starLoopIdOf(n, sf); - if (loopId !== null) events.push({ pos: n.getStart(sf), kind: "loop", id1: loopId }); + if (loopId !== null) events.push({ pos: tokenStartOfNode(n), kind: "loop", id1: loopId }); } } else if (ts.isVariableStatement(n) && braceDepth === 0) { const assign = starAssignOf(n, sf); @@ -745,7 +744,7 @@ export function cjsLexedExportsOfFile(sf: ts.SourceFile): CjsLexedExports { for (const spec of starAssigns.get(ev.id1) ?? []) reexports.push(spec); } else { reexports = []; // every `module.exports =` clears — stars included (probed) - const req = leadingRequireOf(ev.rhs, sf); + const req = leadingRequireOf(ev.rhs); if (req !== null) { reexports.push(req.spec); } else if (ts.isObjectLiteralExpression(ev.rhs)) { diff --git a/packages/compiler/src/frontend/contract-types.ts b/packages/compiler/src/frontend/contract-types.ts index c30788fa1..4ded2a1a0 100644 --- a/packages/compiler/src/frontend/contract-types.ts +++ b/packages/compiler/src/frontend/contract-types.ts @@ -130,7 +130,7 @@ function markerNamed( function nameText(node: ts.Node | undefined): string { if (node === undefined) return ""; if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) return node.text; - return node.getText(); + return ts.getTextOfNode(node); } function contractDiag( diff --git a/packages/compiler/src/frontend/experimental-import.ts b/packages/compiler/src/frontend/experimental-import.ts index 8fc2238bf..3b35a196a 100644 --- a/packages/compiler/src/frontend/experimental-import.ts +++ b/packages/compiler/src/frontend/experimental-import.ts @@ -73,7 +73,7 @@ export function lifetimeRegionMemberOf( if ( !ts.isClassDeclaration(owner) || owner.name?.text !== "LifetimeRegion" || - !isExperimentalDeclarationFile(owner.getSourceFile(), known) + !isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known) ) return null; const name = access.name.text; @@ -105,7 +105,7 @@ export function internTableMemberOf( if ( !ts.isClassDeclaration(owner) || owner.name?.text !== "InternTable" || - !isExperimentalDeclarationFile(owner.getSourceFile(), known) + !isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known) ) return null; const name = access.name.text; @@ -135,7 +135,7 @@ export function threadSlotsMemberOf( if ( !ts.isClassDeclaration(owner) || owner.name?.text !== "ThreadSlots" || - !isExperimentalDeclarationFile(owner.getSourceFile(), known) + !isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known) ) return null; const name = access.name.text; diff --git a/packages/compiler/src/frontend/fork-target.test.ts b/packages/compiler/src/frontend/fork-target.test.ts index 7a3e23c9b..5e56cea5d 100644 --- a/packages/compiler/src/frontend/fork-target.test.ts +++ b/packages/compiler/src/frontend/fork-target.test.ts @@ -7,6 +7,7 @@ import { fallbackDtsPath } from "./dts-paths.js"; import { Ts7Host } from "./ts7/program-host.js"; import { Ts7Api } from "./ts7/rpc-api.js"; import { ModuleKind, ModuleResolutionKind } from "./ts7/enums.js"; +import { getTextOfNode } from "./ts7/node-functions.js"; test("fork discovery batches only candidate bindings and preserves aliases and shadowing", () => { const directory = mkdtempSync(join(tmpdir(), "scriptc-fork-discovery-")); @@ -49,7 +50,7 @@ test("fork discovery batches only candidate bindings and preserves aliases and s ["first.ts", "second.ts", "third.ts"].map((name) => join(directory, name)), ); expect(batch).toHaveBeenCalledTimes(1); - expect(batch.mock.calls[0]![0].map((node) => node.getText())).toEqual([ + expect(batch.mock.calls[0]![0].map((node) => getTextOfNode(node))).toEqual([ "start", "child", "launch", diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts index 592bbb2d6..38c8f5f07 100644 --- a/packages/compiler/src/frontend/fork-target.ts +++ b/packages/compiler/src/frontend/fork-target.ts @@ -12,7 +12,7 @@ export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): bo return ( declarations.length > 0 && declarations.every((declaration) => { - const source = declaration.getSourceFile(); + const source = ts.getSourceFileOfNode(declaration); return ( source.isDeclarationFile && (tsgoPath(source.fileName) === fallbackDtsPath() || isNodeTypesPath(source.fileName)) @@ -61,7 +61,7 @@ export function isBuiltinMemberImport( if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = checker.getAliasedSymbol(symbol); const targetDecl = target.declarations[0]; - if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) { + if (targetDecl !== undefined && ts.getSourceFileOfNode(targetDecl).isDeclarationFile) { for ( let parent: ts.Node | undefined = targetDecl.parent; parent !== undefined && !ts.isSourceFile(parent); @@ -69,7 +69,8 @@ export function isBuiltinMemberImport( ) { if (ts.isModuleDeclaration(parent) && ts.isStringLiteral(parent.name)) { return ( - canonicalBuiltinModule(parent.name.text) === moduleName && target.name === memberName + canonicalBuiltinModule(parent.name.text) === moduleName && + ts.symbolName(target) === memberName ); } } @@ -188,7 +189,7 @@ function staticString( ): string | null { const current = strip(expr); if (ts.isStringLiteralLike(current)) return current.text; - if (importMetaUrl(current)) return pathToFileURL(current.getSourceFile().fileName).href; + if (importMetaUrl(current)) return pathToFileURL(ts.getSourceFileOfNode(current).fileName).href; if (ts.isTemplateExpression(current)) { let value = current.head.text; for (const span of current.templateSpans.nodes) { diff --git a/packages/compiler/src/frontend/inferred-js-diagnostics.ts b/packages/compiler/src/frontend/inferred-js-diagnostics.ts index 7095cea5d..b21d19cb0 100644 --- a/packages/compiler/src/frontend/inferred-js-diagnostics.ts +++ b/packages/compiler/src/frontend/inferred-js-diagnostics.ts @@ -16,7 +16,7 @@ export function inferredJsDiagnosticSuppressed( const checker = program.getTypeChecker(); let suppressed = false; ts.walkPreorder(file, (node) => { - if (node.getEnd() < diagnostic.pos || node.getStart(file) > diagnostic.end) return "skip"; + if (node.end < diagnostic.pos || ts.tokenStartOfNode(node) > diagnostic.end) return "skip"; if ( diagnostic.code === 2565 && isJsSourceFileName(file.fileName) && @@ -30,7 +30,7 @@ export function inferredJsDiagnosticSuppressed( ts.isPropertyDeclaration(decl) && !decl.type && !decl.initializer && - !/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))), + !/@type\b/.test(file.text.slice(decl.pos, ts.tokenStartOfNode(decl))), ) ) { // Bare JS fields exist with value undefined before the first write; @@ -44,14 +44,14 @@ export function inferredJsDiagnosticSuppressed( const declaration = signature && signature.declaration; if ( declaration && - isJsSourceFileName(declaration.getSourceFile().fileName) && + isJsSourceFileName(ts.getSourceFileOfNode(declaration).fileName) && (ts.isFunctionDeclaration(declaration) || ts.isFunctionExpression(declaration) || ts.isMethodDeclaration(declaration)) && node.arguments.nodes.length > declaration.parameters.nodes.length && - node.arguments.nodes[declaration.parameters.nodes.length]?.getStart(file) === + ts.tokenStartOfNode(node.arguments.nodes[declaration.parameters.nodes.length]!) === diagnostic.pos && - node.arguments.nodes[node.arguments.nodes.length - 1]?.getEnd() === diagnostic.end && + node.arguments.nodes[node.arguments.nodes.length - 1]!.end === diagnostic.end && bodyReadsArguments(declaration) ) { suppressed = true; @@ -68,7 +68,7 @@ export function inferredJsDiagnosticSuppressed( const iterable = checker.getTypeAtLocation(node.expression); const iterator = checker .getPropertiesOfType(iterable) - .find((property) => property.name.startsWith("__@iterator@")); + .find((property) => ts.symbolName(property).startsWith("__@iterator@")); if (!iterator) return undefined; const signatures = checker.getCallSignatures( checker.getTypeOfSymbolAtLocation(iterator, node.expression), @@ -88,7 +88,7 @@ export function inferredJsDiagnosticSuppressed( declaration && ts.isMethodDeclaration(declaration) && declaration.type === undefined && - isJsSourceFileName(declaration.getSourceFile().fileName) && + isJsSourceFileName(ts.getSourceFileOfNode(declaration).fileName) && declaration.body?.statements?.nodes.length === 1 && ts.isThrowStatement(declaration.body.statements.nodes[0]!) ) { diff --git a/packages/compiler/src/frontend/lib-contract.ts b/packages/compiler/src/frontend/lib-contract.ts index 0432d06e2..28532ecd8 100644 --- a/packages/compiler/src/frontend/lib-contract.ts +++ b/packages/compiler/src/frontend/lib-contract.ts @@ -115,7 +115,7 @@ const CONTRACT_GLOBAL_TYPES = new Set(["Array", "ReadonlyArray", "Uint8Array"]); type TypeBindings = Map>; function locOf(file: ts.SourceFile, node: ts.Node): SrcLoc { - return { file: file.fileName, start: node.getStart(), end: node.end }; + return { file: file.fileName, start: ts.tokenStartOfNode(node), end: node.end }; } function isExported(stmt: ts.Node): boolean { @@ -248,7 +248,7 @@ function typeShape( const lit = node.literal; if (ts.isStringLiteral(lit)) return { k: "stringLit", text: lit.text }; if (lit.kind === ts.SyntaxKind.NullKeyword) return { k: "absent", unit: "null" }; - return { k: "unsupported", text: node.getText(file) }; + return { k: "unsupported", text: ts.getTextOfNode(node) }; } if (ts.isArrayTypeNode(node)) return { k: "array", elem: typeShape(file, node.elementType, bindings) }; @@ -280,15 +280,19 @@ function typeShape( if (ts.isConditionalTypeNode(node)) { return { k: "unsupported", - text: `a conditional type (${node.getText(file)})`, + text: `a conditional type (${ts.getTextOfNode(node)})`, computed: "conditional", }; } if (ts.isMappedTypeNode(node)) { - return { k: "unsupported", text: `a mapped type (${node.getText(file)})`, computed: "mapped" }; + return { + k: "unsupported", + text: `a mapped type (${ts.getTextOfNode(node)})`, + computed: "mapped", + }; } if (ts.isTypeReferenceNode(node)) { - if (!ts.isIdentifier(node.typeName)) return { k: "unsupported", text: node.getText(file) }; + if (!ts.isIdentifier(node.typeName)) return { k: "unsupported", text: ts.getTextOfNode(node) }; const name = node.typeName.text; const global = CONTRACT_GLOBAL_TYPES.has(name) && isUnshadowedGlobalType(file, node, name, bindings); @@ -302,10 +306,10 @@ function typeShape( return { k: "array", elem: typeShape(file, node.typeArguments.nodes[0]!, bindings) }; } if ((node.typeArguments?.nodes.length ?? 0) > 0) - return { k: "unsupported", text: node.getText(file) }; + return { k: "unsupported", text: ts.getTextOfNode(node) }; return { k: "ref", name }; } - return { k: "unsupported", text: node.getText(file) }; + return { k: "unsupported", text: ts.getTextOfNode(node) }; } /** Unwrap `expr as const` / parenthesized initializers. */ @@ -362,7 +366,7 @@ function envMsgArray(expr: ts.Expression): { env: string; msg: string }[] | null /** One mergeable declaration site, `file:line` (1-based line). */ function siteOf(file: ts.SourceFile, node: ts.Node): string { - return `${file.fileName}:${ts.getLineAndCharacterOfPosition(file, node.getStart()).line + 1}`; + return `${file.fileName}:${ts.getLineAndCharacterOfPosition(file, ts.tokenStartOfNode(node)).line + 1}`; } /** Everything the sidecar projection needs from the program's syntax diff --git a/packages/compiler/src/frontend/lib-exports.ts b/packages/compiler/src/frontend/lib-exports.ts index 738dd2f19..6a42074bd 100644 --- a/packages/compiler/src/frontend/lib-exports.ts +++ b/packages/compiler/src/frontend/lib-exports.ts @@ -35,7 +35,7 @@ export function entryFunctionExports(entry: ts.SourceFile): Map 0, async: mods?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true, generator: stmt.asteriskToken !== undefined, diff --git a/packages/compiler/src/frontend/lowering/assert-instances.ts b/packages/compiler/src/frontend/lowering/assert-instances.ts index ee8a34ea2..1166c6afa 100644 --- a/packages/compiler/src/frontend/lowering/assert-instances.ts +++ b/packages/compiler/src/frontend/lowering/assert-instances.ts @@ -194,7 +194,7 @@ function onlyMethodCallReferences(lowerer: Lowerer, name: ts.Identifier): boolea } ts.forEachChild(node, visit); }; - visit(name.getSourceFile()); + visit(ts.getSourceFileOfNode(name)); return ok; } diff --git a/packages/compiler/src/frontend/lowering/assertion-members.ts b/packages/compiler/src/frontend/lowering/assertion-members.ts index d61c379d6..484963fa8 100644 --- a/packages/compiler/src/frontend/lowering/assertion-members.ts +++ b/packages/compiler/src/frontend/lowering/assertion-members.ts @@ -79,7 +79,7 @@ export function extendAssertedRecordMembers( symbol.declarations.length > 0 && symbol.declarations.every( (d) => - !lowerer.isStdlibFile(d.getSourceFile()) && + !lowerer.isStdlibFile(ts.getSourceFileOfNode(d)) && (ts.isTypeLiteralNode(d) || ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)), diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index 88b0f91dc..83b9aa6f9 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -16,7 +16,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t let written = false; // Text pre-check keeps the file walk cheap: only same-named // identifiers pay a symbol resolution. - const symText = sym.name; + const symText = ts.symbolName(sym); const namesSym = (e: ts.Node): boolean => ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym; const scanTarget = (t: ts.Expression): void => { @@ -31,7 +31,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) { const walk = (n: ts.Node): void => { if (namesSym(n)) written = true; - else n.forEachChild(walk); + else ts.forEachChild(n, walk); }; walk(e); } @@ -54,9 +54,9 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t ) { scanTarget(n.initializer as ts.Expression); } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; - decl.getSourceFile().forEachChild(visit); + ts.forEachChild(ts.getSourceFileOfNode(decl), visit); lowerer.neverReassignedCache.set(sym, !written); return !written; } @@ -123,7 +123,7 @@ export function nullishValueUnitOf( !decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || - decl.getSourceFile().isDeclarationFile || + ts.getSourceFileOfNode(decl).isDeclarationFile || decl.initializer === undefined || !ts.isVariableStatement(decl.parent?.parent) ) { @@ -154,7 +154,7 @@ export function nullishValueUnitOf( /** True when any identifier resolving to `sym` sits inside a class * heritage clause anywhere in the program. */ function usedInHeritageClause(lowerer: Lowerer, sym: ts.Symbol): boolean { - const symText = sym.name; + const symText = ts.symbolName(sym); let found = false; const visit = (n: ts.Node): void => { if (found) return; @@ -171,12 +171,12 @@ function usedInHeritageClause(lowerer: Lowerer, sym: ts.Symbol): boolean { } return; } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; for (const file of lowerer.program.getImplementationSourceFiles()) { if (found) break; if (file.isDeclarationFile) continue; - file.forEachChild(visit); + ts.forEachChild(file, visit); } return found; } @@ -236,7 +236,7 @@ export function nullishGenericBindingUnitOf( * value knowable. Compound assignments, ++/--, for-in/of cursors, and * destructuring targets all disqualify. */ function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): boolean { - const symText = sym.name; + const symText = ts.symbolName(sym); const namesSym = (e: ts.Node): boolean => ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym; let ok = true; @@ -253,7 +253,7 @@ function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): bool } else if (ts.isArrayLiteralExpression(lhs) || ts.isObjectLiteralExpression(lhs)) { const walk = (m: ts.Node): void => { if (namesSym(m)) ok = false; - else m.forEachChild(walk); + else ts.forEachChild(m, walk); }; walk(lhs); } @@ -274,9 +274,9 @@ function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): bool t = (t as ts.ParenthesizedExpression).expression; if (namesSym(t)) ok = false; } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; - decl.getSourceFile().forEachChild(visit); + ts.forEachChild(ts.getSourceFileOfNode(decl), visit); return ok; } @@ -332,7 +332,7 @@ export function deadUnmappableBinding( // the no-storage family never claims it (catch bindings sit outside a // variable statement too and stay out the same way). if (!ts.isVariableStatement(decl.parent?.parent)) return false; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (sf.isDeclarationFile || isJsSourceFile(sf)) return false; if (decl.initializer !== undefined && !sideEffectFreeValueExpr(lowerer, decl.initializer)) return false; @@ -344,7 +344,7 @@ export function deadUnmappableBinding( // bug), so only survivors of the syntactic filters pay it. Mappable // types keep their real storage. if (lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(sym)) !== null) return false; - const symText = sym.name; + const symText = ts.symbolName(sym); const namesSym = (e: ts.Node): boolean => ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym; let dead = true; @@ -353,7 +353,7 @@ export function deadUnmappableBinding( if (ts.isIdentifier(n) && n.text === symText) { // Declaration-name occurrences are not reads. if (n.parent !== undefined && ts.isVariableDeclaration(n.parent) && n.parent.name === n) { - n.forEachChild(visit); + ts.forEachChild(n, visit); return; } // A plain-assignment LHS is a WRITE — dead only when the RHS @@ -375,12 +375,12 @@ export function deadUnmappableBinding( if (namesSym(n)) dead = false; return; } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; for (const file of lowerer.program.getImplementationSourceFiles()) { if (!dead) break; if (file.isDeclarationFile) continue; - file.forEachChild(visit); + ts.forEachChild(file, visit); } if (dead) lowerer.deadBindings.add(sym); return dead; diff --git a/packages/compiler/src/frontend/lowering/builtin-function-fences.ts b/packages/compiler/src/frontend/lowering/builtin-function-fences.ts index e6221384b..312596c1f 100644 --- a/packages/compiler/src/frontend/lowering/builtin-function-fences.ts +++ b/packages/compiler/src/frontend/lowering/builtin-function-fences.ts @@ -25,7 +25,7 @@ export function builtinFunctionFenceValue( `library functions as values ('${display}' has no function-value lowering; call it directly)`, ); lowerer.runtimeFences.push(diag); - const line = ts.getLineAndCharacterOfPosition(node.getSourceFile(), loc.start).line + 1; + const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1; lowerer.liftedFns.push({ name, params: [], diff --git a/packages/compiler/src/frontend/lowering/builtins/async-context.ts b/packages/compiler/src/frontend/lowering/builtins/async-context.ts index e6de99b09..18bd93a86 100644 --- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts +++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts @@ -35,7 +35,7 @@ export function lowerDiagnosticsSubscriber(lowerer: Lowerer, node: ts.Expression if (ts.isIdentifier(node) && node.text === "setImmediate") { const sym = lowerer.checker.getSymbolAtLocation(node); const decls = sym ? sym.declarations : []; - if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) { + if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))) { return { kind: "libCall", fn: "timers.setImmediateFnValue", diff --git a/packages/compiler/src/frontend/lowering/builtins/blob.ts b/packages/compiler/src/frontend/lowering/builtins/blob.ts index 3fc4ec173..94c8c7ffe 100644 --- a/packages/compiler/src/frontend/lowering/builtins/blob.ts +++ b/packages/compiler/src/frontend/lowering/builtins/blob.ts @@ -52,10 +52,10 @@ export function blobClassOf(lowerer: Lowerer, node: ts.Expression): BlobClass | function blobReceiverClass(lowerer: Lowerer, node: ts.Expression): BlobClass | null { const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node)); const sym = type.getAliasSymbol() ?? type.getSymbol(); - if (!sym || (sym.name !== "Blob" && sym.name !== "File") || !lowerer.isStdlibSymbol(sym)) { - return null; - } - return sym.name; + if (!sym) return null; + const name = ts.symbolName(sym); + if ((name !== "Blob" && name !== "File") || !lowerer.isStdlibSymbol(sym)) return null; + return name; } function memberNameOf(access: ts.PropertyAccessExpression | ts.ElementAccessExpression) { diff --git a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts index 07113eee7..eaa08c9dd 100644 --- a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts +++ b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts @@ -140,7 +140,7 @@ export function lowerBuiltinClassCall(lowerer: Lowerer, call: ts.CallExpression) kind: "dynInvoke", recv: receiver, method, - calleeName: callee.getText(), + calleeName: ts.getTextOfNode(callee), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, diff --git a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts index afb221d04..661fec528 100644 --- a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts +++ b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts @@ -50,7 +50,7 @@ export function registerBuiltinCallableAlias( // These checked native functions retain a runtime slot in JavaScript so // aliases can escape without narrowing their arguments to string. if ( - isJsSourceFile(nameNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(nameNode)) && target.module === "util" && ["stripVTControlCharacters", "toUSVString", "isDeepStrictEqual", "styleText"].includes( target.member, diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts index e1a25d169..af7270ea9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts @@ -122,12 +122,14 @@ export function lowerChildMethodCall( } const listenerNode = stripTypeCasts(call.arguments.nodes[1]!); if ( - isJsSourceFile(listenerNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) && (ts.isArrowFunction(listenerNode) || ts.isFunctionExpression(listenerNode)) && listenerNode.parameters.nodes.some((param) => param.dotDotDotToken) && listenerNode.parameters.nodes.every((param) => !param.type) && !/@(?:param|type)\b/.test( - listenerNode.getSourceFile().text.slice(listenerNode.pos, listenerNode.getStart()), + ts + .getSourceFileOfNode(listenerNode) + .text.slice(listenerNode.pos, ts.tokenStartOfNode(listenerNode)), ) ) { for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param); @@ -142,10 +144,10 @@ export function lowerChildMethodCall( : lowerer.lowerExpr(call.arguments.nodes[1]!); if ( narrowed.length > 0 || - (isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") || + (isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) && cb.type.kind === "dyn") || (cb.type.kind === "func" && (cb.type.rest || - (isJsSourceFile(listenerNode.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) && cb.type.params.some((param) => param.kind === "dyn")))) ) { if ( @@ -756,7 +758,7 @@ function childStreamEncodedAt( * admit null. */ function narrowedExitListenerParams(lowerer: Lowerer, listener: ts.Expression): ts.Symbol[] { if (!ts.isArrowFunction(listener) && !ts.isFunctionExpression(listener)) return []; - if (isJsSourceFile(listener.getSourceFile())) return []; + if (isJsSourceFile(ts.getSourceFileOfNode(listener))) return []; const symbols: ts.Symbol[] = []; const wanted: readonly string[] = ["f64", "string"]; listener.parameters.nodes.slice(0, 2).forEach((param, i) => { diff --git a/packages/compiler/src/frontend/lowering/builtins/class-values.ts b/packages/compiler/src/frontend/lowering/builtins/class-values.ts index 416217716..4d073c264 100644 --- a/packages/compiler/src/frontend/lowering/builtins/class-values.ts +++ b/packages/compiler/src/frontend/lowering/builtins/class-values.ts @@ -97,7 +97,7 @@ export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol) if (decl === undefined || !ts.isVariableDeclaration(decl)) return false; if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false; if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false; - if (!isJsSourceFile(decl.getSourceFile())) return true; + if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return true; let init = decl.initializer; while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression; if (init === undefined) return true; @@ -157,9 +157,10 @@ export function runtimeClassValueOfType(lowerer: Lowerer, type: ts.Type): string const symbol = type.getSymbol(); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; let info = lowerer.builtinEmitterInfoOf(symbol) ?? lowerer.builtinStreamInfoOf(symbol); - if (!info && symbol.name.endsWith("Constructor")) { + if (!info && ts.symbolName(symbol).endsWith("Constructor")) { for (const [irName, rec] of RUNTIME_ERROR_CLASSES) { - if (`${rec.lib}Constructor` === symbol.name) info = lowerer.classes.get(irName) ?? null; + if (`${rec.lib}Constructor` === ts.symbolName(symbol)) + info = lowerer.classes.get(irName) ?? null; } } return info?.valueCtorParams !== undefined ? info.def.name : null; diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index e69011522..717e09933 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -136,7 +136,7 @@ export function zlibStreamModeOf( if (alias !== null) return alias.kind === "zlib" ? alias.mode : null; const target = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - const mode = ZLIB_STREAM_MODES.get(target.name); + const mode = ZLIB_STREAM_MODES.get(ts.symbolName(target)); if (mode === undefined) return null; return target.declarations.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d)) ? mode @@ -144,7 +144,7 @@ export function zlibStreamModeOf( } function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean { - const file = d.getSourceFile(); + const file = ts.getSourceFileOfNode(d); if (!lowerer.isStdlibFile(file) || isNodeTypesPath(file.fileName)) return false; for (let node: ts.Node | undefined = d.parent; node; node = node.parent) { if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) { diff --git a/packages/compiler/src/frontend/lowering/builtins/constants.ts b/packages/compiler/src/frontend/lowering/builtins/constants.ts index 611805d94..35f0fc095 100644 --- a/packages/compiler/src/frontend/lowering/builtins/constants.ts +++ b/packages/compiler/src/frontend/lowering/builtins/constants.ts @@ -170,7 +170,7 @@ export function fsConstantMemberRead(expr: ts.PropertyAccessExpression): IrExpr type: DYN, loc, }; - return isJsSourceFile(expr.getSourceFile()) + return isJsSourceFile(ts.getSourceFileOfNode(expr)) ? read : { kind: "dynCheck", value: read, type: F64, loc }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index cd490bc87..e6834abd4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -240,7 +240,9 @@ function cryptoKdfInput(lowerer: Lowerer, node: ts.Expression): IrExpr { const symbol = lowerer.typeOf(node).getSymbol(); if ( type?.kind === "bytes" || - (symbol?.name === "ArrayBuffer" && lowerer.isStdlibSymbol(symbol)) + (symbol !== undefined && + ts.symbolName(symbol) === "ArrayBuffer" && + lowerer.isStdlibSymbol(symbol)) ) { const value = lowerer.coerceInto(node, lowerer.lowerExpr(node), DYN); return { kind: "libCall", fn: "bytes.bufferSource", args: [value], type: BYTES_U8, loc }; @@ -878,8 +880,8 @@ export function isCipherType(lowerer: Lowerer, type: ts.Type): boolean { const symbol = type.getAliasSymbol() ?? type.getSymbol(); return ( !!symbol && - CIPHER_TYPE_NAMES.has(symbol.name) && - symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())) + CIPHER_TYPE_NAMES.has(ts.symbolName(symbol)) && + symbol.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) ); } @@ -898,7 +900,7 @@ function v8CalleeText(node: ts.Expression): string { if (ts.isPropertyAccessExpression(node)) return `${v8CalleeText(node.expression)}.${node.name.text}`; if (ts.isCallExpression(node)) return `${v8CalleeText(node.expression)}(...)`; - return node.getText(); + return ts.getTextOfNode(node); } /** Calls on native Cipheriv/Decipheriv handles dispatch through the @@ -959,7 +961,7 @@ export function lowerCryptoHashMethodCall( return { kind: "seqExpr", stmts: evaluated, - result: nodeThrowExpr(1, "", `${access.getText()} is not a function`, DYN, loc), + result: nodeThrowExpr(1, "", `${ts.getTextOfNode(access)} is not a function`, DYN, loc), type: DYN, loc, }; diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index fb1c92257..1b9a9c03a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -9,7 +9,12 @@ import { varRef } from "../../../ir/build.js"; function isStdlibDisposableStack(lowerer: Lowerer, node: ts.Expression): boolean { const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node)); for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) { - if (symbol?.name === "DisposableStack" && lowerer.isStdlibSymbol(symbol)) return true; + if ( + symbol !== undefined && + ts.symbolName(symbol) === "DisposableStack" && + lowerer.isStdlibSymbol(symbol) + ) + return true; } return false; } diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 439c9fca3..d95442829 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -27,9 +27,10 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet = new Set([ export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean { const sym = t.getSymbol(); return ( - sym?.name === "ExecException" && + sym !== undefined && + ts.symbolName(sym) === "ExecException" && sym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 3fbea04aa..49875e004 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -250,7 +250,7 @@ export function lowerFsLadderCall( ); } if (bi.module !== "fs" && bi.module !== "fs/promises") return null; - if (!isJsSourceFile(expr.getSourceFile())) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; const args = expr.arguments.nodes; if (args.some(ts.isSpreadElement)) return null; // Every ladder argument crosses as a dyn value; an argument that @@ -1564,7 +1564,7 @@ export function lowerDirentMethodCall( if (call.questionDotToken || access.questionDotToken) return null; if (!lowerer.isStdlibMember(access)) return null; const recvSym = lowerer.typeOf(access.expression).getSymbol(); - if (recvSym?.name !== "Dirent") return null; + if (recvSym === undefined || ts.symbolName(recvSym) !== "Dirent") return null; const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "record") return null; const shape = lowerer.shapes.get(receiver.type.shapeId); diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts index 22e524690..498f9b6d4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts @@ -19,7 +19,7 @@ import { } from "../../../ir/ir.js"; function isFsFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean { - const file = d.getSourceFile(); + const file = ts.getSourceFileOfNode(d); if (!lowerer.isStdlibFile(file) || isNodeTypesPath(file.fileName)) return false; for (let node: ts.Node | undefined = d.parent; node; node = node.parent) { if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) { @@ -153,11 +153,13 @@ export function fsStreamClassOf( lowerer: Lowerer, symbol: ts.Symbol | null | undefined, ): "ReadStream" | "WriteStream" | null { - if (!symbol || (symbol.name !== "ReadStream" && symbol.name !== "WriteStream")) return null; + if (!symbol) return null; + const name = ts.symbolName(symbol); + if (name !== "ReadStream" && name !== "WriteStream") return null; const declared = symbol.declarations.some( (d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d), ); - return declared ? symbol.name : null; + return declared ? name : null; } /** `new fs.ReadStream(path, options)` / `new fs.WriteStream(...)`: the diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index b48030ba5..54bcae4b5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -504,7 +504,7 @@ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(node); return ( symbol !== undefined && - symbol.declarations.every((decl) => lowerer.isStdlibFile(decl.getSourceFile())) + symbol.declarations.every((decl) => lowerer.isStdlibFile(ts.getSourceFileOfNode(decl))) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/key-object.ts b/packages/compiler/src/frontend/lowering/builtins/key-object.ts index ff6c0b38b..7e680fa20 100644 --- a/packages/compiler/src/frontend/lowering/builtins/key-object.ts +++ b/packages/compiler/src/frontend/lowering/builtins/key-object.ts @@ -23,7 +23,7 @@ export function isKeyObjectClass(lowerer: Lowerer, node: ts.Expression): boolean export function isKeyObjectValue(lowerer: Lowerer, node: ts.Expression): boolean { const type = lowerer.typeOf(node); const sym = type.getAliasSymbol() ?? type.getSymbol(); - return !!sym && sym.name === "KeyObject" && lowerer.isStdlibSymbol(sym); + return !!sym && ts.symbolName(sym) === "KeyObject" && lowerer.isStdlibSymbol(sym); } /** `v instanceof KeyObject`. */ @@ -51,7 +51,7 @@ export function lowerKeyObjectCall(lowerer: Lowerer, call: ts.CallExpression): I kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN), method: access.name.text, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc: locOf(call), diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 8a5365d18..5465aeafa 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -135,7 +135,7 @@ function builtinModuleBindingOf( if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = lowerer.checker.getAliasedSymbol(symbol); const tdecl = target.declarations[0]; - if (tdecl !== undefined && tdecl.getSourceFile().isDeclarationFile) { + if (tdecl !== undefined && ts.getSourceFileOfNode(tdecl).isDeclarationFile) { for ( let p: ts.Node | undefined = tdecl.parent; p !== undefined && !ts.isSourceFile(p); @@ -143,7 +143,7 @@ function builtinModuleBindingOf( ) { if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) { const module = canonicalBuiltinModule(p.name.text); - if (module !== null) return { module, member: target.name }; + if (module !== null) return { module, member: ts.symbolName(target) }; break; } } @@ -297,14 +297,14 @@ export function createRequireCalleeFileOf( ): ts.SourceFile | null { const e = stripTypeCasts(callee); if (ts.isCallExpression(e)) { - return createRequireBaseCallOf(lowerer, e) !== null ? e.getSourceFile() : null; + return createRequireBaseCallOf(lowerer, e) !== null ? ts.getSourceFileOfNode(e) : null; } if (!ts.isIdentifier(e)) return null; const symbol = lowerer.checker.getSymbolAtLocation(e); const decl = symbol ? symbol.declarations[0] : undefined; if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null; return createRequireBindingDecl(lowerer, decl.name, decl.initializer) - ? decl.getSourceFile() + ? ts.getSourceFileOfNode(decl) : null; } diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index 895c1a9d6..c85f2c777 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -57,7 +57,7 @@ export function lowerBuiltinModuleCall( fn: BuiltinModuleFn, loc: SrcLoc, ): IrExpr { - const name = expr.expression.getText(); + const name = ts.getTextOfNode(expr.expression); if (bi.module === "util/types") { return lowerUtilTypeCall(lowerer, expr, bi, loc, name); } diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts index afa9dc1ea..6b2e9e589 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts @@ -292,7 +292,7 @@ export function lowerImportMetaResolveCall( ); } const result = resolveImportMetaRuntime( - call.getSourceFile().fileName, + ts.getSourceFileOfNode(call).fileName, specifier, lowerer.targetPlatform, lowerer.frontendServices, @@ -314,8 +314,8 @@ function requireResolverBaseFile(lowerer: Lowerer, receiver: ts.Expression): ts. const created = createRequireCalleeFileOf(lowerer, receiver); if (created !== null) return created; return lowerer.isStdlibGlobal(receiver, "require") && - !isNodeEsmFile(receiver.getSourceFile(), lowerer.program) - ? receiver.getSourceFile() + !isNodeEsmFile(ts.getSourceFileOfNode(receiver), lowerer.program) + ? ts.getSourceFileOfNode(receiver) : null; } diff --git a/packages/compiler/src/frontend/lowering/builtins/navigator.ts b/packages/compiler/src/frontend/lowering/builtins/navigator.ts index 637edaf3c..f90448c1e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/navigator.ts +++ b/packages/compiler/src/frontend/lowering/builtins/navigator.ts @@ -20,7 +20,8 @@ export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolea if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false; if (lowerer.isStdlibGlobal(expr, "navigator")) return true; return ( - isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined + isJsSourceFile(ts.getSourceFileOfNode(expr)) && + lowerer.checker.getSymbolAtLocation(expr) === undefined ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/net-address.ts b/packages/compiler/src/frontend/lowering/builtins/net-address.ts index d99d63fd3..9e795bca6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/net-address.ts +++ b/packages/compiler/src/frontend/lowering/builtins/net-address.ts @@ -31,8 +31,10 @@ export function netAddressClassOf(lowerer: Lowerer, node: ts.Expression): NetAdd function netAddressValueClass(lowerer: Lowerer, node: ts.Expression): NetAddressClass | null { const type = lowerer.typeOf(node); const sym = type.getAliasSymbol() ?? type.getSymbol(); - if (!sym || (sym.name !== "SocketAddress" && sym.name !== "BlockList")) return null; - return lowerer.isStdlibSymbol(sym) ? sym.name : null; + if (!sym) return null; + const name = ts.symbolName(sym); + if (name !== "SocketAddress" && name !== "BlockList") return null; + return lowerer.isStdlibSymbol(sym) ? name : null; } /** `new SocketAddress(options?)` and `new BlockList()`: the argument @@ -127,7 +129,7 @@ export function lowerNetAddressCall(lowerer: Lowerer, call: ts.CallExpression): kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN), method: name, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index 7aac3b80c..72304e6a9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -285,7 +285,7 @@ function lowerPerfArguments(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL }); continue; } - const spelling = arg.expression.getText(); + const spelling = ts.getTextOfNode(arg.expression); const items = checkedIterableSpread(lowerer, lowerer.lowerExpr(arg.expression), spelling, loc); stmts.push({ kind: "exprStmt", @@ -527,7 +527,11 @@ export function fencePerfEntryKeyWalk( * receiver's checker type. Its value is the runtime histogram handle. */ function isIntervalHistogram(lowerer: Lowerer, node: ts.Expression): boolean { const symbol = lowerer.typeOf(node).getSymbol(); - return symbol?.name === "IntervalHistogram" && lowerer.isStdlibSymbol(symbol); + return ( + symbol !== undefined && + ts.symbolName(symbol) === "IntervalHistogram" && + lowerer.isStdlibSymbol(symbol) + ); } /** IntervalHistogram's numeric members, as perf.eldStat selectors. */ diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 9ac1fc059..78dc6ef14 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -192,14 +192,14 @@ export function lowerProcStreamMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (isJsSourceFile(call.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(call))) { const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind === "dyn") { return { kind: "dynInvoke", recv: receiver, method: name, - calleeName: call.expression.getText(), + calleeName: ts.getTextOfNode(call.expression), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -214,7 +214,7 @@ export function lowerProcStreamMethodCall( kind: "dynInvoke", recv: receiver, method: name, - calleeName: call.expression.getText(), + calleeName: ts.getTextOfNode(call.expression), args: [], type: DYN, loc, @@ -276,7 +276,7 @@ export function lowerProcessStreamProperty( ? lowerer.stdlibGlobalMember(expr.expression, "process") : null; if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (direct === "stdin" || direct === "stdout" || direct === "stderr" || @@ -584,7 +584,7 @@ export function lowerProcessProperty( } // A stored process.nextTick in JavaScript (`cond ? setImmediate : // process.nextTick`): the interned builtin function value. - if (member === "nextTick" && isJsSourceFile(expr.getSourceFile())) { + if (member === "nextTick" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { return builtinFunctionValue("process.nextTick", loc); } if (member === "getBuiltinModule") { @@ -667,7 +667,7 @@ export function lowerProcessProperty( // OUTER expressions dispatch first. if ( member === "stdin" || - ((member === "stdout" || member === "stderr") && isJsSourceFile(expr.getSourceFile())) + ((member === "stdout" || member === "stderr") && isJsSourceFile(ts.getSourceFileOfNode(expr))) ) { return { kind: "libCall", @@ -697,7 +697,10 @@ export function lowerProcessMethodCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken) return null; - if (isJsSourceFile(call.getSourceFile()) && ts.isPropertyAccessExpression(access.expression)) { + if ( + isJsSourceFile(ts.getSourceFileOfNode(call)) && + ts.isPropertyAccessExpression(access.expression) + ) { const stream = lowerer.stdlibGlobalMember(access.expression, "process"); if (stream === "stdin" || stream === "stdout" || stream === "stderr") { if (call.arguments.nodes.some(ts.isSpreadElement)) @@ -706,7 +709,7 @@ export function lowerProcessMethodCall( kind: "dynInvoke", recv: lowerer.lowerExpr(access.expression), method: access.name.text, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc: locOf(call), @@ -883,7 +886,7 @@ export function lowerProcessMethodCall( loc, }, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [ lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN), lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), @@ -1080,7 +1083,7 @@ export function lowerProcessMethodCall( // `writeOut: (str) => process.stdout.write(str)` — str untyped): // a runtime string writes; anything else throws the dynCheck's // honest TypeError at the call. - if (data.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) { + if (data.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(call))) { data = { kind: "dynCheck", value: data, type: STRING, loc }; } const isBytes = data.type.kind === "bytes" && data.type.elem === "u8"; @@ -1095,7 +1098,7 @@ export function lowerProcessMethodCall( let callback: IrExpr | undefined; if (callbackNode) { callback = lowerer.lowerExpr(callbackNode); - if (callback.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) { + if (callback.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(call))) { callback = { kind: "dynCheck", value: callback, diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 968d3529e..668ef01f8 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -225,7 +225,7 @@ export function lowerPromiseStaticCall( const neverEntry = (el: ts.Expression): boolean => { const t = lowerer.typeOf(el); const arg = - t.getSymbol()?.name === "Promise" + ts.symbolNameOf(t.getSymbol()) === "Promise" ? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0] : undefined; return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0; @@ -349,12 +349,12 @@ export function lowerPromiseStaticCall( return { kind: "intrinsic", name: "promise.all", args: [entriesArr], type: resultT, loc }; } - if (!isJsSourceFile(call.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(call))) { const tuple = lowerTuplePromiseAll(lowerer, call, elems); if (tuple) return tuple; } if ( - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && elems.every((entry) => lowerer.dynConvertible(entry.type)) ) { return checkedPromiseAll( @@ -375,7 +375,7 @@ export function lowerPromiseStaticCall( ); } const entries = lowerer.lowerExpr(argNode); - if (isJsSourceFile(call.getSourceFile()) && entries.type.kind === "dyn") + if (isJsSourceFile(ts.getSourceFileOfNode(call)) && entries.type.kind === "dyn") return checkedPromiseAll(lowerer, entries, loc); // loadPlugin(p)))` — the loadPlugins shape, where the checker @@ -434,7 +434,7 @@ export function lowerPromiseStaticCall( `one iterable of promises is the supported form: Promise.${member}(ps)`, ); } - if (isJsSourceFile(call.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(call))) { // JavaScript entries are checked-dynamic: any iterable, thenables // and plain values included, exactly like Promise.all. if ( @@ -706,7 +706,7 @@ export function lowerPromiseStaticCall( // Promise.resolve answers THAT promise — the same object, adopted — // and wraps every other value. if ( - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && value.type.kind === "dyn" && resultT.inner.kind === "dyn" ) { diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index bcf77a39b..ede65d788 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -46,10 +46,10 @@ function isReadlineModule(spec: string | null): boolean { export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean { const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); - if (sym?.name !== "Interface") return false; + if (sym === undefined || ts.symbolName(sym) !== "Interface") return false; return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; - if (!lowerer.isStdlibFile(d.getSourceFile())) return false; + if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false; return isReadlineModule(ambientModuleOf(d)); }); } @@ -74,7 +74,7 @@ const RL_EVENT_LIST = * readline/promises question. */ function returnsPromise(lowerer: Lowerer, call: ts.CallExpression): boolean { const t = lowerer.checker.getTypeAtLocation(call); - return (t.getSymbol()?.name ?? "") === "Promise"; + return (ts.symbolNameOf(t.getSymbol()) ?? "") === "Promise"; } /** The `{ signal }` options argument of the promise question: the signal @@ -107,10 +107,10 @@ function questionSignal(lowerer: Lowerer, node: ts.Expression | undefined): ts.E function isReadlineUiTyped(lowerer: Lowerer, node: ts.Expression): boolean { const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); - if (sym?.name !== "Readline") return false; + if (sym === undefined || ts.symbolName(sym) !== "Readline") return false; return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; - if (!lowerer.isStdlibFile(d.getSourceFile())) return false; + if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false; const spec = ambientModuleOf(d); return spec === "readline/promises" || spec === "node:readline/promises"; }); diff --git a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts index fc507e2bd..230441d31 100644 --- a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts +++ b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts @@ -10,10 +10,10 @@ export function isBuiltinClassInstance( ): boolean { const type = lowerer.checker.getTypeAtLocation(node); const symbol = type.getAliasSymbol() ?? type.getSymbol(); - if (symbol?.name !== name) return false; + if (symbol === undefined || ts.symbolName(symbol) !== name) return false; return symbol.declarations.some( (declaration) => (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && - lowerer.isStdlibFile(declaration.getSourceFile()), + lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration)), ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts b/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts index 4b61f98c1..7db3f8dcc 100644 --- a/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts +++ b/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts @@ -17,7 +17,7 @@ export function runtimeConstructorReceiver( let e = expr; while (ts.isParenthesizedExpression(e)) e = e.expression; if (!ts.isPropertyAccessExpression(e) || e.questionDotToken) return null; - if (e.name.text !== "constructor" || isJsSourceFile(e.getSourceFile())) return null; + if (e.name.text !== "constructor" || isJsSourceFile(ts.getSourceFileOfNode(e))) return null; const mapped = lowerer.mapTypeOf(lowerer.typeOf(e.expression)); if (mapped?.kind !== "object") return null; const info = lowerer.classes.get(mapped.className); diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index e89b1f07e..7229d5b84 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -360,13 +360,15 @@ export function storedTextCodecClassOf( expr: ts.Expression, ): TextCodecCtor["cls"] | null { const sym = lowerer.typeOf(expr).getSymbol(); - if (!sym || (sym.name !== "TextEncoder" && sym.name !== "TextDecoder")) return null; + if (!sym) return null; + const name = ts.symbolName(sym); + if (name !== "TextEncoder" && name !== "TextDecoder") return null; return sym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) - ? sym.name + ? name : null; } @@ -517,8 +519,9 @@ function directTextCodecCtorOf(lowerer: Lowerer, expr: ts.Expression): TextCodec if (callee.text !== "TextDecoder" && callee.text !== "TextEncoder") return null; const sym = lowerer.resolveValueSymbol(callee); if (!sym || !lowerer.isStdlibSymbol(sym)) return null; - if (sym.name !== "TextDecoder" && sym.name !== "TextEncoder") return null; - return { cls: sym.name, ctor }; + const name = ts.symbolName(sym); + if (name !== "TextDecoder" && name !== "TextEncoder") return null; + return { cls: name, ctor }; } /** Inline codec calls avoid allocating a receiver; stored receivers use diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index 9f950cddc..7230d18cd 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -154,9 +154,9 @@ function isTimerHandleTyped( ): boolean { const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); - if (sym?.name !== name) return false; + if (sym === undefined || ts.symbolName(sym) !== name) return false; return sym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ); } function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean { diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index 3de973335..dca8a7cc0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -842,7 +842,7 @@ export function lowerLegacyUrlCall( if (bi.member === "domainToASCII" || bi.member === "domainToUnicode") { args.push({ kind: "boolLit", value: expr.arguments.nodes.length === 0, type: BOOL, loc }); } else { - const warn = !insideNodeModules(expr.getSourceFile().fileName); + const warn = !insideNodeModules(ts.getSourceFileOfNode(expr).fileName); args.push({ kind: "boolLit", value: warn, type: BOOL, loc }); } return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts index 2a9ff9eef..93cba84c5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -103,7 +103,7 @@ export function fenceWebCryptoMember( use: "read" | "call", ): IrExpr | null { if (use !== "read" || !ts.isPropertyAccessExpression(access)) return null; - if (isJsSourceFile(access.getSourceFile())) return null; + if (isJsSourceFile(ts.getSourceFileOfNode(access))) return null; if (isSubtleReceiver(lowerer, access)) { lowerer.noLowering("crypto.subtle as a value", access, SUBTLE_HINT); } @@ -123,8 +123,8 @@ export function isWebCryptoMemberType(lowerer: Lowerer, type: ts.Type): boolean const symbol = type.getAliasSymbol() ?? type.getSymbol(); return ( symbol !== undefined && - symbol.name !== "Crypto" && - WEBCRYPTO_DYN_TYPES.has(symbol.name) && + ts.symbolName(symbol) !== "Crypto" && + WEBCRYPTO_DYN_TYPES.has(ts.symbolName(symbol)) && lowerer.isStdlibSymbol(symbol) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts index 335fdb969..1426041a2 100644 --- a/packages/compiler/src/frontend/lowering/builtins/workers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts @@ -174,10 +174,10 @@ function propertyNameText(name: ts.PropertyName): string | undefined { function isWorkerThreadsClass(lowerer: Lowerer, node: ts.Expression, name: string): boolean { const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getSymbol(); - if (sym?.name !== name) return false; + if (sym === undefined || ts.symbolName(sym) !== name) return false; return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; - if (!lowerer.isStdlibFile(d.getSourceFile())) return false; + if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false; let up: ts.Node | undefined = d.parent; while (up) { if (ts.isModuleDeclaration(up) && ts.isStringLiteral(up.name)) diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index db7691a1c..49f7c8ec0 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -143,7 +143,7 @@ export function completeArgs( loc: value.loc, }; } - value = checkedIterableSpread(lowerer, value, arg.expression.getText(), loc); + value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc); if (spreads === 1 && index === argNodes.length - 1) { stmts.push({ kind: "exprStmt", @@ -153,7 +153,7 @@ export function completeArgs( args: [ ref(), value, - { kind: "strLit", value: arg.expression.getText(), type: STRING, loc }, + { kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc }, ], type: VOID, loc, @@ -605,7 +605,7 @@ export function lowerSpreadArgsCall( // TypeScript reaches this lane only for checked-dynamic variadic callees // (`(...args: unknown[]) => R`), whose ABI is the JS one. if ( - !isJsSourceFile(expr.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && !(callee.type.kind === "func" && callee.type.rest === true && callee.type.restAbi === undefined) ) return null; @@ -613,7 +613,7 @@ export function lowerSpreadArgsCall( if (callee.type.kind !== "dyn" && callee.type.kind !== "func") return null; const calleeName = ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression) - ? expr.expression.getText() + ? ts.getTextOfNode(expr.expression) : ts.isIdentifier(expr.expression) ? expr.expression.text : "value"; @@ -640,8 +640,8 @@ export function lowerSpreadArgsCall( if (p.spreadOf !== null) { // The spelling rides along for V8's nullish spread-call // TypeError ("v is not iterable (cannot read property ...)"). - spreads.push({ arg: args.length, what: p.spreadOf.getText() }); - args.push(checkedIterableSpread(lowerer, p.v, p.spreadOf.getText(), loc)); + spreads.push({ arg: args.length, what: ts.getTextOfNode(p.spreadOf) }); + args.push(checkedIterableSpread(lowerer, p.v, ts.getTextOfNode(p.spreadOf), loc)); } else { args.push(lowerer.coerceInto(p.node!, p.v, DYN)); } diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 954122a16..47cdd834f 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -148,7 +148,9 @@ export function documentedParameter(param: ts.ParameterDeclaration): boolean { return ( param.type !== undefined || /@(?:param|type)\b/.test( - param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()), + ts + .getSourceFileOfNode(param) + .text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)), ) ); } @@ -158,7 +160,7 @@ export function checkedJsDocumentedParameter( lowerer: Lowerer, param: ts.ParameterDeclaration, ): boolean { - return lowerer.checkedJsFile(param.getSourceFile()) && documentedParameter(param); + return lowerer.checkedJsFile(ts.getSourceFileOfNode(param)) && documentedParameter(param); } /** One parameter's ParamShape — the shared signature-shaped collection @@ -180,10 +182,10 @@ export function checkedJsDocumentedParameter( export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): ParamShape { const stage = lowerer.stageSourceType(param.name); if (stage) return { type: stage, mode: "required" }; - if (isJsSourceFile(param.getSourceFile()) && ts.isSetAccessorDeclaration(param.parent)) + if (isJsSourceFile(ts.getSourceFileOfNode(param)) && ts.isSetAccessorDeclaration(param.parent)) return { type: DYN, mode: "required" }; if ( - isJsSourceFile(param.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(param)) && ts.isConstructorDeclaration(param.parent) && !param.dotDotDotToken && !checkedJsDocumentedParameter(lowerer, param) @@ -207,12 +209,14 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa lowerer.isStdlibGlobal(construction.expression, "Promise"); if ( !nativeExecutor && - isJsSourceFile(param.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(param)) && !param.type && !param.dotDotDotToken && !ts.isSetAccessorDeclaration(param.parent) && !/@(?:param|type)\b/.test( - param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()), + ts + .getSourceFileOfNode(param) + .text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)), ) ) { return param.initializer @@ -221,10 +225,12 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa } if ( param.initializer && - isJsSourceFile(param.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(param)) && !param.type && !/@(?:param|type)\b/.test( - param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()), + ts + .getSourceFileOfNode(param) + .text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)), ) ) { return { type: DYN, mode: "omittable", bodyType: DYN }; @@ -233,10 +239,12 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa // contract. Consume the actual iterable, including surplus elements. if ( ts.isArrayBindingPattern(param.name) && - isJsSourceFile(param.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(param)) && !param.type && !/@(?:param|type)\b/.test( - param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()), + ts + .getSourceFileOfNode(param) + .text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)), ) ) { if (param.dotDotDotToken) return { type: DYN, mode: "dynRest" }; @@ -298,7 +306,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa // ARRAY param the dyn call thunk fills with the call's surplus // arguments; the binding is that array (dynRest — funcType marks // `rest`, and the value only ever calls through the boxed thunk). - if (isJsSourceFile(param.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(param))) { const restMapped = lowerer.mapTypeOf(lowerer.typeOf(param.name)); // Unmapped JS rest parameters use the native variadic checked-value ABI. @@ -325,7 +333,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa // including typed arrays passed through checked callbacks. Extracting // a native array here would copy it and discard element writes. if ( - isJsSourceFile(param.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(param)) && !param.type && (raw.kind === "array" || (raw.kind === "union" && @@ -337,7 +345,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa } let type = lowerer.runtimeOptionalBindingType(param.name, lowerer.irTypeOf(param.name)); const body = param.parent?.body; - if (isJsSourceFile(param.getSourceFile()) && body && type.kind !== "dyn") { + if (isJsSourceFile(ts.getSourceFileOfNode(param)) && body && type.kind !== "dyn") { const symbol = lowerer.checker.getSymbolAtLocation(param.name); let acceptsUndefined = false; ts.walkPreorder(body, (node) => { @@ -397,7 +405,7 @@ export function restTupleShapes( if ( param.dotDotDotToken === undefined || !ts.isIdentifier(param.name) || - isJsSourceFile(param.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(param)) ) { return null; } @@ -435,7 +443,7 @@ export function paramShapes( // rather than as the declaration's unbound type parameter. if ( blameOf !== undefined && - !isJsSourceFile(declParam.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(declParam)) && (tsType.flags & ts.TypeFlags.Any) === 0 && declParam.type !== undefined && mentionsTypeParameter(lowerer, lowerer.checker.getTypeFromTypeNode(declParam.type)) && @@ -450,7 +458,8 @@ export function paramShapes( lowerer.mapTypeOf(tsType) ?? // A TypeScript instantiation at `any` (a type argument inferred // from an untyped value) takes the checked-dynamic slot. - ((tsType.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(declParam.getSourceFile()) + ((tsType.flags & ts.TypeFlags.Any) !== 0 && + !isJsSourceFile(ts.getSourceFileOfNode(declParam)) ? DYN : lowerer.irTypeOf(declParam.name)), ), @@ -668,7 +677,7 @@ export function requireExactArityValue( const ownType = lowerer.typeOf(contextual); mappedFn = lowerer.mapTypeOf(ownType) ?? - (isJsSourceFile(contextual.getSourceFile()) + (isJsSourceFile(ts.getSourceFileOfNode(contextual)) ? dynFallbackType(lowerer, contextual, ownType) : null); } @@ -706,7 +715,7 @@ export function requireExactArityValue( target === undefined && contextual !== null && (ts.isArrowFunction(contextual) || ts.isFunctionExpression(contextual)) && - npmStaticPackageOfPath(contextual.getSourceFile().fileName) !== null + npmStaticPackageOfPath(ts.getSourceFileOfNode(contextual).fileName) !== null ) { return; } @@ -771,7 +780,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // routes generic declarations (direct calls, namespace/CJS member // calls, value references) resolves it through genericFnsBySymbol // unchanged. - if (implicitMonoFile(decl.getSourceFile()) && !isAsync && !isGenerator) { + if (implicitMonoFile(ts.getSourceFileOfNode(decl)) && !isAsync && !isGenerator) { const implicit = implicitAnyParamSymbolsOf(lowerer, decl); if (implicit) { const nameText = decl.name?.text ?? "%default"; @@ -780,7 +789,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify(decl.getSourceFile(), nsPathPrefix(decl) + nameText), + qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), typeParams: [], instances: new Map(), implicitParams: implicit, @@ -815,7 +824,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // collide. The anonymous default export takes the synthetic // "%default" spelling ('%' cannot appear in a user identifier). name: lowerer.qualify( - decl.getSourceFile(), + ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + (decl.name?.text ?? "%default"), ), params, @@ -844,7 +853,7 @@ function appendArgumentsParameter( ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) ) || - !isJsSourceFile(node.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(node)) || !bodyReadsArguments(node) ) return; @@ -854,7 +863,7 @@ function appendArgumentsParameter( (ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent)); if ( !classMethod && - !isNodeEsmFile(node.getSourceFile(), lowerer.program) && + !isNodeEsmFile(ts.getSourceFileOfNode(node), lowerer.program) && !unmappedArguments(node) ) { lowerer.unsupported( @@ -925,7 +934,7 @@ export function lambdaSignature( // actual call tuple, including overloads and omitted arguments. Node's // contextual overload signature cannot define this closure's ABI. if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && !node.type && (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) && node.parameters.nodes.every( @@ -1043,7 +1052,7 @@ export function lambdaSignature( : []; const ctxDecl = ctxSigs.length === 1 ? ctxSigs[0]!.declaration : undefined; const ctxRetRaw = - ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile + ctxDecl && !ts.getSourceFileOfNode(ctxDecl).isDeclarationFile ? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!)) : null; // An async lambda always produces a promise, so a slot returning a @@ -1065,7 +1074,7 @@ export function lambdaSignature( // must match the slot's ABI exactly. const dynVariadicSlot = ctxRet?.kind === "dyn" && - !isJsSourceFile(node.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(node)) && shapes.some((shape) => shape.mode === "dynRest"); if ( dynVariadicSlot || diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index f06dde5a2..30030b18f 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -40,12 +40,12 @@ import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js"; * source declaration) in a CommonJS JS file. */ export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): boolean { if (!ts.isIdentifier(node) || node.text !== "exports") return false; - if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false; + if (!isCjsJsFile(ts.getSourceFileOfNode(node), lowerer.program)) return false; const symbol = lowerer.checker.getSymbolAtLocation(node); const decls = symbol ? symbol.declarations : []; return !decls.some( (d) => - !d.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(d).isDeclarationFile && (ts.isVariableDeclaration(d) || ts.isParameter(d) || ts.isBindingElement(d) || @@ -62,7 +62,7 @@ export function isCjsModuleExports(lowerer: Lowerer, node: ts.Expression): boole return ( isModuleExportsAccess(node) && isCjsModuleGlobal(lowerer, node.expression) && - isCjsJsFile(node.getSourceFile(), lowerer.program) + isCjsJsFile(ts.getSourceFileOfNode(node), lowerer.program) ); } @@ -75,7 +75,7 @@ export function cjsInFileExportGlobalOf( access: ts.PropertyAccessExpression, ): IrGlobal | null { if (access.questionDotToken || !ts.isIdentifier(access.name)) return null; - const sf = access.getSourceFile(); + const sf = ts.getSourceFileOfNode(access); const recv = isModuleExportsAccess(access.expression) || (ts.isIdentifier(access.expression) && @@ -186,7 +186,7 @@ function exportsAssignments( * it runs: `exports` unless the file rebinds it; `module.exports` unless the * file rebinds `exports` or replaces `module.exports`. */ function namesOriginalExports(lowerer: Lowerer, node: ts.Expression): boolean { - const assigned = exportsAssignments(lowerer, node.getSourceFile()); + const assigned = exportsAssignments(lowerer, ts.getSourceFileOfNode(node)); if (assigned.exports) return false; return ts.isIdentifier(node) || !assigned.moduleExports; } @@ -252,7 +252,7 @@ export function usesCjsExportsIdentity(lowerer: Lowerer, sf: ts.SourceFile): boo * refusal. Null when `node` is not such a use. */ export function lowerCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): IrExpr | null { if (!isCjsExportsObjectValue(lowerer, node)) return null; - const sf = node.getSourceFile(); + const sf = ts.getSourceFileOfNode(node); // A module that replaces module.exports answers `module.exports` reads // through the replacement's own machinery (the class-expression whole // export, the replaced-exports reads). @@ -328,7 +328,7 @@ export function cjsExportMemberWriteGlobalOf( lowerer: Lowerer, target: ts.PropertyAccessExpression, ): IrGlobal | null { - const sf = target.getSourceFile(); + const sf = ts.getSourceFileOfNode(target); if (!isCjsJsFile(sf, lowerer.program)) return null; const assigned = exportsAssignments(lowerer, sf); if (assigned.exports || assigned.moduleExports) return null; @@ -458,7 +458,7 @@ export function cjsMemberAliasValueOf(lowerer: Lowerer, ref: ts.Identifier): ts. ) return null; const spec = importDecl.moduleSpecifier.text; - const from = importDecl.getSourceFile(); + const from = ts.getSourceFileOfNode(importDecl); const dep = resolveImport(lowerer.program, from, spec, "import") ?? npmStaticDepSf7(lowerer.program, from, spec, "import"); @@ -552,7 +552,7 @@ function rereadableMemberValue( const sym = lowerer.resolveValueSymbol(value); if (sym === null || sym.declarations.length !== 1) return false; const decl = sym.declarations[0]!; - if (decl.getSourceFile() !== stmt.getSourceFile()) return false; + if (ts.getSourceFileOfNode(decl) !== ts.getSourceFileOfNode(stmt)) return false; // A function declaration is initialized when the module starts; any // other binding must be initialized before the write stores it. if (!ts.isFunctionDeclaration(decl) && decl.end > stmt.pos) return false; diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 174f9dc7c..2b28c6533 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -56,7 +56,7 @@ export function inlineProgramRequireOf( if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression)) return null; const spec = requireSpecOf(e); if (spec === null || canonicalBuiltinModule(spec) !== null) return null; - const sf = e.getSourceFile(); + const sf = ts.getSourceFileOfNode(e); if (!isCjsJsFile(sf, lowerer.program)) return null; if (!isCjsRequireGlobal(lowerer, e.expression)) return null; if (createRequireCalleeFileOf(lowerer, e.expression) !== null) return null; @@ -201,7 +201,7 @@ export function lowerUncompiledRequireCall( call: ts.CallExpression, ): IrExpr | null { if (!ts.isIdentifier(call.expression) || call.expression.text !== "require") return null; - const sf = call.getSourceFile(); + const sf = ts.getSourceFileOfNode(call); if (!isCjsJsFile(sf, lowerer.program) || !isCjsRequireGlobal(lowerer, call.expression)) return null; if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null; @@ -240,7 +240,7 @@ export function requireNestedPatternsOf(decl: ts.VariableDeclaration): ts.Bindin if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return []; const spec = requireSpecOf(decl.initializer); if (spec === null || canonicalBuiltinModule(spec) !== null) return []; - if (!isJsSourceFile(decl.getSourceFile())) return []; + if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return []; return decl.name.elements.nodes.filter( (e) => e.name !== undefined && diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts index cd83c8709..aae221b30 100644 --- a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts +++ b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts @@ -175,7 +175,7 @@ export function tscInteropDefaultValueOf( export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boolean { const name = tscHelperDeclName(stmt); if (name === null) return false; - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); const helperSpans = new Map(); for (const s of sf.statements.nodes) { const n = tscHelperDeclName(s); @@ -192,11 +192,11 @@ export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boole const parent = node.parent; if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node) return undefined; - const at = node.getStart(sf); - if (at >= decl.getStart(sf) && at < decl.getEnd()) return undefined; + const at = ts.tokenStartOfNode(node); + if (at >= ts.tokenStartOfNode(decl) && at < decl.end) return undefined; for (const [other, otherDecl] of helperSpans) { if (other !== helper && dead.has(other)) { - if (at >= otherDecl.getStart(sf) && at < otherDecl.getEnd()) return undefined; + if (at >= ts.tokenStartOfNode(otherDecl) && at < otherDecl.end) return undefined; } } const call = parent; diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index 859938845..6ca685684 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -39,8 +39,8 @@ export function isClassCallback(lowerer: Lowerer, info: ClassInfo, name: string) !found.sig.abstract && (lowerer.prototypeMethodAccesses.has(name) || (info.decl !== null && - isJsSourceFile(info.decl.getSourceFile()) && - !lowerer.checkedJsFile(info.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && + !lowerer.checkedJsFile(ts.getSourceFileOfNode(info.decl)) && found.sig.params.length === 0 && found.sig.ret.kind === "void")) ); @@ -171,7 +171,7 @@ export function classCallbackCall( const spreads: { arg: number; what: string }[] = []; const args = argumentNodes.map((argument, index) => { if (!ts.isSpreadElement(argument)) return lowerer.lowerExprExpecting(argument, DYN); - const spelling = argument.expression.getText(); + const spelling = ts.getTextOfNode(argument.expression); spreads.push({ arg: index, what: spelling }); return checkedIterableSpread(lowerer, lowerer.lowerExpr(argument.expression), spelling, loc); }); diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 5fc2a3cec..de84e3fed 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1158,7 +1158,7 @@ export class ClassDynamicDispatch { if (this.bagClasses.has(plan.key)) continue; this.bagClasses.add(plan.key); this.ensurePropertyBag(info); - if (info.decl && isJsSourceFile(info.decl.getSourceFile())) { + if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) { let root = info; while (root.base && !root.base.def.runtime) root = root.base; const track = (current: ClassInfo): void => { @@ -3246,7 +3246,7 @@ export class ClassDynamicDispatch { for (const name of this.boxedConstructors) { if (this.staticBagClasses.has(name)) continue; const info = lowerer.classes.get(name); - if (!info?.decl || !isJsSourceFile(info.decl.getSourceFile())) continue; + if (!info?.decl || !isJsSourceFile(ts.getSourceFileOfNode(info.decl))) continue; const loc = this.staticPropertyBag.loc; const bag = classStaticData(lowerer, info, loc); if (!bag) continue; diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 981cd0c1a..cc8700469 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -152,10 +152,11 @@ export function collectClassMethodMutations( const required = properties.filter( (property) => (property.flags & ts.SymbolFlags.Optional) === 0, ); - if (required.some((property) => !FUNCTION_MEMBER_NAMES.has(property.name))) return true; + if (required.some((property) => !FUNCTION_MEMBER_NAMES.has(ts.symbolName(property)))) + return true; return ( required.length === 0 && - properties.every((property) => !FUNCTION_MEMBER_NAMES.has(property.name)) + properties.every((property) => !FUNCTION_MEMBER_NAMES.has(ts.symbolName(property))) ); }; // A string index whose values cannot be functions excludes callable @@ -167,7 +168,7 @@ export function collectClassMethodMutations( .getIndexInfosOfType(lowerer.typeOf(value)) .some( (index) => - (index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.valueType), + (index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.type), ); const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => { if (!ts.isIdentifier(key)) return null; diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 795307c93..1197d9bdd 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -92,7 +92,8 @@ export function lowerClassMethodValue( loc, }; }; - const checked = isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(value.type); + const checked = + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(value.type); if (checked) value = lowerer.coerceToExpected(value, DYN); // Select the declaration when extracting the value. Calling the value // later must not redispatch the method name on a different receiver. @@ -374,7 +375,7 @@ function reflectedMethodValue( if (!member || (!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.body) return null; let needsNativeReceiver = - !isJsSourceFile(declaration.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(declaration)) || info.localClass !== undefined || lowerer.errorHierarchyClassOf(info.def.name); ts.walkPreorder(member, (node) => { diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts index 298ff45dc..33515bf2c 100644 --- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts @@ -87,7 +87,7 @@ function codeOwner(node: ts.Node): ts.Node { !ts.isPropertyDeclaration(current) ) current = current.parent; - return current ?? node.getSourceFile(); + return current ?? ts.getSourceFileOfNode(node); } /** A local variable or parameter that only its own function's code @@ -102,21 +102,21 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean { const decl = symbol.valueDeclaration; let owned = decl !== undefined && - decl.getSourceFile() === id.getSourceFile() && + ts.getSourceFileOfNode(decl) === ts.getSourceFileOfNode(id) && ((ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) || (ts.isParameter(decl) && ts.isIdentifier(decl.name))); if (owned && decl) { const owner = codeOwner(decl); const names = (node: ts.Node): boolean => ts.isIdentifier(node) && - node.text === symbol.name && + node.text === ts.symbolName(symbol) && lowerer.resolveValueSymbol(node) === symbol; const written = (target: ts.Node): boolean => { let found = false; const walk = (node: ts.Node): void => { if (found) return; if (names(node)) found = true; - else node.forEachChild(walk); + else ts.forEachChild(node, walk); }; walk(target); return found; @@ -144,9 +144,9 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean { ) target = node.initializer; if (target && codeOwner(node) !== owner && written(target)) owned = false; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - owner.forEachChild(visit); + ts.forEachChild(owner, visit); } lowerer.protocolOwnedBindings.set(symbol, owned); return owned; @@ -302,7 +302,7 @@ export function lowerProtocolMethodCall( kind: "dynCall", callee: probed ? varRef("p.callee", DYN, loc) : read(), receiver: varRef("p.0", DYN, loc), - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), calleeNameValue: varRef("p.calleeName", STRING, loc), args: params.map((type, i) => lowerer.coerceToExpected(varRef(`p.${i + 1}`, type, loc), DYN)), type: DYN, @@ -378,7 +378,7 @@ export function lowerProtocolMethodCall( receiver, ...(callee ? [varRef(callee.id, DYN, loc)] : []), ...args, - { kind: "strLit", value: access.getText(), type: STRING, loc }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc }, ], type: result, loc, @@ -487,7 +487,7 @@ export function lowerProtocolPropertyRead( lowerer: Lowerer, access: ts.PropertyAccessExpression, ): IrExpr | null { - if (isJsSourceFile(access.getSourceFile())) return null; + if (isJsSourceFile(ts.getSourceFileOfNode(access))) return null; const receiverType = lowerer.typeOf(access.expression); if (!lowerer.classImplementedProtocol(receiverType)) return null; const name = access.name.text; diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 5a27f0815..4de7f03ad 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -46,9 +46,9 @@ export class ClassProtocols { ) ) continue; - const entries = this.methods.get(property.name) ?? []; + const entries = this.methods.get(ts.symbolName(property)) ?? []; entries.push(node); - this.methods.set(property.name, entries); + this.methods.set(ts.symbolName(property), entries); } } if ( @@ -472,7 +472,7 @@ export class ClassProtocols { if (facts.observed.has(symbol) || facts.written.has(symbol)) return true; const target = facts.targets.get(symbol); if (target === undefined) return true; - const names = new Set(this.checker.getPropertiesOfType(target).map((p) => p.name)); + const names = new Set(this.checker.getPropertiesOfType(target).map((p) => ts.symbolName(p))); const origins = facts.origins.get(symbol) ?? new Set(); const writesNames = (written: Set | "all"): boolean => written === "all" || [...written].some((name) => names.has(name)); @@ -517,7 +517,7 @@ export class ClassProtocols { .getPropertiesOfType(declared) .some( (property) => - names.has(property.name) && + names.has(ts.symbolName(property)) && property.declarations.some( (declaration) => ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration), @@ -575,7 +575,9 @@ export class ClassProtocols { return ( symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0 && - symbol.declarations.some((declaration) => !declaration.getSourceFile().isDeclarationFile) && + symbol.declarations.some( + (declaration) => !ts.getSourceFileOfNode(declaration).isDeclarationFile, + ) && // `typeof C` (the class value) shares the class symbol. this.checker.getConstructSignatures(type).length === 0 ); @@ -598,7 +600,8 @@ export class ClassProtocols { this.methods.size > 0 && symbol.declarations.some( (declaration) => - ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile, + ts.isInterfaceDeclaration(declaration) && + !ts.getSourceFileOfNode(declaration).isDeclarationFile, ) && this.protocol(type) ); @@ -617,7 +620,7 @@ export class ClassProtocols { const declarations = symbol.declarations; return ( declarations.length > 0 && - declarations.every((declaration) => !declaration.getSourceFile().isDeclarationFile) + declarations.every((declaration) => !ts.getSourceFileOfNode(declaration).isDeclarationFile) ); } @@ -704,14 +707,15 @@ export class ClassProtocols { !symbol || !symbol.declarations.some( (declaration) => - ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile, + ts.isInterfaceDeclaration(declaration) && + !ts.getSourceFileOfNode(declaration).isDeclarationFile, ) ) return false; for (const property of properties) { const memberType = this.checker.getTypeOfSymbol(property); if (this.checker.getCallSignatures(memberType).length === 0) continue; - for (const declaration of this.methods.get(property.name) ?? []) { + for (const declaration of this.methods.get(ts.symbolName(property)) ?? []) { const classSymbol = declaration.name ? this.checker.getSymbolAtLocation(declaration.name) : undefined; diff --git a/packages/compiler/src/frontend/lowering/class-static-data.ts b/packages/compiler/src/frontend/lowering/class-static-data.ts index af5425727..dd6927d18 100644 --- a/packages/compiler/src/frontend/lowering/class-static-data.ts +++ b/packages/compiler/src/frontend/lowering/class-static-data.ts @@ -38,7 +38,7 @@ export function classStaticDataFor( ): IrExpr | null { if ( !info.decl || - !isJsSourceFile(info.decl.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(info.decl)) || name.startsWith("#") || constructorMembers.has(name) ) diff --git a/packages/compiler/src/frontend/lowering/code-generation.ts b/packages/compiler/src/frontend/lowering/code-generation.ts index b3bd9243a..c3a5c0ea7 100644 --- a/packages/compiler/src/frontend/lowering/code-generation.ts +++ b/packages/compiler/src/frontend/lowering/code-generation.ts @@ -126,8 +126,12 @@ function useSiteType(lowerer: Lowerer, expr: ts.Expression): IrType { // elsewhere — the throw always fires first. const type = lowerer.typeOf(expr); const symbol = type.getSymbol(); - if (symbol?.name === "Function" && lowerer.isStdlibSymbol(symbol)) { - return isJsSourceFile(expr.getSourceFile()) ? DYN : F64; + if ( + symbol !== undefined && + ts.symbolName(symbol) === "Function" && + lowerer.isStdlibSymbol(symbol) + ) { + return isJsSourceFile(ts.getSourceFileOfNode(expr)) ? DYN : F64; } const mapped = lowerer.mapTypeOf(type); if ( diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts index 5449b0943..5d14d5492 100644 --- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts +++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts @@ -91,7 +91,7 @@ export function collectionInput( const symbol = type.getSymbol(); if ( !symbol || - (symbol.name !== "MapIterator" && symbol.name !== "SetIterator") || + (ts.symbolName(symbol) !== "MapIterator" && ts.symbolName(symbol) !== "SetIterator") || !lowerer.isStdlibSymbol(symbol) ) return null; diff --git a/packages/compiler/src/frontend/lowering/collection-narrowing.ts b/packages/compiler/src/frontend/lowering/collection-narrowing.ts index 29b358e99..6c1ca80f4 100644 --- a/packages/compiler/src/frontend/lowering/collection-narrowing.ts +++ b/packages/compiler/src/frontend/lowering/collection-narrowing.ts @@ -11,9 +11,12 @@ export function concreteCollectionNarrow( ): ts.Type | null { if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return null; const symbol = narrowed.getSymbol(); - const name = symbol?.name; + const name = ts.symbolNameOf(symbol); if (name !== "Map" && name !== "Set") return null; - if (!symbol || !symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile()))) + if ( + !symbol || + !symbol.declarations.some((decl) => lowerer.isStdlibFile(ts.getSourceFileOfNode(decl))) + ) return null; const args = lowerer.checker.getTypeArguments(narrowed as ts.TypeReference); if (args.length === 0 || !args.every((arg) => (arg.flags & ts.TypeFlags.Any) !== 0)) return null; diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts index 77f72dc15..6807946d0 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts @@ -16,7 +16,7 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo const ctor = cls.members.nodes.find(ts.isConstructorDeclaration) ?? null; const fields = cls.members.nodes .filter(ts.isPropertyDeclaration) - .map((member) => ({ name: member.name.getText(), initializer: member.initializer })); + .map((member) => ({ name: ts.getTextOfNode(member.name), initializer: member.initializer })); const fieldNames = new Set(fields.map((field) => field.name)); return constructionEscape({ ctor, @@ -30,7 +30,7 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo const name = call.expression.name.text; const method = cls.members.nodes.find( (member): member is ts.MethodDeclaration => - ts.isMethodDeclaration(member) && member.name.getText() === name, + ts.isMethodDeclaration(member) && ts.getTextOfNode(member.name) === name, ); const isPrivate = method !== undefined && diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.ts b/packages/compiler/src/frontend/lowering/construction-escapes.ts index b9c8d7e07..3fdae4ab4 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.ts @@ -23,7 +23,7 @@ export function mentionsReceiver(node: ts.Node): boolean { found = true; return; } - child.forEachChild(visit); + ts.forEachChild(child, visit); }; visit(node); return found; @@ -68,9 +68,9 @@ function containsReturn(node: ts.Node): boolean { found = true; return; } - child.forEachChild(visit); + ts.forEachChild(child, visit); }; - node.forEachChild(visit); + ts.forEachChild(node, visit); return found; } @@ -100,7 +100,7 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca return !(ts.isIdentifier(node.name) && assigned.has(name) && inputs.isField(name)); } let found = false; - node.forEachChild((child) => { + ts.forEachChild(node, (child) => { if (!found && mentionsReceiver(child)) found = exposes(child, depth); }); return found; diff --git a/packages/compiler/src/frontend/lowering/constructor-returns.ts b/packages/compiler/src/frontend/lowering/constructor-returns.ts index bee2e7567..0e600e700 100644 --- a/packages/compiler/src/frontend/lowering/constructor-returns.ts +++ b/packages/compiler/src/frontend/lowering/constructor-returns.ts @@ -86,9 +86,9 @@ function ownValueReturns(ctor: ts.ConstructorDeclaration): ts.ReturnStatement[] const visit = (node: ts.Node): void => { if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; if (ts.isReturnStatement(node) && node.expression !== undefined) out.push(node); - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - ctor.body?.forEachChild(visit); + if (ctor.body !== undefined) ts.forEachChild(ctor.body, visit); return out; } @@ -188,9 +188,9 @@ function ownReturns(body: ts.Node): ts.ReturnStatement[] { const visit = (node: ts.Node): void => { if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; if (ts.isReturnStatement(node)) out.push(node); - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - body.forEachChild(visit); + ts.forEachChild(body, visit); return out; } @@ -225,7 +225,7 @@ export function constructorReturnPlan( // the class too (`if (C.instance) return C.instance;`). const runtimeInstance = info.localClass === undefined && - isJsSourceFile(ctor.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(ctor)) && [...returns].some(([stmt, kind]) => kind === "mixed" && !insideFinallyTry(stmt, ctor)); if ([...returns.values()].includes("instance") || runtimeInstance) { slot = { @@ -277,7 +277,7 @@ export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatemen }; } const feature = foreignObjectFeature(info); - if (kind === "object" || !isJsSourceFile(stmt.getSourceFile())) + if (kind === "object" || !isJsSourceFile(ts.getSourceFileOfNode(stmt))) lowerer.unsupported("SC1090", stmt, feature); // A JavaScript value that is an object only sometimes: a primitive // still answers the constructed instance, an object refuses. @@ -296,7 +296,7 @@ export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatemen }); const diag = unsupportedDiag("SC1090", loc, feature); lowerer.runtimeFences.push(diag); - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); const line = ts.getLineAndCharacterOfPosition(sf, loc.start).line + 1; return { kind: "block", @@ -460,7 +460,7 @@ export function superIntoResultSlot( const slot = plan.slot; const display = base.def.name.replace(/^%|^%m\d+\./, ""); const feature = `extending a class whose constructor can return an existing instance ('super()' would rebind 'this' to the instance '${display}' returns)`; - if (!isJsSourceFile(blame.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(blame))) { lowerer.unsupported("SC1090", blame, feature); return call; } @@ -470,7 +470,8 @@ export function superIntoResultSlot( if (!lowerer.liftedFns.some((fn) => fn.name === name)) { const diag = unsupportedDiag("SC1090", loc, feature); lowerer.runtimeFences.push(diag); - const line = ts.getLineAndCharacterOfPosition(blame.getSourceFile(), loc.start).line + 1; + const line = + ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(blame), loc.start).line + 1; const instance = { id: "instance.0", name: "instance", type, mutable: false }; const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false }; lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 735361f54..5406c3ec1 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -304,7 +304,7 @@ function lowerFastCallback( fastLoweringDepth > 0 || slow.arity <= elemIndex || !ts.isArrowFunction(argNode) || - isJsSourceFile(argNode.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(argNode)) || valueT?.kind !== "union" || typeEquals(valueT, elem) || elem.kind === "union" || @@ -1489,7 +1489,7 @@ export function lowerArrayFlatMapCall( kind: "dynInvoke", recv: { kind: "dynFrom", value: receiver, liveRef: true, type: DYN, loc }, method: "flatMap", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [{ kind: "dynFrom", value: fnArg, type: DYN, loc }], type: DYN, loc, @@ -2085,7 +2085,7 @@ export function lowerArraySortCall( // Arithmetic on inferred JS values retains a checked result because // it can produce BigInt. The sort callback still requires a number. const result = - fnArg.type.ret.kind === "dyn" && isJsSourceFile(argNode.getSourceFile()) + fnArg.type.ret.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(argNode)) ? F64 : fnArg.type.ret; const expected = funcOf( @@ -2223,7 +2223,7 @@ export function lowerDynArrayFilterCall( recv, fnArg, { kind: "strLit", value: access.name.text, type: STRING, loc }, - { kind: "strLit", value: access.getText(), type: STRING, loc }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc }, ], type: arrayOf(elemT), loc, @@ -2264,7 +2264,7 @@ export function lowerDynArrayFlatMapCall( kind: "dynInvoke", recv, method: "flatMap", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [boxed], type: DYN, loc, @@ -2286,7 +2286,7 @@ export function lowerDynArrayFlatMapCall( recv, fnArg, { kind: "strLit", value: access.name.text, type: STRING, loc }, - { kind: "strLit", value: access.getText(), type: STRING, loc }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc }, ], type: arrayOf(ret.elem), loc, diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 110af2b56..53d46d00d 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -296,7 +296,7 @@ export function lowerArrayConstructor( // reference. Known element forms retain native arrays, including their // existing sparse storage and indexed property contracts. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (result?.kind !== "array" || result.elem.kind === "dyn") ) { return { @@ -321,7 +321,7 @@ export function lowerArrayConstructor( : null; if (elem !== null && isSupportedArrayElem(elem)) result = arrayOf(elem); } - if (isJsSourceFile(expr.getSourceFile()) && (result === null || result.kind === "dyn")) + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && (result === null || result.kind === "dyn")) result = arrayOf(DYN); if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) { lowerer.badType(expr, lowerer.typeOf(expr)); @@ -823,7 +823,7 @@ export function lowerArrayFromCall( const loc = locOf(call); const args = call.arguments.nodes; if ( - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && args.length >= 1 && args.length <= 3 && !args.some(ts.isSpreadElement) diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 7ef96457c..12078720e 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -353,7 +353,7 @@ export function lowerArrayMethodCall( // machinery answered string[]): same rule, any-typed spelling. ((lowerer.typeOf(access.expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 && - isJsSourceFile(access.getSourceFile()))) + isJsSourceFile(ts.getSourceFileOfNode(access)))) ) { const probe = lowerer.lowerExpr(access.expression); if (probe.type.kind === "array") { @@ -1039,7 +1039,7 @@ export function lowerArrayMethodCall( kind: "dynInvoke", recv: receiver, method: "join", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc, @@ -2096,7 +2096,7 @@ export function lowerTupleReadMethodCall( kind: "dynInvoke", recv: { kind: "dynFrom", value: receiver, type: DYN, loc }, method: "flatMap", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [{ kind: "dynFrom", value: fnArg, type: DYN, loc }], type: DYN, loc, diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index c5a41cf32..f8e49a516 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -120,7 +120,7 @@ function lowerBytesSortCall( fnArg.type.params.every((param) => lowerer.coercibleValue(F64, param)) ) { const result = - fnArg.type.ret.kind === "dyn" && isJsSourceFile(argNode.getSourceFile()) + fnArg.type.ret.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(argNode)) ? F64 : fnArg.type.ret; const expected = funcOf( @@ -187,7 +187,7 @@ export function lowerBytesNew( ): IrExpr | null { if ( symbol != null && - (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer") && + (ts.symbolName(symbol) === "ArrayBuffer" || ts.symbolName(symbol) === "SharedArrayBuffer") && lowerer.isStdlibSymbol(symbol) ) { const args = expr.arguments?.nodes ?? []; @@ -203,18 +203,19 @@ export function lowerBytesNew( : dynUndefinedExpr(locOf(expr)); return { kind: "libCall", - fn: symbol.name === "SharedArrayBuffer" ? "sharedArrayBuffer.new" : "arrayBuffer.new", + fn: + ts.symbolName(symbol) === "SharedArrayBuffer" ? "sharedArrayBuffer.new" : "arrayBuffer.new", args: [length], type: DYN, loc: locOf(expr), }; } - if (symbol && symbol.name === "DataView" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "DataView" && lowerer.isStdlibSymbol(symbol)) { return lowerDataViewNew(lowerer, expr); } - const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined; + const elem = symbol ? own(BYTES_CTORS, ts.symbolName(symbol)) : undefined; if (!elem || !symbol || !lowerer.isStdlibSymbol(symbol)) return null; - const name = symbol.name; + const name = ts.symbolName(symbol); const type = bytesOf(elem); const loc = locOf(expr); const args = expr.arguments?.nodes ?? []; @@ -291,7 +292,7 @@ export function lowerBytesStaticCall( } if (access.name.text !== "from" || !ts.isIdentifier(access.expression)) return null; const symbol = lowerer.resolveValueSymbol(access.expression); - const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined; + const elem = symbol ? own(BYTES_CTORS, ts.symbolName(symbol)) : undefined; if (!elem || !lowerer.isStdlibSymbol(symbol ?? undefined)) return null; if ( call.arguments.nodes.length < 1 || @@ -442,7 +443,7 @@ export function lowerBytesMethodCall( const nArgs = call.arguments.nodes.length; // Encoded stream callbacks can receive strings even when JavaScript's // contextual inference still describes their parameter as a Buffer. - if (name === "toString" && nArgs <= 1 && isJsSourceFile(call.getSourceFile())) { + if (name === "toString" && nArgs <= 1 && isJsSourceFile(ts.getSourceFileOfNode(call))) { const lowered = lowerer.lowerExpr(access.expression); const receiver = lowered.kind === "dynCheck" && lowered.value.type.kind === "dyn" ? lowered.value : lowered; @@ -455,7 +456,7 @@ export function lowerBytesMethodCall( call.arguments.nodes[0] ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), - { kind: "strLit", value: access.getText(), type: STRING, loc }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc }, ], type: STRING, loc, @@ -1334,7 +1335,8 @@ function lowerBufferStaticValue( args.length <= 3 && !args.some(ts.isSpreadElement) && ((symbol) => - symbol != null && (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer"))( + symbol != null && + (ts.symbolName(symbol) === "ArrayBuffer" || ts.symbolName(symbol) === "SharedArrayBuffer"))( lowerer.typeOf(args[0]!).getSymbol(), ) ) { @@ -1650,7 +1652,9 @@ function lowerBufferStaticValue( // flavor. Plain bytes are Uint8Arrays at this boundary; inspecting // only the bytes tag would incorrectly identify them as Buffers. const checked = - isJsSourceFile(call.getSourceFile()) && v.kind === "dynCheck" && v.value.type.kind === "dyn" + isJsSourceFile(ts.getSourceFileOfNode(call)) && + v.kind === "dynCheck" && + v.value.type.kind === "dyn" ? v.value : v; if (checked.type.kind === "dyn") { diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index dcd9934bd..18754f088 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -59,7 +59,7 @@ export function lowerArrayCallback( if ( ts.isIdentifier(argNode) && argNode.text === "Boolean" && - !isJsSourceFile(argNode.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(argNode)) && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined) ) { const valueT = lead[0]; diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts index 7c4ddf220..0bf126830 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts @@ -67,7 +67,7 @@ export function lowerGroupByStaticCall( const fNode = call.arguments.nodes[1]!; // An unannotated JavaScript callback parameter takes the element. const f = - isJsSourceFile(fNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(fNode)) && (ts.isArrowFunction(fNode) || ts.isFunctionExpression(fNode)) ? lowerArrayCallback(lowerer, fNode, [elem], groupT, true).fnArg : lowerer.lowerExpr(fNode); diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index 6a5290c0f..6aa6d6faf 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -60,7 +60,7 @@ export function lowerMapMethodCall( if (!MAP_METHODS.has(name)) return null; let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); let probedUntyped = false; - if (receiverIr?.kind !== "map" && isJsSourceFile(access.getSourceFile())) { + if (receiverIr?.kind !== "map" && isJsSourceFile(ts.getSourceFileOfNode(access))) { const probed = tryLowerExpression(lowerer, access.expression); if (probed?.type.kind === "map") { receiverIr = probed.type; @@ -427,7 +427,7 @@ export function lowerCollectionSpread( loc, }, then: entries, - else_: nodeThrowExpr(1, "", `${node.getText()} is not iterable`, entries.type, loc), + else_: nodeThrowExpr(1, "", `${ts.getTextOfNode(node)} is not iterable`, entries.type, loc), type: entries.type, loc, }, @@ -544,7 +544,7 @@ export function lowerSetMethodCall( // Set, but the VALUE lowered as a real Set of // identity tokens (the new-Set probe) — the lowered receiver's type // is the honest dispatch key. - if (receiverIr?.kind !== "set" && isJsSourceFile(access.getSourceFile())) { + if (receiverIr?.kind !== "set" && isJsSourceFile(ts.getSourceFileOfNode(access))) { const probed = tryLowerExpression(lowerer, access.expression); if (probed?.type.kind === "set") { receiverIr = probed.type; @@ -973,7 +973,7 @@ export function lowerSetSeedNew( { kind: "libCall", fn: "dyn.iterPack", - args: [source, strLit(node.getText(), loc)], + args: [source, strLit(ts.getTextOfNode(node), loc)], type: DYN, loc, }, @@ -1165,7 +1165,7 @@ export function lowerMapSeedNew( } if (ts.isArrayLiteralExpression(argNode)) { if ( - isJsSourceFile(argNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(argNode)) && argNode.elements.nodes.some((entry) => !ts.isArrayLiteralExpression(entry)) ) { return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT); diff --git a/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts b/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts index 523fda46e..32f90bfe7 100644 --- a/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts +++ b/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts @@ -142,7 +142,7 @@ export function dynStringReceiver( args: [ recv, { kind: "strLit", value: access.name.text, type: STRING, loc }, - { kind: "strLit", value: access.getText(), type: STRING, loc }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc }, ], type: STRING, loc, diff --git a/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts b/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts index f2967c298..397bf6ec8 100644 --- a/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts +++ b/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts @@ -29,7 +29,7 @@ export function execResultCaptureMask(lowerer: Lowerer, call: ts.CallExpression) const symbol = lowerer.resolveValueSymbol(decl.name); if (!symbol) return null; const read = new Set(); - for (const use of identifiersNamed(lowerer, decl.getSourceFile(), decl.name.text)) { + for (const use of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) { if (use === decl.name || lowerer.resolveValueSymbol(use) !== symbol) continue; const ref = outerValue(use); const parent = ref.parent; diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 4017335ae..c23e6931c 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -684,7 +684,7 @@ function isHelperReceiver(lowerer: Lowerer, expr: ts.Expression): boolean { return ( mapped?.kind === "dyn" && symbol !== undefined && - ADAPTED_ITERATOR_TYPES.has(symbol.name) && + ADAPTED_ITERATOR_TYPES.has(ts.symbolName(symbol)) && lowerer.isStdlibSymbol(symbol) ); } diff --git a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts index f936e27cb..794bd27c2 100644 --- a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts +++ b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts @@ -242,7 +242,7 @@ function constBindingReadsByElements( } function usesOf(lowerer: Lowerer, name: ts.Identifier, symbol: ts.Symbol): ts.Identifier[] { - return identifiersNamed(lowerer, name.getSourceFile(), name.text).filter( + return identifiersNamed(lowerer, ts.getSourceFileOfNode(name), name.text).filter( (use) => use !== name && lowerer.resolveValueSymbol(use) === symbol, ); } diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index e3ca2270d..b80eb7ac0 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -62,8 +62,9 @@ export class DeferredModuleInitializers { const info = exactClassOfReceiver(lowerer, owner); if ( info && - info.decl?.getSourceFile() === statement.getSourceFile() && - info.decl.getStart() < statement.getStart() && + info.decl !== null && + ts.getSourceFileOfNode(info.decl) === ts.getSourceFileOfNode(statement) && + ts.tokenStartOfNode(info.decl) < ts.tokenStartOfNode(statement) && !info.localClass && !info.classDecorators && !info.generic && @@ -101,7 +102,7 @@ export class DeferredModuleInitializers { loc: locOf(statement), }; const context = lowerer.ctx; - const moduleIndex = [...lowerer.initNameOf.keys()].indexOf(statement.getSourceFile()); + const moduleIndex = [...lowerer.initNameOf.keys()].indexOf(ts.getSourceFileOfNode(statement)); const initializers = this.pending.get(key) ?? []; initializers.push({ loc: block.loc, @@ -269,14 +270,14 @@ function registryInitializer( return false; expression = expression.arguments.nodes[0]!; } - const packageSource = npmStaticPackageOfPath(statement.getSourceFile().fileName) !== null; + const packageSource = npmStaticPackageOfPath(ts.getSourceFileOfNode(statement).fileName) !== null; const initialized = (declaration: ts.Node): boolean => { - const source = declaration.getSourceFile(); - return source === statement.getSourceFile() - ? declaration.getStart() < statement.getStart() + const source = ts.getSourceFileOfNode(declaration); + return source === ts.getSourceFileOfNode(statement) + ? ts.tokenStartOfNode(declaration) < ts.tokenStartOfNode(statement) : [...lowerer.initNameOf.keys()].indexOf(source) >= 0 && [...lowerer.initNameOf.keys()].indexOf(source) < - [...lowerer.initNameOf.keys()].indexOf(statement.getSourceFile()); + [...lowerer.initNameOf.keys()].indexOf(ts.getSourceFileOfNode(statement)); }; const callable = (callee: ts.Expression): boolean => { if (!ts.isIdentifier(callee)) return false; @@ -324,7 +325,7 @@ function registryInitializer( // bundlers. Arguments still need to be safe to omit: an annotation // never licenses dropping argument effects or a throwing property read. if (packageSource && (ts.isCallExpression(node) || ts.isNewExpression(node))) { - const trivia = node.getSourceFile().text.slice(node.pos, node.getStart()); + const trivia = ts.getSourceFileOfNode(node).text.slice(node.pos, ts.tokenStartOfNode(node)); if (!/[@#]__PURE__/.test(trivia)) return false; if (!callable(node.expression)) return false; return (node.arguments?.nodes ?? []).every((argument) => value(argument, depth + 1)); @@ -337,8 +338,8 @@ function registryInitializer( const info = exactClassOfReceiver(lowerer, node.expression.expression); return ( !!info?.decl && - info.decl.getSourceFile() === statement.getSourceFile() && - info.decl.getStart() < statement.getStart() && + ts.getSourceFileOfNode(info.decl) === ts.getSourceFileOfNode(statement) && + ts.tokenStartOfNode(info.decl) < ts.tokenStartOfNode(statement) && classWriteIsUnobserved(lowerer, info, statement) && (findMethodOn(lowerer, info, node.name.text) !== null || findGenericMethodOn(lowerer, info, node.name.text) !== null) @@ -398,7 +399,7 @@ function registryInitializer( if ( functionDeclaration && ts.isFunctionDeclaration(functionDeclaration) && - functionDeclaration.getSourceFile() === statement.getSourceFile() + ts.getSourceFileOfNode(functionDeclaration) === ts.getSourceFileOfNode(statement) ) return true; // Reading an earlier initialized binding executes none of its @@ -410,8 +411,8 @@ function registryInitializer( functionDeclaration && ts.isVariableDeclaration(functionDeclaration) && functionDeclaration.initializer && - functionDeclaration.getSourceFile() === statement.getSourceFile() && - functionDeclaration.getStart() < statement.getStart() + ts.getSourceFileOfNode(functionDeclaration) === ts.getSourceFileOfNode(statement) && + ts.tokenStartOfNode(functionDeclaration) < ts.tokenStartOfNode(statement) ) return true; const info = symbol && lowerer.classBySymbol.get(symbol); @@ -421,8 +422,8 @@ function registryInitializer( // decorated classes and local factories keep ordinary initialization. return ( !!declaration && - declaration.getSourceFile() === statement.getSourceFile() && - declaration.getStart() < statement.getStart() && + ts.getSourceFileOfNode(declaration) === ts.getSourceFileOfNode(statement) && + ts.tokenStartOfNode(declaration) < ts.tokenStartOfNode(statement) && ts.isClassDeclaration(declaration) && !info!.classDecorators && !info!.generic && @@ -476,19 +477,20 @@ function registryOwnerIsData( ) ) return false; - if (declaration.getSourceFile() !== statement.getSourceFile()) { + if (ts.getSourceFileOfNode(declaration) !== ts.getSourceFileOfNode(statement)) { const modules = [...lowerer.initNameOf.keys()]; return ( !!frozen && - modules.indexOf(declaration.getSourceFile()) >= 0 && - modules.indexOf(declaration.getSourceFile()) < modules.indexOf(statement.getSourceFile()) + modules.indexOf(ts.getSourceFileOfNode(declaration)) >= 0 && + modules.indexOf(ts.getSourceFileOfNode(declaration)) < + modules.indexOf(ts.getSourceFileOfNode(statement)) ); } - if (declaration.getStart() >= statement.getStart()) return false; + if (ts.tokenStartOfNode(declaration) >= ts.tokenStartOfNode(statement)) return false; if (frozen) return true; let safe = true; const visit = (node: ts.Node): void => { - if (!safe || node.getStart() >= statement.getStart()) return; + if (!safe || ts.tokenStartOfNode(node) >= ts.tokenStartOfNode(statement)) return; if ( ts.isIdentifier(node) && node !== declaration.name && @@ -515,7 +517,7 @@ function registryOwnerIsData( } ts.forEachChild(node, visit); }; - visit(statement.getSourceFile()); + visit(ts.getSourceFileOfNode(statement)); return safe; } @@ -527,7 +529,7 @@ function classWriteIsUnobserved( info: ClassInfo, statement: ts.Statement, ): boolean { - const source = statement.getSourceFile(); + const source = ts.getSourceFileOfNode(statement); const declaration = info.decl!; if ( declaration.members.nodes.some( @@ -543,7 +545,10 @@ function classWriteIsUnobserved( ) return false; for (const earlier of source.statements.nodes) { - if (earlier.getStart() <= declaration.getStart() || earlier.getStart() >= statement.getStart()) + if ( + ts.tokenStartOfNode(earlier) <= ts.tokenStartOfNode(declaration) || + ts.tokenStartOfNode(earlier) >= ts.tokenStartOfNode(statement) + ) continue; if ( ts.isFunctionDeclaration(earlier) || diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index a77f33c6b..1c362cb09 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -281,7 +281,7 @@ export function scanDivergences( ts.isVariableDeclaration(decl) && decl.initializer !== undefined && ts.isObjectLiteralExpression(decl.initializer) && - decl.getSourceFile() === inner.getSourceFile() + ts.getSourceFileOfNode(decl) === ts.getSourceFileOfNode(inner) ) { lit = decl.initializer; binding = inner.text; @@ -291,7 +291,9 @@ export function scanDivergences( const contextual = safely(() => checker.getContextualType(lit!)); if (contextual === undefined) continue; if (namedObjectDeclaration(checker, contextual) === null) continue; - const declared = safely(() => checker.getPropertiesOfType(contextual).map((p) => p.name)); + const declared = safely(() => + checker.getPropertiesOfType(contextual).map((p) => ts.symbolName(p)), + ); if (declared === undefined) continue; const written = lit.properties.nodes.map((p) => propertyNameText(p)!); const assigned = @@ -463,7 +465,7 @@ function identityUse(root: ts.Node, name: string, from = -1): IdentityUse | null return cur; }; const textOf = (node: ts.Node): string => { - const text = node.getText(); + const text = ts.getTextOfNode(node); return text.length > 60 ? `${text.slice(0, 57)}...` : text; }; const visit = (node: ts.Node): "skip" | "stop" | undefined => { @@ -591,7 +593,7 @@ function elementWrite(root: ts.Node, name: string, from: number): string | null ) target = node.operand; if (target !== undefined && throughElement(target)) { - found = target.getText(); + found = ts.getTextOfNode(target); return "stop"; } if ( @@ -670,8 +672,8 @@ function widthCopyBetween( // still observes the same writes: no divergence to report. const accessor = ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor; if (sourceProps.some((p) => (p.flags & accessor) !== 0)) return null; - const sourceNames = new Set(sourceProps.map((p) => p.name)); - const targetNames = checker.getPropertiesOfType(t).map((p) => p.name); + const sourceNames = new Set(sourceProps.map((p) => ts.symbolName(p))); + const targetNames = checker.getPropertiesOfType(t).map((p) => ts.symbolName(p)); if (targetNames.length === sourceNames.size && targetNames.every((n) => sourceNames.has(n))) return null; } @@ -684,7 +686,7 @@ function isProgramClassType(checker: ts.TypeChecker, type: ts.Type): boolean { return ( symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0 && - symbol.declarations.some((d) => !d.getSourceFile().isDeclarationFile) + symbol.declarations.some((d) => !ts.getSourceFileOfNode(d).isDeclarationFile) ); } @@ -698,7 +700,7 @@ function isPlainRecordType(checker: ts.TypeChecker, type: ts.Type): boolean { if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0) return false; // Library-declared object types (Map, Date, Promise, ...) are not records. const decls = symbol === undefined ? [] : symbol.declarations; - if (decls.some((d) => d.getSourceFile().isDeclarationFile)) return false; + if (decls.some((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return false; return true; } @@ -711,7 +713,7 @@ function namedObjectDeclaration(checker: ts.TypeChecker, type: ts.Type): ts.Node if (symbol === undefined) return null; if ((symbol.flags & ts.SymbolFlags.Class) !== 0) return null; const decl = symbol.declarations[0]; - if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; + if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null; return decl; } @@ -765,7 +767,7 @@ function classDeclarationOfTypeNode( const symbol = lowerer.checker.getSymbolAtLocation(node.typeName); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null; const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); - if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; + if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null; return decl as ts.ClassLikeDeclaration; } @@ -776,7 +778,7 @@ function classDeclarationOfType( const symbol = type.getSymbol(); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null; const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); - if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; + if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null; return decl as ts.ClassLikeDeclaration; } diff --git a/packages/compiler/src/frontend/lowering/dyn-streams.ts b/packages/compiler/src/frontend/lowering/dyn-streams.ts index 9694e3820..51bcf0ea8 100644 --- a/packages/compiler/src/frontend/lowering/dyn-streams.ts +++ b/packages/compiler/src/frontend/lowering/dyn-streams.ts @@ -34,14 +34,18 @@ export function nodeStreamInterfaceSide(lowerer: Lowerer, type: ts.Type): "r" | (t.flags & ts.TypeFlags.Intersection) !== 0 ? ts.constituentTypes(t).flatMap(parts) : [t]; for (const part of parts(lowerer.checker.getNonNullableType(type))) { const psym = part.getSymbol() ?? part.getAliasSymbol(); - if (psym?.name !== "ReadableStream" && psym?.name !== "WritableStream") continue; + if ( + (psym === undefined || ts.symbolName(psym) !== "ReadableStream") && + (psym === undefined || ts.symbolName(psym) !== "WritableStream") + ) + continue; const declared = psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - lowerer.isStdlibFile(d.getSourceFile()) && + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientNamespace(d, "NodeJS"), ); - if (declared) return psym.name === "ReadableStream" ? "r" : "w"; + if (declared) return ts.symbolName(psym) === "ReadableStream" ? "r" : "w"; } return null; } @@ -161,7 +165,7 @@ export function lowerDynStreamMemberCall( kind: "dynInvoke", recv: receiverRef, method: access.name.text, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index d311d6d79..a80367dd0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -83,7 +83,7 @@ export function lowerArrayLiteral( // structurally equal checker types. Their native class ABIs still differ. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && mapped?.kind === "array" && mapped.elem.kind === "classval" ) @@ -202,7 +202,7 @@ export function lowerArrayLiteral( // keep the checked-dynamic fallback below. if ( lowerer.implicitParamTypes !== null && - npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null && + npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null && (mapped === null || mapped.kind === "dyn" || (mapped.kind === "array" && mapped.elem.kind === "dyn")) && @@ -222,7 +222,7 @@ export function lowerArrayLiteral( mapped = arrayOf(first.type); } } - if (!expected && isJsSourceFile(expr.getSourceFile()) && mapped?.kind === "array") { + if (!expected && isJsSourceFile(ts.getSourceFileOfNode(expr)) && mapped?.kind === "array") { const elem = mapped.elem; const arms = elem.kind === "union" ? lowerer.unions.get(elem.unionId)?.arms : [elem]; if (arms?.length && arms.every((arm) => arm.kind === "classval")) mapped = DYN; @@ -234,7 +234,7 @@ export function lowerArrayLiteral( } if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.elements.nodes.some( (element) => ts.isSpreadElement(element) && @@ -247,7 +247,7 @@ export function lowerArrayLiteral( // only in a checked-dynamic array: a static element type would copy it. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && mapped?.kind !== "dyn" && expr.elements.nodes.some( (element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element), @@ -269,7 +269,7 @@ export function lowerArrayLiteral( // real keys. A structural union can otherwise drop descriptor fields. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && mapped?.kind === "array" && mapped.elem.kind === "union" && expr.elements.nodes.some(ts.isObjectLiteralExpression) @@ -316,7 +316,7 @@ export function lowerArrayLiteral( // to DYN wholesale now (mapType's dyn-element array rule), so the // literal IS the dyn array. if ( - isJsSourceFile(expr.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(expr)) || mapped?.kind === "dyn" || lowerer.checkerAnyArray(expr) ) { diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 8ef631f60..07d983cc0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -531,7 +531,7 @@ export function lowerNativeFunctionAssignment( ): IrExpr | null { const target = expr.left; if ( - !isJsSourceFile(expr.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(expr)) || (!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) || target.questionDotToken ) @@ -903,7 +903,7 @@ export function lowerCompoundValueToTarget( // catchable TypeError, as the expanded assignment does). if ( (target.type.kind === "dyn" || rhs.type.kind === "dyn") && - (isJsSourceFile(expr.getSourceFile()) || + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || (lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type))) ) { const left = lowerer.coerceInto(expr.left, read, DYN); @@ -1142,7 +1142,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr recv && (recv.type.kind === "dyn" || (recv.type.kind === "array" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(recv.type))) ) { return lowerDynMemberAssignment( @@ -1534,7 +1534,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr // Numeric operators dispatch on ToNumeric's actual Number/BigInt // result, including values flowing through native callable properties. if ( - isJsSourceFile(expr.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(expr)) || op === ts.SyntaxKind.PlusToken || [ ts.SyntaxKind.LessThanToken, @@ -2409,7 +2409,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE } if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && op === ts.SyntaxKind.AmpersandAmpersandToken && left.type.kind === "union" && lowerer.unions.get(left.type.unionId)?.arms.some((arm) => arm.kind === "bool") && @@ -2457,7 +2457,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE op === ts.SyntaxKind.AmpersandAmpersandToken && target === null && lowerer.implicitParamTypes !== null && - npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null && + npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null && (lowerer.typeOf(expr).flags & ts.TypeFlags.Any) !== 0 && left.type.kind === "union" ) { @@ -2618,7 +2618,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE // When both operands have native checked representations, preserve // the deciding value and short-circuit evaluation in that domain. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type) ) { diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index f39b7c428..7ef4b9f25 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -231,7 +231,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre let receiverIr = neverTaintedJsType(lowerer, expr.expression, lowerer.typeOf(expr.expression)) ? null : lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { const receiver = tryLowerExpression(lowerer, expr.expression); if (receiver?.type.kind === "dyn") { const key = lowerer.lowerExprExpecting(expr.argumentExpression, DYN); @@ -316,7 +316,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre locOf(expr.argumentExpression), expr.argumentExpression, ); - if (isJsSourceFile(expr.getSourceFile()) && recv.type.kind === "bytes") { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && recv.type.kind === "bytes") { return { kind: "dynKeyGet", value: lowerer.coerceInto(expr.expression, recv, DYN), @@ -565,7 +565,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre if ( receiver?.type.kind === "dyn" || (receiver && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (receiver.type.kind === "union" || receiver.type.kind === "bytes") && lowerer.dynConvertible(receiver.type)) ) { @@ -769,7 +769,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre if ( arr.type.kind === "array" && (index.type.kind === "dyn" || - (isJsSourceFile(expr.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && index.type.kind !== "f64" && lowerer.dynConvertible(index.type))) ) { @@ -822,7 +822,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre arr.type.kind === "array" && arr.kind === "arrIntrinsic" && arr.method === "slice" && - npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null + npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null ) { const safe = lowerSafeIndexRead(lowerer, arr, index, locOf(expr)); if (safe) return safe; @@ -1306,7 +1306,11 @@ export function lowerRecordKeyRead( ) { declared = shape.fields[0]!.type; } - if (!declared && isJsSourceFile(expr.getSourceFile()) && recordKeyResultOk(lowerer, shape, DYN)) + if ( + !declared && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && + recordKeyResultOk(lowerer, shape, DYN) + ) declared = DYN; if (!declared) { lowerer.unsupported( @@ -1425,7 +1429,7 @@ export function lowerElementCompoundParts( if (isKeyedFieldAccess(lowerer, target)) return { expr: lowerFieldCompoundValue(lowerer, target, op, expr.right, loc), stmts: null }; const checked = lowerer.lowerExpr(target.expression); - if (checked?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + if (checked?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const stmts: IrStmt[] = []; const save = (value: IrExpr, name: string): IrExpr => { const local = lowerer.declareHiddenLocal(name, value.type); @@ -1516,7 +1520,7 @@ export function lowerElementCompoundParts( const rhs = lowerer.lowerExpr(expr.right); const numericRhs = rhs.type.kind === "dyn" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && receiver.type.kind === "bytes" ? { kind: "libCall" as const, fn: "dyn.toNumberCoerce" as const, args: [rhs], type: F64, loc } : lowerOptionalNumber(lowerer, rhs, loc); @@ -1566,7 +1570,7 @@ export function lowerElementCompoundParts( type: F64, loc, }; - } else if (valueType.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + } else if (valueType.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const left = lowerer.coerceToExpected(oldRef(), DYN), right = lowerer.coerceToExpected(rhs, DYN); computed = dynCompoundValue(op, left, right, loc); @@ -1700,7 +1704,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): loc: locOf(expr), }; if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && receiver.type.kind === "object" && lowerer.dynConvertible(receiver.type) ) { @@ -1724,7 +1728,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): loc: locOf(expr), }; } - if (checkedReceiver?.type.kind === "object" && isJsSourceFile(expr.getSourceFile())) { + if (checkedReceiver?.type.kind === "object" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const info = lowerer.classes.get(checkedReceiver.type.className); const key = recordKeyLiteralText(target.argumentExpression) ?? @@ -1739,7 +1743,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): } } let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(target.expression)); - if (isJsSourceFile(expr.getSourceFile()) && checkedReceiver) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && checkedReceiver) { const represented = checkedReceiver.type.kind === "union" ? lowerer.stripUndefinedArm(checkedReceiver.type) diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index c2e5ddcbb..b5f99d682 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -133,7 +133,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: ); } if (projection?.kind === "builtin-function") { - if (!isJsSourceFile(expr.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) { const callable = lowerer.lowerBuiltinCallableValue( { module: projection.module, member: projection.member }, loc, @@ -149,7 +149,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // `EventEmitter`, `Readable` or `TypeError` (or a const alias of // one) as a value in a TypeScript file: the runtime class's class // object. JavaScript files keep the identity token. - if (!isJsSourceFile(expr.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) { const runtimeClass = runtimeClassValueOf(lowerer, expr); if (runtimeClass) return lowerer.classValueRef(runtimeClass, expr); } @@ -187,7 +187,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: const clause = decl.parent.parent.parent; if ( (ts.isCaseClause(clause) || ts.isDefaultClause(clause)) && - !(expr.getStart() >= clause.getStart() && expr.end <= clause.end) + !(ts.tokenStartOfNode(expr) >= ts.tokenStartOfNode(clause) && expr.end <= clause.end) ) { lowerer.unsupported( "SC1090", @@ -385,7 +385,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: (expr.text === "__dirname" || expr.text === "__filename") && lowerer.isStdlibGlobal(expr, expr.text) ) { - const sf = expr.getSourceFile(); + const sf = ts.getSourceFileOfNode(expr); if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName)) { return { kind: "libCall", @@ -552,7 +552,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // JavaScript sources: an otherwise unsupported builtin VALUE is // the same identity-token story as stdlib globals above (the // harness adds worker_threads.Worker to its identity Set). - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { // EventEmitter and the stream classes are the runtime's untyped // constructors, so constructor functions can inherit from them. const classKind = builtinMemberClassKind(bi.module, bi.member); @@ -652,7 +652,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // type-mapper.ts pins the one concrete signature `(value: string) => // primitive`; direct calls `String(x)` never reach here — the call // lowering intercepts them with the wider static coercions). - const jsPrimitiveCtorSelector = isJsSourceFile(expr.getSourceFile()); + const jsPrimitiveCtorSelector = isJsSourceFile(ts.getSourceFileOfNode(expr)); const primitiveName = stdlibGlobalNameOf(lowerer, expr); if ( primitiveName === "String" || @@ -664,7 +664,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: } if (lowerer.isStdlibGlobal(expr, "ArrayBuffer")) return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc }; - if (isJsSourceFile(expr.getSourceFile()) && lowerer.isStdlibGlobal(expr, "Array")) + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.isStdlibGlobal(expr, "Array")) return { kind: "libCall", fn: "dyn.arrayConstructor", args: [], type: DYN, loc }; if ( Object.values(BYTES_ELEMENT_NAME).includes(expr.text) && @@ -763,10 +763,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: expr, canonical, loc, - isJsSourceFile(expr.getSourceFile()), + isJsSourceFile(ts.getSourceFileOfNode(expr)), ); if (builtinValue !== null) return builtinValue; - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc }; } // The families with a WHY: each hint states what makes the @@ -986,7 +986,7 @@ function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr | if ( binding !== undefined && (ts.isImportSpecifier(binding) || ts.isExportSpecifier(binding)) && - !isCjsJsFile(binding.getSourceFile(), lowerer.program) + !isCjsJsFile(ts.getSourceFileOfNode(binding), lowerer.program) ) { const snapshot = cjsAccessorSnapshotOf(lowerer, getter); if (snapshot !== null) diff --git a/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts index 7a4f9c557..e53d27aa9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts +++ b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts @@ -44,7 +44,7 @@ export function literalFieldKeys(lowerer: Lowerer, keyNode: ts.Expression): stri export function isKeyedFieldAccess(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean { if ( access.questionDotToken || - isJsSourceFile(access.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(access)) || lowerer.isProcessEnv(access.expression) ) return false; diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 9f98ac2db..dec06fdd9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -92,7 +92,7 @@ export function lowerTernary( ) { falseArmNarrows.push(n); } - n.forEachChild(collect); + ts.forEachChild(n, collect); }; collect(expr.whenFalse); } @@ -333,7 +333,7 @@ export function lowerTernary( // call ABI. A checker join can erase a rest/arguments pack even though // a later call supplies additional observable arguments. const callableJoin = - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && thenRaw.type.kind === "func" && elseRaw.type.kind === "func" && !typeEquals(thenRaw.type, elseRaw.type) && @@ -343,7 +343,7 @@ export function lowerTernary( // a conditional's type even after that field acquired other values. // A checked arm must retain its actual representation at the join. const checkedJsJoin = - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (thenRaw.type.kind === "dyn" || elseRaw.type.kind === "dyn") && lowerer.dynConvertible(thenRaw.type) && lowerer.dynConvertible(elseRaw.type); @@ -385,7 +385,7 @@ export function lowerTernary( export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr { if ( (expr.type.kind === "union" || expr.type.kind === "dyn") && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isPropertyAccessExpression(node) ) { const symbol = lowerer.checker.getSymbolAtLocation(node.name); @@ -396,7 +396,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx ts.isPropertyDeclaration(decl) && !decl.initializer && !decl.type && - isJsSourceFile(decl.getSourceFile()), + isJsSourceFile(ts.getSourceFileOfNode(decl)), ) ) { // Inference describes later writes, not the initial undefined. @@ -416,7 +416,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx // An inferred JS member's storage may be deliberately wider than its // declaration (prototype-selected buffers, null-initialized scratch // objects). Extract at the consuming operation, not at the read. - if (isJsSourceFile(node.getSourceFile()) && ts.isPropertyAccessExpression(node)) { + if (isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isPropertyAccessExpression(node)) { const receiver = lowerer.mapTypeOf(lowerer.typeOf(node.expression)); if (receiver?.kind === "object") { const info = lowerer.classes.get(receiver.className); @@ -432,7 +432,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx } } if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) ) { let receiver = node.expression; @@ -461,7 +461,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx if (bound && lowerer.mapTypeOf(bound)?.kind === "dyn") return expr; } } - if (isJsSourceFile(node.getSourceFile()) && ts.isIdentifier(node)) { + if (isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isIdentifier(node)) { const symbol = lowerer.resolveValueSymbol(node); if ( symbol && @@ -495,7 +495,9 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx decl.initializer && !decl.type && !/@(?:param|type)\b/.test( - decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), + ts + .getSourceFileOfNode(decl) + .text.slice(decl.parent?.pos ?? decl.pos, ts.tokenStartOfNode(decl)), ), ) ) @@ -522,7 +524,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx // has no such property. Preserve the checked read for JS capability // probes instead of treating a declaration as a runtime type guard. if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isPropertyAccessExpression(node) && ["isTTY", "columns", "rows"].includes(node.name.text) && lowerer.isStdlibMember(node) @@ -532,7 +534,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx // checker flow calls its non-null branch never. The live checked // value remains authoritative; never is not a numeric guard. if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && (lowerer.typeOf(node).flags & ts.TypeFlags.Never) !== 0 ) return expr; @@ -839,11 +841,11 @@ export function v8CalleeText(node: ts.Node): string { const object = v8CalleeText(node.expression); return ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key) ? `${object}.${key.text}` - : `${object}[${key.getText()}]`; + : `${object}[${ts.getTextOfNode(key)}]`; } if (ts.isPropertyAccessExpression(node) && !node.questionDotToken) return `${v8CalleeText(node.expression)}.${node.name.text}`; - return node.getText(); + return ts.getTextOfNode(node); } /** Node's TypeError for the operation consuming `node` when it holds a diff --git a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts index e3b9bf025..51b7f4028 100644 --- a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts +++ b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts @@ -25,7 +25,7 @@ export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrEx if (args.length !== 2 || args.some(ts.isSpreadElement)) lowerer.noLowering("new Proxy with this argument list", expr); const handler = args[1]!; - if (isJsSourceFile(expr.getSourceFile()) && ts.isObjectLiteralExpression(handler)) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && ts.isObjectLiteralExpression(handler)) { for (const prop of handler.properties.nodes) { const callback = ts.isMethodDeclaration(prop) ? prop diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 77c4c8fc5..303b6ed55 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -276,7 +276,7 @@ export function lowerDynObjectLiteral( const raw = builtin === "path" || builtin === "path/posix" || builtin === "path/win32" ? pathModuleValue(lowerer, builtin === "path" ? "path/posix" : builtin, locOf(prop)) - : isJsSourceFile(expr.getSourceFile()) + : isJsSourceFile(ts.getSourceFileOfNode(expr)) ? lowerer.lowerExprExpecting(prop.expression, DYN) : lowerer.lowerExpr(prop.expression); fenceSymbolFieldCopy(lowerer, prop.expression, raw.type); @@ -410,7 +410,7 @@ export function lowerDynObjectLiteral( // Repeated references must compare equal and mutations remain shared. if ( !boxValue && - isJsSourceFile(prop.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(prop)) && v.kind === "dynFrom" && v.value.type.kind === "array" ) { @@ -495,7 +495,7 @@ function fenceClosureProbe( slotType: IrType | undefined, attempt: () => IrExpr, ): IrExpr | null { - if (!isJsSourceFile(node.getSourceFile())) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(node))) return null; let inner: ts.Node = node; while (ts.isParenthesizedExpression(inner)) inner = inner.expression; if ( @@ -580,7 +580,10 @@ function literalUnionArmOf( if (own.isUnionType()) return null; props.length = 0; for (const property of lowerer.checker.getPropertiesOfType(own)) { - props.push({ name: property.name, type: lowerer.checker.getTypeOfSymbol(property) }); + props.push({ + name: ts.symbolName(property), + type: lowerer.checker.getTypeOfSymbol(property), + }); } } /** Cheap literal discriminants avoid checker traffic for unrelated arms. @@ -766,7 +769,7 @@ export function lowerObjectLiteral( // Preserve their checked arrays instead of assigning never[] a numeric ABI. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.some( (prop) => ts.isPropertyAssignment(prop) && @@ -779,7 +782,7 @@ export function lowerObjectLiteral( } if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.some( (prop) => ts.isPropertyAssignment(prop) && @@ -794,7 +797,7 @@ export function lowerObjectLiteral( // the represented binding rather than checking it back into that type. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.some((prop) => { let value = ts.isPropertyAssignment(prop) ? prop.initializer @@ -822,7 +825,7 @@ export function lowerObjectLiteral( // JavaScript object methods/accessors carry a live receiver. Keep the // literal in checked native storage instead of erasing its prototype ABI. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (expr.properties.nodes.length === 0 || expr.properties.nodes.some( (prop) => @@ -838,7 +841,7 @@ export function lowerObjectLiteral( // binding. Construct in that representation: an intermediate record would // check nested aliases back into inferred shapes and lose their identity. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && ts.isVariableDeclaration(expr.parent) && expr.parent.initializer === expr && ts.isIdentifier(expr.parent.name) && @@ -850,7 +853,7 @@ export function lowerObjectLiteral( // methods absent from its inferred record shape. Copy the real properties. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.some( (property) => ts.isSpreadAssignment(property) && @@ -870,7 +873,7 @@ export function lowerObjectLiteral( // values. TypeScript keeps the record world and its fence: this shape // only arises in checked-dynamic JS. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.some((p) => { const n = ts.isSpreadAssignment(p) ? undefined : p.name; return n !== undefined && ts.isComputedPropertyName(n); @@ -917,7 +920,7 @@ export function lowerObjectLiteral( // receiver (capturing the record under construction would be an // RC cycle), and the generic lexical-this walk would incorrectly // capture an ENCLOSING method's `this` — the object-method rule. - if (!isJsSourceFile(expr.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) { rejectThisInObjectAccessor(lowerer, prop.body ?? prop); } } @@ -993,7 +996,7 @@ export function lowerObjectLiteral( // `null`, which maps to nothing on its own). if ( tsType.isUnionType() && - ts.constituentTypes(tsType).some((t) => t.getSymbol()?.name === "PromiseLike") + ts.constituentTypes(tsType).some((t) => ts.symbolNameOf(t.getSymbol()) === "PromiseLike") ) { tsType = lowerer.checker.getAwaitedType(tsType) ?? tsType; } @@ -1014,7 +1017,7 @@ export function lowerObjectLiteral( // A JavaScript call can contextually type an object literal as string even // though the literal itself is a record. Keep its own shape so a caller // performing ToString can convert it after the value is built. - if (mapped?.kind === "string" && isJsSourceFile(expr.getSourceFile())) { + if (mapped?.kind === "string" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const own = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (own?.kind === "record") mapped = own; } @@ -1147,7 +1150,7 @@ export function lowerObjectLiteral( // (rest-param function types) narrow away. SEMANTICS.md documents the // enumeration divergence. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (!mapped || expr.properties.nodes.some( (p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p), @@ -1187,7 +1190,7 @@ export function lowerObjectLiteral( // consumers ride the keyed-dyn paths. TypeScript keeps the fence. if ( mapped?.kind === "dyn" || - (!mapped && isJsSourceFile(expr.getSourceFile())) || + (!mapped && isJsSourceFile(ts.getSourceFileOfNode(expr))) || (!expected && mapped && jsOpenObjectType(expr, mapped, lowerer.shapes, lowerer.unions).kind === "dyn") @@ -1203,7 +1206,7 @@ export function lowerObjectLiteral( // extra fields, spreads, accessors, and keyed shapes retain their paths. if ( expected && - !isJsSourceFile(expr.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.every( (p) => ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p), ) @@ -1255,7 +1258,7 @@ export function lowerObjectLiteral( // JavaScript Dates carry native object identity in checked storage. An // inferred scalar Date field would erase that identity before boxing. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && shape.fields.some((field) => field.type.kind === "date") ) { return lowerDynObjectLiteral(lowerer, expr); @@ -1265,7 +1268,7 @@ export function lowerObjectLiteral( // so the literal's shape NARROWS to its plain fields (reads resolve // through the lifted accessors; Object.keys over the value omits // accessor names — the documented divergence). - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { const accessorNames = new Set( expr.properties.nodes .filter((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) @@ -1301,7 +1304,7 @@ export function lowerObjectLiteral( ); if ( topLevelJsDecl && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && assignedProperties.length === expr.properties.nodes.length ) { const provided = new Set(assignedProperties.map((prop) => propNameText(lowerer, prop.name))); @@ -1328,7 +1331,7 @@ export function lowerObjectLiteral( // multiple spreads, spread-after-explicit order, and shape-changing width // copies retain their existing lowering and diagnostics. if ( - !isJsSourceFile(expr.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && !shape.indexValue && !shape.tuple && !shapeHasAccessorSlots(shape) && @@ -2219,7 +2222,7 @@ export function lowerObjectLiteral( // closures is side-effect-free, so their position among the data // fields' source-order evaluation is unobservable — exactly JS, // where accessor definitions evaluate nothing. - if (isJsSourceFile(expr.getSourceFile())) continue; + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) continue; const name = propNameText(lowerer, prop.name); // key-form-checked above const slotName = `${ts.isGetAccessorDeclaration(prop) ? "%get" : "%set"}:${name}`; const slotT = fieldTypes.get(slotName); @@ -2341,7 +2344,7 @@ export function lowerObjectLiteral( if ( !(err instanceof PoisonError) || !pureMember || - !isJsSourceFile(expr.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(expr)) || lowerer.diagSink !== null || lowerer.diags.length <= propDiagsBefore || lowerer.diags.slice(propDiagsBefore).some((d) => d.code === "SC9001") @@ -2436,7 +2439,7 @@ export function lowerObjectLiteral( * their identity. Changing a discriminator or adding variant-specific * fields needs a different result layout and stays on the ordinary path. */ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpression): IrExpr | null { - if (isJsSourceFile(expr.getSourceFile())) return null; + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; const spread = expr.properties.nodes[0]; if (!spread || !ts.isSpreadAssignment(spread)) return null; let sourceNode = spread.expression; diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 995514a4d..5dc512a6c 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -92,7 +92,7 @@ export function isRequireMainFilename(lowerer: Lowerer, expr: ts.Expression): bo const main = expr.expression; if (!ts.isPropertyAccessExpression(main) || main.name.text !== "main") return false; if (!lowerer.isStdlibGlobal(main.expression, "require")) return false; - return !isNodeEsmFile(expr.getSourceFile(), lowerer.program); + return !isNodeEsmFile(ts.getSourceFileOfNode(expr), lowerer.program); } export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): boolean { @@ -257,14 +257,14 @@ export function lowerOptionalChain( const spreads: { arg: number; what: string }[] = []; const args = expr.arguments.nodes.map((arg, index) => { if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN); - spreads.push({ arg: index, what: arg.expression.getText() }); + spreads.push({ arg: index, what: ts.getTextOfNode(arg.expression) }); return lowerer.lowerExprExpecting(arg.expression, DYN); }); const body: IrExpr = { kind: "dynCall", callee, ...(thisValue ? { receiver: thisValue } : {}), - calleeName: recvNode.getText(), + calleeName: ts.getTextOfNode(recvNode), args, ...(spreads.length ? { spreads } : {}), type: DYN, diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 941f5c3f6..36ea0e8c2 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -205,7 +205,7 @@ function isParseArgsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpress const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (!sym) return false; return sym.declarations.some((d) => { - if (!lowerer.isStdlibFile(d.getSourceFile())) return false; + if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false; let inParseArgsType = false; for (let node: ts.Node | undefined = d.parent; node; node = node.parent) { if ( @@ -233,7 +233,7 @@ function isStatFsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression return ( !!symbol && symbol.declarations.some((declaration) => { - if (!lowerer.isStdlibFile(declaration.getSourceFile())) return false; + if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration))) return false; const parent = declaration.parent; if (!ts.isInterfaceDeclaration(parent)) return false; if ( @@ -536,7 +536,7 @@ export function lowerPropertyAccessExpr( if ( represented && isMatchSliceType(lowerer, represented.type) && - (lowerer.isStdlibMember(expr) || isJsSourceFile(expr.getSourceFile())) + (lowerer.isStdlibMember(expr) || isJsSourceFile(ts.getSourceFileOfNode(expr))) ) { const recv = represented; const key: IrExpr = { kind: "strLit", value: expr.name.text, type: STRING, loc }; @@ -569,7 +569,7 @@ export function lowerPropertyAccessExpr( ts.isIdentifier(expr.expression) && expr.expression.text === "arguments" && !lowerer.ctx.argumentsLocal && - lowerer.typeOf(expr.expression).getSymbol()?.name === "IArguments" && + ts.symbolNameOf(lowerer.typeOf(expr.expression).getSymbol()) === "IArguments" && lowerer.isStdlibSymbol(lowerer.typeOf(expr.expression).getSymbol()) ) { let fn: ts.Node | undefined = expr.parent; @@ -640,9 +640,9 @@ export function lowerPropertyAccessExpr( if ( !expr.questionDotToken && isModuleExportsAccess(expr) && - isCjsJsFile(expr.getSourceFile(), lowerer.program) + isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program) ) { - const whole = cjsClassExprWholeExportOf(expr.getSourceFile()); + const whole = cjsClassExprWholeExportOf(ts.getSourceFileOfNode(expr)); if (whole) { let inBody = false; for ( @@ -655,7 +655,7 @@ export function lowerPropertyAccessExpr( break; } } - if (inBody || expr.getStart() > whole.stmt.getEnd()) { + if (inBody || ts.tokenStartOfNode(expr) > whole.stmt.end) { return lowerer.classValueRef(lowerer.lowerClassExpressionInfo(whole.classExpr), expr); } } @@ -802,7 +802,7 @@ export function lowerPropertyAccessExpr( if ( !expr.questionDotToken && stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis" && - isJsSourceFile(expr.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(expr)) ) { const canonical = stdlibGlobalNameOf(lowerer, expr); if (canonical !== null) { @@ -850,7 +850,7 @@ export function lowerPropertyAccessExpr( // them, and calls dispatch through the dyn method machinery // instead. if ( - (isJsSourceFile(expr.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && (lowerer.mapTypeOf(lowerer.typeOf(expr.expression)) === null || (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Never) !== 0 || // A never-tainted receiver type maps (never rides as f64) but @@ -891,7 +891,7 @@ export function lowerPropertyAccessExpr( : { kind: "strIntrinsic", method: "length", receiver: recv, args: [], type: F64, loc }; } } - if (isJsSourceFile(expr.getSourceFile()) && expr.name.text === "constructor") { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.name.text === "constructor") { const receiver = tryLowerExpression(lowerer, expr.expression); if ( receiver?.type.kind === "dyn" || @@ -955,7 +955,7 @@ export function lowerPropertyAccessExpr( // storage can expose its own name and length in both source languages. if ( !["apply", "bind", "call", "toString", "caller", "arguments"].includes(expr.name.text) && - (isJsSourceFile(expr.getSourceFile()) || + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || expr.name.text === "name" || expr.name.text === "length" || expr.name.text === "constructor") @@ -968,7 +968,9 @@ export function lowerPropertyAccessExpr( const receiverSymbol = receiverType.getSymbol(); const checkedFunction = lowerer.checker.getCallSignatures(receiverType).length > 0 || - (receiverSymbol?.name === "Function" && lowerer.isStdlibSymbol(receiverSymbol)); + (receiverSymbol !== undefined && + ts.symbolName(receiverSymbol) === "Function" && + lowerer.isStdlibSymbol(receiverSymbol)); if (probed?.type.kind === "dyn" && checkedFunction) { const key: IrExpr = { kind: "strLit", @@ -990,7 +992,7 @@ export function lowerPropertyAccessExpr( ); } if ( - (isJsSourceFile(expr.getSourceFile()) || expr.name.text === "constructor") && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || expr.name.text === "constructor") && probed?.type.kind === "func" && canBoxFuncIntoDyn( probed.type, @@ -1024,17 +1026,17 @@ export function lowerPropertyAccessExpr( if (expr.name.text === "done" || expr.name.text === "value") { const receiverType = lowerer.typeOf(expr.expression); const recvSym = - receiverType.getAliasSymbol()?.name === "IteratorResult" + ts.symbolNameOf(receiverType.getAliasSymbol()) === "IteratorResult" ? receiverType.getAliasSymbol() : receiverType.getSymbol(); if ( - (recvSym?.name === "IteratorResult" || - recvSym?.name === "IteratorYieldResult" || - recvSym?.name === "IteratorReturnResult") && + ((recvSym !== undefined && ts.symbolName(recvSym) === "IteratorResult") || + (recvSym !== undefined && ts.symbolName(recvSym) === "IteratorYieldResult") || + (recvSym !== undefined && ts.symbolName(recvSym) === "IteratorReturnResult")) && recvSym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const recv = lowerer.lowerExpr(expr.expression); @@ -1085,7 +1087,8 @@ export function lowerPropertyAccessExpr( // members owned by these implemented families bypass the stdlib fence. const segmentDataSymbol = lowerer.typeOf(expr.expression).getSymbol(); const segmentDataProperty = - segmentDataSymbol?.name === "SegmentData" && + segmentDataSymbol !== undefined && + ts.symbolName(segmentDataSymbol) === "SegmentData" && lowerer.isStdlibSymbol(segmentDataSymbol) && ["segment", "index", "input", "isWordLike"].includes(expr.name.text); // os.constants members vary by platform (the declared groups list @@ -1192,7 +1195,7 @@ export function lowerPropertyAccessExpr( // validated dynCheck as usual. if ( recvLowered.type.kind === "dyn" || - (isJsSourceFile(expr.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && (["f64", "bool", "bigint", "string", "symbol"].includes(recvLowered.type.kind) || (recvLowered.type.kind === "union" && lowerer.dynConvertible(recvLowered.type)))) ) { @@ -1260,8 +1263,8 @@ export function lowerPropertyAccessExpr( recvShape && !recvShape.tuple && lowerer.implicitParamTypes !== null && - isJsSourceFile(expr.getSourceFile()) && - npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null + isJsSourceFile(ts.getSourceFileOfNode(expr)) && + npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null ) { return { kind: "seqExpr", @@ -1508,7 +1511,12 @@ export function lowerIntrinsicProperty( | "BigInt" )[]) { if (!lowerer.chainBlocked(expr) && lowerer.isStdlibGlobal(expr, name)) { - return primitiveCtorClosure(lowerer, name, locOf(expr), isJsSourceFile(expr.getSourceFile())); + return primitiveCtorClosure( + lowerer, + name, + locOf(expr), + isJsSourceFile(ts.getSourceFileOfNode(expr)), + ); } } if (lowerer.stdlibGlobalMember(expr, "Reflect") === "ownKeys") { @@ -1686,7 +1694,7 @@ export function lowerIntrinsicProperty( ) return null; const name = expr.name.text; - if (kind === "array" && name === "constructor" && isJsSourceFile(expr.getSourceFile())) { + if (kind === "array" && name === "constructor" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const receiver = lowerer.lowerExpr(expr.expression); if (receiver.type.kind === "dyn") return { @@ -1714,7 +1722,7 @@ export function lowerIntrinsicProperty( const strictReceiver = (expected: IrType): IrExpr => { const value = lowerer.lowerExpr(expr.expression); if (value.type.kind === "dyn") { - if (isJsSourceFile(expr.getSourceFile()) && name === "length") return value; + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && name === "length") return value; return lowerer.coerceInto(expr.expression, value, expected); } return lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, expected, name) ?? value; @@ -2234,7 +2242,7 @@ function lowerReplacedExportsRead( if (expr.questionDotToken) return null; const recv = expr.expression; if (!ts.isIdentifier(recv) || recv.text !== "exports") return null; - const sf = expr.getSourceFile(); + const sf = ts.getSourceFileOfNode(expr); if (!isJsSourceFile(sf) || isNodeEsmFile(sf, lowerer.program)) return null; if (lowerer.peekLocal(recv) || lowerer.globalOf(recv)) return null; // a user binding shadows // Write position is the export-assignment machinery's territory. @@ -2271,7 +2279,7 @@ function lowerCjsExportTableThisMember( expr: ts.PropertyAccessExpression, ): IrExpr | null { if (expr.expression.kind !== ts.SyntaxKind.ThisKeyword || expr.questionDotToken) return null; - if (!isJsSourceFile(expr.getSourceFile())) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; // The enclosing accessor, crossing only arrow boundaries (arrows // inherit `this`; any other function form rebinds it). let n: ts.Node | undefined = expr.parent; @@ -2471,7 +2479,8 @@ function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressio const groups = matchResultNamedGroupsOf(lowerer, expr.expression); // An untraced regex: TypeScript reaches the member fence; JavaScript // would otherwise read the slice's absent expando as undefined. - if (groups === null && isJsSourceFile(expr.getSourceFile())) lowerer.stdlibMemberFence(expr); + if (groups === null && isJsSourceFile(ts.getSourceFileOfNode(expr))) + lowerer.stdlibMemberFence(expr); if (groups === null) return null; if (groups.length === 0) { // Node: a match result of a group-less regex answers undefined — @@ -2488,7 +2497,7 @@ function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressio } const recv = lowerer.lowerExpr(expr.expression); // A JavaScript callback parameter holds the row as a dynamic array. - const dynRow = recv.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile()); + const dynRow = recv.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)); if (!dynRow && !isMatchSliceType(lowerer, recv.type)) return null; return lowerGroupsProjection(lowerer, recv, groups, loc); } @@ -3065,7 +3074,7 @@ export function lowerUnionProperty( // instance argument (e.g. Matrix4.makeTranslation). Preserve primitive // missing-property semantics and dispatch class data through its native // capsule instead of demanding a field shared by every union arm. - if (isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(value.type)) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(value.type)) { return { kind: "dynKeyGet", value: lowerer.coerceToExpected(value, DYN), @@ -3217,7 +3226,7 @@ export function classFieldTarget( const represented = saved ?? lowerer.lowerExpr(receiverNode); if ( represented.type.kind === "dyn" && - isJsSourceFile(receiverNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(receiverNode)) && !fieldName.startsWith("#") ) { return { container: "dynamic", obj: represented, field: fieldName, fieldType: DYN }; @@ -3264,7 +3273,7 @@ export function classFieldTarget( if (fieldType) { const obj = lowerObjectReceiver(); if ( - isJsSourceFile(receiverNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(receiverNode)) && !fieldName.startsWith("#") && [...lowerer.classes.values()].some( (candidate) => @@ -3302,7 +3311,7 @@ export function classFieldTarget( fieldType: getF ? getF.sig.ret : setF!.sig.params[0]!.type, }; } - if (info.decl && isJsSourceFile(info.decl.getSourceFile())) { + if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) { const property = lowerer.checker.getPropertyOfType( lowerer.checker.getTypeAtLocation(receiverNode), fieldName, @@ -3521,7 +3530,7 @@ export function fieldTarget( : [declared]; return declaredArms.every((a) => ivArms.some((b) => typeEquals(a, b))); }; - if (!nameSym || nameSym.name === ts.InternalSymbolName.Index || canonicalized()) { + if (!nameSym || ts.symbolName(nameSym) === ts.InternalSymbolName.Index || canonicalized()) { let obj = lowerer.lowerExpr(access.expression); if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) { const present = lowerer.stripUndefinedArm(obj.type); @@ -3633,7 +3642,7 @@ export function fieldGetExpr( } if ( !target.field.startsWith("#") && - isJsSourceFile(blame.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(blame)) && lowerer.dynConvertible(getF.sig.ret) ) { const value: IrExpr = { @@ -3915,7 +3924,9 @@ export function builtinGlobalValueAt( )); if (reflects) { const name = - owner !== null ? `${owner}.${callee.name.text}` : callee.getText().replace(/\s+/g, ""); + owner !== null + ? `${owner}.${callee.name.text}` + : ts.getTextOfNode(callee).replace(/\s+/g, ""); lowerer.noLowering( `${name}(${canonical}, ...)`, expr, @@ -3958,8 +3969,8 @@ function fenceBuiltinConstructorStaticRead( const symbol = lowerer.typeOf(access.expression).getSymbol(); if ( symbol !== undefined && - symbol.name.endsWith("Constructor") && - GLOBAL_CONSTRUCTOR_VALUES.has(symbol.name.slice(0, -"Constructor".length)) && + ts.symbolName(symbol).endsWith("Constructor") && + GLOBAL_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && lowerer.isStdlibSymbol(symbol) ) lowerer.stdlibMemberFence(access); @@ -4036,7 +4047,7 @@ function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpr return ( decls.length > 0 && decls.every((d) => { - if (!isJsSourceFile(d.getSourceFile())) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(d))) return false; const assign = ts.isBinaryExpression(d) ? d : d.parent; return ( assign !== undefined && diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 91d1468c2..f8b8c7604 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -470,7 +470,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc ); } const constructor = tryLowerExpression(lowerer, expr.right); - if (constructor?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return checkedClassInstanceOf(lowerer, value, constructor, loc); } @@ -478,7 +478,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // function's prototype against the value's prototype chain. if ( constructor?.type.kind === "func" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(constructor.type) ) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); @@ -1080,7 +1080,11 @@ function lowerPrivateIn( // when the value is a class instance at or below the declarer. A typed // reference carries the class it was boxed under: the declarer or a // descendant answers true; an ancestor answers per instance. - if (recv.type.kind === "dyn" && recv.kind === "varRef" && isJsSourceFile(expr.getSourceFile())) { + if ( + recv.type.kind === "dyn" && + recv.kind === "varRef" && + isJsSourceFile(ts.getSourceFileOfNode(expr)) + ) { // A function-local class has one brand per evaluation. if (info.localClass) { const owner = localPrivateBrandClass(lowerer, priv); @@ -1381,7 +1385,7 @@ export function lowerInExpression( recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined; if ( (classInfo && (classInfo.callableBase || hasRuntimeStatics(classInfo))) || - (isJsSourceFile(expr.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && recv.type.kind === "func" && lowerer.dynConvertible(recv.type)) ) { diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index 41a05ac2b..46005e228 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -71,7 +71,7 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc (operand.text === "__dirname" || operand.text === "__filename") && lowerer.isStdlibGlobal(operand, operand.text) ) { - const sf = operand.getSourceFile(); + const sf = ts.getSourceFileOfNode(operand); if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName)) return { kind: "strLit", value: "undefined", type: STRING, loc }; } @@ -204,7 +204,7 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc // its own lowering as an identity token (a string), so the operand's // representation cannot answer. Node's answer follows the export's // kind, and the binding read has no effects. - const imported = isJsSourceFile(tested.getSourceFile()) + const imported = isJsSourceFile(ts.getSourceFileOfNode(tested)) ? lowerer.builtinImportOf(tested) : null; if (imported && builtinModuleConstOf(lowerer, imported.module, imported.member) === undefined) { diff --git a/packages/compiler/src/frontend/lowering/expressions/union-fields.ts b/packages/compiler/src/frontend/lowering/expressions/union-fields.ts index f2dd0d0f0..6454bc1fd 100644 --- a/packages/compiler/src/frontend/lowering/expressions/union-fields.ts +++ b/packages/compiler/src/frontend/lowering/expressions/union-fields.ts @@ -117,7 +117,7 @@ export function lowerUnionFieldRead( access: ts.PropertyAccessExpression, receiver: IrExpr, ): IrExpr | null { - if (receiver.type.kind !== "union" || isJsSourceFile(access.getSourceFile())) return null; + if (receiver.type.kind !== "union" || isJsSourceFile(ts.getSourceFileOfNode(access))) return null; const unionId = receiver.type.unionId; const arms = lowerer.unions.get(unionId)?.arms; if (!arms || arms.length < 2) return null; diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 41d8f6332..104057906 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -110,7 +110,7 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio } // Match the checked numeric boundary used by JS binary arithmetic. const operand = - raw.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile()) + raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) ? { kind: "dynCheck" as const, value: raw, type: F64, loc } : lowerOptionalNumber(lowerer, raw, loc, expr.operand); if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]); @@ -263,7 +263,7 @@ export function lowerIncDec( const receiver = save(raw, "%incrementArray"); const rawIndex = lowerer.lowerExpr(access.argumentExpression); const index = save( - rawIndex.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile()) + rawIndex.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) ? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [rawIndex], type: F64, loc } : rawIndex, "%incrementIndex", @@ -308,10 +308,13 @@ export function lowerIncDec( // class-backed protocol view): the expanded form `o.v = o.v + 1`, its // read checked into number like any other read of the member. const checkedNumber = - !isJsSourceFile(expr.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && raw.type.kind === "dyn" && lowerer.mapTypeOf(lowerer.typeOf(access))?.kind === "f64"; - if ((isJsSourceFile(expr.getSourceFile()) || checkedNumber) && raw.type.kind === "dyn") { + if ( + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || checkedNumber) && + raw.type.kind === "dyn" + ) { const receiver = save(raw, "%incrementReceiver"); const key = save( ts.isElementAccessExpression(access) @@ -352,7 +355,7 @@ export function lowerIncDec( ), ); } - if (isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(raw.type)) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(raw.type)) { const receiver = save(lowerer.coerceInto(access.expression, raw, DYN), "%incrementReceiver"); const key = save( ts.isElementAccessExpression(access) @@ -818,7 +821,7 @@ export function lowerFieldCompoundValue( rhsNode ? lowerer.lowerExpr(rhsNode) : { kind: "numLit", value: 1, type: F64, loc }, "%compoundRhs", ); - if (isJsSourceFile(access.getSourceFile()) && lowerer.dynConvertible(rhs.type)) { + if (isJsSourceFile(ts.getSourceFileOfNode(access)) && lowerer.dynConvertible(rhs.type)) { const numeric = ["-", "*", "/", "%", "**"].includes(op); const right = lowerer.coerceToExpected(rhs, DYN); const value = save( @@ -912,7 +915,7 @@ export function lowerFieldCompoundValue( "%compoundRhs", ); const numericRhs: IrExpr = - rhs.type.kind === "dyn" && isJsSourceFile(access.getSourceFile()) + rhs.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(access)) ? { kind: "dynCheck", value: rhs, type: F64, loc: rhs.loc } : lowerOptionalNumber(lowerer, rhs, loc); let value: IrExpr; @@ -927,7 +930,7 @@ export function lowerFieldCompoundValue( } else if (bigintValue) { value = bigintValue; } else if ( - (target.fieldType.kind === "dyn" && isJsSourceFile(access.getSourceFile())) || + (target.fieldType.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(access))) || // An untyped right operand: the expanded `o.f = o.f op rhs`; the field // write below checks the result into the declared field type. (rhs.type.kind === "dyn" && diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index c0e70013c..9500d6d2d 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -98,8 +98,8 @@ export function appendImplicitUndefinedReturn( /** Whether the source explicitly documents a return contract. */ export function hasExplicitJsDocReturn(decl: ts.SignatureDeclaration): boolean { - const sourceFile = decl.getSourceFile(); - return /@returns?\b/.test(sourceFile.text.slice(decl.pos, decl.getStart(sourceFile))); + const sourceFile = ts.getSourceFileOfNode(decl); + return /@returns?\b/.test(sourceFile.text.slice(decl.pos, ts.tokenStartOfNode(decl))); } export function promiseCarriesDyn(lowerer: Lowerer, type: IrType): boolean { @@ -130,14 +130,14 @@ export function declaredReturnType( decl: ts.SignatureDeclaration, blame: ts.Node, ): IrType { - if (isJsSourceFile(decl.getSourceFile()) && ts.isGetAccessorDeclaration(decl)) return DYN; + if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && ts.isGetAccessorDeclaration(decl)) return DYN; const sig = lowerer.checker.getSignatureFromDeclaration(decl); if (!sig) lowerer.unsupported("SC1090", decl, "this function form"); const retTsType = lowerer.checker.getReturnTypeOfSignature(sig); // An inferred null/undefined return in JS can read a mutable checked-value // field. Keep the actual value instead of restricting it to the initializer. if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && (retTsType.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 ) return DYN; @@ -154,7 +154,7 @@ export function declaredReturnType( ) mappedReturn = mapAsyncGeneratorAnnotation(retTsType, lowerer.typeCtx); if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && !decl.asteriskToken && mappedReturn?.kind !== "promise" @@ -162,7 +162,11 @@ export function declaredReturnType( const fallback = dynFallbackType(lowerer, decl, retTsType); return fallback?.kind === "promise" ? fallback : { kind: "promise", inner: DYN }; } - if (mappedReturn && isJsSourceFile(decl.getSourceFile()) && functionCanReturnUndefined(decl)) + if ( + mappedReturn && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + functionCanReturnUndefined(decl) + ) mappedReturn = lowerer.withUndefinedArmOf(mappedReturn) ?? mappedReturn; // A generic-class method body that can return an indexed read that // misses returns undefined there, whatever the declared result says. @@ -171,33 +175,33 @@ export function declaredReturnType( mappedReturn.kind === "promise" ? { kind: "promise", inner: lowerer.runtimeOptionalType(mappedReturn.inner) } : lowerer.runtimeOptionalType(mappedReturn); - if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "void") { + if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && mappedReturn?.kind === "void") { let returnsValue = false; const scan = (node: ts.Node): void => { if (node !== decl && ts.isFunctionLike(node)) return; if (ts.isReturnStatement(node) && node.expression) returnsValue = true; - node.forEachChild(scan); + ts.forEachChild(node, scan); }; scan(decl); if (returnsValue) return DYN; } - if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "array") return DYN; + if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && mappedReturn?.kind === "array") return DYN; if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasExplicitJsDocReturn(decl) && mappedReturn?.kind === "record" ) return DYN; if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasExplicitJsDocReturn(decl) && mappedReturn?.kind === "date" ) return DYN; if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasExplicitJsDocReturn(decl) && mappedReturn?.kind === "union" && @@ -207,7 +211,7 @@ export function declaredReturnType( // JS inference assigns Number to arithmetic over any, although the // runtime operands can both be BigInts. Preserve that native result. if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasExplicitJsDocReturn(decl) && mappedReturn?.kind === "f64" @@ -237,13 +241,13 @@ export function declaredReturnType( ) ) numericAnyReturn = true; - node.forEachChild(check); + ts.forEachChild(node, check); }; check(decl); if (numericAnyReturn) return DYN; } if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !hasExplicitJsDocReturn(decl) && mappedReturn !== null && promiseCarriesDyn(lowerer, mappedReturn) && @@ -264,7 +268,7 @@ export function declaredReturnType( // value rides its own box, calls go through the boxed thunk (JS // arity), and typed slots re-check with dynCheck as usual. if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && decl.type === undefined && lowerer.mapTypeOf(retTsType)?.kind === "func" ) { @@ -304,7 +308,7 @@ export function declaredReturnType( (arm.kind === "object" && !hasExplicitJsDocReturn(decl)), ); if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && decl.type === undefined && recordReturn !== null && recordReturn !== undefined && @@ -468,5 +472,5 @@ export function blockBodyOf(decl: ts.FunctionLikeDeclaration): ts.Block | null { const body = decl.body; if (body === undefined) return null; if (ts.isBlock(body)) return body; - return decl.forEachChild((c) => (ts.isBlock(c) ? c : undefined)) ?? null; + return ts.forEachChild(decl, (c) => (ts.isBlock(c) ? c : undefined)) ?? null; } diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index b586884c5..880cccc8b 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -225,7 +225,7 @@ export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDecla lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify(decl.getSourceFile(), nsPathPrefix(decl) + nameText), + qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), typeParams, instances: new Map(), }); @@ -306,7 +306,7 @@ export function genericCallInstance( // untyped value, such as an untyped forwarding call) is checked-dynamic. const mappedReturn = lowerer.mapTypeOf(retTs) ?? - ((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile()) + ((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(ts.getSourceFileOfNode(info.decl)) ? DYN : null); if (!mappedReturn) { @@ -552,7 +552,7 @@ function internGenericInstance( return JSON.stringify(tsBound.value); } const bound = bindings.get(tp); - return bound ? lowerer.fmt(bound) : tp.name; + return bound ? lowerer.fmt(bound) : ts.symbolName(tp); }) .join(", "); // Deep polymorphic recursion renders unbounded types — keep messages sane. @@ -699,7 +699,8 @@ function unifySignatureBindings( if (!bindings.has(sym)) { const mapped = lowerer.mapTypeOf(inst) ?? - ((inst.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile()) + ((inst.flags & ts.TypeFlags.Any) !== 0 && + !isJsSourceFile(ts.getSourceFileOfNode(info.decl)) ? DYN : null); // A visitor's inferred T can be void. It still binds the @@ -811,7 +812,7 @@ function unifySignatureBindings( } if (declared.flags & ts.TypeFlags.Object) { for (const dp of lowerer.checker.getPropertiesOfType(declared)) { - const ip = lowerer.checker.getPropertyOfType(inst, dp.name); + const ip = lowerer.checker.getPropertyOfType(inst, ts.symbolName(dp)); if (ip) unify( lowerer.checker.getTypeOfSymbol(dp), @@ -1171,7 +1172,7 @@ function genericValueInstance( const rendered = info.typeParams .map((tp) => { const bound = bindings.get(tp); - return bound ? lowerer.fmt(bound) : tp.name; + return bound ? lowerer.fmt(bound) : ts.symbolName(tp); }) .join(", "); lowerer.typeParamBindings = bindings; @@ -1270,7 +1271,7 @@ function paramWrittenInBody( hit = true; return; } - m.forEachChild(scan); + ts.forEachChild(m, scan); }; scan(n); return hit; @@ -1305,7 +1306,7 @@ function paramWrittenInBody( written = true; return; } - n.forEachChild(walk); + ts.forEachChild(n, walk); }; walk(body); return written; @@ -1480,7 +1481,7 @@ export function implicitAnyParamSymbolsOf( }); // Destructured object parameters monomorphize too (implicitPatternsOf); // their slots stay null here. - if (!hasImplicitParams && isJsSourceFile(decl.getSourceFile())) + if (!hasImplicitParams && isJsSourceFile(ts.getSourceFileOfNode(decl))) hasImplicitParams = implicitPatternsOf(lowerer, decl).some((pattern) => pattern !== null); return hasImplicitParams ? out : null; } @@ -1630,7 +1631,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | // declaredReturnType widens non-generic JavaScript functions. if ( mapped !== null && - isJsSourceFile(info.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && functionCanReturnUndefined(info.decl) ) return lowerer.withUndefinedArmOf(mapped) ?? mapped; @@ -1678,7 +1679,7 @@ function bindableImplicitIr(t: IrType | null): t is IrType { function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrType | null { while (ts.isParenthesizedExpression(arg)) arg = arg.expression; if ( - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) ) { const saved = lowerer.diagSink; @@ -1701,7 +1702,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp } if ( ts.isObjectLiteralExpression(arg) && - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && (arg.properties.nodes.length === 0 || arg.properties.nodes.some( (property) => @@ -1716,7 +1717,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp if (ts.isIdentifier(arg)) return lowerer.peekLocal(arg)?.type ?? lowerer.globalOf(arg)?.type ?? null; if ( - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && (ts.isPropertyAccessExpression(arg) || ts.isElementAccessExpression(arg)) ) { // Mutable JS members can retain an initializer-only checker type (for @@ -1726,7 +1727,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp if (represented) return represented.type; } if ( - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && (ts.isCallExpression(arg) || ts.isConditionalExpression(arg) || (ts.isBinaryExpression(arg) && @@ -1843,7 +1844,7 @@ export function implicitCallInstance( (neverTaintedJsType(lowerer, arg, t) ? null : lowerer.mapTypeOf(t)) ?? dynFallbackType(lowerer, arg, t); if ( - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && ts.isArrayLiteralExpression(arg) && mapped?.kind === "array" && (mapped.elem.kind === "classval" || @@ -1868,7 +1869,7 @@ export function implicitCallInstance( // preserve represented arms through the checked boundary instead // of specializing a call to a union that would discard them. if ( - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && mapped?.kind === "func" && !lowerer.dynConvertible(mapped) ) { @@ -2066,7 +2067,7 @@ export function implicitLocalFnNodeOf( if (cached !== undefined) return cached ? (cached.decl as ts.FunctionExpression | ts.ArrowFunction) : null; const probe = (): ts.FunctionExpression | ts.ArrowFunction | null => { - if (!implicitMonoFile(decl.getSourceFile())) return null; + if (!implicitMonoFile(ts.getSourceFileOfNode(decl))) return null; if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null; let init: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; @@ -2097,7 +2098,7 @@ export function implicitLocalFnNodeOf( const d = s ? s.valueDeclaration : undefined; if ( d && - d.getSourceFile() === decl.getSourceFile() && + ts.getSourceFileOfNode(d) === ts.getSourceFileOfNode(decl) && !(d.pos >= init.pos && d.end <= init.end) ) { // Declared outside the initializer, in this file: a capture @@ -2116,7 +2117,7 @@ export function implicitLocalFnNodeOf( } } } - n.forEachChild(scan); + ts.forEachChild(n, scan); }; scan(init.body); return captures ? null : init; @@ -2154,8 +2155,8 @@ export function implicitLocalFnInfoOf( decl: fnNode, baseName: name, qualifiedName: lowerer.qualify( - decl.getSourceFile(), - nsPathPrefix(stmt ?? decl, decl) + `${name}%l${decl.getStart()}`, + ts.getSourceFileOfNode(decl), + nsPathPrefix(stmt ?? decl, decl) + `${name}%l${ts.tokenStartOfNode(decl)}`, ), typeParams: [], instances: new Map(), @@ -2249,7 +2250,10 @@ export function objLitGenericFnInfoOf( const info: GenericFnInfo = { decl: fnNode, baseName: name, - qualifiedName: lowerer.qualify(fnNode.getSourceFile(), `%ol${fnNode.getStart()}.${name}`), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(fnNode), + `%ol${ts.tokenStartOfNode(fnNode)}.${name}`, + ), typeParams, instances: new Map(), objectLiteral: true, @@ -2381,7 +2385,10 @@ export function bindingGenericFnInfoOf( const info: GenericFnInfo = { decl: fnNode, baseName: name, - qualifiedName: lowerer.qualify(decl.getSourceFile(), nsPathPrefix(stmt ?? decl, decl) + name), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(stmt ?? decl, decl) + name, + ), typeParams, instances: new Map(), }; diff --git a/packages/compiler/src/frontend/lowering/import-meta.ts b/packages/compiler/src/frontend/lowering/import-meta.ts index baf550e6d..796e64b69 100644 --- a/packages/compiler/src/frontend/lowering/import-meta.ts +++ b/packages/compiler/src/frontend/lowering/import-meta.ts @@ -20,7 +20,7 @@ export function isImportMeta(expr: ts.Expression): boolean { /** File-backed metadata is fixed by the compiled module graph. Reads share * one implementation across dot access, folded keys and selected bindings. */ export function importMetaField(lowerer: Lowerer, node: ts.Node, name: string): IrExpr { - const sf = node.getSourceFile(); + const sf = ts.getSourceFileOfNode(node); const fileName = lowerer.targetPlatform === "wasi" ? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")) diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts index 32bb71858..fc69237db 100644 --- a/packages/compiler/src/frontend/lowering/intern-tables.ts +++ b/packages/compiler/src/frontend/lowering/intern-tables.ts @@ -142,7 +142,7 @@ interface TableTypes { /** The key and value types of an InternTable checker type. */ function tableTypes(lowerer: Lowerer, type: ts.Type, blame: ts.Node): TableTypes { const parts = type.isUnionType() ? ts.constituentTypes(type) : [type]; - const table = parts.find((p) => p.getSymbol()?.name === "InternTable"); + const table = parts.find((p) => ts.symbolNameOf(p.getSymbol()) === "InternTable"); const args = table ? lowerer.checker.getTypeArguments(table as ts.TypeReference) : []; if (args.length !== 2) lowerer.noLowering("InternTable without type arguments", blame); const key = lowerer.mapTypeOf(args[0]!); diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index d39e5cceb..b9bbf6216 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -81,10 +81,10 @@ export function isValueReturningConstructorClass(decl: ts.ClassDeclaration): boo if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return; if (ts.isReturnStatement(node) && node.expression !== undefined && !insideArrow(node, ctor!)) returnsValue = true; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - ctor.body.forEachChild(visit); - for (const parameter of ctor.parameters.nodes) parameter.forEachChild(visit); + ts.forEachChild(ctor.body, visit); + for (const parameter of ctor.parameters.nodes) ts.forEachChild(parameter, visit); return returnsValue && !observesThis; } @@ -215,7 +215,7 @@ export function lowerValueReturningConstructorNew( "spread arguments when constructing a class whose constructor returns a value", ); } - const key = `ctorValue:${decl.getSourceFile().fileName}:${decl.getStart()}`; + const key = `ctorValue:${ts.getSourceFileOfNode(decl).fileName}:${ts.tokenStartOfNode(decl)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { name = `%ctorValue.${lowerer.valueHelpers.size}`; @@ -235,7 +235,7 @@ export function lowerValueReturningConstructorNew( { kind: "runtimeFence", code: "SC1090", - message: `constructors that return no object (the bare instance of '${decl.name?.text ?? "class"}' has no representation here) are not supported yet [SC1090 at ${decl.getSourceFile().fileName}:${ts.getLineAndCharacterOfPosition(decl.getSourceFile(), ctor.getStart()).line + 1}]`, + message: `constructors that return no object (the bare instance of '${decl.name?.text ?? "class"}' has no representation here) are not supported yet [SC1090 at ${ts.getSourceFileOfNode(decl).fileName}:${ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(decl), ts.tokenStartOfNode(ctor)).line + 1}]`, loc: locOf(ctor), }, ]; @@ -261,8 +261,8 @@ export function lowerValueReturningConstructorNew( .map((arg) => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) })); const result = lowerer.declareHiddenLocal("%constructed", DYN); const value: IrExpr = { kind: "varRef", localId: result.id, type: DYN, loc }; - const sf = expr.getSourceFile(); - const line = ts.getLineAndCharacterOfPosition(sf, expr.getStart()).line + 1; + const sf = ts.getSourceFileOfNode(expr); + const line = ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(expr)).line + 1; const objectLike: IrExpr = { kind: "logical", op: "||", diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts index 7c688dcda..927745253 100644 --- a/packages/compiler/src/frontend/lowering/json-require.ts +++ b/packages/compiler/src/frontend/lowering/json-require.ts @@ -36,7 +36,7 @@ export function jsonRequireTarget( if (call === undefined || !ts.isCallExpression(call)) return null; const spec = requireSpecOf(call); if (spec === null || !ts.isIdentifier(call.expression)) return null; - const sf = call.getSourceFile(); + const sf = ts.getSourceFileOfNode(call); if (!isCjsJsFile(sf, lowerer.program)) return null; if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null; const target = resolveImport(lowerer.program, sf, spec, "require"); diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index cb31ecc79..460f4d53a 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -887,10 +887,10 @@ export function lowerAssertDirectCall( * call's SOURCE TEXT (Node re-reads the file at runtime — the frontend has * the same bytes at compile time). */ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - requireStatementPosition(lowerer, expr, expr.expression.getText()); + requireStatementPosition(lowerer, expr, ts.getTextOfNode(expr.expression)); if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { lowerer.noLowering( - `${expr.expression.getText()} with ${expr.arguments.nodes.length} arguments`, + `${ts.getTextOfNode(expr.expression)} with ${expr.arguments.nodes.length} arguments`, expr, "the supported forms are assert(value) and assert(value, message)", ); @@ -912,7 +912,7 @@ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): // Node reads the CALLSITE LINE and slices the expression out of it // (internal/errors/error_source.js) — a call spanning lines truncates // at the first newline, so the compile-time text does too. - const source = expr.getText().split("\n", 1)[0]!; + const source = ts.getTextOfNode(expr).split("\n", 1)[0]!; const text = `The expression evaluated to a falsy value:\n\n ${source}\n`; msg = { kind: "strLit", value: text, type: STRING, loc }; generated = true; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index db4c03402..b1b4086cd 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -678,7 +678,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const isStorage = parts.length > 0 && parts.every((part) => { - const name = part.getSymbol()?.name; + const name = ts.symbolNameOf(part.getSymbol()); return name === "ArrayBuffer" || name === "SharedArrayBuffer"; }); if (isStorage) { @@ -852,7 +852,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { (source.type.kind === "dyn" || source.type.kind === "func" || (source.type.kind === "union" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(source.type)) || ((source.type.kind === "map" || source.type.kind === "set") && lowerer.dynConvertible(source.type)) || @@ -868,7 +868,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.lowerExprExpecting(ts.isSpreadElement(arg) ? arg.expression : arg, DYN), ); const spreads = expr.arguments.nodes.flatMap((arg, index) => - ts.isSpreadElement(arg) ? [{ arg: index, what: arg.expression.getText() }] : [], + ts.isSpreadElement(arg) ? [{ arg: index, what: ts.getTextOfNode(arg.expression) }] : [], ); return { kind: "seqExpr", @@ -879,7 +879,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { kind: "dynCall", callee, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, ...(spreads.length > 0 ? { spreads } : {}), type: DYN, @@ -1184,7 +1184,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // argument list. Typed .ts spreads keep completeArgs' rest packing. const jsSpreadArgs = expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) && - isJsSourceFile(expr.getSourceFile()); + isJsSourceFile(ts.getSourceFileOfNode(expr)); if (lowerer.isTopLevelFnSymbol(expr.expression) && !lowerer.peekLocal(expr.expression)) { // `import g = N.f; g()` — the alias's own source-order guards // (a no-op for every non-import= binding). @@ -1394,12 +1394,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if ( g && (g.type.kind === "func" || g.type.kind === "dyn") && - !cjsExportCallIgnoresThis(lowerer, expr.getSourceFile(), expr.expression.name.text) + !cjsExportCallIgnoresThis(lowerer, ts.getSourceFileOfNode(expr), expr.expression.name.text) ) { lowerer.unsupported( "SC1090", expr, - `calling '${expr.expression.getText()}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`, + `calling '${ts.getTextOfNode(expr.expression)}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`, ); } if (g && (g.type.kind === "func" || g.type.kind === "dyn")) { @@ -1598,7 +1598,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const boxed = lowerDynReceiverMethodCall(lowerer, expr, expr.expression, true); if (boxed) return boxed; } - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { const staticField = lowerStaticFieldRead(lowerer, expr.expression); if (staticField?.type.kind === "dyn" && !expr.arguments.nodes.some(ts.isSpreadElement)) { return { @@ -1666,7 +1666,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.unsupported( "SC1090", expr, - `calls through function-with-properties values ('${expr.expression.expression.getText()}' is callable AND carries members — the chalk shape; no hybrid representation exists yet: export the base and the property as separate functions)`, + `calls through function-with-properties values ('${ts.getTextOfNode(expr.expression.expression)}' is callable AND carries members — the chalk shape; no hybrid representation exists yet: export the base and the property as separate functions)`, ); } } @@ -1691,7 +1691,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ); } } - lowerer.unsupported("SC1090", expr, `method calls like '${expr.expression.getText()}'`); + lowerer.unsupported("SC1090", expr, `method calls like '${ts.getTextOfNode(expr.expression)}'`); } // `s[Symbol.iterator]()` on a string: the string iterator as a value. @@ -1730,7 +1730,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ) { const access = expr.expression; const value = tryLowerExpression(lowerer, access.expression); - if (value?.type.kind === "array" && !isJsSourceFile(expr.getSourceFile())) { + if (value?.type.kind === "array" && !isJsSourceFile(ts.getSourceFileOfNode(expr))) { const callback = lowerArrayCallbackCall(lowerer, expr, access, value); if (callback) return callback; } @@ -1765,7 +1765,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { kind: "dynCall", callee, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -1884,7 +1884,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const calleeName = ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression) - ? expr.expression.getText() + ? ts.getTextOfNode(expr.expression) : ts.isIdentifier(expr.expression) ? expr.expression.text : "value"; @@ -1946,7 +1946,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // per use like every any-origin value. if ( (expr.arguments.nodes.length > callee.type.params.length || callee.type.rest === true) && - (isJsSourceFile(expr.getSourceFile()) || + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || (callee.type.rest === true && callee.type.restAbi === undefined)) && !expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) && canBoxFuncIntoDyn( @@ -1978,7 +1978,7 @@ function lowerCheckedCallableValue( callee.type.kind !== "func" || !( (callee.type.rest === true && callee.type.restAbi === undefined) || - (isJsSourceFile(call.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(call)) && call.arguments.nodes.length > callee.type.params.length) ) || !lowerer.dynConvertible(callee.type) @@ -2543,7 +2543,7 @@ function lowerArrayPrototypeBorrowCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if ( - !isJsSourceFile(call.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(call)) || call.questionDotToken || access.questionDotToken || access.name.text !== "call" || @@ -2755,7 +2755,7 @@ function lowerObjectOwnCall( result = guardNullProto( receiverRef, type.shapeId, - `${methodAccess.getText()} is not a function`, + `${ts.getTextOfNode(methodAccess)} is not a function`, result, loc, ); @@ -3377,7 +3377,7 @@ function lowerDynReceiverMethodCall( const recvSymbol = recvTs.getSymbol(); if ( recvSymbol && - CHECKED_ITERATOR_TYPES.has(recvSymbol.name) && + CHECKED_ITERATOR_TYPES.has(ts.symbolName(recvSymbol)) && lowerer.isStdlibSymbol(recvSymbol) && ITERATOR_HELPER_METHODS.has(access.name.text) && lowerer.isStdlibMember(access) @@ -3445,7 +3445,7 @@ function lowerDynReceiverMethodCall( kind: "dynCall", callee: member, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -3467,7 +3467,7 @@ function lowerDynReceiverMethodCall( return null; if ( probed?.type.kind === "union" && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && lowerer.unions .get(probed.type.unionId) ?.arms.every( @@ -3517,7 +3517,7 @@ function lowerDynReceiverMethodCall( if ( checkerUntyped && recv.type.kind === "union" && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && isStringMethodName(access.name.text) && lowerer.unions .get(recv.type.unionId) @@ -3603,7 +3603,7 @@ function lowerDynReceiverMethodCall( kind: "dynInvoke", recv: value, method: "normalize", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: hasArgument ? [argument] : [], type: DYN, loc, @@ -3639,7 +3639,7 @@ function lowerDynReceiverMethodCall( // dynKeyGet cannot stand in for JavaScript's prototype lookup. Only // an authored declaration can justify the own-member path. const authored = prop.declarations.some( - (decl) => !lowerer.isStdlibFile(decl.getSourceFile()), + (decl) => !lowerer.isStdlibFile(ts.getSourceFileOfNode(decl)), ); return ( authored && @@ -3681,7 +3681,7 @@ function lowerDynReceiverMethodCall( lowerer, call, access.name.text, - access.getText(), + ts.getTextOfNode(access), recv, ); if (primitive) return primitive; @@ -3696,7 +3696,7 @@ function lowerDynReceiverMethodCall( args: [ recv, argument ? lowerer.lowerExprExpecting(argument, DYN) : dynUndefinedExpr(locOf(call)), - { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc: locOf(call) }, ], type: STRING, loc: locOf(call), @@ -3711,7 +3711,10 @@ function lowerDynReceiverMethodCall( return { kind: "libCall", fn: "dyn.valueOf", - args: [recv, { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }], + args: [ + recv, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc: locOf(call) }, + ], type: DYN, loc: locOf(call), }; @@ -3799,15 +3802,15 @@ function lowerDynReceiverMethodCall( // statement cannot leave the expression). const stringRefusal = (read: IrExpr): IrExpr => { if (!stringOnlyName) return read; - const sf = call.getSourceFile(); - const line = ts.getLineAndCharacterOfPosition(sf, call.getStart(sf)).line + 1; + const sf = ts.getSourceFileOfNode(call); + const line = ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(call)).line + 1; return { kind: "ternary", cond: { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc }, then: nodeThrowExpr( 0, "SC1090", - `method calls like '${access.getText()}' on a string are not supported yet [SC1090 at ${sf.fileName}:${line}]`, + `method calls like '${ts.getTextOfNode(access)}' on a string are not supported yet [SC1090 at ${sf.fileName}:${line}]`, DYN, loc, ), @@ -3843,7 +3846,7 @@ function lowerDynReceiverMethodCall( kind: "dynCall", callee, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc, @@ -4133,7 +4136,7 @@ export function lowerDynDispatchMethodCall( ): IrExpr | null { const method = access.name.text; if ( - (!DYN_DISPATCH_METHODS.has(method) && !isJsSourceFile(call.getSourceFile())) || + (!DYN_DISPATCH_METHODS.has(method) && !isJsSourceFile(ts.getSourceFileOfNode(call))) || lowerer.chainBlocked(call, access) ) return null; @@ -4242,7 +4245,7 @@ export function lowerDynDispatchMethodCall( kind: "dynInvoke", recv: { kind: "varRef", localId: saved.id, type: DYN, loc: locOf(access.expression) }, method, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [dynUndefinedExpr(locOf(call.arguments.nodes[0]!))], type: DYN, loc: locOf(call), @@ -4264,7 +4267,7 @@ export function lowerDynDispatchMethodCall( kind: "dynInvoke", recv, method, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc: locOf(call), @@ -4484,7 +4487,7 @@ function lowerNumberToStringCall( call.arguments.nodes[0] ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(locOf(call)), - { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc: locOf(call) }, ], type: STRING, loc: locOf(call), @@ -4607,7 +4610,7 @@ function lowerDefaultToStringCall( // A null-prototype record (Object.groupBy) has no toString: Node // throws " is not a function". const nullProtoT = nullProtoRecordShape(lowerer, recvT); - const missing = nullProtoT ? `${access.getText()} is not a function` : null; + const missing = nullProtoT ? `${ts.getTextOfNode(access)} is not a function` : null; if (!missing && pureObjectToStringReceiver(access.expression)) return constant; const recv = lowerer.lowerExpr(access.expression); const key = `objToStr:${typeKey(recv.type)}${missing ? `:${missing}` : ""}`; @@ -4758,7 +4761,7 @@ function lowerPrimitiveProtoCall( (name === "toFixed" || name === "toExponential") && call.arguments.nodes.length <= 1 && !call.arguments.nodes.some(ts.isSpreadElement) && - isJsSourceFile(call.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(call)) ) { const operand = lowerer.lowerExpr(recv); if ( @@ -4771,7 +4774,7 @@ function lowerPrimitiveProtoCall( lowerer, call, name, - `${recv.getText()}.${name}`, + `${ts.getTextOfNode(recv)}.${name}`, lowerer.coerceToExpected(operand, DYN), ); if (dispatched) return dispatched; @@ -4784,7 +4787,7 @@ function lowerPrimitiveProtoCall( kind: "dynInvoke", recv: lowerer.coerceToExpected(operand, DYN), method: name, - calleeName: `${recv.getText()}.${name}`, + calleeName: `${ts.getTextOfNode(recv)}.${name}`, args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc: locOf(call), @@ -4794,7 +4797,7 @@ function lowerPrimitiveProtoCall( name === "toPrecision" && call.arguments.nodes.length <= 1 && !call.arguments.nodes.some(ts.isSpreadElement) && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && recvKind === "union" ) { const operand = lowerer.lowerExpr(recv); @@ -4803,7 +4806,7 @@ function lowerPrimitiveProtoCall( lowerer, call, name, - `${recv.getText()}.${name}`, + `${ts.getTextOfNode(recv)}.${name}`, lowerer.coerceToExpected(operand, DYN), ); if (dispatched) return dispatched; @@ -5023,9 +5026,10 @@ function isSoleAbstractMethod(decl: ts.FunctionDeclaration | ts.MethodDeclaratio if (!ts.getModifiers(decl)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword)) return false; const owner = decl.parent; if (!ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) return false; - const name = decl.name.getText(); + const name = ts.getTextOfNode(decl.name); return !owner.members.nodes.some( - (member) => member !== decl && ts.isMethodDeclaration(member) && member.name.getText() === name, + (member) => + member !== decl && ts.isMethodDeclaration(member) && ts.getTextOfNode(member.name) === name, ); } @@ -5043,7 +5047,7 @@ function reconcileOverloadReturn( call.type.kind === "object" && rdecl && ts.isMethodDeclaration(rdecl) && - (!isJsSourceFile(rdecl.getSourceFile()) || returnsOnlyThis(rdecl)) + (!isJsSourceFile(ts.getSourceFileOfNode(rdecl)) || returnsOnlyThis(rdecl)) ) { call = lowerer.maybeNarrow(call, expr); } @@ -5205,7 +5209,7 @@ export function lowerTaggedTemplate(lowerer: Lowerer, expr: ts.TaggedTemplateExp let callee = lowerer.lowerExpr(expr.tag); if ( callee.type.kind === "func" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(callee.type) ) { callee = { kind: "dynFrom", value: callee, type: DYN, loc }; @@ -5223,7 +5227,7 @@ export function lowerTaggedTemplate(lowerer: Lowerer, expr: ts.TaggedTemplateExp ]; const calleeName = ts.isPropertyAccessExpression(expr.tag) || ts.isElementAccessExpression(expr.tag) - ? expr.tag.getText() + ? ts.getTextOfNode(expr.tag) : ts.isIdentifier(expr.tag) ? expr.tag.text : "value"; @@ -5293,7 +5297,7 @@ function loweredTemplateStrings( loc, }) : null; - const key = `templateStringsGlobal:${loc.file}:${expr.template.getStart()}:${raw ? "checked" : "typed"}`; + const key = `templateStringsGlobal:${loc.file}:${ts.tokenStartOfNode(expr.template)}:${raw ? "checked" : "typed"}`; let getter = lowerer.valueHelpers.get(key); if (!getter) { const site = lowerer.globalsList.length; @@ -6008,7 +6012,7 @@ export function lowerPromiseMethodCall( kind: "dynInvoke", recv: lowerer.coerceToExpected(receiver, DYN), method: "finally", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [lowerer.coerceToExpected(cb, DYN)], type: DYN, loc, @@ -6109,7 +6113,7 @@ export function lowerPromiseMethodCall( if ( member === "catch" && (inner.kind === "dyn" || - (isJsSourceFile(call.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(call)) && (inner.kind === "void" || lowerer.dynConvertible(inner)))) ) { let cb = lowerer.lowerExpr(call.arguments.nodes[0]!); @@ -7503,7 +7507,7 @@ function lowerObjectStaticCall( const prototype = lowerClassPrototypeAssign(lowerer, call); if (prototype) return prototype; if ( - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && call.arguments.nodes.length > 0 && !call.arguments.nodes.some(ts.isSpreadElement) ) { @@ -7775,7 +7779,12 @@ function lowerObjectStaticCall( args: [ pRef(), src, - { kind: "strLit", value: srcNode.getText(), type: STRING, loc: argLoc }, + { + kind: "strLit", + value: ts.getTextOfNode(srcNode), + type: STRING, + loc: argLoc, + }, ], type: VOID, loc: argLoc, @@ -7832,7 +7841,9 @@ function lowerObjectStaticCall( (exactClassOfReceiver(lowerer, targetNode.expression) !== null || storedClassValueType(lowerer, targetNode.expression)?.kind === "classval"); const declaredTarget = - target?.type.kind === "dyn" && !prototypeTarget && !isJsSourceFile(targetNode.getSourceFile()) + target?.type.kind === "dyn" && + !prototypeTarget && + !isJsSourceFile(ts.getSourceFileOfNode(targetNode)) ? lowerer.mapTypeOf(lowerer.typeOf(targetNode)) : null; if (target && declaredTarget && isDynTypedRefType(declaredTarget)) @@ -8031,7 +8042,7 @@ function lowerObjectStaticCall( while (ts.isParenthesizedExpression(inner) || ts.isAsExpression(inner)) inner = inner.expression; const value = - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && (ts.isObjectLiteralExpression(inner) || ts.isArrayLiteralExpression(inner)) ? lowerer.lowerExprExpecting(argNode, DYN) : lowerer.lowerExpr(argNode); @@ -8252,7 +8263,7 @@ function lowerObjectStaticCall( probed.type.kind === "bool"); const isJsUnion = probed?.type.kind === "union" && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && lowerer.dynConvertible(probed.type); if ( isDyn || @@ -8274,7 +8285,7 @@ function lowerObjectStaticCall( // write to the arrays well typed. JavaScript may store anything into // them, so it keeps the checked-dynamic arrays. const resultT = - member !== "values" && !isJsSourceFile(call.getSourceFile()) + member !== "values" && !isJsSourceFile(ts.getSourceFileOfNode(call)) ? lowerer.mapTypeOf(lowerer.typeOf(call)) : null; if (member === "keys" && resultT?.kind === "array" && resultT.elem.kind === "string") @@ -8297,7 +8308,11 @@ function lowerObjectStaticCall( // program only iterates or indexes the result, or passes it to a // read-only array method, a native vector of the checked values skips // building that array and, for for-of, the iterator protocol. - if (member === "values" && !isJsSourceFile(call.getSourceFile()) && consumesValueList(call)) + if ( + member === "values" && + !isJsSourceFile(ts.getSourceFileOfNode(call)) && + consumesValueList(call) + ) return { kind: "libCall", fn: "dyn.objValueList", @@ -8335,7 +8350,7 @@ function lowerObjectStaticCall( if ( (member === "values" || member === "entries") && (enumerationType?.kind === "dyn" || - (enumerationType === null && isJsSourceFile(call.getSourceFile()))) + (enumerationType === null && isJsSourceFile(ts.getSourceFileOfNode(call)))) ) { let receiver = lowerer.coerceToExpected(lowerer.lowerExpr(argNode), DYN); if (receiver.type.kind !== "dyn") lowerer.badType(argNode, lowerer.typeOf(argNode)); @@ -8866,7 +8881,7 @@ export function lowerRecordFieldCall( const target = lowerer.fieldTarget(access); const loc = locOf(call); const receiverLocal = - target && isJsSourceFile(call.getSourceFile()) + target && isJsSourceFile(ts.getSourceFileOfNode(call)) ? lowerer.declareHiddenLocal("%callReceiver", target.obj.type) : null; const init: IrStmt | null = @@ -8886,7 +8901,7 @@ export function lowerRecordFieldCall( // values (util.DebugLogger). if ( callee.type.kind === "dyn" && - (isJsSourceFile(call.getSourceFile()) || + (isJsSourceFile(ts.getSourceFileOfNode(call)) || lowerer.checker.getCallSignatures(lowerer.typeOf(access)).length > 0) ) { const receiver = @@ -8904,7 +8919,7 @@ export function lowerRecordFieldCall( kind: "dynCall", callee, ...(receiver ? { receiver } : {}), - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -8975,7 +8990,7 @@ function lowerUrlStaticCall( if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined) return null; if (!ts.isIdentifier(callee.expression)) return null; const sym = lowerer.resolveValueSymbol(callee.expression); - if (!sym || !lowerer.isStdlibSymbol(sym) || sym.name !== "URL") return null; + if (!sym || !lowerer.isStdlibSymbol(sym) || ts.symbolName(sym) !== "URL") return null; if (callee.name.text === "canParse" || callee.name.text === "parse") { return lowerUrlFactory(lowerer, call, callee.name.text); } @@ -9333,7 +9348,7 @@ function lowerObjLitGenericMethodCall( kind: "dynInvoke", recv: { kind: "dynFrom", value, type: DYN, loc }, method: name, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -9342,7 +9357,7 @@ function lowerObjLitGenericMethodCall( lowerer.unsupported( "SC1090", call, - `Function.prototype.${name} on a function whose signature cannot cross the checked-value boundary (call '${access.expression.getText()}(...)' directly)`, + `Function.prototype.${name} on a function whose signature cannot cross the checked-value boundary (call '${ts.getTextOfNode(access.expression)}(...)' directly)`, ); } // STANDARD-LIBRARY generic members (Promise.then, Object. @@ -9370,7 +9385,7 @@ function lowerObjLitGenericMethodCall( kind: "dynInvoke", recv: receiver, method: name, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -9431,7 +9446,7 @@ export function lowerObjectMethodCall( (lowerer.implicitParamTypes !== null || lowerer.classImplementedProtocol(lowerer.typeOf(access.expression)) || ts.isNewExpression(access.expression) || - isJsSourceFile(call.getSourceFile())) + isJsSourceFile(ts.getSourceFileOfNode(call))) ? ts.isNewExpression(access.expression) ? lowerer.lowerExpr(access.expression) : tryLowerExpression(lowerer, access.expression) @@ -9450,7 +9465,7 @@ export function lowerObjectMethodCall( if (dispatched) return dispatched; const arms = lowerer.unions.get(mappedReceiver.unionId)?.arms; if ( - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && arms?.every( (arm) => arm.kind === "object" && !lowerer.classes.get(arm.className)?.def.runtime, ) && @@ -9604,7 +9619,7 @@ export function lowerObjectMethodCall( callee, lowerer.armTag(callee.type.unionId, callableType), callableType, - () => `${access.getText()} is not a function`, + () => `${ts.getTextOfNode(access)} is not a function`, ); const helper = inPlace ? null @@ -9618,7 +9633,7 @@ export function lowerObjectMethodCall( if ( ((callee.type.rest === true && callee.type.restAbi === undefined) || call.arguments.nodes.length > callee.type.params.length) && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && lowerer.dynConvertible(callee.type) ) { const boxed: IrExpr = { kind: "dynFrom", value: callee, type: DYN, loc }; @@ -9629,7 +9644,7 @@ export function lowerObjectMethodCall( kind: "dynCall", callee: boxed, ...receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc, @@ -9647,7 +9662,7 @@ export function lowerObjectMethodCall( kind: "dynCall", callee, ...receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc, @@ -9656,7 +9671,7 @@ export function lowerObjectMethodCall( } if ( !fieldType && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && !info.def.runtime && !info.builtinError && !info.builtinEmitter && @@ -9686,7 +9701,7 @@ export function lowerObjectMethodCall( kind: "dynCall", callee, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -9735,7 +9750,7 @@ export function lowerObjectMethodCall( call.arguments.nodes, fallback, loc, - access.getText(), + ts.getTextOfNode(access), ); return { kind: "seqExpr", @@ -9771,7 +9786,7 @@ export function lowerObjectMethodCall( const declarerDecl = found.declarer.decl; if ( adapted && - !(declarerDecl && isJsSourceFile(declarerDecl.getSourceFile())) && + !(declarerDecl && isJsSourceFile(ts.getSourceFileOfNode(declarerDecl))) && !lowerer.overrideBelow(info, method) ) { lowerer.noteEdge(`%${found.declarer.def.name}.${method}`); @@ -9861,7 +9876,7 @@ function lowerUnionObjectMethodCall( kind: "dynCall", callee, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -10023,7 +10038,7 @@ function lowerUnionObjectDynFieldCall( kind: "dynCall", callee, receiver: lowerer.coerceToExpected(receiverRef, DYN), - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc, @@ -10405,7 +10420,7 @@ function maybePromiseHandlerResult( function lowerObjectAssignOntoError(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { const argNodes = call.arguments.nodes; if ( - isJsSourceFile(call.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(call)) || argNodes.length < 2 || argNodes.some((a) => ts.isSpreadElement(a)) ) diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 99d854a6a..49e7ad9fc 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -183,9 +183,9 @@ export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { while (value && ts.isParenthesizedExpression(value)) value = value.expression; if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false; } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - body.forEachChild(visit); + ts.forEachChild(body, visit); return valid; } @@ -216,9 +216,9 @@ export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean { } if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false; } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - body.forEachChild(visit); + ts.forEachChild(body, visit); return valid; } @@ -341,7 +341,7 @@ export function storedClassValueType( } if ( (ts.isArrowFunction(callee) || ts.isFunctionExpression(callee)) && - isJsSourceFile(callee.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(callee)) && !callee.typeParameters?.nodes.length && !lowerer.preparingClassFactories.has(callee) ) { @@ -410,7 +410,7 @@ export function storedClassValueType( } if ( !factory || - !isJsSourceFile(factory.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(factory)) || factory.typeParameters?.nodes.length || !ts.isVariableDeclaration(factory.parent) || factory.parent.initializer !== factory || @@ -808,7 +808,7 @@ function symbolSlotReturnType( ): IrType | null { if (symbolFields.size === 0) return null; if (fnLike.type !== undefined || !fnLike.body) return null; - if (!isJsSourceFile(fnLike.getSourceFile())) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(fnLike))) return null; if (fnLike.asteriskToken !== undefined) return null; if (fnLike.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; const stmts = fnLike.body.statements.nodes; @@ -818,9 +818,9 @@ function symbolSlotReturnType( const visit = (n: ts.Node): void => { if (ts.isFunctionLike(n)) return; if (ts.isReturnStatement(n)) returns.push(n); - n.forEachChild(visit); + ts.forEachChild(n, visit); }; - fnLike.body.forEachChild(visit); + ts.forEachChild(fnLike.body, visit); let out: IrType | null = null; for (const r of returns) { let e = r.expression; @@ -1055,7 +1055,7 @@ export function builtinStreamInfoOf( } let irName: string | null = null; for (const [name, rec] of RUNTIME_STREAM_CLASSES) { - if (rec.lib === symbol.name) irName = name; + if (rec.lib === ts.symbolName(symbol)) irName = name; } if (!irName) return null; // Both the bundled declarations and @types/node declare the classes @@ -1096,7 +1096,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E // JavaScript documentation cannot guarantee that a call or an indexed // read returns a present number. Numeric fields receiving those values // must retain undefined, so later arithmetic performs JS coercion (NaN). - if (type.kind === "f64" && isJsSourceFile(expression.getSourceFile())) { + if (type.kind === "f64" && isJsSourceFile(ts.getSourceFileOfNode(expression))) { const assignment = expression.parent; const left = ts.isBinaryExpression(assignment) ? assignment.left : null; const field = @@ -1128,7 +1128,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E if (ts.isCallExpression(rhs) || ts.isElementAccessExpression(rhs)) acceptsUnchecked = true; } - node.forEachChild(scan); + ts.forEachChild(node, scan); }; scan(owner); if (acceptsUnchecked) return DYN; @@ -1165,7 +1165,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E owner = owner.parent; if (owner && (ts.isClassDeclaration(owner) || ts.isClassExpression(owner)) && owner.name) { const className = owner.name.text; - for (const statement of owner.getSourceFile().statements.nodes) { + for (const statement of ts.getSourceFileOfNode(owner).statements.nodes) { if ( !ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression) @@ -1268,7 +1268,7 @@ function returnsWidenedJsField( ret: IrType, ): boolean { if ( - !isJsSourceFile(member.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(member)) || member.type || member.asteriskToken || hasExplicitJsDocReturn(member) @@ -1331,7 +1331,7 @@ function returnsWidenedJsField( const scan = (node: ts.Node): void => { if (result || ts.isFunctionLike(node)) return; if (ts.isReturnStatement(node) && node.expression && widened(node.expression)) result = true; - node.forEachChild(scan); + ts.forEachChild(node, scan); }; if (member.body) scan(member.body); return result; @@ -1410,7 +1410,7 @@ export function builtinEmitterInfoOf( } return null; } - if (symbol.name !== "EventEmitter") return null; + if (ts.symbolName(symbol) !== "EventEmitter") return null; const declared = symbol.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; let node: ts.Node | undefined = d.parent; @@ -1440,7 +1440,7 @@ export function builtinErrorInfoOf( if (alias !== null) return alias.kind === "error" ? alias.info : null; if (!lowerer.isStdlibSymbol(symbol)) return null; for (const [irName, rec] of RUNTIME_ERROR_CLASSES) { - if (rec.lib === symbol.name) return lowerer.classes.get(irName) ?? null; + if (rec.lib === ts.symbolName(symbol)) return lowerer.classes.get(irName) ?? null; } return null; } @@ -1585,9 +1585,9 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration) if ( baseDeclaration && !lowerer.isStdlibSymbol(baseSymbol ?? undefined) && - !baseDeclaration.getSourceFile().isDeclarationFile && - (baseDeclaration.getSourceFile() !== decl.getSourceFile() || - baseDeclaration.getStart() < decl.getStart()) + !ts.getSourceFileOfNode(baseDeclaration).isDeclarationFile && + (ts.getSourceFileOfNode(baseDeclaration) !== ts.getSourceFileOfNode(decl) || + ts.tokenStartOfNode(baseDeclaration) < ts.tokenStartOfNode(decl)) ) lowerer.collectClassShape(baseDeclaration); lowerer.collectClassShapeInner(decl); @@ -1792,7 +1792,7 @@ export function collectClassShapeInner( // source (V8 has not shipped the proposal; the type-stripping // loaders leave `@dec` in place) — there is no runtime behavior // to be exact against. - if (isJsSourceFile(decl.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) { lowerer.unsupported( "SC1090", (classDecoratorNodes[0] ?? decoratorNodesOf(decoratedMembers[0]!)[0])!, @@ -1930,7 +1930,7 @@ export function collectClassShapeInner( const source = signatures.length === 1 ? signatures[0]!.declaration : undefined; if ( !source || - !isJsSourceFile(source.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(source)) || !(ts.isFunctionDeclaration(source) || ts.isFunctionExpression(source)) ) return false; @@ -2046,13 +2046,13 @@ export function collectClassShapeInner( const baseDecl = propBase.decl; if ( baseDecl != null && - baseDecl.getSourceFile() === decl.getSourceFile() && - baseDecl.getStart() > decl.getStart() + ts.getSourceFileOfNode(baseDecl) === ts.getSourceFileOfNode(decl) && + ts.tokenStartOfNode(baseDecl) > ts.tokenStartOfNode(decl) ) { lowerer.unsupported( "SC1090", t, - `extending '${t.expression.getText()}' above the statement that assigns it (the property is still undefined when this class evaluates — Node throws here; assign the base first)`, + `extending '${ts.getTextOfNode(t.expression)}' above the statement that assigns it (the property is still undefined when this class evaluates — Node throws here; assign the base first)`, ); } base = propBase; @@ -2070,7 +2070,7 @@ export function collectClassShapeInner( lowerer.unsupported( "SC1090", t, - `extending the reassigned property '${t.expression.getText()}' (the runtime base is whichever assignment ran last — bind the class exactly once)`, + `extending the reassigned property '${ts.getTextOfNode(t.expression)}' (the runtime base is whichever assignment ran last — bind the class exactly once)`, ); } } @@ -2289,7 +2289,7 @@ export function collectClassShapeInner( // Public JS accessors need live constructor descriptors, including // receiver-sensitive getters and setters inherited by subclasses. const checkedStatics = - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && ((base && hasRuntimeStatics(base)) || decl.members.nodes.some( (member) => @@ -2363,7 +2363,7 @@ export function collectClassShapeInner( for (const member of decl.members.nodes) { if ( (functionLocalClass(decl) || checkedStatics) && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && (ts.isClassStaticBlockDeclaration(member) || ts .getModifiers(member) @@ -2374,7 +2374,7 @@ export function collectClassShapeInner( } if ( functionLocalClass(decl) && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && member.body && !ts.isPrivateIdentifier(member.name) @@ -2446,7 +2446,7 @@ export function collectClassShapeInner( if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name)); if ( type.kind === "dyn" && - !isJsSourceFile(member.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && !lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)) && (member.type !== undefined || (lowerer.typeOf(member.name).flags & ts.TypeFlags.Any) === 0) @@ -2480,7 +2480,7 @@ export function collectClassShapeInner( } staticMethods.set(member.name.text, { params: shapes, ret: ft.ret, member }); if ( - implicitMonoFile(decl.getSourceFile()) && + implicitMonoFile(ts.getSourceFileOfNode(decl)) && !functionLocalClass(decl) && inst === undefined && decl.typeParameters === undefined @@ -2537,7 +2537,7 @@ export function collectClassShapeInner( // read of an Error-rooted class is the checked-dynamic property // (fieldTarget's errorCause container), so only types that read // back unchanged from it are accepted. - if (!isJsSourceFile(member.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(member))) { if ( modifiers?.some( (m) => @@ -2756,10 +2756,12 @@ export function collectClassShapeInner( // unconditional `this[key] = ...` at the constructor's top // level), checked with the unguarded fields below. const declaredOnly = - !member.initializer && !isJsSourceFile(member.getSourceFile()) && !!member.type; + !member.initializer && !isJsSourceFile(ts.getSourceFileOfNode(member)) && !!member.type; if ( fieldName === null || - (!member.initializer && !isJsSourceFile(member.getSourceFile()) && !declaredOnly) + (!member.initializer && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && + !declaredOnly) ) { lowerer.unsupported( "SC1090", @@ -2777,7 +2779,9 @@ export function collectClassShapeInner( ? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred)) : DYN; let type = - mapped?.kind === "record" && isJsSourceFile(member.getSourceFile()) && !member.type + mapped?.kind === "record" && + isJsSourceFile(ts.getSourceFileOfNode(member)) && + !member.type ? DYN : mapped ? jsOpenObjectType(member, mapped, lowerer.shapes, lowerer.unions) @@ -2786,11 +2790,11 @@ export function collectClassShapeInner( !type || type.kind === "void" || (type.kind === "dyn" && - !isJsSourceFile(member.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && !lowerer.classFieldStoresDyn(inferred)) ) lowerer.badType(member, inferred); - if (isJsSourceFile(member.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(member))) { type = inheritedJsFieldType(lowerer, decl, fieldName, type); if (base?.fields.get(fieldName)?.kind === "dyn") type = DYN; if (base && !fields.has(fieldName) && lowerer.dynConvertible(type)) type = DYN; @@ -2904,7 +2908,7 @@ export function collectClassShapeInner( // as undefined on construction, verified), and reads/writes ride // the ordinary undefined-armed union machinery. let type = lowerer.irTypeOf(member.name); - if (isJsSourceFile(member.getSourceFile()) && !member.initializer) { + if (isJsSourceFile(ts.getSourceFileOfNode(member)) && !member.initializer) { const constructor = decl.members.nodes.find( (part): part is ts.ConstructorDeclaration => ts.isConstructorDeclaration(part) && !!part.body, @@ -2932,19 +2936,25 @@ export function collectClassShapeInner( // Unannotated JavaScript callback slots retain the original // callable and its receiver instead of adapting its signature. if ( - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && !member.type && - !/@type\b/.test(member.getSourceFile().text.slice(member.pos, member.getStart())) && + !/@type\b/.test( + ts.getSourceFileOfNode(member).text.slice(member.pos, ts.tokenStartOfNode(member)), + ) && (type.kind === "func" || type.kind === "record") && lowerer.dynConvertible(type) ) type = DYN; // JS permits reads before a constructor's first assignment even // when its inferred field type omits undefined. - if (isJsSourceFile(member.getSourceFile()) && !member.initializer && type.kind !== "dyn") { + if ( + isJsSourceFile(ts.getSourceFileOfNode(member)) && + !member.initializer && + type.kind !== "dyn" + ) { type = lowerer.withUndefinedArmOf(type) ?? DYN; } - if (isJsSourceFile(member.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(member))) { type = inheritedJsFieldType(lowerer, decl, member.name.text, type); if (base?.fields.get(member.name.text)?.kind === "dyn") type = DYN; if (base && !fields.has(member.name.text) && lowerer.dynConvertible(type)) type = DYN; @@ -2955,7 +2965,7 @@ export function collectClassShapeInner( // storage as implicit constructor-assigned JS fields. if ( type.kind === "dyn" && - !isJsSourceFile(member.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && !lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)) ) { lowerer.unsupported( @@ -3227,7 +3237,7 @@ export function collectClassShapeInner( // name (the override stays on the vtable), a shadowed field, or // a generic-class instantiation's member. if ( - implicitMonoFile(decl.getSourceFile()) && + implicitMonoFile(ts.getSourceFileOfNode(decl)) && !functionLocalClass(decl) && ts.isIdentifier(member.name) && !lowerer.virtualJsMethods.has(member) && @@ -3268,14 +3278,14 @@ export function collectClassShapeInner( // JS inference may retain only a symbol sentinel while a method // also returns values from an untyped mutable field or local. if ( - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && !member.type && !hasExplicitJsDocReturn(member) && ft.ret.kind === "symbol" ) ft.ret = DYN; if ( - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && lowerer.prototypeMethodAccesses.has(mName) && !lowerer.findMethodOn(base, mName)?.declarer.def.runtime && shapes.every((p) => p.mode === "required" || p.mode === "omittable") @@ -3329,7 +3339,7 @@ export function collectClassShapeInner( // against the STATIC receiver's shape, so `m(x?: number)` and // `m(x: number | undefined)` interchange soundly in overrides. const overridden = lowerer.findMethodOn(base, mName); - if (overridden && isJsSourceFile(member.getSourceFile())) { + if (overridden && isJsSourceFile(ts.getSourceFileOfNode(member))) { // An unannotated JS override can return a checked value into // the inherited native ABI, just as its untyped parameters do. // Body lowering inserts the conversion at each return. @@ -3377,7 +3387,7 @@ export function collectClassShapeInner( // readers keep the exact-signature fence. if ( overridden && - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && shapes.length < overridden.sig.params.length && !bodyReadsArguments(member) && shapes.every( @@ -3405,9 +3415,9 @@ export function collectClassShapeInner( // a statically typed receiver see the same ABI. if ( overridden && - !isJsSourceFile(member.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && overridden.declarer.decl !== null && - isJsSourceFile(overridden.declarer.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(overridden.declarer.decl)) && member.asteriskToken === undefined && !overridden.sig.async && !overridden.sig.gen && @@ -3458,9 +3468,9 @@ export function collectClassShapeInner( )) ) { const fencedJsOverride = - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && overridden.declarer.decl !== null && - isJsSourceFile(overridden.declarer.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(overridden.declarer.decl)) && overridden.sig.gen === undefined; // A TypeScript override that only changes the result, into a // type that converts into the inherited one (`Promise` over @@ -3510,7 +3520,7 @@ export function collectClassShapeInner( // into the inherited type (`toString() { return // this.message }` over Error's). (ft.ret.kind === "dyn" && - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && overridden.sig.ret.kind !== "void" && canDynCheckTo( overridden.sig.ret, @@ -3584,7 +3594,7 @@ export function collectClassShapeInner( overridden && !returnAdapters.has(mName) && (asyncMember || overridden.sig.async === true) && - !isJsSourceFile(member.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && !functionLocalClass(decl) && member.asteriskToken === undefined && overridden.sig.gen === undefined && @@ -3606,7 +3616,7 @@ export function collectClassShapeInner( (asyncMember || overridden.sig.async === true) ) { const message = `overriding ${overridden.sig.async === true ? "the async method" : "a method with an async method"} '${mName}' (async methods dispatch statically — the vtable slot machinery has no fiber-spawn story)`; - if (isJsSourceFile(member.getSourceFile())) + if (isJsSourceFile(ts.getSourceFileOfNode(member))) methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message)); else lowerer.unsupported("SC1090", member.name, message); } @@ -3615,7 +3625,7 @@ export function collectClassShapeInner( (member.asteriskToken !== undefined || overridden.sig.gen !== undefined) ) { const message = `overriding the generator method '${mName}' (generator methods dispatch statically — a virtual slot cannot enter their spawn wrapper)`; - if (isJsSourceFile(member.getSourceFile())) + if (isJsSourceFile(ts.getSourceFileOfNode(member))) methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message)); else lowerer.unsupported("SC1090", member.name, message); } @@ -3687,7 +3697,7 @@ export function collectClassShapeInner( const mName = `${isGet ? "get" : "set"}:${prop}`; if ( fields.has(prop) && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && base?.fields.has(prop) && !fieldOrder.some((field) => field.name === prop) ) { @@ -4002,7 +4012,7 @@ export function collectClassShapeInner( // assignment ran — the zeroed-memory trap a TS declaration order // forbids via strictPropertyInitialization — so they keep a named // fence instead of reading undefined without a diagnostic. - if (isJsSourceFile(decl.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) { const hasAccessor = (name: string): boolean => methods.has(`get:${name}`) || methods.has(`set:${name}`) || @@ -4026,7 +4036,7 @@ export function collectClassShapeInner( // goes through the key. const lateBoundByKey = new Map(); for (const p of instType ? lowerer.checker.getPropertiesOfType(instType) : []) { - if (!p.name.startsWith("__@")) continue; + if (!ts.symbolName(p).startsWith("__@")) continue; const keySym = lateBoundKeySymOf(lowerer, p); if (keySym) lateBoundByKey.set(keySym, p); } @@ -4082,7 +4092,7 @@ export function collectClassShapeInner( if ( t && (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 && - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) type = DYN; if (type) type = jsOpenObjectType(assign, type, lowerer.shapes, lowerer.unions); @@ -4200,10 +4210,14 @@ export function collectClassShapeInner( // static layouts cannot represent. Named fence, at the first // assignment site. for (const p of instType ? lowerer.checker.getPropertiesOfType(instType) : []) { - if (errorRooted && p.name === "cause") continue; - if (fields.has(p.name) || methods.has(p.name)) continue; - if (hasAccessor(p.name)) continue; - if (base && (base.fields.has(p.name) || lowerer.findMethodOn(base, p.name))) continue; + if (errorRooted && ts.symbolName(p) === "cause") continue; + if (fields.has(ts.symbolName(p)) || methods.has(ts.symbolName(p))) continue; + if (hasAccessor(ts.symbolName(p))) continue; + if ( + base && + (base.fields.has(ts.symbolName(p)) || lowerer.findMethodOn(base, ts.symbolName(p))) + ) + continue; const site = p.declarations.find( (d) => ts.isPropertyAccessExpression(d) || @@ -4217,7 +4231,7 @@ export function collectClassShapeInner( // identities (symbol parameters, mutable bindings, computed // descriptions) or assignments outside the constructor's top // level. - if (p.name.startsWith("__@")) { + if (ts.symbolName(p).startsWith("__@")) { const keySym = lateBoundKeySymOf(lowerer, p); const key = keySym ? classSymbolKeyOfSymbol(lowerer, keySym) : null; if (key && symbolFields.has(key.identity)) continue; @@ -4237,24 +4251,26 @@ export function collectClassShapeInner( // Unmappable inferences and arm-less kinds keep the fence. // TypeScript classes keep the compile-time fence too: an annotated // program can spell `T | undefined` itself. - if (isJsSourceFile(decl.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) { const original = undefArmedFieldType(lowerer, p); const armed = - original === null ? null : inheritedJsFieldType(lowerer, decl, p.name, original); + original === null + ? null + : inheritedJsFieldType(lowerer, decl, ts.symbolName(p), original); if (armed !== null) { // Unknown JS properties use the instance bag: they do not // exist until assigned, and a subclass may declare the same // name as an ordinary layout field. if (armed.kind === "dyn") continue; - fields.set(p.name, armed); - fieldOrder.push({ name: p.name, type: armed, initializer: undefined }); + fields.set(ts.symbolName(p), armed); + fieldOrder.push({ name: ts.symbolName(p), type: armed, initializer: undefined }); continue; } } lowerer.unsupported( "SC1090", site, - `fields assigned outside the constructor's top level ('this.${p.name}' would be undefined until the first assignment runs — assign it unconditionally at the top of the constructor)`, + `fields assigned outside the constructor's top level ('this.${ts.symbolName(p)}' would be undefined until the first assignment runs — assign it unconditionally at the top of the constructor)`, ); } } @@ -4297,7 +4313,7 @@ export function collectClassShapeInner( if ( ctor && !mixinForwarding && - isJsSourceFile(ctor.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(ctor)) && !ctorParams.some((shape) => shape.mode === "rest" || shape.mode === "dynRest") && bodyReadsArguments(ctor) ) { @@ -4692,7 +4708,7 @@ export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt // Keep their refusal attached to the class declaration instead of // letting a poisoned initializer escape the module lowering pass. if (!(error instanceof PoisonError)) throw error; - if (info.decl && isJsSourceFile(info.decl.getSourceFile())) { + if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) { const fence = lowerer.diagSink === null ? lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" }) @@ -4819,8 +4835,16 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] | { pos: number; kind: "field"; f: ClassInfo["staticFields"][number] } | { pos: number; kind: "block"; b: ts.ClassStaticBlockDeclaration }; const items: Item[] = [ - ...info.staticFields.map((f): Item => ({ pos: f.initializer.getStart(), kind: "field", f })), - ...(info.staticBlocks ?? []).map((b): Item => ({ pos: b.getStart(), kind: "block", b })), + ...info.staticFields.map((f): Item => ({ + pos: ts.tokenStartOfNode(f.initializer), + kind: "field", + f, + })), + ...(info.staticBlocks ?? []).map((b): Item => ({ + pos: ts.tokenStartOfNode(b), + kind: "block", + b, + })), ].sort((a, b) => a.pos - b.pos); for (const item of items) { if (item.kind === "block") { @@ -4867,7 +4891,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] } catch (e) { if (!(e instanceof PoisonError)) throw e; lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file); - if (isJsSourceFile(f.initializer.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(f.initializer))) { const fence = lowerer.diagSink === null ? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" }) @@ -4926,7 +4950,7 @@ export function analyzeClassDecoration(lowerer: Lowerer, info: ClassInfo): void ts.isVariableDeclaration(vdecl) && vdecl.initializer === undefined && (ts.getCombinedModifierFlags(vdecl) & ts.ModifierFlags.Ambient) !== 0 && - !vdecl.getSourceFile().isDeclarationFile; + !ts.getSourceFileOfNode(vdecl).isDeclarationFile; if (ambientVar || ambientUndefinedFnSymbolOf(lowerer, ambientCallee) !== null) { shapes.push({ kind: "ambientThrow", name: ambientCallee.text }); continue; @@ -5549,7 +5573,7 @@ export function lowerStaticFieldRead( // JavaScript often assigns public statics after the class declaration. // Their storage stays mutable and checked, while reads retain the // checker's concrete type instead of losing the method receiver. - const type = isJsSourceFile(expr.getSourceFile()) + const type = isJsSourceFile(ts.getSourceFileOfNode(expr)) ? null : lowerer.mapTypeOf(lowerer.typeOf(expr)); return type ? lowerer.coerceToExpected(value, type) : value; @@ -5590,7 +5614,7 @@ function namedEvaluationName(expr: ts.ClassLikeDeclaration): string { * (private-brands.ts), so the class must be a named declaration (undici's * webidl FastIterableIterator). The checker rejects writes to that binding. */ function localPrivateFieldsAgree(expr: ts.ClassLikeDeclaration): boolean { - if (!isJsSourceFile(expr.getSourceFile()) || !ts.isClassDeclaration(expr) || !expr.name) + if (!isJsSourceFile(ts.getSourceFileOfNode(expr)) || !ts.isClassDeclaration(expr) || !expr.name) return false; if ( expr.members.nodes.some( @@ -5698,7 +5722,7 @@ export function lowerClassExpressionInfo( expr.members.nodes.some( (member) => decoratorNodesOf(member).length || - (!isJsSourceFile(expr.getSourceFile()) && + (!isJsSourceFile(ts.getSourceFileOfNode(expr)) && (ts.isClassStaticBlockDeclaration(member) || ts .getModifiers(member) @@ -5822,7 +5846,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara decoratorNodesOf(expr).length > 0 || expr.members.nodes.some((m) => decoratorNodesOf(m).length > 0) ) { - if (!isJsSourceFile(expr.getSourceFile()) && expr.typeParameters === undefined) { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.typeParameters === undefined) { const thrown = guaranteedDecorationThrow(lowerer, expr); if (thrown) { // The expression's static type never materializes — the read @@ -6108,12 +6132,12 @@ function enclosingTopLevelClassAssignment(expr: ts.ClassExpression): ts.BinaryEx * `Common.I`): a second assignment makes the binding dynamic, so * callers refuse to pin the first one's class. */ function countAssignmentsTo(assign: ts.BinaryExpression): number { - const sf = assign.getSourceFile(); + const sf = ts.getSourceFileOfNode(assign); // `exports.I` and `module.exports.I` are the SAME binding in Node // (exports aliases module.exports until a table replaces it) — fold // the member spellings together before comparing. const canon = (lhs: ts.Expression): string => { - const text = lhs.getText().replace(/\s+/g, ""); + const text = ts.getTextOfNode(lhs).replace(/\s+/g, ""); return text.startsWith("module.exports.") ? text.slice("module.".length) : text; }; const target = canon(assign.left); @@ -6171,10 +6195,10 @@ export function lowerStaticMethodCall( if (!ts.isIdentifier(access.expression)) { if ( !isModuleExportsAccess(access.expression) || - !isCjsJsFile(access.getSourceFile(), lowerer.program) + !isCjsJsFile(ts.getSourceFileOfNode(access), lowerer.program) ) { let receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); - if (receiverType?.kind !== "classval" && isJsSourceFile(call.getSourceFile())) { + if (receiverType?.kind !== "classval" && isJsSourceFile(ts.getSourceFileOfNode(call))) { const receiver = tryLowerExpression(lowerer, access.expression); if (receiver?.type.kind === "classval") receiverType = receiver.type; } @@ -6195,7 +6219,7 @@ export function lowerStaticMethodCall( loc: locOf(call), }; } - const whole = cjsClassExprWholeExportOf(access.getSourceFile()); + const whole = cjsClassExprWholeExportOf(ts.getSourceFileOfNode(access)); if (!whole) return null; return staticCallOn( lowerer, @@ -6492,9 +6516,9 @@ function hasLexicalSuper(root: ts.Node): boolean { ts.isClassExpression(node) ) return; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - root.forEachChild(visit); + ts.forEachChild(root, visit); return found; } @@ -6523,9 +6547,9 @@ function capturesEnclosingScope( if (decl && decl !== scope && inside(decl, scope) && !inside(decl, member)) captured = true; return; } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - member.forEachChild(visit); + ts.forEachChild(member, visit); return captured; } @@ -6648,7 +6672,10 @@ export function lowerClassValueProperty( // `module.exports.label` in a class-replaced CJS module: the // receiver is the exact exported class, and the read is // side-effect-free — as bindable as an identifier. - !(isModuleExportsAccess(expr.expression) && isCjsJsFile(expr.getSourceFile(), lowerer.program)) + !( + isModuleExportsAccess(expr.expression) && + isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program) + ) ) { // Devirtualized reads DISCARD the receiver value, so only // side-effect-free receivers are claimed (the instanceOf fold @@ -6912,7 +6939,7 @@ export function collectInheritedJsFieldWrites( if (name !== null) addWrite(name, part.right); } } - part.forEachChild(scan); + ts.forEachChild(part, scan); }; for (const member of node.members.nodes) { if ( @@ -6953,7 +6980,7 @@ export function collectInheritedJsFieldWrites( if ( !owner || (!ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) || - !isJsSourceFile(owner.getSourceFile()) + !isJsSourceFile(ts.getSourceFileOfNode(owner)) ) continue; let fields = lowerer.inheritedJsFieldWrites.get(owner); @@ -6991,7 +7018,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou if ( node.heritageClauses?.nodes.length && accessors.length && - isJsSourceFile(node.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(node)) ) { const symbol = lowerer.typeOf(node).getSymbol(); const instance = symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null; @@ -7008,7 +7035,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou if ( (ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration)) && - isJsSourceFile(declaration.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(declaration)) ) { lowerer.checkedVirtualJsAccessors.add(declaration); lowerer.checkedVirtualJsAccessors.add(member); @@ -7050,12 +7077,12 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou for (const declaration of property ? property.declarations : []) { if ( ts.isMethodDeclaration(declaration) && - isJsSourceFile(declaration.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(declaration)) && !checkedJsOverride(lowerer, declaration, member) ) { lowerer.virtualJsMethods.add(declaration); if ( - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && !member.typeParameters?.nodes && !member.asteriskToken && !member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) && @@ -7101,7 +7128,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou if (node !== member && ts.isFunctionLike(node)) return; if (ts.isReturnStatement(node) && node.expression) lowerer.checkedVirtualJsValueReturns.add(member); - node.forEachChild(scan); + ts.forEachChild(node, scan); }; scan(member); } @@ -7134,7 +7161,7 @@ function checkedJsOverride( ): boolean { return [base, member].every( (method) => - lowerer.checkedJsFile(method.getSourceFile()) && + lowerer.checkedJsFile(ts.getSourceFileOfNode(method)) && method.parameters.nodes.every(documentedParameter), ); } @@ -7282,7 +7309,7 @@ export function lowerClassGenericMethodCall( result.type.kind === "object" && !ts.isExpressionStatement(call.parent) && ts.isMethodDeclaration(info.decl) && - isJsSourceFile(info.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && returnsOnlyThisValue(info.decl) ) { return lowerer.maybeNarrow(result, call); @@ -7387,7 +7414,7 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction out.push(lowerer.lowerClassCtor(info)); } catch (e) { if (!(e instanceof PoisonError)) throw e; - if (info.decl && isJsSourceFile(info.decl.getSourceFile())) { + if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) { const types: IrType[] = [ { kind: "object", className }, ...info.ctorParams.map((param) => param.type), @@ -8300,7 +8327,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL } catch (e) { if (!(e instanceof PoisonError)) throw e; lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file); - if (isJsSourceFile(f.initializer.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(f.initializer))) { const fence = lowerer.diagSink === null ? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" }) @@ -8799,7 +8826,7 @@ export function lowerSuperMethodCall( kind: "dynCall", callee, receiver: lowerer.coerceToExpected(thisRef, DYN), - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -8974,7 +9001,7 @@ export function inheritedBuiltinCtorFence( lowerer.runtimeFences.push(diag); const line = info.decl !== null - ? ts.getLineAndCharacterOfPosition(info.decl.getSourceFile(), at.start).line + 1 + ? ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(info.decl), at.start).line + 1 : null; const where = line !== null ? ` [${diag.code} at ${at.file}:${line}]` : ""; return { kind: "runtimeFence", code: diag.code, message: `${diag.message}${where}`, loc }; @@ -9422,7 +9449,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // lowering ahead of the assignment statement). if ( !expr.expression.questionDotToken && - ((isCjsJsFile(expr.getSourceFile(), lowerer.program) && + ((isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program) && (isModuleExportsAccess(expr.expression.expression) || (ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "exports" && @@ -9438,7 +9465,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { memberSym, resolved, lowerer.resolveValueSymbol(expr.expression.name) ?? undefined, - lowerer.cjsModuleExportSymbol(expr.getSourceFile(), expr.expression.name.text), + lowerer.cjsModuleExportSymbol(ts.getSourceFileOfNode(expr), expr.expression.name.text), ]; const exportSym = candidates.find((s) => s !== undefined); const g = candidates @@ -9653,7 +9680,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // libregexp engine the literals ride. The pattern compiles EAGERLY, // so bad input throws Node's catchable SyntaxError at construction. // Checked arguments retain native regexes and perform runtime coercion. - if (symbol && symbol.name === "RegExp" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "RegExp" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments?.nodes ?? []; if (args.length > 2) { lowerer.noLowering(`new RegExp with ${args.length} arguments`, expr); @@ -9721,19 +9748,19 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, }; } - if (symbol && symbol.name === "URL" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "URL" && lowerer.isStdlibSymbol(symbol)) { return lowerUrlNew(lowerer, expr); } // `new URLSearchParams(init?)`: the WHATWG list (stdlib provenance — // see lowerSearchParamsNew for the lowered init shapes). - if (symbol && symbol.name === "URLSearchParams" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "URLSearchParams" && lowerer.isStdlibSymbol(symbol)) { return lowerSearchParamsNew(lowerer, expr, loc); } // Date's read-only value slice: store the constructor's TimeClip'd // epoch milliseconds as the scalar date kind. That is sufficient // for every getter and toISOString; identity and setters stay // fenced, so copying the scalar cannot create an observable lie. - if (symbol && symbol.name === "Date" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "Date" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) { lowerer.noLowering( @@ -9750,7 +9777,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // pass numbers, completed with the spec's defaults (date 1, time // parts 0) for the seven-f64 ABI. if (args.length > 1) { - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { return { kind: "libCall", fn: "date.nativeNewFields", @@ -9781,7 +9808,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } return { kind: "libCall", fn: "date.newLocal", args: fields, type: DATE_T, loc }; } - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { return { kind: "libCall", // One argument: a string parses with the full date parser. @@ -9826,7 +9853,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // normalized at run time (Node's normalizeEncoding, which throws // ERR_UNKNOWN_ENCODING for an unknown name). Omitted means utf8, // Node's default. - if (symbol && symbol.name === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments?.nodes ?? []; if (args.length > 1) { lowerer.noLowering("new StringDecoder with 2 arguments", expr, undefined, symbol); @@ -9860,12 +9887,13 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, }; } + const codec = symbol ? ts.symbolName(symbol) : undefined; if ( symbol && - (symbol.name === "TextDecoder" || symbol.name === "TextEncoder") && + (codec === "TextDecoder" || codec === "TextEncoder") && lowerer.isStdlibSymbol(symbol) ) { - return lowerTextCodecNew(lowerer, expr, symbol.name); + return lowerTextCodecNew(lowerer, expr, codec); } // `new Uint8Array(...)` / `new Uint32Array(...)` / `new // Float32Array(...)` / `new Float64Array(...)`: the typed-array @@ -9899,11 +9927,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // types get their half named specifically instead of the component // fence (SC2009, which names Map slots at value positions elsewhere). // Both Array() and new Array() share the elements/count lowering. - if (symbol?.name === "Object" && lowerer.isStdlibSymbol(symbol)) { + if (symbol !== null && ts.symbolName(symbol) === "Object" && lowerer.isStdlibSymbol(symbol)) { const object = lowerObjectConstructor(lowerer, expr.arguments?.nodes ?? [], loc); if (object) return object; } - if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) { + if (symbol !== null && ts.symbolName(symbol) === "Array" && lowerer.isStdlibSymbol(symbol)) { return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []); } // A const alias (`const M = Map`) constructs the same Map. @@ -9919,7 +9947,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { : null; let tsType = lowerer.typeOf(expr); let mapped = lowerer.mapTypeOf(tsType); - if (mapped === null && isJsSourceFile(expr.getSourceFile())) mapped = lowerer.irTypeOf(expr); + if (mapped === null && isJsSourceFile(ts.getSourceFileOfNode(expr))) + mapped = lowerer.irTypeOf(expr); const fieldType = assignedThisFieldType(lowerer, expr); if (mapped?.kind !== "map" && fieldType?.kind === "map") mapped = fieldType; // A fresh map built from pair literals can use its destination's @@ -9981,7 +10010,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // unconditionally; the format path never touches them. TypeScript // keeps the compile fence. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (expr.arguments?.nodes.length ?? 0) === 0 && targs.length > 0 && targs.every((t) => (t.flags & ts.TypeFlags.Any) !== 0) @@ -10017,7 +10046,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } // Weak containers share checked native key identities and lifetime hooks. if ( - (symbol?.name === "WeakMap" || symbol?.name === "WeakSet") && + ((symbol !== null && ts.symbolName(symbol) === "WeakMap") || + (symbol !== null && ts.symbolName(symbol) === "WeakSet")) && lowerer.isStdlibSymbol(symbol) ) { const args = expr.arguments?.nodes ?? []; @@ -10025,13 +10055,13 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { lowerer.unsupported( "SC1090", expr, - `${symbol.name} construction with surplus or spread arguments`, + `${ts.symbolName(symbol)} construction with surplus or spread arguments`, ); } return { kind: "libCall", - fn: symbol.name === "WeakMap" ? "weakMap.new" : "weakSet.new", + fn: ts.symbolName(symbol) === "WeakMap" ? "weakMap.new" : "weakSet.new", args: [args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc)], type: DYN, loc, @@ -10050,7 +10080,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } if ( !mapped && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (expr.arguments?.nodes.length ?? 0) <= 1 && !expr.arguments?.nodes.some(ts.isSpreadElement) ) { @@ -10073,7 +10103,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // the honest construction is a Set of those scalars. if ( !mapped && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (expr.arguments?.nodes.length ?? 0) === 1 && ts.isArrayLiteralExpression(expr.arguments!.nodes[0]!) && !(expr.arguments!.nodes[0] as ts.ArrayLiteralExpression).elements.nodes.some( @@ -10116,7 +10146,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // `new AsyncLocalStorage()` (node:async_hooks): a fresh store id — // an f64 handle (type-mapper.ts), the Channel story. Construction options // ({ defaultValue, name }) have no lowering yet. - if (symbol?.name === "AsyncLocalStorage" && lowerer.isStdlibSymbol(symbol)) { + if ( + symbol !== null && + ts.symbolName(symbol) === "AsyncLocalStorage" && + lowerer.isStdlibSymbol(symbol) + ) { if ((expr.arguments?.nodes.length ?? 0) > 0) { lowerer.noLowering( "new AsyncLocalStorage(options)", @@ -10127,7 +10161,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { return { kind: "libCall", fn: "als.new", args: [], type: F64, loc }; } // `new Promise((resolve) => ...)`: the ambient Promise constructor. - if (symbol?.name === "Promise" && lowerer.isStdlibSymbol(symbol)) { + if (symbol !== null && ts.symbolName(symbol) === "Promise" && lowerer.isStdlibSymbol(symbol)) { const type = lowerer.irTypeOf(expr); if (type.kind !== "promise") lowerer.badType(expr, lowerer.typeOf(expr)); const args = expr.arguments?.nodes ?? []; @@ -10143,7 +10177,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (ts.isIdentifier(a0) && a0.text === "setImmediate") { const sym = lowerer.checker.getSymbolAtLocation(a0); const decls = sym ? sym.declarations : []; - if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) { + if ( + decls.length > 0 && + decls.every((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) + ) { // The settled value is the undefined dyn value — the result // is promise whatever T the checker inferred for the // unusual executor (Promise in the suite's shape). @@ -10474,7 +10511,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if ( def.arms.every( (arm) => - arm.kind === "classval" || (isJsSourceFile(expr.getSourceFile()) && isUnitType(arm)), + arm.kind === "classval" || + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && isUnitType(arm)), ) && lowerer.dynConvertible(calleeT) ) { @@ -10507,7 +10545,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const sym = raw && raw.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(raw) : raw; const hint = - sym && lowerer.isStdlibSymbol(sym) ? own(STDLIB_CTOR_HINTS, sym.name) : undefined; + sym && lowerer.isStdlibSymbol(sym) ? own(STDLIB_CTOR_HINTS, ts.symbolName(sym)) : undefined; if (hint !== undefined) { lowerer.unsupported("SC1090", expr, hint); } diff --git a/packages/compiler/src/frontend/lowering/lower-comptime.ts b/packages/compiler/src/frontend/lowering/lower-comptime.ts index f239eea82..e442b34a5 100644 --- a/packages/compiler/src/frontend/lowering/lower-comptime.ts +++ b/packages/compiler/src/frontend/lowering/lower-comptime.ts @@ -86,7 +86,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr try { if (lowerer.frontendServices === undefined) throw new Error("compile-time evaluation requires frontend services"); - result = lowerer.frontendServices.evaluateComptime(cb.getText(), COMPTIME_TIMEOUT_MS); + result = lowerer.frontendServices.evaluateComptime(ts.getTextOfNode(cb), COMPTIME_TIMEOUT_MS); } catch (e) { const detail = comptimeFailureDetail(e); lowerer.pushDiag(comptimeFailedDiag(detail, locOf(cb))); @@ -226,7 +226,7 @@ export function rejectComptimeCaptures( lowerer: Lowerer, cb: ts.ArrowFunction | ts.FunctionExpression, ): void { - const sf = cb.getSourceFile(); + const sf = ts.getSourceFileOfNode(cb); const inTypePosition = (node: ts.Node): boolean => { for (let p: ts.Node | undefined = node.parent; p && p !== cb; p = p.parent) { if (ts.isTypeNode(p)) return true; @@ -249,7 +249,7 @@ export function rejectComptimeCaptures( const decls = symbol ? symbol.declarations : undefined; if (symbol && decls && decls.length > 0) { const ownLocal = decls.every( - (d) => d.getSourceFile() === sf && d.pos >= cb.pos && d.end <= cb.end, + (d) => ts.getSourceFileOfNode(d) === sf && d.pos >= cb.pos && d.end <= cb.end, ); if (!ownLocal) { if (node.text === "comptime" && lowerer.isStdlibSymbol(symbol)) { diff --git a/packages/compiler/src/frontend/lowering/lower-console.ts b/packages/compiler/src/frontend/lowering/lower-console.ts index a7fac4f51..eec308342 100644 --- a/packages/compiler/src/frontend/lowering/lower-console.ts +++ b/packages/compiler/src/frontend/lowering/lower-console.ts @@ -209,7 +209,7 @@ export function lowerConsoleInstanceCall( if (access.questionDotToken) return null; const recvType = lowerer.typeOf(access.expression); const sym = recvType.getAliasSymbol() ?? recvType.getSymbol(); - if (!sym || sym.name !== "Console" || !lowerer.isStdlibSymbol(sym)) return null; + if (!sym || ts.symbolName(sym) !== "Console" || !lowerer.isStdlibSymbol(sym)) return null; const toStderr = OUTPUT_MEMBERS.get(access.name.text); if (toStderr === undefined) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 17522971d..4cb238352 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -195,7 +195,7 @@ export function lowerDgramDnsModuleCall( sawType = true; } else if (name === "reuseAddr") { reuseAddr = lowerer.lowerExprExpecting(prop.initializer, BOOL); - } else if (name === "signal" && isJsSourceFile(expr.getSourceFile())) { + } else if (name === "signal" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { // A provably-non-AbortSignal signal (the invalid-input probes: // strings, numbers, plain records) throws Node's // validateAbortSignal ladder; plausible signal values keep the @@ -578,7 +578,7 @@ export function lowerDgramMethodCall( return { kind: "libCall", fn, args: [receiver, data, port, host], type: VOID, loc }; } if ( - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && args.length <= 5 && !args.some(ts.isSpreadElement) ) { diff --git a/packages/compiler/src/frontend/lowering/lower-enums.ts b/packages/compiler/src/frontend/lowering/lower-enums.ts index cc2797fb9..e72dd2780 100644 --- a/packages/compiler/src/frontend/lowering/lower-enums.ts +++ b/packages/compiler/src/frontend/lowering/lower-enums.ts @@ -36,14 +36,14 @@ import { nodeThrowExpr } from "../../ir/build.js"; * the only name forms tsc allows on enum members). */ function memberNameText(member: ts.EnumMember): string { const n = member.name; - return ts.isIdentifier(n) || ts.isStringLiteralLike(n) ? n.text : n.getText(); + return ts.isIdentifier(n) || ts.isStringLiteralLike(n) ? n.text : ts.getTextOfNode(n); } /** Ambient: a `declare`d declaration, or one living in a .d.ts. */ function isAmbientEnumDecl(decl: ts.EnumDeclaration): boolean { return ( (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Ambient) !== 0 || - decl.getSourceFile().isDeclarationFile + ts.getSourceFileOfNode(decl).isDeclarationFile ); } @@ -126,7 +126,7 @@ function lowerEnumMemberRead( // runtime values, so SC2020-family member fences own those. A user's // `declare enum` follows the // `declare const __VERSION__` stance: ReferenceError at the access. - const sf = enumDecl.getSourceFile(); + const sf = ts.getSourceFileOfNode(enumDecl); if (lowerer.isStdlibFile(sf)) return null; const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (!declared || declared.kind === "void") return null; @@ -165,10 +165,10 @@ function lowerEnumReverseRead( key: ts.Expression, ): IrExpr | null { const loc = locOf(expr); - const enumName = recvSym.name; + const enumName = ts.symbolName(recvSym); // Ambient: no object exists — the member-read stance. if (decls.every((d) => isAmbientEnumDecl(d))) { - if (decls.some((d) => lowerer.isStdlibFile(d.getSourceFile()))) { + if (decls.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))) { return null; } return { diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index f2968cebd..f8588fe48 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -414,7 +414,11 @@ function isErrorMonitorRef(lowerer: Lowerer, arg: ts.Expression): boolean { const isMonitorType = (t: ts.Type): boolean => { if (!(t.flags & ts.TypeFlags.UniqueESSymbol)) return false; const symbol = t.getSymbol(); - return symbol?.name === "errorMonitor" && lowerer.isStdlibSymbol(symbol); + return ( + symbol !== undefined && + ts.symbolName(symbol) === "errorMonitor" && + lowerer.isStdlibSymbol(symbol) + ); }; // The unique symbol TYPE names its declaration through any alias. if (isMonitorType(lowerer.typeOf(arg))) return true; @@ -1697,7 +1701,7 @@ export function emitOverrideShapeReason( const p0T = lowerer.mapTypeOf(lowerer.typeOf(p0.name)); const jsUnannotated = p0.type === undefined && - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && (p0T === null || p0T.kind === "dyn"); if (p0T?.kind !== "string" && !jsUnannotated) { return "the event parameter must be typed 'string' (symbol event names have no lowering)"; @@ -1731,9 +1735,9 @@ export function emitOverrideShapeReason( "the event parameter cannot be reassigned (the forward's event name must stay pinned)"; } } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; - member.body.forEachChild(visit); + ts.forEachChild(member.body, visit); return reason; } diff --git a/packages/compiler/src/frontend/lowering/lower-expando.ts b/packages/compiler/src/frontend/lowering/lower-expando.ts index 5ed97d4d7..6adce8f67 100644 --- a/packages/compiler/src/frontend/lowering/lower-expando.ts +++ b/packages/compiler/src/frontend/lowering/lower-expando.ts @@ -98,8 +98,8 @@ function expandoFnSymbolOf(lowerer: Lowerer, recv: ts.Expression): ts.Symbol | n const sym = lowerer.resolveValueSymbol(recv); if (!sym) return null; const decl = sym.valueDeclaration; - if (!decl || decl.getSourceFile().isDeclarationFile) return null; - if (isJsSourceFile(decl.getSourceFile())) return null; + if (!decl || ts.getSourceFileOfNode(decl).isDeclarationFile) return null; + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; if (ts.isFunctionDeclaration(decl)) { return ts.isSourceFile(decl.parent) ? sym : null; } @@ -177,7 +177,7 @@ export function collectExpandoMembers(lowerer: Lowerer, sf: ts.SourceFile): void } const existing = memberOf(members, w.key); if (existing) { - existing.firstWriteStart = Math.min(existing.firstWriteStart, node.getStart()); + existing.firstWriteStart = Math.min(existing.firstWriteStart, ts.tokenStartOfNode(node)); } else if ( !(typeof w.key === "string" && READONLY_FN_MEMBERS.has(w.key)) && !nsOwnedMember(lowerer, w.access) @@ -198,18 +198,22 @@ export function collectExpandoMembers(lowerer: Lowerer, sf: ts.SourceFile): void } if (type?.kind === "void" && isUnitOnlyTsType(tsType)) type = unitOnlyUnion(lowerer.unions); if (type && type.kind !== "void") { - const fnName = w.fnSym.name; + const fnName = ts.symbolName(w.fnSym); const memberName = typeof w.key === "string" ? w.key - : `sym%${w.key.name}%${w.key.declarations[0]?.getStart() ?? 0}`; + : `sym%${ts.symbolName(w.key)}%${(w.key.declarations.length === 0 ? undefined : ts.tokenStartOfNode(w.key.declarations[0]!)) ?? 0}`; const g: IrGlobal = { id: `%g.${tag}${fnName}%.${memberName}`, name: `${fnName}.${memberName}`, type, mutable: true, }; - const entry: ExpandoMember = { global: g, firstWriteStart: node.getStart(), file: sf }; + const entry: ExpandoMember = { + global: g, + firstWriteStart: ts.tokenStartOfNode(node), + file: sf, + }; if (typeof w.key === "string") members.names.set(w.key, entry); else members.symbols.set(w.key, entry); lowerer.globalsList.push(g); @@ -248,7 +252,7 @@ function nsOwnedMember( if (ts.isModuleDeclaration(p)) { return !( p.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) || - p.getSourceFile().isDeclarationFile + ts.getSourceFileOfNode(p).isDeclarationFile ); } } @@ -308,11 +312,11 @@ export function expandoMemberRead( const member = memberOf(lowerer.expandoMembers.get(fnSym), key); if (!member) return null; if ( - access.getSourceFile() === member.file && + ts.getSourceFileOfNode(access) === member.file && initExecutingPosition(access) && - access.getStart() < member.firstWriteStart + ts.tokenStartOfNode(access) < member.firstWriteStart ) { - const name = typeof key === "string" ? key : key.name; + const name = typeof key === "string" ? key : ts.symbolName(key); lowerer.unsupported( "SC1090", access, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index e770b8d58..7789f8a86 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -193,9 +193,9 @@ export function templateRawTextOf( ): string { const own = (node as { rawText?: string }).rawText; if (own !== undefined) return own; - const sf = node.getSourceFile(); - const start = node.getStart(sf); - const end = node.getEnd(); + const sf = ts.getSourceFileOfNode(node); + const start = ts.tokenStartOfNode(node); + const end = node.end; const tailTrim = node.kind === ts.SyntaxKind.TemplateHead || node.kind === ts.SyntaxKind.TemplateMiddle ? 2 : 1; return sf.text.slice(start + 1, end - tailTrim); @@ -296,14 +296,14 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // integer within ±(2^53−1)) and non-integer spellings carry // nothing, so the IR is unchanged for programs that held their // numbers. - const spelled = expr.getText().replace(/_/g, ""); + const spelled = ts.getTextOfNode(expr).replace(/_/g, ""); if (/^\d+$/.test(spelled) && String(Number(spelled)) !== spelled) { return { kind: "numLit", value: Number(spelled), spelling: spelled, type: F64, loc }; } return { kind: "numLit", value, type: F64, loc }; } if (ts.isBigIntLiteral(expr)) { - const spelling = expr.getText().replace(/_/g, "").replace(/n$/i, ""); + const spelling = ts.getTextOfNode(expr).replace(/_/g, "").replace(/n$/i, ""); return { kind: "libCall", fn: "bigint.parse", @@ -549,7 +549,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // the read answers the strict-mode plain-call undefined, the old // constant. TypeScript keeps the fence (noImplicitThis makes it a // compile-time story there). - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { // Module scope of a CommonJS file: `this` is module.exports. const exportsThis = lowerCjsTopLevelThis(lowerer, expr); if (exportsThis) return exportsThis; @@ -1548,7 +1548,7 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr // statement ruled non-null without narrowing it (jose's secs()). if ( receiverT?.kind === "union" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && !expr.questionDotToken && !hasOptionalChainGuard(expr.expression) ) { @@ -1738,7 +1738,8 @@ export function stringWrapperToString(lowerer: Lowerer, node: ts.Expression): Ir while (ts.isParenthesizedExpression(e)) e = e.expression; if (!ts.isNewExpression(e) || !ts.isIdentifier(e.expression)) return null; const symbol = lowerer.resolveValueSymbol(e.expression); - if (symbol?.name !== "String" || !lowerer.isStdlibSymbol(symbol)) return null; + if (symbol === null || ts.symbolName(symbol) !== "String" || !lowerer.isStdlibSymbol(symbol)) + return null; const args = e.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) return null; if (args.length === 0) return { kind: "strLit", value: "", type: STRING, loc: locOf(e) }; @@ -2027,7 +2028,7 @@ export function aliasTypeofNarrows( const leaf = aliasTypeofArmOf(lowerer, e, strip); if (leaf) out.push(leaf); }; - if (implicitMonoFile(cond.getSourceFile())) walk(cond, polarity); + if (implicitMonoFile(ts.getSourceFileOfNode(cond))) walk(cond, polarity); return out; } @@ -2127,7 +2128,7 @@ export function cjsAccessorSnapshotOf( ): { id: string; type: IrType } | null { const hit = lowerer.cjsAccessorSnapshots.get(getter); if (hit !== undefined) return hit; - if (!lexerVisibleAccessors(getter.getSourceFile()).includes(getter)) return null; + if (!lexerVisibleAccessors(ts.getSourceFileOfNode(getter)).includes(getter)) return null; const call = cjsAccessorCall(lowerer, getter, locOf(getter)); if (call === null) return null; const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type }; @@ -2576,7 +2577,7 @@ export function lowerOptionalArrayIndexRead( key: IrExpr, ): IrExpr | null { if ( - !isJsSourceFile(expr.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(expr)) || expr.questionDotToken || hasOptionalChainGuard(expr.expression) ) diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 58778fc14..bec8528c4 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -130,7 +130,7 @@ function declareFunctionArguments( lowerer.ctx.argumentsLocal = local; } if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && !ts.isArrowFunction(node) && shapes.length === 1 && shapes[0]?.mode === "dynRest" && @@ -204,7 +204,7 @@ export function lowerLambda( // Fluent methods must return that receiver without a class cast. if ( ts.isMethodDeclaration(node) && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && !node.type && !node.asteriskToken && !hasExplicitJsDocReturn(node) && @@ -236,7 +236,7 @@ export function lowerLambda( if ( !( ts.isMethodDeclaration(node) && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && canBoxFuncIntoDyn( funcType, (id) => lowerer.shapes.get(id), @@ -287,14 +287,14 @@ export function lowerLambda( if (lowerer.methodLambdasInProgress.has(node)) lowerer.methodLambdasInProgress.set(node, { fnName, type: funcType, selfReads: 0 }); const inferredSignature = - isJsSourceFile(node.getSourceFile()) && !node.type + isJsSourceFile(ts.getSourceFileOfNode(node)) && !node.type ? lowerer.checker.getSignatureFromDeclaration(node) : undefined; const returnsConstructor = inferredSignature && producesConstructor(lowerer, lowerer.checker.getReturnTypeOfSignature(inferredSignature)); if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && !node.type && !isAsync && !isGenerator && @@ -316,7 +316,7 @@ export function lowerLambda( try { const receiver = (checkedReceiver || - isJsSourceFile(node.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(node)) || node.parameters.nodes.some(isThisParameter)) && !ts.isArrowFunction(node) ? declareCallReceiver(lowerer, node, isGenerator) @@ -461,7 +461,7 @@ export function lowerLambda( // position. ICEs (SC9001) stay compile errors, exactly like // lowerStmts; probe mode (diagSink) keeps the poison. if (!(e instanceof PoisonError)) throw e; - if (!isJsSourceFile(node.getSourceFile())) throw e; + if (!isJsSourceFile(ts.getSourceFileOfNode(node))) throw e; const params: IrParam[] = funcType.params.map((t, i) => ({ localId: `%pf${i}`, name: `%pf${i}`, @@ -510,7 +510,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I lowerer.fnStack.push(ctx); try { const receiver = - decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(decl.getSourceFile()) + decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(ts.getSourceFileOfNode(decl)) ? declareCallReceiver(lowerer, decl, sig.generator !== undefined) : null; const { params, prologue } = lowerer.declareParams(decl.parameters.nodes, sig.params); @@ -563,7 +563,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I // at the declaration's position — so a reachable-but-broken // signature stops the RUN at its own site instead of the build. // ICEs (SC9001) stay compile errors, exactly like lowerStmts. - if (isJsSourceFile(decl.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) { // An ABI type naming a class that never REGISTERED (the sentence- // walker idiom's path type — the #private fence) is fine to emit: // callers CAN lower calls to this symbol (a same-typed param diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index b1d0a1aa5..19a75a0b4 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -207,7 +207,7 @@ function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression (d) => ts.isInterfaceDeclaration(d.parent) && (d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") && - lowerer.isStdlibFile(d.getSourceFile()), + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ); } @@ -272,7 +272,7 @@ export function isSettledIteratorValueRead( ts.isInterfaceDeclaration(d.parent) && (d.parent.name.text === "IteratorYieldResult" || d.parent.name.text === "IteratorReturnResult") && - lowerer.isStdlibFile(d.getSourceFile()), + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) return false; @@ -282,13 +282,13 @@ export function isSettledIteratorValueRead( sym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ); const alias = receiver.getAliasSymbol(); const symbol = receiver.getSymbol(); if ( - !(alias?.name === "IteratorResult" && stdlib(alias)) && - !(symbol?.name === "IteratorReturnResult" && stdlib(symbol)) + !(alias !== undefined && ts.symbolName(alias) === "IteratorResult" && stdlib(alias)) && + !(symbol !== undefined && ts.symbolName(symbol) === "IteratorReturnResult" && stdlib(symbol)) ) return false; const checked = lowerer.mapTypeOf(lowerer.typeOf(node)); diff --git a/packages/compiler/src/frontend/lowering/lower-global-value.ts b/packages/compiler/src/frontend/lowering/lower-global-value.ts index d0b251e84..4b8e7b10a 100644 --- a/packages/compiler/src/frontend/lowering/lower-global-value.ts +++ b/packages/compiler/src/frontend/lowering/lower-global-value.ts @@ -14,7 +14,7 @@ export function lowerGlobalValue(lowerer: Lowerer, expr: ts.Expression): IrExpr // lib slice) but absent in Node 24: reads answer undefined. const names = lowerer.checker .getPropertiesOfType(lowerer.typeOf(expr)) - .map((property) => property.name) + .map((property) => ts.symbolName(property)) .filter((name) => name !== TEMPORAL_GLOBAL); return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 1310469fb..a8657f978 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -493,7 +493,7 @@ function classInspectsThroughView(lowerer: Lowerer, info: ClassInfo | undefined) if (info.subclasses.length > 0) return true; return ( info.decl != null && - isJsSourceFile(info.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && classDynViewSupported( info.def.fields, (id) => lowerer.shapes.get(id), @@ -2660,7 +2660,7 @@ function lowerUtilCallableCall( * a statically typed callback parameter cannot receive a value its type * excludes. Untyped (JavaScript or unknown) slots keep the wrapper dyn. */ function callbackifyErrorExcludesNull(lowerer: Lowerer, expr: ts.CallExpression): boolean { - if (isJsSourceFile(expr.getSourceFile())) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; const checker = lowerer.checker; const slot = checker.getContextualType(expr) ?? lowerer.typeOf(expr); const wrapper = checker.getCallSignatures(slot)[0]; @@ -2766,7 +2766,7 @@ function lowerGetCallSitesCall( loc: SrcLoc, ): IrExpr | null { if (expr.arguments.nodes.length !== 0) return null; - if (!isJsSourceFile(expr.getSourceFile())) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; const frame = (): IrExpr => { const field = (name: string, value: IrExpr): { key: IrExpr; value: IrExpr } => ({ key: { kind: "strLit", value: name, type: STRING, loc }, @@ -2778,7 +2778,7 @@ function lowerGetCallSitesCall( field("functionName", { kind: "strLit", value: "", type: STRING, loc }), field("scriptName", { kind: "strLit", - value: expr.getSourceFile().fileName, + value: ts.getSourceFileOfNode(expr).fileName, type: STRING, loc, }), diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index 90789b4a0..766059dfb 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -174,7 +174,7 @@ function mixinFnShapeInner( if (n === t.expression) return undefined; if ( ts.isIdentifier(n) && - n.text === p.name.getText() && + n.text === ts.getTextOfNode(p.name) && lowerer.checker.getSymbolAtLocation(n) === paramSym ) { extraRef = true; @@ -451,8 +451,8 @@ function instantiateMixinCall( const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!); const instName = lowerer.qualify( - call.getSourceFile(), - `%mx${call.getStart()}.${shape.classNode.name?.text ?? ""}`, + ts.getSourceFileOfNode(call), + `%mx${ts.tokenStartOfNode(call)}.${shape.classNode.name?.text ?? ""}`, ); const ordinal = lowerer.mixinOrdinals.get(shape.classNode) ?? 0; lowerer.mixinOrdinals.set(shape.classNode, ordinal + 1); @@ -513,7 +513,10 @@ function instantiateMixinCall( ); } if (info.mixinInstance) { - info.mixinInstance.statics = { sf: call.getSourceFile(), pos: holder.getStart() }; + info.mixinInstance.statics = { + sf: ts.getSourceFileOfNode(call), + pos: ts.tokenStartOfNode(holder), + }; } } return info; diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index be68abaca..fcc68c787 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -797,7 +797,7 @@ export function lowerMockMethod( const objShape = objType?.kind === "record" ? lowerer.shapes.get(objType.shapeId) : undefined; const fn = objShape?.fields.find((f) => f.name === field)?.type; if (!objType || !objShape || fn?.kind !== "func") { - const typeName = lowerer.typeOf(args[0]!).getSymbol()?.name; + const typeName = ts.symbolNameOf(lowerer.typeOf(args[0]!).getSymbol()); lowerer.noLowering( typeName && !typeName.startsWith("__") ? `${what} on '${typeName}'` diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 11db0ec8a..f0199d6a4 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -692,7 +692,10 @@ export function collectJsonImports(lowerer: Lowerer, parts: FileParts[]): void { const nameSym = lowerer.checker.getSymbolAtLocation(clause.name); if (!nameSym || !(nameSym.flags & ts.SymbolFlags.Alias)) continue; const target = lowerer.checker.getAliasedSymbol(nameSym); - const jsonSf = target.declarations[0]?.getSourceFile(); + const jsonSf = + target.declarations.length === 0 + ? undefined + : ts.getSourceFileOfNode(target.declarations[0]!); if (!jsonSf || !jsonSf.fileName.endsWith(".json")) continue; try { const tsType = lowerer.typeOf(clause.name); @@ -1175,7 +1178,7 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi if ( (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) && !e.typeParameters?.nodes.length && - isJsSourceFile(e.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(e)) ) return true; if (ts.isIdentifier(e) || ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { @@ -1274,7 +1277,7 @@ export function collectGlobals( const mutableVar = vd !== undefined && ts.isVariableDeclaration(vd) && - !vd.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(vd).isDeclarationFile && (ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const) === 0; // Immutable targets: alias plumbing — importers resolve to the // target's storage (the alias-resolved read IS the snapshot when @@ -1546,7 +1549,7 @@ export function collectGlobals( const mutableVar = vd !== undefined && ts.isVariableDeclaration(vd) && - !vd.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(vd).isDeclarationFile && (ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const) === 0; if (!mutableVar) continue; const diagsBefore = lowerer.diags.length; @@ -1557,7 +1560,7 @@ export function collectGlobals( } if (type.kind === "void") lowerer.badType(stmt.name, lowerer.typeOf(stmt.name)); const g: IrGlobal = { - id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${stmt.getStart()}`, + id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${ts.tokenStartOfNode(stmt)}`, name: stmt.name.text, type, mutable: false, @@ -2282,12 +2285,12 @@ export function collectGlobals( : undefined; const callDeclaration = callSignature && callSignature.declaration; const callType = - callDeclaration && isJsSourceFile(callDeclaration.getSourceFile()) + callDeclaration && isJsSourceFile(ts.getSourceFileOfNode(callDeclaration)) ? lowerer.mapTypeOf(lowerer.typeOf(nameNode)) : null; const inferredJsCall = callDeclaration && - isJsSourceFile(callDeclaration.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(callDeclaration)) && (!callType || (unannotatedCall && callType.kind === "union" && @@ -2710,7 +2713,7 @@ export function declSymbolOf( ts.canHaveModifiers(decl) && ts.getModifiers(decl)?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) === true; if (!isDefault) return undefined; - const sym = defaultExportSymbolOf(lowerer, decl.getSourceFile()); + const sym = defaultExportSymbolOf(lowerer, ts.getSourceFileOfNode(decl)); return sym ?? undefined; } @@ -2728,7 +2731,7 @@ export function declSymbolOf( * expression so the expression's OWN diagnostic wins when it has one, * then poisons. */ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): IrStmt | null { - const symbol = defaultExportSymbolOf(lowerer, stmt.getSourceFile()); + const symbol = defaultExportSymbolOf(lowerer, ts.getSourceFileOfNode(stmt)); if (symbol && symbol.flags & ts.SymbolFlags.Alias && !lowerer.globalsBySymbol.has(symbol)) return null; const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; @@ -2741,7 +2744,7 @@ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): while (ts.isParenthesizedExpression(inner)) inner = inner.expression; if ( ts.isClassExpression(inner) && - !isJsSourceFile(stmt.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(stmt)) && inner.typeParameters === undefined && (decoratorNodesOf(inner).length > 0 || inner.members.nodes.some((m) => decoratorNodesOf(m).length > 0)) && @@ -2961,7 +2964,7 @@ export function lowerFileInit( c.decl && ts.isClassDeclaration(c.decl) && !c.mixinInstance && - c.decl.getSourceFile() === sf && + ts.getSourceFileOfNode(c.decl) === sf && (lowerer.classInitFlags.has(c.def.name) || c.callableBase !== undefined || c.def.baseValueGlobal !== undefined || @@ -2970,7 +2973,7 @@ export function lowerFileInit( (c.staticBlocks?.length ?? 0) > 0 || c.classDecorators !== undefined), ) - .map((c) => ({ pos: c.decl!.getStart(), info: c })); + .map((c) => ({ pos: ts.tokenStartOfNode(c.decl!), info: c })); // Statics-bearing MIXIN instantiations whose call evaluates in THIS // file: their declaration-time code runs when the call does — the // top-level statement lower-mixins recorded. Insertion order is @@ -2990,7 +2993,7 @@ export function lowerFileInit( // anything the statement's lowering emits. Hoisted class // declarations never share a kept statement's start, so `<=` // changes nothing for them. - while (at < statics.length && statics[at]!.pos <= stmt.getStart()) { + while (at < statics.length && statics[at]!.pos <= ts.tokenStartOfNode(stmt)) { body.push( ...withClassInitFlag( lowerer, diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 79ac7c278..9997eb7d9 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -178,10 +178,10 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { const nested = p.parent !== undefined && ts.isModuleBlock(p.parent); const nonExported = nested && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Export) === 0; - segs.unshift(p.name.text + (nonExported ? `%${p.getStart()}` : "")); + segs.unshift(p.name.text + (nonExported ? `%${ts.tokenStartOfNode(p)}` : "")); } } - return `${segs.join("%")}${exported ? "" : `%${parent.getStart()}`}.`; + return `${segs.join("%")}${exported ? "" : `%${ts.tokenStartOfNode(parent)}`}.`; } /** The registered namespace-block kind a symbol's declaration sits in @@ -275,7 +275,11 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile stmt.moduleSpecifier !== undefined && ts.isStringLiteral(stmt.moduleSpecifier) ) { - const dep = resolveImport(lowerer.program, d.getSourceFile(), stmt.moduleSpecifier.text); + const dep = resolveImport( + lowerer.program, + ts.getSourceFileOfNode(d), + stmt.moduleSpecifier.text, + ); if (dep && !dep.isDeclarationFile && lowerer.fileTag.has(dep)) return dep; } } @@ -334,7 +338,7 @@ export function ambientNsRootOf(lowerer: Lowerer, e: ts.Expression): ts.Identifi return null; } if (ts.isModuleDeclaration(d) && !ts.isIdentifier(d.name)) return null; - if (d.getSourceFile().isDeclarationFile) return null; // lib/@types keep their own fences + if (ts.getSourceFileOfNode(d).isDeclarationFile) return null; // lib/@types keep their own fences if ( ts.isModuleDeclaration(d) ? !isAmbientModuleDecl(d) @@ -432,7 +436,7 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id if (decls.length === 0) return null; for (const d of decls) { if (!ts.isVariableDeclaration(d) || d.initializer !== undefined) return null; - if (d.getSourceFile().isDeclarationFile) return null; // lib/@types keep their own fences + if (ts.getSourceFileOfNode(d).isDeclarationFile) return null; // lib/@types keep their own fences if (!(ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient)) return null; } return root; @@ -491,7 +495,7 @@ export function trapDeclRootOf( if (root === null) return null; for (const nameNode of boundIdentifiersOf(decl.name)) { const sym = lowerer.checker.getSymbolAtLocation(nameNode); - if (sym && bindingEverWritten(lowerer, sym, decl.getSourceFile())) { + if (sym && bindingEverWritten(lowerer, sym, ts.getSourceFileOfNode(decl))) { const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(sym)); if (mapped !== null && mapped.kind !== "void") return null; } @@ -516,7 +520,7 @@ export function trapDeclRootOf( * initializer into an ordinary lowering that fences). ambientUndefVarRootOf * carries its own collect-guard, so the extra question stays a probe. */ function bindingEverWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): boolean { - const symText = sym.name; + const symText = ts.symbolName(sym); const namesSym = (e: ts.Node): boolean => ts.isIdentifier(e) && e.text === symText && lowerer.checker.getSymbolAtLocation(e) === sym; const patternWrites = (target: ts.Node): boolean => { @@ -525,9 +529,9 @@ function bindingEverWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile) const walk = (m: ts.Node): void => { if (hit) return; if (namesSym(m)) hit = true; - else m.forEachChild(walk); + else ts.forEachChild(m, walk); }; - target.forEachChild(walk); + ts.forEachChild(target, walk); return hit; }; let written = false; @@ -572,9 +576,9 @@ function bindingEverWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile) written = true; } } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; - sf.forEachChild(visit); + ts.forEachChild(sf, visit); return written; } @@ -635,7 +639,7 @@ export function ambientUndefinedFnSymbolOf( } else if (!ts.isModuleDeclaration(d) && !ts.isClassDeclaration(d)) { return null; } - if (d.getSourceFile().isDeclarationFile) return null; + if (ts.getSourceFileOfNode(d).isDeclarationFile) return null; if (!(ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient)) return null; } return sawFn ? sym : null; @@ -676,7 +680,7 @@ function initExecutingPosition(node: ts.Node): boolean { function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): number { let earliest = Infinity; for (const d of sym.declarations) { - if (d.getSourceFile() === sf) earliest = Math.min(earliest, d.getStart()); + if (ts.getSourceFileOfNode(d) === sf) earliest = Math.min(earliest, ts.tokenStartOfNode(d)); } return earliest; } @@ -691,10 +695,10 @@ export function fenceEarlyAliasUse(lowerer: Lowerer, ident: ts.Identifier, refNo const aliasDecl = sym.declarations.find(ts.isImportEqualsDeclaration); if (!aliasDecl || ts.isExternalModuleReference(aliasDecl.moduleReference)) return; if (!initExecutingPosition(refNode)) return; - const sf = refNode.getSourceFile(); - if (aliasDecl.getSourceFile() !== sf) return; // cross-file: module init order applies - const aliasStart = aliasDecl.getStart(); - if (refNode.getStart() < aliasStart) { + const sf = ts.getSourceFileOfNode(refNode); + if (ts.getSourceFileOfNode(aliasDecl) !== sf) return; // cross-file: module init order applies + const aliasStart = ts.tokenStartOfNode(aliasDecl); + if (ts.tokenStartOfNode(refNode) < aliasStart) { lowerer.unsupported( "SC1090", refNode, @@ -730,8 +734,8 @@ export function fenceEarlyNsMemberRef( memberSym: ts.Symbol, ): void { if (!initExecutingPosition(access)) return; - const sf = access.getSourceFile(); - const refStart = access.getStart(); + const sf = ts.getSourceFileOfNode(access); + const refStart = ts.tokenStartOfNode(access); const memberStart = earliestSameFileDeclStart(lowerer, memberSym, sf); if (memberStart !== Infinity && refStart < memberStart) { lowerer.unsupported( diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index fee25b10f..37675777c 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -45,7 +45,7 @@ function cjsGlobal(lowerer: Lowerer, node: ts.Expression, name: "module" | "requ const decls = symbol ? symbol.declarations : []; const sourceShadow = decls.some( (decl) => - !decl.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(decl).isDeclarationFile && (ts.isVariableDeclaration(decl) || ts.isParameter(decl) || ts.isBindingElement(decl) || @@ -55,7 +55,7 @@ function cjsGlobal(lowerer: Lowerer, node: ts.Expression, name: "module" | "requ ts.isNamespaceImport(decl) || ts.isImportEqualsDeclaration(decl)), ); - return !sourceShadow && !isNodeEsmFile(node.getSourceFile(), lowerer.program); + return !sourceShadow && !isNodeEsmFile(ts.getSourceFileOfNode(node), lowerer.program); } function graphSyntax(lowerer: Lowerer, sf: ts.SourceFile): boolean { @@ -174,7 +174,7 @@ export function cjsModuleRegistryPrelude(lowerer: Lowerer, loc: IrExpr["loc"]): } export function cjsModuleRef(lowerer: Lowerer, node: ts.Node): IrExpr | null { - const id = lowerer.cjsModuleIdOf.get(node.getSourceFile()); + const id = lowerer.cjsModuleIdOf.get(ts.getSourceFileOfNode(node)); return id === undefined ? null : numLit(id, locOf(node)); } @@ -203,8 +203,8 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean { const symbol = type.getSymbol(); if ( symbol && - (symbol.name === "Module" || symbol.name === "ScriptcModule") && - symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) + (ts.symbolName(symbol) === "Module" || ts.symbolName(symbol) === "ScriptcModule") && + symbol.declarations.some((decl) => lowerer.isStdlibFile(ts.getSourceFileOfNode(decl))) ) { return true; } @@ -217,7 +217,7 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean { (ts.isInterfaceDeclaration(parent) || ts.isClassDeclaration(parent)) && parent.name && (parent.name.text === "ScriptcModule" || parent.name.text === "Module") && - lowerer.isStdlibFile(parent.getSourceFile()) + lowerer.isStdlibFile(ts.getSourceFileOfNode(parent)) ) { return true; } @@ -237,7 +237,7 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean { * file assigns an object or function. */ function moduleExportsAlwaysTruthy(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { if (!truthinessOnly(expr)) return false; - const sf = expr.getSourceFile(); + const sf = ts.getSourceFileOfNode(expr); if (isNodeEsmFile(sf, lowerer.program)) return false; let truthy = true; ts.walkPreorder(sf, (node) => { @@ -307,7 +307,7 @@ export function isCjsTopLevelThis(lowerer: Lowerer, node: ts.Node): boolean { ) return false; } - return isCjsJsFile(node.getSourceFile(), lowerer.program); + return isCjsJsFile(ts.getSourceFileOfNode(node), lowerer.program); } export function usesCjsTopLevelThis(lowerer: Lowerer, sf: ts.SourceFile): boolean { @@ -341,7 +341,7 @@ export function usesCjsTopLevelThis(lowerer: Lowerer, sf: ts.SourceFile): boolea * behave as in Node. */ export function lowerCjsTopLevelThis(lowerer: Lowerer, expr: ts.Expression): IrExpr | null { if (!isCjsTopLevelThis(lowerer, expr)) return null; - const global = lowerer.cjsExportsThis.get(expr.getSourceFile()); + const global = lowerer.cjsExportsThis.get(ts.getSourceFileOfNode(expr)); if (!global) return null; let receiver: ts.Node = expr; while (ts.isParenthesizedExpression(receiver.parent)) receiver = receiver.parent; @@ -433,7 +433,7 @@ export function lowerNodeModuleIdentifier(lowerer: Lowerer, expr: ts.Identifier) if (!isCjsModuleGlobal(lowerer, expr)) return null; const ref = cjsModuleRef(lowerer, expr); if (ref) return ref; - if (isCjsJsFile(expr.getSourceFile(), lowerer.program)) { + if (isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program)) { lowerer.unsupported( "SC1090", expr, diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index b4e64c9d0..afae7dd7b 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -719,7 +719,7 @@ export function lowerNetModuleCall( // ERR_INVALID_ARG_TYPE on 'options' before any server exists. DYN // result — the checked-dynamic lane's concise arrows box it, and // the throw means it never materializes. - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { const t = lowerer.mapTypeOf(lowerer.typeOf(args[0]!)); if (t !== null && (t.kind === "string" || t.kind === "f64" || t.kind === "bool")) { const raw = lowerer.lowerExpr(args[0]!); @@ -770,7 +770,7 @@ export function lowerNetModuleCall( // invalid-input probes build theirs with computed keys): the // checked-dynamic walk validates Node-order and the compiler-rendered // fence is the post-validation tail. - if (args.length === 1 && isJsSourceFile(expr.getSourceFile())) { + if (args.length === 1 && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const t = lowerer.mapTypeOf(lowerer.typeOf(args[0]!)); if (t !== null && (t.kind === "dyn" || t.kind === "record")) { const raw = lowerer.lowerExpr(args[0]!); @@ -885,7 +885,7 @@ function lowerNetConnectOptions( ): IrExpr { const args = expr.arguments.nodes; const optsNode = args[0] as ts.ObjectLiteralExpression; - const isJs = isJsSourceFile(expr.getSourceFile()); + const isJs = isJsSourceFile(ts.getSourceFileOfNode(expr)); // The checked-dynamic option-bag route (JS sources): a literal with // computed keys (the invalid-input probes' spelling) or an objectMode- // trio member rides WHOLE to the runtime walk — Node's Socket-ctor @@ -1524,7 +1524,7 @@ function lowerNetServerMethodCall( } else if ( key === "signal" && ts.isPropertyAssignment(prop) && - isJsSourceFile(call.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(call)) ) { // A provably-non-AbortSignal signal (the invalid-input probes: // strings, numbers, plain records) throws Node's @@ -2334,7 +2334,7 @@ function lowerNetSocketMethodCall( if ( encT.value === "buffer" && chunkT.kind === "string" && - isJsSourceFile(call.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(call)) ) { lowerer.lowerExpr(args[0]!); // evaluation order (effect-free in practice) return nodeThrowExpr(1, "ERR_UNKNOWN_ENCODING", "Unknown encoding: buffer", VOID, loc); @@ -4851,7 +4851,7 @@ function tlsLiteralNeedsRuntimeWalk( node: ts.ObjectLiteralExpression, includeHttpTimeoutOptions = false, ): boolean { - const jsSource = isJsSourceFile(node.getSourceFile()); + const jsSource = isJsSourceFile(ts.getSourceFileOfNode(node)); return node.properties.nodes.some( (p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && @@ -5071,7 +5071,7 @@ function lowerTlsModuleCall( bi.member === "getCACertificates" && args.length === 1 && !args.some(ts.isSpreadElement) && - isJsSourceFile(expr.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(expr)) ) { // The type-argument ladder (validateString + the documented name // set); the real CA list has no lowering, so a valid name meets the @@ -5737,7 +5737,7 @@ function lowerHttp2ModuleCall( if (ts.isConditionalExpression(inner)) { const whenTrue = stripParensAndCasts(inner.whenTrue); const whenFalse = stripParensAndCasts(inner.whenFalse); - const condText = inner.condition.getText(); + const condText = ts.getTextOfNode(inner.condition); const ok = ts.isObjectLiteralExpression(whenTrue) && ts.isObjectLiteralExpression(whenFalse) && @@ -5748,7 +5748,7 @@ function lowerHttp2ModuleCall( ts.isStringLiteral(whenTrue.properties.nodes[0]!.name)) && (whenTrue.properties.nodes[0]!.name as ts.Identifier | ts.StringLiteral).text === "SNICallback" && - whenTrue.properties.nodes[0]!.initializer.getText() === condText; + ts.getTextOfNode(whenTrue.properties.nodes[0]!.initializer) === condText; if (!ok) { lowerer.noLowering( "createSecureServer options with a conditional spread", @@ -7533,7 +7533,7 @@ function lowerHttpResMethodCall( } if (name === "writeContinue" || name === "writeProcessing" || name === "writeEarlyHints") { requireStatementPosition(lowerer, call, `res.${name}(...)`); - if (lowerer.typeOf(access.expression).getSymbol()?.name === "Http2ServerResponse") { + if (ts.symbolNameOf(lowerer.typeOf(access.expression).getSymbol()) === "Http2ServerResponse") { lowerer.noLowering( `Http2ServerResponse.${name}`, call, @@ -7642,7 +7642,7 @@ function lowerHttpResMethodCall( } if (name === "addTrailers") { requireStatementPosition(lowerer, call, "res.addTrailers(...)"); - if (lowerer.typeOf(access.expression).getSymbol()?.name === "Http2ServerResponse") { + if (ts.symbolNameOf(lowerer.typeOf(access.expression).getSymbol()) === "Http2ServerResponse") { lowerer.noLowering( "Http2ServerResponse.addTrailers", call, diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 78d690c6b..bc8955a81 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -667,7 +667,7 @@ export function lowerStaticResponseCall(lowerer: Lowerer, call: ts.CallExpressio } const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const sym = recvType.getAliasSymbol() ?? recvType.getSymbol(); - if (!sym || sym.name !== "Response" || !lowerer.isStdlibSymbol(sym)) return null; + if (!sym || ts.symbolName(sym) !== "Response" || !lowerer.isStdlibSymbol(sym)) return null; const recv = lowerer.lowerExpr(access.expression); if (recv.type.kind !== "dyn") return null; return lowerStaticFixedFetchMethodCall(lowerer, call, access, member, recv, [], (receiver) => @@ -753,7 +753,7 @@ function lowerStaticFixedFetchMethodCall( loc, }, then: answer, - else_: nodeThrowExpr(1, "", `${access.getText()} is not a function`, answer.type, loc), + else_: nodeThrowExpr(1, "", `${ts.getTextOfNode(access)} is not a function`, answer.type, loc), type: answer.type, loc, }; @@ -927,7 +927,7 @@ export function fenceStaticResponseMember( ): IrExpr | null { const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const sym = recvType.getAliasSymbol() ?? recvType.getSymbol(); - if (!sym || sym.name !== "Response" || !lowerer.isStdlibSymbol(sym)) return null; + if (!sym || ts.symbolName(sym) !== "Response" || !lowerer.isStdlibSymbol(sym)) return null; const members = fetchAccessMemberNames(lowerer, access); if (members === null) return null; const status = (member: string) => @@ -969,7 +969,7 @@ export function fenceStaticHeadersMember( ): IrExpr | null { const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const sym = recvType.getAliasSymbol() ?? recvType.getSymbol(); - if (!sym || sym.name !== "Headers" || !lowerer.isStdlibSymbol(sym)) return null; + if (!sym || ts.symbolName(sym) !== "Headers" || !lowerer.isStdlibSymbol(sym)) return null; const members = fetchAccessMemberNames(lowerer, access); if (members === null) return null; const status = (member: string) => @@ -1026,7 +1026,8 @@ function isStdlibFetchInterface(lowerer: Lowerer, node: ts.Node, owner: string): for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) { // NodeJS.ReadableStream shares the WHATWG interface's name. if ( - symbol?.name === owner && + symbol !== undefined && + ts.symbolName(symbol) === owner && lowerer.isStdlibSymbol(symbol) && !symbol.declarations.some( (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"), @@ -1093,10 +1094,10 @@ function fetchInterfaceInventoryOwner( const symbol = [type.getSymbol(), type.getAliasSymbol()].find( (candidate) => candidate !== undefined && - NODE24_FETCH_COMPAT_PROFILE.inventory.interfaces.includes(candidate.name) && + NODE24_FETCH_COMPAT_PROFILE.inventory.interfaces.includes(ts.symbolName(candidate)) && lowerer.isStdlibSymbol(candidate), ); - return symbol ? { owner: symbol.name, symbol } : null; + return symbol ? { owner: ts.symbolName(symbol), symbol } : null; } function fenceFetchObjectMembers( @@ -1308,7 +1309,7 @@ export function lowerFetchElementMethodCall( kind: "dynInvoke", recv: receiverRef, method: member, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: argumentRefs, type: DYN, loc, @@ -1321,7 +1322,7 @@ export function lowerFetchElementMethodCall( let result: IrExpr = nodeThrowExpr( 1, "", - `${access.getText()} is not a function`, + `${ts.getTextOfNode(access)} is not a function`, invocationType, loc, ); @@ -1627,9 +1628,10 @@ export function lowerStaticAbortControllerCall( const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( - symbol?.name !== "AbortController" || + symbol === undefined || + ts.symbolName(symbol) !== "AbortController" || !symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return null; @@ -1652,7 +1654,7 @@ export function lowerStaticAbortControllerCall( kind: "dynInvoke", recv: receiverRef, method: "abort", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: argumentRefs, type: DYN, loc, @@ -1681,9 +1683,10 @@ export function lowerStaticReadableStreamControllerCall( const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( - symbol?.name !== "ReadableStreamDefaultController" || + symbol === undefined || + ts.symbolName(symbol) !== "ReadableStreamDefaultController" || !symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return null; @@ -1707,7 +1710,7 @@ export function lowerStaticReadableStreamControllerCall( kind: "dynInvoke", recv: receiver, method: member, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: argumentRefs, type: DYN, loc, @@ -1756,7 +1759,7 @@ export function lowerStaticReadableStreamCancelCall( kind: "dynInvoke", recv: receiverRef, method: "cancel", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: argumentRefs, type: DYN, loc, @@ -1789,9 +1792,10 @@ export function lowerStaticAbortSignalListenerCall( const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( - (symbol?.name !== "AbortSignal" && symbol?.name !== "EventTarget") || + ((symbol === undefined || ts.symbolName(symbol) !== "AbortSignal") && + (symbol === undefined || ts.symbolName(symbol) !== "EventTarget")) || !symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return null; @@ -1814,7 +1818,7 @@ export function lowerStaticAbortSignalListenerCall( kind: "dynInvoke", recv: receiver, method: member, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: argumentRefs, type: DYN, loc, @@ -2146,9 +2150,10 @@ export function lowerStaticReadableStreamReaderCall( const receiverTs = lowerer.typeOf(access.expression); const symbol = receiverTs.getSymbol(); if ( - symbol?.name !== "ReadableStreamDefaultReader" || + symbol === undefined || + ts.symbolName(symbol) !== "ReadableStreamDefaultReader" || !symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return null; @@ -2189,7 +2194,7 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression if (call.arguments.nodes.length !== 1) { lowerer.unsupported("SC1090", call, "dynamic import() with import attributes"); } - const res = lowerer.dynImports.get(`${call.getSourceFile().fileName}\u0000${arg.text}`); + const res = lowerer.dynImports.get(`${ts.getSourceFileOfNode(call).fileName}\u0000${arg.text}`); if (res === undefined) lowerer.refuseUnsupportedApi("'import()'", call); if (res.kind === "program-module") { return lowerOwnStaticModuleImport(lowerer, call, arg); @@ -2569,6 +2574,6 @@ export function npmMemberFence(lowerer: Lowerer, access: ts.PropertyAccessExpres export function npmPackageOfSymbol(lowerer: Lowerer, sym: ts.Symbol | undefined): string | null { const decls = sym ? sym.declarations : undefined; if (!decls || decls.length === 0) return null; - if (!decls.every((d) => lowerer.isNpmFile(d.getSourceFile()))) return null; - return npmPackageNameOf(decls[0]!.getSourceFile().fileName); + if (!decls.every((d) => lowerer.isNpmFile(ts.getSourceFileOfNode(d)))) return null; + return npmPackageNameOf(ts.getSourceFileOfNode(decls[0]!).fileName); } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 5efdfae76..69f6cc99b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -256,8 +256,8 @@ function npmStaticReturnedEmptyArrayType( decl.initializer === undefined || !ts.isArrayLiteralExpression(decl.initializer) || decl.initializer.elements.nodes.length !== 0 || - !isJsSourceFile(decl.getSourceFile()) || - npmStaticPackageOfPath(decl.getSourceFile().fileName) === null || + !isJsSourceFile(ts.getSourceFileOfNode(decl)) || + npmStaticPackageOfPath(ts.getSourceFileOfNode(decl).fileName) === null || lowerer.ctx.returnType.kind !== "array" ) { return null; @@ -296,8 +296,8 @@ function npmStaticOptionalSelfWriteType( ): (IrType & { kind: "union" }) | null { if ( !ts.isIdentifier(decl.name) || - !isJsSourceFile(decl.getSourceFile()) || - npmStaticPackageOfPath(decl.getSourceFile().fileName) === null || + !isJsSourceFile(ts.getSourceFileOfNode(decl)) || + npmStaticPackageOfPath(ts.getSourceFileOfNode(decl).fileName) === null || initial.kind === "union" ) { return null; @@ -359,7 +359,9 @@ function stdlibTypeNamed(lowerer: Lowerer, node: ts.Node, name: string): boolean if (t.flags & ts.TypeFlags.Union) return constituentTypes(t).some(visit); const sym = t.getAliasSymbol() ?? t.getSymbol(); return ( - sym?.name === name && sym.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())) + sym !== undefined && + ts.symbolName(sym) === name && + sym.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) ); }; return visit(lowerer.typeOf(node)); @@ -613,7 +615,7 @@ function pureAnnotatedDeadConst(stmt: ts.Statement, sf: ts.SourceFile): boolean return decls.every((d) => { let init = d.initializer; if (init === undefined) return false; - const trivia = sf.text.slice(init.pos, init.getStart(sf)); + const trivia = sf.text.slice(init.pos, ts.tokenStartOfNode(init)); if (!trivia.includes("@__PURE__") && !trivia.includes("#__PURE__")) return false; // `as` casts are type-world wrappers (`(() => {...})() as unknown as // { new (): any }` — cookie's NullObject shape); the runtime @@ -640,7 +642,7 @@ function pureAnnotatedDeadConst(stmt: ts.Statement, sf: ts.SourceFile): boolean * probes that would otherwise claim (and diagnose) the call-initializer * shape. */ export function provenanceElidedConstDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (!isProvenanceSourceFile(sf.fileName)) return false; const stmt = decl.parent?.parent; if (!ts.isVariableStatement(stmt) || !pureAnnotatedDeadConst(stmt, sf)) return false; @@ -683,7 +685,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir // runtimeFence — a catchable, SC-coded throw at the statement's // position — instead of failing the whole build). TypeScript keeps // compile fences. ICEs (SC9001) always stay compile errors. - const sf = stmts[0]?.getSourceFile(); + const sf = stmts.length === 0 ? undefined : ts.getSourceFileOfNode(stmts[0]!); const deferFences = sf !== undefined && isJsSourceFile(sf); // Register this list as open for the forward-capture machinery: a // nested function lowering mid-list may pre-declare a LATER const of @@ -959,7 +961,7 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b return false; let type = lowerer.irTypeOf(decl.name); if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && (ts.isObjectLiteralExpression(decl.initializer) || @@ -1107,7 +1109,7 @@ function jsBindingKeepsCheckedObject( nameNode: ts.Identifier, type: IrType, ): boolean { - if (!isJsSourceFile(nameNode.getSourceFile())) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(nameNode))) return false; const host = hostVariableDeclarationOf(nameNode); if (!host || host.type || hasJsTypeAnnotation(host)) return false; const arms = type.kind === "union" ? (lowerer.unions.get(type.unionId)?.arms ?? []) : [type]; @@ -1122,8 +1124,9 @@ function jsBindingKeepsCheckedObject( function mentionsSymbol(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean { if (ts.isIdentifier(node)) return lowerer.checker.getSymbolAtLocation(node) === symbol; return ( - node.forEachChild((child) => (mentionsSymbol(lowerer, child, symbol) ? true : undefined)) === - true + ts.forEachChild(node, (child) => + mentionsSymbol(lowerer, child, symbol) ? true : undefined, + ) === true ); } @@ -1169,7 +1172,8 @@ function lowerJsVarFromExisting( symbol: ts.Symbol, ): IrStmt | null { if (!ts.isIdentifier(decl.name) || !decl.initializer) return null; - if (!isJsSourceFile(decl.getSourceFile()) || lowerer.ctx.hoistedVars.has(symbol)) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(decl)) || lowerer.ctx.hoistedVars.has(symbol)) + return null; if (lowerer.bindingIn(lowerer.ctx, symbol)) return null; let node: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node)) @@ -1212,13 +1216,13 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul ) return DYN; if ( - isJsSourceFile(nameNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(nameNode)) && ts.isVariableDeclaration(nameNode.parent) && isNativeBuiltinValueInitializer(lowerer, nameNode.parent.initializer) ) return DYN; let type = lowerer.mapTypeOf(lowerer.typeOf(nameNode)); - if (type?.kind === "record" && isJsSourceFile(nameNode.getSourceFile())) { + if (type?.kind === "record" && isJsSourceFile(ts.getSourceFileOfNode(nameNode))) { const shape = lowerer.shapes.get(type.shapeId); if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) type = DYN; } @@ -1821,7 +1825,7 @@ function lowerVarDeclarator( requireSpecOf(decl.initializer) !== null && !isNativeFfiRequire(lowerer, decl.initializer) && !isJsonRequireDecl(lowerer, decl) && - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) { const spec = requireSpecOf(decl.initializer)!; if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false); @@ -2070,7 +2074,7 @@ function lowerMatchAllDrainDecl( export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): boolean { const widened = lowerer.checker.getBaseTypeOfLiteralType(type); const declarationBelongs = (d: ts.Node): boolean => { - if (!lowerer.isStdlibFile(d.getSourceFile())) return false; + if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false; let inParseArgsType = false; for (let node: ts.Node | undefined = d.parent; node; node = node.parent) { if ( @@ -2086,7 +2090,11 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool return false; }; const sym = widened.getAliasSymbol() ?? widened.getSymbol(); - if (sym && isParseArgsDynTypeName(sym.name) && sym.declarations.some(declarationBelongs)) { + if ( + sym && + isParseArgsDynTypeName(ts.symbolName(sym)) && + sym.declarations.some(declarationBelongs) + ) { return true; } // A discriminant check erases the token alias and leaves one anonymous @@ -2143,7 +2151,7 @@ export function lowerDestructuringDecl( // sources keep their own fences below; other carriers keep this one. if ( lowerer.anyOrigin(decl.initializer) && - !isJsSourceFile(decl.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && init.type.kind !== "record" && init.type.kind !== "array" && init.type.kind !== "dyn" @@ -2159,7 +2167,7 @@ export function lowerDestructuringDecl( // the checker's own type instead of the carrier. if ( init.type.kind === "string" && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !checkerStringSource(lowerer, decl.initializer) ) { lowerer.unsupported( @@ -2177,7 +2185,7 @@ export function lowerDestructuringDecl( init.type.kind === "bool" || init.type.kind === "string" || isUnitType(init.type)) && - !isJsSourceFile(decl.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && !( init.type.kind === "string" && staticStringPattern(lowerer, decl.name as ts.ArrayBindingPattern | ts.ObjectBindingPattern) @@ -2201,7 +2209,7 @@ export function lowerDestructuringDecl( // (the message is the fence's — a unit value here is the failure // path either way). TypeScript keeps the fence: narrowing first is // the annotated fix. - if (isJsSourceFile(decl.getSourceFile()) && init.type.kind === "union") { + if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && init.type.kind === "union") { const def = lowerer.unions.get(init.type.unionId); const dataArms = (def?.arms ?? []).filter((a) => !isUnitType(a)); const arm = dataArms[0]; @@ -2260,7 +2268,7 @@ export function lowerDestructuringDecl( if ( init.type.kind === "dyn" && ts.isObjectBindingPattern(decl.name) && - !isJsSourceFile(decl.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && lowerer.anyOrigin(decl.initializer) ) { const spelling = destrSpellingOf(decl.initializer); @@ -2341,7 +2349,7 @@ export function matchGroupsPatternSource( !ts.isObjectBindingPattern(pattern) || pattern.elements.nodes.length === 0 || !(row.type.kind === "dyn" - ? isJsSourceFile(pattern.getSourceFile()) + ? isJsSourceFile(ts.getSourceFileOfNode(pattern)) : isMatchSliceType(lowerer, row.type)) || !pattern.elements.nodes.every((el) => { if (el.dotDotDotToken !== undefined || el.name === undefined) return false; @@ -2384,7 +2392,9 @@ export function lowerBindingPattern( const patternType = lowerer.typeOf(pattern); const patternSymbol = patternType.getSymbol(); allowDynObject ||= - patternSymbol?.name === "SegmentData" && lowerer.isStdlibSymbol(patternSymbol); + patternSymbol !== undefined && + ts.symbolName(patternSymbol) === "SegmentData" && + lowerer.isStdlibSymbol(patternSymbol); allowDynObject ||= (patternType.flags & ts.TypeFlags.Object) !== 0 && lowerer.mapTypeOf(patternType)?.kind === "dyn"; @@ -2842,7 +2852,7 @@ export function lowerBindingPattern( // object patterns use the same exact keyed-read desugar. if ( srcType.kind === "union" && - isJsSourceFile(pattern.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(pattern)) && lowerer.dynConvertible(srcType) ) { lowerer.lowerBindingPattern( @@ -3250,7 +3260,7 @@ export function lowerBindingPattern( lowerer.bindPatternTarget(el.name, value, isLet, out); continue; } - if (!fieldType && isJsSourceFile(el.getSourceFile())) { + if (!fieldType && isJsSourceFile(ts.getSourceFileOfNode(el))) { // Inferred JavaScript shapes can omit optional own properties and // inherit values from their prototype. Read the live checked view. const loc = locOf(el); @@ -3384,7 +3394,7 @@ function patternBindingType(lowerer: Lowerer, name: ts.BindingName): IrType | nu const type = lowerer.typeOf(name); return ( lowerer.mapTypeOf(type) ?? - (isJsSourceFile(name.getSourceFile()) ? dynFallbackType(lowerer, name, type) : null) + (isJsSourceFile(ts.getSourceFileOfNode(name)) ? dynFallbackType(lowerer, name, type) : null) ); } @@ -3477,7 +3487,7 @@ function isRegexMatchResultPattern(lowerer: Lowerer, pattern: ts.ObjectBindingPa const symbol = type.getAliasSymbol() ?? type.getSymbol(); return ( symbol !== undefined && - (symbol.name === "RegExpExecArray" || symbol.name === "RegExpMatchArray") && + (ts.symbolName(symbol) === "RegExpExecArray" || ts.symbolName(symbol) === "RegExpMatchArray") && lowerer.isStdlibSymbol(symbol) ); } @@ -3788,7 +3798,7 @@ function stdlibGlobalTokenDestructure( decl: ts.VariableDeclaration, isLet: boolean, ): IrStmt[] | null { - if (decl.initializer === undefined || !isJsSourceFile(decl.getSourceFile())) return null; + if (decl.initializer === undefined || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; const globalName = stdlibGlobalNameOf(lowerer, decl.initializer); if (globalName === null) return null; if (!ts.isObjectBindingPattern(decl.name)) { @@ -3864,7 +3874,7 @@ function stdlibGlobalTokenDestructure( `destructuring the non-builtin member '${propName}' of 'globalThis'`, ); } - const canonical = member.name === "global" ? "globalThis" : member.name; + const canonical = ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member); token = `[builtin ${canonical}]`; alias = canonical; // Builtin constructors read as their interned function values, the @@ -4468,7 +4478,7 @@ function classInstanceRestValue( const deferOrderCheck = lowerer.instantiationContext !== null && lowerer.onEdge !== null && - !isJsSourceFile(blame.getSourceFile()); + !isJsSourceFile(ts.getSourceFileOfNode(blame)); if (!deferOrderCheck) { if (!orderMatches(emitOrder)) { lowerer.unsupported( @@ -4486,7 +4496,7 @@ function classInstanceRestValue( // matters. const context = lowerer.instantiationContext; const countFailure = !lowerer.suppressStats; - const sf = blame.getSourceFile(); + const sf = ts.getSourceFileOfNode(blame); lowerer.shapeOrderMetadataFinalizers.push(() => { const current = lowerer.shapes.get(restT.shapeId) ?? restShape; const currentOrder = current.declaredOrder ?? current.fields.map((f) => f.name); @@ -4747,7 +4757,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration) if ( ts.isCallExpression(node.right) && (implicitMethodCallInfersReturn(lowerer, node.right) || - isJsSourceFile(node.right.getSourceFile())) + isJsSourceFile(ts.getSourceFileOfNode(node.right))) ) { invalid = true; return "stop"; @@ -4868,9 +4878,9 @@ export function lowerVarDecl( (decl.name.text === "module" || decl.name.text === "exports") && ts.isVariableStatement(decl.parent?.parent) && ts.isSourceFile(decl.parent?.parent?.parent) && - !isJsSourceFile(decl.getSourceFile()) && - !decl - .getSourceFile() + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && + !ts + .getSourceFileOfNode(decl) .statements.nodes.some( (s) => ts.isImportDeclaration(s) || @@ -5075,7 +5085,7 @@ export function lowerVarDecl( : undefined, ); if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && raw.type.kind === "dyn" && @@ -5147,7 +5157,7 @@ export function lowerVarDecl( // immediately (the type covers the unassigned state), so they // initialize to the interned undefined arm like an omitted optional. let type = lowerer.irTypeOf(decl.name); - if (type.kind === "dyn" && isLet && isJsSourceFile(decl.getSourceFile())) { + if (type.kind === "dyn" && isLet && isJsSourceFile(ts.getSourceFileOfNode(decl))) { type = inferredEvolvingLetType(lowerer, decl) ?? type; } // `var v: void;` / `let x: undefined;` — a unit-only binding rides @@ -5174,7 +5184,7 @@ export function lowerVarDecl( // function (with its hint), not the unmappable declared type. If it // poisons, still declare the local when its type is representable, so // later references to this name don't produce cascading errors. - const inferredObjectType = isJsSourceFile(decl.getSourceFile()) + const inferredObjectType = isJsSourceFile(ts.getSourceFileOfNode(decl)) ? lowerer.mapTypeOf(lowerer.typeOf(decl.name)) : null; const inferredObjectArms = @@ -5191,7 +5201,7 @@ export function lowerVarDecl( inferredObjectArms.some( (arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm), ) || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && ts.isObjectLiteralExpression(decl.initializer) && @@ -5207,7 +5217,7 @@ export function lowerVarDecl( try { init = expandsObject || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && isNativeBuiltinValueInitializer(lowerer, decl.initializer)) ? lowerer.lowerExprExpecting(decl.initializer, DYN) @@ -5244,7 +5254,7 @@ export function lowerVarDecl( (init.type.kind === "moduleNs" ? init.type : null) ?? (init.type.kind === "dyn" ? DYN : null); if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && init.type.kind === "func" @@ -5275,8 +5285,8 @@ export function lowerVarDecl( if ( lowerer.implicitParamTypes !== null && init.type.kind === "dyn" && - isJsSourceFile(decl.getSourceFile()) && - npmStaticPackageOfPath(decl.getSourceFile().fileName) !== null + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + npmStaticPackageOfPath(ts.getSourceFileOfNode(decl).fileName) !== null ) { type = DYN; } @@ -5284,17 +5294,17 @@ export function lowerVarDecl( init.type.kind === "dyn" && (inferredObjectType?.kind === "record" || inferredObjectType?.kind === "array" || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && inferredObjectArms.some((arm) => arm.kind === "record") && inferredObjectArms.every((arm) => isUnitType(arm) || arm.kind === "record")) || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && inferredObjectType !== null && (inferredObjectType.kind === "date" || inferredObjectType.kind === "func" || inferredObjectType.kind === "object" || jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn")) || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && inferredObjectType !== null && (inferredObjectType.kind === "void" || isUnitType(inferredObjectType) || @@ -5312,14 +5322,14 @@ export function lowerVarDecl( expandsObject || preservesObjectIdentity || keepsAssertedObject || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && isNativeBuiltinValueInitializer(lowerer, decl.initializer)) ) { type = DYN; } if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && init.type.kind === "array" && init.type.elem.kind === "dyn" && @@ -5335,14 +5345,14 @@ export function lowerVarDecl( // cannot hold the binding's future. The binding is checked-dynamic // instead (irTypeOf's JS story): writes dynFrom, typed exits dynCheck. // Const keeps the static empty record — no reassignment exists. - if (isLet && type?.kind === "record" && isJsSourceFile(decl.getSourceFile())) { + if (isLet && type?.kind === "record" && isJsSourceFile(ts.getSourceFileOfNode(decl))) { const shape = lowerer.shapes.get(type.shapeId); if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) type = DYN; } if ( isLet && type !== null && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && (type.kind === "object" || type.kind === "symbol" || jsBindingHasOpenWrites(lowerer, decl)) @@ -5366,7 +5376,7 @@ export function lowerVarDecl( if ( returnedEmptyArrayType === null && (type === null || type.kind === "array") && - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) { let initExpr: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(initExpr)) initExpr = initExpr.expression; @@ -5429,7 +5439,7 @@ export function lowerVarDecl( if ( type?.kind === "func" && init.type.kind === "dyn" && - (isJsSourceFile(decl.getSourceFile()) || + (isJsSourceFile(ts.getSourceFileOfNode(decl)) || (!decl.type && !hasJsTypeAnnotation(decl) && type.rest === true && @@ -5438,7 +5448,7 @@ export function lowerVarDecl( type = DYN; } if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && init.type.kind === "dyn" && @@ -5465,7 +5475,7 @@ export function lowerVarDecl( // type (a union with a fence-folded arm — the typeof-'bigint' // dual-mode ternary) adopts the initializer's static type, the // same rule genuine `any` consts take. - (type === null && isJsSourceFile(decl.getSourceFile()))) && + (type === null && isJsSourceFile(ts.getSourceFileOfNode(decl)))) && init.type.kind !== "void" && init.type.kind !== "caught" && !isUnitType(init.type) @@ -5486,7 +5496,7 @@ export function lowerVarDecl( let settledType: IrType = type; if ( !isLet && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !hasJsTypeAnnotation(decl) && decl.initializer && ts.isNewExpression(decl.initializer) @@ -5758,7 +5768,7 @@ function jsObjectBindingExpands( decl.type || !decl.initializer || !ts.isIdentifier(decl.name) || - !isJsSourceFile(decl.getSourceFile()) + !isJsSourceFile(ts.getSourceFileOfNode(decl)) ) return false; let initializer = decl.initializer; @@ -5868,7 +5878,10 @@ function jsObjectBindingExpands( export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean { const statement = decl.parent?.parent; return ( - !!statement && /@type\b/.test(decl.getSourceFile().text.slice(statement.pos, decl.getStart())) + !!statement && + /@type\b/.test( + ts.getSourceFileOfNode(decl).text.slice(statement.pos, ts.tokenStartOfNode(decl)), + ) ); } @@ -5876,7 +5889,7 @@ export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean { * an any-valued callback. Keep those bindings open, including captured writes. */ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { if ( - !isJsSourceFile(decl.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(decl)) || decl.type || hasJsTypeAnnotation(decl) || !ts.isIdentifier(decl.name) @@ -6378,7 +6391,7 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt } if (accessor && errorBase) return { kind: "exprStmt", expr: obj, loc }; } - if (isJsSourceFile(expr.getSourceFile()) && obj.type.kind === "object") { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && obj.type.kind === "object") { const info = lowerer.classes.get(obj.type.className); const keyType = ts.isElementAccessExpression(target) ? lowerer.typeOf(target.argumentExpression) @@ -6399,7 +6412,7 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt } } if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && obj.type.kind === "func" && lowerer.dynConvertible(obj.type) ) { @@ -6510,7 +6523,7 @@ function lowerCjsExportStatement( const symbol = lowerer.checker.getSymbolAtLocation(nameNode) ?? (ts.isIdentifier(nameNode) || ts.isPrivateIdentifier(nameNode) - ? lowerer.cjsModuleExportSymbol(cjs.expr.getSourceFile(), nameNode.text) + ? lowerer.cjsModuleExportSymbol(ts.getSourceFileOfNode(cjs.expr), nameNode.text) : undefined); const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; if (!g) { @@ -6585,7 +6598,7 @@ function lowerCjsExportStatement( // later real assignment keep the ordinary path (the export IS // undefined then). if (cjsVoidInitPreambleReals(cjs) !== null) return { kind: "block", body: [], loc }; - const sf = cjs.expr.getSourceFile(); + const sf = ts.getSourceFileOfNode(cjs.expr); let root: IrGlobal | undefined; for (const stmt of sf.statements.nodes) { const whole = cjsExportAssignmentOf(stmt); @@ -6648,7 +6661,7 @@ function lowerCjsExportStatement( // were already evaluated by the const's own declaration, so this // statement may not RE-evaluate anything: expression-valued entries // (which the direct-table lowering assigns here) keep a fence. - tableObj = cjsExportTargetLiteral(cjs.expr.right, cjs.expr.getSourceFile()); + tableObj = cjsExportTargetLiteral(cjs.expr.right, ts.getSourceFileOfNode(cjs.expr)); resolvedTarget = tableObj !== null; if (!tableObj) { const single = lowerCjsSingleValueExport(lowerer, cjs.expr, loc); @@ -6710,7 +6723,7 @@ function lowerCjsExportStatement( lowerer.rejectThisInObjectMethod(method.body); const symbol = lowerer.checker.getSymbolAtLocation(method.name) ?? - lowerer.cjsModuleExportSymbol(cjs.expr.getSourceFile(), method.name.text); + lowerer.cjsModuleExportSymbol(ts.getSourceFileOfNode(cjs.expr), method.name.text); const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; if (g) { const fn = lowerer.lowerLambda(method); @@ -6985,7 +6998,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // global — the expression path owns it (lowerCreateRequireCall). createRequireCalleeFileOf(lowerer, expr.expression) === null ) { - const sf = expr.getSourceFile(); + const sf = ts.getSourceFileOfNode(expr); const spec = requireSpecOf(expr)!; if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, expr, false); // A bare require of a package NOTHING INSTALLED resolves, in ANY @@ -7031,7 +7044,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // environment-dependent branch); the static export table has one shape. if ( !topLevelJs && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.EqualsToken && (isCjsModuleExports(lowerer, expr.left) || isCjsExportsIdentifier(lowerer, expr.left)) @@ -7535,7 +7548,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm if ( recv && (recv.type.kind === "dyn" || - (isJsSourceFile(expr.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && recv.type.kind === "union" && lowerer.dynConvertible(recv.type))) ) { @@ -7565,7 +7578,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // through the checked-dynamic node and keep the dynamic write, as // do JavaScript files, whose reads use that node too. if ( - !isJsSourceFile(expr.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && recv?.type.kind === "object" && lowerer.isErrorRootedClass(recv.type.className) && expr.left.name.text === "code" @@ -7838,8 +7851,8 @@ function lowerDiscardedLogical(lowerer: Lowerer, expr: ts.BinaryExpression): IrS // picks another lowering, exactly as before. if (!(e instanceof PoisonError) || lowerer.diagSink !== null) throw e; const added = lowerer.diags.slice(diagsBefore); - const file = expr.getSourceFile().fileName; - const start = expr.getStart(); + const file = ts.getSourceFileOfNode(expr).fileName; + const start = ts.tokenStartOfNode(expr); const local = added.length > 0 && added.every( @@ -8110,7 +8123,7 @@ function destructuringAssignInto( const patternEmpty = ts.isObjectLiteralExpression(target) ? target.properties.nodes.length === 0 : target.elements.nodes.length === 0; - if (!patternEmpty && rhs !== null && isJsSourceFile(rhs.getSourceFile())) { + if (!patternEmpty && rhs !== null && isJsSourceFile(ts.getSourceFileOfNode(rhs))) { const globalName = stdlibGlobalNameOf(lowerer, rhs); if (globalName !== null) { lowerer.unsupported( @@ -10905,7 +10918,7 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt { while (root?.base) root = root.base; if ( info?.decl && - isJsSourceFile(info.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && !info.def.runtime && !root?.def.runtime && !info.builtinError && diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 51d1e0377..b9add95a6 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1475,7 +1475,7 @@ export function lowerStreamStaticCall( member === "toWeb" && cls === "%Readable" && call.arguments.nodes.length === 2 && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && ts.isObjectLiteralExpression(call.arguments.nodes[1]!) ) { for (const p of call.arguments.nodes[1]!.properties.nodes) { @@ -1509,7 +1509,7 @@ export function lowerStreamStaticCall( ); } const type: IrType = { kind: "object", className: "%Readable" }; - const js = isJsSourceFile(call.getSourceFile()); + const js = isJsSourceFile(ts.getSourceFileOfNode(call)); const srcNode = args[0]; const optsNode = args[1]; const srcT = srcNode ? lowerer.mapTypeOf(lowerer.typeOf(srcNode)) : null; @@ -2127,7 +2127,7 @@ function lowerStreamArg( loc: locOf(node), }); }; - if (isJsSourceFile(node.getSourceFile()) && lowerer.dynConvertible(v.type)) { + if (isJsSourceFile(ts.getSourceFileOfNode(node)) && lowerer.dynConvertible(v.type)) { if (what === "pipeline") { // Strings and arrays are iterables — VALID pipeline stages in // Node — and a record carrying a readable/writable member may be @@ -3141,7 +3141,7 @@ function lowerOperatorCallback( if (v.type.kind === "dyn") return v; if ( v.type.kind !== "func" && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && lowerer.dynConvertible(v.type) ) { // JavaScript passes any value; Node's validateFunction throws. diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index 32870c2cf..391a77245 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -86,8 +86,8 @@ function requireStatementPosition(lowerer: Lowerer, call: ts.CallExpression, wha * reports the `test` property's column), at the call for plain ones. */ function atStringOf(expr: ts.CallExpression): string { const target = ts.isPropertyAccessExpression(expr.expression) ? expr.expression.name : expr; - const sf = expr.getSourceFile(); - const pos = sf.getLineAndCharacterOfPosition(target.getStart()); + const sf = ts.getSourceFileOfNode(expr); + const pos = sf.getLineAndCharacterOfPosition(ts.tokenStartOfNode(target)); return `${sf.fileName}:${pos.line + 1}:${pos.character + 1}`; } diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 22983fbb4..6b6a1b5d1 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -420,7 +420,7 @@ function lowerTimerCall( lowerer.noLowering(`clearTimeout with ${expr.arguments.nodes.length} arguments`, expr); } let handle = lowerer.lowerExpr(expr.arguments.nodes[0]!); - if (handle.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + if (handle.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const stored = lowerer.declareHiddenLocal("%timer.clear", DYN); const ref = varRef(stored.id, DYN, loc); handle = { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a5f376fc0..e2236f592 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1349,7 +1349,7 @@ export function uncheckedOverloadHandleCall( * `never` at the ROOT stays out (`for (const v of [])`'s loop var — the * dead read the f64 mapping is FOR). */ export function neverTaintedJsType(lowerer: Lowerer, node: ts.Node, t: ts.Type): boolean { - if (!isJsSourceFile(node.getSourceFile())) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(node))) return false; const walk = (x: ts.Type, depth: number): boolean => { if (depth === 0) return false; if (x.isUnionType()) return ts.constituentTypes(x).some((a) => walk(a, depth - 1)); @@ -1379,7 +1379,7 @@ export function ladderFenceExpr( const loc = locOf(node); const d = noLoweringDiag(surface, loc, hint); lowerer.runtimeFences.push(d); - const sf = node.getSourceFile(); + const sf = ts.getSourceFileOfNode(node); const pos = ts.getLineAndCharacterOfPosition(sf, loc.start); return { kind: "strLit", @@ -1391,7 +1391,7 @@ export function ladderFenceExpr( export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): IrType | null { if (t.flags & ts.TypeFlags.Void) return null; - if (!isJsSourceFile(node.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(node))) { if (t.flags & ts.TypeFlags.Any) return DYN; const symbol = t.getSymbol(); const args = @@ -1406,16 +1406,17 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir ts.TypeFlags.Any) !== 0; if ( - symbol?.name === "Promise" && + symbol !== undefined && + ts.symbolName(symbol) === "Promise" && args[0] && (args[0].flags & ts.TypeFlags.Any || anyArray(args[0])) ) - return symbol.name === "Promise" && anyArray(args[0]) + return ts.symbolName(symbol) === "Promise" && anyArray(args[0]) ? { kind: "promise", inner: DYN } : jsFallbackPromiseType(lowerer, t); if ( symbol && - ["Generator", "AsyncGenerator", "IterableIterator"].includes(symbol.name) && + ["Generator", "AsyncGenerator", "IterableIterator"].includes(ts.symbolName(symbol)) && args.some((arg) => arg.flags & ts.TypeFlags.Any) ) return jsFallbackGeneratorType(lowerer, t); @@ -1437,7 +1438,8 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir if (generator) return generator; const symbol = t.getSymbol(); if ( - (symbol?.name === "Map" || symbol?.name === "ReadonlyMap") && + ((symbol !== undefined && ts.symbolName(symbol) === "Map") || + (symbol !== undefined && ts.symbolName(symbol) === "ReadonlyMap")) && lowerer.isStdlibSymbol(symbol) ) { const args = lowerer.checker.getTypeArguments(t as ts.TypeReference); @@ -1526,7 +1528,12 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir * has no fixed shape. The settled value crosses the checked native boundary. */ function jsFallbackPromiseType(lowerer: Lowerer, t: ts.Type): IrType | null { const symbol = t.getSymbol(); - if (symbol?.name !== "Promise" || !lowerer.isStdlibSymbol(symbol)) return null; + if ( + symbol === undefined || + ts.symbolName(symbol) !== "Promise" || + !lowerer.isStdlibSymbol(symbol) + ) + return null; const inner = lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]; return inner ? { kind: "promise", inner: lowerer.mapTypeOf(inner) ?? DYN } : null; } @@ -1537,7 +1544,7 @@ function jsFallbackGeneratorType(lowerer: Lowerer, t: ts.Type): IrType | null { const symbol = t.getSymbol(); if ( !symbol || - !["Generator", "AsyncGenerator", "IterableIterator"].includes(symbol.name) || + !["Generator", "AsyncGenerator", "IterableIterator"].includes(ts.symbolName(symbol)) || !lowerer.isStdlibSymbol(symbol) ) return null; @@ -1545,7 +1552,7 @@ function jsFallbackGeneratorType(lowerer: Lowerer, t: ts.Type): IrType | null { const unit = ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never; return { kind: "generator", - ...(symbol.name === "AsyncGenerator" ? { async: true as const } : {}), + ...(ts.symbolName(symbol) === "AsyncGenerator" ? { async: true as const } : {}), yieldT: yielded && yielded.flags & ts.TypeFlags.Never ? VOID : DYN, retT: !returned || returned.flags & unit ? VOID : DYN, nextT: !resumed || resumed.flags & unit ? UNDEFINED_T : DYN, @@ -1579,7 +1586,7 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir const sigDecl = sig.declaration; const jsUnitReturn = sigDecl !== undefined && - isJsSourceFile(sigDecl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && (retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0; const ret: IrType = jsUnitReturn ? DYN @@ -1613,7 +1620,7 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null { ts.isFunctionExpression(decl) || ts.isArrowFunction(decl) ) || - !isJsSourceFile(decl.getSourceFile()) + !isJsSourceFile(ts.getSourceFileOfNode(decl)) ) return null; const hasRest = decl.parameters.nodes.some((p) => p.dotDotDotToken !== undefined); @@ -1621,7 +1628,8 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null { !hasRest && (ts.isArrowFunction(decl) || !bodyReadsArguments(decl) || - (decl.parameters.nodes.length > 0 && !isNodeEsmFile(decl.getSourceFile(), lowerer.program))) + (decl.parameters.nodes.length > 0 && + !isNodeEsmFile(ts.getSourceFileOfNode(decl), lowerer.program))) ) return null; const shapes = paramShapes(lowerer, decl.parameters.nodes); @@ -2265,7 +2273,7 @@ export class Lowerer { return ( sym !== undefined && sym.declarations.some((d) => { - if (!isJsSourceFile(d.getSourceFile())) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(d))) return false; // The pattern itself, or the `= {}` default of a pattern parameter. const pattern = ts.isObjectLiteralExpression(d) && @@ -2381,7 +2389,7 @@ export class Lowerer { // A checked JavaScript indexed read can already be boxed. The checker // calls the element present, but that inference is not a runtime guard. if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && value.kind === "dynCheck" && value.value.type.kind === "dyn" ) @@ -3305,7 +3313,8 @@ export class Lowerer { if (this.localClassInstantiations.some((entry) => entry.owner === parent)) break; if ( parent.typeParameters?.nodes.length || - (implicitMonoFile(parent.getSourceFile()) && monomorphizedImplicit(this, parent)) + (implicitMonoFile(ts.getSourceFileOfNode(parent)) && + monomorphizedImplicit(this, parent)) ) return null; } @@ -3331,15 +3340,15 @@ export class Lowerer { // lowering, long after the constructor completes. isProgramFile: (sf) => this.fileTag.has(sf), lifetimeRegionClass: (decl) => - isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles) + isExperimentalDeclarationFile(ts.getSourceFileOfNode(decl), this.experimentalFiles) ? lifetimeRegionType(this) : null, internTableClass: (decl) => - isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles) + isExperimentalDeclarationFile(ts.getSourceFileOfNode(decl), this.experimentalFiles) ? internTableType(this) : null, threadSlotsClass: (decl) => - isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles) + isExperimentalDeclarationFile(ts.getSourceFileOfNode(decl), this.experimentalFiles) ? threadSlotsType(this) : null, moduleNamespaceId: (type) => this.moduleNamespaceIdOfType(type), @@ -3405,9 +3414,10 @@ export class Lowerer { const weak = (part: ts.Type): boolean => { const symbol = part.getSymbol(); return ( - (symbol?.name === "WeakMap" || symbol?.name === "WeakSet") && + ((symbol !== undefined && ts.symbolName(symbol) === "WeakMap") || + (symbol !== undefined && ts.symbolName(symbol) === "WeakSet")) && symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && this.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && this.isStdlibFile(ts.getSourceFileOfNode(d)), ) ); }; @@ -3516,9 +3526,12 @@ export class Lowerer { } return false; })() - ? this.qualify(decl.getSourceFile(), `%cx${decl.getStart()}.${decl.name?.text ?? ""}`) + ? this.qualify( + ts.getSourceFileOfNode(decl), + `%cx${ts.tokenStartOfNode(decl)}.${decl.name?.text ?? ""}`, + ) : this.qualify( - decl.getSourceFile(), + ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"), ); for (let index = this.localClassInstantiations.length - 1; index >= 0; index--) { @@ -3762,7 +3775,10 @@ export class Lowerer { return null; if (this.externalTypes.has(importDecl.moduleSpecifier.text)) return importDecl.moduleSpecifier.text; - const dep = this.moduleSourceFileOf(importDecl.getSourceFile(), importDecl.moduleSpecifier); + const dep = this.moduleSourceFileOf( + ts.getSourceFileOfNode(importDecl), + importDecl.moduleSpecifier, + ); return dep !== null && !dep.isDeclarationFile ? this.externalTypeSpecifierOfModuleExport(dep, member, new Set(), new Set()) : null; @@ -3810,7 +3826,7 @@ export class Lowerer { // A namespace OBJECT from a project module is not wholly external; // property accesses resolve their selected member separately above. if (importedName === null) return null; - const dep = this.moduleSourceFileOf(decl.getSourceFile(), specNode); + const dep = this.moduleSourceFileOf(ts.getSourceFileOfNode(decl), specNode); return dep !== null && !dep.isDeclarationFile ? this.externalTypeSpecifierOfModuleExport(dep, importedName, seenSymbols, seenExports) : null; @@ -3818,7 +3834,7 @@ export class Lowerer { for (const decl of declarations) { const owners = this.externalTypeSpecifiersByFile.get( - tsgoPath(resolve(decl.getSourceFile().fileName)), + tsgoPath(resolve(ts.getSourceFileOfNode(decl).fileName)), ); if (owners !== undefined && owners.length > 0) return owners[0]!; } @@ -3863,7 +3879,7 @@ export class Lowerer { } if (d === undefined || spec === undefined || !ts.isStringLiteral(spec) || name !== "default") return null; - const dep = resolveImport(this.program, d.getSourceFile(), spec.text); + const dep = resolveImport(this.program, ts.getSourceFileOfNode(d), spec.text); if (!dep) return null; sym = defaultExportSymbolOf(this, dep) ?? undefined; } @@ -3979,8 +3995,8 @@ export class Lowerer { // Project aliases/self-references resolve through the same entry point as // relative imports; an opted-in --npm-static package is the fallback. const dep = - resolveImport(this.program, importDecl.getSourceFile(), spec) ?? - npmStaticDepSf7(this.program, importDecl.getSourceFile(), spec); + resolveImport(this.program, ts.getSourceFileOfNode(importDecl), spec) ?? + npmStaticDepSf7(this.program, ts.getSourceFileOfNode(importDecl), spec); if (!dep || !isJsSourceFile(dep) || isNodeEsmFile(dep, this.program)) return null; return dep; } @@ -4020,8 +4036,8 @@ export class Lowerer { if (directSpec !== null) { if (isJsonRequireDecl(this, decl)) return null; return ( - resolveImport(this.program, decl.getSourceFile(), directSpec, "require") ?? - npmStaticDepSf7(this.program, decl.getSourceFile(), directSpec, "require") + resolveImport(this.program, ts.getSourceFileOfNode(decl), directSpec, "require") ?? + npmStaticDepSf7(this.program, ts.getSourceFileOfNode(decl), directSpec, "require") ); } return createRequireProgramModuleOf(this, decl.initializer)?.dep ?? null; @@ -4361,7 +4377,7 @@ export class Lowerer { * report on the next tick. Later loads are cache hits either way. */ punycodeRequireWarning(node: ts.Node): IrStmt { const loc = locOf(node); - const fromDependency = /[\\/]node_modules[\\/]/.test(node.getSourceFile().fileName); + const fromDependency = /[\\/]node_modules[\\/]/.test(ts.getSourceFileOfNode(node).fileName); return { kind: "exprStmt", expr: { @@ -4432,8 +4448,8 @@ export class Lowerer { // those globals stay uninitialized: the dep's module body would never // run. const dep = - resolveImport(this.program, node.getSourceFile(), spec, "require") ?? - npmStaticDepSf7(this.program, node.getSourceFile(), spec, "require"); + resolveImport(this.program, ts.getSourceFileOfNode(node), spec, "require") ?? + npmStaticDepSf7(this.program, ts.getSourceFileOfNode(node), spec, "require"); if (!dep || dep.fileName.endsWith(".json")) return null; if (this.asyncInitFiles.has(dep)) { this.unsupported( @@ -4450,7 +4466,7 @@ export class Lowerer { expr: { kind: "call", callee: initName, args: [], type: VOID, loc }, loc, }; - const parentModule = this.cjsModuleIdOf.get(node.getSourceFile()); + const parentModule = this.cjsModuleIdOf.get(ts.getSourceFileOfNode(node)); const childModule = this.cjsModuleIdOf.get(dep); if (parentModule === undefined || childModule === undefined) return init; return { @@ -5211,7 +5227,7 @@ export class Lowerer { const decl = symbol.valueDeclaration; if (!decl || !ts.isPropertyDeclaration(decl) || !ts.isIdentifier(decl.name)) return false; if (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Static) return false; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (isJsSourceFile(sf) || sf.isDeclarationFile) return false; const declared = typeOrNull(decl.name); const mapped = declared ? this.mapTypeOf(declared) : null; @@ -5301,7 +5317,8 @@ export class Lowerer { const initSymbol = initType.getAliasSymbol() ?? initType.getSymbol(); const matchResult = initSymbol !== undefined && - (initSymbol.name === "RegExpExecArray" || initSymbol.name === "RegExpMatchArray"); + (ts.symbolName(initSymbol) === "RegExpExecArray" || + ts.symbolName(initSymbol) === "RegExpMatchArray"); if (arrayType?.kind !== "array" || matchResult) return scanRecordPattern(name, init); const positions = name.elements.nodes.filter((el) => { const key = el.propertyName; @@ -5477,7 +5494,7 @@ export class Lowerer { }; for (const [symbol, signature] of signatureBySymbol) { const declaration = functionDeclBySymbol.get(symbol); - if (!declaration || !isJsSourceFile(declaration.getSourceFile())) continue; + if (!declaration || !isJsSourceFile(ts.getSourceFileOfNode(declaration))) continue; const checked = this.checker.getSignatureFromDeclaration(declaration); const checkedReturn = checked ? this.checker.getReturnTypeOfSignature(checked) : null; // Checker-known optional results already narrow at each use. Only @@ -6319,7 +6336,7 @@ export class Lowerer { const callee = functionDeclBySymbol.get(symbol); if ( callee && - isJsSourceFile(callee.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(callee)) && !node.arguments.nodes.some(ts.isSpreadElement) ) { for (let i = 0; i < sig.params.length; i++) { @@ -6535,7 +6552,7 @@ export class Lowerer { for (const [symbol, decl] of functionDeclBySymbol) if (scanReturns(symbol, decl)) changed = true; const noteGenericReturn = (symbol: ts.Symbol, decl: ts.FunctionLikeDeclaration): void => { - if (optionalReturns.has(symbol) || isJsSourceFile(decl.getSourceFile())) return; + if (optionalReturns.has(symbol) || isJsSourceFile(ts.getSourceFileOfNode(decl))) return; if (!returnsOf(decl).some(genericMayBeOptional)) return; optionalReturns.add(symbol); this.runtimeOptionalGenericReturns.add(decl); @@ -6812,7 +6829,7 @@ export class Lowerer { if ( global.type.kind !== "func" || !ts.isBinaryExpression(declaration) || - !isJsSourceFile(declaration.getSourceFile()) + !isJsSourceFile(ts.getSourceFileOfNode(declaration)) ) continue; const value = peel(declaration.right); @@ -7288,7 +7305,7 @@ export class Lowerer { if (n === 0) return; this.stats.statementsTotal += n; this.stats.statementsFailed += n; - const file = decl.getSourceFile().fileName; + const file = ts.getSourceFileOfNode(decl).fileName; for (let i = 0; i < n; i++) { this.bumpFileStat(file, "total"); this.bumpFileStat(file, "failed"); @@ -7907,8 +7924,8 @@ export class Lowerer { "this statement uses a construct with no static lowering"; const messageLoc = first?.loc ?? loc; const source = first - ? (this.program.getSourceFile(messageLoc.file) ?? node.getSourceFile()) - : node.getSourceFile(); + ? (this.program.getSourceFile(messageLoc.file) ?? ts.getSourceFileOfNode(node)) + : ts.getSourceFileOfNode(node); const pos = ts.getLineAndCharacterOfPosition(source, messageLoc.start); return { kind: "runtimeFence", @@ -8027,10 +8044,12 @@ export class Lowerer { // here; constructor objects such as Headers still can. if ( typeSym && - (FETCH_AMBIENT_TYPES as readonly string[]).includes(typeSym.name) && + (FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(typeSym)) && this.isStdlibSymbol(typeSym) ) { - this.pushDiag(unsupportedNativeApiDiag(`a value of type '${typeSym.name}'`, locOf(node))); + this.pushDiag( + unsupportedNativeApiDiag(`a value of type '${ts.symbolName(typeSym)}'`, locOf(node)), + ); throw new PoisonError(); } // ENUM OBJECTS (`typeof e` — a numeric enum's type carries the @@ -8044,7 +8063,7 @@ export class Lowerer { unsupportedDiag( "SC1090", locOf(node), - `enum objects as values ('${typeSym.name}' — member reads like '${typeSym.name}.X' compile to constants; the object itself has no runtime representation)`, + `enum objects as values ('${ts.symbolName(typeSym)}' — member reads like '${ts.symbolName(typeSym)}.X' compile to constants; the object itself has no runtime representation)`, ), ); throw new PoisonError(); @@ -8074,7 +8093,7 @@ export class Lowerer { ownSym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - this.isStdlibFile(d.getSourceFile()), + this.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node))); @@ -8120,7 +8139,7 @@ export class Lowerer { stdlibOwnSym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - this.isStdlibFile(d.getSourceFile()), + this.isStdlibFile(ts.getSourceFileOfNode(d)), ); // OVERLOADED call signatures (SC2007) — after the generic branch, so a // generic overload set keeps the monomorphization story: a value of a @@ -8182,7 +8201,7 @@ export class Lowerer { noLoweringDiag( this.checker.typeToString(type), locOf(node), - typeHints[stdlibOwnSym?.name ?? ""], + typeHints[ts.symbolNameOf(stdlibOwnSym) ?? ""], ), ); throw new PoisonError(); @@ -8259,7 +8278,7 @@ export class Lowerer { } typeOf(node: ts.Node): ts.Type { - if (ts.isPropertyAccessExpression(node) && isJsSourceFile(node.getSourceFile())) { + if (ts.isPropertyAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) { const receiver = node.expression.kind === ts.SyntaxKind.ThisKeyword && this.fnStack.length > 0 ? this.resolveThis()?.type @@ -8280,7 +8299,7 @@ export class Lowerer { } // A symbol-keyed field read through a CommonJS key-table key reads as // the field's layout type (tableKeyFieldType). - if (ts.isElementAccessExpression(node) && isJsSourceFile(node.getSourceFile())) { + if (ts.isElementAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) { const field = tableKeyFieldType(this, node); if (field) { if (field.kind === "f64") return this.checker.getNumberType(); @@ -8294,7 +8313,7 @@ export class Lowerer { // into native field loads when the method is borrowed by another object. if ( node.kind === ts.SyntaxKind.ThisKeyword && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && this.fnStack.length > 0 && this.resolveThis()?.type.kind === "dyn" ) @@ -8568,7 +8587,7 @@ export class Lowerer { // locals at null/undefined despite later writes. Keep mutable native // storage instead of a slot restricted to the initializer's unit value. if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 ) return DYN; @@ -9599,7 +9618,9 @@ export class Lowerer { : []; const errorOptions = parts.some((part) => { const sym = part.getAliasSymbol() ?? part.getSymbol(); - return sym?.name === "ErrorOptions" && this.isStdlibSymbol(sym); + return ( + sym !== undefined && ts.symbolName(sym) === "ErrorOptions" && this.isStdlibSymbol(sym) + ); }); if (errorOptions) { const actual = this.typeOf(node); @@ -9635,7 +9656,7 @@ export class Lowerer { let literal = node; while (ts.isParenthesizedExpression(literal)) literal = literal.expression; if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && !ts.isArrayLiteralExpression(literal) && e.kind === "dynFrom" && e.value.type.kind === "array" @@ -9667,7 +9688,7 @@ export class Lowerer { if ( e.type.kind === "union" && !typeEquals(e.type, expected) && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && this.dynConvertible(e.type) ) { const sourceArms = this.unions.get(e.type.unionId)?.arms ?? []; @@ -9722,7 +9743,7 @@ export class Lowerer { !ts.isIdentifier(d.name) || !ts.isVariableDeclarationList(d.parent) || !ts.isVariableStatement(d.parent.parent) || - d.getSourceFile().isDeclarationFile || + ts.getSourceFileOfNode(d).isDeclarationFile || (ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient) !== 0 ) { return null; @@ -9941,11 +9962,11 @@ export class Lowerer { const builtinOption = (name: string): boolean => parts.some((part) => { const sym = part.getAliasSymbol() ?? part.getSymbol(); - return sym?.name === name && this.isStdlibSymbol(sym); + return ts.symbolNameOf(sym) === name && this.isStdlibSymbol(sym); }); if (ts.isObjectLiteralExpression(x)) { if ( - isJsSourceFile(x.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(x)) || builtinOption("RequestInit") || builtinOption("ErrorOptions") || builtinOption("SpawnOptions") || @@ -10028,7 +10049,7 @@ export class Lowerer { coerceExistingInto(node: ts.Expression, e: IrExpr, expected: IrType): IrExpr { if ( (expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) || - (expected.kind === "dyn" && isJsSourceFile(node.getSourceFile())) + (expected.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(node))) ) { // A destination that accepts undefined must retain an unchecked // read's storage value even when the checker still calls it present. @@ -10051,7 +10072,8 @@ export class Lowerer { return null; const symbol = this.resolveValueSymbol(value.expression); if (!symbol || !this.isStdlibSymbol(symbol)) return null; - const kind = symbol.name === "Map" ? "map" : symbol.name === "Set" ? "set" : null; + const kind = + ts.symbolName(symbol) === "Map" ? "map" : ts.symbolName(symbol) === "Set" ? "set" : null; if (!kind) return null; const arms = expected.kind === "union" ? (this.unions.get(expected.unionId)?.arms ?? []) : [expected]; @@ -10176,7 +10198,7 @@ export class Lowerer { while (ts.isParenthesizedExpression(literal)) literal = literal.expression; return ( expected.kind === "dyn" && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isObjectLiteralExpression(literal) ); } @@ -10642,7 +10664,7 @@ export class Lowerer { } } } - if (!isJsSourceFile(decl.getSourceFile())) continue; + if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) continue; for (const member of decl.members.nodes) { if (ts.isConstructorDeclaration(member)) { for (const param of member.parameters.nodes) { @@ -10675,7 +10697,7 @@ export class Lowerer { } // npm-static implicit-any classification resolves body parameter // references while collecting the method's shape. - if (implicitMonoFile(decl.getSourceFile()) && member.body) { + if (implicitMonoFile(ts.getSourceFileOfNode(decl)) && member.body) { symbolRoots.push(member.body); } } @@ -12874,14 +12896,14 @@ export class Lowerer { // node:stream/web's exports are the web stream globals themselves: in // JavaScript sources the same identity token as the global spelling // (`require("stream/web").ReadableStream === globalThis.ReadableStream`). - if (bi.module === "stream/web" && isJsSourceFile(expr.getSourceFile())) { + if (bi.module === "stream/web" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { return { kind: "strLit", value: `[builtin ${bi.member}]`, type: STRING, loc }; } // A bare `fs.constants` read (not one of the baked bits above). if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc); // `stream.Readable` as a JavaScript value: the runtime's untyped // constructor, the base a constructor function inherits. - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { const classKind = builtinMemberClassKind(bi.module, bi.member); if (classKind !== null) return builtinClassValue(classKind, loc); } @@ -12889,7 +12911,7 @@ export class Lowerer { this.unsupported( "SC1090", expr, - `library functions as values (call '${expr.getText()}' directly)`, + `library functions as values (call '${ts.getTextOfNode(expr)}' directly)`, ); } this.noLowering( diff --git a/packages/compiler/src/frontend/lowering/module-member-this.ts b/packages/compiler/src/frontend/lowering/module-member-this.ts index 592136bec..8d1592b47 100644 --- a/packages/compiler/src/frontend/lowering/module-member-this.ts +++ b/packages/compiler/src/frontend/lowering/module-member-this.ts @@ -70,9 +70,9 @@ function ignoresOwnThis(lowerer: Lowerer, fn: ts.Node, bindings: Bindings, depth for (const arg of node.arguments.nodes.slice(1)) visit(arg); return; } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - fn.forEachChild(visit); + ts.forEachChild(fn, visit); return ignores; } @@ -176,7 +176,7 @@ function mayRebind(lowerer: Lowerer, fn: ts.Node, param: ts.Identifier): boolean found = true; return; } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; // The function's own parameters declare the name; their defaults can write it. const params = (fn as { parameters?: ts.NodeArray }).parameters; @@ -195,9 +195,9 @@ function returnedExpressions(fn: ts.Node): ts.Expression[] | null { const visit = (node: ts.Node): void => { if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; if (ts.isReturnStatement(node) && node.expression) result.push(node.expression); - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - body.forEachChild(visit); + ts.forEachChild(body, visit); return result; } diff --git a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts index 4270457d5..65efe5b2a 100644 --- a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts +++ b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts @@ -365,20 +365,22 @@ export class NanCodedReads { private removalReason(slot: Slot): string { const badSource = slot.sources.find((s) => s === null || this.sourceInfo(s) === null); if (badSource !== undefined) - return `source ${badSource ? badSource.getText().slice(0, 60) : ""}`; + return `source ${badSource ? ts.getTextOfNode(badSource).slice(0, 60) : ""}`; const nan = this.genuineNaN.has(slot); const badUse = slot.uses.find((u) => !this.useSafe(u) && (nan || !this.reboxConsumer(u))); - if (badUse) - return `${nan ? "nan-use" : "use"} ${transparentParent(badUse).parent?.getText().slice(0, 80) ?? "?"}`; + if (badUse) { + const parent = transparentParent(badUse).parent; + return `${nan ? "nan-use" : "use"} ${parent === undefined ? "?" : ts.getTextOfNode(parent).slice(0, 80)}`; + } return "?"; } private debugReport(reached: Set): void { const name = (slot: Slot): string => { const decl = this.host.declarationOf(slot.symbol); - const sf = decl?.getSourceFile(); - const where = sf && decl ? `${sf.fileName}:${decl.getStart()}` : "?"; - return `${slot.kind} ${slot.symbol.name} @ ${where}`; + const sf = decl === undefined ? undefined : ts.getSourceFileOfNode(decl); + const where = sf && decl ? `${sf.fileName}:${ts.tokenStartOfNode(decl)}` : "?"; + return `${slot.kind} ${ts.symbolName(slot.symbol)} @ ${where}`; }; for (const slot of reached) { const ok = this.eligible.has(slot); @@ -483,7 +485,11 @@ export class NanCodedReads { return true; } const decl = this.host.declarationOf(symbol); - if (!decl || isJsSourceFile(decl.getSourceFile()) || decl.getSourceFile().isDeclarationFile) + if ( + !decl || + isJsSourceFile(ts.getSourceFileOfNode(decl)) || + ts.getSourceFileOfNode(decl).isDeclarationFile + ) return false; let name: ts.Identifier; if (ts.isParameter(decl)) { @@ -565,7 +571,7 @@ export class NanCodedReads { !nonNegativeIntegerLiteral(decl.initializer) || !ts.isVariableDeclarationList(decl.parent) || !(decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) || - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) return false; const name = decl.name; @@ -685,7 +691,7 @@ export class NanCodedReads { !(decl.parent.flags & ts.NodeFlags.Const) || // A module binding can escape through an export. (ts.isVariableStatement(decl.parent.parent) && ts.isSourceFile(decl.parent.parent.parent)) || - isJsSourceFile(decl.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(decl)) || this.host.bytesElemOf(decl.name) === null ) return null; @@ -755,7 +761,7 @@ export class NanCodedReads { if (!decl || (!ts.isFunctionDeclaration(decl) && !ts.isMethodDeclaration(decl))) return null; if (!decl.body || decl.asteriskToken || decl.typeParameters?.nodes || !decl.name) return null; if (!ts.isIdentifier(decl.name)) return null; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (isJsSourceFile(sf) || sf.isDeclarationFile) return null; const flags = ts.getCombinedModifierFlags(decl); if (flags & (ts.ModifierFlags.Async | ts.ModifierFlags.Abstract | ts.ModifierFlags.Ambient)) @@ -831,7 +837,7 @@ export class NanCodedReads { private scanBindingSlot(symbol: ts.Symbol): BindingSlot | null { const decl = this.host.declarationOf(symbol); if (!decl) return null; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (isJsSourceFile(sf) || sf.isDeclarationFile) return null; let nameNode: ts.Identifier; const slot: BindingSlot = { @@ -1362,7 +1368,7 @@ export class NanCodedReads { decl.initializer && ts.isVariableDeclarationList(decl.parent) && decl.parent.flags & ts.NodeFlags.Const && - !isJsSourceFile(decl.getSourceFile()) + !isJsSourceFile(ts.getSourceFileOfNode(decl)) ) return this.plainNumber(decl.initializer, depth + 1); return null; diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index 01801470d..1c991c6ea 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -191,7 +191,8 @@ export class NarrowingStability { const checker = this.lowerer.checker; const signature = checker.getResolvedSignature(call); const declaration = signature ? signature.declaration : undefined; - if (!declaration || !this.lowerer.isStdlibFile(declaration.getSourceFile())) return true; + if (!declaration || !this.lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration))) + return true; const callee = skipOuter(call.expression); if ( ts.isPropertyAccessExpression(callee) && @@ -287,11 +288,11 @@ export class NarrowingStability { const last = chain.links[chain.links.length - 1]; const refKey = last ? `.${last.name}` : (chain.root as ts.Identifier).text; const candidates = facts.refs.get(refKey) ?? []; - const useStart = node.getStart(); + const useStart = ts.tokenStartOfNode(node); let start = -1; for (const candidate of candidates) { if (candidate === node) continue; - const candidateStart = candidate.getStart(); + const candidateStart = ts.tokenStartOfNode(candidate); if (candidateStart >= useStart) break; if (referenceKey(candidate) !== chain.key) continue; start = candidateStart; @@ -313,7 +314,7 @@ export class NarrowingStability { // between. let end = useStart; for (const loop of facts.loops) { - const loopStart = loop.getStart(); + const loopStart = ts.tokenStartOfNode(loop); if (loopStart >= start && loopStart <= useStart && loop.end >= node.end) end = Math.max(end, loop.end); } @@ -376,7 +377,9 @@ export class NarrowingStability { if (decl !== undefined && ts.isVariableDeclaration(decl)) { if (decl.initializer !== undefined && this.mayBeStale(decl.initializer)) result = true; else { - for (const write of this.writesIn(decl.getSourceFile()).get(symbol.name) ?? []) { + for (const write of this.writesIn(ts.getSourceFileOfNode(decl)).get( + ts.symbolName(symbol), + ) ?? []) { const assignment = write.parent; if ( ts.isBinaryExpression(assignment) && @@ -430,7 +433,7 @@ export class NarrowingStability { ts.isParameter(decl)); if (decl && mutableBinding) { const home = flowBoundary(decl); - const writes = this.writesIn(decl.getSourceFile()).get(symbol.name) ?? []; + const writes = this.writesIn(ts.getSourceFileOfNode(decl)).get(ts.symbolName(symbol)) ?? []; for (const write of writes) { if (flowBoundary(write) === home) continue; if (this.lowerer.resolveValueSymbol(write) === symbol) { @@ -497,7 +500,7 @@ export class NarrowingStability { if (name === "toString" || name === "valueOf" || name === "toJSON") facts.conversion = true; if (ts.isComputedPropertyName(node.name)) { - const text = node.name.expression.getText(); + const text = ts.getTextOfNode(node.name.expression); if (text.includes("toPrimitive")) facts.conversion = true; if (text.includes("iterator") || text.includes("asyncIterator")) facts.iteration = true; } @@ -711,7 +714,7 @@ export class NarrowingStability { const symbol = type.getSymbol(); return ( symbol !== undefined && - BUILTIN_COLLECTIONS.has(symbol.name) && + BUILTIN_COLLECTIONS.has(ts.symbolName(symbol)) && this.lowerer.isStdlibSymbol(symbol) ); } @@ -1062,7 +1065,7 @@ function memberUse( function flowBoundary(node: ts.Node): ts.Node { let n: ts.Node | undefined = node.parent; while (n && !isFlowBoundary(n)) n = n.parent; - return n ?? node.getSourceFile(); + return n ?? ts.getSourceFileOfNode(node); } function isFlowBoundary(node: ts.Node): boolean { diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index c9ff3300d..0f932ba93 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -393,7 +393,7 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr (decl) => ts.isFunctionDeclaration(decl) && decl.body === undefined && - libraryCallbackOwnsFile(lowerer, decl.getSourceFile()), + libraryCallbackOwnsFile(lowerer, ts.getSourceFileOfNode(decl)), ); if (callbackShaped) { lowerer.pushDiag( @@ -429,13 +429,13 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr if (lowerer.libraryCallbacks) { const declarations = symbol.declarations; const programDeclarations = declarations.filter((decl) => - libraryCallbackOwnsFile(lowerer, decl.getSourceFile()), + libraryCallbackOwnsFile(lowerer, ts.getSourceFileOfNode(decl)), ); const programAmbient = programDeclarations.length > 0 && programDeclarations.every( (decl) => - decl.getSourceFile().isDeclarationFile || + ts.getSourceFileOfNode(decl).isDeclarationFile || (ts.getCombinedModifierFlags(decl as ts.Declaration) & ts.ModifierFlags.Ambient) !== 0, ); diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts index ddd8d6d56..bcb675692 100644 --- a/packages/compiler/src/frontend/lowering/object-factory-new.ts +++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts @@ -41,7 +41,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { !owner || !declaration?.body || !bindingNeverReassigned(lowerer, symbol, owner) || - !isJsSourceFile(declaration.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(declaration)) || declaration.asteriskToken || declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ) diff --git a/packages/compiler/src/frontend/lowering/private-brands.ts b/packages/compiler/src/frontend/lowering/private-brands.ts index e7b3edbd8..8507cc81a 100644 --- a/packages/compiler/src/frontend/lowering/private-brands.ts +++ b/packages/compiler/src/frontend/lowering/private-brands.ts @@ -30,7 +30,7 @@ export function perEvaluationPrivateNames(info: ClassInfo): boolean { const declaration = info.localClass ? info.decl : null; return ( !!declaration && - isJsSourceFile(declaration.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(declaration)) && declaration.members.nodes.some( (member) => member.name !== undefined && ts.isPrivateIdentifier(member.name), ) diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts index 2f84e988a..d979423b0 100644 --- a/packages/compiler/src/frontend/lowering/require-initialization.ts +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -119,7 +119,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); const bySymbol = new Map(); for (const { decl } of declared) { - const flagId = `%g.${tag}%required.${decl.getStart(sf)}`; + const flagId = `%g.${tag}%required.${ts.tokenStartOfNode(decl)}`; lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); const flag: RequireInitFlag = { flagId, names: new Map() }; for (const id of boundNames(decl.name)) { @@ -148,7 +148,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { if (owner === undefined) continue; if (boundNames(owner.decl.name).includes(id)) continue; // Code after the require runs after it, except inside functions. - if (id.getStart(sf) >= owner.decl.getEnd() && !inDeferredPosition(id)) continue; + if (ts.tokenStartOfNode(id) >= owner.decl.end && !inDeferredPosition(id)) continue; const body = enclosingBody(id); lowerer.requireEarlyReferences.push({ flag: owner.flag, diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts index d3acaff6b..f332d0f51 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts @@ -287,7 +287,7 @@ function assertsSlot(node: ts.Node, operands: Operands): boolean { return; } if ((n.flags & ts.NodeFlags.OptionalChain) !== 0) return; - n.forEachChild(visit); + ts.forEachChild(n, visit); }; if (ts.isVariableStatement(node)) { for (const declaration of node.declarationList.declarations.nodes) @@ -323,7 +323,7 @@ function unchanged(nodes: readonly ts.Node[], { array, index }: Operands): boole let hit = false; const walk = (n: ts.Node): void => { if (ts.isIdentifier(n) && roots.includes(n.text)) hit = true; - n.forEachChild(walk); + ts.forEachChild(n, walk); }; walk(e); return hit; @@ -372,7 +372,7 @@ function unchanged(nodes: readonly ts.Node[], { array, index }: Operands): boole ok = false; return; } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; for (const node of nodes) visit(node); return ok; diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts index 2fa8785c9..5ed011ce1 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts @@ -611,7 +611,7 @@ export class ArrayElementStates { if (this.host.markIterationBinding(name, known)) changed = true; return; } - name.forEachChild(mark); + ts.forEachChild(name, mark); }; if (ts.isVariableDeclarationList(initializer)) { for (const declaration of initializer.declarations.nodes) mark(declaration.name); diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts b/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts index 768932ad3..3e4605f94 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts @@ -21,7 +21,7 @@ export function reassignedBindings( else if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) { const walk = (n: ts.Node): void => { if (ts.isIdentifier(n)) mark(n); - else n.forEachChild(walk); + else ts.forEachChild(n, walk); }; walk(e); } diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts b/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts index adecfa5a9..dd1dce04d 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts @@ -378,9 +378,9 @@ function usesArguments(fn: ts.FunctionExpression): boolean { if (found) return; if (ts.isIdentifier(node) && node.text === "arguments") found = true; else if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node)) - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - fn.body?.forEachChild(visit); + if (fn.body !== undefined) ts.forEachChild(fn.body, visit); return found; } diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts b/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts index f150e314b..3363f197d 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts @@ -33,8 +33,8 @@ export class RuntimeOptionalProvenance { function describe(node: ts.Node | undefined): string { if (!node) return "?"; - const sf = node.getSourceFile(); - const start = node.getStart(); + const sf = ts.getSourceFileOfNode(node); + const start = ts.tokenStartOfNode(node); const pos = ts.getLineAndCharacterOfPosition(sf, start); const text = sf.text.slice(start, node.end).replace(/\s+/g, " "); const short = text.length > 60 ? `${text.slice(0, 57)}...` : text; diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts b/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts index f95694624..50b508a52 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts @@ -21,7 +21,7 @@ function classes(source: string): { const symbolFor = (name: string): ts.Symbol => { let symbol = symbols.get(name); if (!symbol) { - symbol = { name, declarations: [] } as unknown as ts.Symbol; + symbol = { escapedName: name, declarations: [] } as unknown as ts.Symbol; symbols.set(name, symbol); } return symbol; diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-records.ts b/packages/compiler/src/frontend/lowering/runtime-optional-records.ts index bad2523c8..18e136d69 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-records.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-records.ts @@ -351,7 +351,7 @@ export class RecordFlowClasses { private referencesOf(symbol: ts.Symbol): readonly ts.Identifier[] { let list = this.references.get(symbol); if (!list) { - list = (this.names.get(symbol.name) ?? []).filter( + list = (this.names.get(ts.symbolName(symbol)) ?? []).filter( (node) => this.host.symbolOf(node) === symbol, ); this.references.set(symbol, list); @@ -412,7 +412,7 @@ export class RecordFlowClasses { const symbol = ts.isIdentifier(base) ? this.host.symbolOf(base) : null; const target = symbol ? this.host.declarationOf(symbol) : undefined; if (!target || !ts.isClassDeclaration(target)) return true; - const file = target.getSourceFile(); + const file = ts.getSourceFileOfNode(target); if (file.isDeclarationFile || isJsFile(file)) return true; current = target; } @@ -591,7 +591,7 @@ export class RecordFlowClasses { } else { this.markOpen(slot); } - if (this.shorthandNames.has(symbol.name)) this.markOpen(slot); + if (this.shorthandNames.has(ts.symbolName(symbol))) this.markOpen(slot); for (const reference of this.referencesOf(symbol)) { if (declaration && (declaration as { name?: ts.Node }).name === reference) continue; if (!this.use(reference, slot)) this.markOpen(slot); @@ -824,9 +824,9 @@ export function returnExpressions(fn: ts.FunctionLikeDeclaration): ts.Expression } if (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node)) return; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - fn.body?.forEachChild(visit); + if (fn.body !== undefined) ts.forEachChild(fn.body, visit); return out; } @@ -865,9 +865,9 @@ function usesArguments(body: ts.Node): boolean { if (found) return; if (ts.isIdentifier(node) && node.text === "arguments") found = true; else if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node)) - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - body.forEachChild(visit); + ts.forEachChild(body, visit); return found; } diff --git a/packages/compiler/src/frontend/lowering/split-consumers.ts b/packages/compiler/src/frontend/lowering/split-consumers.ts index 9624ac0aa..8195d2792 100644 --- a/packages/compiler/src/frontend/lowering/split-consumers.ts +++ b/packages/compiler/src/frontend/lowering/split-consumers.ts @@ -45,7 +45,7 @@ export function isPrivateSplitConsumer( ) return false; - for (const use of identifiersNamed(lowerer, decl.getSourceFile(), name.text)) { + for (const use of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), name.text)) { if (use === decl.name || use === name) continue; if (lowerer.resolveValueSymbol(use) === symbol) return false; } diff --git a/packages/compiler/src/frontend/lowering/static-function-names.ts b/packages/compiler/src/frontend/lowering/static-function-names.ts index d86e55a80..8868d6745 100644 --- a/packages/compiler/src/frontend/lowering/static-function-names.ts +++ b/packages/compiler/src/frontend/lowering/static-function-names.ts @@ -21,7 +21,7 @@ export function nameReadParams(lowerer: Lowerer, info: GenericFnInfo): Map(); // JavaScript generics read function names through the checked-value // box, which carries the runtime name already. - if (isJsSourceFile(info.decl.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(info.decl))) { info.nameReadParams = out; return out; } diff --git a/packages/compiler/src/frontend/lowering/static-this.ts b/packages/compiler/src/frontend/lowering/static-this.ts index 004a72375..5c26ef0c9 100644 --- a/packages/compiler/src/frontend/lowering/static-this.ts +++ b/packages/compiler/src/frontend/lowering/static-this.ts @@ -31,10 +31,10 @@ export function rejectStaticThis( const keyword = node.kind === ts.SyntaxKind.ThisKeyword ? "this" : "super"; lowerer.unsupported("SC1090", node, message(keyword)); } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; if (includeRoot) visit(root); - else root.forEachChild(visit); + else ts.forEachChild(root, visit); } export function hasStaticThis(root: ts.Node): boolean { @@ -47,9 +47,9 @@ export function hasStaticThis(root: ts.Node): boolean { ts.isClassExpression(node) ) return; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - root.forEachChild(visit); + ts.forEachChild(root, visit); return found; } @@ -60,8 +60,8 @@ export function hasLexicalPrivateReference(root: ts.Node): boolean { const visit = (node: ts.Node): void => { if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return; if (ts.isPrivateIdentifier(node)) found = true; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - root.forEachChild(visit); + ts.forEachChild(root, visit); return found; } diff --git a/packages/compiler/src/frontend/lowering/string-index-bounds.ts b/packages/compiler/src/frontend/lowering/string-index-bounds.ts index acb41514f..6e422b231 100644 --- a/packages/compiler/src/frontend/lowering/string-index-bounds.ts +++ b/packages/compiler/src/frontend/lowering/string-index-bounds.ts @@ -73,7 +73,7 @@ function isBoundary(node: ts.Node): boolean { function containerOf(node: ts.Node): ts.Node { let n = node.parent; while (n && !isBoundary(n)) n = n.parent; - return n ?? node.getSourceFile(); + return n ?? ts.getSourceFileOfNode(node); } function isLoop(node: ts.Node): boolean { @@ -130,7 +130,7 @@ function writeOf(id: ts.Identifier): Write | null { const k = p.operatorToken.kind; if (p.left !== n || k < ts.SyntaxKind.FirstAssignment || k > ts.SyntaxKind.LastAssignment) return null; - const at = id.getStart(); + const at = ts.tokenStartOfNode(id); if (destructured) return { at, step: null, value: null }; if (k === ts.SyntaxKind.EqualsToken) return { at, step: null, value: p.right }; const amount = integerLiteral(p.right); @@ -146,15 +146,15 @@ function writeOf(id: ts.Identifier): Write | null { (p.operator !== ts.SyntaxKind.PlusPlusToken && p.operator !== ts.SyntaxKind.MinusMinusToken) ) return null; - if (destructured) return { at: id.getStart(), step: null, value: null }; + if (destructured) return { at: ts.tokenStartOfNode(id), step: null, value: null }; return { - at: id.getStart(), + at: ts.tokenStartOfNode(id), step: p.operator === ts.SyntaxKind.PlusPlusToken ? 1 : -1, value: null, }; } if (ts.isForOfStatement(p) || ts.isForInStatement(p)) - return p.initializer === n ? { at: id.getStart(), step: null, value: null } : null; + return p.initializer === n ? { at: ts.tokenStartOfNode(id), step: null, value: null } : null; if (ts.isParenthesizedExpression(p)) { n = p; continue; @@ -199,7 +199,7 @@ export class StringIndexBounds { private prove(read: ts.ElementAccessExpression): boolean { // JavaScript declarations are not checked against callers. - if (read.questionDotToken || isJsSourceFile(read.getSourceFile())) return false; + if (read.questionDotToken || isJsSourceFile(ts.getSourceFileOfNode(read))) return false; const textNode = read.expression; const indexNode = read.argumentExpression; if (!ts.isIdentifier(textNode) || !ts.isIdentifier(indexNode)) return false; @@ -268,7 +268,7 @@ export class StringIndexBounds { if (!(list.flags & ts.NodeFlags.Const)) return null; } else if (declaration.initializer) { writes.push({ - at: declaration.name.getStart(), + at: ts.tokenStartOfNode(declaration.name), step: null, value: declaration.initializer, }); @@ -280,7 +280,7 @@ export class StringIndexBounds { const container = containerOf(declaration); let escapes = false; let assigned = 0; - for (const id of this.namesIn(container).get(symbol.name) ?? []) { + for (const id of this.namesIn(container).get(ts.symbolName(symbol)) ?? []) { if (id === nameNode) continue; const write = writeOf(id); if (!write) continue; diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index be2207bcd..a884b39e1 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -3312,7 +3312,7 @@ export function isChildSurfaceMember( const decl = sym ? sym.declarations[0] : undefined; const iface = decl?.parent; if (!decl || !iface || !ts.isInterfaceDeclaration(iface)) return false; - if (lowerer.isStdlibFile(decl.getSourceFile())) return false; + if (lowerer.isStdlibFile(ts.getSourceFileOfNode(decl))) return false; return lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(iface.name))?.kind === "child"; } @@ -3322,7 +3322,9 @@ export function isChildSurfaceMember( * with lib interfaces, so a member can carry declarations from both. A * user's own declaration is in neither, so shadowing never matches. */ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined): boolean { - return !!symbol && symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())); + return ( + !!symbol && symbol.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) + ); } /** The canonical stdlib-global name `expr` denotes, or null. Three @@ -3380,20 +3382,20 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin if ( decl !== undefined && (ts.isVariableDeclaration(decl) || ts.isBindingElement(decl)) && - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) { const imported = lowerer.builtinImportOf(expr); if (imported?.module === "buffer" && imported.member === "Buffer") return "Buffer"; } return null; } - return resolved.name === "global" ? "globalThis" : resolved.name; + return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved); } if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null; const symbol = lowerer.checker.getSymbolAtLocation(expr.name); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - return symbol.name === "global" ? "globalThis" : symbol.name; + return ts.symbolName(symbol) === "global" ? "globalThis" : ts.symbolName(symbol); } return null; } @@ -3527,7 +3529,7 @@ export function nodeTypesOnlySymbol(lowerer: Lowerer, sym: ts.Symbol | null | un if (!decls || decls.length === 0) return false; let viaNode = false; for (const d of decls) { - const sf = d.getSourceFile(); + const sf = ts.getSourceFileOfNode(d); if (sf.isDeclarationFile && isNodeTypesPath(sf.fileName)) viaNode = true; else if (lowerer.isStdlibFile(sf)) return false; } diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index 0791fc6cd..56718615e 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -118,13 +118,13 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS const init = decl.initializer; if (init && (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init))) { const target = classSymbolKeyOf(lowerer, init); - if (!target || bindingWritten(lowerer, sym, decl.getSourceFile())) return null; + if (!target || bindingWritten(lowerer, sym, ts.getSourceFileOfNode(decl))) return null; const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl)); if ( bindingEarlyUse7( lowerer.program, - decl.getSourceFile(), - decl.getSourceFile().statements.nodes.indexOf(list.parent), + ts.getSourceFileOfNode(decl), + ts.getSourceFileOfNode(decl).statements.nodes.indexOf(list.parent), decl, preceding, ) !== null @@ -139,7 +139,7 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS const { registered, description } = call; // Ordinary Symbol() is a fresh identity per evaluation, so only the // module-scope declaration above can name one fixed layout slot. - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (bindingWritten(lowerer, sym, sf)) return null; const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl)); // Creating a function value does not execute its body. The general @@ -268,7 +268,7 @@ function cjsTableSymbolKeyOf( ) return null; const stmt = assign.parent; - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); if (cjsExportDiscardReason(stmt) !== null) return null; const call = literalSymbolCallOf(lowerer, decl.initializer); if (!call) return null; @@ -330,7 +330,7 @@ function tablePropertyWritten( if (node.name.text !== name) return false; const recv = node.expression; if ( - node.getSourceFile() === home && + ts.getSourceFileOfNode(node) === home && ((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv)) ) return true; @@ -381,7 +381,7 @@ function bindingWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): bo // Assignment to obj[key] changes the property, never the key's // binding. The same applies to these leaves in destructuring. if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) return "skip"; - if (!ts.isIdentifier(node) || node.text !== sym.name) return; + if (!ts.isIdentifier(node) || node.text !== ts.symbolName(sym)) return; const targetSym = ts.isShorthandPropertyAssignment(node.parent) ? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent) : lowerer.checker.getSymbolAtLocation(node); diff --git a/packages/compiler/src/frontend/lowering/typed-arguments.ts b/packages/compiler/src/frontend/lowering/typed-arguments.ts index b52a3e059..2fbd83482 100644 --- a/packages/compiler/src/frontend/lowering/typed-arguments.ts +++ b/packages/compiler/src/frontend/lowering/typed-arguments.ts @@ -71,7 +71,7 @@ export function typedArgumentsCandidate(node: ts.Node): node is ts.FunctionExpre return ( ts.isFunctionExpression(node) && node.asteriskToken === undefined && - !isJsSourceFile(node.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(node)) && node.parameters.nodes.every(isThisParameter) && bodyReadsArguments(node) ); @@ -243,7 +243,7 @@ export function onlyForwarded( lowerer.checker.getNonNullableType(lowerer.typeOf(p.parent.expression)), ); const decl = sigs.length === 1 ? sigs[0]!.declaration : undefined; - if (decl && !decl.getSourceFile().isDeclarationFile) return undefined; + if (decl && !ts.getSourceFileOfNode(decl).isDeclarationFile) return undefined; } if ( ts.isVariableDeclaration(p) && @@ -317,7 +317,7 @@ export function forwardedTupleType(lowerer: Lowerer, node: ts.Expression): IrTyp ts.isParameter(decl) && decl.dotDotDotToken !== undefined && ts.isIdentifier(decl.name) && - !isJsSourceFile(decl.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && // Checker-only probe first: an ordinary array rest never maps here. restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx) !== null ) { diff --git a/packages/compiler/src/frontend/lowering/var-hoisting.ts b/packages/compiler/src/frontend/lowering/var-hoisting.ts index c445f1751..6276ab671 100644 --- a/packages/compiler/src/frontend/lowering/var-hoisting.ts +++ b/packages/compiler/src/frontend/lowering/var-hoisting.ts @@ -97,7 +97,7 @@ function hoistingScopeOf(node: ts.Node): Scope { ) return p; } - return node.getSourceFile(); + return ts.getSourceFileOfNode(node); } function statementsOf(scope: Scope): readonly ts.Statement[] { @@ -221,7 +221,7 @@ class ScopeAnalysis { * the statement at index `settle`. `read`: `ref` reads the binding * itself, so tsc vouches for one in the scope's own code. */ settles(scope: Scope, settle: number, ref: ts.Identifier, read = true): boolean { - if (ref.getSourceFile() !== this.sf) return true; + if (ts.getSourceFileOfNode(ref) !== this.sf) return true; const statements = statementsOf(scope); let deferred: ts.Node | null = null; let top: ts.Node | null = null; diff --git a/packages/compiler/src/frontend/module-source-candidates.test.ts b/packages/compiler/src/frontend/module-source-candidates.test.ts index 4ff20decc..8fccef000 100644 --- a/packages/compiler/src/frontend/module-source-candidates.test.ts +++ b/packages/compiler/src/frontend/module-source-candidates.test.ts @@ -6,6 +6,7 @@ import * as ast from "./ts7/ast.js"; import { Ts7Api } from "./ts7/rpc-api.js"; import { Ts7Host } from "./ts7/program-host.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; +import { getTextOfNode } from "./ts7/node-functions.js"; test("module candidates preserve nested call order and belong to one program snapshot", () => { const directory = mkdtempSync(join(tmpdir(), "scriptc-module-candidates-")); @@ -24,7 +25,7 @@ test("module candidates preserve nested call order and belong to one program sna const source = first.getSourceFile(entry)!; const candidates = moduleSourceCandidates(first, source); expect(candidates.specifiers).toEqual(["first", "second", "third", "fourth"]); - expect(candidates.calls.map((call) => call.expression.getText())).toEqual([ + expect(candidates.calls.map((call) => getTextOfNode(call.expression))).toEqual([ "outer", "require", "import", diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts index c61f4a439..b625c456d 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts @@ -24,6 +24,7 @@ import * as ts from "./ts7/syntax.js"; import { helperTokens } from "./helper-tokens.js"; import { canonicalBuiltinModule } from "./builtin-modules.js"; +import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; export interface BundledChunkSplit { /** The bundle with chunk declarations blanked and calls respelled. */ @@ -366,16 +367,16 @@ function helperForm( const ret = stmts[0]; if (stmts.length !== 1 || ret === undefined || !ts.isReturnStatement(ret) || !ret.expression) return null; - const actual = helperTokens(ret.expression.getText(sf)); + const actual = helperTokens(getTextOfNode(ret.expression)); if (actual === null) return null; // The own-key reader: esbuild's alias or the 0.11 spelling. - const keysRef = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(ret.expression.getText(sf)); + const keysRef = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(getTextOfNode(ret.expression)); const keys = keysRef?.[1]; if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null; return actual === template(KEYED_RETURN, cb, mod, keys) ? "keyed" : null; } if (ts.isArrowFunction(body) && body.parameters.nodes.length === 0 && !ts.isBlock(body.body)) { - const actual = helperTokens(body.body.getText(sf)); + const actual = helperTokens(getTextOfNode(body.body)); return actual !== null && actual === template(ARROW_BODY, cb, mod, "") ? "arrow" : null; } return null; @@ -539,7 +540,7 @@ export function splitBundledChunks( const init = t.node.initializer; return ( init !== undefined && - helperTokens(init.getText(sf)) === helperTokens("Object.getOwnPropertyNames") + helperTokens(getTextOfNode(init)) === helperTokens("Object.getOwnPropertyNames") ); }; if (helperNames.length === 0) { @@ -640,8 +641,8 @@ export function splitBundledChunks( }; const forwarded = forwarderSpec(chunk); callEdits.push({ - start: call.getStart(sf), - end: call.getEnd(), + start: tokenStartOfNode(call), + end: call.end, text: `require(${JSON.stringify(forwarded ?? chunk.spec)})`, }); } @@ -656,7 +657,7 @@ export function splitBundledChunks( for (const chunk of chunks.values()) { if (forwarderSpec(chunk) !== null) { // A chunk that only re-exports a builtin module is that module. - bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" }); + bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" }); continue; } const result = chunkSource( @@ -672,7 +673,7 @@ export function splitBundledChunks( ); if (typeof result !== "string") return result; out.push({ path: chunk.path, source: result }); - bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" }); + bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" }); } return { text: applyEdits(source, 0, source.length, bundleEdits), chunks: out }; } @@ -848,8 +849,8 @@ function chunkSource( const p = r.parent; edits.push( ts.isShorthandPropertyAssignment(p) - ? { start: r.getStart(sf), end: r.getEnd(), text: `${name}: ${global}` } - : { start: r.getStart(sf), end: r.getEnd(), text: global }, + ? { start: tokenStartOfNode(r), end: r.end, text: `${name}: ${global}` } + : { start: tokenStartOfNode(r), end: r.end, text: global }, ); } continue; @@ -893,15 +894,17 @@ function chunkSource( } // Declaration order of the bundle: a helper reads only earlier ones. const ordered = [...copied].sort( - (a, b) => top.get(a)!.node.getStart(sf) - top.get(b)!.node.getStart(sf), + (a, b) => tokenStartOfNode(top.get(a)!.node) - tokenStartOfNode(top.get(b)!.node), ); for (const name of ordered) { const node = top.get(name)!.node; - header.push(ts.isFunctionDeclaration(node) ? node.getText(sf) : `var ${node.getText(sf)};`); + header.push( + ts.isFunctionDeclaration(node) ? getTextOfNode(node) : `var ${getTextOfNode(node)};`, + ); } const bodyStrict = strictPrologue(body.statements.nodes); - const text = applyEdits(sf.text, body.getStart(sf) + 1, body.getEnd() - 1, edits); + const text = applyEdits(sf.text, tokenStartOfNode(body) + 1, body.end - 1, edits); const prologue = bundleStrict || bodyStrict ? '"use strict";\n' : ""; return prologue + (header.length > 0 ? header.join("\n") + "\n" : "") + text + "\n"; } diff --git a/packages/compiler/src/frontend/npm-static-bundled-cjs.ts b/packages/compiler/src/frontend/npm-static-bundled-cjs.ts index 89cd0fda1..be43284a6 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-cjs.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-cjs.ts @@ -1,5 +1,6 @@ import * as ts from "./ts7/syntax.js"; import { helperTokens } from "./helper-tokens.js"; +import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; // Bun's cached node-mode interop helper. Match the complete token stream, // including its dependencies: helper names alone are not a semantic contract. @@ -117,14 +118,14 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) } for (const [name, shape] of helperShapes) { const actual = declarations.get(name); - if (!actual || helperTokens(actual.stmt.getText(sf)) !== shape) return null; + if (!actual || helperTokens(getTextOfNode(actual.stmt)) !== shape) return null; } // These initializers must execute before any factory or namespace. The // original bundle would throw if a var helper were still undefined. let helperEnd = 0; for (const name of helperShapes.keys()) { const stmt = declarations.get(name)!.stmt; - if (stmt.getStart(sf) < helperEnd) return null; + if (tokenStartOfNode(stmt) < helperEnd) return null; helperEnd = stmt.end; } const edits: Edit[] = []; @@ -140,7 +141,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) const call = decl.initializer; if (!ts.isIdentifier(call.expression) || call.expression.text !== "__commonJS") continue; if ( - stmt.getStart(sf) < helperEnd || + tokenStartOfNode(stmt) < helperEnd || (ts.canHaveModifiers(stmt) && ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ) @@ -193,7 +194,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) if (unsafe) return null; // The comma expression prevents inferred naming of an anonymous export. edits.push( - { start: call.getStart(sf), end: value.getStart(sf), text: "(0," }, + { start: tokenStartOfNode(call), end: tokenStartOfNode(value), text: "(0," }, { start: value.end, end: call.end, text: ")" }, ); factories.set(name, stmt); @@ -212,7 +213,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) const call = decl.initializer; if (!ts.isIdentifier(call.expression) || call.expression.text !== "__toESM") continue; if ( - stmt.getStart(sf) < helperEnd || + tokenStartOfNode(stmt) < helperEnd || (ts.canHaveModifiers(stmt) && ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ) @@ -230,9 +231,9 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) ) return null; const factory = factories.get(requireCall.expression.text); - if (!factory || factory.end > stmt.getStart(sf)) return null; + if (!factory || factory.end > tokenStartOfNode(stmt)) return null; edits.push({ - start: call.getStart(sf), + start: tokenStartOfNode(call), end: call.end, text: `{default:${requireCall.expression.text}}`, }); @@ -286,7 +287,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) // Remove only helpers that became dead. Other bundle features may share // __defProp, so a recognized declaration alone does not justify erasure. const dead = (n: ts.Node): boolean => - edits.some((e) => n.getStart(sf) >= e.start && n.end <= e.end); + edits.some((e) => tokenStartOfNode(n) >= e.start && n.end <= e.end); const removed = new Set(); for (let changed = true; changed;) { changed = false; @@ -304,7 +305,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) live = true; }); if (!live) { - edits.push({ start: stmt.getStart(sf), end: stmt.end, text: "" }); + edits.push({ start: tokenStartOfNode(stmt), end: stmt.end, text: "" }); removed.add(name); changed = true; } diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.ts index 166676b3a..ac572000d 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-esm.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-esm.ts @@ -40,6 +40,7 @@ import { unparen, type Edit, } from "./npm-static-bundled-chunks.js"; +import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; export interface EsmInitHost { requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean; @@ -92,13 +93,13 @@ function esmHelperForm( const stmts = body.body.statements.nodes; const ret = stmts[0]; if (stmts.length !== 1 || !ret || !ts.isReturnStatement(ret) || !ret.expression) return null; - const text = ret.expression.getText(sf); + const text = getTextOfNode(ret.expression); const keys = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(text)?.[1]; if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null; return helperTokens(text) === template(KEYED_RETURN, f, r, keys) ? "keyed" : null; } if (ts.isArrowFunction(body) && body.parameters.nodes.length === 0 && !ts.isBlock(body.body)) { - const actual = helperTokens(body.body.getText(sf)); + const actual = helperTokens(getTextOfNode(body.body)); return actual !== null && actual === template(ARROW_BODY, f, r, "") ? "arrow" : null; } return null; @@ -117,7 +118,7 @@ function removeDeclarators(sf: ts.SourceFile, dead: ReadonlySet dead.has(d)); if (deadAt.every((x) => x)) { - edits.push({ start: stmt.getStart(sf), end: stmt.getEnd(), text: "" }); + edits.push({ start: tokenStartOfNode(stmt), end: stmt.end, text: "" }); continue; } const lastLive = deadAt.lastIndexOf(false); @@ -125,8 +126,8 @@ function removeDeclarators(sf: ts.SourceFile, dead: ReadonlySet(); @@ -423,7 +424,7 @@ export function lowerBundledEsmInits( } const spec = bareRequireSpecOf(rhs); if (spec === null) continue; - const req = rhs.getText(sf); + const req = getTextOfNode(rhs); const defaultIsModule = interop && (isNodeMode || !host.requireTargetEsModuleStamped(filePath, spec)); // Every other reference must be a plain member read. @@ -442,11 +443,11 @@ export function lowerBundledEsmInits( break; } if (defaultIsModule && access.name.text === "default") - uses.push({ start: access.getStart(sf), end: access.getEnd(), text: `(${req})` }); - else uses.push({ start: r.getStart(sf), end: r.getEnd(), text: req }); + uses.push({ start: tokenStartOfNode(access), end: access.end, text: `(${req})` }); + else uses.push({ start: tokenStartOfNode(r), end: r.end, text: req }); } if (!ok) continue; - edits.push({ start: el.getStart(sf), end: el.getEnd(), text: req }, ...uses); + edits.push({ start: tokenStartOfNode(el), end: el.end, text: req }, ...uses); deadDeclarators.add(decl); } } @@ -500,7 +501,7 @@ export function lowerBundledEsmInits( return false; }; const getterText = (value: ts.Expression): string => - applyEdits(source, value.getStart(sf), value.getEnd(), edits); + applyEdits(source, tokenStartOfNode(value), value.end, edits); /** A call through the namespace (`ns.fn()`, `L().fn()`) passes the object * the read came from as `this`; the lowered object differs from Node's * module object (`__esModule`, property attributes), so it must stay @@ -628,23 +629,23 @@ export function lowerBundledEsmInits( // call through it (`L().fn()`) loses its receiver. if (calledWithReceiver(access as ts.Expression) && !receiverHidden(entries, member)) return loaderWhy; - const init = initCall === null ? "" : `${initCall.getText(sf)}, `; + const init = initCall === null ? "" : `${getTextOfNode(initCall)}, `; edits.push({ - start: access.getStart(sf), - end: access.getEnd(), + start: tokenStartOfNode(access), + end: access.end, text: `(${init}${getterText(value)})`, }); } deadDeclarators.add(d); deadDeclarators.add(memoDecl); - loaderSpans.push({ start: d.getStart(sf), end: d.getEnd() }); + loaderSpans.push({ start: tokenStartOfNode(d), end: d.end }); lowered.add(target); } for (const r of allRefs) { const table = tables.get(r.text); if (table === undefined) continue; - if (loaderSpans.some((span) => r.getStart(sf) >= span.start && r.getEnd() <= span.end)) + if (loaderSpans.some((span) => tokenStartOfNode(r) >= span.start && r.end <= span.end)) continue; const p = r.parent; // The table's own declaration and __export call are handled below. @@ -709,7 +710,7 @@ export function lowerBundledEsmInits( const inner = unparen(ns); const init = ts.isBinaryExpression(inner) && inner.operatorToken.kind === ts.SyntaxKind.CommaToken - ? `${unparen(inner.left).getText(sf)}; ` + ? `${getTextOfNode(unparen(inner.left))}; ` : ""; if (shadowed(holder, table.entries)) return { degrade: `its shipped JS ${why}` }; const entries = table.entries @@ -717,8 +718,8 @@ export function lowerBundledEsmInits( .map(([n, value]) => `${JSON.stringify(n)}: () => ${getterText(value)}`) .join(", "); edits.push({ - start: holder.parent.getStart(sf), - end: holder.parent.getEnd(), + start: tokenStartOfNode(holder.parent), + end: holder.parent.end, text: `${init}__export(${mainTarget}, { ${entries} });`, }); lowered.add(r.text); @@ -771,7 +772,7 @@ export function lowerBundledEsmInits( `get ${/^[A-Za-z_$][\w$]*$/.test(n) ? n : JSON.stringify(n)}() { return ${getterText(value)}; }`, ) .join(", "); - edits.push({ start: p.getStart(sf), end: p.getEnd(), text: `{ ${getters} }` }); + edits.push({ start: tokenStartOfNode(p), end: p.end, text: `{ ${getters} }` }); lowered.add(r.text); } // A table nothing reads (its module is never required) goes too: its @@ -781,7 +782,7 @@ export function lowerBundledEsmInits( const live = (refsByName.get(target) ?? []).some((x) => { const p = x.parent; if (p !== undefined && ts.isVariableDeclaration(p) && p.name === x) return false; - return !(x.getStart(sf) >= table.stmt.getStart(sf) && x.getEnd() <= table.stmt.getEnd()); + return !(tokenStartOfNode(x) >= tokenStartOfNode(table.stmt) && x.end <= table.stmt.end); }); if (!live) lowered.add(target); } @@ -797,7 +798,7 @@ export function lowerBundledEsmInits( writes.has(target) ) return { degrade: ESM_SHAPE_DEGRADE }; - edits.push({ start: table.stmt.getStart(sf), end: table.stmt.getEnd(), text: "" }); + edits.push({ start: tokenStartOfNode(table.stmt), end: table.stmt.end, text: "" }); deadDeclarators.add(decl); } // Every removed declarator in one pass: declarators of one statement @@ -810,13 +811,13 @@ export function lowerBundledEsmInits( const { call, body } = init; edits.push( { - start: call.getStart(sf), - end: body.getStart(sf) + 1, + start: tokenStartOfNode(call), + end: tokenStartOfNode(body) + 1, text: "(() => { let __scriptc_done = false; function __scriptc_init() {", }, { - start: body.getEnd() - 1, - end: call.getEnd(), + start: body.end - 1, + end: call.end, text: "} return function () { if (__scriptc_done) return; __scriptc_done = true; __scriptc_init(); }; })()", }, ); diff --git a/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts index dfe45b862..bf753be8c 100644 --- a/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts +++ b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts @@ -29,6 +29,7 @@ import { unparen, type Edit, } from "./npm-static-bundled-chunks.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; function isModuleExports(e: ts.Expression): e is ts.PropertyAccessExpression { const x = unparen(e); @@ -132,7 +133,7 @@ export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null { continue; } // Without the rebinding, `exports` keeps the replaced object. - if (!whole.rebinds || r.getStart(sf) < whole.stmt.getEnd()) { + if (!whole.rebinds || tokenStartOfNode(r) < whole.stmt.end) { unsafe = true; continue; } @@ -140,8 +141,8 @@ export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null { const shorthand = r.parent !== undefined && ts.isShorthandPropertyAssignment(r.parent) && r.parent.name === r; edits.push({ - start: r.getStart(sf), - end: r.getEnd(), + start: tokenStartOfNode(r), + end: r.end, text: shorthand ? `exports: ${fnName}` : fnName, }); continue; @@ -168,8 +169,8 @@ export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null { (ts.isPropertyAccessExpression(outer) || ts.isElementAccessExpression(outer)) && outer.expression === access ) { - if (access.getStart(sf) < whole.stmt.getEnd() && !insideFunction(access)) unsafe = true; - edits.push({ start: access.getStart(sf), end: access.getEnd(), text: fnName }); + if (tokenStartOfNode(access) < whole.stmt.end && !insideFunction(access)) unsafe = true; + edits.push({ start: tokenStartOfNode(access), end: access.end, text: fnName }); } } } @@ -177,7 +178,7 @@ export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null { if (whole.rebinds) { // `exports = module.exports = W` → `module.exports = W`. const e = unparen(whole.stmt.expression) as ts.BinaryExpression; - edits.push({ start: e.getStart(sf), end: unparen(e.right).getStart(sf), text: "" }); + edits.push({ start: tokenStartOfNode(e), end: tokenStartOfNode(unparen(e.right)), text: "" }); } if (edits.length === 0) return null; return applyEdits(source, 0, source.length, edits); diff --git a/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts b/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts index 1af4ea1ed..1e7489595 100644 --- a/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts +++ b/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts @@ -17,6 +17,7 @@ * never reassigned. Other shapes answer null and keep their existing * treatment. */ import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; function unparen(e: ts.Expression): ts.Expression { let x = e; @@ -219,14 +220,14 @@ export function normalizeCjsExportsVar(sf: ts.SourceFile): string | null { (ts.isPropertyAccessExpression(p) || ts.isElementAccessExpression(p)) && p.expression === n ) { - edits.push({ start: n.getStart(sf), end: n.getEnd(), text: "exports" }); + edits.push({ start: tokenStartOfNode(n), end: n.end, text: "exports" }); return undefined; } ok = false; /* used as a value, assigned, or passed along */ return undefined; }); if (!ok || edits.length === 0) return null; - edits.push({ start: alias.stmt.getStart(sf), end: alias.stmt.getEnd(), text: "" }); + edits.push({ start: tokenStartOfNode(alias.stmt), end: alias.stmt.end, text: "" }); edits.sort((a, b) => a.start - b.start); let out = ""; let at = 0; diff --git a/packages/compiler/src/frontend/npm-static-declaration-syntax.ts b/packages/compiler/src/frontend/npm-static-declaration-syntax.ts index 4a84dcfeb..0d713dcff 100644 --- a/packages/compiler/src/frontend/npm-static-declaration-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-declaration-syntax.ts @@ -10,6 +10,7 @@ * too; inferred checked values validate at the ordinary return boundary. */ import * as ts from "./ts7/syntax.js"; +import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; export interface NpmStaticOverloadParameter { name: string; @@ -56,11 +57,11 @@ export function applyNpmStaticJsDocNamepaths( for (const tag of doc.tags?.nodes ?? []) { const expression = tag.typeExpression; if (expression?.kind !== ts.SyntaxKind.JSDocTypeExpression) continue; - const start = expression.getStart(sourceFile); + const start = tokenStartOfNode(expression); if (source[start] !== "{") continue; let depth = 1; let quote = ""; - for (let i = start + 1; i < tag.getEnd() && depth > 0; i++) { + for (let i = start + 1; i < tag.end && depth > 0; i++) { const char = source[i]!; if (quote !== "") { if (char === "\\") i++; @@ -80,7 +81,7 @@ export function applyNpmStaticJsDocNamepaths( continue; } const name = /^[$A-Z_a-z][$\w]*(?:[.#~][$A-Z_a-z][$\w]*)*/.exec( - source.slice(i, tag.getEnd()), + source.slice(i, tag.end), )?.[0]; if (!name) continue; if (name.includes("~") || name.includes("#")) replacements.set(i, name.length); @@ -171,7 +172,7 @@ export function applyNpmStaticNullableClassFields( hasModifier(member, ts.SyntaxKind.StaticKeyword) ) continue; - const leading = source.slice(member.getFullStart(), member.getStart(sourceFile)); + const leading = source.slice(member.pos, tokenStartOfNode(member)); if (jsDocTypeOf(member) !== undefined || leading.includes("@type")) continue; fields.set(member.name.text, { member, invalid: false }); } @@ -285,7 +286,7 @@ export function applyNpmStaticNullableClassFields( for (const field of fields.values()) { if (field.invalid || field.className === undefined) continue; inserts.push({ - offset: field.member.getStart(sourceFile), + offset: tokenStartOfNode(field.member), text: `/** @type {${field.className} | null} */ `, }); } @@ -358,7 +359,7 @@ export function applyNpmStaticFindReturnWidening( (returnType.typeArguments?.nodes.length ?? 0) !== 0 ) continue; - insertions.push({ offset: returnType.getEnd(), length: " | undefined".length }); + insertions.push({ offset: returnType.end, length: " | undefined".length }); } } if (insertions.length === 0) return null; @@ -428,26 +429,22 @@ function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean { ); } -function safeTypeText( - node: ts.TypeNode, - sourceFile: ts.SourceFile, - className: string, -): string | null { - if (SAFE_KEYWORD_TYPES.has(node.kind) || ts.isThisTypeNode(node)) return node.getText(sourceFile); +function safeTypeText(node: ts.TypeNode, className: string): string | null { + if (SAFE_KEYWORD_TYPES.has(node.kind) || ts.isThisTypeNode(node)) return getTextOfNode(node); if (ts.isLiteralTypeNode(node) && node.literal.kind === ts.SyntaxKind.NullKeyword) return "null"; if (ts.isParenthesizedTypeNode(node)) { - const inner = safeTypeText(node.type, sourceFile, className); + const inner = safeTypeText(node.type, className); return inner === null ? null : `(${inner})`; } if (ts.isArrayTypeNode(node)) { - const element = safeTypeText(node.elementType, sourceFile, className); + const element = safeTypeText(node.elementType, className); return element === null ? null : `${element}[]`; } if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) { - return safeTypeText(node.type, sourceFile, className); + return safeTypeText(node.type, className); } if (ts.isUnionTypeNode(node)) { - const arms = node.types.nodes.map((type) => safeTypeText(type, sourceFile, className)); + const arms = node.types.nodes.map((type) => safeTypeText(type, className)); return arms.some((arm) => arm === null) ? null : arms.join(" | "); } if ( @@ -483,7 +480,7 @@ function overloadSignature( ) { return null; } - const returnType = safeTypeText(method.type, sourceFile, className); + const returnType = safeTypeText(method.type, className); if (returnType === null) return null; const parameters: NpmStaticOverloadParameter[] = []; for (const parameter of method.parameters.nodes) { @@ -496,7 +493,7 @@ function overloadSignature( ) { return null; } - const type = safeTypeText(parameter.type, sourceFile, className); + const type = safeTypeText(parameter.type, className); if (type === null) return null; parameters.push({ name: parameter.name.text, @@ -553,13 +550,9 @@ export function parseNpmStaticDeclarationOverloads( return classes; } -function nullableSelfType( - node: ts.TypeNode, - sourceFile: ts.SourceFile, - className: string, -): string | null { +function nullableSelfType(node: ts.TypeNode, className: string): string | null { if (!ts.isUnionTypeNode(node) || node.types.nodes.length !== 2) return null; - const arms = node.types.nodes.map((type) => safeTypeText(type, sourceFile, className)); + const arms = node.types.nodes.map((type) => safeTypeText(type, className)); return arms.includes(className) && arms.includes("null") ? `${className} | null` : null; } @@ -613,8 +606,7 @@ export function parseNpmStaticDeclarationProperties( continue; } const type = - nullableSelfType(member.type, sourceFile, className) ?? - nullableVoidCallbackType(member.type); + nullableSelfType(member.type, className) ?? nullableVoidCallbackType(member.type); if (type !== null) properties.set(member.name.text, type); } if (properties.size > 0) classes.set(className, properties); @@ -1012,13 +1004,10 @@ export function applyNpmStaticDeclarationOverloads( ) { continue; } - const leading = source.slice( - bodyStatement.getFullStart(), - bodyStatement.getStart(sourceFile), - ); + const leading = source.slice(bodyStatement.pos, tokenStartOfNode(bodyStatement)); if (!leading.includes("@type")) { inserts.push({ - offset: bodyStatement.getStart(sourceFile), + offset: tokenStartOfNode(bodyStatement), text: `/** @type {${backing.type}} */ `, }); } @@ -1036,13 +1025,13 @@ export function applyNpmStaticDeclarationOverloads( (parameter) => parameter.optional && !existing.includes(`[${parameter.name}]`), ); if (missingOptional) - inserts.push({ offset: member.getStart(sourceFile), text: `${implementation} ` }); + inserts.push({ offset: tokenStartOfNode(member), text: `${implementation} ` }); else if ( jsDocs.length === 0 && member.parameters.nodes.length === 0 && SCALAR_METHOD_RETURNS.has(signatures[0]!.returnType) ) { - inserts.push({ offset: member.getStart(sourceFile), text: `${implementation} ` }); + inserts.push({ offset: tokenStartOfNode(member), text: `${implementation} ` }); } } continue; @@ -1050,7 +1039,9 @@ export function applyNpmStaticDeclarationOverloads( const implementation = jsDocs.length === 0 ? implementationComment(statement.name.text, member, signatures) : null; if (jsDocs.length === 0 && implementation === null) continue; - const offset = jsDocs[0]?.getStart(sourceFile) ?? member.getStart(sourceFile); + const offset = + (jsDocs[0] === undefined ? undefined : tokenStartOfNode(jsDocs[0])) ?? + tokenStartOfNode(member); const text = `${signatures.map(overloadComment).join(" ")} ${implementation === null ? "" : implementation + " "}`; inserts.push({ offset, text }); } @@ -1158,9 +1149,9 @@ export function applyNpmStaticDeclarationProperties( !callbackInitializer(member.initializer)) ) continue; - const leading = source.slice(member.getFullStart(), member.getStart(sourceFile)); + const leading = source.slice(member.pos, tokenStartOfNode(member)); if (jsDocTypeOf(member) !== undefined || leading.includes("@type")) continue; - inserts.push({ offset: member.getStart(sourceFile), text: `/** @type {${type}} */ ` }); + inserts.push({ offset: tokenStartOfNode(member), text: `/** @type {${type}} */ ` }); } const constructor = statement.members.nodes.find( (member): member is ts.ConstructorDeclaration => @@ -1184,12 +1175,9 @@ export function applyNpmStaticDeclarationProperties( } const type = properties.get(left.name.text); if (type === undefined) continue; - const leading = source.slice( - bodyStatement.getFullStart(), - bodyStatement.getStart(sourceFile), - ); + const leading = source.slice(bodyStatement.pos, tokenStartOfNode(bodyStatement)); if (leading.includes("@type")) continue; - inserts.push({ offset: bodyStatement.getStart(sourceFile), text: `/** @type {${type}} */ ` }); + inserts.push({ offset: tokenStartOfNode(bodyStatement), text: `/** @type {${type}} */ ` }); } } if (inserts.length === 0) return null; diff --git a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts index 348b04817..c48420625 100644 --- a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts @@ -6,6 +6,7 @@ import { isModuleExports, } from "./cjs-syntax.js"; import { rewriteBundledFunctionImports } from "./npm-static-bundled-cjs.js"; +import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; /** Resolution stays with the compilation host. Syntax never reads disk or * starts a parser implicitly, so native and Node hosts run the same pass. */ @@ -57,7 +58,7 @@ function liveExportTableFirst( }; let sawTable = false; for (const stmt of sf.statements.nodes) { - if (stmt.getStart(sf) >= installAt) return sawTable; + if (tokenStartOfNode(stmt) >= installAt) return sawTable; if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) continue; if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) continue; if (ts.isVariableStatement(stmt)) { @@ -146,7 +147,7 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string } cur.kind === ts.SyntaxKind.FalseKeyword || cur.kind === ts.SyntaxKind.NullKeyword ) { - return { kind: "hoist", text: cur.getText() }; + return { kind: "hoist", text: getTextOfNode(cur) }; } // ident-rooted member chains: this.x / a.b.c / a["k"].d — side-effect // free reads the tail const can evaluate once. @@ -161,7 +162,7 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string } ) { walk = walk.expression; } - if (ts.isIdentifier(walk)) return { kind: "hoist", text: cur.getText() }; + if (ts.isIdentifier(walk)) return { kind: "hoist", text: getTextOfNode(cur) }; return null; } @@ -182,7 +183,7 @@ function chaseObjectLiteral(e: ts.Expression): { kind: "hoist"; text: string } | ) return null; } - return { kind: "hoist", text: cur.getText() }; + return { kind: "hoist", text: getTextOfNode(cur) }; } /** The getter body's returned expression for the two descriptor getter @@ -326,7 +327,7 @@ function recognizedToEsmDecl(d: ts.VariableDeclaration): boolean { let to: ts.Expression = expr.arguments.nodes[0]!; while (ts.isParenthesizedExpression(to)) to = to.expression; if (!ts.isConditionalExpression(to)) return false; - const condText = to.condition.getText(); + const condText = getTextOfNode(to.condition); if (!condText.includes(isNodeMode) || !condText.includes("__esModule")) return false; let stamp: ts.Expression = to.whenTrue; while (ts.isParenthesizedExpression(stamp)) stamp = stamp.expression; @@ -443,7 +444,7 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH ) { if (parent.name.text === "default" && defaultIsModule) { if (!readOnlyAccess(parent)) return fail(TO_ESM_ESCAPE_DEGRADE); - pads.push({ start: child.getEnd(), end: parent.getEnd() }); + pads.push({ start: child.end, end: parent.end }); } } else { return fail(TO_ESM_ESCAPE_DEGRADE); @@ -451,8 +452,8 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH // the wrapper itself: callee + '(' down, and everything after the // require argument (`, 1` included) — the bare require survives at its // original offsets. - pads.push({ start: call.getStart(sf), end: arg0!.getStart(sf) }); - pads.push({ start: arg0!.getEnd(), end: call.getEnd() }); + pads.push({ start: tokenStartOfNode(call), end: tokenStartOfNode(arg0!) }); + pads.push({ start: arg0!.end, end: call.end }); } if (bindings.size > 0) { @@ -513,7 +514,7 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH violated = true; return; } - pads.push({ start: n.expression.getEnd(), end: n.getEnd() }); + pads.push({ start: n.expression.end, end: n.end }); } }; ts.walkPreorder(sf, padDefaults); @@ -628,8 +629,8 @@ export function rewriteBundlerCjsSyntax( ts.isIdentifier(rhs.arguments.nodes[0]!) ) { esbuildMainTarget = (rhs.arguments.nodes[0] as ts.Identifier).text; - tablePos = stmt.getStart(sf); - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + tablePos = tokenStartOfNode(stmt); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; continue; } @@ -662,9 +663,9 @@ export function rewriteBundlerCjsSyntax( names, finalRhs: cur, viaBareExports, - pos: stmt.getStart(sf), - start: stmt.getStart(sf), - end: stmt.getEnd(), + pos: tokenStartOfNode(stmt), + start: tokenStartOfNode(stmt), + end: stmt.end, }); continue; // classified later (only when a rewrite actually happens) } @@ -684,7 +685,7 @@ export function rewriteBundlerCjsSyntax( isModuleExports(inner.left) && ts.isObjectLiteralExpression(inner.right) ) { - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; continue; } @@ -695,7 +696,7 @@ export function rewriteBundlerCjsSyntax( const table = exportCallOf(e); if (table !== null) { exportTables.push(table); - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; continue; } @@ -705,7 +706,7 @@ export function rewriteBundlerCjsSyntax( const spec = bareRequireSpecOf(e.arguments.nodes[0]!); if (spec !== null) { addStarSpec(spec); - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; continue; } @@ -716,7 +717,7 @@ export function rewriteBundlerCjsSyntax( const spec = bareRequireSpecOf(e.arguments.nodes[1]!); if (spec !== null) { addStarSpec(spec); - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; continue; } @@ -725,7 +726,7 @@ export function rewriteBundlerCjsSyntax( const def = definePropertyExportOf(e); if (def !== null) { if (def.esModuleStamp) { - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); // the canonical table re-stamps it (lexer-visibly — merve detects // this defineProperty form, so parity wants a visible entry) continue; @@ -733,7 +734,7 @@ export function rewriteBundlerCjsSyntax( const chased = def.value !== null ? chaseValue(def.value) : null; if (chased !== null) { definePropEntries.push({ name: def.name, value: def.value }); - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; } // unchaseable descriptor: LEAVE the statement (its runtime fence @@ -792,7 +793,7 @@ export function rewriteBundlerCjsSyntax( // The table feeding module.exports keeps a getter it cannot chase // as a live accessor rather than a snapshot. if (spec.kind === "any" && value !== null && t.target === esbuildMainTarget) - claim(name, { name, kind: "getter", value: value.getText() }); + claim(name, { name, kind: "getter", value: getTextOfNode(value) }); else claim(name, spec); } } @@ -951,8 +952,8 @@ export function rewriteBundlerCjsSyntax( helperDecls.set( d.name.text, decls.length === 1 - ? { start: stmt.getStart(sf), end: stmt.getEnd() } - : { start: d.getStart(sf), end: d.getEnd() }, + ? { start: tokenStartOfNode(stmt), end: stmt.end } + : { start: tokenStartOfNode(d), end: d.end }, ); } if (anyHelper && decls.length > 1) helperStmts.push(stmt); @@ -966,8 +967,8 @@ export function rewriteBundlerCjsSyntax( if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node) return; const list = refs.get(node.text); - if (list === undefined) refs.set(node.text, [node.getStart(sf)]); - else list.push(node.getStart(sf)); + if (list === undefined) refs.set(node.text, [tokenStartOfNode(node)]); + else list.push(tokenStartOfNode(node)); } }; ts.walkPreorder(sf, record); @@ -999,12 +1000,12 @@ export function rewriteBundlerCjsSyntax( const deadAt = decls.map((d) => { if (!ts.isIdentifier(d.name) || !removed.has(d.name.text)) return false; const span = helperDecls.get(d.name.text)!; - if (span.start !== d.getStart(sf)) return false; + if (span.start !== tokenStartOfNode(d)) return false; declaratorSpans.add(span); return true; }); if (deadAt.every((x) => x)) { - declaratorPads.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + declaratorPads.push({ start: tokenStartOfNode(stmt), end: stmt.end }); continue; } // Dead declarators before the last live one take their trailing @@ -1015,8 +1016,8 @@ export function rewriteBundlerCjsSyntax( const d = decls[i]!; declaratorPads.push( i < lastLive - ? { start: d.getStart(sf), end: decls[i + 1]!.getStart(sf) } - : { start: decls[lastLive]!.getEnd(), end: d.getEnd() }, + ? { start: tokenStartOfNode(d), end: tokenStartOfNode(decls[i + 1]!) } + : { start: decls[lastLive]!.end, end: d.end }, ); } } diff --git a/packages/compiler/src/frontend/npm-static-subtyping.ts b/packages/compiler/src/frontend/npm-static-subtyping.ts index 04c66a559..18d69d4c4 100644 --- a/packages/compiler/src/frontend/npm-static-subtyping.ts +++ b/packages/compiler/src/frontend/npm-static-subtyping.ts @@ -19,8 +19,8 @@ export function isNpmStaticSubclassArgument( symbol.declarations.some( (declaration) => (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && - /\.[cm]?js$/.test(declaration.getSourceFile().fileName) && - npmStaticPackageOfPath(declaration.getSourceFile().fileName) !== null, + /\.[cm]?js$/.test(ts.getSourceFileOfNode(declaration).fileName) && + npmStaticPackageOfPath(ts.getSourceFileOfNode(declaration).fileName) !== null, ) ); }; @@ -50,12 +50,12 @@ export function isNpmStaticSubclassArgument( }; let accepted = false; ts.walkPreorder(source, (node) => { - if (node.end < diagnostic.pos || node.getStart(source) > diagnostic.end) return "skip"; + if (node.end < diagnostic.pos || ts.tokenStartOfNode(node) > diagnostic.end) return "skip"; if (!ts.isCallExpression(node) && !ts.isNewExpression(node)) return undefined; const index = node.arguments?.nodes.findIndex( (argument) => - argument.getStart(source) === diagnostic.pos && argument.end === diagnostic.end, + ts.tokenStartOfNode(argument) === diagnostic.pos && argument.end === diagnostic.end, ) ?? -1; if (index < 0) return undefined; const parameter = checker.getResolvedSignature(node)?.getParameters()[index]; diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index c29f31c8a..2e75542d7 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1236,7 +1236,7 @@ export function isEsModuleStamp(expr: ts.Expression): boolean { function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean { const cjs = cjsExportAssignmentOf(stmt); if (cjs === null) return false; - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); const plainMember = (left: ts.Expression): boolean => { if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return false; // A replacement export can have setters or non-writable function @@ -1291,7 +1291,7 @@ function sourceBinding7(program: ts.Program, ident: ts.Identifier): boolean { return ( symbol !== undefined && symbol.declarations.some( - (decl) => !decl.getSourceFile().isDeclarationFile && !ts.isSourceFile(decl), + (decl) => !ts.getSourceFileOfNode(decl).isDeclarationFile && !ts.isSourceFile(decl), ) ); } @@ -1318,7 +1318,7 @@ function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean { if (ts.isEmptyStatement(s) || ts.isFunctionDeclaration(s)) return true; if (ts.isExpressionStatement(s) && ts.isStringLiteral(s.expression)) return true; // directive if (isRequireStatement7(s)) return true; - if (isCjsJsFile7(s.getSourceFile(), program)) { + if (isCjsJsFile7(ts.getSourceFileOfNode(s), program)) { if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) { const call = s.expression as ts.CallExpression; const object = (call.expression as ts.PropertyAccessExpression).expression as ts.Identifier; @@ -1445,7 +1445,7 @@ function requireEarlyReference7( if (cached !== undefined) return cached; let found: ts.Identifier | null = null; const stmt = decl.parent?.parent; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) { const stmts = sf.statements.nodes; const k = stmts.indexOf(stmt); @@ -2021,7 +2021,7 @@ function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): bool if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = checker.getAliasedSymbol(symbol); const targetDecl = target.declarations[0]; - if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) { + if (targetDecl !== undefined && ts.getSourceFileOfNode(targetDecl).isDeclarationFile) { for ( let p: ts.Node | undefined = targetDecl.parent; p !== undefined && !ts.isSourceFile(p); @@ -2029,7 +2029,8 @@ function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): bool ) { if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) { return ( - canonicalBuiltinModule(p.name.text) === "module" && target.name === "createRequire" + canonicalBuiltinModule(p.name.text) === "module" && + ts.symbolName(target) === "createRequire" ); } } @@ -2113,7 +2114,7 @@ function stableCreateRequireBindingReason7( return "its createRequire binding is redeclared"; if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0) return "its createRequire result escapes through an exported binding"; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); const refs = identifierOccurrences7(sf, decl.name.text); checker.prefetchSymbolNodesExact(refs); for (const ref of refs) { @@ -2180,7 +2181,7 @@ function npmStaticModuleImportReason7( } if (!bindings || !ts.isNamedImports(bindings)) return null; const checker = program.getTypeChecker(); - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); for (const specifier of bindings.elements.nodes) { if ((specifier.propertyName?.text ?? specifier.name.text) !== "createRequire") continue; const imported = checker.getSymbolAtLocation(specifier.name); @@ -2260,7 +2261,7 @@ function requireCallBindingKind7(program: ts.Program, node: ts.Node): RequireCal const symbol = checker.getSymbolAtLocation(call.expression); if (symbol === undefined) return "ambient"; const decls = symbol.declarations; - if (decls.every((d) => d.getSourceFile().isDeclarationFile)) return "ambient"; + if (decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return "ambient"; return isCreateRequireBinding7(program, call.expression) ? "createRequire" : "source"; } @@ -2415,15 +2416,15 @@ function selfImportTdzFences7( // are initialized. Hoisted functions have none. const tdzEndOf = (aliased: ts.Symbol): number | null => { const d = aliased.valueDeclaration ?? aliased.declarations[0]; - if (d === undefined || d.getSourceFile() !== sf) return null; + if (d === undefined || ts.getSourceFileOfNode(d) !== sf) return null; if (ts.isFunctionDeclaration(d)) return null; - return d.getEnd(); + return d.end; }; const fence = (ref: ts.Identifier, name: string): void => { diags.push( unsupportedDiag( "SC1030", - { file: sf.fileName, start: ref.getStart(sf), end: ref.getEnd() }, + { file: sf.fileName, start: ts.tokenStartOfNode(ref), end: ref.end }, `the self-import binding '${name}' read above its declaration (the import aliases this module's OWN binding — Node initializes it when the declaration executes, so this read throws ReferenceError; move the read below the declaration)`, ), ); @@ -2439,7 +2440,7 @@ function selfImportTdzFences7( ts.isIdentifier(node) && node !== bindingName && node.text === bindingName.text && - node.getStart(sf) < end && + ts.tokenStartOfNode(node) < end && !insideFunction(node) && checker.getSymbolAtLocation(node) === sym ) { @@ -2472,7 +2473,7 @@ function selfImportTdzFences7( const memberSym = checker.getSymbolAtLocation(node.name); if (memberSym !== undefined) { const end = tdzEndOf(memberSym); - if (end !== null && node.getStart(sf) < end) + if (end !== null && ts.tokenStartOfNode(node) < end) fence(node.expression, `${nsName.text}.${node.name.text}`); } } @@ -2672,7 +2673,7 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod if (sym === undefined) return false; if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym); const decls = sym.declarations; - return decls.length > 0 && decls.every((d) => d.getSourceFile().isDeclarationFile); + return decls.length > 0 && decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile); }; const hasDecorator = (n: ts.Node): boolean => ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).some( @@ -2946,8 +2947,8 @@ export function makeCycleAdmission( } }; const lineOf = (node: ts.Node): string => { - const nsf = node.getSourceFile(); - return `${nsf.fileName}:${ts.getLineAndCharacterOfPosition(nsf, node.getStart(nsf)).line + 1}`; + const nsf = ts.getSourceFileOfNode(node); + return `${nsf.fileName}:${ts.getLineAndCharacterOfPosition(nsf, ts.tokenStartOfNode(node)).line + 1}`; }; // Cluster verdict memo (keyed by the component array identity): true // when every member is an ES module passing the inert-top-level bar. @@ -3454,7 +3455,7 @@ function exactCycleRefusal7( const targetOf = (stmt: ts.ImportDeclaration | ts.ExportDeclaration): ts.SourceFile | null => { const spec = stmt.moduleSpecifier; if (spec === undefined || !ts.isStringLiteral(spec)) return null; - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); return resolveImport7(program, sf, spec.text) ?? npmStaticDepSf7(program, sf, spec.text); }; for (const m of comp) { @@ -3688,8 +3689,8 @@ function computeCycleEarlyBindings7( module: m, exposed, rank, - start: stmt.getStart(m), - end: stmt.getEnd(), + start: ts.tokenStartOfNode(stmt), + end: stmt.end, reference: null, folded: null, }); @@ -3734,13 +3735,13 @@ function computeCycleEarlyBindings7( if ( c.kind === "class" && sf === c.module && - ref.getStart(sf) >= c.start && - ref.getEnd() <= c.end + ts.tokenStartOfNode(ref) >= c.start && + ref.end <= c.end ) continue; let early: boolean; if (inDeferredPosition7(ref)) early = c.exposed; - else if (sf === c.module) early = ref.getStart(sf) < c.start; + else if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start; else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false; else early = !known || rankOf.get(sf)! < c.rank; if (early) c.reference = ref; @@ -3879,7 +3880,7 @@ function moduleEarlyBindingsOf7( store(stmt.name, [stmt]); } else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) { store(stmt.name, [stmt]); - candidates.push({ name: stmt.name, index, start: stmt.getStart(sf), cls: stmt }); + candidates.push({ name: stmt.name, index, start: ts.tokenStartOfNode(stmt), cls: stmt }); candidateNames.add(stmt.name.text); } else if (ts.isVariableStatement(stmt)) { const lexical = (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; @@ -3891,7 +3892,7 @@ function moduleEarlyBindingsOf7( } if (!lexical) continue; for (const name of boundNames7(d.name)) { - candidates.push({ name, index, start: stmt.getStart(sf) }); + candidates.push({ name, index, start: ts.tokenStartOfNode(stmt) }); candidateNames.add(name.text); } } @@ -3983,9 +3984,10 @@ function moduleEarlyBindingsOf7( c !== undefined && node !== c.name && !reachedRefs.has(symbol) && - !(c.cls !== undefined && node.getStart(sf) >= c.cls.pos && node.end <= c.cls.end) + !(c.cls !== undefined && ts.tokenStartOfNode(node) >= c.cls.pos && node.end <= c.cls.end) ) { - if (inDeferredPosition7(node) || node.getStart(sf) < c.start) reachedRefs.set(symbol, node); + if (inDeferredPosition7(node) || ts.tokenStartOfNode(node) < c.start) + reachedRefs.set(symbol, node); } for (const unit of unitsBySymbol.get(symbol) ?? []) { if (reachedUnits.has(unit)) continue; @@ -4004,8 +4006,8 @@ function moduleEarlyBindingsOf7( const report = (declaration: ts.Node): void => { while ( next < candidates.length && - candidates[next]!.name.getStart(sf) >= declaration.getStart(sf) && - candidates[next]!.name.getEnd() <= declaration.getEnd() + ts.tokenStartOfNode(candidates[next]!.name) >= ts.tokenStartOfNode(declaration) && + candidates[next]!.name.end <= declaration.end ) { const c = candidates[next++]!; const symbol = checker.getSymbolAtLocation(c.name); @@ -4065,7 +4067,7 @@ function returnsOnlyClosuresOver7( const decls = symbol === undefined ? [] : symbol.declarations; if (decls.length !== 1) return false; const decl = decls[0]!; - if (decl.getSourceFile().isDeclarationFile) return false; + if (ts.getSourceFileOfNode(decl).isDeclarationFile) return false; let fn: ts.Node | undefined; if (ts.isFunctionDeclaration(decl)) fn = decl; else if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined) { @@ -4186,12 +4188,13 @@ function laterJobCallee7(checker: ts.TypeChecker, callee: ts.Expression): boolea } if (symbol === undefined) return false; const decls = symbol.declarations; - if (decls.length === 0 || !decls.every((d) => d.getSourceFile().isDeclarationFile)) return false; + if (decls.length === 0 || !decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) + return false; if (LATER_JOB_BUILTINS7.has(name.text)) return true; // The JavaScript standard library's callbacks mostly run synchronously; // package declarations describe user code. return decls.every((d) => { - const file = d.getSourceFile().fileName; + const file = ts.getSourceFileOfNode(d).fileName; return isNodeTypesPath(file) || file === fallbackDtsPath(); }); } @@ -4334,8 +4337,8 @@ function resolveNpmImport7( } function locOf7(node: ts.Node): { file: string; start: number; end: number } { - const sf = node.getSourceFile(); - return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() }; + const sf = ts.getSourceFileOfNode(node); + return { file: sf.fileName, start: ts.tokenStartOfNode(node), end: node.end }; } /** The program file holding an opted-in package's resolved runtime JS: @@ -4867,7 +4870,7 @@ function preflight7(load: LoadResult): { } if (reDep && !reDep.fileName.endsWith(".json")) { deps.push({ dep: reDep, stmt }); - depPositions.get(sf)!.push({ dep: reDep, pos: stmt.getStart(sf) }); + depPositions.get(sf)!.push({ dep: reDep, pos: ts.tokenStartOfNode(stmt) }); } continue; } @@ -4915,7 +4918,7 @@ function preflight7(load: LoadResult): { refusals.get(sf)!.push({ crash: { message, className, loc: locOf7(stmt) }, fallback: unsupportedDiag("SC1010", locOf7(stmt), fallbackFeature), - pos: stmt.getStart(sf), + pos: ts.tokenStartOfNode(stmt), }); }; // Project modules win before npm fallback: this is the source graph @@ -5128,7 +5131,7 @@ function preflight7(load: LoadResult): { } if (dep && !isJson) { deps.push({ dep, stmt }); - depPositions.get(sf)!.push({ dep, pos: stmt.getStart(sf) }); + depPositions.get(sf)!.push({ dep, pos: ts.tokenStartOfNode(stmt) }); } } { @@ -5168,7 +5171,11 @@ function preflight7(load: LoadResult): { for (let k = 0; k < stmts.length; k++) { const stmt = stmts[k]!; for (const req of requiresOf7(stmt)) { - const loc = { file: sf.fileName, start: req.node.getStart(sf), end: req.node.getEnd() }; + const loc = { + file: sf.fileName, + start: ts.tokenStartOfNode(req.node), + end: req.node.end, + }; const bindingKind = requireCallBindingKind7(program, req.node); // TypeScript source files join this scan only through the // canonical createRequire bridge. Their ordinary require-shaped @@ -5314,7 +5321,7 @@ function preflight7(load: LoadResult): { ); for (const call of nestedBareRequires) { const spec = requireSpecOf7(call)!; - const loc = { file: sf.fileName, start: call.getStart(sf), end: call.getEnd() }; + const loc = { file: sf.fileName, start: ts.tokenStartOfNode(call), end: call.end }; const bindingKind = requireCallBindingKind7(program, call); if (!jsRequireFile && bindingKind !== "createRequire") continue; if (isNodeEsmFile7(sf, program)) { @@ -5730,7 +5737,7 @@ function cjsNamedImportLinkCheck( // swaps ` as ` for `: ` — reproduced exactly, quirks included // (multi-line import statements simply lose the hint). const text = bad.sf.text; - const pos = bad.nameNode.getStart(bad.sf); + const pos = ts.tokenStartOfNode(bad.nameNode); const lineStart = text.lastIndexOf("\n", pos - 1) + 1; const lineEnd = text.indexOf("\n", pos); let line = text.slice(lineStart, lineEnd < 0 ? text.length : lineEnd); @@ -6044,7 +6051,7 @@ function earlierLinkCrash( if (!element.isTypeOnly) { entries.push({ local: element.name.text, - start: (element.propertyName ?? element.name).getStart(sf), + start: ts.tokenStartOfNode(element.propertyName ?? element.name), kind: 0, }); } @@ -6068,7 +6075,7 @@ function earlierLinkCrash( for (const element of exportClause.elements.nodes) { if ( !element.isTypeOnly && - (element.propertyName ?? element.name).getStart(sf) === crash.loc.start + ts.tokenStartOfNode(element.propertyName ?? element.name) === crash.loc.start ) { return [1, reexport]; } @@ -6134,7 +6141,11 @@ export function isJsSourceFile(sf: ts.SourceFile): boolean { export function isCheckedJsSourceFile(sf: ts.SourceFile, projectCheckJs: boolean): boolean { if (!isJsSourceFile(sf) || isNodeModulesPath(sf.fileName)) return false; let checked = projectCheckJs; - const head = sf.text.slice(0, sf.statements.nodes[0]?.getStart(sf) ?? sf.text.length); + const head = sf.text.slice( + 0, + (sf.statements.nodes.length === 0 ? undefined : ts.tokenStartOfNode(sf.statements.nodes[0]!)) ?? + sf.text.length, + ); for (const line of head.split("\n")) { const directive = /^\s*\/\/\s*@ts-(no)?check\b/.exec(line); if (directive) checked = directive[1] === undefined; @@ -6333,8 +6344,8 @@ export function cjsVoidInitPreambleReals( tail = tail.right; } if (!isVoid(tail)) return null; - const sf = cjs.expr.getSourceFile(); - const myPos = cjs.expr.getStart(sf); + const sf = ts.getSourceFileOfNode(cjs.expr); + const myPos = ts.tokenStartOfNode(cjs.expr); const reals = new Map< string, Extract, { kind: "member" }> @@ -6347,7 +6358,7 @@ export function cjsVoidInitPreambleReals( other?.kind === "member" && ts.isIdentifier(other.name) && other.name.text === name && - other.expr.getStart(sf) > myPos && + ts.tokenStartOfNode(other.expr) > myPos && !isVoid(other.value), ); if (real?.kind !== "member") return null; @@ -6505,7 +6516,7 @@ export function isCjsExportTableLiteral(obj: ts.ObjectLiteralExpression): boolea } const decl = obj.parent; if (!ts.isVariableDeclaration(decl) || decl.initializer !== obj) return false; - const sf = obj.getSourceFile(); + const sf = ts.getSourceFileOfNode(obj); if (!isJsSourceFile(sf)) return false; for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); diff --git a/packages/compiler/src/frontend/services.test.ts b/packages/compiler/src/frontend/services.test.ts index bd172bbc5..e491b0ac5 100644 --- a/packages/compiler/src/frontend/services.test.ts +++ b/packages/compiler/src/frontend/services.test.ts @@ -7,6 +7,7 @@ import { Ts7Api } from "./ts7/rpc-api.js"; import { checkPreflight, isNodeEsmFile, loadProgram } from "./program.js"; import { clearResolveCaches } from "./resolve.js"; import type { Ts7ApiOptions } from "./ts7/program-host.js"; +import { getTextOfNode } from "./ts7/node-functions.js"; const directories: string[] = []; afterEach(() => { @@ -80,7 +81,7 @@ test("syntax sessions reuse one connection and returned ASTs survive edits and c services.close(); services.close(); expect(connections[0]!.close).toHaveBeenCalledTimes(1); - expect(before.statements.nodes[0]!.getText()).toBe(before.text); + expect(getTextOfNode(before.statements.nodes[0]!)).toBe(before.text); }); test("program hosts remain usable after their creating services close", () => { diff --git a/packages/compiler/src/frontend/tier-analysis.ts b/packages/compiler/src/frontend/tier-analysis.ts index 2ed5c8c9b..b75e491a8 100644 --- a/packages/compiler/src/frontend/tier-analysis.ts +++ b/packages/compiler/src/frontend/tier-analysis.ts @@ -107,8 +107,8 @@ function nodeAt(sf: ts.SourceFile, start: number, end: number): ts.Node { let node: ts.Node = sf; for (;;) { let next: ts.Node | undefined; - node.forEachChild((child) => { - if (child.getStart(sf) <= start && end <= child.getEnd()) { + ts.forEachChild(node, (child) => { + if (ts.tokenStartOfNode(child) <= start && end <= child.end) { next = child; return true; } @@ -263,23 +263,23 @@ function functionName(node: ts.Node): string { if (ts.isBinaryExpression(parent) && parent.right === node) { const left = parent.left; if (ts.isIdentifier(left)) return left.text; - if (ts.isPropertyAccessExpression(left)) return left.getText(); + if (ts.isPropertyAccessExpression(left)) return ts.getTextOfNode(left); } } - const sf = node.getSourceFile(); - const pos = ts.getLineAndCharacterOfPosition(sf, node.getStart(sf)); + const sf = ts.getSourceFileOfNode(node); + const pos = ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(node)); return ``; } function locOfNode(node: ts.Node): SrcLoc { - const sf = node.getSourceFile(); - return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() }; + const sf = ts.getSourceFileOfNode(node); + return { file: sf.fileName, start: ts.tokenStartOfNode(node), end: node.end }; } function enclosingFunction(node: ts.Node): ts.Node { let current: ts.Node | undefined = node.parent; while (current !== undefined && !isFunctionLikeOrFile(current)) current = current.parent; - return current ?? node.getSourceFile(); + return current ?? ts.getSourceFileOfNode(node); } function functionUnit(node: ts.Node, instance: string | null): DemotionUnit { @@ -423,7 +423,10 @@ class UnitResolver { return this.declarationUnit(binding); } if (ts.isVariableDeclaration(decl) || ts.isParameter(decl) || ts.isBindingElement(decl)) { - const name = nameText(decl.name) ?? decl.name?.getText() ?? ""; + const name = + nameText(decl.name) ?? + (decl.name === undefined ? undefined : ts.getTextOfNode(decl.name)) ?? + ""; const scope = enclosingFunction(decl); const unit: DemotionUnit = { kind: "binding", name, loc: locOfNode(decl) }; if (ts.isParameter(decl) && ts.isFunctionLikeDeclaration(decl.parent)) @@ -437,7 +440,7 @@ class UnitResolver { /** The unit an operand expression denotes. */ valueUnit(expr: ts.Node): { unit: DemotionUnit; reflection: boolean } | null { const { root, viaPrototype } = rootOf(expr); - const sf = root.getSourceFile(); + const sf = ts.getSourceFileOfNode(root); if (root.kind === ts.SyntaxKind.ThisKeyword) { const owner = enclosingFunction(root); if (ts.isSourceFile(owner)) @@ -475,7 +478,7 @@ class UnitResolver { if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = this.checker.getAliasedSymbol(symbol); const decls = symbol === undefined ? [] : symbol.declarations; - const decl = decls.find((d) => !d.getSourceFile().isDeclarationFile); + const decl = decls.find((d) => !ts.getSourceFileOfNode(d).isDeclarationFile); if (decl === undefined) { // CommonJS `exports`/`module` as values: the module object. if (isModuleObjectName(root)) diff --git a/packages/compiler/src/frontend/ts7/api-conformance.ts b/packages/compiler/src/frontend/ts7/api-conformance.ts new file mode 100644 index 000000000..304b4f32f --- /dev/null +++ b/packages/compiler/src/frontend/ts7/api-conformance.ts @@ -0,0 +1,297 @@ +/* Compile-time checks that scriptc's AST and semantic objects have the shapes + * of tsc-ts/api's interfaces. The frontend keeps its own types, whose nodes + * extend the AstNode class, because native code reads class fields directly + * and reads through a structural interface dynamically. This module only + * declares types and emits nothing. */ + +import type * as api from "tsc-ts/api"; +import type { AstNode, AstNodeList } from "./ast-node.js"; +import type * as ast from "./ast-types.js"; + +type Conforms = [T, U]; + +/* Members where tsgo's wire, which the RPC model reads, differs from tsc-ts: + * - SourceFile.externalModuleIndicator is `true` where moduleDetection makes a + * file a module without an import or export. + * - JSDocTypeLiteral.jsdocPropertyTags is one tag. + * - SyntaxList.children is a node list. + * - SyntheticExpression's isSpread and type are factory fields that a parsed + * tree never has. */ +type Without = Omit; + +export interface AstConformance { + Node: Conforms; + NodeList: Conforms; + SourceFile: Conforms< + Without, + Without + >; + ArrayBindingPattern: Conforms; + ArrayLiteralExpression: Conforms; + ArrayTypeNode: Conforms; + ArrowFunction: Conforms; + AsExpression: Conforms; + AwaitExpression: Conforms; + BigIntLiteral: Conforms; + BinaryExpression: Conforms; + BindingElement: Conforms; + Block: Conforms; + BodyBase: Conforms; + BreakStatement: Conforms; + CallExpression: Conforms; + CallSignatureDeclaration: Conforms; + CaseBlock: Conforms; + CaseClause: Conforms; + CatchClause: Conforms; + ClassDeclaration: Conforms; + ClassElementBase: Conforms; + ClassExpression: Conforms; + ClassLikeBase: Conforms; + ClassStaticBlockDeclaration: Conforms< + ast.ClassStaticBlockDeclaration, + api.ClassStaticBlockDeclaration + >; + ComputedPropertyName: Conforms; + ConditionalExpression: Conforms; + ConditionalTypeNode: Conforms; + ConstructSignatureDeclaration: Conforms< + ast.ConstructSignatureDeclaration, + api.ConstructSignatureDeclaration + >; + ConstructorDeclaration: Conforms; + ConstructorTypeNode: Conforms; + ContinueStatement: Conforms; + DebuggerStatement: Conforms; + DeclarationBase: Conforms; + Decorator: Conforms; + DefaultClause: Conforms; + DeleteExpression: Conforms; + DoStatement: Conforms; + ElementAccessExpression: Conforms; + EmptyStatement: Conforms; + EnumDeclaration: Conforms; + EnumMember: Conforms; + ExportAssignment: Conforms; + ExportDeclaration: Conforms; + ExportSpecifier: Conforms; + ExpressionBase: Conforms; + ExpressionStatement: Conforms; + ExpressionWithTypeArguments: Conforms< + ast.ExpressionWithTypeArguments, + api.ExpressionWithTypeArguments + >; + ExternalModuleReference: Conforms; + ForInStatement: Conforms; + ForOfStatement: Conforms; + ForStatement: Conforms; + FunctionDeclaration: Conforms; + FunctionExpression: Conforms; + FunctionLikeBase: Conforms; + FunctionLikeWithBodyBase: Conforms; + FunctionTypeNode: Conforms; + GetAccessorDeclaration: Conforms; + HeritageClause: Conforms; + Identifier: Conforms; + IfStatement: Conforms; + ImportAttribute: Conforms; + ImportAttributes: Conforms; + ImportClause: Conforms; + ImportDeclaration: Conforms; + ImportEqualsDeclaration: Conforms; + ImportSpecifier: Conforms; + ImportTypeNode: Conforms; + IndexSignatureDeclaration: Conforms; + IndexedAccessTypeNode: Conforms; + InferTypeNode: Conforms; + InterfaceDeclaration: Conforms; + IntersectionTypeNode: Conforms; + IterationStatementBase: Conforms; + JSDoc: Conforms; + JSDocAllType: Conforms; + JSDocAugmentsTag: Conforms; + JSDocCallbackTag: Conforms; + JSDocCommentBase: Conforms; + JSDocDeprecatedTag: Conforms; + JSDocImplementsTag: Conforms; + JSDocImportTag: Conforms; + JSDocLink: Conforms; + JSDocLinkCode: Conforms; + JSDocLinkPlain: Conforms; + JSDocNameReference: Conforms; + JSDocNonNullableType: Conforms; + JSDocNullableType: Conforms; + JSDocOptionalType: Conforms; + JSDocOverloadTag: Conforms; + JSDocOverrideTag: Conforms; + JSDocParameterTag: Conforms; + JSDocPrivateTag: Conforms; + JSDocPropertyTag: Conforms; + JSDocProtectedTag: Conforms; + JSDocPublicTag: Conforms; + JSDocReadonlyTag: Conforms; + JSDocReturnTag: Conforms; + JSDocSatisfiesTag: Conforms; + JSDocSeeTag: Conforms; + JSDocSignature: Conforms; + JSDocTagBase: Conforms; + JSDocTemplateTag: Conforms; + JSDocText: Conforms; + JSDocThisTag: Conforms; + JSDocThrowsTag: Conforms; + JSDocTypeBase: Conforms; + JSDocTypeExpression: Conforms; + JSDocTypeLiteral: Conforms< + Without, + Without + >; + JSDocTypeTag: Conforms; + JSDocTypedefTag: Conforms; + JSDocUnknownTag: Conforms; + JSDocVariadicType: Conforms; + JsxAttribute: Conforms; + JsxAttributes: Conforms; + JsxClosingElement: Conforms; + JsxClosingFragment: Conforms; + JsxElement: Conforms; + JsxExpression: Conforms; + JsxFragment: Conforms; + JsxNamespacedName: Conforms; + JsxOpeningElement: Conforms; + JsxOpeningFragment: Conforms; + JsxSelfClosingElement: Conforms; + JsxSpreadAttribute: Conforms; + JsxText: Conforms; + KeywordExpression: Conforms; + KeywordTypeNode: Conforms; + LabeledStatement: Conforms; + LeftHandSideExpressionBase: Conforms< + ast.LeftHandSideExpressionBase, + api.LeftHandSideExpressionBase + >; + LiteralExpressionBase: Conforms; + LiteralLikeNodeBase: Conforms; + LiteralTypeNode: Conforms; + MappedTypeNode: Conforms; + MemberExpressionBase: Conforms; + MetaProperty: Conforms; + MethodDeclaration: Conforms; + MethodSignatureDeclaration: Conforms< + ast.MethodSignatureDeclaration, + api.MethodSignatureDeclaration + >; + MissingDeclaration: Conforms; + ModifiersBase: Conforms; + ModuleBlock: Conforms; + ModuleDeclaration: Conforms; + NamedExports: Conforms; + NamedImports: Conforms; + NamedMemberBase: Conforms; + NamedTupleMember: Conforms; + NamespaceExport: Conforms; + NamespaceExportDeclaration: Conforms< + ast.NamespaceExportDeclaration, + api.NamespaceExportDeclaration + >; + NamespaceImport: Conforms; + NewExpression: Conforms; + NoSubstitutionTemplateLiteral: Conforms< + ast.NoSubstitutionTemplateLiteral, + api.NoSubstitutionTemplateLiteral + >; + NodeBase: Conforms; + NodeWithTypeArgumentsBase: Conforms; + NonNullExpression: Conforms; + NotEmittedStatement: Conforms; + NotEmittedTypeElement: Conforms; + NumericLiteral: Conforms; + ObjectBindingPattern: Conforms; + ObjectLiteralElementBase: Conforms; + ObjectLiteralExpression: Conforms; + OmittedExpression: Conforms; + OptionalTypeNode: Conforms; + ParameterDeclaration: Conforms; + ParenthesizedExpression: Conforms; + ParenthesizedTypeNode: Conforms; + PartiallyEmittedExpression: Conforms< + ast.PartiallyEmittedExpression, + api.PartiallyEmittedExpression + >; + PostfixUnaryExpression: Conforms; + PrefixUnaryExpression: Conforms; + PrimaryExpressionBase: Conforms; + PrivateIdentifier: Conforms; + PropertyAccessExpression: Conforms; + PropertyAssignment: Conforms; + PropertyDeclaration: Conforms; + PropertySignatureDeclaration: Conforms< + ast.PropertySignatureDeclaration, + api.PropertySignatureDeclaration + >; + QualifiedName: Conforms; + RegularExpressionLiteral: Conforms; + RestTypeNode: Conforms; + ReturnStatement: Conforms; + SatisfiesExpression: Conforms; + SemicolonClassElement: Conforms; + SetAccessorDeclaration: Conforms; + ShorthandPropertyAssignment: Conforms< + ast.ShorthandPropertyAssignment, + api.ShorthandPropertyAssignment + >; + SpreadAssignment: Conforms; + SpreadElement: Conforms; + StatementBase: Conforms; + StringLiteral: Conforms; + SwitchStatement: Conforms; + SyntaxList: Conforms, Without>; + SyntheticExpression: Conforms< + Without, + Without + >; + SyntheticReferenceExpression: Conforms< + ast.SyntheticReferenceExpression, + api.SyntheticReferenceExpression + >; + TaggedTemplateExpression: Conforms; + TemplateExpression: Conforms; + TemplateHead: Conforms; + TemplateLiteralLikeNodeBase: Conforms< + ast.TemplateLiteralLikeNodeBase, + api.TemplateLiteralLikeNodeBase + >; + TemplateLiteralTypeNode: Conforms; + TemplateLiteralTypeSpan: Conforms; + TemplateMiddle: Conforms; + TemplateSpan: Conforms; + TemplateTail: Conforms; + ThisTypeNode: Conforms; + ThrowStatement: Conforms; + Token: Conforms; + TryStatement: Conforms; + TupleTypeNode: Conforms; + TypeAliasDeclaration: Conforms; + TypeAssertion: Conforms; + TypeElementBase: Conforms; + TypeLiteralNode: Conforms; + TypeNodeBase: Conforms; + TypeOfExpression: Conforms; + TypeOperatorNode: Conforms; + TypeParameterDeclaration: Conforms; + TypePredicateNode: Conforms; + TypeQueryNode: Conforms; + TypeReferenceNode: Conforms; + UnaryExpressionBase: Conforms; + UnionOrIntersectionTypeNodeBase: Conforms< + ast.UnionOrIntersectionTypeNodeBase, + api.UnionOrIntersectionTypeNodeBase + >; + UnionTypeNode: Conforms; + UpdateExpressionBase: Conforms; + VariableDeclaration: Conforms; + VariableDeclarationList: Conforms; + VariableStatement: Conforms; + VoidExpression: Conforms; + WhileStatement: Conforms; + WithStatement: Conforms; + YieldExpression: Conforms; +} diff --git a/packages/compiler/src/frontend/ts7/ast-guards.generated.ts b/packages/compiler/src/frontend/ts7/ast-guards.generated.ts index aeaf56e9a..d163ac18d 100644 --- a/packages/compiler/src/frontend/ts7/ast-guards.generated.ts +++ b/packages/compiler/src/frontend/ts7/ast-guards.generated.ts @@ -4,6 +4,7 @@ import type { AbstractKeyword, AccessExpression, AccessorDeclaration, AccessorKeyword, AdditiveOperator, AdditiveOperatorOrHigher, AnyImportSyntax, ArrayBindingElement, ArrayBindingPattern, ArrayDestructuringAssignment, ArrayLiteralExpression, ArrayTypeNode, ArrowFunction, AsExpression, AssertKeyword, AssertionExpression, AssertsKeyword, AssignmentOperator, AssignmentOperatorOrHigher, AssignmentOperatorToken, AsteriskToken, AsyncKeyword, AwaitExpression, AwaitKeyword, BigIntLiteral, BinaryExpression, BinaryOperator, BinaryOperatorToken, BindingElement, BindingName, BindingPattern, BitwiseOperator, BitwiseOperatorOrHigher, Block, BlockOrExpression, BooleanLiteral, BreakOrContinueStatement, BreakStatement, CallExpression, CallLikeExpression, CallOrNewExpression, CallSignatureDeclaration, CaseBlock, CaseClause, CaseKeyword, CatchClause, ClassDeclaration, ClassExpression, ClassLikeDeclaration, ClassStaticBlockDeclaration, ColonToken, CompoundAssignmentOperator, ComputedPropertyName, ConciseBody, ConditionalExpression, ConditionalTypeNode, ConstKeyword, ConstructSignatureDeclaration, ConstructorDeclaration, ConstructorTypeNode, ContinueStatement, DebuggerStatement, DeclarationName, DeclareKeyword, Decorator, DefaultClause, DefaultKeyword, DeleteExpression, DestructuringAssignment, DoStatement, DotDotDotToken, DotToken, ElementAccessExpression, EmptyStatement, EndOfFile, EntityName, EnumDeclaration, EnumMember, EqualityOperator, EqualityOperatorOrHigher, EqualsGreaterThanToken, EqualsToken, ExclamationToken, ExponentiationOperator, ExportAssignment, ExportDeclaration, ExportKeyword, ExportSpecifier, Expression, ExpressionStatement, ExpressionWithTypeArguments, ExternalModuleReference, FalseLiteral, ForInStatement, ForInitializer, ForOfStatement, ForStatement, FunctionBody, FunctionDeclaration, FunctionExpression, FunctionLikeDeclaration, FunctionTypeNode, GetAccessorDeclaration, HeritageClause, Identifier, IfStatement, ImportAttribute, ImportAttributeName, ImportAttributes, ImportClause, ImportClauseOrBindingPattern, ImportDeclaration, ImportEqualsDeclaration, ImportExpression, ImportPhaseModifierSyntaxKind, ImportSpecifier, ImportTypeNode, InKeyword, IndexSignatureDeclaration, IndexedAccessTypeNode, InferTypeNode, InterfaceDeclaration, IntersectionTypeNode, JSDoc, JSDocAllType, JSDocAugmentsTag, JSDocCallbackTag, JSDocComment, JSDocDeprecatedTag, JSDocFullName, JSDocImplementsTag, JSDocImportTag, JSDocLink, JSDocLinkCode, JSDocLinkPlain, JSDocNameReference, JSDocNonNullableType, JSDocNullableType, JSDocOptionalType, JSDocOverloadTag, JSDocOverrideTag, JSDocParameterTag, JSDocPrivateTag, JSDocPropertyTag, JSDocProtectedTag, JSDocPublicTag, JSDocReadonlyTag, JSDocReturnTag, JSDocSatisfiesTag, JSDocSeeTag, JSDocSignature, JSDocTemplateTag, JSDocText, JSDocThisTag, JSDocThrowsTag, JSDocTypeExpression, JSDocTypeLiteral, JSDocTypeTag, JSDocTypedefTag, JSDocUnknownTag, JSDocVariadicType, JsxAttribute, JsxAttributeLike, JsxAttributeName, JsxAttributeValue, JsxAttributes, JsxChild, JsxClosingElement, JsxClosingFragment, JsxElement, JsxExpression, JsxFragment, JsxNamespacedName, JsxOpeningElement, JsxOpeningFragment, JsxOpeningLikeElement, JsxSelfClosingElement, JsxSpreadAttribute, JsxTagNameExpression, JsxText, JsxTokenSyntaxKind, KeywordExpression, KeywordExpressionSyntaxKind, KeywordTypeNode, KeywordTypeSyntaxKind, LabeledStatement, LeftHandSideExpression, LiteralExpression, LiteralLikeNode, LiteralToken, LiteralTypeNode, LogicalOperator, LogicalOperatorOrHigher, LogicalOrCoalescingAssignmentOperator, MappedTypeNode, MemberName, MetaProperty, MethodDeclaration, MethodSignatureDeclaration, MinusToken, MissingDeclaration, Modifier, ModifierLike, ModifierSyntaxKind, ModuleBlock, ModuleBody, ModuleDeclaration, ModuleExportName, ModuleName, ModuleReference, MultiplicativeOperator, MultiplicativeOperatorOrHigher, NamedExportBindings, NamedExports, NamedImportBindings, NamedImports, NamedImportsOrExports, NamedTupleMember, NamespaceExport, NamespaceExportDeclaration, NamespaceImport, NewExpression, NoSubstitutionTemplateLiteral, Node, NonNullExpression, NotEmittedStatement, NotEmittedTypeElement, NullLiteral, NumericLiteral, NumericOrStringLikeLiteral, ObjectBindingPattern, ObjectDestructuringAssignment, ObjectLiteralElementLike, ObjectLiteralExpression, ObjectLiteralLikeNode, ObjectTypeDeclaration, OmittedExpression, OptionalTypeNode, OutKeyword, OverrideKeyword, ParameterDeclaration, ParenthesizedExpression, ParenthesizedTypeNode, PartiallyEmittedExpression, PlusToken, PostfixUnaryExpression, PostfixUnaryOperator, PrefixUnaryExpression, PrefixUnaryOperator, PrivateIdentifier, PrivateKeyword, PropertyAccessExpression, PropertyAssignment, PropertyDeclaration, PropertyName, PropertyNameLiteral, PropertySignatureDeclaration, ProtectedKeyword, PseudoLiteralSyntaxKind, PseudoLiteralToken, PublicKeyword, QualifiedName, QuestionDotToken, QuestionToken, ReadonlyKeyword, RegularExpressionLiteral, RelationalOperator, RelationalOperatorOrHigher, RestTypeNode, ReturnStatement, SatisfiesExpression, SemicolonClassElement, SetAccessorDeclaration, ShiftOperator, ShiftOperatorOrHigher, ShorthandPropertyAssignment, SignatureDeclaration, SourceFile, SpreadAssignment, SpreadElement, Statement, StaticKeyword, StringLiteral, StringLiteralLikeNode, SuperExpression, SwitchStatement, SyntaxList, SyntheticExpression, SyntheticReferenceExpression, TaggedTemplateExpression, TemplateExpression, TemplateHead, TemplateLiteral, TemplateLiteralLikeNode, TemplateLiteralToken, TemplateLiteralTypeNode, TemplateLiteralTypeSpan, TemplateMiddle, TemplateMiddleOrTail, TemplateSpan, TemplateTail, ThisExpression, ThisTypeNode, ThrowStatement, Token, TriviaSyntaxKind, TrueLiteral, TryStatement, TupleTypeNode, TypeAliasDeclaration, TypeAssertion, TypeLiteralNode, TypeNode, TypeOfExpression, TypeOperatorNode, TypeParameterDeclaration, TypePredicateNode, TypePredicateParameterName, TypeQueryNode, TypeReferenceNode, UnaryExpressionBase, UnionOrIntersectionTypeNode, UnionTypeNode, VariableDeclaration, VariableDeclarationList, VariableOrParameterDeclaration, VariableOrPropertyDeclaration, VariableStatement, VoidExpression, WhileStatement, WithStatement, YieldExpression } from "./ast-types.js"; import { SyntaxKind, NodeFlags, ScriptKind, OuterExpressionKinds } from "./enums.js"; +import { getSourceFileOfNode } from "./node-functions.js"; export function isToken(node: Node | undefined): node is Token { if (node === undefined) return false; @@ -1913,7 +1914,7 @@ export function skipOuterExpressions(node: Node, kinds: OuterExpressionKinds = O function isJSDocTypeAssertion(node: Node | undefined): boolean { if (node === undefined) return false; - const sourceFile = node.getSourceFile(); + const sourceFile = getSourceFileOfNode(node); if (sourceFile.scriptKind !== ScriptKind.JS && sourceFile.scriptKind !== ScriptKind.JSX) { return false; } diff --git a/packages/compiler/src/frontend/ts7/ast-node.test.ts b/packages/compiler/src/frontend/ts7/ast-node.test.ts index e2440b787..b9b8f0e7b 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.test.ts @@ -20,7 +20,13 @@ import { walkPreorder } from "./ast.js"; import { ts7Executable } from "./rpc-api.js"; import { Ts7RpcClient } from "./rpc-client.js"; import { spawnTs7Wire } from "./rpc-process.js"; -import { isJSDocTypeLiteral, isJSDocPropertyTag } from "./ast-guards.generated.js"; +import { + isArrayLiteralExpression, + isCallExpression, + isJSDocTypeLiteral, + isJSDocPropertyTag, +} from "./ast-guards.generated.js"; +import { ModifierFlags, SyntaxKind } from "./enums.js"; const require = createRequire(import.meta.url); const sdkRoot = dirname(require.resolve("typescript/package.json")); @@ -95,6 +101,8 @@ const cases: Record = { '/** @typedef {Object} Options\n * @property {string} name\n * @property {number} count\n */\n/** @type {Options} */ const options = { name: "x", count: 2 };', "missing.ts": "const incomplete = ;\nfunction f( {\n", "empty.ts": "", + "lists.ts": + "const trailing = [1, 2 /* last */, ];\nconst plain = [1, 2];\nf(1, 2,);\nexport abstract class Flags { protected static readonly value = 1; }\n", }; const decoded = new Map(); let directory: string; @@ -159,6 +167,26 @@ function ids( return nodes?.map((n) => (n as OracleNode).index); } +test("node lists report a trailing comma and modifier lists their flags", () => { + const { file } = decoded.get("lists.ts")!; + const lists: [SyntaxKind, boolean][] = []; + const modifiers: number[] = []; + walkPreorder(file.root, (node) => { + if (isArrayLiteralExpression(node)) lists.push([node.kind, node.elements.hasTrailingComma()]); + if (isCallExpression(node)) lists.push([node.kind, node.arguments.hasTrailingComma()]); + if (node.modifiers !== undefined) modifiers.push(node.modifiers.modifierFlags); + }); + expect(lists).toEqual([ + [SyntaxKind.ArrayLiteralExpression, true], + [SyntaxKind.ArrayLiteralExpression, false], + [SyntaxKind.CallExpression, true], + ]); + expect(modifiers).toEqual([ + ModifierFlags.Export | ModifierFlags.Abstract, + ModifierFlags.Protected | ModifierFlags.Static | ModifierFlags.Readonly, + ]); +}); + test("JSDoc property refinements describe the pinned native wire", () => { const { file, oracle } = decoded.get("properties.js")!; let checked = 0; diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index 3760fff8d..8eaa87c72 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -35,6 +35,25 @@ export class AstNodeList { get end(): number { return this.wire.end(this.index) >>> 0; } + + /** Whether a comma follows the last node before the list's end. */ + hasTrailingComma(): boolean { + const last = this.nodes[this.nodes.length - 1]; + if (last === undefined) return false; + const text = last.getSourceFile().text; + const next = astSkipTrivia(text, last.end, false, false); + return next < this.end && text.charCodeAt(next) === 44; + } +} + +/** A declaration's modifiers, with the flags of all of them: tsc-ts's + * ModifierList. */ +export class AstModifierList extends AstNodeList { + get modifierFlags(): number { + let result = 0; + for (const modifier of this.nodes) result |= astModifierFlag(modifier.kind); + return result; + } } /** One immutable AST response owns one object per node and one array per @@ -85,6 +104,12 @@ export class AstFile { } list(index: number): AstNodeList { + return new AstNodeList(this.wire, index, this.listNodes(index)); + } + modifierList(index: number): AstModifierList { + return new AstModifierList(this.wire, index, this.listNodes(index)); + } + private listNodes(index: number): AstNode[] { let nodes = this.lists[index]; if (nodes === undefined) { nodes = []; @@ -92,7 +117,7 @@ export class AstFile { this.lists[index] = nodes; this.materialized?.(); } - return new AstNodeList(this.wire, index, nodes); + return nodes; } namedChild(node: AstNode, name: string): number { @@ -422,6 +447,11 @@ export class AstNode { return result; } + get modifiers(): AstModifierList | undefined { + const index = this.file.namedChild(this, "modifiers"); + return index === 0 ? undefined : this.file.modifierList(index); + } + get body(): AstNode | undefined { const body = this.childNode("body"); // TS7 can serialize a reparsed @type callable before a method's @@ -599,9 +629,6 @@ export class AstNode { get members(): AstNodeList | undefined { return this.childList("members"); } - get modifiers(): AstNodeList | undefined { - return this.childList("modifiers"); - } get moduleReference(): AstNode | undefined { return this.childNode("moduleReference"); } diff --git a/packages/compiler/src/frontend/ts7/ast-types.generated.ts b/packages/compiler/src/frontend/ts7/ast-types.generated.ts index de6ff892f..59f01e9ea 100644 --- a/packages/compiler/src/frontend/ts7/ast-types.generated.ts +++ b/packages/compiler/src/frontend/ts7/ast-types.generated.ts @@ -3,7 +3,7 @@ // Regenerate when changing the TypeScript pin; do not edit by hand. import type { AstNode } from "./ast-node.js"; -import type { JsxTagNamePropertyAccess, NodeArray } from "./ast-types.js"; +import type { JsxTagNamePropertyAccess, ModifierArray, NodeArray } from "./ast-types.js"; import { SyntaxKind, NodeFlags, ModifierFlags, TokenFlags } from "./enums.js"; type Node = AstNode; @@ -142,7 +142,7 @@ export interface DeclarationBase extends Node { } export interface ModifiersBase extends Node { - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.VariableStatement | SyntaxKind.Parameter | SyntaxKind.MissingDeclaration | SyntaxKind.FunctionDeclaration | SyntaxKind.ClassDeclaration | SyntaxKind.ClassExpression | SyntaxKind.InterfaceDeclaration | SyntaxKind.TypeAliasDeclaration | SyntaxKind.EnumMember | SyntaxKind.EnumDeclaration | SyntaxKind.ImportDeclaration | SyntaxKind.ExportAssignment | SyntaxKind.NamespaceExportDeclaration | SyntaxKind.Constructor | SyntaxKind.GetAccessor | SyntaxKind.SetAccessor | SyntaxKind.IndexSignature | SyntaxKind.MethodSignature | SyntaxKind.MethodDeclaration | SyntaxKind.PropertySignature | SyntaxKind.PropertyDeclaration | SyntaxKind.ClassStaticBlockDeclaration | SyntaxKind.BinaryExpression | SyntaxKind.ArrowFunction | SyntaxKind.FunctionExpression | SyntaxKind.PropertyAssignment | SyntaxKind.ShorthandPropertyAssignment | SyntaxKind.FunctionType | SyntaxKind.ConstructorType | SyntaxKind.ModuleDeclaration | SyntaxKind.ImportEqualsDeclaration | SyntaxKind.ExportDeclaration | SyntaxKind.TypeParameter; } @@ -170,7 +170,7 @@ export interface FunctionLikeWithBodyBase extends Node { } export interface ClassLikeBase extends Node { - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: Identifier | undefined; readonly typeParameters: NodeArray | undefined; @@ -209,7 +209,7 @@ export interface ClassElementBase extends Node { } export interface NamedMemberBase extends Node { - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -397,7 +397,7 @@ export interface Block extends Node { export interface VariableStatement extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.VariableStatement; readonly declarationList: VariableDeclarationList; @@ -420,7 +420,7 @@ export interface VariableDeclarationList extends Node { export interface ParameterDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.Parameter; readonly dotDotDotToken: DotDotDotToken | undefined; @@ -441,14 +441,14 @@ export interface BindingElement extends Node { export interface MissingDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.MissingDeclaration; } export interface FunctionDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -461,7 +461,7 @@ export interface FunctionDeclaration extends Node { export interface ClassDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: Identifier | undefined; readonly typeParameters: NodeArray | undefined; @@ -472,7 +472,7 @@ export interface ClassDeclaration extends Node { export interface ClassExpression extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: Identifier | undefined; readonly typeParameters: NodeArray | undefined; @@ -490,7 +490,7 @@ export interface HeritageClause extends Node { export interface InterfaceDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.InterfaceDeclaration; readonly name: Identifier; @@ -501,7 +501,7 @@ export interface InterfaceDeclaration extends Node { export interface TypeAliasDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.TypeAliasDeclaration; readonly name: Identifier; @@ -511,7 +511,7 @@ export interface TypeAliasDeclaration extends Node { export interface EnumMember extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -521,7 +521,7 @@ export interface EnumMember extends Node { export interface EnumDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.EnumDeclaration; readonly name: Identifier; @@ -546,7 +546,7 @@ export interface NotEmittedTypeElement extends Node { export interface ImportDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.ImportDeclaration; readonly importClause: ImportClause | undefined; @@ -574,7 +574,7 @@ export interface NamedImports extends Node { export interface ExportAssignment extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.ExportAssignment; readonly isExportEquals: boolean; @@ -584,7 +584,7 @@ export interface ExportAssignment extends Node { export interface NamespaceExportDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.NamespaceExportDeclaration; readonly name: Identifier; @@ -628,7 +628,7 @@ export interface ConstructSignatureDeclaration extends Node { export interface ConstructorDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -639,7 +639,7 @@ export interface ConstructorDeclaration extends Node { } export interface GetAccessorDeclaration extends Node { - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -653,7 +653,7 @@ export interface GetAccessorDeclaration extends Node { } export interface SetAccessorDeclaration extends Node { - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -668,7 +668,7 @@ export interface SetAccessorDeclaration extends Node { export interface IndexSignatureDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -678,7 +678,7 @@ export interface IndexSignatureDeclaration extends Node { export interface MethodSignatureDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -690,7 +690,7 @@ export interface MethodSignatureDeclaration extends Node { export interface MethodDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -704,7 +704,7 @@ export interface MethodDeclaration extends Node { export interface PropertySignatureDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -715,7 +715,7 @@ export interface PropertySignatureDeclaration extends Node { export interface PropertyDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -731,7 +731,7 @@ export interface SemicolonClassElement extends Node { export interface ClassStaticBlockDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.ClassStaticBlockDeclaration; readonly body: Block; @@ -786,7 +786,7 @@ export interface NoSubstitutionTemplateLiteral extends Node { export interface BinaryExpression extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.BinaryExpression; readonly left: Expression; @@ -818,7 +818,7 @@ export interface YieldExpression extends Node { export interface ArrowFunction extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -831,7 +831,7 @@ export interface ArrowFunction extends Node { export interface FunctionExpression extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -969,7 +969,7 @@ export interface SpreadAssignment extends Node { export interface PropertyAssignment extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -980,7 +980,7 @@ export interface PropertyAssignment extends Node { export interface ShorthandPropertyAssignment extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -1180,7 +1180,7 @@ export interface ParenthesizedTypeNode extends Node { export interface FunctionTypeNode extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -1190,7 +1190,7 @@ export interface FunctionTypeNode extends Node { export interface ConstructorTypeNode extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -1562,7 +1562,7 @@ export interface JSDocNameReference extends Node { export interface ModuleDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly asteriskToken: AsteriskToken | undefined; readonly body: ModuleBody | undefined; @@ -1573,7 +1573,7 @@ export interface ModuleDeclaration extends Node { export interface ImportEqualsDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.ImportEqualsDeclaration; readonly isTypeOnly: boolean; @@ -1583,7 +1583,7 @@ export interface ImportEqualsDeclaration extends Node { export interface ExportDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.ExportDeclaration; readonly isTypeOnly: boolean; @@ -1647,7 +1647,7 @@ export interface JSDocLinkCode extends Node { export interface TypeParameterDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.TypeParameter; readonly name: Identifier; @@ -1842,7 +1842,7 @@ export interface CaseClause extends Node { export interface DefaultClause extends Node { readonly flags: NodeFlags; readonly kind: SyntaxKind.DefaultClause; - readonly expression: Expression; + readonly expression: undefined; readonly statements: NodeArray; } diff --git a/packages/compiler/src/frontend/ts7/ast-types.ts b/packages/compiler/src/frontend/ts7/ast-types.ts index 6d421e58d..e40e40e30 100644 --- a/packages/compiler/src/frontend/ts7/ast-types.ts +++ b/packages/compiler/src/frontend/ts7/ast-types.ts @@ -1,4 +1,4 @@ -import type { AstNode, AstNodeList } from "./ast-node.js"; +import type { AstModifierList, AstNode, AstNodeList } from "./ast-node.js"; import { SyntaxKind } from "./enums.js"; import type { EndOfFile, @@ -14,8 +14,11 @@ export type Node = AstNode; /** A node list. The type is the list class itself, not a structural shape: * native code represents a class and a record type differently. */ export type NodeArray = AstNodeList; +/** A declaration's modifiers: a node list with the flags of all of them. */ +export type ModifierArray = AstModifierList; export type Path = string; -export type __String = string; +/** An escaped symbol name, the key of member and export tables (TypeScript 7's `__String`). */ +export type __String = string & { readonly __escapedIdentifier: void }; export interface TextRange { pos: number; end: number; diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index ec26aa0b0..a92cfafd8 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -53,6 +53,7 @@ import { ModifierFlags, SyntaxKind } from "./enums.js"; // The frontend's node types, guards and token spellings share the concrete // AstNode layout. Parser and transform factories remain outside this client. export * from "./ast-types.js"; +export * from "./node-functions.js"; export * from "./ast-guards.generated.js"; export { tokenToString } from "./ast-tokens.generated.js"; diff --git a/packages/compiler/src/frontend/ts7/checker.test.ts b/packages/compiler/src/frontend/ts7/checker.test.ts index 7b3647b2d..ac062b283 100644 --- a/packages/compiler/src/frontend/ts7/checker.test.ts +++ b/packages/compiler/src/frontend/ts7/checker.test.ts @@ -10,10 +10,10 @@ import { ts7Executable } from "./rpc-api.js"; import { Ts7Session } from "./session.js"; import { SemanticChecker } from "./semantic-checker.js"; import { SemanticSnapshot } from "./semantic-model.js"; -import { TypeFlags } from "./enums.js"; +import { SignatureKind, SymbolFlags, TypeFlags } from "./enums.js"; import { createProgram, Ts7Host } from "./program-adapter.js"; import type { AstNode } from "./ast-node.js"; -import type { SourceFile } from "./ast-types.js"; +import type { Identifier, SourceFile } from "./ast-types.js"; const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const protocolPath = (path: string) => path.replaceAll("\\", "/"); @@ -167,6 +167,12 @@ test("all public query and prefetch entry points reject a disposed facade before const calls: Record unknown> = { getCallSignatures: () => facade.getCallSignatures(type), getConstructSignatures: () => facade.getConstructSignatures(type), + getSignaturesOfType: () => facade.getSignaturesOfType(type, SignatureKind.Call), + resolveName: () => facade.resolveName("value", SymbolFlags.Value, name), + getResolvedSymbol: () => facade.getResolvedSymbol(name as Identifier), + getAnyType: () => facade.getAnyType(), + getNeverType: () => facade.getNeverType(), + getBigIntType: () => facade.getBigIntType(), getPropertyOfType: () => facade.getPropertyOfType(type, "missing"), prefetchSourceFile: () => facade.prefetchSourceFile(root), prefetchSourceFileStructures: () => facade.prefetchSourceFileStructures([]), diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index 41eb0d0b9..4f2d60294 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -35,7 +35,7 @@ import { CheckerCache } from "./checker-cache.js"; * repeats an IPC request. All paths are verified against the raw checker * and against 5.9.3 by the adapter's suites. */ -import type { Node, SourceFile } from "./ast-types.js"; +import type { Identifier, Node, SourceFile } from "./ast-types.js"; import type { IndexInfo, InterfaceType, @@ -46,9 +46,10 @@ import type { TypeReference, } from "./semantic-types.js"; import type { SemanticChecker as Checker } from "./semantic-checker.js"; -import type { SemanticProject as Project } from "./semantic-model.js"; +import { symbolNameOf, type SemanticProject as Project } from "./semantic-model.js"; import { isTypeNode, walkPreorder } from "./ast.js"; -import { SignatureKind, SyntaxKind, TypeFlags } from "./enums.js"; +import { SignatureKind, SyntaxKind, TypeFlags, type SymbolFlags } from "./enums.js"; +import { getSourceFileOfNode } from "./node-functions.js"; /** Array-overload chunk size: large enough that per-request overhead * vanishes, small enough to keep any single JSON-RPC payload modest. */ @@ -259,6 +260,14 @@ export class CheckerFacade { return sigs; } + /** The call or construct signatures of `type`, as getCallSignatures and getConstructSignatures + * answer them. */ + getSignaturesOfType(type: Type, kind: SignatureKind): readonly Signature[] { + return kind === SignatureKind.Call + ? this.getCallSignatures(type) + : this.getConstructSignatures(type); + } + /** 5.9.3's type.getProperty(name). */ getPropertyOfType(type: Type, name: string): Ts7Symbol | undefined { this.ensureActive(); @@ -370,7 +379,7 @@ export class CheckerFacade { private markManaged(roots: readonly Node[]): void { for (const root of roots) { - const sf = root.getSourceFile(); + const sf = getSourceFileOfNode(root); this.cache.managedTypes.add(sf); this.cache.managedSymbols.add(sf); } @@ -447,7 +456,7 @@ export class CheckerFacade { private autoPrefetch(node: Node, kind: "types" | "symbols"): void { if (this.options.autoPrefetch === false) return; - const sf = node.getSourceFile(); + const sf = getSourceFileOfNode(node); // A referenced declaration rarely needs the rest of its library's // metadata. Keep those queries direct; explicit whole-file prefetch // remains available to consumers that do need the complete surface. @@ -786,6 +795,38 @@ export class CheckerFacade { return this.raw.getTypeOfSymbolAtLocation(symbol, location); } + resolveName( + name: string, + meaning: SymbolFlags, + location?: Node, + excludeGlobals?: boolean, + ): Ts7Symbol | undefined { + this.ensureActive(); + return this.raw.resolveName(name, meaning, location, excludeGlobals); + } + + /** The value `node` names where it is: resolveName of its text with SymbolFlags.Value and + * SymbolFlags.ExportValue. */ + getResolvedSymbol(node: Identifier): Ts7Symbol | undefined { + this.ensureActive(); + return this.raw.getResolvedSymbol(node); + } + + getAnyType(): Type { + this.ensureActive(); + return this.anyType(); + } + + getNeverType(): Type { + this.ensureActive(); + return this.intrinsic("never", () => this.raw.getNeverType()); + } + + getBigIntType(): Type { + this.ensureActive(); + return this.intrinsic("bigint", () => this.raw.getBigIntType()); + } + getUnknownType(): Type { this.ensureActive(); if (this.cache.unknownType === null) this.cache.unknownType = this.raw.getUnknownType(); @@ -851,7 +892,7 @@ export class CheckerFacade { * programs see the es2025 lib's Promise (the ambient world forces it). */ private promiseArgumentOf(type: Type): Type | null { if (!type.isTypeReference()) return null; - const name = type.getTarget()?.getSymbol()?.name; + const name = symbolNameOf(type.getTarget()?.getSymbol()); if (name !== "Promise" && name !== "PromiseLike") return null; const args = this.getTypeArguments(type); return args[0] ?? null; diff --git a/packages/compiler/src/frontend/ts7/diagnostic.ts b/packages/compiler/src/frontend/ts7/diagnostic.ts new file mode 100644 index 000000000..db3ff0e63 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/diagnostic.ts @@ -0,0 +1,38 @@ +import type { DiagnosticCategory } from "./enums.generated.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; + +/** A diagnostic with every member set, as tsc-ts/api's Diagnostic has them. The server leaves out + * the members a diagnostic doesn't have. `fileName` is undefined for global and program + * diagnostics. */ +export interface Ts7Diagnostic { + readonly fileName: string | undefined; + readonly pos: number; + readonly end: number; + readonly code: number; + readonly category: DiagnosticCategory; + readonly text: string; + readonly reportsUnnecessary: boolean; + readonly reportsDeprecated: boolean; + readonly messageChain: readonly Ts7Diagnostic[]; + readonly relatedInformation: readonly Ts7Diagnostic[]; +} + +/** `data` with its absent members set: undefined, false, or no chained or related diagnostics. */ +export function ts7Diagnostic(data: Ts7DiagnosticData): Ts7Diagnostic { + const chain: Ts7Diagnostic[] = []; + for (const item of data.messageChain ?? []) chain.push(ts7Diagnostic(item)); + const related: Ts7Diagnostic[] = []; + for (const item of data.relatedInformation ?? []) related.push(ts7Diagnostic(item)); + return { + fileName: data.fileName, + pos: data.pos, + end: data.end, + code: data.code, + category: data.category, + text: data.text, + reportsUnnecessary: data.reportsUnnecessary ?? false, + reportsDeprecated: data.reportsDeprecated ?? false, + messageChain: chain, + relatedInformation: related, + }; +} diff --git a/packages/compiler/src/frontend/ts7/node-functions.ts b/packages/compiler/src/frontend/ts7/node-functions.ts new file mode 100644 index 000000000..3b37779e8 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/node-functions.ts @@ -0,0 +1,23 @@ +/* Position and text functions over nodes, with tsc-ts/api's names. The + * frontend calls these rather than node methods, so that its node types can + * be tsc-ts/api's interfaces, which have no methods. Today every node is an + * AstNode, and each function forwards to the AstNode method. */ + +import type { AstNode } from "./ast-node.js"; +import type { Node, SourceFile } from "./ast-types.js"; + +/** The start of the node's first token, after its leading trivia, and before + * its JSDoc comments when `includeJSDoc` is true. */ +export function tokenStartOfNode(node: Node, includeJSDoc = false): number { + return (node as AstNode).getStart(undefined, includeJSDoc); +} + +/** The node's text from its first token to its end. */ +export function getTextOfNode(node: Node): string { + return (node as AstNode).getText(); +} + +/** The source file that contains the node. */ +export function getSourceFileOfNode(node: Node): SourceFile { + return (node as AstNode).getSourceFile(); +} diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 2f2fdf76b..dbc0804d5 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -21,7 +21,7 @@ import { dirname, join, resolve } from "node:path"; import { Ts7Api } from "./api.js"; import type { Ts7TimingInfo } from "./session-timing.js"; import type { Ts7FileSystem } from "./rpc-filesystem.js"; -import type { CompilerOptions, Diagnostic } from "./semantic-types.js"; +import type { CompilerOptions, Diagnostic, SourceFileMetadata } from "./semantic-types.js"; import type { SourceFile } from "./ast-types.js"; import type { Ts7SessionProject as Project, Ts7SessionSnapshot as Snapshot } from "./session.js"; import { CheckerFacade } from "./checker.js"; @@ -343,10 +343,22 @@ export class Ts7Program { return this.project.program.getSyntacticDiagnostics(sourceFile?.fileName); } + getBindDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] { + return this.project.program.getBindDiagnostics(sourceFile?.fileName); + } + getSemanticDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] { return this.project.program.getSemanticDiagnostics(sourceFile?.fileName); } + getSuggestionDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] { + return this.project.program.getSuggestionDiagnostics(sourceFile?.fileName); + } + + getDeclarationDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] { + return this.project.program.getDeclarationDiagnostics(sourceFile?.fileName); + } + getGlobalDiagnostics(): readonly Diagnostic[] { return this.project.program.getGlobalDiagnostics(); } @@ -359,6 +371,10 @@ export class Ts7Program { return this.project.program.getConfigFileParsingDiagnostics(); } + getSourceFileMetadata(fileName: string): SourceFileMetadata | undefined { + return this.project.program.getSourceFileMetadata(resolve(fileName)); + } + isSourceFileDefaultLibrary(file: SourceFile): boolean { return this.project.program.isSourceFileDefaultLibrary(file); } diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts index 5c5f2e2fc..90a7a25cf 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts @@ -10,6 +10,8 @@ import { tsgoExecutable } from "./rpc-api.js"; import { spawnTs7Wire } from "./rpc-process.js"; import { Ts7Session } from "./session.js"; import { SyntaxKind } from "./enums.js"; +import { getSourceFileOfNode } from "./node-functions.js"; +import { forEachChild } from "./ast.js"; const source = [ "export class Box { value = 1; }", @@ -98,15 +100,15 @@ test("synthetic type roots and descendants match the pinned SDK without claiming expect(actual.file.node(1)).toBe(actual); expect(actual.parent).toBeUndefined(); expect(() => actual.file.sourceFile).toThrow("expected a source file root"); - expect(() => actual.getSourceFile()).toThrow("expected a source file root"); - actual.forEachChild((child) => { + expect(() => getSourceFileOfNode(actual)).toThrow("expected a source file root"); + forEachChild(actual, (child) => { expect(child.parent).toBe(actual); }); checked++; } expect(checked).toBe(6); expect(root.file.sourceFile).toBe(root); - expect(root.getSourceFile()).toBe(root); + expect(getSourceFileOfNode(root)).toBe(root); } finally { snapshot.dispose(); sdkSnapshot.dispose(); diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.ts b/packages/compiler/src/frontend/ts7/semantic-checker.ts index 17ca43bf8..92aa77333 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.ts @@ -16,16 +16,32 @@ import { type SemanticQuery, } from "./semantic-model.js"; import { SemanticSymbolFlags } from "./semantic-schema.generated.js"; +import type { TypePredicateKind } from "./enums.generated.js"; -export interface SemanticIndexInfo { - keyType: SemanticType; - valueType: SemanticType; - isReadonly: boolean; - declaration: SemanticNodeHandle | undefined; +/** An index signature of a type. Its declaration resolves on first read: most queries only count a + * type's index infos. */ +export class SemanticIndexInfo { + private declarationNode: AstNode | undefined; + private declarationResolved = false; + + constructor( + readonly keyType: SemanticType, + readonly type: SemanticType, + readonly isReadonly: boolean, + private readonly declarationHandle: SemanticNodeHandle | undefined, + ) {} + + get declaration(): AstNode | undefined { + if (!this.declarationResolved) { + this.declarationNode = this.declarationHandle?.resolve(); + this.declarationResolved = true; + } + return this.declarationNode; + } } export interface SemanticTypePredicate { - kind: number; + kind: TypePredicateKind; parameterIndex: number; parameterName: string | undefined; type: SemanticType | undefined; @@ -382,15 +398,17 @@ export class SemanticChecker { const data = this.project.request("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), - })); + return (data ?? []).map( + (item) => + new SemanticIndexInfo( + this.project.type(item.keyType), + this.project.type(item.valueType), + item.isReadonly ?? false, + item.declaration === undefined + ? undefined + : new SemanticNodeHandle(item.declaration, this.project), + ), + ); } private getWellKnownSymbols(): WellKnownSymbols { this.project.ensureActive(); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.test.ts b/packages/compiler/src/frontend/ts7/semantic-model.test.ts index e0230e8fe..36a445bc8 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.test.ts @@ -23,7 +23,13 @@ import { isUnionType, type Type, } from "typescript/unstable/sync"; -import { SemanticNodeHandle, SemanticSnapshot, SemanticType } from "./semantic-model.js"; +import { + SemanticNodeHandle, + SemanticSnapshot, + SemanticType, + symbolNameOf, +} from "./semantic-model.js"; +import type { __String } from "./ast-types.js"; import { SemanticObjectFlags as ObjectFlags, SemanticSignatureFlags as SignatureFlags, @@ -97,7 +103,7 @@ test("absent handles and empty known lists do not issue requests", () => { expect(type.getExtendsType()).toBeUndefined(); expect(type.getBaseType()).toBeUndefined(); expect(type.getConstraint()).toBeUndefined(); - expect(type.getTypes()).toBeUndefined(); + expect(type.getTypes()).toEqual([]); expect(type.getBaseTypes()).toBeUndefined(); expect(type.getTypeParameters()).toEqual([]); expect(type.getOuterTypeParameters()).toEqual([]); @@ -304,8 +310,8 @@ test("symbol tables keep escaped keys, display names, canonical context and cach 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(symbolNameOf(members.get("___x" as __String))).toBe("__x"); + expect(symbolNameOf(exports.get("__x" as __String))).toBe("__x"); expect(parent.getMembers()).toBe(members); expect(parent.getExports()).toBe(exports); expect(parent.getParent()).toBe(first.symbol(symbol(2))); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index 4920c96ae..5d1bab78e 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -4,6 +4,8 @@ import type { SemanticTypeData as TypeResponse, } from "./semantic-schema.generated.js"; import { AstNode } from "./ast-node.js"; +import type { __String } from "./ast-types.js"; +import type * as enums from "./enums.generated.js"; import { parseAstNodeHandle } from "./ast-wire.js"; import type { SemanticChecker, SemanticJsDocTag } from "./semantic-checker.js"; import { parseSemanticJson } from "./semantic-json.js"; @@ -290,9 +292,9 @@ export class SemanticNodeHandle { export class SemanticSymbol { readonly id: number; - readonly escapedName: string; - readonly name: string; - readonly flags: number; + /** The key of the member and export tables the symbol is in; `symbolName` gives its name. */ + readonly escapedName: __String; + readonly flags: enums.SymbolFlags; readonly checkFlags: number; /** The declarations' node handles, as the server sends them. */ readonly declarationHandles: SemanticNodeHandle[]; @@ -302,16 +304,15 @@ export class SemanticSymbol { private valueDeclarationResolved = false; private readonly parent: number | undefined; private readonly exportSymbol: number | undefined; - private membersCache: Map | undefined; - private exportsCache: Map | 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.escapedName = data.name as __String; this.flags = data.flags; this.checkFlags = data.checkFlags; this.declarationHandles = (data.declarations ?? []).map( @@ -362,18 +363,18 @@ export class SemanticSymbol { if (result === undefined) throw new Error(`Missing export symbol for ${this.id}`); return result; } - private table(method: string): Map { - const table = new Map(); + 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 { + getMembers(): Map<__String, SemanticSymbol> { this.canonicalProject.ensureActive(); if (this.membersCache === undefined) this.membersCache = this.table("getMembersOfSymbol"); return this.membersCache; } - getExports(): Map { + getExports(): Map<__String, SemanticSymbol> { this.canonicalProject.ensureActive(); if (this.exportsCache === undefined) this.exportsCache = this.table("getExportsOfSymbol"); return this.exportsCache; @@ -392,10 +393,26 @@ export class SemanticSymbol { } } +/** The name of `symbol`, as TypeScript 7's API gives it: its escaped name, with the extra `_` of a + * name that starts with `__` removed. */ +export function symbolName(symbol: SemanticSymbol): string { + const name: string = symbol.escapedName; + return name.startsWith("___") ? name.slice(1) : name; +} + +/** `symbolName(symbol)`, or undefined without a symbol. */ +export function symbolNameOf(symbol: SemanticSymbol | null | undefined): string | undefined { + return symbol === undefined || symbol === null ? undefined : symbolName(symbol); +} + /** Type refinements keep the same native object, including its project and * lazy handle caches. Narrowed fields are read from their original storage. */ -export interface SemanticObjectType extends SemanticType { - readonly objectFlags: number; +export type SemanticObjectType = SemanticType; +export interface SemanticTypeParameter extends SemanticType { + readonly isThisType: boolean; +} +export interface SemanticTemplateLiteralType extends SemanticType { + readonly texts: string[]; } export interface SemanticIntrinsicType extends SemanticType { readonly intrinsicName: string; @@ -416,7 +433,7 @@ export interface SemanticBooleanLiteralType extends SemanticLiteralType { readonly value: boolean; } export interface SemanticTupleType extends SemanticObjectType { - readonly elementFlags: number[]; + readonly elementFlags: enums.ElementFlags[]; readonly fixedLength: number; readonly readonly: boolean; } @@ -424,12 +441,14 @@ export interface SemanticTupleType extends SemanticObjectType { /** Kind-specific accessors share one nominal layout. */ export class SemanticType { readonly id: number; - readonly flags: number; - readonly objectFlags: number | undefined; + readonly flags: enums.TypeFlags; + /** The object flags of an object type; 0 for other types, as TypeScript 7's API gives them. */ + readonly objectFlags: enums.ObjectFlags; readonly value: string | number | boolean | bigint | undefined; readonly intrinsicName: string | undefined; + /** Whether a type parameter is the `this` type; undefined for other types. */ readonly isThisType: boolean | undefined; - readonly elementFlags: number[] | undefined; + readonly elementFlags: enums.ElementFlags[] | undefined; readonly fixedLength: number | undefined; readonly readonly: boolean | undefined; readonly texts: string[] | undefined; @@ -444,7 +463,7 @@ export class SemanticType { ) { this.id = data.id; this.flags = data.flags; - this.objectFlags = data.objectFlags; + this.objectFlags = data.objectFlags ?? 0; const value = data.value; if ((data.flags & TypeFlags.BigIntLiteral) !== 0 && value != null) { if (typeof value !== "string") @@ -454,7 +473,8 @@ export class SemanticType { this.value = value ?? undefined; } this.intrinsicName = data.intrinsicName; - this.isThisType = data.isThisType; + this.isThisType = + (data.flags & TypeFlags.TypeParameter) !== 0 ? (data.isThisType ?? false) : undefined; this.elementFlags = data.elementFlags; this.fixedLength = data.fixedLength; this.readonly = data.readonly; @@ -494,26 +514,26 @@ export class SemanticType { getConstraint(): SemanticType | undefined { return this.project.fetchType(this.id, "getConstraintOfType", this.data.substConstraint); } - getTypeParameters(): SemanticType[] { + getTypeParameters(): SemanticTypeParameter[] { return this.project.fetchTypes( this.id, "getTypeParametersOfType", this.data.typeParameters ?? [], - ); + ) as SemanticTypeParameter[]; } - getOuterTypeParameters(): SemanticType[] { + getOuterTypeParameters(): SemanticTypeParameter[] { return this.project.fetchTypes( this.id, "getOuterTypeParametersOfType", this.data.outerTypeParameters ?? [], - ); + ) as SemanticTypeParameter[]; } - getLocalTypeParameters(): SemanticType[] { + getLocalTypeParameters(): SemanticTypeParameter[] { return this.project.fetchTypes( this.id, "getLocalTypeParametersOfType", this.data.localTypeParameters ?? [], - ); + ) as SemanticTypeParameter[]; } getAliasTypeArguments(): SemanticType[] { return this.project.fetchTypes( @@ -522,10 +542,11 @@ export class SemanticType { this.data.aliasTypeArguments ?? [], ); } - getTypes(): SemanticType[] | undefined { + /** The members of a union or intersection, or the types of a template literal type; empty for + * other types. */ + getTypes(): SemanticType[] { this.project.ensureActive(); - if ((this.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral)) === 0) - return undefined; + if ((this.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral)) === 0) return []; if (this.constituentsPanic !== undefined) throw this.constituentsPanic; if (this.constituents === undefined) { try { @@ -567,7 +588,7 @@ export class SemanticType { return result; } isClassOrInterface(): boolean { - return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.ClassOrInterface) !== 0; + return this.isObjectType() && (this.objectFlags & ObjectFlags.ClassOrInterface) !== 0; } isUnionType(): boolean { return (this.flags & TypeFlags.Union) !== 0; @@ -575,7 +596,7 @@ export class SemanticType { isIntersectionType(): boolean { return (this.flags & TypeFlags.Intersection) !== 0; } - isObjectType(): this is SemanticObjectType { + isObjectType(): boolean { return (this.flags & TypeFlags.Object) !== 0; } isIntrinsicType(): this is SemanticIntrinsicType { @@ -600,10 +621,10 @@ export class SemanticType { return (this.flags & TypeFlags.BooleanLiteral) !== 0; } isTypeReference(): boolean { - return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.Reference) !== 0; + return this.isObjectType() && (this.objectFlags & ObjectFlags.Reference) !== 0; } - isTupleType(): boolean { - return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.Tuple) !== 0; + isTupleType(): this is SemanticTupleType { + return this.isObjectType() && (this.objectFlags & ObjectFlags.Tuple) !== 0; } isIndexType(): boolean { return (this.flags & TypeFlags.Index) !== 0; @@ -617,20 +638,20 @@ export class SemanticType { isSubstitutionType(): boolean { return (this.flags & TypeFlags.Substitution) !== 0; } - isTemplateLiteralType(): boolean { + isTemplateLiteralType(): this is SemanticTemplateLiteralType { return (this.flags & TypeFlags.TemplateLiteral) !== 0; } isStringMappingType(): boolean { return (this.flags & TypeFlags.StringMapping) !== 0; } - isTypeParameter(): boolean { + isTypeParameter(): this is SemanticTypeParameter { return (this.flags & TypeFlags.TypeParameter) !== 0; } } export class SemanticSignature { readonly id: number; - readonly flags: number; + readonly flags: enums.SignatureFlags; /** The declaration's node handle, as the server sends it. */ readonly declarationHandle: SemanticNodeHandle | undefined; private declarationNode: AstNode | undefined; @@ -658,12 +679,12 @@ export class SemanticSignature { return this.declarationNode; } - getTypeParameters(): SemanticType[] { + getTypeParameters(): SemanticTypeParameter[] { return this.project.fetchTypes( this.id, "getTypeParametersOfSignature", this.data.typeParameters ?? [], - ); + ) as SemanticTypeParameter[]; } getParameters(): SemanticSymbol[] { return this.project.fetchSymbols( diff --git a/packages/compiler/src/frontend/ts7/semantic-types.ts b/packages/compiler/src/frontend/ts7/semantic-types.ts index b4f3d5f2e..ea43e3885 100644 --- a/packages/compiler/src/frontend/ts7/semantic-types.ts +++ b/packages/compiler/src/frontend/ts7/semantic-types.ts @@ -1,27 +1,40 @@ /** Frontend names over the concrete native client. No SDK object or - * structural cast participates in the production checker boundary. */ + * structural cast participates in the production checker boundary. The + * names follow tsc-ts/api's. */ export type { SemanticType as Type, - SemanticType as InterfaceType, - SemanticType as TypeReference, SemanticType as UnionOrIntersectionType, SemanticType as UnionType, + SemanticType as IntersectionType, + SemanticType as FreshableType, + SemanticType as InterfaceType, + SemanticType as TypeReference, SemanticTupleType as TupleType, // A reference carries its element flags on the target, not necessarily on // itself. Claiming a tuple shape here makes a native presence test constant. - SemanticObjectType as TupleTypeReference, + SemanticType as TupleTypeReference, + SemanticLiteralType as LiteralType, SemanticStringLiteralType as StringLiteralType, SemanticNumberLiteralType as NumberLiteralType, + SemanticBigIntLiteralType as BigIntLiteralType, SemanticBooleanLiteralType as BooleanLiteralType, + SemanticIntrinsicType as IntrinsicType, SemanticObjectType as ObjectType, + SemanticTypeParameter as TypeParameter, + SemanticType as IndexType, + SemanticType as IndexedAccessType, + SemanticType as ConditionalType, + SemanticType as SubstitutionType, + SemanticTemplateLiteralType as TemplateLiteralType, + SemanticType as StringMappingType, SemanticSignature as Signature, SemanticSymbol as Symbol, } from "./semantic-model.js"; +export { symbolName, symbolNameOf } from "./semantic-model.js"; export type { SemanticIndexInfo as IndexInfo, SemanticTypePredicate as TypePredicate, } from "./semantic-checker.js"; -export type { - Ts7CompilerOptionsData as CompilerOptions, - Ts7DiagnosticData as Diagnostic, -} from "./session-schema.generated.js"; +export type { Ts7CompilerOptionsData as CompilerOptions } from "./session-schema.generated.js"; +export type { Ts7Diagnostic as Diagnostic } from "./diagnostic.js"; +export type { Ts7SourceMetadata as SourceFileMetadata } from "./session-schema.generated.js"; diff --git a/packages/compiler/src/frontend/ts7/session.test.ts b/packages/compiler/src/frontend/ts7/session.test.ts index 522b72fe6..d957fe0dd 100644 --- a/packages/compiler/src/frontend/ts7/session.test.ts +++ b/packages/compiler/src/frontend/ts7/session.test.ts @@ -14,6 +14,8 @@ import { spawnTs7Wire } from "./rpc-process.js"; import { tsgoExecutable } from "./rpc-api.js"; import { Ts7Session, Ts7SessionProject, Ts7SessionSnapshot } from "./session.js"; import { AstNode } from "./ast-node.js"; +import { ts7Diagnostic } from "./diagnostic.js"; +import { getTextOfNode } from "./node-functions.js"; const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const protocolPath = (path: string) => path.replaceAll("\\", "/"); @@ -104,8 +106,11 @@ test("owned snapshots preserve SDK options, diagnostics, metadata and default-pr "getSuggestionDiagnostics", "getDeclarationDiagnostics", ] as const) { - expect(project.program[name](file), name).toEqual(expected.program[name](file)); - expect(project.program[name](), name).toEqual(expected.program[name]()); + // scriptc's diagnostics have every member set (ts7Diagnostic); the SDK's leave absent ones out. + expect(project.program[name](file), name).toEqual( + expected.program[name](file).map(ts7Diagnostic), + ); + expect(project.program[name](), name).toEqual(expected.program[name]().map(ts7Diagnostic)); } for (const name of [ "getProgramDiagnostics", @@ -289,7 +294,7 @@ test("a released latest snapshot cannot lend stale ASTs to the same project", () source = "export const third = true;"; const third = session.updateSnapshot({ fileChanges: { created: [file] } }); expect(third.getProject(config)!.program.getSourceFile(file)!.text).toBe(source); - expect(old.statements.nodes[0]!.getText()).toBe("export const value = 1;"); + expect(getTextOfNode(old.statements.nodes[0]!)).toBe("export const value = 1;"); } finally { session.close(); rmSync(dir, { recursive: true, force: true }); diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index 4933da9d6..87b97d25e 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -1,3 +1,4 @@ +import { ts7Diagnostic, type Ts7Diagnostic } from "./diagnostic.js"; import type { SourceFile } from "./ast-types.js"; import { AstFile, AstNode } from "./ast-node.js"; import { Ts7RpcClient } from "./rpc-client.js"; @@ -314,31 +315,35 @@ export class Ts7SessionProgram { isSourceFileDefaultLibrary(file: AstNode): boolean { return this.getSourceFileMetadataByPath(file.path)?.isDefaultLibrary ?? false; } - private diagnostics(method: string, file?: SemanticDocument): Ts7DiagnosticData[] { - return this.session.request(method, this.query(file)) ?? []; + private diagnostics(method: string, file?: SemanticDocument): Ts7Diagnostic[] { + const result: Ts7Diagnostic[] = []; + for (const data of this.session.request(method, this.query(file)) ?? + []) + result.push(ts7Diagnostic(data)); + return result; } - getSyntacticDiagnostics(file?: SemanticDocument): Ts7DiagnosticData[] { + getSyntacticDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSyntacticDiagnostics", file); } - getBindDiagnostics(file?: SemanticDocument): Ts7DiagnosticData[] { + getBindDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getBindDiagnostics", file); } - getSemanticDiagnostics(file?: SemanticDocument): Ts7DiagnosticData[] { + getSemanticDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSemanticDiagnostics", file); } - getSuggestionDiagnostics(file?: SemanticDocument): Ts7DiagnosticData[] { + getSuggestionDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSuggestionDiagnostics", file); } - getDeclarationDiagnostics(file?: SemanticDocument): Ts7DiagnosticData[] { + getDeclarationDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getDeclarationDiagnostics", file); } - getProgramDiagnostics(): Ts7DiagnosticData[] { + getProgramDiagnostics(): Ts7Diagnostic[] { return this.diagnostics("getProgramDiagnostics"); } - getGlobalDiagnostics(): Ts7DiagnosticData[] { + getGlobalDiagnostics(): Ts7Diagnostic[] { return this.diagnostics("getGlobalDiagnostics"); } - getConfigFileParsingDiagnostics(): Ts7DiagnosticData[] { + getConfigFileParsingDiagnostics(): Ts7Diagnostic[] { return this.diagnostics("getConfigFileParsingDiagnostics"); } dispose(): void { diff --git a/packages/compiler/src/frontend/ts7/source-parser.test.ts b/packages/compiler/src/frontend/ts7/source-parser.test.ts index d5b54f080..b6950b3d5 100644 --- a/packages/compiler/src/frontend/ts7/source-parser.test.ts +++ b/packages/compiler/src/frontend/ts7/source-parser.test.ts @@ -8,6 +8,7 @@ import type { Ts7FileSystem } from "./rpc-filesystem.js"; import { Ts7SourceParser, type Ts7SourceKind } from "./source-parser.js"; import { closeSourceParser, parseSourceFile } from "./source-parser-node.js"; import * as ts from "./syntax.js"; +import { getTextOfNode, tokenStartOfNode } from "./node-functions.js"; const parsers: Ts7SourceParser[] = []; const directories: string[] = []; @@ -46,9 +47,9 @@ test("each parse owns its text and nodes across same-path edits, deletion and cl expect(restored.text).toBe(first.text); parser.close(); parser.close(); - expect(first.statements.nodes[0]!.getText()).toBe("export const first = 1;"); + expect(getTextOfNode(first.statements.nodes[0]!)).toBe("export const first = 1;"); expect(second.statements.nodes[0]!.parent).toBe(second); - expect(other.statements.nodes[0]!.getText()).toBe("exports.third = 3;"); + expect(getTextOfNode(other.statements.nodes[0]!)).toBe("exports.third = 3;"); expect(() => parser.parse("late.ts", "", "ts")).toThrow("closed"); }); @@ -114,9 +115,9 @@ for (const { name, kind, source } of syntaxCases) { expect(file.isDeclarationFile).toBe(oracle.isDeclarationFile); expect( file.statements.nodes.map((statement) => ({ - start: statement.getStart(), + start: tokenStartOfNode(statement), end: statement.end, - text: statement.getText(), + text: getTextOfNode(statement), })), ).toEqual( oracle.statements.map((statement) => ({ @@ -198,7 +199,11 @@ test("JSDoc callable types cannot replace method body views", () => { const expected = oracle.statements[0]!; if (!ts.isClassDeclaration(native) || !ts5.isClassDeclaration(expected)) throw new Error("expected class"); - expect(native.members.nodes.map((member) => member.body?.getText())).toEqual( + expect( + native.members.nodes.map((member) => + member.body === undefined ? undefined : getTextOfNode(member.body), + ), + ).toEqual( expected.members.map((member) => ts5.isMethodDeclaration(member) ? member.body?.getText() : undefined, ), @@ -231,7 +236,7 @@ test("a parser starts a new server once it has parsed its source budget, and ear expect(servers).toHaveLength(2); const third = parser.parse("b.ts", "export const b = 3;", "ts"); expect(servers).toHaveLength(2); - expect(first.statements.nodes[0]!.getText()).toBe(`export const a = ${"1 + ".repeat(20)}1;`); - expect(second.statements.nodes[0]!.getText()).toBe("export const b = 2;"); - expect(third.statements.nodes[0]!.getText()).toBe("export const b = 3;"); + expect(getTextOfNode(first.statements.nodes[0]!)).toBe(`export const a = ${"1 + ".repeat(20)}1;`); + expect(getTextOfNode(second.statements.nodes[0]!)).toBe("export const b = 2;"); + expect(getTextOfNode(third.statements.nodes[0]!)).toBe("export const b = 3;"); }); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index c2fa0a06d..3129e9e3c 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -69,7 +69,7 @@ export function jsOpenObjectType( shapes: ShapeRegistry, unions: UnionRegistry, ): IrType { - if (!decl || !isJsSourceFile(decl.getSourceFile())) return type; + if (!decl || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return type; const arms = type.kind === "union" ? unions.get(type.unionId)?.arms : [type]; const present = arms?.filter((arm) => arm.kind !== "undefinedT"); const parameter = ts.isParameter(decl) @@ -1004,13 +1004,13 @@ export function isGeneralNodeStreamInterface( if (psym === undefined) return false; return psym.declarations.some((d) => { if (!(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d))) return false; - if (!ctx.isStdlibFile(d.getSourceFile())) return false; - if (psym.name === "ReadableStream" || psym.name === "WritableStream") + if (!ctx.isStdlibFile(ts.getSourceFileOfNode(d))) return false; + if (ts.symbolName(psym) === "ReadableStream" || ts.symbolName(psym) === "WritableStream") return ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"); - if (psym.name === "ChildStdin") return isDeclaredInAmbientModule(d, "child_process"); + if (ts.symbolName(psym) === "ChildStdin") return isDeclaredInAmbientModule(d, "child_process"); return ( - (psym.name === "Readable" || psym.name === "Writable") && - isNodeTypesPath(d.getSourceFile().fileName) && + (ts.symbolName(psym) === "Readable" || ts.symbolName(psym) === "Writable") && + isNodeTypesPath(ts.getSourceFileOfNode(d).fileName) && isDeclaredInAmbientModule(d, "stream") ); }); @@ -1027,7 +1027,7 @@ export function isWebAssemblyType( if (!symbol) return false; return symbol.declarations.some( (d) => - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"), ); } @@ -1315,7 +1315,7 @@ export function isLegacyUrlSymbol( ): boolean { if (!symbol) return false; for (const d of symbol.declarations) { - if (!ctx.isStdlibFile(d.getSourceFile())) continue; + if (!ctx.isStdlibFile(ts.getSourceFileOfNode(d))) continue; let family = false; for (let n: ts.Node | undefined = d; n; n = n.parent) { if (ts.isInterfaceDeclaration(n) && LEGACY_URL_TYPES.has(n.name.text)) family = true; @@ -1355,7 +1355,10 @@ export function isOsConstantsSymbol( const alias = types && ts.isTypeAliasDeclaration(d) && d.name.text === "SignalConstants"; const signals = !types && ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.name.text === "signals"; - if (!(alias || signals || ts.isModuleDeclaration(d)) || !ctx.isStdlibFile(d.getSourceFile())) + if ( + !(alias || signals || ts.isModuleDeclaration(d)) || + !ctx.isStdlibFile(ts.getSourceFileOfNode(d)) + ) continue; let inConstants = ts.isModuleDeclaration(d) && d.name.text === "constants"; for (let n: ts.Node | undefined = d.parent; n; n = n.parent) { @@ -1378,14 +1381,14 @@ function isWorkerHandleSymbol( ctx: Pick, ): boolean { return ( - (symbol?.name === "Worker" || - symbol?.name === "MessagePort" || - symbol?.name === "MessageChannel" || - symbol?.name === "BroadcastChannel") && + ((symbol !== undefined && ts.symbolName(symbol) === "Worker") || + (symbol !== undefined && ts.symbolName(symbol) === "MessagePort") || + (symbol !== undefined && ts.symbolName(symbol) === "MessageChannel") || + (symbol !== undefined && ts.symbolName(symbol) === "BroadcastChannel")) && symbol.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "worker_threads"), ) ); @@ -1404,11 +1407,11 @@ function isLookupOptionsUnion(type: ts.Type, ctx: Pick ctx.isStdlibFile(d.getSourceFile())) + !/^Lookup(One|All)?Options$/.test(ts.symbolName(symbol)) || + !symbol.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) return false; - if (symbol.name === "LookupAllOptions") all = true; + if (ts.symbolName(symbol) === "LookupAllOptions") all = true; } return all; } @@ -1455,12 +1458,12 @@ function isFetchBodyUnion(type: ts.Type, ctx: Pick ctx.isStdlibFile(d.getSourceFile())) + !FETCH_BODY_MEMBERS.has(ts.symbolName(symbol)) || + !symbol.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) return false; - if (symbol.name === "FormData") formData = true; - if (symbol.name === "URLSearchParams") searchParams = true; + if (ts.symbolName(symbol) === "FormData") formData = true; + if (ts.symbolName(symbol) === "URLSearchParams") searchParams = true; } return formData && searchParams; } @@ -1480,7 +1483,7 @@ function isDynInterfaceSubset(type: ts.Type, ctx: TypeMapperCtx): boolean { if ( parent === undefined || !ts.isInterfaceDeclaration(parent) || - !ctx.isStdlibFile(parent.getSourceFile()) || + !ctx.isStdlibFile(ts.getSourceFileOfNode(parent)) || (owner !== undefined && owner !== parent) ) return false; @@ -1502,16 +1505,17 @@ function isFetchHeadersUnion(type: ts.Type, ctx: Pick ctx.isStdlibFile(d.getSourceFile())) + symbol !== undefined && + ts.symbolName(symbol) === "Headers" && + symbol.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) { headers = true; continue; } - const alias = member.getAliasSymbol()?.name; + const alias = ts.symbolNameOf(member.getAliasSymbol()); if ( - symbol?.name === "Array" || - symbol?.name === "ReadonlyArray" || + (symbol !== undefined && ts.symbolName(symbol) === "Array") || + (symbol !== undefined && ts.symbolName(symbol) === "ReadonlyArray") || alias === "Record" || alias === "HeaderRecord" ) { @@ -1540,7 +1544,7 @@ function isReceiveMessageResult( !fn || !ts.isFunctionDeclaration(fn) || fn.name?.text !== "receiveMessageOnPort" || - !ctx.isStdlibFile(decl.getSourceFile()) || + !ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) || !isDeclaredInAmbientModule(fn, "worker_threads") ) { return false; @@ -1584,9 +1588,9 @@ export function mapAsyncGeneratorAnnotation(type: ts.Type, ctx: TypeMapperCtx): const symbol = type.getSymbol(); if ( !symbol || - !["AsyncIterator", "AsyncIterableIterator", "AsyncIterable"].includes(symbol.name) || + !["AsyncIterator", "AsyncIterableIterator", "AsyncIterable"].includes(ts.symbolName(symbol)) || !symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) return null; @@ -1615,11 +1619,13 @@ export function isFetchHandleType( const symbol = member.getSymbol(); if ( !symbol || - (symbol.name !== "Headers" && symbol.name !== "Request" && symbol.name !== "Response") || + (ts.symbolName(symbol) !== "Headers" && + ts.symbolName(symbol) !== "Request" && + ts.symbolName(symbol) !== "Response") || !symbol.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) return false; @@ -1700,7 +1706,7 @@ function spreadsCheckedDynamic(symbol: ts.Symbol | undefined, ctx: TypeMapperCtx if (symbol === undefined || (symbol.flags & ts.SymbolFlags.ObjectLiteral) === 0) return false; const decl = symbol.declarations[0]; if (decl === undefined || !ts.isObjectLiteralExpression(decl)) return false; - if (isJsSourceFile(decl.getSourceFile())) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) return false; if (ctx.checker.getContextualType(decl)) return false; return decl.properties.nodes.some( (prop) => @@ -1714,7 +1720,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // TypeScript's intrinsic globalThis symbol has no declaration. A stored // reference needs native object identity, including var's undefined state. const intrinsic = type.getSymbol(); - if (intrinsic?.name === "globalThis" && intrinsic.declarations.length === 0) return DYN; + if ( + intrinsic !== undefined && + ts.symbolName(intrinsic) === "globalThis" && + intrinsic.declarations.length === 0 + ) + return DYN; if (ctx.jsDynamicDeclaration !== undefined && intrinsic !== undefined) { const objectLiteral = (intrinsic.flags & ts.SymbolFlags.ObjectLiteral) !== 0; const classInstance = @@ -1753,8 +1764,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // intersection mapping decomposes it into unsupported utility types. const directAlias = type.getAliasSymbol(); if ( - directAlias?.name === "ExecFileException" && - directAlias.declarations.some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + directAlias !== undefined && + ts.symbolName(directAlias) === "ExecFileException" && + directAlias.declarations.some((decl) => ctx.isStdlibFile(ts.getSourceFileOfNode(decl))) ) { return { kind: "object", className: "%Error" }; } @@ -1763,11 +1775,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // ordinary Error as well. const execException = type.getSymbol(); if ( - execException?.name === "ExecException" && + execException !== undefined && + ts.symbolName(execException) === "ExecException" && execException.declarations.some( (decl) => ts.isInterfaceDeclaration(decl) && - ctx.isStdlibFile(decl.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) && isDeclaredInAmbientModule(decl, "child_process"), ) ) { @@ -1788,7 +1801,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( type.isIntersectionType() && ts.constituentTypes(type).every((t) => { - if (t.getSymbol()?.name !== "Promise") return false; + if (ts.symbolNameOf(t.getSymbol()) !== "Promise") return false; const arg = ctx.checker.getTypeArguments(t as ts.TypeReference)[0]; return arg !== undefined && isWebAssemblyType(arg, ctx); }) @@ -1807,7 +1820,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (ctx.erasingSignature || ctx.isErasing?.()) && !(type.flags & ts.TypeFlags.TypeParameter) && !checker.getCallSignatures(type).some((sig) => sig === ctx.erasingSignature) && - !ERASURE_KEPT_CONTAINERS.has(type.getSymbol()?.name ?? "") && + !ERASURE_KEPT_CONTAINERS.has(ts.symbolNameOf(type.getSymbol()) ?? "") && mentionsErasedTypeParam(type, ctx, 0) ) { contextResolutions++; @@ -1890,8 +1903,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // constructors. Only the standard library's interface gets this ABI. const errorOptionsSymbol = widened.getSymbol(); if ( - errorOptionsSymbol?.name === "ErrorOptions" && - errorOptionsSymbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + errorOptionsSymbol !== undefined && + ts.symbolName(errorOptionsSymbol) === "ErrorOptions" && + errorOptionsSymbol.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) { return DYN; } @@ -1906,7 +1920,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { { const parseArgsSym = widened.getAliasSymbol() ?? widened.getSymbol(); const belongs = (declaration: ts.Node): boolean => { - if (!ctx.isStdlibFile(declaration.getSourceFile())) return false; + if (!ctx.isStdlibFile(ts.getSourceFileOfNode(declaration))) return false; let family = false; for (let node: ts.Node | undefined = declaration; node; node = node.parent) { if ( @@ -1922,7 +1936,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { }; if ( parseArgsSym && - PARSE_ARGS_DYN_TYPES.has(parseArgsSym.name) && + PARSE_ARGS_DYN_TYPES.has(ts.symbolName(parseArgsSym)) && parseArgsSym.declarations.some(belongs) ) return DYN; @@ -1942,10 +1956,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const boxSym = widened.getSymbol(); if ( boxSym && - (boxSym.name === "Number" || boxSym.name === "String" || boxSym.name === "Boolean") && - boxSym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + (ts.symbolName(boxSym) === "Number" || + ts.symbolName(boxSym) === "String" || + ts.symbolName(boxSym) === "Boolean") && + boxSym.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) { - return boxSym.name === "Number" ? F64 : boxSym.name === "String" ? STRING : BOOL; + return ts.symbolName(boxSym) === "Number" + ? F64 + : ts.symbolName(boxSym) === "String" + ? STRING + : BOOL; } } // The lib's PRIMITIVE-CONSTRUCTOR interfaces as TYPES (`typeof String`, @@ -1963,16 +1983,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const ctorSym = widened.getSymbol(); if ( ctorSym && - (ctorSym.name === "StringConstructor" || - ctorSym.name === "NumberConstructor" || - ctorSym.name === "BooleanConstructor") && - ctorSym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + (ts.symbolName(ctorSym) === "StringConstructor" || + ts.symbolName(ctorSym) === "NumberConstructor" || + ts.symbolName(ctorSym) === "BooleanConstructor") && + ctorSym.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) { return funcOf( [STRING], - ctorSym.name === "StringConstructor" + ts.symbolName(ctorSym) === "StringConstructor" ? STRING - : ctorSym.name === "NumberConstructor" + : ts.symbolName(ctorSym) === "NumberConstructor" ? F64 : BOOL, ); @@ -2076,7 +2096,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // declaration file. { const classSym = widened.getSymbol(); - const name = classSym?.name; + const name = ts.symbolNameOf(classSym); if (name === "LifetimeRegion" || name === "InternTable" || name === "ThreadSlots") { for (const d of classSym!.declarations) { if (!ts.isClassDeclaration(d)) continue; @@ -2104,7 +2124,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return decls && decls.length > 0 && decls.every((d) => { - const sf = d.getSourceFile(); + const sf = ts.getSourceFileOfNode(d); return sf.isDeclarationFile && !ctx.isStdlibFile(sf) && !ctx.isExternalTypeFile(sf); }) ? decls @@ -2114,7 +2134,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { decls === null || decls.every( (d) => - packageTypeFile(d.getSourceFile(), ctx) && + packageTypeFile(ts.getSourceFileOfNode(d), ctx) && (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d) || ts.isTypeLiteralNode(d) || @@ -2321,18 +2341,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Nameless class DECLARATIONS are legal only as `export default class // {}` — classNamer spells them "%anon" (unique per file), the same // name their collection registered. - !classDecl.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(classDecl).isDeclarationFile && checker.getConstructSignatures(widened).length === 0 && // A TypeScript class declared OUTSIDE the lowered program (reached // through `import type` alone) never registers, and the program can // hold no instance of it: its instance type maps below as the // structural type it describes, like an interface. - (ctx.isProgramFile(classDecl.getSourceFile()) || isJsSourceFile(classDecl.getSourceFile())) + (ctx.isProgramFile(ts.getSourceFileOfNode(classDecl)) || + isJsSourceFile(ts.getSourceFileOfNode(classDecl))) ) { // A JavaScript class outside the lowered program (jsdoc `typeof // import()` over a never-required module) stays unmappable: JSDoc // types are not checked, so its slots fall back to the dynamic value. - if (!ctx.isProgramFile(classDecl.getSourceFile())) return null; + if (!ctx.isProgramFile(ts.getSourceFileOfNode(classDecl))) return null; // The class inside a MIXIN function: per-call-site instantiations // share this one node — the CURRENT instantiation (collecting or // lowering) is the answer; outside any mixin context the node's type @@ -2376,10 +2397,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( classDecl && (ts.isClassDeclaration(classDecl) || ts.isClassExpression(classDecl)) && - !classDecl.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(classDecl).isDeclarationFile && checker.getConstructSignatures(widened).length > 0 ) { - if (!ctx.isProgramFile(classDecl.getSourceFile())) return null; + if (!ctx.isProgramFile(ts.getSourceFileOfNode(classDecl))) return null; // The MIXIN class node's static side (`typeof C` inside the mixin): // the current instantiation's classval, like the instance type above. { @@ -2445,9 +2466,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // naming the offending half, as does the `new Map` lowering per site. const psym = widened.getSymbol(); const isStdlibInterface = (name: string): boolean => - psym?.name === name && + psym !== undefined && + ts.symbolName(psym) === name && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); // The builtin Error classes: references to the lib's Error interfaces map to the runtime-provided class // hierarchy (provenance, not the name — a user's own `class Error` @@ -2461,8 +2483,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // node:assert's AssertionError is declared as a class (in the shipped // fallback and in @types/node), not as a lib interface. if ( - psym?.name === "AssertionError" && - psym.declarations.some((d) => ts.isClassDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym !== undefined && + ts.symbolName(psym) === "AssertionError" && + psym.declarations.some( + (d) => ts.isClassDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), + ) ) return { kind: "object", className: "%AssertionError" }; // The lib's `Object` interface is a TOP type: tsc admits every @@ -2479,12 +2504,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // representation. The node:module class's merged instance interface is // included, while unrelated user interfaces named Module stay structural. if ( - (psym?.name === "ScriptcModule" || psym?.name === "Module") && + ((psym !== undefined && ts.symbolName(psym) === "ScriptcModule") || + (psym !== undefined && ts.symbolName(psym) === "Module")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (psym.name === "ScriptcModule" || + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + (ts.symbolName(psym) === "ScriptcModule" || isDeclaredInAmbientNamespace(d, "NodeJS") || isDeclaredInAmbientModule(d, "module")), ) @@ -2499,11 +2525,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // NodeJS.EventEmitter interface (the structural surface net.Socket // et al. extend) is the same runtime value. Provenance-checked. if ( - psym?.name === "EventEmitter" && + psym !== undefined && + ts.symbolName(psym) === "EventEmitter" && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "events") || isDeclaredInAmbientNamespace(d, "NodeJS")), ) ) { @@ -2514,11 +2541,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // precedent). The NAME is generic, so the provenance check adds the // enclosing ambient module, like net.Server. if ( - psym?.name === "Interface" && + psym !== undefined && + ts.symbolName(psym) === "Interface" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "readline") || isDeclaredInAmbientModule(d, "readline/promises")), ) @@ -2529,11 +2557,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // it keeps Node's own shape (an undefined name, the escape sequences' // code member). if ( - psym?.name === "Key" && + psym !== undefined && + ts.symbolName(psym) === "Key" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "readline") || isDeclaredInAmbientModule(d, "readline/promises")), ) @@ -2542,11 +2571,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } // readline/promises.Readline: an f64 handle the same way. if ( - psym?.name === "Readline" && + psym !== undefined && + ts.symbolName(psym) === "Readline" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "readline/promises"), ) ) { @@ -2559,11 +2589,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // plus the ambient module suffices for both the fallback declarations // and @types/node's class Channel. if ( - psym?.name === "Channel" && + psym !== undefined && + ts.symbolName(psym) === "Channel" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "diagnostics_channel"), ) ) { @@ -2573,11 +2604,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // the runtime's store id space (the Channel story — stores are // process-lived; contexts ride the fiber machinery). if ( - psym?.name === "AsyncLocalStorage" && + psym !== undefined && + ts.symbolName(psym) === "AsyncLocalStorage" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "async_hooks"), ) ) { @@ -2586,11 +2618,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // diagnostics_channel.TracingChannel: the same f64-handle story over the // runtime's tracing registry (five event channels per entry). if ( - psym?.name === "TracingChannel" && + psym !== undefined && + ts.symbolName(psym) === "TracingChannel" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "diagnostics_channel"), ) ) { @@ -2600,7 +2633,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const encodeResult = psym && psym.declarations.some((decl) => { - if (!ctx.isStdlibFile(decl.getSourceFile())) return false; + if (!ctx.isStdlibFile(ts.getSourceFileOfNode(decl))) return false; if (ts.isInterfaceDeclaration(decl) && decl.name.text === "EncodeIntoResult") return true; const parent = decl.parent; return ( @@ -2625,19 +2658,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // WHATWG codec instances have ordinary reference identity and ownership. // Their private encoding slot is omitted from enumeration and JSON. if ( - (psym?.name === "TextEncoder" || psym?.name === "TextDecoder") && + ((psym !== undefined && ts.symbolName(psym) === "TextEncoder") || + (psym !== undefined && ts.symbolName(psym) === "TextDecoder")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "record", shapeId: ctx.shapes.intern( [ - { name: `%${psym.name}`, type: F64 }, - ...(psym.name === "TextDecoder" + { name: `%${ts.symbolName(psym)}`, type: F64 }, + ...(ts.symbolName(psym) === "TextDecoder" ? [ { name: "%fatal", type: BOOL }, { name: "%ignoreBOM", type: BOOL }, @@ -2659,11 +2693,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // special-cased in lowerNew / lowerStringDecoderMethodCall, so the // record shape never surfaces. if ( - psym?.name === "StringDecoder" && + psym !== undefined && + ts.symbolName(psym) === "StringDecoder" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { @@ -2688,14 +2723,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( flags & ts.TypeFlags.Object && promiseDeclarations.length > 0 && - promiseDeclarations.every((d) => d.getSourceFile().isDeclarationFile) + promiseDeclarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile) ) { const props = checker.getPropertiesOfType(widened); if ( props.length === 3 && - ["promise", "resolve", "reject"].every((n) => props.some((p) => p.name === n)) + ["promise", "resolve", "reject"].every((n) => props.some((p) => ts.symbolName(p) === n)) ) { - const promProp = props.find((p) => p.name === "promise")!; + const promProp = props.find((p) => ts.symbolName(p) === "promise")!; const promT = mapType(checker.getTypeOfSymbol(promProp), ctx); if (promT?.kind === "promise") { const inner = promT.inner; @@ -2744,9 +2779,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // option record whose members include functions, agents and signals. // Values ride checked-dynamic storage, like SpawnOptions. if ( - psym?.name === "RequestOptions" && + psym !== undefined && + ts.symbolName(psym) === "RequestOptions" && psym.declarations.some((d) => { - if (!ts.isInterfaceDeclaration(d) || !ctx.isStdlibFile(d.getSourceFile())) return false; + if (!ts.isInterfaceDeclaration(d) || !ctx.isStdlibFile(ts.getSourceFileOfNode(d))) + return false; for (let p: ts.Node | undefined = d.parent; p !== undefined; p = p.parent) { if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) return HTTP_REQUEST_OPTION_MODULES.has(p.name.text); @@ -2769,9 +2806,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { { const alias = widened.getAliasSymbol(); if ( - alias?.name === "ReadableStreamReadResult" && + alias !== undefined && + ts.symbolName(alias) === "ReadableStreamReadResult" && alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const elemTs = widened.getAliasTypeArguments()?.[0]; @@ -2783,10 +2821,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } if ( psym && - (psym.name === "ReadableStreamReadValueResult" || - psym.name === "ReadableStreamReadDoneResult") && + (ts.symbolName(psym) === "ReadableStreamReadValueResult" || + ts.symbolName(psym) === "ReadableStreamReadDoneResult") && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const elemTs = checker.getTypeArguments(widened as ts.TypeReference)[0]; @@ -2798,11 +2836,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // node:net's SocketAddress and BlockList: native handles (scr_net.c) // whose getters and methods dispatch through the handle. if ( - (psym?.name === "SocketAddress" || psym?.name === "BlockList") && + ((psym !== undefined && ts.symbolName(psym) === "SocketAddress") || + (psym !== undefined && ts.symbolName(psym) === "BlockList")) && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return DYN; @@ -2813,11 +2852,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // checked-dynamic objects. if ( psym && - WEBCRYPTO_DYN_TYPES.has(psym.name) && + WEBCRYPTO_DYN_TYPES.has(ts.symbolName(psym)) && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return DYN; @@ -2826,20 +2865,21 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // whose getters and methods dispatch through the handle. The JsonWebKey // that export() returns is a plain checked-dynamic object. if ( - (psym?.name === "KeyObject" || psym?.name === "JsonWebKey") && + ((psym !== undefined && ts.symbolName(psym) === "KeyObject") || + (psym !== undefined && ts.symbolName(psym) === "JsonWebKey")) && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return DYN; } if ( psym && - (FETCH_AMBIENT_TYPES as readonly string[]).includes(psym.name) && + (FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(psym)) && !( - psym.name === "ReadableStream" && + ts.symbolName(psym) === "ReadableStream" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"), ) @@ -2847,7 +2887,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return DYN; @@ -2860,12 +2900,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Symbol.asyncDispose method. A DisposableStack is a native handle whose // methods are function values of its checked-dynamic view. if ( - (psym?.name === "Console" || - psym?.name === "Disposable" || - psym?.name === "AsyncDisposable" || - psym?.name === "DisposableStack") && + ((psym !== undefined && ts.symbolName(psym) === "Console") || + (psym !== undefined && ts.symbolName(psym) === "Disposable") || + (psym !== undefined && ts.symbolName(psym) === "AsyncDisposable") || + (psym !== undefined && ts.symbolName(psym) === "DisposableStack")) && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return DYN; @@ -2875,8 +2915,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // representation; lowerCipherMethodCall dispatches their methods. if ( psym !== undefined && - CIPHER_TYPE_NAMES.has(psym.name) && - psym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + CIPHER_TYPE_NAMES.has(ts.symbolName(psym)) && + psym.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) { return DYN; } @@ -3003,11 +3043,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and by @types/node (whose NonSharedBuffer alias resolves to // the same symbol); provenance-checked like URL. if ( - psym?.name === "Buffer" && + psym !== undefined && + ts.symbolName(psym) === "Buffer" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return bytesOf("u8"); @@ -3018,11 +3059,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // instance type). Provenance, not the name: a user's own URL maps as a // record/class like any other. if ( - psym?.name === "URL" && + psym !== undefined && + ts.symbolName(psym) === "URL" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "url" }; @@ -3031,11 +3073,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // interface by the shipped fallback and by @types/node (plus `class // URLSearchParams` in the "url" module). Provenance-checked like URL. if ( - psym?.name === "URLSearchParams" && + psym !== undefined && + ts.symbolName(psym) === "URLSearchParams" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "searchParams" }; @@ -3043,11 +3086,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fs.Stats: @types/node's `class Stats` (module "fs") or the fallback // declarations' interface. Provenance-checked like URL. if ( - psym?.name === "Stats" && + psym !== undefined && + ts.symbolName(psym) === "Stats" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "stats" }; @@ -3055,13 +3099,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Capacity snapshots and options keep their live object representation // across numeric/BigInt overloads, runtime options, and callbacks. if ( - (psym?.name === "StatsFs" || - psym?.name === "BigIntStatsFs" || - psym?.name === "StatFsOptions") && + ((psym !== undefined && ts.symbolName(psym) === "StatsFs") || + (psym !== undefined && ts.symbolName(psym) === "BigIntStatsFs") || + (psym !== undefined && ts.symbolName(psym) === "StatFsOptions")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "fs"), ) ) @@ -3071,11 +3115,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // distinguishes it from user interfaces with the same name; both the // fallback and @types/node declare it in "fs/promises". if ( - psym?.name === "FileHandle" && + psym !== undefined && + ts.symbolName(psym) === "FileHandle" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "fs/promises"), ) ) { @@ -3084,11 +3129,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fs.Dir: an open directory stream, declared in "fs" by both the // fallback and @types/node. if ( - psym?.name === "Dir" && + psym !== undefined && + ts.symbolName(psym) === "Dir" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "fs"), ) ) { @@ -3099,11 +3145,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // reads work either way) or the fallback declarations' plain interface. // Provenance-checked like Stats. if ( - psym?.name === "SpawnSyncReturns" && + psym !== undefined && + ts.symbolName(psym) === "SpawnSyncReturns" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "spawnRes" }; @@ -3113,13 +3160,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // refinements only tell the checker which stdio properties are null. if ( psym !== undefined && - (psym.name === "ChildProcess" || - psym.name === "ChildProcessWithoutNullStreams" || - psym.name === "ChildProcessByStdio") && + (ts.symbolName(psym) === "ChildProcess" || + ts.symbolName(psym) === "ChildProcessWithoutNullStreams" || + ts.symbolName(psym) === "ChildProcessByStdio") && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "child_process"), ) ) { @@ -3133,25 +3180,27 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // that module (http's Server) stays unmapped and fences at its use // site. if ( - (psym?.name === "Server" || psym?.name === "Socket" || psym?.name === "TLSSocket") && + ((psym !== undefined && ts.symbolName(psym) === "Server") || + (psym !== undefined && ts.symbolName(psym) === "Socket") || + (psym !== undefined && ts.symbolName(psym) === "TLSSocket")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "net") || // http.Server extends net.Server in @types/node — and tls.Server // / https.Server extend it transitively: same lowered surface // (listen/close/address/'error'), same handle kind. tls.TLSSocket // extends net.Socket the same way: post-handshake it IS a net // socket to every lowered member. - (psym.name === "Server" && + (ts.symbolName(psym) === "Server" && (isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "tls") || isDeclaredInAmbientModule(d, "https"))) || - (psym.name === "TLSSocket" && isDeclaredInAmbientModule(d, "tls"))), + (ts.symbolName(psym) === "TLSSocket" && isDeclaredInAmbientModule(d, "tls"))), ) ) { - return psym.name === "Server" ? { kind: "netServer" } : { kind: "netSocket" }; + return ts.symbolName(psym) === "Server" ? { kind: "netServer" } : { kind: "netSocket" }; } // tls.SecureContext: the opaque parsed-cert/key handle SNI callbacks // answer with (tls.createSecureContext({ cert, key }) mints it; the TLS @@ -3159,11 +3208,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // @types/node declares `interface SecureContext` inside `declare module // "tls"`, and the shipped fallback mirrors it. if ( - psym?.name === "SecureContext" && + psym !== undefined && + ts.symbolName(psym) === "SecureContext" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "tls"), ) ) { @@ -3172,11 +3222,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Scrypt's runtime options retain property accessors and aliases. Use // checked values so presence probes and repeated getter reads match Node. if ( - psym?.name === "ScryptOptions" && + psym !== undefined && + ts.symbolName(psym) === "ScryptOptions" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "crypto"), ) ) @@ -3185,26 +3236,28 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // provenance module-qualified so user classes with these common names // retain their ordinary structural/class representation. if ( - (psym?.name === "Hash" || psym?.name === "Hmac") && + ((psym !== undefined && ts.symbolName(psym) === "Hash") || + (psym !== undefined && ts.symbolName(psym) === "Hmac")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "crypto"), ) ) { - return psym.name === "Hash" ? CRYPTOHASH_T : CRYPTOHMAC_T; + return ts.symbolName(psym) === "Hash" ? CRYPTOHASH_T : CRYPTOHMAC_T; } // fs.FSWatcher: the fs.watch handle (scr_watch.c). Module-checked like // net.Server — @types/node declares `interface FSWatcher extends // EventEmitter` inside `declare module "fs"`, and the shipped fallback // mirrors it. if ( - psym?.name === "FSWatcher" && + psym !== undefined && + ts.symbolName(psym) === "FSWatcher" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "fs"), ) ) { @@ -3239,11 +3292,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ts.constituentTypes(type).some((part) => { const sym = part.getSymbol(); return ( - (sym?.name === "ReadableStream" || sym?.name === "WritableStream") && + ((sym !== undefined && ts.symbolName(sym) === "ReadableStream") || + (sym !== undefined && ts.symbolName(sym) === "WritableStream")) && sym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientNamespace(d, "NodeJS"), ) ); @@ -3252,22 +3306,24 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return DYN; } const nodeWritableInterface = - psym?.name === "WritableStream" && + psym !== undefined && + ts.symbolName(psym) === "WritableStream" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientNamespace(d, "NodeJS"), ); if (nodeWritableInterface && ctx.generalNodeStreams === true) { return ctx.dynNodeStreams === true ? DYN : { kind: "object", className: "%Writable" }; } if ( - (psym?.name === "WriteStream" && + (psym !== undefined && + ts.symbolName(psym) === "WriteStream" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "tty") || (isDeclaredInAmbientModule(d, "process") && isDeclaredInAmbientNamespace(d, "NodeJS"))), )) || @@ -3280,21 +3336,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // stream.Writable. As with stream.Readable below, unsupported producers // of the broad @types class fence where they are constructed. if ( - (psym?.name === "ChildStdin" && + (psym !== undefined && + ts.symbolName(psym) === "ChildStdin" && ctx.generalNodeStreams !== true && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "child_process"), )) || - (psym?.name === "Writable" && + (psym !== undefined && + ts.symbolName(psym) === "Writable" && ctx.generalNodeStreams !== true && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isNodeTypesPath(d.getSourceFile().fileName) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isNodeTypesPath(ts.getSourceFileOfNode(d).fileName) && isDeclaredInAmbientModule(d, "stream"), )) ) { @@ -3304,11 +3362,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // ChildStdin is the runtime Writable too (child.stdioStream bridges it). if ( ctx.generalNodeStreams === true && - psym?.name === "ChildStdin" && + psym !== undefined && + ts.symbolName(psym) === "ChildStdin" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "child_process"), ) ) { @@ -3326,12 +3385,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Writable join them under the general representation. for (const [irName, rec] of RUNTIME_STREAM_CLASSES) { if ( - psym?.name === rec.lib && + psym !== undefined && + ts.symbolName(psym) === rec.lib && psym.declarations.some( (d) => ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (!isNodeTypesPath(d.getSourceFile().fileName) || + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + (!isNodeTypesPath(ts.getSourceFileOfNode(d).fileName) || ctx.generalNodeStreams === true || (rec.lib !== "Readable" && rec.lib !== "Writable")) && isDeclaredInAmbientModule(d, "stream"), @@ -3344,12 +3404,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // @types/node's): their instances are runtime Transform streams. if ( psym !== undefined && - ZLIB_STREAM_MODES.has(psym.name) && + ZLIB_STREAM_MODES.has(ts.symbolName(psym)) && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || - (ts.isInterfaceDeclaration(d) && isNodeTypesPath(d.getSourceFile().fileName))) && - ctx.isStdlibFile(d.getSourceFile()) && + (ts.isInterfaceDeclaration(d) && isNodeTypesPath(ts.getSourceFileOfNode(d).fileName))) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "zlib"), ) ) { @@ -3359,29 +3419,35 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // @types/node's): their instances are runtime Readable / Writable // streams (scr_fs_stream.c). if ( - (psym?.name === "ReadStream" || psym?.name === "WriteStream") && + ((psym !== undefined && ts.symbolName(psym) === "ReadStream") || + (psym !== undefined && ts.symbolName(psym) === "WriteStream")) && psym.declarations.some( (d) => ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "fs"), ) ) { - return { kind: "object", className: psym.name === "ReadStream" ? "%Readable" : "%Writable" }; + return { + kind: "object", + className: ts.symbolName(psym) === "ReadStream" ? "%Readable" : "%Writable", + }; } if ( - (psym?.name === "Readable" && + (psym !== undefined && + ts.symbolName(psym) === "Readable" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "stream"), )) || - (psym?.name === "ReadableStream" && + (psym !== undefined && + ts.symbolName(psym) === "ReadableStream" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientNamespace(d, "NodeJS"), )) ) { @@ -3390,7 +3456,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // checked-dynamic stream slots, NodeJS.ReadableStream holds any // stream value (dyn-streams.ts); stream.Readable keeps the class. if (ctx.generalNodeStreams === true) { - if (ctx.dynNodeStreams === true && psym.name === "ReadableStream") return DYN; + if (ctx.dynNodeStreams === true && ts.symbolName(psym) === "ReadableStream") return DYN; return { kind: "object", className: "%Readable" }; } return { kind: "childStream" }; @@ -3400,11 +3466,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // EventEmitter` and the fallback declarations' interface both live // inside `declare module "dgram"`. if ( - psym?.name === "Socket" && + psym !== undefined && + ts.symbolName(psym) === "Socket" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "dgram"), ) ) { @@ -3417,11 +3484,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // `declare module "node:test"` (isDeclaredInAmbientModule answers for // both spellings). if ( - (psym?.name === "TestContext" || psym?.name === "SuiteContext") && + ((psym !== undefined && ts.symbolName(psym) === "TestContext") || + (psym !== undefined && ts.symbolName(psym) === "SuiteContext")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "test"), ) ) { @@ -3431,18 +3499,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // records built from F (mock-shapes.ts). Mock then maps as the hybrid // { %call: F, mock } record through mapHybridCallableIntersection. if ( - (psym?.name === "MockFunctionContext" || psym?.name === "MockFunctionCall") && + ((psym !== undefined && ts.symbolName(psym) === "MockFunctionContext") || + (psym !== undefined && ts.symbolName(psym) === "MockFunctionCall")) && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "test"), ) ) { const fnTs = checker.getTypeArguments(widened as ts.TypeReference)[0]; const fn = fnTs ? mapType(fnTs, ctx) : null; if (!fn || fn.kind !== "func") return null; - return psym.name === "MockFunctionContext" + return ts.symbolName(psym) === "MockFunctionContext" ? mockContextType(fn, ctx) : mockCallRecordType(fn, ctx); } @@ -3452,9 +3521,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // above; optional metadata uses the same checked property view. // Provenance-checked like the rest. if ( - psym?.name === "ErrnoException" && + psym !== undefined && + ts.symbolName(psym) === "ErrnoException" && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "object", className: "%Error" }; @@ -3466,11 +3536,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // of fencing at the declaration. Module-checked like the rest (https' // Agent extends http's; both live in their ambient modules). if ( - psym?.name === "Agent" && + psym !== undefined && + ts.symbolName(psym) === "Agent" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "https")), ) ) { @@ -3480,15 +3551,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // net.Server (the names repeat nowhere today, but provenance stays the // rule). if ( - (psym?.name === "IncomingMessage" || psym?.name === "ServerResponse") && + ((psym !== undefined && ts.symbolName(psym) === "IncomingMessage") || + (psym !== undefined && ts.symbolName(psym) === "ServerResponse")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "http"), ) ) { - return psym.name === "IncomingMessage" ? { kind: "httpReq" } : { kind: "httpRes" }; + return ts.symbolName(psym) === "IncomingMessage" ? { kind: "httpReq" } : { kind: "httpRes" }; } // http2's compatibility surface (the allowHTTP1 lowering): the secure // server IS a net server (listen/close/address/emit ride the same @@ -3499,19 +3571,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // member at their use sites (lower-server.ts). Module-checked like the // rest. if ( - (psym?.name === "Http2SecureServer" || - psym?.name === "Http2ServerRequest" || - psym?.name === "Http2ServerResponse") && + ((psym !== undefined && ts.symbolName(psym) === "Http2SecureServer") || + (psym !== undefined && ts.symbolName(psym) === "Http2ServerRequest") || + (psym !== undefined && ts.symbolName(psym) === "Http2ServerResponse")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "http2"), ) ) { - return psym.name === "Http2SecureServer" + return ts.symbolName(psym) === "Http2SecureServer" ? { kind: "netServer" } - : psym.name === "Http2ServerRequest" + : ts.symbolName(psym) === "Http2ServerRequest" ? { kind: "httpReq" } : { kind: "httpRes" }; } @@ -3520,33 +3592,34 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // kinds (client and server flavors share a kind — the runtime handle // carries the role). Module-checked like the rest. if ( - (psym?.name === "Http2Server" || - psym?.name === "Http2Session" || - psym?.name === "ClientHttp2Session" || - psym?.name === "ServerHttp2Session" || - psym?.name === "ClientHttp2Stream" || - psym?.name === "ServerHttp2Stream") && + ((psym !== undefined && ts.symbolName(psym) === "Http2Server") || + (psym !== undefined && ts.symbolName(psym) === "Http2Session") || + (psym !== undefined && ts.symbolName(psym) === "ClientHttp2Session") || + (psym !== undefined && ts.symbolName(psym) === "ServerHttp2Session") || + (psym !== undefined && ts.symbolName(psym) === "ClientHttp2Stream") || + (psym !== undefined && ts.symbolName(psym) === "ServerHttp2Stream")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "http2"), ) ) { - return psym.name === "Http2Server" + return ts.symbolName(psym) === "Http2Server" ? { kind: "netServer" } - : psym.name === "ClientHttp2Stream" || psym.name === "ServerHttp2Stream" + : ts.symbolName(psym) === "ClientHttp2Stream" || ts.symbolName(psym) === "ServerHttp2Stream" ? { kind: "http2Stream" } : { kind: "http2Session" }; } // http.ClientRequest (http.request/http.get results) — module-checked // like IncomingMessage. if ( - psym?.name === "ClientRequest" && + psym !== undefined && + ts.symbolName(psym) === "ClientRequest" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "http"), ) ) { @@ -3561,9 +3634,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // declarations' anonymous env type takes the pureIndexShape path below // and interns the SAME shape. if ( - psym?.name === "ProcessEnv" && + psym !== undefined && + ts.symbolName(psym) === "ProcessEnv" && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const value = withUndefinedArm(STRING, ctx.unions); @@ -3653,9 +3727,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { { const alias = widened.getAliasSymbol(); if ( - alias?.name === "IteratorResult" && + alias !== undefined && + ts.symbolName(alias) === "IteratorResult" && alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const targs = widened.getAliasTypeArguments() ?? []; @@ -3775,10 +3850,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ]; const fields: { name: string; type: IrType }[] = []; for (const property of checker.getPropertiesOfType(widened)) { - if (!known.includes(property.name)) return null; + if (!known.includes(ts.symbolName(property))) return null; const type = mapType(checker.getTypeOfSymbol(property), ctx); if (!type) return null; - fields.push({ name: property.name, type }); + fields.push({ name: ts.symbolName(property), type }); } const order = fields.map((field) => field.name); fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); @@ -3787,23 +3862,24 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Filesystem options are data records. Keep their declared optional fields // instead of allowing arbitrary standard-library interfaces into records. if ( - (psym?.name === "FileReadOptions" || - psym?.name === "ReadOptions" || - psym?.name === "ReadOptionsWithBuffer" || - psym?.name === "WriteOptions") && + ((psym !== undefined && ts.symbolName(psym) === "FileReadOptions") || + (psym !== undefined && ts.symbolName(psym) === "ReadOptions") || + (psym !== undefined && ts.symbolName(psym) === "ReadOptionsWithBuffer") || + (psym !== undefined && ts.symbolName(psym) === "WriteOptions")) && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "fs/promises") || isDeclaredInAmbientModule(d, "fs")), ) ) { const fields: { name: string; type: IrType }[] = []; for (const property of checker.getPropertiesOfType(widened)) { - if (!["buffer", "offset", "length", "position"].includes(property.name)) return null; + if (!["buffer", "offset", "length", "position"].includes(ts.symbolName(property))) + return null; const type = mapType(checker.getTypeOfSymbol(property), ctx); if (!type) return null; - fields.push({ name: property.name, type }); + fields.push({ name: ts.symbolName(property), type }); } const order = fields.map((field) => field.name); fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); @@ -3814,28 +3890,28 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record does not model the prototype (unobservable through the lowered // surface); it preserves the caller buffer/string by reference. if ( - (psym?.name === "FileReadResult" || - psym?.name === "FileWriteResult" || - psym?.name === "FileReadvResult" || - psym?.name === "FileWritevResult" || - psym?.name === "ReadVResult" || - psym?.name === "WriteVResult") && + ((psym !== undefined && ts.symbolName(psym) === "FileReadResult") || + (psym !== undefined && ts.symbolName(psym) === "FileWriteResult") || + (psym !== undefined && ts.symbolName(psym) === "FileReadvResult") || + (psym !== undefined && ts.symbolName(psym) === "FileWritevResult") || + (psym !== undefined && ts.symbolName(psym) === "ReadVResult") || + (psym !== undefined && ts.symbolName(psym) === "WriteVResult")) && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "fs/promises") || - ((psym.name === "ReadVResult" || psym.name === "WriteVResult") && + ((ts.symbolName(psym) === "ReadVResult" || ts.symbolName(psym) === "WriteVResult") && isDeclaredInAmbientModule(d, "fs"))), ) ) { const args = checker.getTypeArguments(widened as ts.TypeReference); if (args.length !== 1) return null; const vector = - psym.name === "FileReadvResult" || - psym.name === "FileWritevResult" || - psym.name === "ReadVResult" || - psym.name === "WriteVResult"; + ts.symbolName(psym) === "FileReadvResult" || + ts.symbolName(psym) === "FileWritevResult" || + ts.symbolName(psym) === "ReadVResult" || + ts.symbolName(psym) === "WriteVResult"; const emptyVector = vector && checker.isArrayType(args[0]!) && @@ -3850,9 +3926,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return null; } const count = - psym.name === "FileReadResult" || - psym.name === "FileReadvResult" || - psym.name === "ReadVResult" + ts.symbolName(psym) === "FileReadResult" || + ts.symbolName(psym) === "FileReadvResult" || + ts.symbolName(psym) === "ReadVResult" ? "bytesRead" : "bytesWritten"; const payloadName = vector ? "buffers" : "buffer"; @@ -3884,11 +3960,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // declares it concrete (an interface; @types/node uses a class — both // provenance-checked, the Buffer/URL pattern). if ( - psym?.name === "Dirent" && + psym !== undefined && + ts.symbolName(psym) === "Dirent" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const args = checker.getTypeArguments(widened as ts.TypeReference); @@ -3945,7 +4022,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { d.parent.name.text === "times" && ts.isInterfaceDeclaration(d.parent.parent) && d.parent.parent.name.text === "CpuInfo" && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) return cpuTimes(); @@ -3966,7 +4043,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { }; } if (isStdlibInterface("ParsedPath") || isStdlibInterface("FormatInputPathObject")) { - const field = psym!.name === "ParsedPath" ? STRING : withUndefinedArm(STRING, ctx.unions); + const field = + ts.symbolName(psym!) === "ParsedPath" ? STRING : withUndefinedArm(STRING, ctx.unions); if (!field) return null; return { kind: "record", @@ -4017,7 +4095,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym.declarations.some( (d) => ts.isTypeLiteralNode(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "child_process") || isDeclaredInAmbientModule(d, "util") || isDeclaredInAmbientModule(d, "dns")), @@ -4032,7 +4110,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (checker.getTypeOfSymbol(p).flags & ts.TypeFlags.String) !== 0, ) ? props - .map((p) => p.name) + .map((p) => ts.symbolName(p)) .sort() .join(",") : ""; @@ -4060,11 +4138,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // JSON.stringify of one shows the three fields where Node's // internal-slot object shows {} — the Dirent %dtype edge's cousin. if ( - psym?.name === "X509Certificate" && + psym !== undefined && + ts.symbolName(psym) === "X509Certificate" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "crypto"), ) ) { @@ -4091,13 +4170,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // @types/node declares the classes in perf_hooks and global interfaces // over them; the shipped fallback declares the classes. if ( - (psym?.name === "PerformanceEntry" || - psym?.name === "PerformanceMark" || - psym?.name === "PerformanceMeasure") && + ((psym !== undefined && ts.symbolName(psym) === "PerformanceEntry") || + (psym !== undefined && ts.symbolName(psym) === "PerformanceMark") || + (psym !== undefined && ts.symbolName(psym) === "PerformanceMeasure")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "record", shapeId: perfEntryShape(ctx.shapes, ctx.unions) }; @@ -4254,7 +4333,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { isStdlibInterface("NetworkInterfaceInfoIPv4") || isStdlibInterface("NetworkInterfaceInfoIPv6") ) { - const v6 = psym!.name === "NetworkInterfaceInfoIPv6"; + const v6 = ts.symbolName(psym!) === "NetworkInterfaceInfoIPv6"; const cidrArms = [NULL_T, STRING].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); const cidr: IrType = { kind: "union", unionId: ctx.unions.intern(cidrArms) }; const scopeid = v6 ? F64 : withUndefinedArm(F64, ctx.unions); @@ -4402,7 +4481,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym !== undefined && psym.declarations.length > 0 && psym.declarations.every( - (d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const CHILD_SURFACE = new Set([ @@ -4437,24 +4516,26 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const props = checker.getPropertiesOfType(widened); let core = 0; const childMemberType = (p: ts.Symbol): boolean => { - if (["kill", "on", "once", "off", "removeListener", "unref", "ref"].includes(p.name)) { + if ( + ["kill", "on", "once", "off", "removeListener", "unref", "ref"].includes(ts.symbolName(p)) + ) { return checker.getCallSignatures(checker.getTypeOfSymbol(p)).length > 0; } const expected = - p.name === "stdout" || p.name === "stderr" + ts.symbolName(p) === "stdout" || ts.symbolName(p) === "stderr" ? "childStream" - : p.name === "stdin" + : ts.symbolName(p) === "stdin" ? "childWriter" - : p.name === "killed" + : ts.symbolName(p) === "killed" ? "bool" : "f64"; const unit = - p.name === "pid" + ts.symbolName(p) === "pid" ? "undefinedT" - : p.name === "exitCode" || - p.name === "stdin" || - p.name === "stdout" || - p.name === "stderr" + : ts.symbolName(p) === "exitCode" || + ts.symbolName(p) === "stdin" || + ts.symbolName(p) === "stdout" || + ts.symbolName(p) === "stderr" ? "nullT" : null; const mapped = mapType(checker.getTypeOfSymbol(p), ctx); @@ -4482,8 +4563,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const childShaped = props.length > 0 && props.every((p) => { - if (!CHILD_SURFACE.has(p.name) || !childMemberType(p)) return false; - if (CHILD_CORE.has(p.name)) core++; + if (!CHILD_SURFACE.has(ts.symbolName(p)) || !childMemberType(p)) return false; + if (CHILD_CORE.has(ts.symbolName(p))) core++; return true; }); if (childShaped && core >= 2) return CHILD_T; @@ -4688,7 +4769,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // arity ABI selected by lambdaSignature. Treating an inferred any[] // declaration as a typed packed-array closure would bypass its boxed // spread/error semantics and change coverage classification. - if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null; + if (rest && decl !== undefined && isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; // A fixed-tuple rest (`...args: [A, B]`, `...args: Parameters` over // a bound F) flattens to its positional parameters. Pattern rests keep // their packed-record ABI and stay unmapped as values. @@ -4722,8 +4803,8 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null ((ts.isArrowFunction(decl.parent) || ts.isFunctionExpression(decl.parent)) && decl.initializer !== undefined && decl.type === undefined && - !isJsSourceFile(decl.getSourceFile()))) && - !decl.getSourceFile().isDeclarationFile && + !isJsSourceFile(ts.getSourceFileOfNode(decl)))) && + !ts.getSourceFileOfNode(decl).isDeclarationFile && (checker.getTypeOfSymbol(p).flags & ts.TypeFlags.Any) !== 0 ) pt = DYN; @@ -4731,7 +4812,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null decl && ts.isParameter(decl) && ts.isSetAccessorDeclaration(decl.parent) && - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) pt = DYN; // An inferred JavaScript binding pattern consumes an iterable; it @@ -4751,17 +4832,19 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null ts.isIdentifier(construction.expression) && construction.expression.text === "Promise" && constructorSymbol && - constructorSymbol.declarations.some((site) => ctx.isStdlibFile(site.getSourceFile())); + constructorSymbol.declarations.some((site) => ctx.isStdlibFile(ts.getSourceFileOfNode(site))); if ( !nativeExecutor && decl && ts.isParameter(decl) && !decl.dotDotDotToken && !ts.isSetAccessorDeclaration(decl.parent) && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !/@(?:param|type)\b/.test( - decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), + ts + .getSourceFileOfNode(decl) + .text.slice(decl.parent?.pos ?? decl.pos, ts.tokenStartOfNode(decl)), ) ) pt = DYN; @@ -4782,7 +4865,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null (ts.isArrowFunction(decl.parent) || ts.isFunctionExpression(decl.parent) || ts.isFunctionDeclaration(decl.parent)) && - !decl.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(decl).isDeclarationFile && (checker.getTypeOfSymbol(p).flags & ts.TypeFlags.Any) !== 0 ) pt = DYN; @@ -4807,7 +4890,8 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null if ( rest && decl !== undefined && - (!decl.getSourceFile().isDeclarationFile || isNodeTestDefaultFunctionParam(decl, ctx)) && + (!ts.getSourceFileOfNode(decl).isDeclarationFile || + isNodeTestDefaultFunctionParam(decl, ctx)) && (pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker))) ) { dynRest = true; @@ -4832,7 +4916,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null const sigDecl = sig.declaration; const jsUnitReturn = sigDecl !== undefined && - isJsSourceFile(sigDecl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && (retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0; let ret = jsUnitReturn || (retT.flags & ts.TypeFlags.Any) !== 0 @@ -4840,24 +4924,33 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null : retT.flags & ts.TypeFlags.Never ? VOID : mapType(retT, ctx); - if (sigDecl && ts.isGetAccessorDeclaration(sigDecl) && isJsSourceFile(sigDecl.getSourceFile())) + if ( + sigDecl && + ts.isGetAccessorDeclaration(sigDecl) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) + ) + ret = DYN; + if (sigDecl && isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && ret?.kind === "array") ret = DYN; - if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array") ret = DYN; if ( sigDecl && ts.isFunctionLike(sigDecl) && - isJsSourceFile(sigDecl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && !sigDecl.type && - !/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) && + !/@returns?\b/.test( + ts.getSourceFileOfNode(sigDecl).text.slice(sigDecl.pos, ts.tokenStartOfNode(sigDecl)), + ) && ret?.kind === "record" ) ret = DYN; if ( sigDecl && ts.isFunctionLike(sigDecl) && - isJsSourceFile(sigDecl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && !sigDecl.type && - !/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) && + !/@returns?\b/.test( + ts.getSourceFileOfNode(sigDecl).text.slice(sigDecl.pos, ts.tokenStartOfNode(sigDecl)), + ) && (ret?.kind === "date" || (ret?.kind === "union" && ctx.unions.get(ret.unionId)?.arms.some((arm) => arm.kind === "func"))) @@ -4866,7 +4959,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null if ( ret && sigDecl && - isJsSourceFile(sigDecl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && functionCanReturnUndefined(sigDecl) ) ret = withUndefinedArm(ret, ctx.unions) ?? ret; @@ -4899,12 +4992,12 @@ function unionDiscriminant( // no slot to compare (discriminantField refuses tuple shapes too). if (parts.some((part) => ctx.shapes.get(part.mapped.shapeId)?.tuple)) return undefined; for (const candidate of checker.getPropertiesOfType(parts[0]!.source)) { - if (candidate.name.startsWith("__@")) continue; + if (ts.symbolName(candidate).startsWith("__@")) continue; const byTag = new Map(); const owner = new Map(); let complete = true; for (const part of parts) { - const prop = checker.getPropertyOfType(part.source, candidate.name); + const prop = checker.getPropertyOfType(part.source, ts.symbolName(candidate)); if ( !prop || prop.flags & @@ -4935,7 +5028,7 @@ function unionDiscriminant( } if (complete) { return { - field: candidate.name, + field: ts.symbolName(candidate), cases: [...byTag] .sort(([a], [b]) => a - b) .map(([tag, values]) => ({ @@ -4977,7 +5070,8 @@ function mapClassView( if ( declarations.length === 0 || !declarations.every( - (decl) => ts.isInterfaceDeclaration(decl) && !ctx.isStdlibFile(decl.getSourceFile()), + (decl) => + ts.isInterfaceDeclaration(decl) && !ctx.isStdlibFile(ts.getSourceFileOfNode(decl)), ) ) return undefined; @@ -5015,7 +5109,7 @@ function mapClassView( const classBase = declaration && (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && - !declaration.getSourceFile().isDeclarationFile; + !ts.getSourceFileOfNode(declaration).isDeclarationFile; const mapped = classBase ? mapType(base, viewCtx) : mapClassView(base, viewCtx, seen); if (mapped === null) return null; if (mapped === undefined) continue; @@ -5032,7 +5126,7 @@ function mapClassView( ) return null; for (const property of checker.getPropertiesOfType(type)) { - const original = checker.getPropertyOfType(baseType, property.name); + const original = checker.getPropertyOfType(baseType, ts.symbolName(property)); if (!original) return null; if (original === property) continue; const originalDeclarations = original.declarations; @@ -5078,7 +5172,7 @@ function mapCollectionView( if ( declarations.length === 0 || !declarations.every( - (decl) => ts.isInterfaceDeclaration(decl) && !ctx.isStdlibFile(decl.getSourceFile()), + (decl) => ts.isInterfaceDeclaration(decl) && !ctx.isStdlibFile(ts.getSourceFileOfNode(decl)), ) ) return null; @@ -5126,18 +5220,20 @@ function mapCollectionView( const baseSymbol = base.getSymbol(); const native = baseSymbol && - ["Map", "ReadonlyMap", "Set", "ReadonlySet"].includes(baseSymbol.name) && + ["Map", "ReadonlyMap", "Set", "ReadonlySet"].includes(ts.symbolName(baseSymbol)) && baseSymbol.declarations.some( - (decl) => ts.isInterfaceDeclaration(decl) && ctx.isStdlibFile(decl.getSourceFile()), + (decl) => ts.isInterfaceDeclaration(decl) && ctx.isStdlibFile(ts.getSourceFileOfNode(decl)), ); const mapped = native ? mapType(base, baseCtx) : mapCollectionView(base, baseCtx, seen); if (mapped?.kind !== "map" && mapped?.kind !== "set") return null; const hasType = funcOf([mapped.kind === "map" ? mapped.key : mapped.elem], BOOL); for (const property of checker.getPropertiesOfType(type)) { - const own = property.declarations.filter((decl) => !ctx.isStdlibFile(decl.getSourceFile())); + const own = property.declarations.filter( + (decl) => !ctx.isStdlibFile(ts.getSourceFileOfNode(decl)), + ); if (own.length === 0) continue; if ( - property.name !== "has" || + ts.symbolName(property) !== "has" || property.flags & ts.SymbolFlags.Optional || !own.every((decl) => decl.kind === ts.SyntaxKind.MethodSignature) ) @@ -5247,12 +5343,13 @@ function boundSignatureAlias( const { checker, resolveTypeParamTs } = ctx; if (!resolveTypeParamTs) return null; const alias = type.getAliasSymbol(); - if (!alias || (alias.name !== "Parameters" && alias.name !== "ReturnType")) return null; + const aliasName = alias === undefined ? undefined : ts.symbolName(alias); + if (!alias || (aliasName !== "Parameters" && aliasName !== "ReturnType")) return null; const aliasArgs = type.getAliasTypeArguments(); const arg = aliasArgs[0]; if (aliasArgs.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return null; const isLibAlias = alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); if (!isLibAlias) return null; const bound = resolveTypeParamTs(arg); @@ -5260,7 +5357,7 @@ function boundSignatureAlias( const sigs = checker.getCallSignatures(bound); const sig = sigs[0]; if (sigs.length !== 1 || !sig || sig.getTypeParameters().length > 0) return null; - return { name: alias.name, sig }; + return { name: aliasName, sig }; } /** One position of a fixed-arity parameter tuple. An optional position @@ -5463,7 +5560,7 @@ function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | nul const { resolveTypeParam } = ctx; if (!resolveTypeParam) return null; const alias = type.getAliasSymbol(); - if (!alias || alias.name !== "Awaited") return null; + if (!alias || ts.symbolName(alias) !== "Awaited") return null; const aliasArgs = type.getAliasTypeArguments(); const arg = aliasArgs[0]; if (aliasArgs.length !== 1 || !arg) return null; @@ -5474,7 +5571,7 @@ function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | nul if (!(arg.flags & ts.TypeFlags.TypeParameter) && viaSignature?.name !== "ReturnType") return null; // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one. const isLibAlias = alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); if (!isLibAlias) return null; const bound = viaSignature ? mapBoundSignatureAlias(arg, ctx) : resolveTypeParam(arg); @@ -5618,7 +5715,7 @@ function mapBoundMappedType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null const fields: { name: string; type: IrType }[] = []; const order: string[] = []; for (const property of checker.getPropertiesOfType(bound)) { - const name = property.name; + const name = ts.symbolName(property); const evaluated = evalMappedTemplate(template, new Map([[key, name]]), new Map(), ctx); let type = evaluated ? mapType(evaluated, ctx) : null; if (!type) return null; @@ -5886,8 +5983,8 @@ function matchInferThenable( const symbol = t.getSymbol(); return ( symbol !== undefined && - names.includes(symbol.name) && - symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + names.includes(ts.symbolName(symbol)) && + symbol.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ); }; if (!stdlibNamed(extendsType, ["PromiseLike", "Promise"])) return undefined; @@ -5919,11 +6016,11 @@ function mentionsMappedParams( function readonlyClassType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const alias = type.getAliasSymbol(); - if (alias?.name !== "Readonly") return null; + if (alias === undefined || ts.symbolName(alias) !== "Readonly") return null; const args = type.getAliasTypeArguments(); if (args.length !== 1 || !args[0]) return null; const isLibAlias = alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); if (!isLibAlias) return null; const mapped = mapType(args[0], ctx); @@ -5934,19 +6031,20 @@ function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType | const { resolveTypeParam, shapes, unions } = ctx; if (!resolveTypeParam) return null; const alias = widened.getAliasSymbol(); - if (!alias || (alias.name !== "Partial" && alias.name !== "Readonly")) return null; + if (!alias || (ts.symbolName(alias) !== "Partial" && ts.symbolName(alias) !== "Readonly")) + return null; const aliasArgs = widened.getAliasTypeArguments(); const arg = aliasArgs[0]; if (aliasArgs.length !== 1 || !arg) return null; if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null; // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one. const isLibAlias = alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); if (!isLibAlias) return null; const bound = resolveTypeParam(arg); if (!bound || bound.kind !== "record") return null; - if (alias.name === "Readonly") return bound; + if (ts.symbolName(alias) === "Readonly") return bound; const shape = shapes.get(bound.shapeId); if (!shape) return null; // Partial over an index-signature shape would need `V | undefined` @@ -6072,7 +6170,7 @@ export function isUnitOnlyTsType(t: ts.Type, resolveTypeParam?: TypeParamResolve /** `ReturnType` over a type parameter bound to a void-returning * function (a variadic wrapper's result slot): a unit-only value. */ function boundVoidReturnType(p: ts.Type, resolveTypeParam?: TypeParamResolver): boolean { - if (!resolveTypeParam || p.getAliasSymbol()?.name !== "ReturnType") return false; + if (!resolveTypeParam || ts.symbolNameOf(p.getAliasSymbol()) !== "ReturnType") return false; const args = p.getAliasTypeArguments(); const arg = args[0]; if (args.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return false; @@ -6163,8 +6261,8 @@ function mapHybridCallableIntersection(widened: ts.Type, ctx: TypeMapperCtx): Ir const pt = mapType(checker.getTypeOfSymbol(p), ctx); if (!pt || pt.kind === "void") return null; - fields.push({ name: p.name, type: pt }); - declaredOrder.push(p.name); + fields.push({ name: ts.symbolName(p), type: pt }); + declaredOrder.push(ts.symbolName(p)); } if (fields.length === 1) return null; // no properties — it is just F fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); @@ -6206,8 +6304,8 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType "URIError", "AggregateError", "SuppressedError", - ].includes(symbol.name) && - symbol.declarations.some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + ].includes(ts.symbolName(symbol)) && + symbol.declarations.some((decl) => ctx.isStdlibFile(ts.getSourceFileOfNode(decl))) ); }); if (!hasError) return null; @@ -6228,9 +6326,9 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType return null; for (const property of ctx.checker.getPropertiesOfType(part)) { const expected = - property.name === "errno" + ts.symbolName(property) === "errno" ? "f64" - : ["code", "syscall", "path", "dest"].includes(property.name) + : ["code", "syscall", "path", "dest"].includes(ts.symbolName(property)) ? "string" : null; if (!expected) { @@ -6258,11 +6356,11 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType for (const property of extra) { const field = mapType(ctx.checker.getTypeOfSymbol(property), ctx); if (!field || field.kind === "void") return null; - if (property.name === "message" || property.name === "name") { + if (ts.symbolName(property) === "message" || ts.symbolName(property) === "name") { if (field.kind !== "string") return null; continue; } - if (property.name === "stack" || property.name === "cause") return null; + if (ts.symbolName(property) === "stack" || ts.symbolName(property) === "cause") return null; if ( !canDynCheckTo( field, @@ -6382,7 +6480,8 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null const { checker } = ctx; const symbol = type.getSymbol(); const decl = symbol ? symbol.declarations[0] : undefined; - if (decl === undefined || !CONTRACT_TYPES_FILE.test(decl.getSourceFile().fileName)) return null; + if (decl === undefined || !CONTRACT_TYPES_FILE.test(ts.getSourceFileOfNode(decl).fileName)) + return null; const props = checker.getPropertiesOfType(type); const seen = new Set(); for (const prop of props) { @@ -6462,14 +6561,14 @@ function recordProvenanceOk(t: ts.Type, ctx: TypeMapperCtx): boolean { .every((property) => property.declarations.every( (decl) => - !decl.getSourceFile().isDeclarationFile || - ctx.isExternalTypeFile(decl.getSourceFile()) || - packageTypeFile(decl.getSourceFile(), ctx), + !ts.getSourceFileOfNode(decl).isDeclarationFile || + ctx.isExternalTypeFile(ts.getSourceFileOfNode(decl)) || + packageTypeFile(ts.getSourceFileOfNode(decl), ctx), ), ); } return !decls.some((d) => { - const sf = d.getSourceFile(); + const sf = ts.getSourceFileOfNode(d); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }); } @@ -6546,7 +6645,7 @@ export function erasedGenericMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType const sigs = ctx.checker.getCallSignatures(t); if (sigs.length !== 1) return undefined; const sig = sigs[0]!; - const own = new Set(sig.getTypeParameters()); + const own = new Set(sig.getTypeParameters()); const outer = ctx.resolveTypeParam; const erased = mapType(t, { ...ctx, @@ -6578,7 +6677,7 @@ export function isOverloadedCallableMemberType(t: ts.Type, checker: ts.TypeCheck if (decl === undefined) return false; if (!ts.isMethodSignatureDeclaration(decl) && !ts.isCallSignatureDeclaration(decl)) return false; - const file = decl.getSourceFile(); + const file = ts.getSourceFileOfNode(decl); return !file.isDeclarationFile && !isJsSourceFile(file); }); } @@ -6640,7 +6739,7 @@ function joinOverloadTypes(types: readonly IrType[], ctx: TypeMapperCtx): IrType export function erasedOverloadMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType | undefined { const funcs: (IrType & { kind: "func" })[] = []; for (const sig of ctx.checker.getCallSignatures(t)) { - const own = new Set(sig.getTypeParameters()); + const own = new Set(sig.getTypeParameters()); const outer = ctx.resolveTypeParam; const mapped = mapCallSignature( sig, @@ -6686,7 +6785,7 @@ export function erasedOverloadMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string { const { checker } = ctx; for (const sig of checker.getCallSignatures(t)) { - const own = new Set(sig.getTypeParameters()); + const own = new Set(sig.getTypeParameters()); const outer = ctx.resolveTypeParam; const sigCtx: TypeMapperCtx = own.size === 0 @@ -6699,14 +6798,14 @@ function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string { const mapped = mapCallSignature(sig, sigCtx); if (mapped?.kind === "func" && mapped.rest !== true && mapped.argumentsAll !== true) continue; const decl = sig.declaration; - const spelled = decl ? decl.getText().replace(/\s+/g, " ").replace(/;$/, "") : ""; + const spelled = decl ? ts.getTextOfNode(decl).replace(/\s+/g, " ").replace(/;$/, "") : ""; const which = spelled ? `overload '${spelled}'` : "one overload"; if (mapped?.kind === "func") return `is an overload set, and its ${which} has a rest parameter (an overload set stores one fixed-arity function)`; for (const param of sig.getParameters()) { const paramTs = checker.getTypeOfSymbol(param); if (!mapType(paramTs, sigCtx)) - return `is an overload set, and its ${which} has parameter '${param.name}' of type '${checker.typeToString(paramTs)}', which does not compile`; + return `is an overload set, and its ${which} has parameter '${ts.symbolName(param)}' of type '${checker.typeToString(paramTs)}', which does not compile`; } const retTs = checker.getReturnTypeOfSignature(sig); return `is an overload set, and its ${which} returns '${checker.typeToString(retTs)}', which does not compile`; @@ -6892,9 +6991,7 @@ function dictSpreadLiteralShape( const infos = checker.getIndexInfosOfType(contextual); const unknownValued = infos.length > 0 && - infos.every( - (info) => (info.valueType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0, - ); + infos.every((info) => (info.type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0); if (!unknownValued) return null; } const kinds = props.map((p) => @@ -6906,7 +7003,7 @@ function dictSpreadLiteralShape( if (checker.getPropertiesOfType(t).length === 0) { const infos = checker.getIndexInfosOfType(t); if (infos.length !== 1 || !(infos[0]!.keyType.flags & ts.TypeFlags.String)) return null; - return mapType(infos[0]!.valueType, ctx); + return mapType(infos[0]!.type, ctx); } // Another such literal's type (`{ ...tags, repo }` over a binding // initialized by `{ ...base, plan }`) is stored as the same kind of @@ -7033,7 +7130,7 @@ function spreadMemberNamesTs(checker: ts.TypeChecker, e: ts.Expression): string[ const names: string[] = []; for (const l of literals) { const t = checker.getNonNullableType(checker.getTypeAtLocation(l)); - for (const property of checker.getPropertiesOfType(t)) names.push(property.name); + for (const property of checker.getPropertiesOfType(t)) names.push(ts.symbolName(property)); } return names; } @@ -7085,11 +7182,11 @@ function refinedConstituentShape( ): string | null { if (!widened.isIntersectionType()) return null; const { checker, shapes } = ctx; - const names = checker.getPropertiesOfType(widened).map((prop) => prop.name); + const names = checker.getPropertiesOfType(widened).map((prop) => ts.symbolName(prop)); let found: string | null = null; for (const part of ts.constituentTypes(widened)) { if ((part.flags & ts.TypeFlags.Object) === 0 || shapes.inProgress.has(part)) continue; - const own = new Set(checker.getPropertiesOfType(part).map((prop) => prop.name)); + const own = new Set(checker.getPropertiesOfType(part).map((prop) => ts.symbolName(prop))); if (own.size !== names.length || !names.every((name) => own.has(name))) continue; const mapped = mapType(part, ctx); if (mapped?.kind !== "record") continue; @@ -7166,7 +7263,7 @@ function mapRecordTypeInner( let v: IrType | null = null; for (const info of indexInfos) { if (!stringKey(info.keyType) && !(info.keyType.flags & ts.TypeFlags.Number)) return null; - const iv = mapType(info.valueType, ctx); + const iv = mapType(info.type, ctx); // JSONValue>` (a package's own JSON alias) and `Record` @@ -7262,7 +7359,7 @@ function mapRecordTypeInner( if (ctx.classImplementedProtocol?.(widened)) return DYN; // Symbol-keyed records use the checked object's descriptor table. A // checker-internal __@ name is not a string field in a native struct. - if (props.some((property) => property.name.startsWith("__@"))) return DYN; + if (props.some((property) => ts.symbolName(property).startsWith("__@"))) return DYN; // A computed shape with NO members is not a real empty record: inside a // generic body `Partial` resolves to no members at all (keyof T is // still unknown there) — interning `{}` would give a wrong type. The @@ -7321,13 +7418,13 @@ function mapRecordTypeInner( (ts.isObjectLiteralExpression(d.parent) || ts.isInterfaceDeclaration(d.parent) || d.parent.kind === ts.SyntaxKind.TypeLiteral) && - !d.getSourceFile().isDeclarationFile && - !isJsSourceFile(d.getSourceFile()), + !ts.getSourceFileOfNode(d).isDeclarationFile && + !isJsSourceFile(ts.getSourceFileOfNode(d)), ); if (!accessorOwned || (!getDecl && !setDecl)) return null; // Symbol-keyed accessors have no foldable literal name to fill at // the literal — no slot to make. - if (p.name.startsWith("__@")) return null; + if (ts.symbolName(p).startsWith("__@")) return null; // An index-signature shape stores accessor NAMES nowhere the keyed // read/walk machinery can answer (the overflow would miss where // Node dispatches the getter) — those shapes stay unmapped. @@ -7349,8 +7446,9 @@ function mapRecordTypeInner( if (readT !== null && typeKey(readT) !== typeKey(writeT)) return null; } if (readT !== null || getDecl) - fields.push({ name: `%get:${p.name}`, type: funcOf([], readT ?? VOID) }); - if (writeT !== null) fields.push({ name: `%set:${p.name}`, type: funcOf([writeT], VOID) }); + fields.push({ name: `%get:${ts.symbolName(p)}`, type: funcOf([], readT ?? VOID) }); + if (writeT !== null) + fields.push({ name: `%set:${ts.symbolName(p)}`, type: funcOf([writeT], VOID) }); continue; } // Computed shapes carry provenance per MEMBER: a utility type over a @@ -7360,7 +7458,7 @@ function mapRecordTypeInner( if ( computed && p.declarations.some((d) => { - const sf = d.getSourceFile(); + const sf = ts.getSourceFileOfNode(d); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) ) { @@ -7410,11 +7508,11 @@ function mapRecordTypeInner( // A DATA property spelled like a reserved accessor slot (`{ "%get:x": // v }` — a string-literal key): mapping it would collide with the // accessor dispatch, so the shape stays unmapped. - if (accessorSlotProp(p.name) !== null) return null; + if (accessorSlotProp(ts.symbolName(p)) !== null) return null; // an options record with `signal?: AbortSignal` must not turn its - fields.push({ name: p.name, type: pt }); + fields.push({ name: ts.symbolName(p), type: pt }); } // getPropertiesOfType yields DECLARATION order (interface/alias/literal // source order) — captured before the canonical sort so Object.keys/ @@ -7509,7 +7607,7 @@ function isNodeTestDefaultFunctionParam(decl: ts.Declaration, ctx: TypeMapperCtx fnType.parent !== undefined && ts.isTypeParameterDeclaration(fnType.parent) && fnType.parent.defaultType === fnType && - ctx.isStdlibFile(decl.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) && isDeclaredInAmbientModule(decl, "test") ); } @@ -7585,11 +7683,11 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): const psym = widened.getSymbol(); const container = psym !== undefined && - Object.prototype.hasOwnProperty.call(STDLIB_CONTAINERS, psym.name) && + Object.prototype.hasOwnProperty.call(STDLIB_CONTAINERS, ts.symbolName(psym)) && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) - ? psym.name + ? ts.symbolName(psym) : undefined; if (container !== undefined) { const spec = STDLIB_CONTAINERS[container]!; @@ -7731,7 +7829,7 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): for (const p of sig.getParameters()) { const pTs = checker.getTypeOfSymbol(p); if (!mapType(pTs, ctx)) { - return `the function shape is supported, but its parameter '${p.name}' has type '${text(pTs)}', which does not compile`; + return `the function shape is supported, but its parameter '${ts.symbolName(p)}' has type '${text(pTs)}', which does not compile`; } } const retTs = checker.getReturnTypeOfSignature(sig); @@ -7763,14 +7861,14 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx // Accessor members and symbol-keyed members have their own multi-shaped // rules (mapRecordTypeInner) — no single-member claim is honest there. if ((p.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) !== 0) continue; - if (p.name.startsWith("__@")) continue; + if (ts.symbolName(p).startsWith("__@")) continue; // Computed shapes over LIBRARY members (`Readonly`): the record // fence there is per-member PROVENANCE (mapRecordTypeInner's rule), not // any one member's type — that story stays with the residual fence. if ( computed && p.declarations.some((d) => { - const sf = d.getSourceFile(); + const sf = ts.getSourceFileOfNode(d); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) ) { @@ -7779,7 +7877,7 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx const fieldTs = checker.getTypeOfSymbol(p); if (isOverloadedCallableMemberType(fieldTs, checker)) { if (erasedOverloadMemberType(fieldTs, ctx) !== undefined) continue; - return `the record shape is supported, but its member '${p.name}' ${describeOverloadSetBlocker(fieldTs, ctx)}`; + return `the record shape is supported, but its member '${ts.symbolName(p)}' ${describeOverloadSetBlocker(fieldTs, ctx)}`; } let pt = isGenericCallableMemberType(fieldTs, checker) ? erasedGenericMemberType(fieldTs, ctx) @@ -7788,7 +7886,7 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) pt = unitOnlyUnion(ctx.unions); if (!pt || pt.kind === "void") { - return `the record shape is supported, but its member '${p.name}' has type '${checker.typeToString(fieldTs)}', which does not compile`; + return `the record shape is supported, but its member '${ts.symbolName(p)}' has type '${checker.typeToString(fieldTs)}', which does not compile`; } } return null; diff --git a/packages/compiler/src/frontend/worker-target.ts b/packages/compiler/src/frontend/worker-target.ts index 99c97dded..5193f0539 100644 --- a/packages/compiler/src/frontend/worker-target.ts +++ b/packages/compiler/src/frontend/worker-target.ts @@ -84,7 +84,8 @@ export function isWorkerConstructor(program: ts.Program, expression: ts.Expressi export function workerModulePath(program: ts.Program, expression: ts.Expression): string | null { if (ts.isIdentifier(expression) && expression.text === "__filename") { - if (isNodeGlobal(program, expression)) return resolve(expression.getSourceFile().fileName); + if (isNodeGlobal(program, expression)) + return resolve(ts.getSourceFileOfNode(expression).fileName); } const value = staticForkString(program, expression); if (value === null) return null; diff --git a/packages/compiler/src/ir/traverse.test.ts b/packages/compiler/src/ir/traverse.test.ts index 874e7846e..fbe84644a 100644 --- a/packages/compiler/src/ir/traverse.test.ts +++ b/packages/compiler/src/ir/traverse.test.ts @@ -28,7 +28,8 @@ const program = createProgram([path], { noLib: true, noResolve: true, types: [] const source = program.getSourceFile(path)!; afterAll(() => program.dispose()); function childType(type: ts.TypeNode): boolean { - if (ts.isTypeReferenceNode(type)) return ["IrExpr", "IrStmt"].includes(type.typeName.getText()); + if (ts.isTypeReferenceNode(type)) + return ["IrExpr", "IrStmt"].includes(ts.getTextOfNode(type.typeName)); if (ts.isArrayTypeNode(type)) return childType(type.elementType); if (ts.isUnionTypeNode(type)) return type.types.nodes.some(childType); if (ts.isTypeLiteralNode(type)) @@ -47,15 +48,15 @@ for (const name of ["IrExpr", "IrStmt"]) { for (const variant of alias.type.types.nodes) { if (!ts.isTypeLiteralNode(variant)) throw new Error("IR variants must be records"); const members = variant.members.nodes.filter(ts.isPropertySignature); - const tag = members.find((member) => member.name.getText() === "kind")!.type!; - const kind = JSON.parse(tag.getText()) as string; + const tag = members.find((member) => ts.getTextOfNode(member.name) === "kind")!.type!; + const kind = JSON.parse(ts.getTextOfNode(tag)) as string; test(`${name}.${kind}: every declared child is visited and rewritten, metadata is preserved`, () => { for (const presence of [true, false]) { const expected: (IrExpr | IrStmt)[] = []; function value(type: ts.TypeNode): unknown { if (ts.isTypeReferenceNode(type)) { const node = - type.typeName.getText() === "IrExpr" + ts.getTextOfNode(type.typeName) === "IrExpr" ? num(expected.length) : statement(expected.length); expected.push(node); @@ -72,7 +73,7 @@ for (const name of ["IrExpr", "IrStmt"]) { }; for (const member of type.members.nodes) { if (ts.isPropertySignature(member) && member.type && childType(member.type)) - result[member.name.getText()] = value(member.type); + result[ts.getTextOfNode(member.name)] = value(member.type); } return result; } @@ -86,7 +87,7 @@ for (const name of ["IrExpr", "IrStmt"]) { }; for (const member of members) { if (member.type && childType(member.type) && (presence || !member.postfixToken)) - input[member.name.getText()] = value(member.type); + input[ts.getTextOfNode(member.name)] = value(member.type); } const before = structuredClone(input); const seen: (IrExpr | IrStmt)[] = []; @@ -135,7 +136,7 @@ for (const name of ["IrExpr", "IrStmt"]) { expect(output.loc).toBe(loc); expect((output as unknown as Record)["metadata"]).toBe(input["metadata"]); for (const member of members) { - const key = member.name.getText(); + const key = ts.getTextOfNode(member.name); if (member.postfixToken && !presence) expect(Object.hasOwn(output, key)).toBe(false); } } @@ -217,7 +218,7 @@ test("module traversal covers every declared type slot", () => { const active = new Set(); function slots(node: ts.TypeNode): unknown { if (ts.isTypeReferenceNode(node)) { - const name = node.typeName.getText(); + const name = ts.getTextOfNode(node.typeName); if (name === "IrType") { const value: IrType = { kind: "regex" }; expected.push(value); @@ -268,7 +269,7 @@ test("module traversal covers every declared type slot", () => { for (const member of members) { if (!ts.isPropertySignature(member) || !member.type) continue; const value = slots(member.type); - if (value !== undefined) result[member.name.getText()] = value; + if (value !== undefined) result[ts.getTextOfNode(member.name)] = value; } return result; } @@ -316,20 +317,20 @@ test("type traversal visits all structural children and leaves named shapes as r if (!ts.isTypeLiteralNode(variant)) throw new Error("IrType variants must be records"); const members = variant.members.nodes.filter(ts.isPropertySignature); const kind = JSON.parse( - members.find((member) => member.name.getText() === "kind")!.type!.getText(), + ts.getTextOfNode(members.find((member) => ts.getTextOfNode(member.name) === "kind")!.type!), ) as string; const node: Record = { kind }; const expected: IrType[] = []; for (const member of members) { if (!member.type) continue; const child = - ts.isTypeReferenceNode(member.type) && member.type.typeName.getText() === "IrType"; + ts.isTypeReferenceNode(member.type) && ts.getTextOfNode(member.type.typeName) === "IrType"; const array = - ts.isArrayTypeNode(member.type) && member.type.elementType.getText() === "IrType"; + ts.isArrayTypeNode(member.type) && ts.getTextOfNode(member.type.elementType) === "IrType"; if (!child && !array) continue; const value: IrType = { kind: "fileHandle" }; expected.push(value); - node[member.name.getText()] = array ? [value] : value; + node[ts.getTextOfNode(member.name)] = array ? [value] : value; } const seen: IrType[] = []; expect( diff --git a/packages/compiler/src/wasm/discover.ts b/packages/compiler/src/wasm/discover.ts index 931bc1dac..de38bf64f 100644 --- a/packages/compiler/src/wasm/discover.ts +++ b/packages/compiler/src/wasm/discover.ts @@ -159,7 +159,7 @@ class Discovery { if (!paths && !base64 && !bytes) return; const file = sf.fileName; const lineOf = (node: ts.Node): number => - ts.getLineAndCharacterOfPosition(sf, node.getStart()).line + 1; + ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(node)).line + 1; ts.walkPreorder( sf, (node) => { diff --git a/packages/compiler/test/ts7/parity.test.ts b/packages/compiler/test/ts7/parity.test.ts index 4904f2720..978de5cc2 100644 --- a/packages/compiler/test/ts7/parity.test.ts +++ b/packages/compiler/test/ts7/parity.test.ts @@ -1,4 +1,5 @@ import { Ts7Host } from "../../src/frontend/ts7/program-adapter.js"; +import { symbolName, symbolNameOf } from "../../src/frontend/ts7/semantic-model.js"; /* The two-world agreement battery: every fixture program is parsed and * checked through BOTH worlds — typescript@5.9.3 in-process and the TS7 * adapter over tsgo — and the worlds must agree on structure (walk order, @@ -331,7 +332,7 @@ for (const battery of ALL_BATTERIES) { if (s5[a] !== undefined && isFixtureSymbol(w(), s5[a]!) && s5[a]!.name !== "export=") { // finding 5: the module.exports receiver's symbol NAME differs in // JS files (5.9.3 "export=", tsgo "module") — pinned separately. - expect(s7[a]!.name, `${file}#${ids[a]} symbol name`).toBe(s5[a]!.name); + expect(symbolName(s7[a]!), `${file}#${ids[a]} symbol name`).toBe(s5[a]!.name); } for (let b = a + 1; b < ids.length; b++) { if (s5[a] === undefined || s5[b] === undefined) continue; @@ -432,7 +433,7 @@ test("finding pinned: tsgo does not synthesize CJS expando members; module resol const moduleIdx = n5.findIndex((n) => ts5.isIdentifier(n) && n.getText() === "module"); expect(moduleIdx).toBeGreaterThanOrEqual(0); expect(w.c5.getSymbolAtLocation(n5[moduleIdx]!)?.name).toBe("export="); - expect(w.c7.getSymbolAtLocation(n7[moduleIdx]!)?.name).toBe("module"); + expect(symbolNameOf(w.c7.getSymbolAtLocation(n7[moduleIdx]!))).toBe("module"); }); test("getAwaitedType shim agrees with 5.9.3 across the async battery (pinned limitation aside)", () => { @@ -487,7 +488,7 @@ test("symbol declarations resolve back into the same client AST", () => { const sym7 = w.c7.getSymbolAtLocation(n7[i]!); expect(sym7).toBeDefined(); const aliased = w.c7.getAliasedSymbol(sym7!); - expect(aliased.name).toBe("answer"); + expect(symbolName(aliased)).toBe("answer"); const declHandle = aliased.declarationHandles[0]; expect(declHandle).toBeDefined(); const decl = declHandle!.resolve(w.p7.project); diff --git a/packages/compiler/test/ts7/rpc-api.test.ts b/packages/compiler/test/ts7/rpc-api.test.ts index a8b4aebce..a39c71321 100644 --- a/packages/compiler/test/ts7/rpc-api.test.ts +++ b/packages/compiler/test/ts7/rpc-api.test.ts @@ -14,7 +14,21 @@ import { SemanticSignature, SemanticSymbol, SemanticType, + symbolName, } from "../../src/frontend/ts7/semantic-model.js"; +import { ts7Diagnostic, type Ts7Diagnostic } from "../../src/frontend/ts7/diagnostic.js"; +import type { Ts7DiagnosticData } from "../../src/frontend/ts7/session-schema.generated.js"; + +// The SDK's symbols have `name`; scriptc's give it with symbolName, which reads `escapedName`, as the +// SDK's symbols also have it. +function nameOf(symbol: SdkSymbol): string { + return symbolName(symbol as unknown as SemanticSymbol); +} + +// scriptc's diagnostics have every member set; the SDK's leave absent ones out. +function diagnostics(list: readonly unknown[]): Ts7Diagnostic[] { + return list.map((item) => ts7Diagnostic(item as Ts7DiagnosticData)); +} // scriptc's symbols keep their declarations' node handles in // `declarationHandles`, and the SDK's in `declarations`. @@ -51,12 +65,12 @@ function programFacts(snapshot: Snapshot, config: string, path: string) { options: project.program.getCompilerOptions(), text: file.text, type: checker.typeToString(type), - symbol: symbol.name, + symbol: nameOf(symbol), declaration: declarationHandlesOf(symbol)[0]!.resolve()!.getText(file), - syntactic: project.program.getSyntacticDiagnostics(path), - semantic: project.program.getSemanticDiagnostics(path), - globals: project.program.getGlobalDiagnostics(), - config: project.program.getConfigFileParsingDiagnostics(), + syntactic: diagnostics(project.program.getSyntacticDiagnostics(path)), + semantic: diagnostics(project.program.getSemanticDiagnostics(path)), + globals: diagnostics(project.program.getGlobalDiagnostics()), + config: diagnostics(project.program.getConfigFileParsingDiagnostics()), defaultLibrary: project.program.isSourceFileDefaultLibrary(file), externalLibrary: project.program.isSourceFileFromExternalLibrary(file), }; @@ -233,7 +247,7 @@ test("native semantic registries preserve project scope and snapshot lifetimes", .declarationList.declarations, )[0], texts: [a.checker.typeToString(typeA), b.checker.typeToString(typeB)], - parameters: signatureA.getParameters().map((symbol) => symbol.name), + parameters: signatureA.getParameters().map(nameOf), }, symbolA, typeA, @@ -258,7 +272,7 @@ test("native semantic registries preserve project scope and snapshot lifetimes", first.dispose(); expect(() => actual.typeA.getSymbol()).toThrow("disposed"); expect(() => declarationHandlesOf(actual.symbolA)[0]!.resolve()).toThrow("disposed"); - expect(next.signatureA.getParameters()[0]!.name).toBe("value"); + expect(nameOf(next.signatureA.getParameters()[0]!)).toBe("value"); expect(second.getProject(firstConfig)!.checker.typeToString(next.typeA)).toBe("42"); second.dispose(); expect(() => next.signatureA.getParameters()).toThrow("disposed"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbe529577..9c42ad00c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,8 +107,8 @@ importers: specifier: workspace:* version: link:../runtime tsc-ts: - specifier: 0.0.3 - version: 0.0.3 + specifier: 0.0.5 + version: 0.0.5 typescript: specifier: 7.0.2 version: 7.0.2 @@ -1357,6 +1357,10 @@ packages: cpu: [x64] os: [win32] + '@scriptc/threads@0.3.6': + resolution: {integrity: sha512-XZb4JhXM/ivhx4MMLxqLLF2mCI28nUK+VVKlCnELO83tacUuGlq7YCe/8LwvzO7fZwy/rCH4+genAYYEc6BF6Q==} + engines: {node: '>=24.0.0'} + '@sinclair/typebox@0.25.24': resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} @@ -1370,42 +1374,37 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} - '@tsc-ts/darwin-arm64@0.0.3': - resolution: {integrity: sha512-NSP4IhLx658tY21ub3ruraZwUwAC3GCbIf1eM0eOKUHfOBBXqq7LU/Loj3zvlvn/tJUyv2VdIDmzrWI/mxp5sA==} - cpu: [arm64] - os: [darwin] - - '@tsc-ts/darwin-x64@0.0.3': - resolution: {integrity: sha512-+BNvqyygkMx0/oHkjK8AmsPrE2+OcE/hGzT0wa2dndiH8lCSDBs+77C7Kmy/FSTWi9ItJShp1+AuWKgQPEOztg==} + '@tsc-ts/darwin-x64@0.0.5': + resolution: {integrity: sha512-CEeHOFfpx5H9V3WSq+V7XGU8tfc06nRN818auiRVwuwPHvpti9gHq0HYkvf/43PH1pIGcgf68sQYdIxJhrQDKQ==} cpu: [x64] os: [darwin] - '@tsc-ts/linux-arm64-gnu@0.0.3': - resolution: {integrity: sha512-I3v0J0L387TEkau0BTGiVx2GdstYfOrwVYwb0pJ/LbFURhrcGcsUODP5bdSOIYCtsp3aAX6e2afdqSIlpsxtbw==} + '@tsc-ts/linux-arm64-gnu@0.0.5': + resolution: {integrity: sha512-qwybOqGDEa35eV9x/3WO81ExAUbUMY8288ATC83kUfYGmOAVStaO+JM2IdrsiI8hLWVC7/f0RFgZ+7dyWh9/nA==} cpu: [arm64] os: [linux] libc: [glibc] - '@tsc-ts/linux-arm64-musl@0.0.3': - resolution: {integrity: sha512-f7wD9D6fC1xL7+ifvMAbJ0TqxOuZP04R2/7c8lb7Gor5SFKe85udJoxf0gocK7mbzWzmEiOJ53J1PujJLBVIxA==} + '@tsc-ts/linux-arm64-musl@0.0.5': + resolution: {integrity: sha512-6oSw3NbQIrnxxCRuEe190MbMFoOrM+P+u1usascQMgSjsMcVRnJ/ztvTnRrCDtfPEHJ5EC8drgWIpcdrhGsB0w==} cpu: [arm64] os: [linux] libc: [musl] - '@tsc-ts/linux-x64-gnu@0.0.3': - resolution: {integrity: sha512-BL9zownW4tHv/qqQkF8OlurHNDXi7xtGtJ6PV4qMeRYPMKsivTIGFRoZLW023KcLOr1Zptaof6X3zpv1nJEIpA==} + '@tsc-ts/linux-x64-gnu@0.0.5': + resolution: {integrity: sha512-KrlBLUcG7M6p7lN+ke+kHVLqy7IiY/H0FxFZzq5slXanhrps/BfjdkUahsyREg7KVprr08xFuXqJTBtLEI4eow==} cpu: [x64] os: [linux] libc: [glibc] - '@tsc-ts/linux-x64-musl@0.0.3': - resolution: {integrity: sha512-g4/lE5sULNGUVLM0Px/jrsjLsQQAYSiL+Rl+JZ+l5JE2k1dXroCejT2+IRB/NucVcz6N8EOUiCRZ13zl+Wtpcg==} + '@tsc-ts/linux-x64-musl@0.0.5': + resolution: {integrity: sha512-fr8yGzkocCcTYoE+NwC+/WGPNteQ2GmOPJTy/z9NcsLchnpSIZ1fzFll15UYPKQlwQdNRJXxKK3npVBtLRdHTg==} cpu: [x64] os: [linux] libc: [musl] - '@tsc-ts/win32-x64-msvc@0.0.3': - resolution: {integrity: sha512-oGtboSHgpuQP1dHAHqWyGjAWCpvHDpciYCi/2ywcp6MT+oOHxC99X48H/PZr29fzYVxCVeaEqWZ/VFU9qLZaKg==} + '@tsc-ts/win32-x64-msvc@0.0.5': + resolution: {integrity: sha512-SQwufJafzsiOBYncHOxYkyELWjLlBijcKB+xOs6hr6Lxxp5MMLoPtvDFNG88c7G5xv2TDb1XbvyxnjQltr6SHw==} cpu: [x64] os: [win32] @@ -2566,8 +2565,8 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} - tsc-ts@0.0.3: - resolution: {integrity: sha512-Qr0i/BxP3ULIPDfr38vRb02x7sj6Q0r70Gubgu4ty2J4CEDCUKzT8RT2KMNqLXI3ZAhl7cncJq7a4LjKYZ1+0g==} + tsc-ts@0.0.5: + resolution: {integrity: sha512-Eea+zNv7y696pqNZtXBd0AX7/6PXIhR7aaEb0EhgpHadbk7VEiURD94/NngeLpYWts2tPjetkpwFLJY0+6yOnQ==} engines: {node: '>=20'} hasBin: true @@ -3428,6 +3427,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true + '@scriptc/threads@0.3.6': {} + '@sinclair/typebox@0.25.24': {} '@standard-schema/spec@1.1.0': {} @@ -3441,25 +3442,22 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsc-ts/darwin-arm64@0.0.3': + '@tsc-ts/darwin-x64@0.0.5': optional: true - '@tsc-ts/darwin-x64@0.0.3': + '@tsc-ts/linux-arm64-gnu@0.0.5': optional: true - '@tsc-ts/linux-arm64-gnu@0.0.3': + '@tsc-ts/linux-arm64-musl@0.0.5': optional: true - '@tsc-ts/linux-arm64-musl@0.0.3': + '@tsc-ts/linux-x64-gnu@0.0.5': optional: true - '@tsc-ts/linux-x64-gnu@0.0.3': + '@tsc-ts/linux-x64-musl@0.0.5': optional: true - '@tsc-ts/linux-x64-musl@0.0.3': - optional: true - - '@tsc-ts/win32-x64-msvc@0.0.3': + '@tsc-ts/win32-x64-msvc@0.0.5': optional: true '@tybys/wasm-util@0.10.3': @@ -4798,15 +4796,16 @@ snapshots: ts-toolbelt@6.15.5: {} - tsc-ts@0.0.3: + tsc-ts@0.0.5: + dependencies: + '@scriptc/threads': 0.3.6 optionalDependencies: - '@tsc-ts/darwin-arm64': 0.0.3 - '@tsc-ts/darwin-x64': 0.0.3 - '@tsc-ts/linux-arm64-gnu': 0.0.3 - '@tsc-ts/linux-arm64-musl': 0.0.3 - '@tsc-ts/linux-x64-gnu': 0.0.3 - '@tsc-ts/linux-x64-musl': 0.0.3 - '@tsc-ts/win32-x64-msvc': 0.0.3 + '@tsc-ts/darwin-x64': 0.0.5 + '@tsc-ts/linux-arm64-gnu': 0.0.5 + '@tsc-ts/linux-arm64-musl': 0.0.5 + '@tsc-ts/linux-x64-gnu': 0.0.5 + '@tsc-ts/linux-x64-musl': 0.0.5 + '@tsc-ts/win32-x64-msvc': 0.0.5 tslib@2.8.1: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d4d370721..a3e992b0a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,14 +7,14 @@ minimumReleaseAgeExclude: - "effect@4.0.0" - "@effect/platform-node@4.0.0" - "@effect/platform-node-shared@4.0.0" - - "@tsc-ts/darwin-arm64@0.0.3" - - "@tsc-ts/darwin-x64@0.0.3" - - "@tsc-ts/linux-arm64-gnu@0.0.3" - - "@tsc-ts/linux-arm64-musl@0.0.3" - - "@tsc-ts/linux-x64-gnu@0.0.3" - - "@tsc-ts/linux-x64-musl@0.0.3" - - "@tsc-ts/win32-x64-msvc@0.0.3" - - "tsc-ts@0.0.3" + - "@tsc-ts/darwin-arm64@0.0.5" + - "@tsc-ts/darwin-x64@0.0.5" + - "@tsc-ts/linux-arm64-gnu@0.0.5" + - "@tsc-ts/linux-arm64-musl@0.0.5" + - "@tsc-ts/linux-x64-gnu@0.0.5" + - "@tsc-ts/linux-x64-musl@0.0.5" + - "@tsc-ts/win32-x64-msvc@0.0.5" + - "tsc-ts@0.0.5" allowBuilds: esbuild: true overrides: diff --git a/scripts/generate-ts7-ast-schema.mjs b/scripts/generate-ts7-ast-schema.mjs index 45a01c213..30c506869 100644 --- a/scripts/generate-ts7-ast-schema.mjs +++ b/scripts/generate-ts7-ast-schema.mjs @@ -268,9 +268,10 @@ if (process.argv.includes("--check")) { // declarations, and require every wire property to have a declared getter. const declarations = readFileSync(join(packageRoot, "dist/api/node/node.generated.d.ts"), "utf8"); const names = [...new Set(Object.values(protocol.childProperties).flat())].sort(); -// These views adapt TS7's shared postfix slot and reparsed JSDoc bodies. -// Their implementations live outside the generated region in AstNode. -const adaptedGetters = new Set(["body", "questionToken"]); +// These views adapt TS7's shared postfix slot and reparsed JSDoc bodies, and +// return modifiers as a modifier list. Their implementations live outside the +// generated region in AstNode. +const adaptedGetters = new Set(["body", "modifiers", "questionToken"]); const getters = names .filter((name) => !adaptedGetters.has(name)) .map((name) => { diff --git a/scripts/generate-ts7-ast-views.mjs b/scripts/generate-ts7-ast-views.mjs index 1525f6246..91e7ac2f7 100644 --- a/scripts/generate-ts7-ast-views.mjs +++ b/scripts/generate-ts7-ast-views.mjs @@ -65,6 +65,8 @@ export function generateAstViews(root, packageRoot, version, check) { if (key === "jsdocPropertyTags") type = "JSDocPropertyTag"; // A list's children are a node list, as every other list field. if (name === "SyntaxList" && key === "children") type = "NodeArray"; + // A declaration's modifiers are a modifier list, with their flags. + if (key === "modifiers") type = type.replace("NodeArray<", "ModifierArray<"); if (member.questionToken) type += " | undefined"; fields.set(key, ` readonly ${key}: ${type};`); } @@ -74,7 +76,7 @@ export function generateAstViews(root, packageRoot, version, check) { ...header, "", 'import type { AstNode } from "./ast-node.js";', - 'import type { JsxTagNamePropertyAccess, NodeArray } from "./ast-types.js";', + 'import type { JsxTagNamePropertyAccess, ModifierArray, NodeArray } from "./ast-types.js";', 'import { SyntaxKind, NodeFlags, ModifierFlags, TokenFlags } from "./enums.js";', "type Node = AstNode;", ]; @@ -99,6 +101,9 @@ export function generateAstViews(root, packageRoot, version, check) { ? `<${node.typeParameters.map((param) => param.getText(generated)).join(", ")}>` : ""; const fields = members(node.name.text); + // A default clause has no expression; the declaration shares the case clause's. + if (node.name.text === "DefaultClause" && fields.has("expression")) + fields.set("expression", " readonly expression: undefined;"); if (!fields.has("kind")) { const kinds = new Set(); for (const [name, child] of interfaces) { @@ -131,6 +136,7 @@ export function generateAstViews(root, packageRoot, version, check) { ...header, "", 'import { SyntaxKind, NodeFlags, ScriptKind, OuterExpressionKinds } from "./enums.js";', + 'import { getSourceFileOfNode } from "./node-functions.js";', ]; for (const stem of ["is.generated", "is"]) { const declarations = parse(`${stem}.d.ts`); @@ -173,10 +179,9 @@ export function generateAstViews(root, packageRoot, version, check) { } let body = fn.body.getText(source); if (name === "isJSDocTypeAssertion") - body = body.replace( - "expression.kind !==", - "expression === undefined || expression.kind !==", - ); + body = body + .replace("expression.kind !==", "expression === undefined || expression.kind !==") + .replace("node.getSourceFile()", "getSourceFileOfNode(node)"); if (head.includes("node: Node") && name.startsWith("is")) { head = head.replace("node: Node", "node: Node | undefined"); body = body.replace("{", "{\n if (node === undefined) return false;"); diff --git a/tests/fixtures/self-hosting/lowering-context-cases.ts b/tests/fixtures/self-hosting/lowering-context-cases.ts index efa27183d..a39063796 100644 --- a/tests/fixtures/self-hosting/lowering-context-cases.ts +++ b/tests/fixtures/self-hosting/lowering-context-cases.ts @@ -35,7 +35,7 @@ function runContextCase(services: FrontendServices, entry: string): string[] { const shadowSymbol = symbols[1]!; const unusedSymbol = symbols[2]!; requireValue(outerSymbol !== shadowSymbol, "shadowed bindings share identity"); - requireValue(outerSymbol.name === shadowSymbol.name, "fixture did not shadow a name"); + requireValue(ts.symbolName(outerSymbol) === ts.symbolName(shadowSymbol), "fixture did not shadow a name"); const source = bindingSource(declarations[0]!); const root = newFnCtx(false, null, null, VOID); const outer = declareContextLocal(root, "value", F64, true, outerSymbol, source); diff --git a/tests/fixtures/self-hosting/ts7-checker-cases.ts b/tests/fixtures/self-hosting/ts7-checker-cases.ts index da0a3dc52..664dccd50 100644 --- a/tests/fixtures/self-hosting/ts7-checker-cases.ts +++ b/tests/fixtures/self-hosting/ts7-checker-cases.ts @@ -2,7 +2,7 @@ import { AstNode } from "../../../packages/compiler/src/frontend/ts7/ast-node.js import type { SourceFile } from "../../../packages/compiler/src/frontend/ts7/ast-types.js"; import { CheckerFacade, constituentTypes } from "../../../packages/compiler/src/frontend/ts7/checker.js"; import { SemanticChecker } from "../../../packages/compiler/src/frontend/ts7/semantic-checker.js"; -import { SemanticType } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js"; +import { SemanticType, symbolName, symbolNameOf } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js"; import { SyntaxKind, TypeFlags } from "../../../packages/compiler/src/frontend/ts7/enums.js"; export function checkerSource(): string { @@ -67,8 +67,8 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque const box = declaration(root, "Box"); const boxSymbol = facade.getSymbolAtLocation(box.name!)!; const boxType = facade.getDeclaredTypeOfSymbol(boxSymbol); - check(facade.getPropertyOfType(boxType, "value")!.name === "value", "property lookup"); - check(facade.getPropertiesOfType(boxType).some((property) => property.name === "method"), "property list"); + check(symbolName(facade.getPropertyOfType(boxType, "value")!) === "value", "property lookup"); + check(facade.getPropertiesOfType(boxType).some((property) => symbolName(property) === "method"), "property list"); check(facade.getBaseTypes(boxType).length === 0, "empty class bases"); const child = facade.getDeclaredTypeOfSymbol(facade.getSymbolAtLocation(declaration(root, "Child").name!)!); check(facade.getBaseTypes(child)[0] === boxType, "class base identity"); @@ -123,7 +123,7 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque check(facade.getTypeArguments(number).length === 0, "nonreference arguments"); const dict = facade.getDeclaredTypeOfSymbol(facade.getSymbolAtLocation(declaration(root, "Dict").name!)!); const index = facade.getIndexInfosOfType(dict)[0]!; - check(index.keyType === string && index.valueType === number && index.isReadonly, "index signature"); + check(index.keyType === string && index.type === number && index.isReadonly, "index signature"); check(facade.getAwaitedType(aliasType(facade, root, "Promised")) === number, "await promise"); check(facade.getAwaitedType(aliasType(facade, root, "MaybePromised")) === number, "await collapsed union"); check(facade.getAwaitedType(aliasType(facade, root, "DifferentPromises")) === undefined, "await distinct union refusal"); diff --git a/tests/fixtures/self-hosting/ts7-client-cases.ts b/tests/fixtures/self-hosting/ts7-client-cases.ts index d97f90892..0feaee6c9 100644 --- a/tests/fixtures/self-hosting/ts7-client-cases.ts +++ b/tests/fixtures/self-hosting/ts7-client-cases.ts @@ -12,6 +12,7 @@ import { SyntaxKind, ScriptTarget, ModuleKind, ModuleResolutionKind, ModuleDetec import { Ts7Session } from "../../../packages/compiler/src/frontend/ts7/session.js"; import { Ts7Paths, ts7DocumentFile } from "../../../packages/compiler/src/frontend/ts7/session-path.js"; import { parseSemanticJson } from "../../../packages/compiler/src/frontend/ts7/semantic-json.js"; +import { symbolName, symbolNameOf } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js"; import { checkSemanticModel, semanticSource } from "./ts7-semantic-cases.js"; import { checkAstViews } from "./ts7-ast-views.js"; @@ -313,7 +314,7 @@ export function runTs7Client(client: Ts7RpcClient, directory: string, report: st const timing = client.timing(); check(timing.requests > 20 && timing.callbacks > 0 && reads > 0, "requests and filesystem callbacks executed"); writeFileSync(report, JSON.stringify({ - typeText, symbol: ownedSymbol.name, diagnostics: diagnostics.map((diagnostic) => diagnostic.code), + typeText, symbol: symbolName(ownedSymbol), diagnostics: diagnostics.map((diagnostic) => diagnostic.code), surrogateBoundary: checkSurrogateBoundary(), semanticSurrogateBoundary: checkSemanticSurrogateBoundary(), semanticModel: true, sessionLifecycle: true, echo: true, binaryAst: true, astIdentity: true, astViews: true, checkerFacade: true, checkerSnapshots: true, virtualFiles: true, retainedSnapshot: true, serverErrorRecovery: true, protocolFailures: true, diff --git a/tests/fixtures/self-hosting/ts7-semantic-cases.ts b/tests/fixtures/self-hosting/ts7-semantic-cases.ts index 41b61b824..07f2d7e14 100644 --- a/tests/fixtures/self-hosting/ts7-semantic-cases.ts +++ b/tests/fixtures/self-hosting/ts7-semantic-cases.ts @@ -1,7 +1,8 @@ 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 { SemanticSignature, SemanticSnapshot, SemanticSymbol, SemanticType, symbolName, symbolNameOf } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js"; +import type { __String } from "../../../packages/compiler/src/frontend/ts7/ast-types.js"; import { SemanticSignatureKind, SemanticSymbolFlags, SemanticTypeFlags } from "../../../packages/compiler/src/frontend/ts7/semantic-schema.generated.js"; export function semanticSource(): string { @@ -113,7 +114,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic 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"); + check(symbolName(pair.getAliasSymbol()!) === "Pair" && pair.getAliasTypeArguments()[0] === intrinsicNumber, "alias metadata"); const box = declaration(root, "Box"); const boxSymbol = symbolAt(checker, box.name!); @@ -121,21 +122,21 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic 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 boundMember = boxSymbol.getMembers().get("value" as __String)!; const checkedMember = checker.getPropertyOfType(boxType, "value")!; check(boundMember !== checkedMember && boundMember.declarationHandles[0]!.resolve() === checkedMember.declarationHandles[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.getPropertiesOfType(boxType).some((symbol) => symbolName(symbol) === "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?.nodes[0], "index signature declaration"); + check(index.keyType === intrinsicString && index.type === intrinsicNumber && index.isReadonly, "index signature types"); + check(index.declaration === declaration(root, "Dict").members?.nodes[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"); @@ -147,7 +148,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic 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]!.declarationHandles[0]!.resolve() === identity.parameters?.nodes[0], "signature parameters"); + check(symbolName(signature.getParameters()[0]!) === "value" && signature.getParameters()[0]!.declarationHandles[0]!.resolve() === identity.parameters?.nodes[0], "signature parameters"); check(signature.declarationHandle!.resolve() === identity, "signature declaration"); check(checker.getReturnTypeOfSignature(signature) === signatureTypes[0], "generic return identity"); check(checker.getParameterType(signature, 0) === signatureTypes[0], "parameter type query"); @@ -159,7 +160,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic 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(thisSignature.hasRestParameter && !thisSignature.isConstruct && symbolName(thisSignature.getThisParameter()!) === "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"); @@ -169,7 +170,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic const moduleSymbol = symbolAt(checker, root); const exports = moduleSymbol.getExports(); - const renamed = exports.get("renamed")!; + const renamed = exports.get("renamed" as __String)!; 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"); From f3c4ba0c200172ee672494912fc5711489b51471 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 13:43:22 -0500 Subject: [PATCH 20/22] Run macOS CI jobs nightly and for pull requests that change darwin code Pushes to main now run every lane except macos, so a merged commit no longer queues the macOS native build, the darwin test slices and the macOS LLVM artifact jobs. The nightly sweep and manual runs still run every lane. Pull requests keep running the macOS lane when scripts/ci-lanes.mjs selects it. - The iOS runtime pack builds move from mobile_runtime_packs (linux lane) to mobile_runtime_packs_ios (macos lane), since they run on macOS runners. The Android pack stays in the linux lane. - The macos_full dispatch input is replaced by macos_suite (full or darwin, default full). Manual runs run the complete macOS suite unless macos_suite is darwin. - ci-lanes.mjs also selects the macos lane for paths naming kqueue, the runtime's platform header, and every file of the darwin suite in scripts/ci-test-plan.mjs (adding tests/harness/differential.test.ts and the darwin runtime pack's archive test). - The required test check counts a skipped macOS job as passing when the macos lane is off, as it already did for pull requests. --- .github/workflows/ci.yml | 74 +++++++++++++++---------- scripts/ci-lanes.mjs | 23 +++++--- scripts/ci-test-plan.mjs | 15 ++--- tests/harness/ci-lanes.test.ts | 89 +++++++++++++++++++++++++++--- tests/harness/ci-test-plan.test.ts | 19 +++---- 5 files changed, 158 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbc921b74..bbc9ddb10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,21 +4,23 @@ on: push: branches: [main] pull_request: - # Nightly full macOS sweep, including on days without a push to main. + # Nightly full macOS sweep. Pushes to main run no macOS jobs, and pull requests run them only when they change + # darwin code (scripts/ci-lanes.mjs). schedule: - cron: "17 7 * * *" - # A manual run; macos_full runs the complete suite on macOS as the nightly sweep does (for example on a - # release candidate commit). + # A manual run, for example on a release candidate commit. It runs every lane; macos_suite selects the complete + # suite on macOS, as the nightly sweep runs it, or the darwin suite that pull requests run. workflow_dispatch: inputs: - macos_full: - description: Run the complete test suite on macOS - type: boolean - default: false + macos_suite: + description: The test suite to run on macOS + type: choice + options: [full, darwin] + default: full concurrency: # A new push to a pull request cancels its previous run. Every push to main runs to completion in a group of - # its own commit, so each merged commit gets a full result (releases require a green run on their commit). The + # its own commit, so each merged commit gets a result (releases require a green run on their commit). The # nightly sweep and manual runs neither cancel nor are cancelled by main pushes. group: ci-${{ github.event_name == 'schedule' && 'schedule' || github.event_name == 'workflow_dispatch' && format('dispatch-{0}', github.run_id) || github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} @@ -59,7 +61,8 @@ jobs: NODE # Which expensive lanes a pull request needs (scripts/ci-lanes.mjs, run from the base commit against the pull - # request's diff, so a pull request cannot change its own selection). Pushes and the nightly sweep run every lane. + # request's diff, so a pull request cannot change its own selection). Pushes to main run every lane except macos, + # whose jobs the nightly sweep runs on main. The nightly sweep and manual runs run every lane. lanes: needs: validation_scope if: ${{ needs.validation_scope.outputs.full == 'true' }} @@ -86,7 +89,9 @@ jobs: HEAD: ${{ github.event.pull_request.head.sha }} run: | every() { printf 'linux=true\nmacos=true\neffects=true\nbootstrap=true\n' | tee -a "$GITHUB_OUTPUT"; } - if [ "$EVENT" != pull_request ]; then + if [ "$EVENT" = push ]; then + printf 'linux=true\nmacos=false\neffects=true\nbootstrap=true\n' | tee -a "$GITHUB_OUTPUT" + elif [ "$EVENT" != pull_request ]; then every elif git show "$BASE:scripts/ci-lanes.mjs" > "$RUNNER_TEMP/ci-lanes.mjs" 2> /dev/null; then node "$RUNNER_TEMP/ci-lanes.mjs" --base "$BASE" --head "$HEAD" --github-output @@ -219,25 +224,25 @@ jobs: tests_macos: needs: [validation_scope, lanes, native_macos] if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.macos == 'true' }} - name: test (macOS, ${{ matrix.flavor }}, ${{ (github.event_name != 'schedule' && !inputs.macos_full) && 'darwin ' || '' }}${{ matrix.shard }}/${{ (github.event_name != 'schedule' && !inputs.macos_full) && '3' || '10' }}) + name: test (macOS, ${{ matrix.flavor }}, ${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && 'darwin ' || '' }}${{ matrix.shard }}/${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && '3' || '10' }}) # arm64 macOS: same clang/libc the differential + number-formatting - # oracles were verified against. Pull requests and pushes to main run the - # darwin suite (scripts/ci-test-plan.mjs) in three slices; the nightly - # schedule and manual runs with macos_full run the complete suite in ten. - # The complete suite on every push needed more macOS runners than the - # repository has. + # oracles were verified against. Pull requests that change darwin code and + # manual runs with macos_suite set to darwin run the darwin suite + # (scripts/ci-test-plan.mjs) in three slices; the nightly schedule and + # other manual runs run the complete suite in ten. The complete suite on + # every push needed more macOS runners than the repository has. runs-on: macos-15 strategy: fail-fast: false matrix: flavor: [plain, san] - shard: ${{ fromJSON((github.event_name != 'schedule' && !inputs.macos_full) && '[1, 2, 3]' || '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]') }} + shard: ${{ fromJSON((github.event_name != 'schedule' && inputs.macos_suite != 'full') && '[1, 2, 3]' || '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]') }} env: # See the Linux tests job for the worker cap and case partitioning. SCRIPTC_TEST_WORKERS: "4" SCRIPTC_SAN: ${{ matrix.flavor == 'san' && '1' || '' }} - SCRIPTC_TEST_SHARD: ${{ matrix.shard }}/${{ (github.event_name != 'schedule' && !inputs.macos_full) && '3' || '10' }} - SCRIPTC_CI_SUITE: ${{ (github.event_name != 'schedule' && !inputs.macos_full) && 'darwin' || 'full' }} + SCRIPTC_TEST_SHARD: ${{ matrix.shard }}/${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && '3' || '10' }} + SCRIPTC_CI_SUITE: ${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && 'darwin' || 'full' }} steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -254,7 +259,7 @@ jobs: path: ${{ runner.temp }} - run: tar -xzf "$RUNNER_TEMP/native-darwin-arm64.tar.gz" - run: pnpm build - - name: Tests (${{ (github.event_name != 'schedule' && !inputs.macos_full) && 'darwin suite' || 'corpus and integration suites' }}) + - name: Tests (${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && 'darwin suite' || 'corpus and integration suites' }}) run: node scripts/ci-test.mjs native_linux: @@ -868,14 +873,25 @@ jobs: strategy: fail-fast: false matrix: - include: - - runtime: runtime-ios-arm64 - runner: macos-15 - - runtime: runtime-ios-simulator-arm64 - runner: macos-15 - - runtime: runtime-android-arm64 - runner: ubuntu-24.04 - runs-on: ${{ matrix.runner }} + runtime: [runtime-android-arm64] + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/mobile-runtime-pack + with: + runtime: ${{ matrix.runtime }} + + # The iOS runtime packs build on macOS runners, so they run in the macos lane. + mobile_runtime_packs_ios: + needs: [validation_scope, lanes] + if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.macos == 'true' }} + name: test (${{ matrix.runtime }}) + strategy: + fail-fast: false + matrix: + runtime: [runtime-ios-arm64, runtime-ios-simulator-arm64] + runs-on: macos-15 timeout-minutes: 20 steps: - uses: actions/checkout@v4 @@ -908,6 +924,7 @@ jobs: windows_cli, llvm_artifacts_windows_x64, mobile_runtime_packs, + mobile_runtime_packs_ios, ] # Run even after failed or cancelled lanes: skipped required jobs allow merging. if: ${{ always() }} @@ -939,6 +956,7 @@ jobs: lane windows_cli "${{ needs.windows_cli.result }}" "${{ needs.lanes.outputs.linux }}" lane llvm_artifacts_windows_x64 "${{ needs.llvm_artifacts_windows_x64.result }}" "${{ needs.lanes.outputs.linux }}" lane mobile_runtime_packs "${{ needs.mobile_runtime_packs.result }}" "${{ needs.lanes.outputs.linux }}" + lane mobile_runtime_packs_ios "${{ needs.mobile_runtime_packs_ios.result }}" "${{ needs.lanes.outputs.macos }}" ;; false) echo "::notice::Full CI is deferred until this PR becomes the lowest unmerged layer. Merge the stack from the bottom up." diff --git a/scripts/ci-lanes.mjs b/scripts/ci-lanes.mjs index 9edc48a1c..b020f9de0 100644 --- a/scripts/ci-lanes.mjs +++ b/scripts/ci-lanes.mjs @@ -7,18 +7,20 @@ // own exemptions; a change to this script runs every lane until it is on the base branch. It uses only Node and git. // // Lanes (each an output, true or false): -// - linux: the Linux build and test shards, the TypeScript 7 parity sweep and the smaller Linux, Windows and mobile +// - linux: the Linux build and test shards, the TypeScript 7 parity sweep and the smaller Linux, Windows and Android // jobs. Off only when every change is documentation. -// - macos: the macOS native build, the darwin test shards and the macOS LLVM artifact jobs. On when the change touches -// a darwin package, a path naming darwin, macOS, iOS or Mach-O, the darwin test files of scripts/ci-test-plan.mjs, -// the LLVM helper (native/, packages/llvm-*) or the shared runtime pack code, or changed lines in packages/{runtime,compiler,cli} that mention darwin-specific APIs (__APPLE__, kqueue, Mach-O -// and similar). +// - macos: the macOS native build, the darwin test shards, the macOS LLVM artifact jobs and the iOS runtime packs. On +// when the change touches a darwin package, a path naming darwin, macOS, iOS, Mach-O or kqueue, the runtime's +// platform header, the darwin suite's test files (scripts/ci-test-plan.mjs), the LLVM helper (native/, +// packages/llvm-*) or the shared runtime pack code, or changed lines in packages/{runtime,compiler,cli} that mention +// darwin-specific APIs (__APPLE__, kqueue, Mach-O and similar). // - effects: the Effect suites. On when the change touches their harness or fixtures. // - bootstrap: the native bootstrap seed and self-rebuild contracts. On when the change touches the bootstrap scripts, // the native CLI build, the cli-* packages or the self-hosting tests. // Every lane is on when the change touches CI configuration, the workspace manifests or lockfile, vitest -// configuration, the shared test harness modules or this script, or when the diff cannot be computed. Pushes to main -// and the nightly sweep do not use this script: they run every lane. +// configuration, the shared test harness modules or this script, or when the diff cannot be computed. Pushes to main, +// the nightly sweep and manual runs do not use this script: pushes to main run every lane except macos, and the others +// run every lane. import { execFileSync } from "node:child_process"; import { appendFileSync } from "node:fs"; @@ -38,9 +40,11 @@ const everything = [ /^packages\/[^/]+\/package\.json$/, ]; const documentation = [/^docs\//, /\.mdx?$/, /^LICENSE$/, /^CHANGELOG\.md$/]; -const darwinPath = /darwin|macos|ios|apple|mach-?o/i; +const darwinPath = /darwin|macos|ios|apple|mach-?o|kqueue/i; // Native code generation and runtime packs every platform shares. const sharedNative = /^native\/|^packages\/(llvm-[^/]+|runtime-pack-common)\//; +// The darwin suite's files (darwinCorpusFiles and darwinFiles in scripts/ci-test-plan.mjs), the LLVM differential +// the macOS artifact jobs run, and the runtime's platform header. const darwinFiles = [ "packages/cli/test/bootstrap.test.ts", "packages/cli/test/native-link-info.test.ts", @@ -49,7 +53,10 @@ const darwinFiles = [ "packages/compiler/test/debug-variables.test.ts", "packages/compiler/test/native-codegen-integration.test.ts", "packages/compiler/test/source-debugging.test.ts", + "packages/runtime-darwin-arm64/test/archive.test.ts", + "packages/runtime/src/scr_platform.h", "packages/runtime/test/string.test.ts", + "tests/harness/differential.test.ts", "tests/harness/ffi-callback-pool.test.ts", "tests/harness/ffi-catalog.test.ts", "tests/harness/ffi-frameworks.test.ts", diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 55eb65e72..2b76b52d6 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -69,13 +69,14 @@ export function sideOwners(flavor, count) { return owners; } -// Pull requests and pushes to main run the full suite on Linux. Their macOS -// lane runs the darwin suite, which keeps the darwin clang/libc/kqueue -// oracles: the Node differential corpus, plus every file whose assertions run -// only on darwin or take darwin-specific branches (including the Mach-O -// library localization arm and the arm64 runtime string oracle). -// The nightly schedule and manual runs with macos_full run the full suite on -// macOS. +// Pull requests and pushes to main run the full suite on Linux. Pull requests +// that change darwin code (scripts/ci-lanes.mjs) also run the darwin suite on +// macOS, which keeps the darwin clang/libc/kqueue oracles: the Node +// differential corpus, plus every file whose assertions run only on darwin or +// take darwin-specific branches (including the Mach-O library localization arm +// and the arm64 runtime string oracle). Pushes to main run no macOS jobs. The +// nightly schedule and manual runs run the full suite on macOS, or the darwin +// suite with macos_suite set to darwin. export const darwinCorpusFiles = ["tests/harness/differential.test.ts"]; export const darwinFiles = [ "packages/cli/test/bootstrap.test.ts", diff --git a/tests/harness/ci-lanes.test.ts b/tests/harness/ci-lanes.test.ts index c0e74a934..faca8a7d0 100644 --- a/tests/harness/ci-lanes.test.ts +++ b/tests/harness/ci-lanes.test.ts @@ -1,7 +1,22 @@ -import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { load } from "js-yaml"; import { expect, test } from "vitest"; import { select } from "../../scripts/ci-lanes.mjs"; +import { darwinCorpusFiles, darwinFiles } from "../../scripts/ci-test-plan.mjs"; + +type Job = { + if?: string; + "runs-on"?: string; + strategy?: { matrix?: { include?: { runner?: string }[] } }; + steps: { id?: string; run?: string }[]; +}; +const workflow = load( + readFileSync(new URL("../../.github/workflows/ci.yml", import.meta.url), "utf8"), +) as { on: { workflow_dispatch: { inputs: Record } }; jobs: Record }; +const lanesScript = workflow.jobs["lanes"]!.steps.find((step) => step.id === "lanes")!.run!; type Selection = { on: Record; reasons: string[] }; const lanesOf = (files: string[], lines: { file: string; text: string }[] = []) => @@ -43,6 +58,16 @@ test.each([ ["tests/harness/ffi-frameworks.test.ts"], { linux: true, macos: true, effects: false, bootstrap: false }, ], + [ + "the kqueue event loop", + ["packages/runtime/src/scr_loop_kqueue.c"], + { linux: true, macos: true, effects: false, bootstrap: false }, + ], + [ + "the runtime's platform header", + ["packages/runtime/src/scr_platform.h"], + { linux: true, macos: true, effects: false, bootstrap: false }, + ], [ "the Effect harness", ["tests/harness/effect.test.ts"], @@ -91,15 +116,61 @@ test("a changed line that mentions a darwin-specific API runs the macOS lane", ( expect(lanesOf([file], [{ file, text: " struct kevent change;" }]).macos).toBe(true); }); +test("every file of the darwin suite runs the macOS lane", () => { + for (const file of [...darwinCorpusFiles, ...darwinFiles]) + expect(lanesOf([file]).macos, file).toBe(true); +}); + test("CI runs the base commit's lane selection against the pull request's diff", () => { - const workflow = load( - readFileSync(new URL("../../.github/workflows/ci.yml", import.meta.url), "utf8"), - ) as { jobs: Record }; - const script = workflow.jobs["lanes"]!.steps.find((step) => step.id === "lanes")!.run!; - expect(script).toContain('git show "$BASE:scripts/ci-lanes.mjs"'); - expect(script).toContain( + expect(lanesScript).toContain('git show "$BASE:scripts/ci-lanes.mjs"'); + expect(lanesScript).toContain( 'node "$RUNNER_TEMP/ci-lanes.mjs" --base "$BASE" --head "$HEAD" --github-output', ); - // Pushes and the nightly sweep, and a base without the script, run every lane. - expect(script.match(/every$/gm)).toHaveLength(2); + // The nightly sweep and manual runs, and a base without the script, run every lane. + expect(lanesScript.match(/every$/gm)).toHaveLength(2); +}); + +function runLanes(event: string) { + const directory = mkdtempSync(join(tmpdir(), "ci-lanes-")); + try { + const output = join(directory, "output"); + writeFileSync(output, ""); + const result = spawnSync("bash", ["-e", "-o", "pipefail", "-c", lanesScript], { + encoding: "utf8", + env: { ...process.env, EVENT: event, GITHUB_OUTPUT: output }, + }); + expect(result.status, result.stderr).toBe(0); + return readFileSync(output, "utf8"); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +test("pushes to main run every lane except macos; the nightly sweep and manual runs run every lane", () => { + expect(runLanes("push")).toBe("linux=true\nmacos=false\neffects=true\nbootstrap=true\n"); + for (const event of ["schedule", "workflow_dispatch"]) + expect(runLanes(event), event).toBe("linux=true\nmacos=true\neffects=true\nbootstrap=true\n"); + expect(workflow.on.workflow_dispatch.inputs["macos_suite"]).toEqual({ + description: "The test suite to run on macOS", + type: "choice", + options: ["full", "darwin"], + default: "full", + }); +}); + +test("every job on a macOS runner is in the macos lane", () => { + const macosJobs = Object.entries(workflow.jobs).filter(([, job]) => + [job["runs-on"], ...(job.strategy?.matrix?.include ?? []).map((entry) => entry.runner)].some( + (runner) => runner?.startsWith("macos"), + ), + ); + expect(macosJobs.map(([name]) => name).sort()).toEqual([ + "llvm_artifacts_macos_arm64", + "llvm_artifacts_macos_x64", + "mobile_runtime_packs_ios", + "native_macos", + "tests_macos", + ]); + for (const [name, job] of macosJobs) + expect(job.if, name).toContain("needs.lanes.outputs.macos == 'true'"); }); diff --git a/tests/harness/ci-test-plan.test.ts b/tests/harness/ci-test-plan.test.ts index 2343407df..34d61346f 100644 --- a/tests/harness/ci-test-plan.test.ts +++ b/tests/harness/ci-test-plan.test.ts @@ -201,12 +201,12 @@ test.for([1, 2, 3])("the darwin suite retains every darwin file with %i shards", /** Expands the ${{ }} expressions of a workflow value for one event: string * literals, ==, !=, !, &&, || and fromJSON, with matrix.shard left as * "{shard}". A value that is a single expression returns its result. */ -function expand(value: string, context: { event: string; macosFull: boolean | null }): unknown { +function expand(value: string, context: { event: string; macosSuite: string | null }): unknown { const evaluate = (expression: string): unknown => new Function( `return (${expression .replaceAll("github.event_name", JSON.stringify(context.event)) - .replaceAll("inputs.macos_full", JSON.stringify(context.macosFull)) + .replaceAll("inputs.macos_suite", JSON.stringify(context.macosSuite)) .replaceAll("fromJSON", "JSON.parse")});`, )(); const single = /^\$\{\{(.*)\}\}$/s.exec(value); @@ -240,18 +240,17 @@ test("pull requests run the full suite on Linux and the darwin suite on macOS", ).toBe("native-linux-x64-gnu"); expect(macos["runs-on"]).toBe("macos-15"); expect(macos.needs).toContain("native_macos"); - // Pull requests, pushes to main and plain manual runs run the darwin suite in - // three slices; the nightly sweep and manual runs with macos_full run the + // Pull requests and manual runs with macos_suite set to darwin run the darwin + // suite in three slices; the nightly sweep and other manual runs run the // complete suite in ten. - for (const [event, macosFull, full] of [ + for (const [event, macosSuite, full] of [ ["pull_request", null, false], - ["push", null, false], - ["workflow_dispatch", false, false], - ["workflow_dispatch", true, true], + ["workflow_dispatch", "darwin", false], + ["workflow_dispatch", "full", true], ["schedule", null, true], ] as const) { - const context = { event, macosFull }; - const label = `${event} ${macosFull}`; + const context = { event, macosSuite }; + const label = `${event} ${macosSuite}`; expect(expand(String(macos.strategy.matrix.shard), context), label).toEqual( full ? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] : [1, 2, 3], ); From eba133dc374f3afd177b05f12170b9d6ad012872 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 13:47:38 -0500 Subject: [PATCH 21/22] Order the undefined members widening adds after an object literal's own Widening an array or union of object literals gives each literal an optional `undefined` member for every property only its siblings declare. A record shape took its key order from the checker's member list, where those members sit wherever the checker put them: TypeScript 7.0.2 by the literal its checker widened first, TypeScript 7.1 and tsc-ts by the sibling that declares them. tsgo and tsc-ts then gave webassembly-wpt-jsapi.mjs different LLVM IR. The members are never own keys of the literal's value, so they now follow the declared members, by name. --- packages/compiler/src/frontend/type-mapper.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index c2fa0a06d..35e162ced 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -7112,6 +7112,30 @@ function refinedConstituentShape( return found; } +/** The optional `undefined` members widening adds to an object literal's + * type for properties only its sibling literals declare (`[{ a: 1 }, { b: 2 + * }]` gives the first `b?: undefined`). They are never own keys of the + * literal's value, so they go after the declared members, by name. Where + * the checker lists them is not observable: TypeScript 7.0.2 orders them + * by the literal its checker widened first, TypeScript 7.1 by the sibling + * that declares them. */ +function wideningAddedProperties( + type: ts.Type, + props: readonly ts.Symbol[], + ctx: TypeMapperCtx, +): Set { + const added = new Set(); + const literal = type.getSymbol()?.declarations.find(ts.isObjectLiteralExpression); + if (literal === undefined || literal.properties.nodes.some(ts.isSpreadAssignment)) return added; + for (const prop of props) { + if ((prop.flags & ts.SymbolFlags.Optional) === 0 || prop.declarations.length === 0) continue; + if (prop.declarations.some((d) => d.parent === literal)) continue; + if ((ctx.checker.getTypeOfSymbol(prop).flags & ts.TypeFlags.Undefined) === 0) continue; + added.add(prop.name); + } + return added; +} + function mapRecordTypeInner( widened: ts.Type, ctx: TypeMapperCtx, @@ -7425,8 +7449,12 @@ function mapRecordTypeInner( // indices, from folded numeric computed keys and quoted numeric keys) // enumerate FIRST ascending, then string keys in insertion order // (OrdinaryOwnPropertyKeys) — Object.keys/JSON/inspect all read it. + const widenedAbsent = wideningAddedProperties(widened, props, ctx); const declaredOrder = esOwnKeyOrder( - fields.filter((f) => accessorSlotProp(f.name) === null).map((f) => f.name), + fields + .filter((f) => accessorSlotProp(f.name) === null && !widenedAbsent.has(f.name)) + .map((f) => f.name) + .concat([...widenedAbsent].sort()), ); // With accessors: every own key in that order, an accessor property // named once for its get/set pair (the checked-dynamic copy lists it). From 81908207ccf2ade20229e557a8f3dd91be526eaf Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 14:14:58 -0500 Subject: [PATCH 22/22] Read a widening-added member's name with symbolName --- packages/compiler/src/frontend/type-mapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a0dfff906..c9a1cc9df 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -7228,7 +7228,7 @@ function wideningAddedProperties( if ((prop.flags & ts.SymbolFlags.Optional) === 0 || prop.declarations.length === 0) continue; if (prop.declarations.some((d) => d.parent === literal)) continue; if ((ctx.checker.getTypeOfSymbol(prop).flags & ts.TypeFlags.Undefined) === 0) continue; - added.add(prop.name); + added.add(ts.symbolName(prop)); } return added; }