diff --git a/src/domain/javascript/electronBundledRequireIdentity.test.ts b/src/domain/javascript/electronBundledRequireIdentity.test.ts index 1904daef9e..ff1e187fa8 100644 --- a/src/domain/javascript/electronBundledRequireIdentity.test.ts +++ b/src/domain/javascript/electronBundledRequireIdentity.test.ts @@ -48,6 +48,38 @@ describe("Electron identity through Webpack externals", () => { expect(electronOf(source).browser_windows).toEqual([]); }); + const GETTER_CONSUMER = "const s=n.n(n(45840));new (s().BrowserWindow)({});"; + it.each([ + ["Reflect.set", 'Reflect.set(n,"n",e=>()=>({BrowserWindow:class{}}));'], + [ + "Reflect.defineProperty", + 'Reflect.defineProperty(n,"n",{value:e=>()=>0});', + ], + ["delete", "delete n.n;"], + ["a destructuring write", "({g:n.n}={g:e=>()=>0});"], + ["an unknown key", "n[key]=e=>()=>0;"], + ])( + "does not infer Electron after a getter write through %s", + (_name, write) => { + expect( + electronOf(bootstrap(EXTERNAL, GETTER_CONSUMER, `${write}n(100);`)) + .browser_windows, + ).toEqual([]); + }, + ); + + it("keeps the getter after a reflective write to another loader key", () => { + expect( + electronOf( + bootstrap( + EXTERNAL, + GETTER_CONSUMER, + 'Object.defineProperty(n,"d",{value:1});n(100);', + ), + ).browser_windows, + ).toHaveLength(1); + }); + it("follows the loader passed to a module factory", () => { const electron = electronOf(bootstrap(EXTERNAL, CONSUMER)); expect(electron.browser_windows).toMatchObject([ diff --git a/src/domain/javascript/javascriptBundledRequireOrigin.ts b/src/domain/javascript/javascriptBundledRequireOrigin.ts index f6e5749f70..dbfe873e08 100644 --- a/src/domain/javascript/javascriptBundledRequireOrigin.ts +++ b/src/domain/javascript/javascriptBundledRequireOrigin.ts @@ -2,7 +2,12 @@ import * as t from "@babel/types"; import { javascriptBootstrapAt } from "./javascriptBootstrap.js"; import { proveJavaScriptBootstrapIntegrity } from "./javascriptBootstrapIntegrity.js"; -import { semanticStaticPropertyKey } from "./javascriptAstValues.js"; +import { + semanticStaticPropertyKey, + unwrapJavaScriptExpression, +} from "./javascriptAstValues.js"; +import { collectJavaScriptIdentityWritesSteps } from "./javascriptElectronMemberWrites.js"; +import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; import { semanticMemberChain, semanticRequireOrigin, @@ -18,7 +23,10 @@ import { moduleFactory, modulePropertyName, } from "./javascriptStaticAnalysisHelpers.js"; -import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; +import { + completeSemanticSteps, + traverseJavaScriptAst, +} from "./javascriptSemanticTraversal.js"; /** Loader bindings and literal externals proven for one standalone bootstrap. */ interface BootstrapExternals { @@ -142,60 +150,38 @@ const canonicalWebpackInteropBindings = ( bootstrapBody: t.BlockStatement, state: JavaScriptSemanticAnalysisState, ): ReadonlySet => { - const writes = new Map(); - let reflectiveWrite = false; - traverseJavaScriptAst(state.program, { - enter: (node) => { - if (t.isAssignmentExpression(node) && t.isMemberExpression(node.left)) { - const target = node.left; - const bindingId = t.isIdentifier(target.object) - ? resolveSemanticBindingState( - state, - target.object, - target.object.name, - )?.bindingId - : undefined; - if ( - semanticStaticPropertyKey(target.property, target.computed) === "n" && - bindingId !== undefined && - loaderBindingIds.has(bindingId) - ) - writes.set(bindingId, [...(writes.get(bindingId) ?? []), node]); - } - if ( - !t.isCallExpression(node) || - !t.isMemberExpression(node.callee) || - !t.isIdentifier(node.callee.object, { name: "Object" }) || - ![ - "defineProperty", - "defineProperties", - "assign", - "setPrototypeOf", - ].includes( - semanticStaticPropertyKey( - node.callee.property, - node.callee.computed, - ) ?? "", - ) || - !t.isIdentifier(node.arguments[0]) - ) - return; - reflectiveWrite ||= loaderBindingIds.has( - resolveSemanticBindingState( - state, - node.arguments[0], - node.arguments[0].name, - )?.bindingId ?? "", - ); - }, - }); + // The shared identity-write owner covers assignment, delete, destructuring, + // and Object/Reflect mutators; any write that may select `n` counts. + const writes = new Set(); + const interop = createSemanticModuleInterop(state); + completeSemanticSteps( + collectJavaScriptIdentityWritesSteps( + state.program, + (root, kind) => { + const node = unwrapJavaScriptExpression(root).node; + return kind === "member" && + t.isIdentifier(node) && + loaderBindingIds.has( + resolveSemanticBindingState(state, node, node.name)?.bindingId ?? + "", + ) + ? [["loader"]] + : []; + }, + interop.intrinsicInvocation, + interop.staticString, + interop.isFreshObject, + (node, path) => { + if (path.length < 2 || path[1] === "n" || path[1] === null) + writes.add(node); + }, + ), + ); const proven = new Set(); - if (reflectiveWrite) return proven; - const allWrites = [...writes.values()].flat(); - const [write] = allWrites; + const [write] = writes; if ( - allWrites.length === 1 && - write !== undefined && + writes.size === 1 && + t.isAssignmentExpression(write) && isInstalledBeforeEntryDispatch( write, bootstrapBody,