2026-10-11 09:25:32 +08:00
import * as t from "@babel/types" ;
import {
semanticStaticPropertyKey ,
2026-10-14 08:44:52 +08:00
semanticWriteSiteTarget ,
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 ;
}
2026-10-13 06:24:45 +08:00
/**
* Index writes through possible lexical object identities. Resolve the complete
* object before appending the written key: self-reference properties can denote
* the same object, while replacing that property is a distinct write.
*/
2026-10-12 01:06:33 +08:00
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" ,
2026-10-14 08:44:52 +08:00
writeSite : t.Node ,
2026-10-13 06:24:45 +08:00
) => readonly ( readonly ( string | null )[])[],
2026-10-12 01:06:33 +08:00
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 ,
2026-10-18 08:25:06 +08:00
observeInvocation ?: (
node :
| t . CallExpression
| t . OptionalCallExpression
| t . NewExpression
| t . TaggedTemplateExpression ,
invocation : JavaScriptIntrinsicInvocation | undefined ,
) => void ,
2026-10-12 01:06:33 +08:00
) : 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-18 08:25:06 +08:00
if (
t . isCallExpression ( node ) ||
t . isOptionalCallExpression ( node ) ||
t . isNewExpression ( node ) ||
t . isTaggedTemplateExpression ( node )
) {
const invocation =
t . isNewExpression ( node ) || t . isTaggedTemplateExpression ( node )
? undefined
: intrinsicInvocation ( node );
observeInvocation ? .( node , invocation );
if ( t . isNewExpression ( node ) || t . isTaggedTemplateExpression ( node ))
return ;
2026-10-12 01:06:33 +08:00
if ( invocation ? . kind === "opaque" ) {
2026-10-14 08:44:52 +08:00
for ( const scope of origin ( node , "opaque-call" , node ))
retain ( node , scope );
2026-10-12 01:06:33 +08:00
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 )) {
2026-10-14 08:44:52 +08:00
for ( const scope of origin ( node , "opaque-call" , node ))
2026-10-13 06:24:45 +08:00
retain ( node , scope );
2026-10-12 01:06:33 +08:00
}
if ( t . isNode ( target ) && ! t . isSpreadElement ( target )) {
2026-10-14 08:44:52 +08:00
const identities = origin ( target , "member" , node );
2026-10-13 06:24:45 +08:00
if ( identities . length > 0 ) {
2026-10-12 01:06:33 +08:00
const key = keyedMutators . has ( method ?? "" )
? [ staticString ( arguments_ ? .[ 1 ]) ?? null ]
2026-10-14 11:46:14 -07:00
: // Mutating a target's contents does not replace the target.
// Retain an unknown child key, rather than its own identity.
[ null ];
2026-10-13 06:24:45 +08:00
for ( const identity of identities )
retain ( node , [... identity , ... key ]);
2026-10-12 01:06:33 +08:00
} else if ( ! freshObject ( target )) {
2026-10-14 08:44:52 +08:00
for ( const scope of origin ( node , "opaque-call" , node ))
2026-10-13 06:24:45 +08:00
retain ( node , scope );
2026-10-12 01:06:33 +08:00
}
}
}
}
}
2026-10-14 08:44:52 +08:00
const target = semanticWriteSiteTarget ( node );
if ( target === null ) return ;
2026-10-12 01:06:33 +08:00
for ( const leaf of semanticWriteTargets ( target )) {
if ( t . isIdentifier ( leaf )) {
2026-10-14 08:44:52 +08:00
for ( const identity of origin ( leaf , "binding" , node ))
2026-10-13 06:24:45 +08:00
retain ( node , identity );
2026-10-12 01:06:33 +08:00
continue ;
}
2026-10-13 06:24:45 +08:00
const member = unwrapJavaScriptExpression ( leaf ). node ;
if (
! t . isMemberExpression ( member ) &&
! t . isOptionalMemberExpression ( member )
)
continue ;
const key = semanticStaticPropertyKey ( member . property , member . computed );
2026-10-14 08:44:52 +08:00
for ( const identity of origin ( member . object , "member" , node ))
2026-10-13 06:24:45 +08:00
retain ( node , [... identity , key ]);
2026-10-11 09:25:32 +08:00
}
},
});
}