mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -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,123 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import type { JavaScriptIntrinsicInvocation } from "./javascriptElectronMemberWrites.js";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
const arrayArguments = (array: t.Node | undefined) =>
|
||||
t.isArrayExpression(array) &&
|
||||
array.elements.every((element) => t.isExpression(element))
|
||||
? array.elements.filter((element): element is t.Expression =>
|
||||
t.isExpression(element),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const invocationArguments = (
|
||||
method: "call" | "apply",
|
||||
arguments_: JavaScriptIntrinsicInvocation["arguments"],
|
||||
) =>
|
||||
t.isSpreadElement(arguments_?.[0])
|
||||
? undefined
|
||||
: method === "apply"
|
||||
? arrayArguments(arguments_?.[1])
|
||||
: arguments_?.slice(1);
|
||||
|
||||
/**
|
||||
* Normalize known invocation syntax once, retaining bound argument prefixes.
|
||||
* Binding a function alone is not a write; only its eventual call is examined.
|
||||
*/
|
||||
export const resolveJavaScriptIntrinsicInvocation = (
|
||||
node: t.CallExpression | t.OptionalCallExpression,
|
||||
intrinsicPath: (node: t.Node) => readonly string[] | undefined,
|
||||
boundIntrinsic: (node: t.Node) => JavaScriptIntrinsicInvocation | undefined,
|
||||
intrinsicReadChanged: (node: t.Node, path: readonly string[]) => boolean,
|
||||
): JavaScriptIntrinsicInvocation | undefined => {
|
||||
const visited = new Set<t.Node>();
|
||||
let callee: t.Node = node.callee;
|
||||
let arguments_: JavaScriptIntrinsicInvocation["arguments"] = node.arguments;
|
||||
let receiver: t.Node | undefined;
|
||||
for (;;) {
|
||||
callee = unwrapJavaScriptExpression(callee).node;
|
||||
if (visited.has(callee)) return undefined;
|
||||
visited.add(callee);
|
||||
let path = intrinsicPath(callee);
|
||||
if (path !== undefined && intrinsicReadChanged(callee, path))
|
||||
return { kind: "opaque", path, arguments: undefined };
|
||||
if (path === undefined) {
|
||||
if (
|
||||
t.isMemberExpression(callee) ||
|
||||
t.isOptionalMemberExpression(callee)
|
||||
) {
|
||||
const method = semanticStaticPropertyKey(
|
||||
callee.property,
|
||||
callee.computed,
|
||||
);
|
||||
if (method === "call" || method === "apply") {
|
||||
const bound = boundIntrinsic(callee.object);
|
||||
if (
|
||||
bound !== undefined &&
|
||||
intrinsicReadChanged(callee, [...bound.path, method])
|
||||
)
|
||||
return { kind: "opaque", path: bound.path, arguments: undefined };
|
||||
receiver = arguments_?.[0];
|
||||
callee = callee.object;
|
||||
arguments_ = invocationArguments(method, arguments_);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const bound = boundIntrinsic(callee);
|
||||
if (bound === undefined) return undefined;
|
||||
if (bound.kind === "opaque") return bound;
|
||||
path = bound.path;
|
||||
receiver = bound.receiver;
|
||||
arguments_ =
|
||||
bound.arguments === undefined
|
||||
? undefined
|
||||
: [...bound.arguments, ...(arguments_ ?? [])];
|
||||
}
|
||||
// A projected .call/.apply invokes the preceding callable. The actual
|
||||
// Function.prototype methods instead invoke their receiver, retained by bind.
|
||||
while (path.length > 3 && !genericInvocationMethod(path)) {
|
||||
const method = path.at(-1);
|
||||
if (method !== "call" && method !== "apply") break;
|
||||
receiver = arguments_?.[0];
|
||||
arguments_ = invocationArguments(method, arguments_);
|
||||
path = path.slice(0, -1);
|
||||
}
|
||||
const genericMethod = genericInvocationMethod(path);
|
||||
if (genericMethod !== undefined) {
|
||||
if (!t.isExpression(receiver)) return undefined;
|
||||
callee = receiver;
|
||||
receiver = arguments_?.[0];
|
||||
arguments_ = invocationArguments(genericMethod, arguments_);
|
||||
continue;
|
||||
}
|
||||
if (path.join(".") === "intrinsic.Reflect.apply") {
|
||||
const callable = arguments_?.[0];
|
||||
if (!t.isExpression(callable)) return undefined;
|
||||
callee = callable;
|
||||
receiver = arguments_?.[1];
|
||||
arguments_ = arrayArguments(arguments_?.[2]);
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
kind: "known",
|
||||
path,
|
||||
arguments: arguments_,
|
||||
...(receiver === undefined ? {} : { receiver }),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const genericInvocationMethod = (
|
||||
path: readonly string[],
|
||||
): "call" | "apply" | undefined =>
|
||||
path.length === 4 &&
|
||||
path[0] === "intrinsic" &&
|
||||
path[1] === "Function" &&
|
||||
path[2] === "prototype" &&
|
||||
(path[3] === "call" || path[3] === "apply")
|
||||
? path[3]
|
||||
: undefined;
|
||||
Reference in New Issue
Block a user