mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
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:
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,
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user