Preserve CommonJS literal paths and confined ESM URL semantics, including package main fallback, directory intent, Unicode identity, and present opaque modules. Parse extraction inputs at their AST/source owner, retain skipped HTML and source-map coverage, and distinguish endpoint candidates from proven receiver facts. Version structural fingerprints with computed literal contents and propagate them through worker contracts. Reviewed fixes include #1810, #1840, #2642, #2700, #2702, #2714, #2803, #2960, #2968, #2986, #3035, and #3036. Validation: 158 resolver tests passed; public CLI/MCP module lookup controls, package-main/Unicode workflows, and six builtin workflows passed in the current compiled lane. Worker schema/fingerprint regressions passed. Large public pdf.js completion and real Electron analysis remain unverified.
244 lines
8.8 KiB
TypeScript
244 lines
8.8 KiB
TypeScript
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
|
import type { ElectronIpcFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
|
|
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
|
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
|
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
|
import {
|
|
resolveArtifactPathByContext,
|
|
type ResolveArtifactPathInput,
|
|
} from "./JavaScriptArtifactPathResolution.js";
|
|
|
|
/** One IPC fact retaining its exact owning artifact for graph projection. */
|
|
export interface ElectronIpcRecord {
|
|
readonly key: string;
|
|
readonly file: JavaScriptArtifactFile;
|
|
readonly finding: ElectronIpcFinding;
|
|
}
|
|
|
|
/** An unambiguous literal renderer-to-main static pairing. */
|
|
export interface ElectronIpcPairing {
|
|
readonly transmission: ElectronIpcRecord;
|
|
readonly handler: ElectronIpcRecord;
|
|
}
|
|
|
|
/** Pairing state for one renderer transmission without overclaiming ambiguity. */
|
|
export interface ElectronIpcPairingState {
|
|
readonly transmission: ElectronIpcRecord;
|
|
readonly candidates: readonly ElectronIpcRecord[];
|
|
readonly status: "paired" | "ambiguous" | "unpaired";
|
|
}
|
|
|
|
/** Flatten all per-file IPC facts into deterministic application records. */
|
|
export const collectElectronIpcRecords = (
|
|
analysis: JavaScriptModuleArtifactAnalysis,
|
|
): readonly ElectronIpcRecord[] =>
|
|
analysis.files
|
|
.flatMap(({ file, javascript }) =>
|
|
(javascript?.electron.ipc ?? []).map((finding) => ({
|
|
key: ipcRecordKey(file.path, finding),
|
|
file,
|
|
finding,
|
|
})),
|
|
)
|
|
.sort((left, right) => compareUnicodeCodePoints(left.key, right.key));
|
|
|
|
/** Match only exact literal channels and retain ambiguous matches separately. */
|
|
export const classifyElectronIpcPairings = (
|
|
records: readonly ElectronIpcRecord[],
|
|
): readonly ElectronIpcPairingState[] => {
|
|
const handlers = records.filter(isMainHandler);
|
|
return records.filter(isPairableRendererTransmission).map((transmission) => {
|
|
const candidates =
|
|
transmission.finding.channel === null
|
|
? []
|
|
: handlers.filter(
|
|
(handler) =>
|
|
handler.finding.channel === transmission.finding.channel &&
|
|
compatibleIpcModes(transmission.finding, handler.finding),
|
|
);
|
|
return {
|
|
transmission,
|
|
candidates,
|
|
status:
|
|
candidates.length === 1
|
|
? "paired"
|
|
: candidates.length > 1
|
|
? "ambiguous"
|
|
: "unpaired",
|
|
};
|
|
});
|
|
};
|
|
|
|
/** Return only pairings supported by one unique literal main handler. */
|
|
export const unambiguousElectronIpcPairings = (
|
|
records: readonly ElectronIpcRecord[],
|
|
): readonly ElectronIpcPairing[] =>
|
|
classifyElectronIpcPairings(records).flatMap((state) => {
|
|
const handler = state.candidates[0];
|
|
return state.status === "paired" && handler !== undefined
|
|
? [{ transmission: state.transmission, handler }]
|
|
: [];
|
|
});
|
|
|
|
/** Summarize static Electron findings without treating gaps as absence. */
|
|
export const summarizeElectronBoundaries = (
|
|
analysis: JavaScriptModuleArtifactAnalysis,
|
|
directoryPaths?: ReadonlySet<string>,
|
|
): ElectronBoundarySummary => {
|
|
const files = new Map(analysis.files.map(({ file }) => [file.path, file]));
|
|
const javascript = analysis.files.flatMap(({ javascript: value }) =>
|
|
value === null ? [] : [value],
|
|
);
|
|
const windows = javascript.flatMap(
|
|
({ electron }) => electron.browser_windows,
|
|
);
|
|
const bridges = javascript.flatMap(
|
|
({ electron }) => electron.context_bridge_apis,
|
|
);
|
|
const validations = javascript.flatMap(
|
|
({ electron }) => electron.sender_validations,
|
|
);
|
|
const utilities = analysis.files.flatMap(({ file, javascript: value }) =>
|
|
(value?.electron.utility_processes ?? []).map((finding) => ({
|
|
file,
|
|
finding,
|
|
})),
|
|
);
|
|
const nativeBindings = analysis.files.flatMap(({ file, javascript: value }) =>
|
|
(value?.electron.native_addon_bindings ?? []).map((finding) => ({
|
|
file,
|
|
finding,
|
|
})),
|
|
);
|
|
const ipc = collectElectronIpcRecords(analysis);
|
|
const pairing = classifyElectronIpcPairings(ipc);
|
|
const literalChannels = new Set(
|
|
ipc.flatMap(({ finding }) =>
|
|
finding.channel === null ? [] : [finding.channel],
|
|
),
|
|
);
|
|
return {
|
|
browser_windows: windows.length,
|
|
explicit_web_preferences: windows.reduce(
|
|
(count, window) => count + window.web_preferences.length,
|
|
0,
|
|
),
|
|
preload_entrypoints: windows.filter(
|
|
({ preload_path: path }) => path !== null,
|
|
).length,
|
|
context_bridge_apis: bridges.length,
|
|
exposed_api_members: bridges.reduce(
|
|
(count, bridge) => count + bridge.members.length,
|
|
0,
|
|
),
|
|
ipc: {
|
|
operations: ipc.length,
|
|
literal_channels: literalChannels.size,
|
|
dynamic_channel_operations: ipc.filter(
|
|
({ finding }) => finding.channel === null,
|
|
).length,
|
|
renderer_transmissions: ipc.filter(isRendererTransmission).length,
|
|
renderer_listeners: ipc.filter(
|
|
({ finding }) =>
|
|
finding.side === "renderer" && finding.mode === "listen",
|
|
).length,
|
|
main_handlers: ipc.filter(isMainHandler).length,
|
|
paired_renderer_transmissions: pairing.filter(
|
|
({ status }) => status === "paired",
|
|
).length,
|
|
ambiguous_renderer_transmissions: pairing.filter(
|
|
({ status }) => status === "ambiguous",
|
|
).length,
|
|
unpaired_literal_renderer_transmissions: pairing.filter(
|
|
({ status, transmission }) =>
|
|
status === "unpaired" && transmission.finding.channel !== null,
|
|
).length,
|
|
},
|
|
sender_validation_observations: validations.length,
|
|
utility_processes: utilities.length,
|
|
resolved_utility_entrypoints: utilities.filter(
|
|
({ file, finding }) =>
|
|
finding.module_path !== null &&
|
|
resolvesToFile({
|
|
file,
|
|
specifier: finding.module_path,
|
|
context: finding.module_resolution_context,
|
|
files,
|
|
directoryPaths,
|
|
runtimePathKind: "node-entrypoint",
|
|
}),
|
|
).length,
|
|
native_addon_bindings: nativeBindings.length,
|
|
resolved_native_addon_bindings: nativeBindings.filter(({ file, finding }) =>
|
|
resolvesToFile({
|
|
file,
|
|
specifier: finding.specifier,
|
|
context: "module-specifier",
|
|
files,
|
|
directoryPaths,
|
|
kind: "native-addon",
|
|
moduleKind: finding.module_kind,
|
|
}),
|
|
).length,
|
|
};
|
|
};
|
|
|
|
const compatibleIpcModes = (
|
|
transmission: ElectronIpcFinding,
|
|
handler: ElectronIpcFinding,
|
|
): boolean =>
|
|
transmission.mode === "invoke"
|
|
? handler.mode === "handle"
|
|
: transmission.operation === "send-to-host"
|
|
? false
|
|
: transmission.mode === "send" && handler.mode === "listen";
|
|
|
|
const isRendererTransmission = ({ finding }: ElectronIpcRecord): boolean =>
|
|
finding.side === "renderer" &&
|
|
(finding.mode === "send" || finding.mode === "invoke");
|
|
|
|
const isPairableRendererTransmission = (record: ElectronIpcRecord): boolean =>
|
|
isRendererTransmission(record) && record.finding.operation !== "send-to-host";
|
|
|
|
const isMainHandler = ({ finding }: ElectronIpcRecord): boolean =>
|
|
finding.side === "main" &&
|
|
(finding.mode === "listen" || finding.mode === "handle");
|
|
|
|
interface ResolveToFileInput {
|
|
readonly file: JavaScriptArtifactFile;
|
|
readonly specifier: string;
|
|
readonly context: "module-specifier" | "filesystem-expression";
|
|
readonly files: ReadonlyMap<string, JavaScriptArtifactFile>;
|
|
readonly directoryPaths?: ReadonlySet<string> | undefined;
|
|
readonly kind?: JavaScriptArtifactFile["kind"];
|
|
readonly moduleKind?: "import" | "require";
|
|
readonly runtimePathKind?: ResolveArtifactPathInput["runtimePathKind"];
|
|
}
|
|
|
|
const resolvesToFile = (input: ResolveToFileInput): boolean => {
|
|
const resolved = resolveArtifactPathByContext({
|
|
declaredPath: input.specifier,
|
|
sourcePath: input.file.path,
|
|
context: input.context,
|
|
files: input.files,
|
|
...(input.directoryPaths === undefined
|
|
? {}
|
|
: { directoryPaths: input.directoryPaths }),
|
|
...(input.runtimePathKind === undefined
|
|
? {}
|
|
: { runtimePathKind: input.runtimePathKind }),
|
|
...(input.moduleKind === undefined ? {} : { moduleKind: input.moduleKind }),
|
|
}).resolved_path;
|
|
return (
|
|
resolved !== null &&
|
|
(input.kind === undefined || input.files.get(resolved)?.kind === input.kind)
|
|
);
|
|
};
|
|
|
|
const ipcRecordKey = (path: string, finding: ElectronIpcFinding): string => {
|
|
const identity = `${path}\0${String(finding.location.start.line)}:${String(finding.location.start.column)}\0${finding.side}\0${finding.operation}\0${finding.channel ?? finding.channel_expression}\0${JSON.stringify(finding.handler_location)}`;
|
|
return finding.invocation_location === undefined
|
|
? identity
|
|
: `${identity}\0${String(finding.invocation_location.start.line)}:${String(finding.invocation_location.start.column)}`;
|
|
};
|