mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
fix(javascript): analyze first-party sources before dependencies (#9341)
* fix(javascript): analyze first-party sources before dependencies Every source in one application analysis charges the same parent result budget, and sources were offered in lexical inventory order. On an Electron app.asar, node_modules/ sorts before an electron-vite out-*/ directory, so dependency files spent the budget and every first-party main, preload, and renderer file had its semantic analysis not started and its application projection rejected. The summary then reported zero windows, preloads, and bridges while electron_semantics still said "recovered". Offer application JavaScript before JavaScript under node_modules; reported files keep inventory order. Count Electron boundary facts only from files whose projection was admitted, and report electron_semantics "partial" (with a limitation naming the count) when an Electron-loading source was not fully examined or reported. Fixes #9321 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): treat limited Electron examination as partial coverage A static pass can stop early at the Electron wrapper expansion budget or another resource limit without failing the worker. Such a source carries no analysis or projection failure, so its retained IPC or bridge facts still reported electron_semantics "recovered". Read the owned examination status and count a limited Electron-loading source as incomplete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
7 files changed
+418
-44
No files matched your search
@@ -6,7 +6,10 @@ import {
|
||||
ELECTRON_REFERENCE_KINDS,
|
||||
} from "../../domain/javascript/javascriptElectronMemberWrites.js";
|
||||
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import type {
|
||||
AnalyzedJavaScriptArtifactFile,
|
||||
JavaScriptArtifactAnalysis,
|
||||
} from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
resolveArtifactPathByContext,
|
||||
@@ -128,7 +131,7 @@ export const createElectronIpcContext = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether any analyzed source loads an Electron module.
|
||||
* Whether one analyzed source loads an Electron module.
|
||||
*
|
||||
* `analysis_coverage.applicability.electron` is the domain owner of this
|
||||
* question: it already excludes a `type-import`, a `require.resolve` that only
|
||||
@@ -139,42 +142,51 @@ export const createElectronIpcContext = (
|
||||
* Historical evidence may predate that field, so the retained references answer
|
||||
* the same question for those files.
|
||||
*/
|
||||
export const electronModuleReferenced = (
|
||||
analysis: JavaScriptArtifactAnalysis,
|
||||
): boolean =>
|
||||
analysis.files.some(({ javascript, semantic }) => {
|
||||
const applicability = javascript?.analysis_coverage?.applicability.electron;
|
||||
if (applicability !== undefined) return applicability === "observed";
|
||||
const links = semantic?.ir.moduleLinks;
|
||||
if (
|
||||
links !== undefined &&
|
||||
links.some(
|
||||
({ specifier, kind, loaderAuthority }) =>
|
||||
kind !== "type-import" &&
|
||||
loaderAuthority !== "candidate" &&
|
||||
specifier !== null &&
|
||||
ELECTRON_MODULE.test(specifier),
|
||||
)
|
||||
)
|
||||
return true;
|
||||
return (javascript?.references ?? []).some(
|
||||
({ specifier, kind, operation, loader_authority }) =>
|
||||
operation === "load" &&
|
||||
loader_authority !== "candidate" &&
|
||||
ELECTRON_REFERENCE_KINDS.has(kind) &&
|
||||
const electronModuleReferencedBy = ({
|
||||
javascript,
|
||||
semantic,
|
||||
}: AnalyzedJavaScriptArtifactFile): boolean => {
|
||||
const applicability = javascript?.analysis_coverage?.applicability.electron;
|
||||
if (applicability !== undefined) return applicability === "observed";
|
||||
const links = semantic?.ir.moduleLinks;
|
||||
if (
|
||||
links !== undefined &&
|
||||
links.some(
|
||||
({ specifier, kind, loaderAuthority }) =>
|
||||
kind !== "type-import" &&
|
||||
loaderAuthority !== "candidate" &&
|
||||
specifier !== null &&
|
||||
ELECTRON_MODULE.test(specifier),
|
||||
);
|
||||
});
|
||||
)
|
||||
)
|
||||
return true;
|
||||
return (javascript?.references ?? []).some(
|
||||
({ specifier, kind, operation, loader_authority }) =>
|
||||
operation === "load" &&
|
||||
loader_authority !== "candidate" &&
|
||||
ELECTRON_REFERENCE_KINDS.has(kind) &&
|
||||
specifier !== null &&
|
||||
ELECTRON_MODULE.test(specifier),
|
||||
);
|
||||
};
|
||||
|
||||
/** Every Electron boundary fact family the graph reports. */
|
||||
/** Whether any analyzed source loads an Electron module. */
|
||||
export const electronModuleReferenced = (
|
||||
analysis: JavaScriptArtifactAnalysis,
|
||||
): boolean => analysis.files.some(electronModuleReferencedBy);
|
||||
|
||||
/**
|
||||
* Every Electron boundary fact family the graph reports. A rejected
|
||||
* application projection keeps its static facts on the file node only, so
|
||||
* those facts are not part of the reported boundary surface.
|
||||
*/
|
||||
export const electronBoundaryFactCount = (
|
||||
analysis: JavaScriptArtifactAnalysis,
|
||||
): number =>
|
||||
analysis.files.reduce(
|
||||
(count, { javascript }) =>
|
||||
(count, { javascript, application_projection_failure }) =>
|
||||
count +
|
||||
(javascript === null
|
||||
(javascript === null || application_projection_failure !== undefined
|
||||
? 0
|
||||
: javascript.electron.browser_windows.filter(provenElectronFinding)
|
||||
.length +
|
||||
@@ -219,18 +231,57 @@ export const hasCandidateElectronFindings = (
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether the Electron boundary summary came back empty. `undefined` means no
|
||||
* analyzed source names Electron, which is unknown rather than absent: unread
|
||||
* sources and dynamic module names can hide its use.
|
||||
* A completed static pass can still stop early, for example at the Electron
|
||||
* wrapper expansion budget. Syntax recovery and examination facts jointly own that verdict.
|
||||
*/
|
||||
const electronExaminationLimited = ({
|
||||
javascript,
|
||||
}: AnalyzedJavaScriptArtifactFile): boolean => {
|
||||
if (javascript === null) return false;
|
||||
const status = javascript.analysis_coverage?.examination.status;
|
||||
const syntax = javascript.analysis_coverage?.syntax.status;
|
||||
return (
|
||||
javascript.parse_status !== "complete" ||
|
||||
(syntax !== undefined && syntax !== "complete") ||
|
||||
(status !== undefined && status !== "complete")
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Electron-loading sources whose boundary facts may be missing from the
|
||||
* report: their analysis was interrupted or not started, their application
|
||||
* projection was rejected for the result budget, or their static examination
|
||||
* stopped at a resource limit or retained only partial syntax/semantics.
|
||||
*/
|
||||
export const electronIncompleteSourceCount = (
|
||||
analysis: JavaScriptArtifactAnalysis,
|
||||
): number =>
|
||||
analysis.files.filter(
|
||||
(file) =>
|
||||
(file.analysis_failure !== undefined ||
|
||||
file.application_projection_failure !== undefined ||
|
||||
electronExaminationLimited(file)) &&
|
||||
electronModuleReferencedBy(file),
|
||||
).length;
|
||||
|
||||
/**
|
||||
* Whether the Electron boundary summary came back empty or incomplete.
|
||||
* `undefined` means no analyzed source names Electron, which is unknown rather
|
||||
* than absent: unread sources and dynamic module names can hide its use.
|
||||
* `partial` means some facts were reported while other Electron-loading
|
||||
* sources were not fully examined or reported, so missing windows, preloads,
|
||||
* bridges, or IPC operations from those sources are unknown.
|
||||
*/
|
||||
export const electronSemanticsState = (
|
||||
analysis: JavaScriptArtifactAnalysis,
|
||||
): "recovered" | "empty" | undefined =>
|
||||
): "recovered" | "partial" | "empty" | undefined =>
|
||||
electronModuleReferenced(analysis) === false
|
||||
? undefined
|
||||
: electronBoundaryFactCount(analysis) === 0
|
||||
? "empty"
|
||||
: "recovered";
|
||||
: electronIncompleteSourceCount(analysis) > 0
|
||||
? "partial"
|
||||
: "recovered";
|
||||
|
||||
/** Summarize static Electron findings without treating gaps as absence. */
|
||||
export const summarizeElectronBoundaries = (
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
analyzeAndAdoptJavaScriptArtifactFiles,
|
||||
javaScriptAnalysisOrder,
|
||||
} from "./JavaScriptArtifactAnalysis.js";
|
||||
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
|
||||
import { 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;" },
|
||||
});
|
||||
|
||||
const manifest = (path: string): JavaScriptArtifactFile => ({
|
||||
...source(path),
|
||||
kind: "package-json",
|
||||
text: { included: true, value: "{}" },
|
||||
});
|
||||
|
||||
// Lexical inventory order puts `node_modules/` before an electron-vite `out-*`
|
||||
// directory, which is the shape that exhausted the result budget in #9321.
|
||||
const inventory = [
|
||||
source("node_modules/a/index.js"),
|
||||
manifest("node_modules/a/package.json"),
|
||||
source("node_modules/b/lib/yaml.js"),
|
||||
source("out-global-online/main/index.js"),
|
||||
source("out-global-online/preload/index.js"),
|
||||
manifest("package.json"),
|
||||
];
|
||||
|
||||
it("offers application JavaScript before dependency JavaScript", () => {
|
||||
expect(javaScriptAnalysisOrder(inventory).map(({ path }) => path)).toEqual([
|
||||
"node_modules/a/package.json",
|
||||
"out-global-online/main/index.js",
|
||||
"out-global-online/preload/index.js",
|
||||
"package.json",
|
||||
"node_modules/a/index.js",
|
||||
"node_modules/b/lib/yaml.js",
|
||||
]);
|
||||
// A package merely named like a dependency directory is not one.
|
||||
expect(
|
||||
javaScriptAnalysisOrder([
|
||||
source("node_modules.js"),
|
||||
source("src/node_modules_shim.js"),
|
||||
]).map(({ path }) => path),
|
||||
).toEqual(["node_modules.js", "src/node_modules_shim.js"]);
|
||||
});
|
||||
|
||||
it("spends a limited result budget on first-party sources and keeps inventory order", async () => {
|
||||
const javascript = analyzeJavaScriptStaticSource("export const x = 1;");
|
||||
const analyzed: string[] = [];
|
||||
// Admit two sources, then report the budget as spent, as the parent does
|
||||
// once its remaining result capacity is reserved for static observations.
|
||||
const worker = {
|
||||
analyze: async (file: JavaScriptArtifactFile) => {
|
||||
analyzed.push(file.path);
|
||||
const value = { javascript, module: null, projection: null };
|
||||
if (analyzed.length <= 2) return ok(value);
|
||||
return err({
|
||||
...value,
|
||||
error: new AnalysisResourceConstraintError(
|
||||
"analyze_javascript_application",
|
||||
"memory",
|
||||
`semantic analysis of ${file.path} was not started`,
|
||||
{ semantic_result_budget_bytes: 0 },
|
||||
),
|
||||
});
|
||||
},
|
||||
};
|
||||
const outcome = await analyzeAndAdoptJavaScriptArtifactFiles(
|
||||
{
|
||||
files: inventory,
|
||||
unavailable_relevant_files: [],
|
||||
containers: [],
|
||||
text_bytes_read: 120,
|
||||
invalid_utf8_files: 0,
|
||||
},
|
||||
worker,
|
||||
createJavaScriptSemanticGraphProjection(),
|
||||
async () => {},
|
||||
);
|
||||
expect(analyzed).toEqual([
|
||||
"out-global-online/main/index.js",
|
||||
"out-global-online/preload/index.js",
|
||||
"node_modules/a/index.js",
|
||||
"node_modules/b/lib/yaml.js",
|
||||
]);
|
||||
expect(outcome.analysis.files.map(({ file }) => file.path)).toEqual(
|
||||
inventory.map(({ path }) => path),
|
||||
);
|
||||
const failed = outcome.analysis.files
|
||||
.filter(({ analysis_failure }) => analysis_failure !== undefined)
|
||||
.map(({ file }) => file.path);
|
||||
expect(failed).toEqual([
|
||||
"node_modules/a/index.js",
|
||||
"node_modules/b/lib/yaml.js",
|
||||
]);
|
||||
expect(outcome.interruption).toBeNull();
|
||||
});
|
||||
|
||||
it("spends capacity in the caller's requested order for explicitly selected sources", async () => {
|
||||
const files = [
|
||||
source("node_modules/selected/index.js"),
|
||||
source("src/main.js"),
|
||||
];
|
||||
const admitted: string[] = [];
|
||||
const javascript = analyzeJavaScriptStaticSource("export const x = 1;");
|
||||
const outcome = await analyzeAndAdoptJavaScriptArtifactFiles(
|
||||
{
|
||||
files,
|
||||
unavailable_relevant_files: [],
|
||||
containers: [],
|
||||
text_bytes_read: 40,
|
||||
invalid_utf8_files: 0,
|
||||
source_selection: {
|
||||
requested_paths: files.map((file) => file.path),
|
||||
unselected_files: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
analyze: async (file) => {
|
||||
const value = { javascript, module: null, projection: null };
|
||||
admitted.push(file.path);
|
||||
return admitted.length === 1
|
||||
? ok(value)
|
||||
: err({
|
||||
...value,
|
||||
error: new AnalysisResourceConstraintError(
|
||||
"analyze_javascript_application",
|
||||
"memory",
|
||||
"result capacity exhausted",
|
||||
{ semantic_result_budget_bytes: 0 },
|
||||
),
|
||||
});
|
||||
},
|
||||
},
|
||||
createJavaScriptSemanticGraphProjection(),
|
||||
async () => {},
|
||||
);
|
||||
expect(admitted).toEqual(files.map((file) => file.path));
|
||||
expect(
|
||||
outcome.analysis.files.find(({ file }) => file.path === files[0]!.path)
|
||||
?.analysis_failure,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
outcome.analysis.files.find(({ file }) => file.path === files[1]!.path)
|
||||
?.analysis_failure,
|
||||
).toBeDefined();
|
||||
});
|
||||
@@ -97,7 +97,9 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
|
||||
const selectedFileCount =
|
||||
fileSet.files.length - (fileSet.source_selection?.unselected_files ?? 0);
|
||||
let selectedFileIndex = 0;
|
||||
for (const file of fileSet.files) {
|
||||
for (const file of fileSet.source_selection === undefined
|
||||
? javaScriptAnalysisOrder(fileSet.files)
|
||||
: fileSet.files) {
|
||||
let analysisStarted = false;
|
||||
try {
|
||||
await setImmediate();
|
||||
@@ -308,12 +310,49 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
|
||||
);
|
||||
}
|
||||
}
|
||||
restoreInventoryOrder(fileSet.files, state.files);
|
||||
return {
|
||||
analysis: finalizeArtifactAnalysis(state, preloadLimitations),
|
||||
interruption,
|
||||
};
|
||||
};
|
||||
|
||||
/** A source inside an installed dependency package, not the application. */
|
||||
const dependencySourcePath = (path: string): boolean =>
|
||||
path.split("/").slice(0, -1).includes("node_modules");
|
||||
|
||||
/**
|
||||
* Every source in one analysis charges the same parent result budget, so the
|
||||
* order sources are offered decides which ones keep semantic and application
|
||||
* facts when that budget runs out. Inventory order is lexical, which lets a
|
||||
* large `node_modules` tree spend the budget before first-party entrypoints,
|
||||
* preloads, and Electron windows are examined. Offer application JavaScript
|
||||
* first and dependency JavaScript last; metadata files keep their position.
|
||||
*/
|
||||
export const javaScriptAnalysisOrder = (
|
||||
files: readonly JavaScriptArtifactFile[],
|
||||
): JavaScriptArtifactFile[] => {
|
||||
const application: JavaScriptArtifactFile[] = [];
|
||||
const dependencies: JavaScriptArtifactFile[] = [];
|
||||
for (const file of files) {
|
||||
if (file.kind === "javascript" && dependencySourcePath(file.path))
|
||||
dependencies.push(file);
|
||||
else application.push(file);
|
||||
}
|
||||
return [...application, ...dependencies];
|
||||
};
|
||||
|
||||
/** Analysis order is an admission policy; reported files keep inventory order. */
|
||||
const restoreInventoryOrder = (
|
||||
inventory: readonly JavaScriptArtifactFile[],
|
||||
analyzed: AnalyzedJavaScriptArtifactFile[],
|
||||
): void => {
|
||||
const positions = new Map(inventory.map((file, index) => [file, index]));
|
||||
const position = ({ file }: AnalyzedJavaScriptArtifactFile): number =>
|
||||
positions.get(file) ?? inventory.length;
|
||||
analyzed.sort((left, right) => position(left) - position(right));
|
||||
};
|
||||
|
||||
const finalizeArtifactAnalysis = (
|
||||
state: MutableArtifactAnalysis,
|
||||
preloadLimitations: readonly string[],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import {
|
||||
electronIncompleteSourceCount,
|
||||
electronSemanticsState,
|
||||
createElectronIpcContext,
|
||||
hasCandidateElectronFindings,
|
||||
@@ -396,11 +397,16 @@ const graphLimitations = (
|
||||
? [
|
||||
"Electron modules are referenced but no Electron boundary fact was recovered from inert syntax. Zero IPC operations and zero BrowserWindows here mean empty coverage, not evidence that the application has none.",
|
||||
]
|
||||
: electronSemantics === "recovered"
|
||||
: electronSemantics === "partial"
|
||||
? [
|
||||
`Electron boundary facts are partial: ${String(electronIncompleteSourceCount(context.analysis))} Electron-loading source(s) were not fully examined or reported because syntax was recovered, semantics remained unresolved, analysis was interrupted, or a resource limit was reached. BrowserWindows, preloads, bridges, and IPC operations from those sources are unknown, not absent; analyze fewer sources, such as first-party main and preload files selected with source_paths.`,
|
||||
"Electron relationships are derived from inert syntax; runtime registration, reachability, defaults, and enforcement remain unproven.",
|
||||
]
|
||||
: []),
|
||||
: electronSemantics === "recovered"
|
||||
? [
|
||||
"Electron relationships are derived from inert syntax; runtime registration, reachability, defaults, and enforcement remain unproven.",
|
||||
]
|
||||
: []),
|
||||
...(ipc.some(({ finding }) => finding.channel === null)
|
||||
? [
|
||||
"Dynamic IPC channel expressions remain unresolved and are never paired by textual similarity.",
|
||||
|
||||
@@ -7,10 +7,13 @@ import { moduleResolutionModeForJavaScriptReference } from "../../domain/javascr
|
||||
import { analyzeParsedJavaScriptSemanticsSteps } from "../../domain/javascript/javascriptSemanticAnalysis.js";
|
||||
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
|
||||
import { analyzeParsedJavaScriptStaticSourceSteps } from "../../domain/javascript/javascriptStaticAnalysis.js";
|
||||
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import type {
|
||||
AnalyzedJavaScriptArtifactFile,
|
||||
JavaScriptArtifactAnalysis,
|
||||
} from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { javascriptArtifactAnalysisScopes } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { completeJavaScriptAnalysisSteps } from "./JavaScriptAnalysisControl.js";
|
||||
import { buildImmutableJavaScriptArtifactGraphSteps as buildGraph } from "./JavaScriptArtifactGraphBuilder.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
@@ -86,10 +89,19 @@ const graphFor = async (
|
||||
electronSemantics: string | undefined;
|
||||
}> => {
|
||||
const base = await analyzedEntry(source, path);
|
||||
const entry = mutate === undefined ? base : mutate(base);
|
||||
return graphForEntries([mutate === undefined ? base : mutate(base)]);
|
||||
};
|
||||
|
||||
const graphForEntries = (
|
||||
entries: readonly AnalyzedJavaScriptArtifactFile[],
|
||||
): {
|
||||
limitations: readonly string[];
|
||||
electronSemantics: string | undefined;
|
||||
browserWindows: number;
|
||||
} => {
|
||||
const analysis: JavaScriptArtifactAnalysis = {
|
||||
jsonDocuments: new JavaScriptJsonDocuments(),
|
||||
files: [entry],
|
||||
files: [...entries],
|
||||
packages: [],
|
||||
json_modules: [],
|
||||
html_scripts: [],
|
||||
@@ -102,10 +114,10 @@ const graphFor = async (
|
||||
limitations: [],
|
||||
};
|
||||
const fileSet = {
|
||||
files: [entry.file],
|
||||
files: entries.map(({ file }) => file),
|
||||
unavailable_relevant_files: [],
|
||||
containers: [],
|
||||
text_bytes_read: entry.file.bytes,
|
||||
text_bytes_read: entries.reduce((total, { file }) => total + file.bytes, 0),
|
||||
invalid_utf8_files: 0,
|
||||
};
|
||||
const steps = buildGraph(snapshot, fileSet, analysis);
|
||||
@@ -114,6 +126,7 @@ const graphFor = async (
|
||||
return {
|
||||
limitations: next.value.graph.limitations,
|
||||
electronSemantics: next.value.electronSummary.electron_semantics,
|
||||
browserWindows: next.value.electronSummary.browser_windows,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -344,3 +357,102 @@ it("classifies a bundle module by its loader, not by its name", async () => {
|
||||
);
|
||||
expect(externalUnderKey.electronSemantics).toBe("recovered");
|
||||
});
|
||||
|
||||
it("reports partial coverage when an Electron source was dropped for the result budget", async () => {
|
||||
// #9321: IPC handlers from an admitted source were reported while the
|
||||
// window source's projection was rejected, so zero windows is unknown.
|
||||
const reported = await analyzedEntry(
|
||||
`const electron = require("electron");\nelectron.ipcMain.handle("ping", () => "pong");\n`,
|
||||
"out/main/ipc.cjs",
|
||||
);
|
||||
const window = await analyzedEntry(
|
||||
`const electron = require("electron");\nnew electron.BrowserWindow({ webPreferences: { contextIsolation: true } });\n`,
|
||||
"out/main/window.cjs",
|
||||
);
|
||||
expect(window.javascript?.electron.browser_windows).toHaveLength(1);
|
||||
const complete = graphForEntries([reported, window]);
|
||||
expect(complete.electronSemantics).toBe("recovered");
|
||||
expect(complete.browserWindows).toBe(1);
|
||||
|
||||
const notStarted = new AnalysisResourceConstraintError(
|
||||
"analyze_javascript_application",
|
||||
"memory",
|
||||
"semantic analysis of out/main/window.cjs was not started",
|
||||
{ semantic_result_budget_bytes: 0 },
|
||||
);
|
||||
const dropped: AnalyzedJavaScriptArtifactFile[] = [
|
||||
{
|
||||
...window,
|
||||
application_projection_failure: {
|
||||
reason: "result budget exhausted",
|
||||
error: projectAnalysisError(notStarted),
|
||||
limits: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
...window,
|
||||
semantic: null,
|
||||
analysis_failure: {
|
||||
scope: javascriptArtifactAnalysisScopes.javascript,
|
||||
status: "interrupted",
|
||||
reason: `out/main/window.cjs: ${notStarted.message}`,
|
||||
error: projectAnalysisError(notStarted),
|
||||
limits: notStarted.reportedLimits,
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const entry of dropped) {
|
||||
const partial = graphForEntries([reported, entry]);
|
||||
expect(partial.electronSemantics).toBe("partial");
|
||||
expect(partial.limitations).not.toContain(EMPTY_COVERAGE);
|
||||
expect(partial.limitations).toContainEqual(
|
||||
expect.stringContaining(
|
||||
"Electron boundary facts are partial: 1 Electron-loading source(s)",
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("reports partial coverage when a successful static pass stopped at a limit", async () => {
|
||||
// The wrapper expansion budget ends Electron call-path discovery without
|
||||
// failing the worker, so no failure field marks the source as incomplete.
|
||||
const { electronSemantics, limitations } = await graphFor(
|
||||
`const electron = require("electron");\nelectron.ipcMain.handle("ping", () => "pong");\n`,
|
||||
(entry) => {
|
||||
const javascript = entry.javascript!;
|
||||
const coverage = javascript.analysis_coverage!;
|
||||
return {
|
||||
...entry,
|
||||
javascript: {
|
||||
...javascript,
|
||||
analysis_coverage: {
|
||||
...coverage,
|
||||
examination: {
|
||||
...coverage.examination,
|
||||
status: "limited",
|
||||
electron_call_paths_limited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
"main.cjs",
|
||||
);
|
||||
expect(electronSemantics).toBe("partial");
|
||||
expect(limitations).toContainEqual(
|
||||
expect.stringContaining(
|
||||
"Electron boundary facts are partial: 1 Electron-loading source(s)",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps recovered Electron facts partial after syntax recovery", async () => {
|
||||
const source = `import { BrowserWindow } from "electron"; new BrowserWindow({webPreferences: {sandbox: true}}); const duplicate = 1; const duplicate = 2;`;
|
||||
const entry = await analyzedEntry(source, "main.mjs");
|
||||
expect(entry.javascript?.parse_status).toBe("partial");
|
||||
expect(entry.javascript?.electron.browser_windows.length).toBeGreaterThan(0);
|
||||
expect(graphForEntries([entry])).toMatchObject({
|
||||
electronSemantics: "partial",
|
||||
browserWindows: 1,
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user