2026-10-11 09:25:32 +08:00
import * as t from "@babel/types" ;
import {
semanticStaticPropertyKey ,
2026-10-12 01:06:33 +08:00
semanticWriteTargets ,
2026-10-11 09:25:32 +08:00
unwrapJavaScriptExpression ,
} from "./javascriptAstValues.js" ;
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js" ;
/** Match Electron's supported module entry points. */
export const ELECTRON_MODULE =
/^electron(?:\/(?:common|main|renderer|utility))?$/u;
2026-10-10 19:31:08 -07:00
/**
* Electron's CommonJS exports define no `default` member, so a leading
* `default` segment can only denote the module object itself, as an ES module
* default import (`import electron from "electron"`) binds it.
*/
export const electronExportPath = (
importedPath : readonly string [],
) : readonly string [] =>
importedPath [ 0 ] === "default" ? importedPath . slice ( 1 ) : importedPath ;
2026-10-11 09:25:32 +08:00
/** Static identity recovery cannot establish the absence of Electron usage. */
export const ELECTRON_IDENTITY_LIMITATION =
2026-10-12 01:06:33 +08:00
"Electron API identity follows proven lexical module aliases and structurally verified module-namespace interop. Other reassignments, dynamic selections, and visibly overwritten namespace members are unresolved; missing findings do not establish absence. Arbitrary call side effects and runtime registration are not evaluated." ;
2026-10-11 09:25:32 +08:00
/** Preserve exact property segments instead of flattening literal dotted keys. */
export const electronMemberPath = (
node : t.Node ,
) : {
readonly root : t.Node ;
readonly members : readonly ( string | null )[];
} => {
let root = unwrapJavaScriptExpression ( node ). node ;
const members : ( string | null )[] = [];
while ( t . isMemberExpression ( root ) || t . isOptionalMemberExpression ( root )) {
members . push ( semanticStaticPropertyKey ( root . property , root . computed ));
root = unwrapJavaScriptExpression ( root . object ). node ;
}
return { root , members : members.toReversed () };
};
2026-10-12 01:06:33 +08:00
/** A known intrinsic callable and its examined positional arguments. */
export interface JavaScriptIntrinsicInvocation {
readonly kind : "known" | "opaque" ;
/** The original bound receiver; rebinding cannot replace it. */
readonly receiver? : t.Node ;
readonly path : readonly string [];
readonly arguments :
| readonly t . CallExpression [ "arguments" ][ number ][]
| undefined ;
}
/** Index member and binding writes through caller-proven lexical identities. */
export function * collectJavaScriptIdentityWritesSteps (
2026-10-11 09:25:32 +08:00
program : t.Program ,
2026-10-12 01:06:33 +08:00
origin : (
root : t.Node ,
kind : "binding" | "member" | "opaque-call" ,
) => readonly string [] | undefined ,
intrinsicInvocation : (
node : t.CallExpression | t . OptionalCallExpression ,
) => JavaScriptIntrinsicInvocation | undefined ,
staticString : ( node : t.Node | undefined ) => string | undefined ,
freshObject : ( node : t.Node ) => boolean ,
retain : ( node : t.Node , path : readonly ( string | null )[]) => void ,
) : Generator < void > {
const objectMutators = new Set ([
"defineProperty" ,
"defineProperties" ,
"assign" ,
"setPrototypeOf" ,
]);
const reflectMutators = new Set ([
"set" ,
"deleteProperty" ,
"defineProperty" ,
"setPrototypeOf" ,
]);
const keyedMutators = new Set ([ "set" , "deleteProperty" , "defineProperty" ]);
2026-10-11 09:25:32 +08:00
yield * traverseJavaScriptAstSteps ( program , {
enter : ( node ) => {
2026-10-12 01:06:33 +08:00
if ( t . isCallExpression ( node ) || t . isOptionalCallExpression ( node )) {
const invocation = intrinsicInvocation ( node );
if ( invocation ? . kind === "opaque" ) {
const scope = origin ( node , "opaque-call" );
if ( scope !== undefined ) retain ( node , scope );
return ;
}
const callee = invocation ? . path ;
const arguments_ = invocation ? . arguments ;
const method = callee ? . at ( - 1 );
const owner = callee ? . length === 3 ? callee [ 1 ] : undefined ;
if (
( owner === "Object" && objectMutators . has ( method ?? "" )) ||
( owner === "Reflect" && reflectMutators . has ( method ?? "" ))
) {
// Reflect.set can write through its explicit receiver as well as
// dispatching through its target. Both identities need examination.
const targets =
owner === "Reflect" &&
method === "set" &&
arguments_ !== undefined &&
( arguments_ . length >= 4 || arguments_ . some ( t . isSpreadElement ))
? [
arguments_ [ 0 ],
arguments_ . some ( t . isSpreadElement )
? undefined
: arguments_ [ 3 ],
]
: [ arguments_ ? .[ 0 ]];
for ( const target of targets ) {
// A known mutator with an opaque argument list can overwrite any
// captured intrinsic. Do not turn an unexamined target into proof.
if ( target === undefined || t . isSpreadElement ( target )) {
const scope = origin ( node , "opaque-call" );
if ( scope !== undefined ) retain ( node , scope );
}
if ( t . isNode ( target ) && ! t . isSpreadElement ( target )) {
const { root , members } = electronMemberPath ( target );
const exported = origin ( root , "member" );
if ( exported !== undefined ) {
const key = keyedMutators . has ( method ?? "" )
? [ staticString ( arguments_ ? .[ 1 ]) ?? null ]
: [];
retain ( node , [... exported , ... members , ... key ]);
} else if ( ! freshObject ( target )) {
const scope = origin ( node , "opaque-call" );
if ( scope !== undefined ) retain ( node , scope );
}
}
}
}
}
2026-10-11 09:25:32 +08:00
const target = t . isAssignmentExpression ( node )
? node.left
: t.isUpdateExpression ( node )
? node.argument
: t.isUnaryExpression ( node , { operator : "delete" })
? node.argument
: t.isForOfStatement ( node ) || t . isForInStatement ( node )
? node.left
: undefined ;
if ( target === undefined ) return ;
2026-10-12 01:06:33 +08:00
for ( const leaf of semanticWriteTargets ( target )) {
if ( t . isIdentifier ( leaf )) {
const exported = origin ( leaf , "binding" );
if ( exported !== undefined ) retain ( node , exported );
continue ;
}
const { root , members } = electronMemberPath ( leaf );
const exported = origin ( root , "member" );
if ( exported !== undefined ) retain ( node , [... exported , ... members ]);
2026-10-11 09:25:32 +08:00
}
},
});
}