mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00: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:
1 parent
c8269ca497
commit
f62a2baad8
21 files changed
+898
-125
No files matched your search
@@ -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,
|
||||
});
|
||||
Reference in new issue
Block a user