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) => {