fix(javascript): retain static facts under projection pressure (#7986)

* fix(javascript): retain static facts under projection pressure

* fix(javascript): scope IPC ambiguity to omitted projections

* fix(javascript): preserve IPC uncertainty for omitted sources

* fix(javascript): qualify IPC pairing by wrapper coverage

* fix(javascript): retain provisional IPC wrapper matches
This commit is contained in:
morluto authored and GitHub committed 2026-10-29 14:02:35 +08:00
1 parent f60e42a9ae
commit 54f5cf7e14
13 files changed
+1174 -40

No files matched your search

@@ -27,6 +27,8 @@ export interface ElectronIpcPairingState {
readonly transmission: ElectronIpcRecord;
readonly candidates: readonly ElectronIpcRecord[];
readonly status: "paired" | "ambiguous" | "unpaired";
/** An observed match that may have unseen competitors when coverage is incomplete. */
readonly coverageLimitedCandidate: ElectronIpcRecord | null;
}
/** Flatten all per-file IPC facts into deterministic application records. */
@@ -55,6 +57,7 @@ export interface ElectronIpcContext {
export const createElectronIpcContext = (
analysis: JavaScriptArtifactAnalysis,
applicationOwner: (file: JavaScriptArtifactFile) => string | null,
coverageComplete = true,
): ElectronIpcContext => {
const records = collectElectronIpcRecords(analysis);
const owners = new Map(
@@ -81,11 +84,20 @@ export const createElectronIpcContext = (
// Unknown ownership can conceal competing dispatch contexts. Different owners
// do not prove runtime isolation, so retain ambiguity rather than absence.
const uncertain =
!coverageComplete ||
owner == null ||
matching.some((handler) => owners.get(handler.key) == null);
return {
transmission,
candidates,
coverageLimitedCandidate:
!coverageComplete &&
owner != null &&
candidates.length === 1 &&
candidates[0]?.finding.receiver === undefined &&
!matching.some((handler) => owners.get(handler.key) == null)
? (candidates[0] ?? null)
: null,
status:
!uncertain &&
candidates.length === 1 &&
@@ -1,5 +1,6 @@
import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import { IPC_COVERAGE_LIMITED_PAIRING_BASIS } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { electronCallPathId } from "./ElectronCallPathIdentity.js";
@@ -137,7 +138,21 @@ export const addElectronCallPaths = (
projected.push({ file, path, coverage, nodes });
}
}
for (const { transmission, handler } of ipc.pairings) {
const wrapperCandidates = [
...ipc.pairings.map((pairing) => ({ ...pairing, coverageLimited: false })),
...ipc.states.flatMap(({ transmission, coverageLimitedCandidate }) =>
coverageLimitedCandidate === null
? []
: [
{
transmission,
handler: coverageLimitedCandidate,
coverageLimited: true,
},
],
),
];
for (const { transmission, handler, coverageLimited } of wrapperCandidates) {
const sources = projected.filter(
(p) => p.path.entry_kind === "bridge-member" && matches(p, transmission),
);
@@ -160,15 +175,22 @@ export const addElectronCallPaths = (
operation: "join-electron-wrapper-paths",
properties: {
channel: transmission.finding.channel,
pairing_basis: "unique-exact-literal-channel",
pairing_basis: coverageLimited
? IPC_COVERAGE_LIMITED_PAIRING_BASIS
: "unique-exact-literal-channel",
resolution: "candidate",
handler_path: to.file.path,
bridge_conditions: from.path.conditions,
handler_conditions: to.path.conditions,
},
limitations: [
"The unique static channel match does not establish runtime registration, sender acceptance, or dispatch.",
],
limitations: coverageLimited
? [
"Incomplete source coverage may hide competing handlers; uniqueness of this observed channel match cannot be established.",
"The match does not establish runtime registration, sender acceptance, or dispatch.",
]
: [
"The unique static channel match does not establish runtime registration, sender acceptance, or dispatch.",
],
});
}
}
@@ -0,0 +1,77 @@
import { expect, it } from "vitest";
import { analyzeAndAdoptJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
import {
AnalysisCancelledError,
AnalysisResourceConstraintError,
} from "../../domain/analysisErrorCore.js";
import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { err, ok } from "../../domain/result.js";
const source = (path: string): JavaScriptArtifactFile => ({
path,
container_sha256: "a".repeat(64),
sha256: "b".repeat(64),
bytes: 20,
inventory_artifact_id: path,
kind: "javascript",
unpacked: false,
text: { included: true, value: "export const x = 1;" },
});
it.each([false, true])(
"marks only the earlier omitted projection when a later source %s is cancelled",
async (cancelled) => {
const files = [source("first.js"), source("second.js")];
const javascript = analyzeJavaScriptStaticSource("export const x = 1;");
const error = new AnalysisResourceConstraintError(
"analyze_javascript_application",
"memory",
"earlier projection omitted",
{},
{},
);
let call = 0;
const worker = {
analyze: async () => {
call++;
const value = { javascript, module: null, projection: null };
if (call === 1) return ok(value);
const revokedStaticProjections = [{ sourceIndex: 0, error }];
return cancelled
? err({
...value,
error: new AnalysisCancelledError(
"analyze_javascript_application",
),
revokedStaticProjections,
})
: ok({ ...value, revokedStaticProjections });
},
};
const outcome = await analyzeAndAdoptJavaScriptArtifactFiles(
{
files,
unavailable_relevant_files: [],
containers: [],
text_bytes_read: 40,
invalid_utf8_files: 0,
},
worker,
createJavaScriptSemanticGraphProjection(),
async () => {},
);
expect(outcome.analysis.files[0]).toMatchObject({
file: { path: "first.js" },
javascript: { parse_status: "complete" },
application_projection_failure: {
reason: expect.stringContaining("earlier projection omitted"),
},
});
expect(outcome.analysis.files[1]).not.toHaveProperty(
"application_projection_failure",
);
expect(outcome.interruption === null).toBe(!cancelled);
},
);
@@ -82,6 +82,7 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
readonly interruption: AnalysisError | null;
}> => {
const state = emptyArtifactAnalysis();
const sourceIndices: number[] = [];
let interruption: AnalysisError | null = null;
const selectedFileCount =
fileSet.files.length - (fileSet.source_selection?.unselected_files ?? 0);
@@ -119,6 +120,22 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
onPhase === undefined ? undefined : (phase) => onPhase(file, phase),
);
const facts = result.ok ? result.value : result.error;
for (const revoked of facts.revokedStaticProjections ?? []) {
const index = sourceIndices[revoked.sourceIndex];
const previous = index === undefined ? undefined : state.files[index];
if (previous === undefined)
throw new TypeError(
"Revoked application projection has no retained source",
);
state.files[index!] = {
...previous,
application_projection_failure: {
reason: revoked.error.message,
error: projectAnalysisError(revoked.error),
limits: revoked.error.reportedLimits,
},
};
}
const applicationProjectionFailure = facts.applicationProjectionFailure;
if (facts.projection !== null)
projection.adoptFileProjection(facts.projection);
@@ -139,6 +156,7 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
error: projectAnalysisError(error),
limits,
};
sourceIndices.push(state.files.length);
state.files.push({
file,
javascript: facts.javascript,
@@ -98,10 +98,14 @@ function* buildJavaScriptArtifactGraphInputSteps(
const applicationOwnershipUnknownContainers = new Set<string>();
const applicationModuleGapSources = new Map<string, Set<string>>();
const applicationModuleReferencesRetained = new Set<string>();
let ipcSourceGap = false;
const recordModuleGap = (file: {
path: string;
container_sha256: string;
kind?: string;
}) => {
if (file.kind === "javascript" || file.kind === undefined)
ipcSourceGap = true;
const container = applicationContainerIdentity(fileSet, file);
const sources =
applicationModuleGapSources.get(container) ?? new Set<string>();
@@ -123,11 +127,24 @@ function* buildJavaScriptArtifactGraphInputSteps(
(!("text" in file) || !file.text.included)
)
recordModuleGap(file);
// Unread HTML can conceal inline scripts with Electron IPC calls.
if (file.kind === "html" && (!("text" in file) || !file.text.included))
ipcSourceGap = true;
}
}
// Omitted module relationships can hide another launch owner. Do not expand
// retained facts beyond the projection guard merely to recover ownership.
for (const analyzed of analysis.files) {
if (
analyzed.file.kind === "javascript" &&
(analyzed.javascript === null ||
analyzed.javascript.analysis_coverage === undefined ||
analyzed.javascript.analysis_coverage.examination
.electron_call_paths_limited ||
analyzed.javascript.analysis_coverage.examination
.semantic_resource_limits.length > 0)
)
ipcSourceGap = true;
if (
analyzed.application_projection_failure !== undefined ||
analyzed.analysis_failure !== undefined ||
@@ -198,8 +215,19 @@ function* buildJavaScriptArtifactGraphInputSteps(
yield* addJavaScriptModuleRelationshipsSteps(projectionContext);
const findingLimitations =
yield* addJavaScriptStaticFindingsSteps(projectionContext);
const ipc = createElectronIpcContext(analysis, (file) =>
applicationRootPath(projectionContext, file),
const coverage = graphCoverage(context);
// Graph-wide partial coverage can describe unrelated dynamic values or
// source maps. Qualify IPC pairing by omitted JavaScript sources instead:
// their hidden handlers could compete with an observed match.
const ipc = createElectronIpcContext(
projectionContext.analysis,
(file) => applicationRootPath(projectionContext, file),
snapshot.coverage.status === "complete" &&
!ipcSourceGap &&
analysis.html_inline_scripts.length === 0 &&
!fileSet.unavailable_relevant_files.some(
({ kind }) => kind === undefined,
),
);
addElectronBoundaries(projectionContext, ipc);
yield;
@@ -207,10 +235,9 @@ function* buildJavaScriptArtifactGraphInputSteps(
yield;
addJavaScriptSourceMapOriginals(projectionContext);
yield;
const coverage = graphCoverage(context);
return {
electronSummary: summarizeElectronBoundaries(
analysis,
projectionContext.analysis,
ipc,
context.directoryPaths,
),