mirror of
https://github.com/morluto/rea
synced 2026-10-25 14:01:13 -04:00
refactor(javascript): own coverage facts and reuse block prefixes
Separate syntax, semantic uncertainty, examination limits and observed Electron applicability at the static producer, then preserve them through worker, graph and retained/inline summary views. Derive the established parse_status from the same facts and leave historical measurements unknown. Give semantic resource-limit classifications one schema owner. Replace repeated block-prefix searches and copies with collector-scoped syntax indexes. Keep invocation environments separate, charge indexing/replay to the existing work budget and yield during prefix work. Remove the superseded synchronous collector entrypoint. Validated producer, worker, CLI/MCP, historical and partial-coverage regressions, plus actual cancellation during indexing. Bounded authored 128/256-call controls preserved all prior facts while replacing repeated search/copy work with one index pass; this is not a throughput or agent-token benchmark.
This commit is contained in:
@@ -195,6 +195,27 @@ For supplied passive browser or Electron observations, use
|
||||
[runtime reconciliation](javascript-runtime-reconciliation.md). It compares
|
||||
existing Evidence without rerunning static analysis or contacting CDP.
|
||||
|
||||
Each newly analyzed JavaScript file retains `analysis_coverage` in its graph
|
||||
observation. `syntax` distinguishes complete parsing, recovered syntax errors,
|
||||
failed parsing and unexamined source. `semantics` records the number of observed
|
||||
unknown findings and typed uncertainty, including returned-reference widening
|
||||
and unavailable type-only compiler options. `examination` reports static semantic
|
||||
resource limits and Electron wrapper expansion limits independently. A completed
|
||||
pass does not establish complete runtime semantics. Route examination and result
|
||||
capacity remain in the separate route inventory.
|
||||
|
||||
Electron applicability is `observed` when a static Electron module reference is
|
||||
retained, and otherwise `unknown`: a source without such a reference does not
|
||||
prove that the application is inapplicable. Sources excluded by `source_paths`
|
||||
retain `not-examined` with a reason and null semantic counts. Historical Evidence
|
||||
without this facet remains unmeasured. The existing `parse_status` remains its
|
||||
conservative overall summary; finer facts do not relabel earlier partial results.
|
||||
|
||||
Summary delivery counts these facts under `summary.coverage.static_sources`.
|
||||
Read exact per-file measurements with `inspect_analysis_view`, selecting
|
||||
`kind: "javascript-facts"` and `property_keys: ["analysis_coverage"]`, or inspect
|
||||
the module item. Both views use retained Evidence and preserve source provenance.
|
||||
|
||||
## ASAR integrity
|
||||
|
||||
ASAR inventory checks Electron integrity metadata for embedded archive entries
|
||||
|
||||
@@ -130,6 +130,11 @@ describe("skipped JavaScript parse reporting", () => {
|
||||
const context = contextFor(file, analysisFor([skippedEntry(file)]));
|
||||
addJavaScriptArtifactFiles(context);
|
||||
expect(fileObservation(context).parse_status).toBe("skipped");
|
||||
expect(fileObservation(context).analysis_coverage).toMatchObject({
|
||||
syntax: { status: "not-examined", error_count: null },
|
||||
semantics: { unknown_findings: null },
|
||||
examination: { status: "not-examined" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps parse_status null for a JavaScript file never attempted", () => {
|
||||
@@ -157,6 +162,21 @@ describe("skipped JavaScript parse reporting", () => {
|
||||
const context = contextFor(file, analysisFor([entry]));
|
||||
addJavaScriptArtifactFiles(context);
|
||||
expect(fileObservation(context).parse_status).toBe(javascript.parse_status);
|
||||
expect(fileObservation(context).analysis_coverage).toEqual(
|
||||
javascript.analysis_coverage,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not reconstruct coverage from a historical overall summary", () => {
|
||||
const file = skippedFile();
|
||||
const { analysis_coverage: _coverage, ...javascript } =
|
||||
analyzeJavaScriptStaticSource(file.text.included ? file.text.value : "");
|
||||
const context = contextFor(
|
||||
file,
|
||||
analysisFor([{ file, javascript, semantic: null }]),
|
||||
);
|
||||
addJavaScriptArtifactFiles(context);
|
||||
expect(fileObservation(context)).not.toHaveProperty("analysis_coverage");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,16 +7,19 @@ import {
|
||||
partialApplicationCoverage,
|
||||
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
||||
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { javascriptArtifactAnalysisScopes } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import {
|
||||
javascriptArtifactAnalysisScopes,
|
||||
isSkippedJavaScriptAnalysis,
|
||||
} from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import { freezeJsonSnapshot } from "../../domain/immutableJson.js";
|
||||
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import { unexaminedJavaScriptStaticCoverage } from "../../domain/javascript/javascriptStaticCoverage.js";
|
||||
import type {
|
||||
JavaScriptArtifactContainer,
|
||||
JavaScriptArtifactFile,
|
||||
} from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import type { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js";
|
||||
import { isSkippedJavaScriptAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import {
|
||||
addArtifactContainsEdge,
|
||||
applicationManifestPath,
|
||||
@@ -648,6 +651,21 @@ const createFileTarget = (
|
||||
text_status: file.text.included ? "included" : file.text.reason,
|
||||
parse_status:
|
||||
javascript?.parse_status ?? (skipped ? "skipped" : null),
|
||||
...(javascript?.analysis_coverage !== undefined
|
||||
? { analysis_coverage: javascript.analysis_coverage }
|
||||
: javascript === null && file.kind === "javascript"
|
||||
? {
|
||||
analysis_coverage: unexaminedJavaScriptStaticCoverage(
|
||||
!file.text.included
|
||||
? file.text.reason === "outside-analysis-scope"
|
||||
? "Source is outside the caller-selected source_paths."
|
||||
: `JavaScript text was unavailable: ${file.text.reason}.`
|
||||
: skipped
|
||||
? "JavaScript analysis did not complete."
|
||||
: "JavaScript source analysis was not performed.",
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
json_parse_status: json?.status ?? null,
|
||||
json_top_level_keys: json?.top_level_keys ?? [],
|
||||
omitted_json_top_level_keys:
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { completeJavaScriptAnalysisSteps } from "./JavaScriptAnalysisControl.js";
|
||||
import { collectElectronWrappedCallPathsSteps } from "../../domain/javascript/electronWrappedCallPaths.js";
|
||||
import {
|
||||
indexJavaScriptBindingsWithReceiversSteps,
|
||||
indexJavaScriptElectronIdentitySteps,
|
||||
} from "../../domain/javascript/javascriptSemanticAnalysis.js";
|
||||
import { completeSemanticSteps } from "../../domain/javascript/javascriptSemanticTraversal.js";
|
||||
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
|
||||
import { createJavaScriptAnalysisAccumulator } from "../../domain/javascript/javascriptStaticAnalysisState.js";
|
||||
|
||||
it("cancels actual call-path work while its first seed indexes a long block", async () => {
|
||||
const source = `const e = require("electron"); {
|
||||
e.ipcMain.on("first", event => event.sender.copy());
|
||||
${"0;".repeat(2048)}
|
||||
}`;
|
||||
const parsed = parseJavaScriptSource(source);
|
||||
if (parsed === null) throw new Error("Expected parsed source");
|
||||
const index = completeSemanticSteps(
|
||||
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
|
||||
);
|
||||
const identity = completeSemanticSteps(
|
||||
indexJavaScriptElectronIdentitySteps(parsed.program, index.bindings),
|
||||
);
|
||||
const accumulator = createJavaScriptAnalysisAccumulator(source.length);
|
||||
const work = collectElectronWrappedCallPathsSteps(
|
||||
parsed.program,
|
||||
index.bindings,
|
||||
source,
|
||||
accumulator,
|
||||
identity,
|
||||
);
|
||||
// The first candidate has not yet been expanded: this checkpoint is inside
|
||||
// its block indexing, before the walker's completed batch is yielded.
|
||||
expect(work.next()).toEqual({ done: false, value: undefined });
|
||||
expect(accumulator.ipc).toEqual([]);
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "cancel prefix indexing" };
|
||||
controller.abort(reason);
|
||||
await expect(
|
||||
completeJavaScriptAnalysisSteps(work, controller.signal),
|
||||
).rejects.toMatchObject({ reason: "cancelled", cause: reason });
|
||||
expect(work.next()).toEqual({ done: true, value: undefined });
|
||||
expect(accumulator.ipc).toEqual([]);
|
||||
expect(accumulator.electronCallPathLimitReached).toBeUndefined();
|
||||
|
||||
// A new analysis owns a new index and can still complete the same source.
|
||||
const completed = createJavaScriptAnalysisAccumulator(source.length);
|
||||
expect(
|
||||
completeSemanticSteps(
|
||||
collectElectronWrappedCallPathsSteps(
|
||||
parsed.program,
|
||||
index.bindings,
|
||||
source,
|
||||
completed,
|
||||
identity,
|
||||
),
|
||||
),
|
||||
).toContainEqual(expect.objectContaining({ entry: "first", conditions: [] }));
|
||||
expect(completed.electronCallPathLimitReached).toBeUndefined();
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { applicationCoverageSchema } from "../javascript/javascriptApplicationEv
|
||||
import type { JavaScriptApplicationAnalysisResult } from "../javascript/javascriptApplicationAnalysis.js";
|
||||
import { javaScriptSemanticGraphInputSchema } from "../javascript/javascriptSemanticGraphSchemas.js";
|
||||
import { javaScriptSourceSelectionSchema } from "../javascript/javascriptSourceSelection.js";
|
||||
import { javaScriptStaticCoverageSchema } from "../javascript/javascriptStaticCoverage.js";
|
||||
|
||||
/** Source completeness is independent of exhausting a selected fact page. */
|
||||
export const javascriptApplicationSourceCoverageSchema = z.strictObject({
|
||||
@@ -37,3 +38,39 @@ export const javascriptApplicationSourceCoverage = (
|
||||
semantic_unknown_count: analysis.semantic_graph.unknowns.length,
|
||||
};
|
||||
};
|
||||
|
||||
/** Count only source-file observations; historical missing facts remain unmeasured. */
|
||||
export const javascriptStaticSourceCoverageSummary = (
|
||||
analysis: JavaScriptApplicationAnalysisResult,
|
||||
) => {
|
||||
const summary = {
|
||||
sources: 0,
|
||||
unmeasured_sources: 0,
|
||||
syntax: { complete: 0, recovered: 0, failed: 0, "not-examined": 0 },
|
||||
semantics: { unknown_sources: 0, unexamined_sources: 0 },
|
||||
examination: { complete: 0, limited: 0, "not-examined": 0 },
|
||||
electron_applicability: { observed: 0, unknown: 0 },
|
||||
};
|
||||
for (const node of analysis.graph.nodes) {
|
||||
for (const observation of node.observations) {
|
||||
if (observation.properties.file_kind !== "javascript") continue;
|
||||
summary.sources += 1;
|
||||
const parsed = javaScriptStaticCoverageSchema.safeParse(
|
||||
observation.properties.analysis_coverage,
|
||||
);
|
||||
if (!parsed.success) {
|
||||
summary.unmeasured_sources += 1;
|
||||
continue;
|
||||
}
|
||||
const coverage = parsed.data;
|
||||
summary.syntax[coverage.syntax.status] += 1;
|
||||
summary.examination[coverage.examination.status] += 1;
|
||||
summary.electron_applicability[coverage.applicability.electron] += 1;
|
||||
if (coverage.semantics.unknown_findings === null)
|
||||
summary.semantics.unexamined_sources += 1;
|
||||
else if (coverage.semantics.uncertainty.length > 0)
|
||||
summary.semantics.unknown_sources += 1;
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { err, ok, type Result } from "../result.js";
|
||||
import type { JavaScriptModuleReference } from "./javascriptModuleReferences.js";
|
||||
import {
|
||||
javascriptApplicationSourceCoverage,
|
||||
javascriptStaticSourceCoverageSummary,
|
||||
type JavaScriptApplicationSourceCoverage,
|
||||
} from "./javascriptApplicationCoverage.js";
|
||||
import { projectJavaScriptFacts } from "./javascriptFactsView.js";
|
||||
@@ -406,6 +407,7 @@ export const projectJavaScriptApplicationView = (
|
||||
statistics: jsonObjectSchema.parse(analysis.statistics),
|
||||
electron: jsonObjectSchema.parse(analysis.summary),
|
||||
coverage: jsonObjectSchema.parse({
|
||||
static_sources: javascriptStaticSourceCoverageSummary(analysis),
|
||||
application: analysis.graph.coverage,
|
||||
semantic: {
|
||||
...analysis.semantic_graph.coverage,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import * as t from "@babel/types";
|
||||
import { expect, it } from "vitest";
|
||||
import { ElectronCallPathBlockPrefixes } from "./electronCallPathBlockPrefixes.js";
|
||||
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
||||
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
|
||||
|
||||
it("indexes a real flat block once, retaining contextual guards in source order", () => {
|
||||
const parsed = parseJavaScriptSource(`{
|
||||
${"0;".repeat(2048)}
|
||||
if (uncertain) ordinary();
|
||||
if (flag) throw 0;
|
||||
electron.send("tail");
|
||||
}`);
|
||||
const block = parsed?.program.body[0];
|
||||
if (!t.isBlockStatement(block)) throw new Error("Expected parsed block");
|
||||
const first = block.body[0];
|
||||
const tail = block.body.at(-1);
|
||||
if (first === undefined || tail === undefined)
|
||||
throw new Error("Expected block statements");
|
||||
let examined = 0;
|
||||
const prefixes = new ElectronCallPathBlockPrefixes(() => {
|
||||
examined += 1;
|
||||
return true;
|
||||
});
|
||||
const steps = prefixes.before(block, tail);
|
||||
expect(steps.next()).toEqual({ done: false, value: undefined });
|
||||
expect(examined).toBe(1024);
|
||||
const prefix = completeSemanticSteps(steps);
|
||||
expect(examined).toBe(block.body.length);
|
||||
expect(prefix).toMatchObject({
|
||||
before: 2050,
|
||||
entries: [
|
||||
{
|
||||
position: 2048,
|
||||
fact: {
|
||||
kind: "branch",
|
||||
test: { type: "Identifier", name: "uncertain" },
|
||||
consequentExits: false,
|
||||
alternateExits: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
position: 2049,
|
||||
fact: {
|
||||
kind: "branch",
|
||||
test: { type: "Identifier", name: "flag" },
|
||||
consequentExits: true,
|
||||
alternateExits: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
for (const statement of block.body) {
|
||||
const earlier = completeSemanticSteps(prefixes.before(block, statement));
|
||||
expect(earlier?.entries).toBe(prefix?.entries);
|
||||
}
|
||||
expect(examined).toBe(block.body.length);
|
||||
expect(completeSemanticSteps(prefixes.before(block, first))?.before).toBe(0);
|
||||
});
|
||||
|
||||
it("does not publish an incomplete block index when its work budget ends", () => {
|
||||
const parsed = parseJavaScriptSource(
|
||||
`{ 0; 1; if (stop) throw 0; send("tail"); }`,
|
||||
);
|
||||
const block = parsed?.program.body[0];
|
||||
if (!t.isBlockStatement(block)) throw new Error("Expected parsed block");
|
||||
const tail = block.body.at(-1);
|
||||
if (tail === undefined) throw new Error("Expected terminal statement");
|
||||
let remaining = 2;
|
||||
const prefixes = new ElectronCallPathBlockPrefixes(() => --remaining >= 0);
|
||||
expect(completeSemanticSteps(prefixes.before(block, tail))).toBeUndefined();
|
||||
remaining = block.body.length;
|
||||
expect(completeSemanticSteps(prefixes.before(block, tail))).toMatchObject({
|
||||
before: 3,
|
||||
entries: [{ position: 2, fact: { kind: "branch" } }],
|
||||
});
|
||||
expect(remaining).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import * as t from "@babel/types";
|
||||
import { symbolicDefiniteExit } from "./javascriptSymbolicControlFlow.js";
|
||||
|
||||
/** Syntax alone can establish an exit, but never a branch's contextual truth. */
|
||||
export type ElectronStatementFallthrough =
|
||||
| { readonly kind: "exit" }
|
||||
| {
|
||||
readonly kind: "branch";
|
||||
readonly test: t.Expression;
|
||||
readonly consequentExits: boolean;
|
||||
readonly alternateExits: boolean;
|
||||
};
|
||||
|
||||
interface PrefixEntry {
|
||||
readonly position: number;
|
||||
readonly fact: ElectronStatementFallthrough;
|
||||
}
|
||||
|
||||
interface BlockIndex {
|
||||
readonly positions: WeakMap<t.Node, number>;
|
||||
readonly entries: readonly PrefixEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* One collector's immutable syntax facts. Positions replace repeated body
|
||||
* searches; ordinary fallthrough statements need no replay. Conditional tests
|
||||
* remain present even when neither branch exits, since evaluating them belongs
|
||||
* to the invocation's symbolic environment and can establish unknown coverage.
|
||||
*/
|
||||
export class ElectronCallPathBlockPrefixes {
|
||||
readonly #blocks = new WeakMap<t.BlockStatement, BlockIndex>();
|
||||
readonly #facts = new WeakMap<
|
||||
t.Statement,
|
||||
ElectronStatementFallthrough | null
|
||||
>();
|
||||
|
||||
constructor(private readonly admitVisit: () => boolean) {}
|
||||
|
||||
statement(statement: t.Statement): ElectronStatementFallthrough | null {
|
||||
const cached = this.#facts.get(statement);
|
||||
if (cached !== undefined) return cached;
|
||||
const fact: ElectronStatementFallthrough | null = symbolicDefiniteExit(
|
||||
statement,
|
||||
)
|
||||
? { kind: "exit" }
|
||||
: t.isIfStatement(statement)
|
||||
? {
|
||||
kind: "branch",
|
||||
test: statement.test,
|
||||
consequentExits: symbolicDefiniteExit(statement.consequent),
|
||||
alternateExits: symbolicDefiniteExit(statement.alternate),
|
||||
}
|
||||
: null;
|
||||
this.#facts.set(statement, fact);
|
||||
return fact;
|
||||
}
|
||||
|
||||
*before(
|
||||
block: t.BlockStatement,
|
||||
child: t.Node,
|
||||
): Generator<
|
||||
void,
|
||||
| { readonly entries: readonly PrefixEntry[]; readonly before: number }
|
||||
| undefined
|
||||
> {
|
||||
let index = this.#blocks.get(block);
|
||||
if (index === undefined) {
|
||||
const positions = new WeakMap<t.Node, number>();
|
||||
const entries: PrefixEntry[] = [];
|
||||
for (const [position, statement] of block.body.entries()) {
|
||||
if (!this.admitVisit()) return undefined;
|
||||
positions.set(statement, position);
|
||||
const fact = this.statement(statement);
|
||||
if (fact !== null) entries.push({ position, fact });
|
||||
if ((position + 1) % 1024 === 0) yield;
|
||||
}
|
||||
index = { positions, entries };
|
||||
this.#blocks.set(block, index);
|
||||
}
|
||||
const before = index.positions.get(child);
|
||||
return before === undefined
|
||||
? undefined
|
||||
: { entries: index.entries, before };
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,100 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
|
||||
it("preserves every direct observation in a long flat sibling block", () => {
|
||||
const channels = Array.from({ length: 256 }, (_, index) => `flat:${index}`);
|
||||
const result = analyzeJavaScriptStaticSource(`const e=require("electron"); {
|
||||
${channels.map((channel) => `e.ipcRenderer.send("${channel}");`).join("\n")}
|
||||
}`);
|
||||
expect(result.electron.ipc.map(({ channel }) => channel)).toEqual(channels);
|
||||
expect(result.electron.call_paths ?? []).toEqual([]);
|
||||
expect(result.parse_status).toBe("complete");
|
||||
});
|
||||
|
||||
it("keeps sibling guards, definite exits and nested shadowing separate", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`const e=require("electron"); {
|
||||
if (stop) throw new Error("stop");
|
||||
e.ipcMain.on("guarded", event => event.sender.copy());
|
||||
{
|
||||
const e = {ipcMain: {on() {}}};
|
||||
e.ipcMain.on("shadowed", event => event.sender.copy());
|
||||
}
|
||||
if (true) throw new Error("end");
|
||||
e.ipcMain.on("unreachable", event => event.sender.copy());
|
||||
}`);
|
||||
expect(
|
||||
result.electron.call_paths?.map(
|
||||
({ entry, terminal, steps, conditions }) => ({
|
||||
entry,
|
||||
terminal,
|
||||
terminalStart: steps.at(-1)?.location.start,
|
||||
conditions: conditions.map(({ expression }) => expression),
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
entry: "guarded",
|
||||
terminal: "electron.ipcMain.on",
|
||||
terminalStart: { line: 3, column: 4 },
|
||||
conditions: ["!(stop)"],
|
||||
},
|
||||
{
|
||||
entry: "guarded",
|
||||
terminal: "ipc-event.sender.copy",
|
||||
terminalStart: { line: 3, column: 37 },
|
||||
conditions: ["!(stop)"],
|
||||
},
|
||||
]);
|
||||
// Raw syntax after a definite exit remains an observation, not a reachable
|
||||
// callback path. The shadowed ordinary object provides no Electron identity.
|
||||
expect(result.electron.ipc.map(({ channel }) => channel)).toEqual([
|
||||
"guarded",
|
||||
"unreachable",
|
||||
]);
|
||||
});
|
||||
|
||||
it("evaluates shared return syntax separately for each invocation environment", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`const e=require("electron");
|
||||
function install(channel, enabled) {
|
||||
if (!enabled) return;
|
||||
e.ipcMain.on(channel, event => event.sender.copy());
|
||||
}
|
||||
install("enabled", true);
|
||||
install("disabled", false);
|
||||
install("uncertain", externalFlag);
|
||||
`);
|
||||
expect(
|
||||
result.electron.call_paths?.map(
|
||||
({ entry, terminal, steps, conditions }) => ({
|
||||
entry,
|
||||
terminal,
|
||||
invocationStart: steps[0]?.location.start,
|
||||
conditions: conditions.map(({ expression }) => expression),
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
entry: "enabled",
|
||||
terminal: "ipc-event.sender.copy",
|
||||
invocationStart: { line: 6, column: 4 },
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
entry: "uncertain",
|
||||
terminal: "electron.ipcMain.on",
|
||||
invocationStart: { line: 8, column: 4 },
|
||||
conditions: ["!(!enabled)"],
|
||||
},
|
||||
{
|
||||
entry: "uncertain",
|
||||
terminal: "ipc-event.sender.copy",
|
||||
invocationStart: { line: 8, column: 4 },
|
||||
conditions: ["!(!enabled)"],
|
||||
},
|
||||
]);
|
||||
expect(result.parse_status).toBe("partial");
|
||||
});
|
||||
|
||||
const bundle = (entry: string) => `(() => {
|
||||
const modules = {
|
||||
2(module) { module.exports = require("electron"); },
|
||||
|
||||
@@ -33,17 +33,22 @@ import {
|
||||
boundedExpression,
|
||||
contextBridgeMemberPathDepth,
|
||||
} from "./electronStaticAnalysisValues.js";
|
||||
import { argumentNode } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import {
|
||||
argumentNode,
|
||||
range,
|
||||
calleeName,
|
||||
} from "./javascriptStaticAnalysisHelpers.js";
|
||||
import {
|
||||
childNodes,
|
||||
traverseJavaScriptValueAst,
|
||||
traverseJavaScriptValueAstSteps,
|
||||
} from "./javascriptSemanticTraversal.js";
|
||||
import { range, calleeName } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import { ipcDescriptor } from "./electronStaticAnalysisIpc.js";
|
||||
import { symbolicLogicalRight } from "./javascriptSymbolicControlFlow.js";
|
||||
import {
|
||||
symbolicDefiniteExit,
|
||||
symbolicLogicalRight,
|
||||
} from "./javascriptSymbolicControlFlow.js";
|
||||
ElectronCallPathBlockPrefixes,
|
||||
type ElectronStatementFallthrough,
|
||||
} from "./electronCallPathBlockPrefixes.js";
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
|
||||
const callLabel = (node: t.Node): string =>
|
||||
@@ -227,16 +232,19 @@ interface PathContext {
|
||||
}
|
||||
|
||||
/** Substitutes inert wrapper syntax; every emitted path remains a candidate. */
|
||||
export const collectElectronWrappedCallPaths = (
|
||||
export function* collectElectronWrappedCallPathsSteps(
|
||||
program: t.Program,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
source: string,
|
||||
accumulator: JavaScriptAnalysisAccumulator,
|
||||
identity: JavaScriptElectronIdentity,
|
||||
): readonly ElectronCallPath[] => {
|
||||
): Generator<void, readonly ElectronCallPath[]> {
|
||||
// Bounds repeated interprocedural expansion independently of bundle bytes.
|
||||
// Exhaustion is reported as unknown coverage, never as a negative finding.
|
||||
let remainingVisits = 1_000_000;
|
||||
const prefixes = new ElectronCallPathBlockPrefixes(
|
||||
() => --remainingVisits >= 0,
|
||||
);
|
||||
const resolver = new JavaScriptSymbolicBindings(state, program, identity);
|
||||
const paths: ElectronCallPath[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -282,28 +290,26 @@ export const collectElectronWrappedCallPaths = (
|
||||
};
|
||||
};
|
||||
const sequentialFallthrough = (
|
||||
statement: t.Statement,
|
||||
fact: ElectronStatementFallthrough | null,
|
||||
env: SymbolicEnvironment,
|
||||
context: PathContext,
|
||||
): { readonly continues: boolean; readonly context: PathContext } => {
|
||||
if (symbolicDefiniteExit(statement)) return { continues: false, context };
|
||||
if (!t.isIfStatement(statement)) return { continues: true, context };
|
||||
const truth = symbolicTruth(resolver.evaluate(statement.test, env));
|
||||
if (fact === null) return { continues: true, context };
|
||||
if (fact.kind === "exit") return { continues: false, context };
|
||||
const truth = symbolicTruth(resolver.evaluate(fact.test, env));
|
||||
if (truth !== undefined)
|
||||
return symbolicDefiniteExit(
|
||||
truth ? statement.consequent : statement.alternate,
|
||||
)
|
||||
return (truth ? fact.consequentExits : fact.alternateExits)
|
||||
? { continues: false, context }
|
||||
: { continues: true, context };
|
||||
if (symbolicDefiniteExit(statement.consequent))
|
||||
if (fact.consequentExits)
|
||||
return {
|
||||
continues: true,
|
||||
context: condition(context, statement.test, false),
|
||||
context: condition(context, fact.test, false),
|
||||
};
|
||||
if (symbolicDefiniteExit(statement.alternate))
|
||||
if (fact.alternateExits)
|
||||
return {
|
||||
continues: true,
|
||||
context: condition(context, statement.test),
|
||||
context: condition(context, fact.test),
|
||||
};
|
||||
return { continues: true, context };
|
||||
};
|
||||
@@ -1148,7 +1154,11 @@ export const collectElectronWrappedCallPaths = (
|
||||
let next = context;
|
||||
for (const statement of node.body) {
|
||||
walk(statement, env, next);
|
||||
const fallthrough = sequentialFallthrough(statement, env, next);
|
||||
const fallthrough = sequentialFallthrough(
|
||||
prefixes.statement(statement),
|
||||
env,
|
||||
next,
|
||||
);
|
||||
if (!fallthrough.continues) break;
|
||||
next = fallthrough.context;
|
||||
}
|
||||
@@ -1194,70 +1204,98 @@ export const collectElectronWrappedCallPaths = (
|
||||
call(node, env, context);
|
||||
for (const child of childNodes(node)) walk(child, env, context);
|
||||
}
|
||||
traverseJavaScriptValueAst(program, state.erasedSyntaxNodes, {
|
||||
enter: (node) => {
|
||||
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
|
||||
return;
|
||||
const isolatedSeed = t.isStringLiteral(node.arguments[0])
|
||||
? undefined
|
||||
: isolatedBridgeCall(node);
|
||||
if (!t.isStringLiteral(node.arguments[0]) && isolatedSeed === undefined)
|
||||
return;
|
||||
const env = isolatedSeed?.env ?? resolver.environmentAt(node);
|
||||
let context: PathContext = {
|
||||
entryNode: node,
|
||||
calls: [],
|
||||
entry: "",
|
||||
entryKind: null,
|
||||
channel: null,
|
||||
steps: [],
|
||||
conditions: [],
|
||||
active: new Set(),
|
||||
};
|
||||
for (
|
||||
let child: t.Node = node, parent = state.parentsByNode.get(child);
|
||||
parent && !t.isFunction(parent);
|
||||
child = parent, parent = state.parentsByNode.get(child)
|
||||
) {
|
||||
if (t.isIfStatement(parent) || t.isConditionalExpression(parent)) {
|
||||
if (child === parent.test) continue;
|
||||
const truth = symbolicTruth(resolver.evaluate(parent.test, env));
|
||||
const positive = child === parent.consequent;
|
||||
if (truth !== undefined && truth !== positive) return;
|
||||
if (truth === undefined)
|
||||
context = condition(context, parent.test, positive);
|
||||
} else if (t.isLogicalExpression(parent) && child === parent.right) {
|
||||
const right = symbolicLogicalRight(
|
||||
parent.operator,
|
||||
resolver.evaluate(parent.left, env),
|
||||
let prefixVisits = 0;
|
||||
function* seed(
|
||||
node: t.CallExpression | t.OptionalCallExpression,
|
||||
): Generator<void, void> {
|
||||
if (remainingVisits < 0) return;
|
||||
const isolatedSeed = t.isStringLiteral(node.arguments[0])
|
||||
? undefined
|
||||
: isolatedBridgeCall(node);
|
||||
if (!t.isStringLiteral(node.arguments[0]) && isolatedSeed === undefined)
|
||||
return;
|
||||
const env = isolatedSeed?.env ?? resolver.environmentAt(node);
|
||||
let context: PathContext = {
|
||||
entryNode: node,
|
||||
calls: [],
|
||||
entry: "",
|
||||
entryKind: null,
|
||||
channel: null,
|
||||
steps: [],
|
||||
conditions: [],
|
||||
active: new Set(),
|
||||
};
|
||||
for (
|
||||
let child: t.Node = node, parent = state.parentsByNode.get(child);
|
||||
parent && !t.isFunction(parent);
|
||||
child = parent, parent = state.parentsByNode.get(child)
|
||||
) {
|
||||
if (--remainingVisits < 0) return;
|
||||
if (++prefixVisits % 1024 === 0) yield;
|
||||
if (t.isIfStatement(parent) || t.isConditionalExpression(parent)) {
|
||||
if (child === parent.test) continue;
|
||||
const truth = symbolicTruth(resolver.evaluate(parent.test, env));
|
||||
const positive = child === parent.consequent;
|
||||
if (truth !== undefined && truth !== positive) return;
|
||||
if (truth === undefined)
|
||||
context = condition(context, parent.test, positive);
|
||||
} else if (t.isLogicalExpression(parent) && child === parent.right) {
|
||||
const right = symbolicLogicalRight(
|
||||
parent.operator,
|
||||
resolver.evaluate(parent.left, env),
|
||||
);
|
||||
if (right === false) return;
|
||||
if (right === undefined)
|
||||
context = condition(
|
||||
context,
|
||||
parent.left,
|
||||
parent.operator === "??" ? "nullish" : parent.operator === "&&",
|
||||
);
|
||||
if (right === false) return;
|
||||
if (right === undefined)
|
||||
context = condition(
|
||||
context,
|
||||
parent.left,
|
||||
parent.operator === "??" ? "nullish" : parent.operator === "&&",
|
||||
);
|
||||
} else if (t.isBlockStatement(parent)) {
|
||||
const index = parent.body.findIndex(
|
||||
(statement) => statement === child,
|
||||
);
|
||||
if (index >= 0) {
|
||||
for (const statement of parent.body.slice(0, index)) {
|
||||
const fallthrough = sequentialFallthrough(
|
||||
statement,
|
||||
env,
|
||||
context,
|
||||
);
|
||||
if (!fallthrough.continues) return;
|
||||
context = fallthrough.context;
|
||||
}
|
||||
} else if (t.isBlockStatement(parent)) {
|
||||
const prefix = yield* prefixes.before(parent, child);
|
||||
if (remainingVisits < 0) return;
|
||||
if (prefix !== undefined) {
|
||||
for (const entry of prefix.entries) {
|
||||
if (entry.position >= prefix.before) break;
|
||||
if (--remainingVisits < 0) return;
|
||||
if (++prefixVisits % 1024 === 0) yield;
|
||||
const fallthrough = sequentialFallthrough(entry.fact, env, context);
|
||||
if (!fallthrough.continues) return;
|
||||
context = fallthrough.context;
|
||||
}
|
||||
}
|
||||
}
|
||||
call(node, env, context, isolatedSeed?.callee);
|
||||
}
|
||||
call(node, env, context, isolatedSeed?.callee);
|
||||
}
|
||||
// The shared walker supplies one bounded batch at a time. Prefix work must
|
||||
// run outside its synchronous callback so cancellation can interrupt a large
|
||||
// block index or a genuinely long sequence of contextual branch facts.
|
||||
const seeds: (t.CallExpression | t.OptionalCallExpression)[] = [];
|
||||
const traversal = traverseJavaScriptValueAstSteps(
|
||||
program,
|
||||
state.erasedSyntaxNodes,
|
||||
{
|
||||
enter: (node) => {
|
||||
if (t.isCallExpression(node) || t.isOptionalCallExpression(node))
|
||||
seeds.push(node);
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
try {
|
||||
for (;;) {
|
||||
const batch = traversal.next();
|
||||
for (const node of seeds) {
|
||||
yield* seed(node);
|
||||
if (remainingVisits < 0) break;
|
||||
}
|
||||
seeds.length = 0;
|
||||
if (batch.done || remainingVisits < 0) break;
|
||||
yield;
|
||||
}
|
||||
} finally {
|
||||
traversal.return();
|
||||
}
|
||||
recordBridgeExposures();
|
||||
if (remainingVisits < 0 || resolver.truncated) {
|
||||
accumulator.unknownFindings += 1;
|
||||
@@ -1265,4 +1303,4 @@ export const collectElectronWrappedCallPaths = (
|
||||
}
|
||||
if (resolver.unresolved) accumulator.unknownFindings += 1;
|
||||
return paths;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ it("preserves possible ambient effects and discloses projection widening in sour
|
||||
expect(ir.limitations).toContain(RETURNED_REFERENCE_WIDENING_LIMITATION);
|
||||
const facts = analyzeJavaScriptStaticSource(source);
|
||||
expect(facts.limitations).toContain(RETURNED_REFERENCE_WIDENING_LIMITATION);
|
||||
expect(facts.analysis_coverage?.semantics.uncertainty).toContain(
|
||||
"returned-reference-widening",
|
||||
);
|
||||
expect(facts.endpoints).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ value: "/preserved", mechanism: "call:fetch" }),
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** Primitive values admitted into the constant lattice. */
|
||||
export type JavaScriptSemanticPrimitive = string | number | boolean | null;
|
||||
|
||||
/** Resource bound that prevented an exact semantic value. */
|
||||
export type JavaScriptSemanticResourceLimit =
|
||||
| "primitive-candidates"
|
||||
| "primitive-bytes"
|
||||
| "expression-depth"
|
||||
| "getter-expansion"
|
||||
| "alias-selections";
|
||||
export const javaScriptSemanticResourceLimitSchema = z.enum([
|
||||
"primitive-candidates",
|
||||
"primitive-bytes",
|
||||
"expression-depth",
|
||||
"getter-expansion",
|
||||
"alias-selections",
|
||||
]);
|
||||
|
||||
/** Resource-limit classifications shared by static facts and semantic values. */
|
||||
export type JavaScriptSemanticResourceLimit = z.infer<
|
||||
typeof javaScriptSemanticResourceLimitSchema
|
||||
>;
|
||||
|
||||
type JavaScriptSemanticObjectValue = {
|
||||
readonly status: "object";
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as t from "@babel/types";
|
||||
import { JavaScriptRouteScan } from "./javascriptRouteScan.js";
|
||||
import type { JavaScriptRouteSourceFacts } from "./javascriptRouteFacts.js";
|
||||
import { RETURNED_REFERENCE_WIDENING_LIMITATION } from "./javascriptSemanticCoverage.js";
|
||||
import { collectElectronWrappedCallPaths } from "./electronWrappedCallPaths.js";
|
||||
import { collectElectronWrappedCallPathsSteps } from "./electronWrappedCallPaths.js";
|
||||
import {
|
||||
ELECTRON_IDENTITY_LIMITATION,
|
||||
ELECTRON_MODULE,
|
||||
@@ -76,6 +76,10 @@ import {
|
||||
isTypeOnlyImportDeclaration,
|
||||
semanticImportEqualsSpecifier,
|
||||
} from "./javascriptModuleDeclaration.js";
|
||||
import {
|
||||
javaScriptStaticParseStatus,
|
||||
type JavaScriptStaticCoverage,
|
||||
} from "./javascriptStaticCoverage.js";
|
||||
|
||||
/** Parse one JavaScript artifact and recover static structure only. */
|
||||
export const analyzeJavaScriptStaticSource = (
|
||||
@@ -154,7 +158,7 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
|
||||
finishPartialNetworkEndpoints(findings, bindings, index);
|
||||
finishSqliteStorageCalls(findings, bindings, index);
|
||||
}
|
||||
accumulator.electronCallPaths = collectElectronWrappedCallPaths(
|
||||
accumulator.electronCallPaths = yield* collectElectronWrappedCallPathsSteps(
|
||||
file.program,
|
||||
bindings,
|
||||
source,
|
||||
@@ -191,6 +195,43 @@ const finalizeStaticAnalysis = (
|
||||
routes: JavaScriptRouteSourceFacts,
|
||||
): JavaScriptStaticAnalysis => {
|
||||
const parserErrors = file.errors.length;
|
||||
const typeOnlySideEffect = hasUncertainTypeOnlyModuleSideEffect(file.program);
|
||||
const electronObserved = accumulator.references.some(
|
||||
({ value }) =>
|
||||
value.specifier !== null && ELECTRON_MODULE.test(value.specifier),
|
||||
);
|
||||
const resourceLimits = [...bindings.valueResourceLimits].sort();
|
||||
const coverage: JavaScriptStaticCoverage = {
|
||||
syntax:
|
||||
parserErrors === 0
|
||||
? { status: "complete", error_count: 0 }
|
||||
: { status: "recovered", error_count: parserErrors },
|
||||
semantics: {
|
||||
unknown_findings: accumulator.unknownFindings,
|
||||
uncertainty: [
|
||||
...(accumulator.unknownFindings > 0
|
||||
? ["dynamic-findings" as const]
|
||||
: []),
|
||||
...(bindings.returnedReferenceWidenings.size > 0
|
||||
? ["returned-reference-widening" as const]
|
||||
: []),
|
||||
...(typeOnlySideEffect
|
||||
? ["type-only-module-side-effect" as const]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
examination: {
|
||||
status:
|
||||
resourceLimits.length > 0 ||
|
||||
accumulator.electronCallPathLimitReached === true
|
||||
? "limited"
|
||||
: "complete",
|
||||
semantic_resource_limits: resourceLimits,
|
||||
electron_call_paths_limited:
|
||||
accumulator.electronCallPathLimitReached === true,
|
||||
},
|
||||
applicability: { electron: electronObserved ? "observed" : "unknown" },
|
||||
};
|
||||
const limitations = [
|
||||
...(bindings.valueResourceLimits.has("alias-selections")
|
||||
? [semanticResourceLimitReason("alias-selections")]
|
||||
@@ -218,26 +259,17 @@ const finalizeStaticAnalysis = (
|
||||
: [
|
||||
"One or more static keys, expressions, or Electron boundary values were dynamic and remain unknown.",
|
||||
]),
|
||||
...(hasUncertainTypeOnlyModuleSideEffect(file.program)
|
||||
...(typeOnlySideEffect
|
||||
? [
|
||||
"Compiler options are unavailable; inline type-only imports or re-exports may preserve a module side-effect dependency.",
|
||||
]
|
||||
: []),
|
||||
...(accumulator.references.some(
|
||||
({ value }) =>
|
||||
value.specifier !== null && ELECTRON_MODULE.test(value.specifier),
|
||||
)
|
||||
? [ELECTRON_IDENTITY_LIMITATION]
|
||||
: []),
|
||||
...(electronObserved ? [ELECTRON_IDENTITY_LIMITATION] : []),
|
||||
"JavaScript syntax was parsed as data and was never evaluated.",
|
||||
];
|
||||
return {
|
||||
parse_status:
|
||||
parserErrors > 0 ||
|
||||
accumulator.unknownFindings > 0 ||
|
||||
bindings.valueResourceLimits.has("alias-selections")
|
||||
? "partial"
|
||||
: "complete",
|
||||
parse_status: javaScriptStaticParseStatus(coverage),
|
||||
analysis_coverage: coverage,
|
||||
parse_error_count: parserErrors,
|
||||
visited_ast_nodes: accumulator.visitedNodes,
|
||||
references: finalizeLocatedFindings(
|
||||
|
||||
@@ -23,10 +23,17 @@ import {
|
||||
compareUnicodeCodePoints,
|
||||
compositeKey,
|
||||
} from "../unicodeCodePointOrder.js";
|
||||
import { unexaminedJavaScriptStaticCoverage } from "./javascriptStaticCoverage.js";
|
||||
|
||||
/** Explicit result for source text that Babel cannot parse. */
|
||||
export const failedJavaScriptStaticAnalysis = (): JavaScriptStaticAnalysis => ({
|
||||
parse_status: "failed",
|
||||
analysis_coverage: {
|
||||
...unexaminedJavaScriptStaticCoverage(
|
||||
"JavaScript source could not be parsed.",
|
||||
),
|
||||
syntax: { status: "failed", error_count: null },
|
||||
},
|
||||
parse_error_count: 1,
|
||||
visited_ast_nodes: 0,
|
||||
references: [],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ElectronStaticFindings } from "./electronStaticAnalysisTypes.js";
|
||||
import type { JavaScriptRouteSourceFacts } from "./javascriptRouteFacts.js";
|
||||
import type { JavaScriptStaticCoverage } from "./javascriptStaticCoverage.js";
|
||||
|
||||
/** Token semantics shared by bundle producers and worker admission. */
|
||||
export const JAVASCRIPT_AST_FINGERPRINT_ALGORITHM = "babel-ast-v3";
|
||||
@@ -124,7 +125,10 @@ export interface JavaScriptRolePath {
|
||||
|
||||
/** Deterministic AST-only analysis of one JavaScript source file. */
|
||||
export interface JavaScriptStaticAnalysis {
|
||||
/** Established overall summary; syntax alone is reported in analysis_coverage. */
|
||||
readonly parse_status: "complete" | "partial" | "failed";
|
||||
/** Historical facts omit this measurement; consumers must not infer it. */
|
||||
readonly analysis_coverage?: JavaScriptStaticCoverage;
|
||||
readonly parse_error_count: number;
|
||||
readonly visited_ast_nodes: number;
|
||||
readonly references: readonly JavaScriptStaticReference[];
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
import {
|
||||
javaScriptStaticParseStatus,
|
||||
unexaminedJavaScriptStaticCoverage,
|
||||
} from "./javascriptStaticCoverage.js";
|
||||
import { aliasSelectionBudgetSource } from "../../../tests/fixtures/javascriptAliasSelectionBudget.js";
|
||||
|
||||
it("separates dynamic semantic uncertainty from completed syntax and examination", () => {
|
||||
const result = analyzeJavaScriptStaticSource(
|
||||
"const {ipcMain}=require('electron'); ipcMain.on(channel, () => {});",
|
||||
);
|
||||
expect(result.parse_status).toBe("partial");
|
||||
expect(result.analysis_coverage).toMatchObject({
|
||||
syntax: { status: "complete", error_count: 0 },
|
||||
semantics: { unknown_findings: 1, uncertainty: ["dynamic-findings"] },
|
||||
examination: {
|
||||
status: "complete",
|
||||
semantic_resource_limits: [],
|
||||
electron_call_paths_limited: false,
|
||||
},
|
||||
applicability: { electron: "observed" },
|
||||
});
|
||||
expect(result.electron.ipc).toMatchObject([
|
||||
{ channel: null, channel_expression: "channel" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("distinguishes recovered syntax from unparseable source without inventing an error total", () => {
|
||||
const recovered = analyzeJavaScriptStaticSource("let value; let value;");
|
||||
expect(recovered.parse_status).toBe("partial");
|
||||
expect(recovered.analysis_coverage?.syntax).toEqual({
|
||||
status: "recovered",
|
||||
error_count: recovered.parse_error_count,
|
||||
});
|
||||
const failed = analyzeJavaScriptStaticSource("const value = ;");
|
||||
expect(failed.parse_status).toBe("failed");
|
||||
expect(failed.analysis_coverage).toMatchObject({
|
||||
syntax: { status: "failed", error_count: null },
|
||||
semantics: { unknown_findings: null },
|
||||
examination: { status: "not-examined" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps framework applicability unknown without positive Electron evidence", () => {
|
||||
const ordinary = analyzeJavaScriptStaticSource("export const value = 1;");
|
||||
expect(ordinary.parse_status).toBe("complete");
|
||||
expect(ordinary.analysis_coverage?.applicability.electron).toBe("unknown");
|
||||
const electron = analyzeJavaScriptStaticSource(
|
||||
"const {ipcMain}=require('electron');",
|
||||
);
|
||||
expect(electron.analysis_coverage?.applicability.electron).toBe("observed");
|
||||
});
|
||||
|
||||
it("reports compiler-dependent module side effects independently of complete syntax", () => {
|
||||
const result = analyzeJavaScriptStaticSource(
|
||||
"import { type Config } from './config';",
|
||||
);
|
||||
expect(result.analysis_coverage?.syntax).toEqual({
|
||||
status: "complete",
|
||||
error_count: 0,
|
||||
});
|
||||
expect(result.analysis_coverage?.semantics.uncertainty).toContain(
|
||||
"type-only-module-side-effect",
|
||||
);
|
||||
});
|
||||
|
||||
it("retains typed examination limits alongside successfully parsed source", () => {
|
||||
const result = analyzeJavaScriptStaticSource(aliasSelectionBudgetSource());
|
||||
expect(result.parse_status).toBe("partial");
|
||||
expect(result.analysis_coverage).toMatchObject({
|
||||
syntax: { status: "complete", error_count: 0 },
|
||||
examination: {
|
||||
status: "limited",
|
||||
semantic_resource_limits: ["alias-selections"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the legacy summary responsibility without relabeling independent limitations", () => {
|
||||
const base = analyzeJavaScriptStaticSource(
|
||||
"export const value = 1;",
|
||||
).analysis_coverage!;
|
||||
expect(
|
||||
javaScriptStaticParseStatus({
|
||||
...base,
|
||||
semantics: {
|
||||
unknown_findings: 0,
|
||||
uncertainty: ["returned-reference-widening"],
|
||||
},
|
||||
examination: {
|
||||
...base.examination,
|
||||
status: "limited",
|
||||
electron_call_paths_limited: true,
|
||||
},
|
||||
}),
|
||||
).toBe("complete");
|
||||
expect(
|
||||
unexaminedJavaScriptStaticCoverage("Caller selected another source."),
|
||||
).toMatchObject({
|
||||
syntax: { status: "not-examined", error_count: null },
|
||||
semantics: { unknown_findings: null },
|
||||
examination: { status: "not-examined" },
|
||||
unexamined_reason: "Caller selected another source.",
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { z } from "zod";
|
||||
import { javaScriptSemanticResourceLimitSchema } from "./javascriptSemanticValueTypes.js";
|
||||
|
||||
/** Independent observations from one inert static pass; no runtime completeness claim. */
|
||||
export const javaScriptStaticCoverageSchema = z.strictObject({
|
||||
syntax: z.discriminatedUnion("status", [
|
||||
z.strictObject({
|
||||
status: z.literal("complete"),
|
||||
error_count: z.literal(0),
|
||||
}),
|
||||
z.strictObject({
|
||||
status: z.literal("recovered"),
|
||||
error_count: z.int().positive(),
|
||||
}),
|
||||
z.strictObject({ status: z.literal("failed"), error_count: z.null() }),
|
||||
z.strictObject({
|
||||
status: z.literal("not-examined"),
|
||||
error_count: z.null(),
|
||||
}),
|
||||
]),
|
||||
semantics: z.strictObject({
|
||||
unknown_findings: z.int().nonnegative().nullable(),
|
||||
uncertainty: z.array(
|
||||
z.enum([
|
||||
"dynamic-findings",
|
||||
"returned-reference-widening",
|
||||
"type-only-module-side-effect",
|
||||
]),
|
||||
),
|
||||
}),
|
||||
examination: z.strictObject({
|
||||
status: z.enum(["complete", "limited", "not-examined"]),
|
||||
semantic_resource_limits: z.array(javaScriptSemanticResourceLimitSchema),
|
||||
electron_call_paths_limited: z.boolean(),
|
||||
}),
|
||||
applicability: z.strictObject({ electron: z.enum(["observed", "unknown"]) }),
|
||||
unexamined_reason: z.string().min(1).exactOptional(),
|
||||
});
|
||||
|
||||
export type JavaScriptStaticCoverage = z.infer<
|
||||
typeof javaScriptStaticCoverageSchema
|
||||
>;
|
||||
|
||||
/** Preserve the established conservative summary, independently of finer facts. */
|
||||
export const javaScriptStaticParseStatus = (
|
||||
coverage: JavaScriptStaticCoverage,
|
||||
): "complete" | "partial" | "failed" =>
|
||||
coverage.syntax.status === "failed" ||
|
||||
coverage.syntax.status === "not-examined"
|
||||
? "failed"
|
||||
: coverage.syntax.status === "recovered" ||
|
||||
(coverage.semantics.unknown_findings ?? 0) > 0 ||
|
||||
coverage.examination.semantic_resource_limits.includes(
|
||||
"alias-selections",
|
||||
)
|
||||
? "partial"
|
||||
: "complete";
|
||||
|
||||
/** Unperformed work has no measured syntax, finding count or framework applicability. */
|
||||
export const unexaminedJavaScriptStaticCoverage = (
|
||||
reason: string,
|
||||
): JavaScriptStaticCoverage => ({
|
||||
syntax: { status: "not-examined", error_count: null },
|
||||
semantics: { unknown_findings: null, uncertainty: [] },
|
||||
examination: {
|
||||
status: "not-examined",
|
||||
semantic_resource_limits: [],
|
||||
electron_call_paths_limited: false,
|
||||
},
|
||||
applicability: { electron: "unknown" },
|
||||
unexamined_reason: reason,
|
||||
});
|
||||
@@ -10,6 +10,28 @@ import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSe
|
||||
import { getterExpansionSource } from "../../../tests/fixtures/javascriptGetterExpansion.js";
|
||||
import { aliasSelectionBudgetSource } from "../../../tests/fixtures/javascriptAliasSelectionBudget.js";
|
||||
|
||||
it("preserves independent static coverage and historical unmeasured facts", () => {
|
||||
const facts = analyzeJavaScriptStaticSource("require(moduleName);");
|
||||
expect(
|
||||
javaScriptWorkerStaticAnalysisSchema.parse(
|
||||
JSON.parse(JSON.stringify(facts)),
|
||||
),
|
||||
).toEqual(facts);
|
||||
const { analysis_coverage: _coverage, ...historical } = facts;
|
||||
expect(javaScriptWorkerStaticAnalysisSchema.parse(historical)).toEqual(
|
||||
historical,
|
||||
);
|
||||
expect(
|
||||
javaScriptWorkerStaticAnalysisSchema.safeParse({
|
||||
...facts,
|
||||
analysis_coverage: {
|
||||
...facts.analysis_coverage,
|
||||
syntax: { status: "complete", error_count: -1 },
|
||||
},
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("transfers bounded alias selection coverage across the worker boundary", () => {
|
||||
const source = aliasSelectionBudgetSource();
|
||||
const facts = analyzeJavaScriptStaticSource(source);
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { javaScriptSemanticUnknownOmissionsSchema } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
||||
import { z } from "zod";
|
||||
import { digestSchema } from "../../domain/digests.js";
|
||||
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import { javaScriptRouteSourceFactsSchema } from "../../domain/javascript/javascriptRouteFacts.js";
|
||||
import { JAVASCRIPT_AST_FINGERPRINT_ALGORITHM } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import type {
|
||||
JavaScriptSemanticProperty,
|
||||
JavaScriptSemanticValue,
|
||||
} from "../../domain/javascript/javascriptSemanticValueTypes.js";
|
||||
import {
|
||||
javaScriptSemanticUnknownOmissionsSchema,
|
||||
javaScriptSemanticNodeSchema,
|
||||
javaScriptSemanticRelationSchema,
|
||||
javaScriptSemanticUnknownSchema,
|
||||
javaScriptSemanticFingerprintSchema,
|
||||
javaScriptSemanticEvidenceContextSchema,
|
||||
} from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
||||
import { z } from "zod";
|
||||
import { digestSchema } from "../../domain/digests.js";
|
||||
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import { javaScriptRouteSourceFactsSchema } from "../../domain/javascript/javascriptRouteFacts.js";
|
||||
import { javaScriptStaticCoverageSchema } from "../../domain/javascript/javascriptStaticCoverage.js";
|
||||
import { JAVASCRIPT_AST_FINGERPRINT_ALGORITHM } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import type {
|
||||
JavaScriptSemanticProperty,
|
||||
JavaScriptSemanticValue,
|
||||
} from "../../domain/javascript/javascriptSemanticValueTypes.js";
|
||||
import { javaScriptSemanticResourceLimitSchema } from "../../domain/javascript/javascriptSemanticValueTypes.js";
|
||||
|
||||
const range =
|
||||
javaScriptSemanticNodeSchema.shape.identity.shape.source_range.unwrap();
|
||||
@@ -319,6 +321,7 @@ const referenceShape = {
|
||||
export const javaScriptWorkerStaticAnalysisSchema = z
|
||||
.strictObject({
|
||||
parse_status: z.enum(["complete", "partial", "failed"]),
|
||||
analysis_coverage: javaScriptStaticCoverageSchema.exactOptional(),
|
||||
routes: javaScriptRouteSourceFactsSchema.exactOptional(),
|
||||
parse_error_count: count,
|
||||
visited_ast_nodes: count,
|
||||
@@ -433,13 +436,6 @@ export const javaScriptWorkerStaticAnalysisSchema = z
|
||||
},
|
||||
})) satisfies z.ZodType<JavaScriptStaticAnalysis>;
|
||||
|
||||
const resourceLimit = z.enum([
|
||||
"primitive-candidates",
|
||||
"primitive-bytes",
|
||||
"expression-depth",
|
||||
"getter-expansion",
|
||||
"alias-selections",
|
||||
]);
|
||||
const primitive = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||
const property: z.ZodType<JavaScriptSemanticProperty> = z.lazy(() =>
|
||||
z.strictObject({
|
||||
@@ -487,7 +483,7 @@ const semanticValue: z.ZodType<JavaScriptSemanticValue> = z.lazy(() =>
|
||||
z.strictObject({
|
||||
status: z.enum(["unknown", "ambiguous", "cycle"]),
|
||||
reason: z.string(),
|
||||
resourceLimit: resourceLimit.optional(),
|
||||
resourceLimit: javaScriptSemanticResourceLimitSchema.optional(),
|
||||
}),
|
||||
])
|
||||
.transform((value) => {
|
||||
@@ -544,19 +540,19 @@ const coverage = z
|
||||
z.strictObject({
|
||||
status: z.literal("complete"),
|
||||
omittedCount: z.literal(0),
|
||||
resourceLimits: z.array(resourceLimit).optional(),
|
||||
resourceLimits: z.array(javaScriptSemanticResourceLimitSchema).optional(),
|
||||
widenedReturnedReferences: z.array(range).optional(),
|
||||
}),
|
||||
z.strictObject({
|
||||
status: z.literal("partial"),
|
||||
omittedCount: z.union([z.literal(0), z.null()]),
|
||||
resourceLimits: z.array(resourceLimit).optional(),
|
||||
resourceLimits: z.array(javaScriptSemanticResourceLimitSchema).optional(),
|
||||
widenedReturnedReferences: z.array(range).optional(),
|
||||
}),
|
||||
z.strictObject({
|
||||
status: z.literal("failed"),
|
||||
omittedCount: z.null(),
|
||||
resourceLimits: z.array(resourceLimit).optional(),
|
||||
resourceLimits: z.array(javaScriptSemanticResourceLimitSchema).optional(),
|
||||
widenedReturnedReferences: z.array(range).optional(),
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -92,6 +92,15 @@ cliTest.for([
|
||||
expect(properties?.parse_status).toBe(
|
||||
path === "main.mjs" ? "complete" : null,
|
||||
);
|
||||
if (/\.(?:mjs|js)$/u.test(path))
|
||||
expect(properties?.analysis_coverage).toMatchObject({
|
||||
syntax: {
|
||||
status: requested.includes(path) ? "complete" : "not-examined",
|
||||
},
|
||||
examination: {
|
||||
status: requested.includes(path) ? "complete" : "not-examined",
|
||||
},
|
||||
});
|
||||
expect(Buffer.compare(await readFile(join(directory, path)), bytes)).toBe(
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -13,8 +13,11 @@ import {
|
||||
analysisViewJavaScriptEvidence,
|
||||
analysisViewBindJavaScriptGraphs,
|
||||
} from "../../fixtures/analysisView.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
|
||||
import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js";
|
||||
import {
|
||||
analysisViewResultSchema,
|
||||
type AnalysisViewResult,
|
||||
} from "../../../src/domain/analysisView/analysisView.js";
|
||||
import {
|
||||
createJavaScriptApplicationGraph,
|
||||
createJavaScriptApplicationNode,
|
||||
@@ -46,6 +49,57 @@ const facts = async (
|
||||
return { evidence, page, wire };
|
||||
};
|
||||
|
||||
/** Summary counts and both Evidence lookup paths preserve selected/excluded source facts. */
|
||||
const expectSelectedSourceCoverage = async (
|
||||
harness: Awaited<ReturnType<typeof connect>>,
|
||||
parent: Evidence,
|
||||
summary: Extract<
|
||||
AnalysisViewResult,
|
||||
{ kind: "summary"; parent_operation: "analyze_javascript_application" }
|
||||
>,
|
||||
): Promise<void> => {
|
||||
expect(summary.summary.coverage).toMatchObject({
|
||||
static_sources: {
|
||||
sources: 2,
|
||||
unmeasured_sources: 0,
|
||||
syntax: { complete: 1, "not-examined": 1 },
|
||||
examination: { complete: 1, "not-examined": 1 },
|
||||
electron_applicability: { observed: 0, unknown: 2 },
|
||||
},
|
||||
});
|
||||
const query = {
|
||||
kind: "javascript-facts",
|
||||
where: { paths: ["a.js", "b.js"], kinds: ["javascript-asset"] },
|
||||
property_keys: ["analysis_coverage"],
|
||||
limit: 64,
|
||||
};
|
||||
const retained = await facts(
|
||||
harness,
|
||||
{ kind: "retained-evidence", evidence_id: parent.evidence_id },
|
||||
query,
|
||||
);
|
||||
const portable = await facts(
|
||||
harness,
|
||||
{ kind: "inline", evidence: parent },
|
||||
query,
|
||||
);
|
||||
expect(portable.evidence).toEqual(retained.evidence);
|
||||
expect(
|
||||
retained.page.rows.map(({ properties }) => properties.analysis_coverage),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
syntax: { status: "complete", error_count: 0 },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
syntax: { status: "not-examined", error_count: null },
|
||||
unexamined_reason:
|
||||
"Source is outside the caller-selected source_paths.",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
it("pages selected real filesystem observations with shared identities and portable/retained parity", async () => {
|
||||
const root = await createTestTempDirectory("rea-retained-file-facts-");
|
||||
await Promise.all(
|
||||
@@ -68,9 +122,15 @@ it("pages selected real filesystem observations with shared identities and porta
|
||||
const summary = analysisViewResultSchema.parse(
|
||||
parseEvidence(preparation.structuredContent).normalized_result,
|
||||
);
|
||||
if (
|
||||
summary.kind !== "summary" ||
|
||||
summary.parent_operation !== "analyze_javascript_application"
|
||||
)
|
||||
throw new Error("Expected JavaScript summary view");
|
||||
const parent = harness.session.evidenceById(summary.parent_evidence_id);
|
||||
if (parent === undefined) throw new Error("Missing retained parent");
|
||||
const source = { kind: "retained-evidence", evidence_id: parent.evidence_id };
|
||||
await expectSelectedSourceCoverage(harness, parent, summary);
|
||||
const query = {
|
||||
kind: "javascript-facts",
|
||||
where: { paths: ["first.node", "second.node", "package.json", "missing"] },
|
||||
|
||||
Reference in New Issue
Block a user