From 59f528f0e5bfd780e314e929db9ebf1ad65c1303 Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Wed, 14 Oct 2026 17:59:44 -0700 Subject: [PATCH] perf(javascript): share one definitions pass and index wrapper IPC dedupe (#3779) Static analysis rebuilt the full lexical definitions index three times per file (open receivers, RPC client roots, binding lookup) and the Electron wrapper pass checked each substituted IPC registration against every prior finding with JSON.stringify, which is quadratic in registrations. Classify open receivers and RPC client roots in one traversal over a single definitions-only index, then extend that same state with ambient writes for binding lookup. Index IPC findings by side, operation, channel and location so duplicate checks only consult same-endpoint candidates. Static facts are byte-identical on the measured corpora. Refs #3670 Co-authored-by: Claude Opus 5.5 --- .../javascript/JavaScriptArtifactAnalysis.ts | 8 +- .../javascriptAnalysisWorkerProcess.ts | 14 +- .../electronWrappedCallPaths.test.ts | 16 ++ .../javascript/electronWrappedCallPaths.ts | 47 ++- .../javascript/javascriptSemanticAnalysis.ts | 271 ++++++++++-------- .../javascriptSemanticAnalysisSteps.test.ts | 10 +- .../javascript/javascriptStaticAnalysis.ts | 26 +- src/domain/webBundleAnalyzerInspection.ts | 15 +- 8 files changed, 220 insertions(+), 187 deletions(-) diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.ts b/src/application/javascript/JavaScriptArtifactAnalysis.ts index 7459d87e9..e721ca450 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.ts @@ -5,10 +5,7 @@ import { setImmediate } from "node:timers/promises"; import { resolveJavaScriptSourceMapReferences } from "../../domain/javascript/javascriptSourceMapPaths.js"; import { analyzeParsedJavaScriptStaticSourceSteps } from "../../domain/javascript/javascriptStaticAnalysis.js"; -import { - analyzeParsedJavaScriptSemanticsSteps, - classifyParsedJavaScriptOpenReceiversSteps, -} from "../../domain/javascript/javascriptSemanticAnalysis.js"; +import { analyzeParsedJavaScriptSemanticsSteps } from "../../domain/javascript/javascriptSemanticAnalysis.js"; import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js"; import { projectJavaScriptModuleSemantics } from "../../domain/javascript/javascriptModuleSemanticIr.js"; import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js"; @@ -382,12 +379,9 @@ function* analyzeArtifactFileSteps< state.parseFailures += 1; return; } - const openReceiverFacts = - yield* classifyParsedJavaScriptOpenReceiversSteps(parsed); const analysis = yield* analyzeParsedJavaScriptStaticSourceSteps( file.text.value, parsed, - openReceiverFacts, ); const staticFindings = findingCount(analysis); yield; diff --git a/src/composition/javascriptAnalysisWorkerProcess.ts b/src/composition/javascriptAnalysisWorkerProcess.ts index d1c3c134e..48c4792df 100644 --- a/src/composition/javascriptAnalysisWorkerProcess.ts +++ b/src/composition/javascriptAnalysisWorkerProcess.ts @@ -1,10 +1,7 @@ import { moduleResolutionModeForJavaScriptReference } from "../domain/javascript/javascriptModuleResolutionMode.js"; import { parseJavaScriptSource } from "../domain/javascript/javascriptSourceParser.js"; import { analyzeParsedJavaScriptStaticSourceSteps } from "../domain/javascript/javascriptStaticAnalysis.js"; -import { - analyzeParsedJavaScriptSemanticsSteps, - classifyParsedJavaScriptOpenReceiversSteps, -} from "../domain/javascript/javascriptSemanticAnalysis.js"; +import { analyzeParsedJavaScriptSemanticsSteps } from "../domain/javascript/javascriptSemanticAnalysis.js"; import { failedJavaScriptStaticAnalysis } from "../domain/javascript/javascriptStaticAnalysisHelpers.js"; import { projectJavaScriptModuleSemantics } from "../domain/javascript/javascriptModuleSemanticIr.js"; import { @@ -33,15 +30,8 @@ const analyze: JavaScriptSourceWorkerAnalyzer = async ( return null; } await callbacks.phase("static"); - const receivers = await completeJavaScriptAnalysisSteps( - classifyParsedJavaScriptOpenReceiversSteps(parsed), - ); const facts = await completeJavaScriptAnalysisSteps( - analyzeParsedJavaScriptStaticSourceSteps( - file.text.value, - parsed, - receivers, - ), + analyzeParsedJavaScriptStaticSourceSteps(file.text.value, parsed), ); const resultBudgetBytes = await callbacks.checkpoint(facts); if (facts.parse_status === "failed") return null; diff --git a/src/domain/javascript/electronWrappedCallPaths.test.ts b/src/domain/javascript/electronWrappedCallPaths.test.ts index 80d4bc7c0..2c953d0a6 100644 --- a/src/domain/javascript/electronWrappedCallPaths.test.ts +++ b/src/domain/javascript/electronWrappedCallPaths.test.ts @@ -353,6 +353,22 @@ it("retains distinct wrapper invocation ranges for one IPC API source", () => { findings[1]?.invocation_location, ); }); +it("keeps one wrapper registration when one invocation reaches it twice", () => { + const result = analyzeJavaScriptStaticSource(` + const e=require("electron"); + const copy=event=>event.sender.copy(); + function register(channel,handler){e.ipcMain.on(channel,handler);} + function twice(channel){register(channel,copy);register(channel,copy);} + twice("same:invocation"); + `); + expect( + result.electron.ipc.filter( + ({ resolution_basis, channel }) => + resolution_basis === "wrapper-substitution" && + channel === "same:invocation", + ), + ).toHaveLength(1); +}); it("keeps a directly observed renderer operation singular when tracing its exposed callback", () => { const result = analyzeJavaScriptStaticSource(` const {contextBridge,ipcRenderer}=require("electron"); diff --git a/src/domain/javascript/electronWrappedCallPaths.ts b/src/domain/javascript/electronWrappedCallPaths.ts index 3d38bbd48..38ad923f0 100644 --- a/src/domain/javascript/electronWrappedCallPaths.ts +++ b/src/domain/javascript/electronWrappedCallPaths.ts @@ -43,6 +43,19 @@ const callLabel = (node: t.Node): string => ? callLabel(node.expressions.at(-1) ?? node) : calleeName(node) || "local call"; +const ipcEndpointKey = (value: ElectronIpcFinding): string => + JSON.stringify([value.side, value.operation, value.channel, value.location]); + +const indexIpcFinding = ( + index: Map, + value: ElectronIpcFinding, +): void => { + const key = ipcEndpointKey(value); + const bucket = index.get(key); + if (bucket === undefined) index.set(key, [value]); + else bucket.push(value); +}; + interface PathContext { readonly entryNode: t.Node; readonly entry: string; @@ -149,6 +162,16 @@ export const collectElectronWrappedCallPaths = ( ) ?? context ); }; + // Duplicate checks consult only facts at the same endpoint and call site. + // Scanning every prior fact made bundles with many registrations quadratic. + let ipcByEndpoint: Map | undefined; + const ipcAtEndpoint = (): Map => { + if (ipcByEndpoint !== undefined) return ipcByEndpoint; + ipcByEndpoint = new Map(); + for (const { value } of accumulator.ipc) + indexIpcFinding(ipcByEndpoint, value); + return ipcByEndpoint; + }; const hasConditions = (value: SymbolicValue): boolean => symbolicVariants(value).some((variant) => variant.conditions?.length); function* bridgeMemberFunctions( @@ -351,24 +374,22 @@ export const collectElectronWrappedCallPaths = ( }; // A fully observed transmission stays one source fact when its callback is // traced. Registrations retain invocation identity because handlers compete. - const duplicate = accumulator.ipc.some( - ({ value }) => - value.side === finding.side && - value.operation === finding.operation && - value.channel === channel && - JSON.stringify(value.location) === JSON.stringify(location) && - ((value.resolution_basis === undefined && + const invocationKey = JSON.stringify(invocationLocation); + const duplicate = (ipcAtEndpoint().get(ipcEndpointKey(finding)) ?? []).some( + (value) => + (value.resolution_basis === undefined && (value.mode === "send" || value.mode === "invoke")) || - JSON.stringify(value.invocation_location) === - JSON.stringify(invocationLocation)), + JSON.stringify(value.invocation_location) === invocationKey, ); const findingGuarded = context.conditions.length > 0 || hasConditions(first) || guardedDispatch; if (!duplicate && !findingGuarded) { - accumulator.ipc.push({ - offset: node.start ?? -1, - value: { ...finding, resolution_basis: "wrapper-substitution" }, - }); + const value: ElectronIpcFinding = { + ...finding, + resolution_basis: "wrapper-substitution", + }; + accumulator.ipc.push({ offset: node.start ?? -1, value }); + indexIpcFinding(ipcAtEndpoint(), value); if (descriptor.mode !== "send" && descriptor.mode !== "invoke" && !fn) accumulator.unknownFindings += 1; } else if (findingGuarded) accumulator.unknownFindings += 1; diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index 79074b474..df6f4d21b 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -223,37 +223,6 @@ export function* analyzeParsedJavaScriptSemanticsSteps( }; } -/** Resolve tRPC client roots by lexical identity, excluding overwritten bindings. */ -export function* classifyParsedJavaScriptRpcClientRootsSteps( - file: ParsedJavaScriptSource, -): Generator> { - const state = createState(file.program); - yield* collectDefinitionsSteps(file.program, state); - const roots = new Set(); - const clients = new Map(); - yield* traverseJavaScriptAstSteps(file.program, { - enter: (node) => { - if (!t.isMemberExpression(node) && !t.isOptionalMemberExpression(node)) - return; - const root = node.object; - if (!t.isIdentifier(root)) return; - const binding = resolveSemanticBindingState(state, root, root.name); - if (binding === undefined) return; - let client = clients.get(binding.bindingId); - if (client === undefined) { - const initializer = semanticStableInitializer(binding, state); - client = - initializer !== undefined && - initializer.projection.length === 0 && - isJavaScriptRpcClientFactory(initializer.node); - clients.set(binding.bindingId, client); - } - if (client) roots.add(root); - }, - }); - return roots; -} - /** * Receiver identity facts for the overloaded `.open` syntax, plus * `configuration` for `.get` reads of a `getConfiguration(...)` result. @@ -268,111 +237,169 @@ export type JavaScriptOpenReceiverFact = | "configuration" | "extension-state"; -/** - * Resolve only the lexical receiver identity needed to distinguish browser - * `window.open` from XHR and global storage `.open` calls, and configuration - * `.get` reads from HTTP client calls. This deliberately builds definitions - * without running semantic value or provenance analysis. - */ -export const classifyParsedJavaScriptOpenReceivers = ( - file: ParsedJavaScriptSource, -): ReadonlyMap => - completeSemanticSteps(classifyParsedJavaScriptOpenReceiversSteps(file)); +/** Lexical receiver identities that select static endpoint interpretations. */ +export interface JavaScriptReceiverFacts { + readonly openReceivers: ReadonlyMap; + readonly rpcClientRoots: ReadonlySet; +} -/** Classify `.open` receivers while yielding during lexical and AST scans. */ -export function* classifyParsedJavaScriptOpenReceiversSteps( +/** + * Resolve only the lexical receiver identities needed to distinguish browser + * `window.open` from XHR and global storage `.open` calls, configuration + * `.get` reads from HTTP client calls, and tRPC client roots (excluding + * overwritten bindings). This deliberately builds definitions without running + * semantic value or provenance analysis. + */ +export const classifyParsedJavaScriptReceivers = ( file: ParsedJavaScriptSource, -): Generator> { +): JavaScriptReceiverFacts => + completeSemanticSteps(classifyParsedJavaScriptReceiversSteps(file)); + +/** Classify receivers while yielding during lexical and AST scans. */ +export function* classifyParsedJavaScriptReceiversSteps( + file: ParsedJavaScriptSource, +): Generator { const state = createState(file.program); yield* collectDefinitionsSteps(file.program, state); + return yield* classifyDefinitionReceiversSteps(file.program, state); +} + +/** + * Index bindings and classify receivers from one definitions pass. Receiver + * facts read the definitions-only phase, so they are classified before the + * same state is extended with ambient writes. + */ +export function* indexJavaScriptBindingsWithReceiversSteps( + program: t.Program, +): Generator< + void, + { + readonly bindings: JavaScriptSemanticAnalysisState; + readonly receivers: JavaScriptReceiverFacts; + } +> { + const state = createState(program); + yield* collectDefinitionsSteps(program, state); + const receivers = yield* classifyDefinitionReceiversSteps(program, state); + yield* indexJavaScriptAmbientWritesSteps(program, state); + return { bindings: state, receivers }; +} + +function* classifyDefinitionReceiversSteps( + program: t.Program, + state: JavaScriptSemanticAnalysisState, +): Generator { const facts = new Map(); const extensionStateReads: number[] = []; - yield* traverseJavaScriptAstSteps(file.program, { - enter: (node) => { - if ( - (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) || - (!t.isMemberExpression(node.callee) && - !t.isOptionalMemberExpression(node.callee)) - ) - return; - const method = semanticStaticPropertyKey( - node.callee.property, - node.callee.computed, + const roots = new Set(); + const clients = new Map(); + const classifyRpcRoot = (node: t.Node): void => { + if (!t.isMemberExpression(node) && !t.isOptionalMemberExpression(node)) + return; + const root = node.object; + if (!t.isIdentifier(root)) return; + const binding = resolveSemanticBindingState(state, root, root.name); + if (binding === undefined) return; + let client = clients.get(binding.bindingId); + if (client === undefined) { + const initializer = semanticStableInitializer(binding, state); + client = + initializer !== undefined && + initializer.projection.length === 0 && + isJavaScriptRpcClientFactory(initializer.node); + clients.set(binding.bindingId, client); + } + if (client) roots.add(root); + }; + const classifyOpenReceiver = (node: t.Node): void => { + if ( + (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) || + (!t.isMemberExpression(node.callee) && + !t.isOptionalMemberExpression(node.callee)) + ) + return; + const method = semanticStaticPropertyKey( + node.callee.property, + node.callee.computed, + ); + if (method === "get") { + if (isConfigurationReceiver(state, node.callee.object)) + facts.set(node.start ?? -1, "configuration"); + else if (isExtensionStateMember(node.callee.object)) + extensionStateReads.push(node.start ?? -1); + return; + } + if (method !== "open" || !t.isIdentifier(node.callee.object)) return; + const receiver = node.callee.object.name; + const binding = resolveSemanticBindingState(state, node, receiver); + if (binding === undefined) { + if (semanticResolutionBlocked(state, node, receiver)) return; + const fact = openGlobalFact(receiver); + if (fact !== undefined) facts.set(node.start ?? -1, fact); + return; + } + // A lexically bound receiver overrides ambient browser globals. Mark an + // otherwise unresolved local as local so name-based window.open + // heuristics do not hide its XHR-like overloads. + facts.set(node.start ?? -1, "local"); + if (receiver === "indexedDB" || receiver === "caches") { + facts.set( + node.start ?? -1, + receiver === "indexedDB" ? "local-indexed-db" : "local-cache-storage", ); - if (method === "get") { - if (isConfigurationReceiver(state, node.callee.object)) - facts.set(node.start ?? -1, "configuration"); - else if (isExtensionStateMember(node.callee.object)) - extensionStateReads.push(node.start ?? -1); - return; - } - if (method !== "open" || !t.isIdentifier(node.callee.object)) return; - const receiver = node.callee.object.name; - const binding = resolveSemanticBindingState(state, node, receiver); - if (binding === undefined) { - if (semanticResolutionBlocked(state, node, receiver)) return; - const fact = openGlobalFact(receiver); - if (fact !== undefined) facts.set(node.start ?? -1, fact); - return; - } - // A lexically bound receiver overrides ambient browser globals. Mark an - // otherwise unresolved local as local so name-based window.open - // heuristics do not hide its XHR-like overloads. - facts.set(node.start ?? -1, "local"); - if (receiver === "indexedDB" || receiver === "caches") { - facts.set( - node.start ?? -1, - receiver === "indexedDB" ? "local-indexed-db" : "local-cache-storage", - ); - return; - } - if (binding.mutable || binding.initializers.length !== 1) return; - const initializer = binding.initializers[0]?.node; - if (!t.isIdentifier(initializer)) return; - const initializerBinding = resolveSemanticBindingState( - state, - initializer, - initializer.name, - ); - if (initializerBinding !== undefined) { - if (initializer.name === "indexedDB" || initializer.name === "caches") { - const localFact = - initializer.name === "indexedDB" - ? "local-indexed-db" - : "local-cache-storage"; - facts.set(node.start ?? -1, localFact); - if ( - initializerBinding.mutable || - initializerBinding.initializers.length !== 1 - ) - return; - const nested = initializerBinding.initializers[0]?.node; - if (t.isIdentifier(nested)) { - const nestedFact = resolveGlobalAliasFact( - state, - initializer, - nested.name, - ); - if (nestedFact !== undefined) - facts.set(node.start ?? -1, nestedFact); - } - } else if (initializer.name === "window") { - facts.set(node.start ?? -1, "local"); + return; + } + if (binding.mutable || binding.initializers.length !== 1) return; + const initializer = binding.initializers[0]?.node; + if (!t.isIdentifier(initializer)) return; + const initializerBinding = resolveSemanticBindingState( + state, + initializer, + initializer.name, + ); + if (initializerBinding !== undefined) { + if (initializer.name === "indexedDB" || initializer.name === "caches") { + const localFact = + initializer.name === "indexedDB" + ? "local-indexed-db" + : "local-cache-storage"; + facts.set(node.start ?? -1, localFact); + if ( + initializerBinding.mutable || + initializerBinding.initializers.length !== 1 + ) + return; + const nested = initializerBinding.initializers[0]?.node; + if (t.isIdentifier(nested)) { + const nestedFact = resolveGlobalAliasFact( + state, + initializer, + nested.name, + ); + if (nestedFact !== undefined) facts.set(node.start ?? -1, nestedFact); } - return; + } else if (initializer.name === "window") { + facts.set(node.start ?? -1, "local"); } - const aliasFact = resolveGlobalAliasFact( - state, - initializer, - initializer.name, - ); - if (aliasFact !== undefined) facts.set(node.start ?? -1, aliasFact); + return; + } + const aliasFact = resolveGlobalAliasFact( + state, + initializer, + initializer.name, + ); + if (aliasFact !== undefined) facts.set(node.start ?? -1, aliasFact); + }; + yield* traverseJavaScriptAstSteps(program, { + enter: (node) => { + classifyRpcRoot(node); + classifyOpenReceiver(node); }, }); if (extensionStateReads.length > 0 && importsVsCodeModule(state)) for (const start of extensionStateReads) facts.set(start, "extension-state"); - return facts; + return { openReceivers: facts, rpcClientRoots: roots }; } /** VS Code passes extension state through ExtensionContext, not an import. */ diff --git a/src/domain/javascript/javascriptSemanticAnalysisSteps.test.ts b/src/domain/javascript/javascriptSemanticAnalysisSteps.test.ts index 633cca121..1afd5e48b 100644 --- a/src/domain/javascript/javascriptSemanticAnalysisSteps.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysisSteps.test.ts @@ -5,8 +5,8 @@ import { analyzeParsedJavaScriptSemanticsSteps, classifyParsedJavaScriptElectronBindings, classifyParsedJavaScriptElectronBindingsSteps, - classifyParsedJavaScriptOpenReceivers, - classifyParsedJavaScriptOpenReceiversSteps, + classifyParsedJavaScriptReceivers, + classifyParsedJavaScriptReceiversSteps, } from "./javascriptSemanticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { @@ -110,15 +110,15 @@ new NativeWindow(); `; const parsed = parseJavaScriptSource(text, "receivers.js"); if (parsed === null) throw new Error("Expected parsed source"); - const openSteps = classifyParsedJavaScriptOpenReceiversSteps(parsed); + const openSteps = classifyParsedJavaScriptReceiversSteps(parsed); let openStep = openSteps.next(); expect(openStep.done).toBe(false); while (openStep.done !== true) openStep = openSteps.next(); - expect([...openStep.value]).toEqual([ + expect([...openStep.value.openReceivers]).toEqual([ [text.indexOf('ambient.open("/local")'), "local"], [text.indexOf('ambient.open("/global")'), "window"], ]); - expect(openStep.value).toEqual(classifyParsedJavaScriptOpenReceivers(parsed)); + expect(openStep.value).toEqual(classifyParsedJavaScriptReceivers(parsed)); const electronSteps = classifyParsedJavaScriptElectronBindingsSteps(parsed); let electronStep = electronSteps.next(); diff --git a/src/domain/javascript/javascriptStaticAnalysis.ts b/src/domain/javascript/javascriptStaticAnalysis.ts index 165ca7fd3..0b87da864 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.ts @@ -42,10 +42,7 @@ import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.j import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; import { indexJavaScriptElectronIdentitySteps, - indexJavaScriptBindingsSteps, - classifyParsedJavaScriptRpcClientRootsSteps, - classifyParsedJavaScriptOpenReceivers, - type JavaScriptOpenReceiverFact, + indexJavaScriptBindingsWithReceiversSteps, } from "./javascriptSemanticAnalysis.js"; import { parseJavaScriptSource, @@ -67,45 +64,38 @@ export const analyzeJavaScriptStaticSource = ( const file = parseJavaScriptSource(source); return file === null ? failedJavaScriptStaticAnalysis() - : analyzeParsedJavaScriptStaticSource( - source, - file, - classifyParsedJavaScriptOpenReceivers(file), - ); + : analyzeParsedJavaScriptStaticSource(source, file); }; /** Recover static structure from an already parsed JavaScript artifact. */ export const analyzeParsedJavaScriptStaticSource = ( source: string, file: ParsedJavaScriptSource, - openReceiverFacts?: ReadonlyMap, ): JavaScriptStaticAnalysis => - completeSemanticSteps( - analyzeParsedJavaScriptStaticSourceSteps(source, file, openReceiverFacts), - ); + completeSemanticSteps(analyzeParsedJavaScriptStaticSourceSteps(source, file)); /** Static analysis that yields during its source traversal; results are unchanged. */ export function* analyzeParsedJavaScriptStaticSourceSteps( source: string, file: ParsedJavaScriptSource, - openReceiverFacts?: ReadonlyMap, ): Generator { const accumulator = createJavaScriptAnalysisAccumulator(source.length); - const bindings = yield* indexJavaScriptBindingsSteps(file.program); + // One definitions pass serves receiver classification and binding lookup. + const { bindings, receivers } = + yield* indexJavaScriptBindingsWithReceiversSteps(file.program); const electronIdentity = yield* indexJavaScriptElectronIdentitySteps( file.program, bindings, ); - const clientRoots = yield* classifyParsedJavaScriptRpcClientRootsSteps(file); yield* traverseStaticSourceSteps( file, { source, accumulator, vitePreloadDependencyTable: indexVitePreloadDependencyTable(file.program), - ...(openReceiverFacts === undefined ? {} : { openReceiverFacts }), + openReceiverFacts: receivers.openReceivers, electronBindings: electronIdentity.bindings, - rpcScan: createJavaScriptRpcScan(source, clientRoots), + rpcScan: createJavaScriptRpcScan(source, receivers.rpcClientRoots), }, bindings, ); diff --git a/src/domain/webBundleAnalyzerInspection.ts b/src/domain/webBundleAnalyzerInspection.ts index 2271d2166..93a4f9c70 100644 --- a/src/domain/webBundleAnalyzerInspection.ts +++ b/src/domain/webBundleAnalyzerInspection.ts @@ -4,14 +4,8 @@ import { sanitizeEndpointCandidate } from "./browserObservation.js"; import { compositeKey } from "./unicodeCodePointOrder.js"; import type { WebPageInspection } from "./browserObservationSchemas.js"; import type { WebBundleAnalysis } from "./webBundleAnalysis.js"; -import { - completeSemanticSteps, - traverseJavaScriptAst, -} from "./javascript/javascriptSemanticTraversal.js"; -import { - classifyParsedJavaScriptOpenReceivers, - classifyParsedJavaScriptRpcClientRootsSteps, -} from "./javascript/javascriptSemanticAnalysis.js"; +import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js"; +import { classifyParsedJavaScriptReceivers } from "./javascript/javascriptSemanticAnalysis.js"; import type { JavaScriptOpenReceiverFact } from "./javascript/javascriptSemanticAnalysis.js"; import { semanticStaticPropertyName } from "./javascript/javascriptAstValues.js"; import { @@ -67,10 +61,11 @@ export const analyzeScript = ( if ((file.errors?.length ?? 0) > 0) accumulator.recoveredScripts.push(script.script_key); detectVendorFingerprints(script, accumulator); - const openReceiverFacts = classifyParsedJavaScriptOpenReceivers(file); + const receivers = classifyParsedJavaScriptReceivers(file); + const openReceiverFacts = receivers.openReceivers; const rpc = createJavaScriptRpcScan( script.source.artifact.text, - completeSemanticSteps(classifyParsedJavaScriptRpcClientRootsSteps(file)), + receivers.rpcClientRoots, ); traverseJavaScriptAst(file, { enter: (node) => {