mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
fix(javascript): prove namespace interop before attributing Electron APIs (#1857)
* fix(javascript): prove Electron module namespace conversion * refactor(javascript): share AST child field metadata Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(javascript): retain destructured intrinsic writes Share one pattern-target walker across lexical definitions, member mutation tracking and Electron identity writes. Keep overwritten globals unresolved without treating property keys or shadowed locals as global replacements. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(javascript): preserve intrinsic projections and fresh target identity Resolve known mutators through static binding projections and prove stable identifier chains to fresh literals before widening unknown mutation targets. Keep reassigned, uninitialized and member-projected targets conservative. Cover both review regressions through source tests and inert CLI/MCP fixtures. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(javascript): retain fresh function and class target identity Recognize stable local function, undecorated class and RegExp targets before widening intrinsic mutations. Keep decorated classes opaque because their binding value can be replaced, and preserve class-initialization writes. Cover fresh targets and replacement decorators in inert CLI/MCP fixtures. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(javascript): preserve intrinsic alias capture timing Validate each alias read after initialization and retain the original function capture across copies. Share declaration initializer facts with RPC and Electron analysis, including class declarations used for fresh-target identity. Accept single-statement control blocks while keeping lexical roles and added operations checked. Cover the capture regressions through inert source and public CLI/MCP workflows. * fix(javascript): respect hoisted fresh function declarations Function declarations provide fresh targets before their textual position. Retain initialization-order checks for variable and class values, including aliases. Cover the reviewed regression with source controls and the public CLI/MCP fixture. * fix(javascript): preserve proven interop result projections * fix(javascript): retain stable declarations and bound intrinsic writes Keep repeated variable declarations with one unconditional initializer while rejecting assignments, conditional initialization and multiple values. Normalize known intrinsic calls, aliases and bound argument prefixes at the semantic owner, retaining opaque targets without tainting unused bindings or fresh local objects. Cover both regressions through source and CLI/MCP tests. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(javascript): retain bound intrinsic mutation effects * fix(javascript): preserve bound receivers for intrinsic invocation * fix(javascript): prove generic binding and invocation effects * fix(javascript): retain Reflect set receiver effects --------- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,774 @@
|
||||
import { parseExpression } from "@babel/parser";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
isUnshadowedGlobal,
|
||||
resolveSemanticBindingState,
|
||||
semanticScopeId,
|
||||
semanticStableInitializer,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
|
||||
import {
|
||||
childNodes,
|
||||
javaScriptAstChildKeys,
|
||||
} from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
electronMemberPath,
|
||||
type JavaScriptIntrinsicInvocation,
|
||||
} from "./javascriptElectronMemberWrites.js";
|
||||
import { resolveJavaScriptIntrinsicInvocation } from "./javascriptIntrinsicInvocation.js";
|
||||
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
// Match the complete Rolldown runtime operations, including its live getters.
|
||||
// Names and formatting are irrelevant; lexical roles, operators and keys are not.
|
||||
const TO_ESM = parseExpression(`(mod, isNodeMode, target) => (
|
||||
target = mod != null ? __create(__getProtoOf(mod)) : {},
|
||||
__copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default")
|
||||
? __defProp(target, "default", {value: mod, enumerable: true}) : target, mod)
|
||||
)`);
|
||||
const COPY_PROPS = parseExpression(`(to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function")
|
||||
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
||||
key = keys[i];
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, {
|
||||
get: ((k) => from[k]).bind(null, key),
|
||||
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
||||
});
|
||||
}
|
||||
return to;
|
||||
}`);
|
||||
const INTRINSICS: Readonly<Record<string, readonly string[]>> = {
|
||||
__create: ["Object", "create"],
|
||||
__getProtoOf: ["Object", "getPrototypeOf"],
|
||||
__defProp: ["Object", "defineProperty"],
|
||||
__getOwnPropNames: ["Object", "getOwnPropertyNames"],
|
||||
__getOwnPropDesc: ["Object", "getOwnPropertyDescriptor"],
|
||||
__hasOwnProp: ["Object", "prototype", "hasOwnProperty"],
|
||||
};
|
||||
|
||||
interface IntrinsicDependency {
|
||||
readonly path: readonly string[];
|
||||
readonly capturedAt?: t.Node;
|
||||
}
|
||||
|
||||
interface InteropOrigin {
|
||||
readonly origin: JavaScriptModuleOrigin;
|
||||
readonly dependencies: readonly IntrinsicDependency[];
|
||||
readonly defaultIsModule: boolean;
|
||||
}
|
||||
|
||||
interface InteropHelper {
|
||||
readonly dependencies: readonly IntrinsicDependency[];
|
||||
readonly readyAfter: number;
|
||||
readonly identity: boolean;
|
||||
}
|
||||
|
||||
/** Prove one inert namespace conversion without relaxing general assignment semantics. */
|
||||
export const createSemanticModuleInterop = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
) => {
|
||||
const helpers = new Map<string, InteropHelper | null>();
|
||||
const origins = new Map<string, InteropOrigin>();
|
||||
const calls = new WeakMap<t.Node, InteropOrigin>();
|
||||
const intrinsicBindings = new Map<string, readonly string[] | null>();
|
||||
const writes: {
|
||||
readonly scope: string;
|
||||
readonly offset: number;
|
||||
readonly coverage: SemanticPropertyPathCoverage;
|
||||
}[] = [];
|
||||
const invalidatedBindings = new Map<string, boolean>();
|
||||
const invalidatedCalls = new WeakMap<t.Node, boolean>();
|
||||
const opaqueMutationPath = (node: t.Node): readonly string[] => {
|
||||
let current: t.Node | undefined = node;
|
||||
while (current !== undefined) {
|
||||
if (t.isFunction(current))
|
||||
return ["opaque-intrinsic", semanticScopeId("function", current)];
|
||||
current = state.parentsByNode.get(current);
|
||||
}
|
||||
return ["intrinsic"];
|
||||
};
|
||||
|
||||
const intrinsicPath = (node: t.Node): readonly string[] | undefined => {
|
||||
let current = node;
|
||||
const visited = new Set<string>();
|
||||
const pending: { id: string; suffixLength: number }[] = [];
|
||||
let suffix: readonly string[] = [];
|
||||
const finish = (
|
||||
path: readonly string[] | undefined,
|
||||
): readonly string[] | undefined => {
|
||||
for (const binding of pending)
|
||||
intrinsicBindings.set(
|
||||
binding.id,
|
||||
path === undefined
|
||||
? null
|
||||
: path.slice(0, path.length - binding.suffixLength),
|
||||
);
|
||||
return path;
|
||||
};
|
||||
for (;;) {
|
||||
const { root, members } = electronMemberPath(current);
|
||||
if (members.some((member) => member === null)) return finish(undefined);
|
||||
suffix = [...(members as readonly string[]), ...suffix];
|
||||
for (const global of ["Object", "Function", "Reflect"])
|
||||
if (isUnshadowedGlobal(root, state, global))
|
||||
return finish(["intrinsic", global, ...suffix]);
|
||||
for (const global of ["globalThis", "global", "window", "self"])
|
||||
if (isUnshadowedGlobal(root, state, global))
|
||||
return finish(["intrinsic", ...suffix]);
|
||||
if (!t.isIdentifier(root)) return finish(undefined);
|
||||
const binding = resolveSemanticBindingState(state, root, root.name);
|
||||
const cached =
|
||||
binding === undefined
|
||||
? undefined
|
||||
: intrinsicBindings.get(binding.bindingId);
|
||||
if (cached !== undefined)
|
||||
return finish(cached === null ? undefined : [...cached, ...suffix]);
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (
|
||||
binding === undefined ||
|
||||
initializer === undefined ||
|
||||
initializer.projection.some((segment) => segment === null) ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return finish(undefined);
|
||||
visited.add(binding.bindingId);
|
||||
pending.push({ id: binding.bindingId, suffixLength: suffix.length });
|
||||
suffix = [...initializer.projection.map(String), ...suffix];
|
||||
current = initializer.node;
|
||||
}
|
||||
};
|
||||
|
||||
// A nominal alias path is sufficient for conservative mutation lookup, but
|
||||
// helper proof also needs each read to follow initialization. An alias copy
|
||||
// retains the earlier function capture; a member read captures it anew.
|
||||
const capturedIntrinsic = (node: t.Node): IntrinsicDependency | undefined => {
|
||||
const path = intrinsicPath(node);
|
||||
if (path === undefined) return undefined;
|
||||
const visited = new Set<string>();
|
||||
let current = node;
|
||||
let capturedAt = node;
|
||||
let suffixLength = 0;
|
||||
for (;;) {
|
||||
const { root, members } = electronMemberPath(current);
|
||||
suffixLength += members.length;
|
||||
if (!t.isIdentifier(root)) return undefined;
|
||||
const binding = resolveSemanticBindingState(state, root, root.name);
|
||||
if (binding === undefined) return { path, capturedAt };
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (
|
||||
initializer === undefined ||
|
||||
(initializer.node.end ?? Infinity) >= (root.start ?? -1) ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return undefined;
|
||||
visited.add(binding.bindingId);
|
||||
if (suffixLength === 0) capturedAt = initializer.node;
|
||||
suffixLength += initializer.projection.length;
|
||||
current = initializer.node;
|
||||
}
|
||||
};
|
||||
|
||||
const staticString = (node: t.Node | undefined): string | undefined => {
|
||||
if (node === undefined) return undefined;
|
||||
const visited = new Set<string>();
|
||||
let current = node;
|
||||
for (;;) {
|
||||
const literal = readExactJavaScriptLiteral(current);
|
||||
if (literal.found)
|
||||
return typeof literal.value === "string" ? literal.value : undefined;
|
||||
const expression = unwrapJavaScriptExpression(current).node;
|
||||
if (!t.isIdentifier(expression)) return undefined;
|
||||
const binding = resolveSemanticBindingState(
|
||||
state,
|
||||
expression,
|
||||
expression.name,
|
||||
);
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (
|
||||
binding === undefined ||
|
||||
initializer === undefined ||
|
||||
initializer.projection.length !== 0 ||
|
||||
(initializer.node.end ?? Infinity) >= (expression.start ?? -1) ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return undefined;
|
||||
visited.add(binding.bindingId);
|
||||
current = initializer.node;
|
||||
}
|
||||
};
|
||||
|
||||
const intrinsicReadChanged = (
|
||||
node: t.Node,
|
||||
path: readonly string[],
|
||||
): boolean => {
|
||||
const capture = capturedIntrinsic(node)?.capturedAt ?? node;
|
||||
const scope = opaqueMutationPath(capture).join(".");
|
||||
const method = path.at(-1);
|
||||
const invocationMethod =
|
||||
method === "bind" || method === "call" || method === "apply";
|
||||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < (capture.start ?? -1) &&
|
||||
(write.coverage.covers(path) ||
|
||||
(invocationMethod &&
|
||||
write.coverage.covers([
|
||||
"intrinsic",
|
||||
"Function",
|
||||
"prototype",
|
||||
method,
|
||||
]))),
|
||||
);
|
||||
};
|
||||
|
||||
const resolveBoundIntrinsic = (
|
||||
node: t.Node,
|
||||
): JavaScriptIntrinsicInvocation | undefined => {
|
||||
const visited = new Set<string>();
|
||||
const argumentGroups: t.CallExpression["arguments"][] = [];
|
||||
const bindReads: t.Node[] = [];
|
||||
let opaqueArguments = false;
|
||||
let receiver: t.Node | undefined;
|
||||
let current = node;
|
||||
for (;;) {
|
||||
current = unwrapJavaScriptExpression(current).node;
|
||||
if (t.isIdentifier(current)) {
|
||||
const binding = resolveSemanticBindingState(
|
||||
state,
|
||||
current,
|
||||
current.name,
|
||||
);
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (
|
||||
binding === undefined ||
|
||||
initializer === undefined ||
|
||||
initializer.projection.length !== 0 ||
|
||||
(initializer.node.end ?? Infinity) >= (current.start ?? -1) ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return undefined;
|
||||
visited.add(binding.bindingId);
|
||||
current = initializer.node;
|
||||
continue;
|
||||
}
|
||||
if (!t.isCallExpression(current)) return undefined;
|
||||
const { root, members } = electronMemberPath(current.callee);
|
||||
let bindTarget = root;
|
||||
let bindingArguments: JavaScriptIntrinsicInvocation["arguments"] =
|
||||
current.arguments;
|
||||
let captured = capturedIntrinsic(current.callee);
|
||||
let terminal = captured?.path.at(-1) === "bind";
|
||||
let path: readonly string[] | undefined = captured?.path.slice(0, -1);
|
||||
if (members.at(-1) === "bind") {
|
||||
// A direct .bind may continue a bound callable, without discarding members.
|
||||
if (!terminal && members.length !== 1) return undefined;
|
||||
bindReads.push(captured?.capturedAt ?? current.callee);
|
||||
} else {
|
||||
// Reuse the invocation owner for generic call/apply/Reflect.apply of bind.
|
||||
const invocation = resolveJavaScriptIntrinsicInvocation(
|
||||
current,
|
||||
intrinsicPath,
|
||||
boundIntrinsic,
|
||||
intrinsicReadChanged,
|
||||
);
|
||||
if (
|
||||
invocation?.kind !== "known" ||
|
||||
invocation.path.join(".") !== "intrinsic.Function.prototype.bind" ||
|
||||
invocation.receiver === undefined
|
||||
)
|
||||
return undefined;
|
||||
bindTarget = invocation.receiver;
|
||||
bindingArguments = invocation.arguments;
|
||||
captured = capturedIntrinsic(bindTarget);
|
||||
terminal = captured !== undefined;
|
||||
path = captured?.path;
|
||||
}
|
||||
receiver = bindingArguments?.[0];
|
||||
const ownArguments = bindingArguments?.slice(1) ?? [];
|
||||
opaqueArguments ||=
|
||||
bindingArguments === undefined || t.isSpreadElement(receiver);
|
||||
opaqueArguments ||= ownArguments.some((argument) =>
|
||||
t.isSpreadElement(argument),
|
||||
);
|
||||
argumentGroups.push(ownArguments);
|
||||
if (!terminal || path === undefined) {
|
||||
current = bindTarget;
|
||||
continue;
|
||||
}
|
||||
const boundPath = [...path, "bind"];
|
||||
const functionBind = ["intrinsic", "Function", "prototype", "bind"];
|
||||
opaqueArguments ||= bindReads.some((capture) => {
|
||||
const scope = opaqueMutationPath(capture).join(".");
|
||||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < (capture.start ?? -1) &&
|
||||
(write.coverage.covers(boundPath) ||
|
||||
write.coverage.covers(functionBind)),
|
||||
);
|
||||
});
|
||||
return {
|
||||
kind: opaqueArguments ? "opaque" : "known",
|
||||
path,
|
||||
...(receiver === undefined ? {} : { receiver }),
|
||||
// Rebinding appends arguments to the earlier bound argument list.
|
||||
arguments: opaqueArguments
|
||||
? undefined
|
||||
: argumentGroups.toReversed().flat(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const activeBoundValues = new WeakSet<t.Node>();
|
||||
const boundIntrinsic = (
|
||||
node: t.Node,
|
||||
): JavaScriptIntrinsicInvocation | undefined => {
|
||||
if (activeBoundValues.has(node)) return undefined;
|
||||
activeBoundValues.add(node);
|
||||
try {
|
||||
return resolveBoundIntrinsic(node);
|
||||
} finally {
|
||||
activeBoundValues.delete(node);
|
||||
}
|
||||
};
|
||||
|
||||
const isFreshObject = (node: t.Node): boolean => {
|
||||
const visited = new Set<string>();
|
||||
let current = node;
|
||||
for (;;) {
|
||||
current = unwrapJavaScriptExpression(current).node;
|
||||
if (
|
||||
t.isObjectExpression(current) ||
|
||||
t.isArrayExpression(current) ||
|
||||
t.isFunction(current) ||
|
||||
(t.isClass(current) && !current.decorators?.length) ||
|
||||
t.isRegExpLiteral(current)
|
||||
)
|
||||
return true;
|
||||
if (!t.isIdentifier(current)) return false;
|
||||
const binding = resolveSemanticBindingState(state, current, current.name);
|
||||
const initializer = unprojectedInitializer(binding, state);
|
||||
if (
|
||||
binding === undefined ||
|
||||
initializer === undefined ||
|
||||
(!t.isFunctionDeclaration(initializer) &&
|
||||
(initializer.end ?? Infinity) >= (current.start ?? -1)) ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return false;
|
||||
visited.add(binding.bindingId);
|
||||
current = initializer;
|
||||
}
|
||||
};
|
||||
|
||||
const helperProof = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
): InteropHelper | undefined => {
|
||||
const cached = helpers.get(binding.bindingId);
|
||||
if (cached !== undefined) return cached ?? undefined;
|
||||
helpers.set(binding.bindingId, null);
|
||||
if (state.scopesById.get(binding.scopeId)?.kind !== "program")
|
||||
return undefined;
|
||||
const wrapper = unprojectedInitializer(binding, state);
|
||||
if (wrapper === undefined) return undefined;
|
||||
if (identityFunction(wrapper, state)) {
|
||||
const proof = {
|
||||
dependencies: [],
|
||||
readyAfter: t.isFunctionDeclaration(wrapper)
|
||||
? -1
|
||||
: (wrapper.end ?? Infinity),
|
||||
identity: true,
|
||||
};
|
||||
helpers.set(binding.bindingId, proof);
|
||||
return proof;
|
||||
}
|
||||
const wrapperRoles = canonicalRoles(wrapper, TO_ESM, state);
|
||||
const copyBinding = wrapperRoles?.get("__copyProps");
|
||||
const copy = unprojectedInitializer(copyBinding, state);
|
||||
const copyRoles =
|
||||
copy === undefined ? undefined : canonicalRoles(copy, COPY_PROPS, state);
|
||||
if (
|
||||
wrapperRoles === undefined ||
|
||||
copyRoles === undefined ||
|
||||
copy === undefined
|
||||
)
|
||||
return undefined;
|
||||
const dependencies: IntrinsicDependency[] = [];
|
||||
let readyAfter = Math.max(wrapper.end ?? Infinity, copy.end ?? Infinity);
|
||||
for (const roles of [wrapperRoles, copyRoles])
|
||||
for (const [role, dependency] of roles) {
|
||||
const expected = INTRINSICS[role];
|
||||
if (expected === undefined) continue;
|
||||
const initializer = unprojectedInitializer(dependency, state);
|
||||
const captured =
|
||||
initializer === undefined
|
||||
? undefined
|
||||
: capturedIntrinsic(initializer);
|
||||
if (
|
||||
initializer === undefined ||
|
||||
captured === undefined ||
|
||||
JSON.stringify(captured.path) !==
|
||||
JSON.stringify(["intrinsic", ...expected])
|
||||
)
|
||||
return undefined;
|
||||
readyAfter = Math.max(readyAfter, initializer.end ?? Infinity);
|
||||
dependencies.push(captured);
|
||||
if (role === "__hasOwnProp")
|
||||
dependencies.push({ path: [...captured.path, "call"] });
|
||||
}
|
||||
dependencies.push(
|
||||
{ path: ["intrinsic", "Function", "prototype", "bind"] },
|
||||
{ path: ["intrinsic", "Function", "prototype", "call"] },
|
||||
);
|
||||
const proof = {
|
||||
dependencies,
|
||||
identity: false,
|
||||
readyAfter,
|
||||
};
|
||||
helpers.set(binding.bindingId, proof);
|
||||
return proof;
|
||||
};
|
||||
|
||||
const provenCall = (node: t.Node) => {
|
||||
if (!t.isCallExpression(node) || !t.isIdentifier(node.callee))
|
||||
return undefined;
|
||||
const helper = resolveSemanticBindingState(
|
||||
state,
|
||||
node.callee,
|
||||
node.callee.name,
|
||||
);
|
||||
const proof = helper === undefined ? undefined : helperProof(helper);
|
||||
const input = node.arguments[0];
|
||||
const mode = node.arguments[1];
|
||||
const literalMode =
|
||||
mode === undefined
|
||||
? { found: true, value: false }
|
||||
: t.isExpression(mode)
|
||||
? readExactJavaScriptLiteral(mode)
|
||||
: { found: false };
|
||||
if (
|
||||
proof === undefined ||
|
||||
proof.readyAfter >= (node.start ?? -1) ||
|
||||
!t.isExpression(input) ||
|
||||
node.arguments.length > 2 ||
|
||||
!literalMode.found
|
||||
)
|
||||
return undefined;
|
||||
return {
|
||||
input,
|
||||
// A truthy node mode forces the module-valued default. Other modes
|
||||
// preserve named exports without proving the source's default property.
|
||||
defaultIsModule: !proof.identity && Boolean(literalMode.value),
|
||||
dependencies: proof.dependencies.map((dependency) => ({
|
||||
...dependency,
|
||||
capturedAt: dependency.capturedAt ?? node,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const callOrigin = (node: t.Node): InteropOrigin | undefined => {
|
||||
const cached = calls.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
const { root, members } = electronMemberPath(node);
|
||||
if (!members.every((member): member is string => member !== null))
|
||||
return undefined;
|
||||
const converted = provenCall(root);
|
||||
const required =
|
||||
converted === undefined
|
||||
? undefined
|
||||
: semanticRequireOrigin(converted.input, state)?.origin;
|
||||
if (
|
||||
converted === undefined ||
|
||||
required === undefined ||
|
||||
required.importedPath.length !== 0 ||
|
||||
(members[0] === "default" && !converted.defaultIsModule)
|
||||
)
|
||||
return undefined;
|
||||
const result = {
|
||||
origin: { ...required, importedPath: members },
|
||||
dependencies: converted.dependencies,
|
||||
defaultIsModule: converted.defaultIsModule,
|
||||
};
|
||||
calls.set(node, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
const origin = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
): InteropOrigin | undefined => {
|
||||
const single = semanticStableInitializer(binding, state);
|
||||
if (single !== undefined) {
|
||||
if (
|
||||
!single.projection.every(
|
||||
(key): key is string => typeof key === "string",
|
||||
)
|
||||
)
|
||||
return undefined;
|
||||
const converted = callOrigin(single.node);
|
||||
if (
|
||||
converted?.origin.importedPath.length === 0 &&
|
||||
single.projection[0] === "default" &&
|
||||
!converted.defaultIsModule
|
||||
)
|
||||
return undefined;
|
||||
const result =
|
||||
converted === undefined
|
||||
? undefined
|
||||
: {
|
||||
...converted,
|
||||
origin: {
|
||||
...converted.origin,
|
||||
importedPath: [
|
||||
...converted.origin.importedPath,
|
||||
...single.projection,
|
||||
],
|
||||
},
|
||||
};
|
||||
if (result !== undefined) origins.set(binding.bindingId, result);
|
||||
return result;
|
||||
}
|
||||
const [initial, converted] = binding.initializers;
|
||||
if (
|
||||
binding.definitions.length !== 2 ||
|
||||
binding.definitions[0]?.kind !== "variable" ||
|
||||
binding.definitions[1]?.kind !== "assignment" ||
|
||||
binding.initializers.length !== 2 ||
|
||||
initial === undefined ||
|
||||
converted === undefined ||
|
||||
initial.projection.length !== 0 ||
|
||||
converted.projection.length !== 0 ||
|
||||
state.conditionalInitializers.has(initial.node)
|
||||
)
|
||||
return undefined;
|
||||
const call = converted.node;
|
||||
const assignment = state.parentsByNode.get(call);
|
||||
const statement =
|
||||
assignment === undefined
|
||||
? undefined
|
||||
: state.parentsByNode.get(assignment);
|
||||
const declaration = state.parentsByNode.get(initial.node);
|
||||
const declarationStatement =
|
||||
declaration === undefined
|
||||
? undefined
|
||||
: state.parentsByNode.get(declaration);
|
||||
if (
|
||||
!t.isCallExpression(call) ||
|
||||
!t.isIdentifier(call.callee) ||
|
||||
!t.isAssignmentExpression(assignment, { operator: "=" }) ||
|
||||
assignment.right !== call ||
|
||||
!t.isIdentifier(assignment.left, { name: binding.name }) ||
|
||||
!t.isExpressionStatement(statement) ||
|
||||
!t.isVariableDeclarator(declaration) ||
|
||||
!t.isVariableDeclaration(declarationStatement) ||
|
||||
state.parentsByNode.get(statement) !==
|
||||
state.parentsByNode.get(declarationStatement) ||
|
||||
(initial.node.start ?? Infinity) >= (call.start ?? -1)
|
||||
)
|
||||
return undefined;
|
||||
const convertedCall = provenCall(call);
|
||||
const input = convertedCall?.input;
|
||||
if (
|
||||
!t.isIdentifier(input) ||
|
||||
resolveSemanticBindingState(state, input, input.name) !== binding
|
||||
)
|
||||
return undefined;
|
||||
const required = semanticRequireOrigin(initial.node, state)?.origin;
|
||||
if (required === undefined || required.importedPath.length !== 0)
|
||||
return undefined;
|
||||
if (convertedCall === undefined) return undefined;
|
||||
const result = {
|
||||
origin: required,
|
||||
dependencies: convertedCall.dependencies,
|
||||
defaultIsModule: convertedCall.defaultIsModule,
|
||||
};
|
||||
origins.set(binding.bindingId, result);
|
||||
return result;
|
||||
};
|
||||
const invalidatedOrigin = (record: InteropOrigin | undefined): boolean =>
|
||||
record?.dependencies.some(({ path, capturedAt }) => {
|
||||
if (capturedAt === undefined) return true;
|
||||
const scope = opaqueMutationPath(capturedAt).join(".");
|
||||
// Captured function values survive later replacement of global
|
||||
// properties. Other callable bodies are not execution evidence.
|
||||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < (capturedAt.start ?? -1) &&
|
||||
write.coverage.covers(path),
|
||||
);
|
||||
}) ?? false;
|
||||
return {
|
||||
origin,
|
||||
callOrigin,
|
||||
intrinsicPath,
|
||||
intrinsicInvocation: (node: t.CallExpression | t.OptionalCallExpression) =>
|
||||
resolveJavaScriptIntrinsicInvocation(
|
||||
node,
|
||||
intrinsicPath,
|
||||
boundIntrinsic,
|
||||
intrinsicReadChanged,
|
||||
),
|
||||
boundIntrinsic,
|
||||
staticString,
|
||||
isFreshObject,
|
||||
opaqueMutationPath,
|
||||
retainWrite: (node: t.Node, path: readonly (string | null)[]) => {
|
||||
const coverage = new SemanticPropertyPathCoverage();
|
||||
coverage.retain(path[0] === "opaque-intrinsic" ? ["intrinsic"] : path);
|
||||
writes.push({
|
||||
scope: opaqueMutationPath(node).join("."),
|
||||
offset: node.start ?? Infinity,
|
||||
coverage,
|
||||
});
|
||||
},
|
||||
invalidated: (binding: JavaScriptSemanticBindingState): boolean => {
|
||||
const cached = invalidatedBindings.get(binding.bindingId);
|
||||
if (cached !== undefined) return cached;
|
||||
const result = invalidatedOrigin(origins.get(binding.bindingId));
|
||||
invalidatedBindings.set(binding.bindingId, result);
|
||||
return result;
|
||||
},
|
||||
invalidatedCall: (node: t.Node): boolean => {
|
||||
const cached = invalidatedCalls.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
const result = invalidatedOrigin(calls.get(node));
|
||||
invalidatedCalls.set(node, result);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const identityFunction = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
if (
|
||||
!t.isFunction(node) ||
|
||||
node.async ||
|
||||
node.generator ||
|
||||
node.params.length !== 1
|
||||
)
|
||||
return false;
|
||||
const parameter = node.params[0];
|
||||
const result = t.isBlockStatement(node.body)
|
||||
? node.body.body.length === 1 && t.isReturnStatement(node.body.body[0])
|
||||
? node.body.body[0].argument
|
||||
: undefined
|
||||
: node.body;
|
||||
return (
|
||||
t.isIdentifier(parameter) &&
|
||||
t.isIdentifier(result) &&
|
||||
resolveSemanticBindingState(state, parameter, parameter.name) !==
|
||||
undefined &&
|
||||
resolveSemanticBindingState(state, parameter, parameter.name) ===
|
||||
resolveSemanticBindingState(state, result, result.name)
|
||||
);
|
||||
};
|
||||
|
||||
const unprojectedInitializer = (
|
||||
binding: JavaScriptSemanticBindingState | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): t.Node | undefined => {
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
return initializer?.projection.length === 0 ? initializer.node : undefined;
|
||||
};
|
||||
|
||||
/** Match operations and lexical roles, allowing single-statement control blocks. */
|
||||
const canonicalRoles = (
|
||||
actual: t.Node,
|
||||
pattern: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): ReadonlyMap<string, JavaScriptSemanticBindingState> | undefined => {
|
||||
const roles = new Map<string, JavaScriptSemanticBindingState>();
|
||||
const used = new Map<string, string>();
|
||||
const pending: { actual: t.Node; pattern: t.Node; parent: t.Node | null }[] =
|
||||
[{ actual, pattern, parent: null }];
|
||||
while (pending.length !== 0) {
|
||||
const pair = pending.pop();
|
||||
if (pair === undefined) return undefined;
|
||||
const { parent } = pair;
|
||||
const controlBody =
|
||||
(t.isIfStatement(parent) &&
|
||||
(parent.consequent === pair.pattern ||
|
||||
parent.alternate === pair.pattern)) ||
|
||||
(t.isForStatement(parent) && parent.body === pair.pattern);
|
||||
const node = controlBody ? controlBodyOperation(pair.actual) : pair.actual;
|
||||
const expected = controlBody
|
||||
? controlBodyOperation(pair.pattern)
|
||||
: pair.pattern;
|
||||
if (node.type !== expected.type) return undefined;
|
||||
const literalKey =
|
||||
parent !== null &&
|
||||
((t.isMemberExpression(parent) &&
|
||||
!parent.computed &&
|
||||
parent.property === expected) ||
|
||||
(t.isObjectProperty(parent) &&
|
||||
!parent.computed &&
|
||||
parent.key === expected));
|
||||
if (t.isIdentifier(expected) && t.isIdentifier(node) && !literalKey) {
|
||||
const binding = resolveSemanticBindingState(state, node, node.name);
|
||||
if (
|
||||
binding === undefined ||
|
||||
(roles.has(expected.name) && roles.get(expected.name) !== binding) ||
|
||||
(used.has(binding.bindingId) &&
|
||||
used.get(binding.bindingId) !== expected.name)
|
||||
)
|
||||
return undefined;
|
||||
roles.set(expected.name, binding);
|
||||
used.set(binding.bindingId, expected.name);
|
||||
}
|
||||
const visitorKeys = javaScriptAstChildKeys(expected);
|
||||
for (const [key, field] of Object.entries(
|
||||
t.NODE_FIELDS[expected.type] ?? {},
|
||||
)) {
|
||||
if (
|
||||
visitorKeys.includes(key) ||
|
||||
(t.isIdentifier(expected) && key === "name" && !literalKey)
|
||||
)
|
||||
continue;
|
||||
const fallback: unknown = Reflect.get(field, "default");
|
||||
if (
|
||||
JSON.stringify(Reflect.get(node, key) ?? fallback) !==
|
||||
JSON.stringify(Reflect.get(expected, key) ?? fallback)
|
||||
)
|
||||
return undefined;
|
||||
}
|
||||
const children = childNodes(node),
|
||||
expectedChildren = childNodes(expected);
|
||||
if (children.length !== expectedChildren.length) return undefined;
|
||||
for (const [index, child] of children.entries()) {
|
||||
const expectedChild = expectedChildren[index];
|
||||
if (expectedChild !== undefined)
|
||||
pending.push({
|
||||
actual: child,
|
||||
pattern: expectedChild,
|
||||
parent: expected,
|
||||
});
|
||||
}
|
||||
}
|
||||
return roles;
|
||||
};
|
||||
|
||||
// Braces around one control-body statement do not add operations. Keep other
|
||||
// blocks, directives and extra statements intact; their semantics need proof.
|
||||
// Lexical declarations/references still pass through canonicalRoles' bijection.
|
||||
const controlBodyOperation = (node: t.Node): t.Node => {
|
||||
while (
|
||||
t.isBlockStatement(node) &&
|
||||
node.directives.length === 0 &&
|
||||
node.body.length === 1
|
||||
) {
|
||||
const statement = node.body[0];
|
||||
if (statement === undefined) break;
|
||||
node = statement;
|
||||
}
|
||||
return node;
|
||||
};
|
||||
Reference in New Issue
Block a user