fix(javascript): separate erased types from runtime semantics

Record erasure during definition indexing and reuse it across runtime references, captures, effects, module links, routes, Electron and Vite. Preserve source type links and emitted decorator and computed-key effects; keep actual runtime reads and escapes conservative.

Validation: 1,647 focused tests and 9 documentation tests passed, with build, static and documentation gates. The inherited reference-lifetimes stress file exhausts a 512 MiB worker on both unmodified 18451e479 and this change; that file remains unverified. TypeScript 7.0.2 emit was inspected for ES2022 and ES2018 without executing emitted code.
This commit is contained in:
morluto committed 2026-10-24 18:00:43 +08:00
1 parent cab78de506
commit 89006609f1
32 files changed
+756 -328

No files matched your search

+7 -2
View File
@@ -510,8 +510,13 @@ escape uncertainty. External prototype changes and unsupported runtime protocols
remain unknown rather than supplying exact post-invocation values.
Identifier references and closure captures share Babel syntax-role facts plus
explicit binding/write roles; noncomputed declaration keys and import/meta names
are not lexical reads. Provenance-only expression-depth omissions enter the same typed source coverage
as value-evaluation bounds and remain visible in retained application Evidence.
are not lexical reads. TypeScript type positions, including `typeof` type queries
and computed keys in erased signatures, do not evaluate JavaScript bindings or
create runtime calls or captures. Decorators on declared class fields can still
run when the class is defined, and a decorated declared or abstract field can
evaluate its computed key. Provenance-only expression-depth omissions enter
the same typed source coverage as value-evaluation bounds and remain visible in
retained application Evidence.
Immediate synchronous function results also share the loader reference owner.
An expression arrow returning the retained CommonJS `require` can preserve that
loader identity. Possible loader results with opaque alternatives or block
@@ -431,3 +431,69 @@ describe("Webpack externals through the loader's completion paths", () => {
]);
});
});
describe("Webpack identity through erased TypeScript syntax", () => {
it.each([
[
"the module table",
bootstrap(
EXTERNAL,
CONSUMER,
"interface RuntimeShape { m(): void } n(100);",
),
],
[
"the fresh module object",
bootstrap(EXTERNAL, CONSUMER).replace(
"return m[t](o,o.exports,n)",
"interface ModuleShape { o(): void } return m[t](o,o.exports,n)",
),
],
[
"the module table through a type query",
bootstrap(EXTERNAL, CONSUMER, "type RuntimeShape = typeof m; n(100);"),
],
[
"the fresh module object through a type query",
bootstrap(EXTERNAL, CONSUMER).replace(
"return m[t](o,o.exports,n)",
"type ModuleShape = typeof o; return m[t](o,o.exports,n)",
),
],
[
"the loader beside an uncalled installer",
bootstrap(
EXTERNAL,
CONSUMER,
"n.m=m;n.i=e=>{for(var k in e)n.m[k]=e[k]};interface RuntimeShape { n(): void } n(100);",
),
],
[
"the loader through a type query beside an uncalled installer",
bootstrap(
EXTERNAL,
CONSUMER,
"n.m=m;n.i=e=>{for(var k in e)n.m[k]=e[k]};type RuntimeShape = typeof n; n(100);",
),
],
])("does not let erased TypeScript syntax escape %s", (_name, source) => {
const electron = electronOf(source);
expect(electron.browser_windows).toMatchObject([
{ preload_path: "./preload.js" },
]);
expect(electron.ipc).toMatchObject([
{ channel: "get-connections", side: "main" },
]);
});
it.each([
"const shape = { [m]: true }; n(100);",
"const kind = typeof m; n(100);",
"m.extra; n(100);",
])(
"keeps an actual table reference outside the loader unproven: %s",
(entry) => {
expectUnresolved(bootstrap(EXTERNAL, CONSUMER, entry));
},
);
});
@@ -734,6 +734,29 @@ it("resolves an arrow handler in place when its inherited this is module scope",
]),
).toEqual([["ch", "wrapper-substitution"]]);
});
it.each(["interface Shape { marker(): void }", "type Shape = typeof marker;"])(
"does not give a handler invocation identity through erased syntax: %s",
(declaration) => {
const result = analyzeJavaScriptStaticSource(`
const { contextBridge, ipcRenderer } = require("electron");
const CH = "ch";
function register(marker) {
const handler = () => { ${declaration} };
ipcRenderer.on(CH, handler);
}
contextBridge.exposeInMainWorld("api", { on: () => register("unused") });
`);
expect(
result.electron.ipc
.filter(({ mode }) => mode === "listen")
.map(({ channel, resolution_basis }) => [
channel,
resolution_basis ?? "direct",
]),
).toEqual([["ch", "wrapper-substitution"]]);
expect(result.parse_status).toBe("complete");
},
);
it("resolves a handler a module-level factory call created once in place", () => {
const result = analyzeJavaScriptStaticSource(`
const { ipcMain } = require("electron");
@@ -14,6 +14,7 @@ import {
semanticResolutionBlocked,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { semanticIdentifierRole } from "./javascriptSemanticProjection.js";
import type { JavaScriptElectronIdentity } from "./javascriptSemanticAnalysis.js";
import type { JavaScriptAnalysisAccumulator } from "./javascriptStaticAnalysisState.js";
import type {
@@ -35,7 +36,7 @@ import {
import { argumentNode } from "./javascriptStaticAnalysisHelpers.js";
import {
childNodes,
traverseJavaScriptAst,
traverseJavaScriptValueAst,
} from "./javascriptSemanticTraversal.js";
import { range, calleeName } from "./javascriptStaticAnalysisHelpers.js";
import { ipcDescriptor } from "./electronStaticAnalysisIpc.js";
@@ -440,28 +441,33 @@ export const collectElectronWrappedCallPaths = (
if (activatedScopes === 0) return { captures: false, traceDependent };
const scopes = new Set(enclosing.slice(0, activatedScopes));
let captures = false;
t.traverseFast(fn.node, (node) => {
if (captures) return;
if (
t.isThisExpression(node) ||
t.isSuper(node) ||
(t.isMetaProperty(node) && node.meta.name === "new")
) {
traverseJavaScriptValueAst(fn.node, state.erasedSyntaxNodes, {
enter: (node, parent, readAncestors) => {
if (captures) return;
if (
t.isThisExpression(node) ||
t.isSuper(node) ||
(t.isMetaProperty(node) && node.meta.name === "new")
) {
captures =
contextOwnerActivated && inheritsFunctionContext(node, fn.node);
return;
}
if (!t.isIdentifier(node)) return;
if (
parent !== null &&
semanticIdentifierRole(node, parent, readAncestors) === null
)
return;
const binding = resolveSemanticBindingState(state, node, node.name);
captures =
contextOwnerActivated && inheritsFunctionContext(node, fn.node);
return;
}
if (!t.isIdentifier(node)) return;
const parent = state.parentsByNode.get(node);
if (parent !== undefined && !t.isReferenced(node, parent)) return;
const binding = resolveSemanticBindingState(state, node, node.name);
captures =
binding === undefined
? (node.name === "arguments" &&
contextOwnerActivated &&
inheritsFunctionContext(node, fn.node)) ||
semanticResolutionBlocked(state, node, node.name)
: scopes.has(binding.scopeId);
binding === undefined
? (node.name === "arguments" &&
contextOwnerActivated &&
inheritsFunctionContext(node, fn.node)) ||
semanticResolutionBlocked(state, node, node.name)
: scopes.has(binding.scopeId);
},
});
return { captures, traceDependent };
};
@@ -1136,6 +1142,7 @@ export const collectElectronWrappedCallPaths = (
env: SymbolicEnvironment,
context: PathContext,
): void {
if (state.erasedSyntaxNodes.has(node)) return;
if (--remainingVisits < 0 || t.isFunction(node) || t.isClass(node)) return;
if (t.isBlockStatement(node)) {
let next = context;
@@ -1187,7 +1194,7 @@ export const collectElectronWrappedCallPaths = (
call(node, env, context);
for (const child of childNodes(node)) walk(child, env, context);
}
traverseJavaScriptAst(program, {
traverseJavaScriptValueAst(program, state.erasedSyntaxNodes, {
enter: (node) => {
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
return;
@@ -2,12 +2,13 @@ import * as t from "@babel/types";
import type { javascriptBootstrapAt } from "./javascriptBootstrap.js";
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
import { semanticIdentifierRole } from "./javascriptSemanticProjection.js";
import {
resolveSemanticBindingState,
semanticStableInitializer,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import { evaluateSemanticExpression } from "./javascriptSemanticValues.js";
type Bootstrap = NonNullable<ReturnType<typeof javascriptBootstrapAt>>;
@@ -89,7 +90,7 @@ const loaderShape = (
const calls: t.CallExpression[] = [];
const modules = new Set<string>();
const returns: (t.Expression | null | undefined)[] = [];
traverseJavaScriptAst(loader.body, {
traverseJavaScriptValueAst(loader.body, state.erasedSyntaxNodes, {
skip: (node) => t.isFunction(node),
enter: (node) => {
if (t.isReturnStatement(node)) returns.push(node.argument);
@@ -284,11 +285,12 @@ const moduleObjectContained = (
state: JavaScriptSemanticAnalysisState,
): boolean => {
let contained = true;
traverseJavaScriptAst(loader.body, {
traverseJavaScriptValueAst(loader.body, state.erasedSyntaxNodes, {
enter: (node, parent, readAncestors) => {
if (!contained || !t.isIdentifier(node) || parent === null) return;
const ancestors = readAncestors();
if (!t.isReferenced(node, parent, ancestors.at(-2))) return;
if (semanticIdentifierRole(node, parent, () => ancestors) === null)
return;
if (bindingOf(state, node) !== moduleId) return;
contained =
!ancestors.some((ancestor) => t.isFunction(ancestor)) &&
@@ -328,7 +330,7 @@ const withCacheReads = (
if (shape.cacheId === undefined) return shape;
const sites = new Set(shape.cacheSites);
let contained = true;
traverseJavaScriptAst(loader.body, {
traverseJavaScriptValueAst(loader.body, state.erasedSyntaxNodes, {
skip: (node) => t.isFunction(node),
enter: (node, parent) => {
if (
@@ -445,13 +447,14 @@ const referencesAreContained = (
state,
);
let contained = true;
traverseJavaScriptAst(call, {
traverseJavaScriptValueAst(call, state.erasedSyntaxNodes, {
skip: () => !contained,
enter: (node, parent, readAncestors) => {
if (!t.isIdentifier(node) || !names.has(node.name) || parent === null)
return;
const ancestors = readAncestors();
if (!t.isReferenced(node, parent, ancestors.at(-2))) return;
if (semanticIdentifierRole(node, parent, () => ancestors) === null)
return;
const id = bindingOf(state, node);
const role = id === undefined ? undefined : tracked.get(id);
if (role === undefined) return;
@@ -486,7 +489,7 @@ const unreadRuntimeDefinitions = (
const definitions = new Map<t.Node, string>();
const readKeys = new Set<string>();
let confined = true;
traverseJavaScriptAst(call, {
traverseJavaScriptValueAst(call, state.erasedSyntaxNodes, {
enter: (node, parent, readAncestors) => {
// A helper invoked as `loader.helper()` can obtain the namespace through
// its receiver instead of a tracked loader identifier.
@@ -494,7 +497,7 @@ const unreadRuntimeDefinitions = (
if (!t.isIdentifier(node) || parent === null) return;
const ancestors = readAncestors();
if (
!t.isReferenced(node, parent, ancestors.at(-2)) ||
semanticIdentifierRole(node, parent, () => ancestors) === null ||
!loaderIds.has(bindingOf(state, node) ?? "")
)
return;
@@ -27,7 +27,7 @@ import {
} from "./javascriptStaticAnalysisHelpers.js";
import {
completeSemanticSteps,
traverseJavaScriptAst,
traverseJavaScriptValueAst,
} from "./javascriptSemanticTraversal.js";
/** Loader bindings and literal externals proven for one standalone bootstrap. */
@@ -188,6 +188,7 @@ const canonicalWebpackInteropBindings = (
completeSemanticSteps(
collectJavaScriptIdentityWritesSteps(
state.program,
state.erasedSyntaxNodes,
(root, kind) => {
return kind === "member" && loaderMayOwnReference(root)
? [["loader"]]
@@ -266,7 +267,7 @@ const isInstalledBeforeEntryDispatch = (
for (let index = 0; index < body.body.length; index += 1) {
const topLevel = body.body[index];
if (topLevel === undefined) continue;
traverseJavaScriptAst(topLevel, {
traverseJavaScriptValueAst(topLevel, state.erasedSyntaxNodes, {
enter: (node, _parent, ancestors) => {
if (
entryIndex < 0 &&
@@ -12,7 +12,7 @@ import {
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAstSteps } from "./javascriptSemanticTraversal.js";
import { semanticPropertyPathKeyMatches } from "./javascriptSemanticPropertyPaths.js";
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
import { evaluateSemanticExpression } from "./javascriptSemanticValues.js";
@@ -441,154 +441,158 @@ export function* indexCommonJsExportReceiversSteps(
if (exportsWrite) exportsIdentity = selected;
};
yield* traverseJavaScriptAstSteps(state.program, {
enter: (node, parent) => {
const outer = execution.at(-1) ?? "ordered";
const mode: Execution =
outer === "deferred" || t.isFunction(node) || t.isClass(node)
? "deferred"
: outer === "conditional" ||
// Mark the whole operation: a later tree-visited RHS must not
// restore certainty after an earlier tree-visited pattern write.
hasDestructuringEvaluationOrder(node) ||
t.isIfStatement(node) ||
t.isSwitchStatement(node) ||
t.isLoop(node) ||
t.isTryStatement(node) ||
t.isObjectPattern(node) ||
t.isArrayPattern(node) ||
(t.isLogicalExpression(parent) && parent.right === node) ||
(t.isAssignmentPattern(parent) && parent.right === node) ||
(t.isAssignmentExpression(parent) &&
["&&=", "||=", "??="].includes(parent.operator) &&
parent.right === node) ||
t.isOptionalCallExpression(parent) ||
(t.isOptionalMemberExpression(parent) &&
parent.property === node) ||
(t.isConditionalExpression(parent) && parent.test !== node)
? "conditional"
: "ordered";
execution.push(mode);
if (
t.isAssignmentExpression(node) &&
commonJsExportName(node.left, state) !== undefined &&
!isCommonJsModuleExports(node.left, state)
) {
const receiver =
t.isMemberExpression(node.left) ||
t.isOptionalMemberExpression(node.left)
? node.left.object
: null;
captures.set(
node,
mode !== "ordered"
? null
: receiver !== null &&
isUnshadowedGlobal(receiver, state, "exports")
? exportsIdentity
: publicIdentity,
);
}
const descriptor = commonJsDefinedExport(node, state);
if (descriptor !== undefined) {
const receiver = isUnshadowedGlobal(
descriptor.receiver,
state,
"exports",
)
? exportsIdentity
: publicIdentity;
const names = describedFields.get(receiver) ?? new Set<string>();
names.add(descriptor.exportedName);
describedFields.set(receiver, names);
}
const defined = descriptor ?? commonJsAssignedExports(node, state);
if (defined !== undefined && t.isCallExpression(node))
captures.set(
node,
mode !== "ordered"
? null
: isUnshadowedGlobal(defined.receiver, state, "exports")
? exportsIdentity
: publicIdentity,
);
},
exit: (node) => {
const mode = execution.pop() ?? "conditional";
if (publicCallWrites.has(node)) {
if (mode === "deferred") deferredReplacement = true;
else publicIdentity = null;
}
if (t.isAssignmentExpression(node)) {
const left = unwrapJavaScriptExpression(node.left).node;
const identity =
node.operator === "=" &&
(t.isIdentifier(left) ||
t.isMemberExpression(left) ||
t.isOptionalMemberExpression(left))
? valueOf(node.right)
: null;
for (const target of semanticWriteTargets(node.left))
write(target, identity, mode);
values.set(node, identity);
} else if (t.isVariableDeclarator(node) && node.init != null) {
for (const target of semanticWriteTargets(node.id))
write(
target,
t.isIdentifier(node.id) ? valueOf(node.init) : null,
mode,
yield* traverseJavaScriptValueAstSteps(
state.program,
state.erasedSyntaxNodes,
{
enter: (node, parent) => {
const outer = execution.at(-1) ?? "ordered";
const mode: Execution =
outer === "deferred" || t.isFunction(node) || t.isClass(node)
? "deferred"
: outer === "conditional" ||
// Mark the whole operation: a later tree-visited RHS must not
// restore certainty after an earlier tree-visited pattern write.
hasDestructuringEvaluationOrder(node) ||
t.isIfStatement(node) ||
t.isSwitchStatement(node) ||
t.isLoop(node) ||
t.isTryStatement(node) ||
t.isObjectPattern(node) ||
t.isArrayPattern(node) ||
(t.isLogicalExpression(parent) && parent.right === node) ||
(t.isAssignmentPattern(parent) && parent.right === node) ||
(t.isAssignmentExpression(parent) &&
["&&=", "||=", "??="].includes(parent.operator) &&
parent.right === node) ||
t.isOptionalCallExpression(parent) ||
(t.isOptionalMemberExpression(parent) &&
parent.property === node) ||
(t.isConditionalExpression(parent) && parent.test !== node)
? "conditional"
: "ordered";
execution.push(mode);
if (
t.isAssignmentExpression(node) &&
commonJsExportName(node.left, state) !== undefined &&
!isCommonJsModuleExports(node.left, state)
) {
const receiver =
t.isMemberExpression(node.left) ||
t.isOptionalMemberExpression(node.left)
? node.left.object
: null;
captures.set(
node,
mode !== "ordered"
? null
: receiver !== null &&
isUnshadowedGlobal(receiver, state, "exports")
? exportsIdentity
: publicIdentity,
);
} else if (t.isForOfStatement(node) || t.isForInStatement(node)) {
if (!t.isVariableDeclaration(node.left))
for (const target of semanticWriteTargets(node.left))
write(target, null, mode);
} else if (
t.isUpdateExpression(node) ||
t.isUnaryExpression(node, { operator: "delete" })
) {
if (t.isUnaryExpression(node, { operator: "delete" })) {
const name = commonJsExportName(node.argument, state);
if (
name !== undefined &&
!isCommonJsModuleExports(node.argument, state) &&
(t.isMemberExpression(node.argument) ||
t.isOptionalMemberExpression(node.argument))
) {
const receiver = isUnshadowedGlobal(
node.argument.object,
state,
"exports",
)
? exportsIdentity
: publicIdentity;
const fields = deletions.get(receiver) ?? new Map();
fields.set(name, {
node,
mode: name === "*" && mode === "ordered" ? "conditional" : mode,
});
deletions.set(receiver, fields);
}
}
write(node.argument, null, mode);
} else if (isUnshadowedGlobal(node, state, "exports")) {
values.set(node, exportsIdentity);
} else if (isCommonJsModuleExports(node, state)) {
values.set(node, publicIdentity);
} else if (
t.isObjectExpression(node) ||
t.isArrayExpression(node) ||
t.isFunctionExpression(node) ||
t.isArrowFunctionExpression(node) ||
t.isClassExpression(node)
) {
values.set(node, node);
fresh.add(node);
} else if (t.isSequenceExpression(node)) {
const last = node.expressions.at(-1);
values.set(node, last === undefined ? null : valueOf(last));
}
const descriptor = commonJsDefinedExport(node, state);
if (descriptor !== undefined) {
const receiver = isUnshadowedGlobal(
descriptor.receiver,
state,
"exports",
)
? exportsIdentity
: publicIdentity;
const names = describedFields.get(receiver) ?? new Set<string>();
names.add(descriptor.exportedName);
describedFields.set(receiver, names);
}
const defined = descriptor ?? commonJsAssignedExports(node, state);
if (defined !== undefined && t.isCallExpression(node))
captures.set(
node,
mode !== "ordered"
? null
: isUnshadowedGlobal(defined.receiver, state, "exports")
? exportsIdentity
: publicIdentity,
);
},
exit: (node) => {
const mode = execution.pop() ?? "conditional";
if (publicCallWrites.has(node)) {
if (mode === "deferred") deferredReplacement = true;
else publicIdentity = null;
}
if (t.isAssignmentExpression(node)) {
const left = unwrapJavaScriptExpression(node.left).node;
const identity =
node.operator === "=" &&
(t.isIdentifier(left) ||
t.isMemberExpression(left) ||
t.isOptionalMemberExpression(left))
? valueOf(node.right)
: null;
for (const target of semanticWriteTargets(node.left))
write(target, identity, mode);
values.set(node, identity);
} else if (t.isVariableDeclarator(node) && node.init != null) {
for (const target of semanticWriteTargets(node.id))
write(
target,
t.isIdentifier(node.id) ? valueOf(node.init) : null,
mode,
);
} else if (t.isForOfStatement(node) || t.isForInStatement(node)) {
if (!t.isVariableDeclaration(node.left))
for (const target of semanticWriteTargets(node.left))
write(target, null, mode);
} else if (
t.isUpdateExpression(node) ||
t.isUnaryExpression(node, { operator: "delete" })
) {
if (t.isUnaryExpression(node, { operator: "delete" })) {
const name = commonJsExportName(node.argument, state);
if (
name !== undefined &&
!isCommonJsModuleExports(node.argument, state) &&
(t.isMemberExpression(node.argument) ||
t.isOptionalMemberExpression(node.argument))
) {
const receiver = isUnshadowedGlobal(
node.argument.object,
state,
"exports",
)
? exportsIdentity
: publicIdentity;
const fields = deletions.get(receiver) ?? new Map();
fields.set(name, {
node,
mode: name === "*" && mode === "ordered" ? "conditional" : mode,
});
deletions.set(receiver, fields);
}
}
write(node.argument, null, mode);
} else if (isUnshadowedGlobal(node, state, "exports")) {
values.set(node, exportsIdentity);
} else if (isCommonJsModuleExports(node, state)) {
values.set(node, publicIdentity);
} else if (
t.isObjectExpression(node) ||
t.isArrayExpression(node) ||
t.isFunctionExpression(node) ||
t.isArrowFunctionExpression(node) ||
t.isClassExpression(node)
) {
values.set(node, node);
fresh.add(node);
} else if (t.isSequenceExpression(node)) {
const last = node.expressions.at(-1);
values.set(node, last === undefined ? null : valueOf(last));
}
},
},
});
);
const effects = new Map<CommonJsExportSite, CommonJsExportReceiverEffect>();
for (const [node, receiver] of captures) {
const status: ReceiverStatus =
@@ -6,7 +6,7 @@ import {
semanticWriteTargets,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAstSteps } from "./javascriptSemanticTraversal.js";
/** Match Electron's supported module entry points. */
export const ELECTRON_MODULE =
@@ -60,6 +60,7 @@ export interface JavaScriptIntrinsicInvocation {
*/
export function* collectJavaScriptIdentityWritesSteps(
program: t.Program,
erasedSyntaxNodes: WeakSet<t.Node>,
origin: (
root: t.Node,
kind: "binding" | "member" | "opaque-call",
@@ -93,7 +94,7 @@ export function* collectJavaScriptIdentityWritesSteps(
"setPrototypeOf",
]);
const keyedMutators = new Set(["set", "deleteProperty", "defineProperty"]);
yield* traverseJavaScriptAstSteps(program, {
yield* traverseJavaScriptValueAstSteps(program, erasedSyntaxNodes, {
enter: (node) => {
if (
t.isCallExpression(node) ||
@@ -27,10 +27,8 @@ export const isTypeOnlyExportDeclaration = (
): boolean =>
node.exportKind === "type" ||
(t.isExportNamedDeclaration(node) &&
node.declaration !== null &&
(t.isTSInterfaceDeclaration(node.declaration) ||
t.isTSTypeAliasDeclaration(node.declaration) ||
t.isTSDeclareFunction(node.declaration)));
node.declaration != null &&
isErasedJavaScriptDeclaration(node.declaration));
/**
* Runtime external-module target for a value import-equals declaration;
+106 -94
View File
@@ -17,7 +17,7 @@ import {
STATIC_ROUTE_CALL_NAMES,
stringValue,
} from "./javascriptStaticAnalysisHelpers.js";
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAstSteps } from "./javascriptSemanticTraversal.js";
import {
semanticStableInitializer,
type JavaScriptBindingIndex,
@@ -312,27 +312,33 @@ export class JavaScriptRouteScan {
return;
if (this.#queue(node)) requests.push({ node, site });
};
yield* traverseJavaScriptAstSteps(root, {
skip: () => this.#result.capacity !== null,
enter: (node) => {
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
const parts = calleeName(node.callee).split(".");
if (navigationRecognition(parts.at(-1)!, parts.at(-2) ?? "") !== true)
return;
const first = node.arguments[0];
if (
first !== undefined &&
!t.isSpreadElement(first) &&
!t.isArgumentPlaceholder(first)
yield* traverseJavaScriptValueAstSteps(
root,
this.index.bindings.erasedSyntaxNodes,
{
skip: () => this.#result.capacity !== null,
enter: (node) => {
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
const parts = calleeName(node.callee).split(".");
if (
navigationRecognition(parts.at(-1)!, parts.at(-2) ?? "") !== true
)
return;
const first = node.arguments[0];
if (
first !== undefined &&
!t.isSpreadElement(first) &&
!t.isArgumentPlaceholder(first)
)
remember(first, node);
} else if (
t.isObjectProperty(node) &&
semanticStaticPropertyName(node.key, node.computed) === "href"
)
remember(first, node);
} else if (
t.isObjectProperty(node) &&
semanticStaticPropertyName(node.key, node.computed) === "href"
)
remember(node.value, node);
remember(node.value, node);
},
},
});
);
if (requests.length === 0 || this.#result.capacity !== null) return;
yield* this.index.indexMemberMutationFactsSteps();
for (const { node, site } of requests) {
@@ -384,87 +390,93 @@ export class JavaScriptRouteScan {
structured: true,
reported: true,
};
yield* traverseJavaScriptAstSteps(value, {
skip: (node) =>
this.#result.capacity !== null ||
isRouteValueBoundary(node, true) ||
seen.has(node),
enter: (node, parent) => {
if (node !== value) {
if (!this.#queue(node)) return;
seen.add(node);
}
this.#dynamicValues.set(node, owner);
let overwritten = false;
if (
t.isObjectProperty(node) &&
!isObjectPrototypeSetter(node) &&
t.isObjectExpression(parent)
) {
const key = semanticStaticPropertyKey(node.key, node.computed);
if (key !== null) {
const fields = this.#fields(parent);
const effective = fields.values.get(key);
// Unknown spreads/keys may preserve earlier data. Only a known
// final slot or method proves that this reference was discarded.
overwritten =
fields.dynamicNames.has(key) ||
(effective !== undefined && effective !== node);
yield* traverseJavaScriptValueAstSteps(
value,
this.index.bindings.erasedSyntaxNodes,
{
skip: (node) =>
this.#result.capacity !== null ||
isRouteValueBoundary(node, true) ||
seen.has(node),
enter: (node, parent) => {
if (node !== value) {
if (!this.#queue(node)) return;
seen.add(node);
}
}
const nonValue =
overwritten ||
t.isTSType(node) ||
!isNavigationDataEdge(node, parent) ||
(parent !== null && nonValueReferences.has(parent));
if (nonValue) nonValueReferences.add(node);
if (
!nonValue &&
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
) {
for (const selected of this.#memberValues(node)) admit(selected);
}
if (
nonValue ||
!t.isIdentifier(node) ||
(parent !== null && !t.isReferenced(node, parent))
)
return;
const declared = this.index.bindingForReferenceAt(node);
if (declared === undefined) return;
for (const sources of [
declared.initializers,
declared.referenceInitializers,
])
for (const source of sources) if (!this.#queue(source.node)) return;
// Different reads may select different values; unresolved order
// retains its possible sources instead of becoming absence.
const binding = semanticBindingAtRead(
declared,
node,
this.index.bindings.parentsByNode,
).binding;
for (const initializers of [
binding.initializers,
binding.referenceInitializers,
])
for (const initializer of initializers) {
if (
semanticReferenceSourcesInactive(
initializer,
this.index.bindings,
this.#dynamicValues.set(node, owner);
let overwritten = false;
if (
t.isObjectProperty(node) &&
!isObjectPrototypeSetter(node) &&
t.isObjectExpression(parent)
) {
const key = semanticStaticPropertyKey(node.key, node.computed);
if (key !== null) {
const fields = this.#fields(parent);
const effective = fields.values.get(key);
// Unknown spreads/keys may preserve earlier data. Only a known
// final slot or method proves that this reference was discarded.
overwritten =
fields.dynamicNames.has(key) ||
(effective !== undefined && effective !== node);
}
}
const nonValue =
overwritten ||
t.isTSType(node) ||
!isNavigationDataEdge(node, parent) ||
(parent !== null && nonValueReferences.has(parent));
if (nonValue) nonValueReferences.add(node);
if (
!nonValue &&
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
) {
for (const selected of this.#memberValues(node)) admit(selected);
}
if (
nonValue ||
!t.isIdentifier(node) ||
(parent !== null && !t.isReferenced(node, parent))
)
return;
const declared = this.index.bindingForReferenceAt(node);
if (declared === undefined) return;
for (const sources of [
declared.initializers,
declared.referenceInitializers,
])
for (const source of sources)
if (!this.#queue(source.node)) return;
// Different reads may select different values; unresolved order
// retains its possible sources instead of becoming absence.
const binding = semanticBindingAtRead(
declared,
node,
this.index.bindings.parentsByNode,
).binding;
for (const initializers of [
binding.initializers,
binding.referenceInitializers,
])
for (const initializer of initializers) {
if (
semanticReferenceSourcesInactive(
initializer,
this.index.bindings,
)
)
)
continue;
for (const selected of this.#initializerValues(initializer))
admit(selected);
}
continue;
for (const selected of this.#initializerValues(initializer))
admit(selected);
}
},
},
});
);
}
}
inspect(node: t.Node, parent: t.Node | null): void {
if (this.index.bindings.erasedSyntaxNodes.has(node)) return;
const expressionParent =
parent === null ? null : (this.#expressionParents.get(parent) ?? parent);
if (
@@ -600,6 +600,7 @@ export function* indexJavaScriptAmbientWritesSteps(
});
yield* collectJavaScriptIdentityWritesSteps(
program,
state.erasedSyntaxNodes,
(root, kind, writeSite) => {
if (kind === "opaque-call") return [];
observeWriteSite?.(writeSite);
@@ -20,10 +20,29 @@ it("uses parser roles for TS, import, and meta names while retaining computed re
.map(({ name }) => name),
).toEqual(["field", "field"]);
expect(ir.references.filter(({ name }) => name === "computed")).toHaveLength(
3,
2,
);
});
it("distinguishes erased type queries from evaluated typeof expressions", () => {
const ir = analyzeJavaScriptSemantics(`
const marker = 1;
type Shape = typeof marker;
function describe() { type Local = typeof marker; }
const kind = typeof marker;
`);
expect(
ir.references
.filter(({ name }) => name === "marker")
.map(({ role, resolution, location }) => [
role,
resolution,
location.start.line,
]),
).toEqual([["read", "resolved", 5]]);
expect(ir.closureCaptures).toEqual([]);
});
it("keeps destructuring writes, defaults and closure captures under one role owner", () => {
const ir = analyzeJavaScriptSemantics(`
let value=0; const source={value:1}; const fallback=2;
@@ -926,3 +926,32 @@ describe("results of methods on an escaped receiver", () => {
});
}, 30000);
});
it.each([
["named", "@dec abstract slot: { mode: string };"],
["computed literal", '@dec abstract ["slot"]: { mode: string };'],
])(
"retains the possible inherited alias through a decorated abstract %s field",
(_name, field) => {
// An emit that erases the field lets this write reach Base.slot. The
// analysis must still invalidate shared even if another emit shadows it.
const setup = `
function dec(...args: unknown[]) {}
const shared = { mode: "initial" };
class Base { slot = shared; }
abstract class Derived extends Base { ${field} }
const Constructor = Derived as unknown as new () => Base;
`;
expect(resultValue(`${setup} return shared.mode;`)).toEqual({
status: "literal",
value: "initial",
});
expect(
resultValue(`
${setup}
new Constructor().slot.mode = "updated";
return shared.mode;
`),
).toMatchObject({ status: "unknown" });
},
);
@@ -325,12 +325,8 @@ describe("JavaScript semantic analysis: class member keys", () => {
marker(value?: number) {}
}
`);
// Only the computed abstract key evaluates the outer binding.
expect(
ir.references
.filter(({ name }) => name === "marker")
.map(({ role, location }) => `${role}@${location.start.line}`),
).toEqual(["read@6"]);
// Abstract signatures, including computed keys, are erased.
expect(ir.references.filter(({ name }) => name === "marker")).toEqual([]);
});
it("does not capture an outer binding through a noncomputed method name", () => {
@@ -58,7 +58,9 @@ import {
} from "./javascriptSemanticState.js";
import {
completeSemanticSteps,
isErasedJavaScriptValueChild,
traverseJavaScriptAstSteps,
traverseJavaScriptValueAstSteps,
} from "./javascriptSemanticTraversal.js";
import {
semanticRequireOrigin,
@@ -690,7 +692,7 @@ function* classifyDefinitionReceiversSteps(
);
if (aliasFact !== undefined) facts.set(node.start ?? -1, aliasFact);
};
yield* traverseJavaScriptAstSteps(program, {
yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, {
enter: (node) => {
classifyRpcRoot(node);
classifyOpenReceiver(node);
@@ -897,6 +899,7 @@ export function* indexJavaScriptElectronIdentitySteps(
};
yield* collectJavaScriptIdentityWritesSteps(
program,
state.erasedSyntaxNodes,
(node, kind) => {
if (kind === "opaque-call") return [interop.opaqueMutationPath(node)];
const { root, members } = electronMemberPath(node);
@@ -922,7 +925,7 @@ export function* indexJavaScriptElectronIdentitySteps(
const intactExport = (path: readonly (string | null)[]): boolean =>
ELECTRON_MODULE.test(path[0] ?? "") &&
!writes.covers(["electron", ...path.slice(1)]);
yield* traverseJavaScriptAstSteps(program, {
yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, {
enter: (node) => {
if (
!t.isCallExpression(node) &&
@@ -1052,6 +1055,7 @@ const createState = (
getterResourceLimits: new Set(),
returnedReferenceWidenings: new Set(),
parentsByNode: new WeakMap(),
erasedSyntaxNodes: new WeakSet(),
scopes: [root],
scopesById: new Map([[root.scopeId, root]]),
scopeByNode: new WeakMap([[program, root]]),
@@ -1124,6 +1128,13 @@ function* collectDefinitionsSteps(
yield* traverseJavaScriptAstSteps(program, {
enter: (node, parent, readAncestors) => {
if (parent !== null) state.parentsByNode.set(node, parent);
if (
(parent !== null && state.erasedSyntaxNodes.has(parent)) ||
isErasedJavaScriptValueChild(node, parent)
) {
state.erasedSyntaxNodes.add(node);
return;
}
if (
t.isWithStatement(node) ||
t.isForInStatement(node) ||
@@ -1438,7 +1449,7 @@ function* indexPrototypeUncertaintySteps(
"defineProperties",
"setPrototypeOf",
]);
yield* traverseJavaScriptAstSteps(program, {
yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, {
enter: (node, parent) => {
const write = semanticWriteSiteTarget(node);
if (write !== null) {
@@ -15,7 +15,7 @@ import {
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import {
@@ -100,7 +100,7 @@ export const collectJavaScriptSemanticAsyncEffects = (
const callableIds = new Set(callables.map(({ callableId }) => callableId));
const callableStack = createSemanticCallableIdStack(callableIds);
const ancestors: t.Node[] = [];
traverseJavaScriptAst(program, {
traverseJavaScriptValueAst(program, state.erasedSyntaxNodes, {
enter: (node, parent) => {
callableStack.enter(node, parent);
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
@@ -26,7 +26,7 @@ import {
parameterBindings,
resolveLocalCallables,
} from "./javascriptSemanticCallResolution.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import { semanticCallableResultKind } from "./javascriptSemanticCallableResults.js";
import { assignedSemanticResultBindings } from "./javascriptSemanticDataEffectHelpers.js";
@@ -93,7 +93,7 @@ export const collectJavaScriptSemanticCalls = (
const callableStack = createSemanticCallableStack((callableId) =>
callableById.get(callableId),
);
traverseJavaScriptAst(program, {
traverseJavaScriptValueAst(program, state.erasedSyntaxNodes, {
enter: (node, parent) => {
callableStack.enter(node, parent);
const owner = enclosingFunction(callableStack.callables);
@@ -25,7 +25,7 @@ import {
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import {
@@ -94,7 +94,7 @@ const collectSpawns = (
);
const callableStack = createSemanticCallableIdStack(admittedCallables);
const ancestors: t.Node[] = [];
traverseJavaScriptAst(program, {
traverseJavaScriptValueAst(program, state.erasedSyntaxNodes, {
enter: (node, parent) => {
callableStack.enter(node, parent);
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
@@ -179,7 +179,7 @@ const collectInteractions = (
spawnByNode,
spawnsByBinding,
};
traverseJavaScriptAst(program, {
traverseJavaScriptValueAst(program, state.erasedSyntaxNodes, {
enter: (node, parent) => {
callableStack.enter(node, parent);
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
@@ -31,7 +31,7 @@ import {
type JavaScriptSemanticMemberEffect,
type JavaScriptSemanticRequiredModuleEffect,
} from "./javascriptSemanticState.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import {
readExactJavaScriptLiteral,
semanticMemberChain,
@@ -901,7 +901,7 @@ export const traverseDataEffects = (
context.callableStack,
);
context.ancestors.length = 0;
traverseJavaScriptAst(program, {
traverseJavaScriptValueAst(program, context.state.erasedSyntaxNodes, {
enter: (node, parent) => {
callableStack.enter(node, parent);
visit(node, parent);
@@ -16,7 +16,7 @@ import { collectJavaScriptSemanticResources } from "./javascriptSemanticResource
import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js";
import { createSemanticCallableIdStack } from "./javascriptSemanticCallableStack.js";
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAstSteps } from "./javascriptSemanticTraversal.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
@@ -94,6 +94,7 @@ export function* collectJavaScriptDerivedSemanticsSteps(
yield;
const assignmentFlows = yield* collectAssignmentFlowsSteps(
program,
state,
callables,
);
const functionFingerprints =
@@ -126,13 +127,14 @@ export function* collectJavaScriptDerivedSemanticsSteps(
/** Keep assignment syntax in the IR so graph consumers never reparse source. */
function* collectAssignmentFlowsSteps(
program: t.Program,
state: JavaScriptSemanticAnalysisState,
callables: readonly JavaScriptSemanticCallable[],
): Generator<void, JavaScriptSemanticIr["assignmentFlows"]> {
const flows: JavaScriptSemanticIr["assignmentFlows"][number][] = [];
const stack = createSemanticCallableIdStack(
new Set(callables.map((item) => item.callableId)),
);
yield* traverseJavaScriptAstSteps(program, {
yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, {
enter: (node, parent) => {
stack.enter(node, parent);
if (!t.isAssignmentExpression(node)) return;
@@ -80,7 +80,7 @@ import {
} from "./javascriptSemanticPropertyPaths.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import {
traverseJavaScriptAstSteps,
traverseJavaScriptValueAstSteps,
childNodes,
} from "./javascriptSemanticTraversal.js";
import {
@@ -1482,7 +1482,7 @@ export function* collectSemanticMemberMutationsSteps(
);
};
const writeOnlyMembers = new WeakSet<t.Node>();
yield* traverseJavaScriptAstSteps(program, {
yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, {
enter: (node, parent, readAncestors) => {
const writeTarget = semanticWriteSiteTarget(node);
if (writeTarget !== null && !semanticWriteSiteReadsFirst(node))
@@ -16,7 +16,7 @@ import {
resolveSemanticBindingState,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
/** Recover bounded static property reads, writes, spreads, and destructuring. */
@@ -33,7 +33,7 @@ export const collectJavaScriptSemanticObjects = (
output,
writeTargets: new Map(),
};
traverseJavaScriptAst(program, {
traverseJavaScriptValueAst(program, state.erasedSyntaxNodes, {
enter: (node, parent) => {
callableStack.enter(node, parent);
const ownerCallableId = callableStack.callables.at(-1) ?? null;
@@ -16,7 +16,7 @@ import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import {
completeSemanticSteps,
traverseJavaScriptAstSteps,
traverseJavaScriptValueAstSteps,
} from "./javascriptSemanticTraversal.js";
import {
evaluateSemanticBinding,
@@ -32,6 +32,7 @@ import {
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import {
isErasedJavaScriptDeclaration,
isTypeOnlyExportDeclaration,
isTypeOnlyExportSpecifier,
isTypeOnlyImportDeclaration,
@@ -97,6 +98,16 @@ export const collectSemanticModuleLink = (
CommonJsExportReceiverEffect
>,
): void => {
// Import/export declarations also describe erased type links. Their nested
// expressions do not establish runtime require or CommonJS export links.
if (
state.erasedSyntaxNodes.has(node) &&
!t.isTSImportEqualsDeclaration(node) &&
!t.isImportDeclaration(node) &&
!t.isExportNamedDeclaration(node) &&
!t.isExportAllDeclaration(node)
)
return;
if (t.isTSImportEqualsDeclaration(node)) collectImportEquals(node, state);
else if (t.isImportDeclaration(node)) collectImports(node, state);
else if (t.isExportAllDeclaration(node))
@@ -117,7 +128,9 @@ export const collectSemanticModuleLink = (
addModuleLink(
state,
{
kind: "export",
kind: isErasedJavaScriptDeclaration(node.declaration)
? "type-import"
: "export",
specifier: null,
importedName: null,
localName: defaultExportLocalName(node.declaration),
@@ -179,7 +192,7 @@ export function* collectSemanticReferencesSteps(
): Generator<void, JavaScriptSemanticReference[]> {
const output: JavaScriptSemanticReference[] = [];
const seen = new Set<string>();
yield* traverseJavaScriptAstSteps(program, {
yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, {
enter: (node, parent, readAncestors) => {
if (
!t.isIdentifier(node) ||
@@ -16,7 +16,7 @@ import {
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
@@ -111,7 +111,7 @@ const collectCandidates = (
);
const callableStack = createSemanticCallableIdStack(admittedCallables);
const ancestors: t.Node[] = [];
traverseJavaScriptAst(program, {
traverseJavaScriptValueAst(program, state.erasedSyntaxNodes, {
enter: (node, parent) => {
callableStack.enter(node, parent);
const details = promiseCandidateDetails(node, state);
@@ -7,7 +7,7 @@ import {
type SemanticCapturedSelection,
} from "./javascriptSemanticProjectedSources.js";
import * as t from "@babel/types";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import { BUILTIN_RESOURCE_METHODS } from "./javascriptBuiltinResourceMethods.js";
import { javaScriptRuntimeContext } from "./javascriptSourceParser.js";
@@ -1404,7 +1404,7 @@ const writesModuleRequire = (
// Only a retained module identity can revoke the host delegate. Escaping
// an unrelated local table is not evidence that it became the host module.
isCommonJsModuleObject(node, state, false);
traverseJavaScriptAst(state.program, {
traverseJavaScriptValueAst(state.program, state.erasedSyntaxNodes, {
enter: (node) => {
const target = t.isAssignmentExpression(node)
? node.left
@@ -135,6 +135,8 @@ export interface JavaScriptSemanticAnalysisState {
/** Callee sites whose possible local returns needed a broader projection. */
readonly returnedReferenceWidenings: Set<t.Node>;
readonly parentsByNode: WeakMap<t.Node, t.Node>;
/** TypeScript syntax omitted from JavaScript value evaluation. */
readonly erasedSyntaxNodes: WeakSet<t.Node>;
readonly scopes: JavaScriptSemanticScopeState[];
readonly scopesById: Map<string, JavaScriptSemanticScopeState>;
/** Scope boundaries and descendant scopes resolved on demand. */
@@ -1,4 +1,49 @@
import * as t from "@babel/types";
import {
isErasedJavaScriptDeclaration,
isTypeOnlyExportDeclaration,
isTypeOnlyExportSpecifier,
isTypeOnlyImportDeclaration,
isTypeOnlyImportSpecifier,
} from "./javascriptModuleDeclaration.js";
/** Whether this child and its descendants contribute no JavaScript value. */
export const isErasedJavaScriptValueChild = (
node: t.Node,
parent: t.Node | null,
): boolean => {
if (
isErasedJavaScriptDeclaration(node) ||
t.isTSType(node) ||
t.isTSTypeElement(node) ||
t.isTSDeclareMethod(node) ||
t.isTSTypeAnnotation(node) ||
t.isTSTypeParameterDeclaration(node) ||
t.isTSTypeParameterInstantiation(node) ||
t.isTSClassImplements(node) ||
t.isTSInterfaceHeritage(node) ||
t.isTSInterfaceBody(node) ||
(t.isImportDeclaration(node) && isTypeOnlyImportDeclaration(node)) ||
((t.isExportNamedDeclaration(node) || t.isExportAllDeclaration(node)) &&
isTypeOnlyExportDeclaration(node)) ||
(t.isImportSpecifier(node) && isTypeOnlyImportSpecifier(node)) ||
(t.isExportSpecifier(node) && isTypeOnlyExportSpecifier(node)) ||
(t.isTSImportEqualsDeclaration(node) && node.importKind === "type")
)
return true;
// Undecorated declared or abstract fields erase their keys and annotations.
// Decorators and a decorated field's computed key may execute at definition.
return (
t.isClassProperty(parent) &&
(parent.declare === true || parent.abstract === true) &&
!t.isDecorator(node) &&
!(
node === parent.key &&
parent.computed === true &&
(parent.decorators?.length ?? 0) > 0
)
);
};
/**
* Visit Babel AST nodes in deterministic source-tree order, excluding skipped subtrees.
@@ -15,6 +60,30 @@ export const traverseJavaScriptAst = (
visitor: JavaScriptAstVisitor,
): void => completeSemanticSteps(traverseJavaScriptAstSteps(root, visitor));
/** Visit only syntax retained as JavaScript values after TypeScript erasure. */
export const traverseJavaScriptValueAst = (
root: t.Node,
erasedSyntaxNodes: WeakSet<t.Node>,
visitor: JavaScriptAstVisitor,
): void =>
completeSemanticSteps(
traverseJavaScriptValueAstSteps(root, erasedSyntaxNodes, visitor),
);
/** Cooperative value-only view of the same deterministic AST traversal. */
export function* traverseJavaScriptValueAstSteps(
root: t.Node,
erasedSyntaxNodes: WeakSet<t.Node>,
visitor: JavaScriptAstVisitor,
): Generator<void, void> {
if (erasedSyntaxNodes.has(root)) return;
yield* traverseJavaScriptAstSteps(root, {
...visitor,
skip: (node) =>
erasedSyntaxNodes.has(node) || visitor.skip?.(node) === true,
});
}
/** Callbacks for one deterministic AST traversal. */
export interface JavaScriptAstVisitor {
/** Omit a child and its subtree without entering or exiting it. */
@@ -113,7 +113,10 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
isElectronRendererLoad(callee, site, findings, index),
source,
accumulator,
vitePreloadDependencyTable: indexVitePreloadDependencyTable(file.program),
vitePreloadDependencyTable: indexVitePreloadDependencyTable(
file.program,
bindings.erasedSyntaxNodes,
),
openReceiverFacts: receivers.openReceivers,
electronBindings: electronIdentity.bindings,
electronExportIntact: electronIdentity.intactExport,
@@ -167,7 +170,8 @@ function* traverseStaticSourceSteps(
enter: (node, parent) => {
findings.accumulator.visitedNodes += 1;
routeScan.inspect(node, parent);
inspectNode(node, findings, bindings);
if (!bindings.erasedSyntaxNodes.has(node))
inspectNode(node, findings, bindings);
return undefined;
},
});
@@ -1,6 +1,7 @@
import * as t from "@babel/types";
import { isIntrinsicMapLookup } from "./javascriptSemanticMapOperations.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import { semanticIdentifierRole } from "./javascriptSemanticProjection.js";
import { htmlUrlText, stripQueryAndFragment } from "../artifactPathSyntax.js";
import { sanitizeEndpointCandidate } from "../browserObservation.js";
@@ -397,9 +398,10 @@ type ViteMapDepsFunction =
/** Index the canonical Vite helper's cache initializer from parsed syntax. */
export const indexVitePreloadDependencyTable = (
program: t.Program,
erasedSyntaxNodes: WeakSet<t.Node>,
): readonly (string | undefined)[] | null => {
const helpers: ViteMapDepsFunction[] = [];
traverseJavaScriptAst(program, {
traverseJavaScriptValueAst(program, erasedSyntaxNodes, {
enter: (node) => {
if (t.isFunctionDeclaration(node) && node.id?.name === "__vite__mapDeps")
helpers.push(node);
@@ -422,7 +424,7 @@ export const indexVitePreloadDependencyTable = (
: mapParameter;
if (!t.isIdentifier(mapIdentifier)) return [];
const tables: t.ArrayExpression[] = [];
traverseJavaScriptAst(helper, {
traverseJavaScriptValueAst(helper, erasedSyntaxNodes, {
skip: (node) => t.isFunction(node),
enter: (node) => {
if (
@@ -454,17 +456,13 @@ export const indexVitePreloadDependencyTable = (
: stringValue(element),
);
let escapedOrMutated = false;
traverseJavaScriptAst(program, {
traverseJavaScriptValueAst(program, erasedSyntaxNodes, {
skip: (node) => node === helper,
enter: (node, parent) => {
if (!t.isIdentifier(node, { name: "__vite__mapDeps" })) return;
enter: (node, parent, readAncestors) => {
if (
(parent === helper &&
t.isFunctionDeclaration(parent) &&
parent.id === node) ||
(t.isVariableDeclarator(parent) &&
parent.init === helper &&
parent.id === node)
!t.isIdentifier(node, { name: "__vite__mapDeps" }) ||
parent === null ||
semanticIdentifierRole(node, parent, readAncestors) === null
)
return;
if (
@@ -0,0 +1,64 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
it("keeps declared exports as source type links without runtime values", () => {
const ir = analyzeJavaScriptSemantics(`
export declare const token: number;
export declare class Shape {}
export default interface DefaultShape {}
`);
expect(ir.coverage.status).not.toBe("failed");
expect(ir.moduleLinks).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "type-import",
localName: "token",
exportedName: "token",
}),
expect.objectContaining({
kind: "type-import",
localName: "Shape",
exportedName: "Shape",
}),
expect.objectContaining({
kind: "type-import",
exportedName: "default",
}),
]),
);
expect(ir.moduleLinks.filter(({ kind }) => kind === "export")).toEqual([]);
});
it("retains computed keys and decorators only when declared or abstract fields are decorated", () => {
const ir = analyzeJavaScriptSemantics(`
const key = Symbol();
function dec(...args: unknown[]) {}
class Declared {
declare [key]: string;
}
abstract class AbstractShape {
abstract [key]: string;
}
class DecoratedDeclared {
@dec declare [key]: string;
}
abstract class DecoratedAbstract {
@dec abstract [key]: string;
}
`);
expect(ir.coverage.status).not.toBe("failed");
expect(ir.references.filter(({ name }) => name === "key")).toHaveLength(2);
expect(ir.references.filter(({ name }) => name === "dec")).toHaveLength(2);
});
it("does not promote a type literal to a route candidate beside a runtime route", () => {
const analysis = analyzeJavaScriptStaticSource(`
createBrowserRouter([{ path: "/admin", Component: Admin }]);
type Ghost = "/admin/ghost";
`);
expect(analysis.routes?.candidates.map(({ path }) => path)).toEqual([
"/admin",
]);
});
@@ -84,6 +84,29 @@ describe("Vite preload dependency recovery", () => {
).toEqual(["./original.js"]);
});
it.each([
["type DependencyHelper = typeof __vite__mapDeps;", "./original.js"],
["const labels = { __vite__mapDeps() {} };", "./original.js"],
["const labels = { [__vite__mapDeps]: true };", null],
["const dependencyTable = __vite__mapDeps.f;", null],
])(
"distinguishes an erased type query from a runtime helper escape: %s",
(read, expected) => {
const analysis = analyzeJavaScriptStaticSource(`
const __vite__mapDeps = (indexes, map = __vite__mapDeps, dependencies =
(map.f || (map.f = ["./original.js"]))) =>
indexes.map((index) => dependencies[index]);
${read}
__vite__mapDeps([0]);
`);
expect(
analysis.references
.filter(({ kind }) => kind === "dynamic-import")
.map(({ specifier }) => specifier),
).toEqual([expected]);
},
);
it("keeps table slots from a spread onward unknown", () => {
const analysis = analyzeJavaScriptStaticSource(`
const __vite__mapDeps = (indexes, map = __vite__mapDeps, dependencies =
@@ -0,0 +1,77 @@
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
for (const surface of ["CLI", "stdio MCP"] as const)
cliTest(
`excludes erased TypeScript reads and handler captures through ${surface}`,
async ({ cli }) => {
const root = await createTestTempDirectory("rea-typescript-erasure-");
await writeFixtureFiles(root, {
"main.cts": [
'import type { Shape } from "./shape";',
'const { contextBridge, ipcRenderer } = require("electron");',
"const marker = 1;",
"type Description = typeof marker;",
"function describe() { type Local = typeof marker; }",
"const kind = typeof marker;",
'const CHANNEL = "ready";',
"function register(value: Shape) {",
" const handler = () => {",
" type Local = typeof value;",
" interface Method { value(): void; }",
" };",
" ipcRenderer.on(CHANNEL, handler);",
"}",
'contextBridge.exposeInMainWorld("api", { on: () => register("unused") });',
].join("\n"),
"shape.d.ts": "export type Shape = string;",
});
const result = await analyzeJavaScriptDirectory(surface, cli, root);
expect(result.statistics).toMatchObject({
parsed_javascript_files: 2,
parse_failures: 0,
});
const graph = result.semantic_graph;
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
const markerReads = graph.relations.filter(
(relation) =>
relation.relation === "reads" &&
nodes.get(relation.target_node_id)?.label === "marker",
);
expect(markerReads).toHaveLength(1);
expect(markerReads[0]?.evidence.location).toMatchObject({
available: true,
value: { kind: "source-range", source: "main.cts", start: { line: 6 } },
});
expect(
graph.relations.filter(
(relation) =>
relation.relation === "captures" &&
nodes.get(relation.source_node_id)?.label === "value",
),
).toEqual([]);
const channels = result.graph.nodes
.filter((node) => node.kind === "ipc-channel")
.flatMap((node) => node.observations);
expect(channels).toHaveLength(1);
expect(channels[0]?.properties).toMatchObject({
channel: "ready",
resolution: "literal",
});
expect(
result.graph.nodes.flatMap((node) => node.observations),
).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
semantic_role: "source-module",
path: "main.cts",
type_relationships: 1,
}),
}),
);
},
);