diff --git a/src/domain/javascript/electronBundledRequireIdentity.test.ts b/src/domain/javascript/electronBundledRequireIdentity.test.ts index 390f967a4d..0e81e70506 100644 --- a/src/domain/javascript/electronBundledRequireIdentity.test.ts +++ b/src/domain/javascript/electronBundledRequireIdentity.test.ts @@ -703,6 +703,10 @@ describe("Electron members destructured from a Webpack external binding", () => "const s=n(45840);const w=s;w.BrowserWindow=class{};const{BrowserWindow}=s;", ], ["a cyclic alias", "var a=b,b=a;const{BrowserWindow}=a;"], + [ + "an alias read before its hoisted source is initialized", + "var saved=s;var s=n(45840);const{BrowserWindow}=saved;", + ], ["a different table key", "const s=n(45841);const{BrowserWindow}=s;"], [ "a rebindable binding exposed to direct eval", diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index 182272200e..7f5d6339a7 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -882,17 +882,7 @@ export function* indexJavaScriptElectronIdentitySteps( const originAvailableAtRead = ( binding: JavaScriptSemanticBindingState, read: t.Node, - ): boolean => { - if ( - binding.directOrigins.length > 0 || - semanticStableInitializer(binding, state) === undefined - ) - return true; - const access = semanticBindingAtRead(binding, read, state); - // An unresolved activation can still carry a possible identity, while an - // ordered read before its initializer cannot refer to that value. - return access.status !== "ordered" || access.hasDefiniteInitializer; - }; + ): boolean => originAvailableAt(binding, read, state); const rootOrigin = (root: t.Node): readonly string[] | undefined => { if (t.isIdentifier(root)) { if (semanticResolutionBlocked(state, root, root.name)) return undefined; @@ -1139,16 +1129,35 @@ const electronRequireOrigin = ( semanticRequireOrigin(node, state) ?? semanticBundledRequireOrigin(node, state); +/** Whether a read can see the binding's stable origin rather than an earlier value. */ +const originAvailableAt = ( + binding: JavaScriptSemanticBindingState, + read: t.Node, + state: JavaScriptSemanticAnalysisState, +): boolean => { + if ( + binding.directOrigins.length > 0 || + semanticStableInitializer(binding, state) === undefined + ) + return true; + const access = semanticBindingAtRead(binding, read, state); + // An unresolved activation can still carry a possible identity, while an + // ordered read before its initializer cannot refer to that value. + return access.status !== "ordered" || access.hasDefiniteInitializer; +}; + /** * General provenance only knows literal `require`, so a bundled external held * in one binding and destructured from another (`const s=n(K);const{X}=s`) * is followed here through stable lexical aliases. Like provenance, a hop - * stops at a rebindable binding that direct eval may have changed. + * stops at a rebindable binding that direct eval may have changed, and at an + * alias read that runs before the aliased binding's initializer. The chain has + * one alias per hop, so one visited set serves the whole walk. */ const bindingOrigin = ( binding: JavaScriptSemanticBindingState, state: JavaScriptSemanticAnalysisState, - visited: ReadonlySet = new Set(), + visited: Set = new Set(), ): JavaScriptModuleOrigin | undefined => { if (binding.directOrigins.length > 0) return binding.directOrigins.length === 1 @@ -1172,13 +1181,14 @@ const bindingOrigin = ( ? electronRequireOrigin(initializer.node, state)?.origin : visited.has(alias.bindingId) || exhausted || + !originAvailableAt(alias, source, state) || [binding, alias].some( (hop) => semanticBindingCanBeRebound(hop) && semanticBindingMayHaveOpaqueEffect(hop, state), ) ? undefined - : bindingOrigin(alias, state, new Set([...visited, binding.bindingId])); + : bindingOrigin(alias, state, visited.add(binding.bindingId)); const projection = initializer.projection; return origin === undefined || !projection.every((key): key is string => typeof key === "string")