Merge remote-tracking branch 'origin/main' into codex/bug-sweep-20261015-final
This commit is contained in:
commit
610f873e91
8 files changed
+220
-187
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<string, ElectronIpcFinding[]>,
|
||||
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<string, ElectronIpcFinding[]> | undefined;
|
||||
const ipcAtEndpoint = (): Map<string, ElectronIpcFinding[]> => {
|
||||
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;
|
||||
|
||||
@@ -223,37 +223,6 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve tRPC client roots by lexical identity, excluding overwritten bindings. */
|
||||
export function* classifyParsedJavaScriptRpcClientRootsSteps(
|
||||
file: ParsedJavaScriptSource,
|
||||
): Generator<void, ReadonlySet<t.Node>> {
|
||||
const state = createState(file.program);
|
||||
yield* collectDefinitionsSteps(file.program, state);
|
||||
const roots = new Set<t.Node>();
|
||||
const clients = new Map<string, boolean>();
|
||||
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<number, JavaScriptOpenReceiverFact> =>
|
||||
completeSemanticSteps(classifyParsedJavaScriptOpenReceiversSteps(file));
|
||||
/** Lexical receiver identities that select static endpoint interpretations. */
|
||||
export interface JavaScriptReceiverFacts {
|
||||
readonly openReceivers: ReadonlyMap<number, JavaScriptOpenReceiverFact>;
|
||||
readonly rpcClientRoots: ReadonlySet<t.Node>;
|
||||
}
|
||||
|
||||
/** 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<void, ReadonlyMap<number, JavaScriptOpenReceiverFact>> {
|
||||
): JavaScriptReceiverFacts =>
|
||||
completeSemanticSteps(classifyParsedJavaScriptReceiversSteps(file));
|
||||
|
||||
/** Classify receivers while yielding during lexical and AST scans. */
|
||||
export function* classifyParsedJavaScriptReceiversSteps(
|
||||
file: ParsedJavaScriptSource,
|
||||
): Generator<void, JavaScriptReceiverFacts> {
|
||||
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<void, JavaScriptReceiverFacts> {
|
||||
const facts = new Map<number, JavaScriptOpenReceiverFact>();
|
||||
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<t.Node>();
|
||||
const clients = new Map<string, boolean>();
|
||||
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. */
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<number, JavaScriptOpenReceiverFact>,
|
||||
): 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<number, JavaScriptOpenReceiverFact>,
|
||||
): Generator<void, JavaScriptStaticAnalysis> {
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user