fix(javascript): bound semantic expansion and resolve lexical receivers

This commit is contained in:
morluto committed 2026-10-09 00:49:30 +08:00
1 parent eaf5734083
commit 16f4609d68
26 files changed
+868 -78

No files matched your search

@@ -2,7 +2,10 @@ import { createHash } from "node:crypto";
import { setImmediate } from "node:timers/promises";
import { analyzeParsedJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
import { analyzeParsedJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js";
import {
analyzeParsedJavaScriptSemantics,
classifyParsedJavaScriptOpenReceivers,
} from "../../domain/javascript/javascriptSemanticAnalysis.js";
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js";
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
import { hasValidSourceMapContents } from "../../domain/sourceMapContents.js";
@@ -170,7 +173,12 @@ const analyzeArtifactFile = <SemanticIr extends JavaScriptModuleSemanticIr>(
state.parseFailures += 1;
return;
}
const analysis = analyzeParsedJavaScriptStaticSource(file.text.value, parsed);
const openReceiverFacts = classifyParsedJavaScriptOpenReceivers(parsed);
const analysis = analyzeParsedJavaScriptStaticSource(
file.text.value,
parsed,
openReceiverFacts,
);
const staticFindings = findingCount(analysis);
const semantics =
analysis.parse_status === "complete" || analysis.parse_status === "partial"
@@ -20,6 +20,13 @@ import {
completeApplicationCoverage,
partialApplicationCoverage,
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptSemanticResourceLimit } from "../../domain/javascript/javascriptSemanticValueTypes.js";
import { semanticCoverageResourceLimits } from "../../domain/javascript/javascriptSemanticCoverage.js";
import {
SEMANTIC_EXPRESSION_DEPTH_LIMIT,
SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT,
semanticResourceLimitReason,
} from "../../domain/javascript/javascriptSemanticResourceLimits.js";
import {
addJavaScriptArtifactContainers,
addJavaScriptArtifactFiles,
@@ -80,6 +87,7 @@ export const buildJavaScriptArtifactGraph = (
};
const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
const resourceLimits = semanticResourceLimits(context);
const sourceMapPolicyGap = context.analysis.source_maps.some(
({ status }) => status === "invalid",
);
@@ -99,10 +107,42 @@ const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
partialJavaScript;
if (context.analysis.truncated_scopes > 0)
return partialApplicationCoverage([], null);
if (resourceLimits.length > 0)
return partialApplicationCoverage(
resourceLimits.map(applicationGraphResourceLimit),
null,
);
if (unknownGap) return partialApplicationCoverage([], null);
return completeApplicationCoverage();
};
const semanticResourceLimits = (
context: JavaScriptArtifactGraphContext,
): JavaScriptSemanticResourceLimit[] =>
[
...new Set(
context.analysis.files.flatMap(({ semantic }) =>
semantic === null
? []
: semanticCoverageResourceLimits(semantic.ir.coverage),
),
),
].sort();
const applicationGraphResourceLimit = (
resourceLimit: JavaScriptSemanticResourceLimit,
) => ({
name: `javascript_semantic_${resourceLimit.replaceAll("-", "_")}`,
value:
resourceLimit === "primitive-candidates"
? SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT
: SEMANTIC_EXPRESSION_DEPTH_LIMIT,
unit:
resourceLimit === "expression-depth"
? ("depth" as const)
: ("items" as const),
});
const graphLimitations = (
context: JavaScriptArtifactGraphContext,
coverage: "complete" | "partial" | "unknown" | "unavailable",
@@ -150,6 +190,7 @@ const graphLimitations = (
"Sender, frame, URL, and origin checks are observations only; REA does not claim that they enforce a complete authorization policy.",
]
: []),
...semanticResourceLimits(context).map(semanticResourceLimitReason),
...(context.analysis.files.some(
({ javascript }) =>
(javascript?.electron.native_addon_bindings.length ?? 0) > 0,
@@ -6,6 +6,12 @@ import {
completeApplicationCoverage,
partialApplicationCoverage,
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import { semanticCoverageResourceLimits } from "../../domain/javascript/javascriptSemanticCoverage.js";
import {
SEMANTIC_EXPRESSION_DEPTH_LIMIT,
SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT,
semanticResourceLimitReason,
} from "../../domain/javascript/javascriptSemanticResourceLimits.js";
import type {
JavaScriptModuleOrigin,
JavaScriptSemanticModuleLink,
@@ -362,15 +368,35 @@ const relationshipEvidence = (
operation,
coverage: semanticCoverage(input.semantic),
confidence,
limitations: [...input.semantic.ir.limitations, ...limitations],
limitations: [
...input.semantic.ir.limitations,
...semanticCoverageResourceLimits(input.semantic.ir.coverage).map(
semanticResourceLimitReason,
),
...limitations,
],
});
const semanticCoverage = (
semantic: SemanticAnalysis,
): JavaScriptArtifactGraphCoverage => {
if (semantic.ir.coverage.status === "complete")
const resourceLimits = semanticCoverageResourceLimits(semantic.ir.coverage);
if (semantic.ir.coverage.status === "complete" && resourceLimits.length === 0)
return completeApplicationCoverage();
return partialApplicationCoverage([], semantic.ir.coverage.omittedCount);
return partialApplicationCoverage(
resourceLimits.map((resourceLimit) => ({
name: `javascript_semantic_${resourceLimit.replaceAll("-", "_")}`,
value:
resourceLimit === "primitive-candidates"
? SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT
: SEMANTIC_EXPRESSION_DEPTH_LIMIT,
unit:
resourceLimit === "expression-depth"
? ("depth" as const)
: ("items" as const),
})),
semantic.ir.coverage.omittedCount,
);
};
const moduleFormat = (
@@ -0,0 +1,51 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js";
it("classifies overloaded open calls from lexical receiver facts", async () => {
const inputPath = await createTestTempDirectory(
"rea-js-open-classification-",
);
await writeFile(
join(inputPath, "app.js"),
`
const popup = window;
popup.open(url, "/preview");
{
const indexedDB = { open() {} };
indexedDB.open("PROPFIND", "/dav");
indexedDB.open(databaseName, "2");
}
const xhr = new XMLHttpRequest();
xhr.open(method, "/api");
indexedDB.open("records", "2");
`,
);
const result = await analyzeJavaScriptApplication({
input_path: inputPath,
format: "directory",
});
if (!result.ok)
throw new Error(`Expected analysis success: ${result.error.message}`);
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
const endpoints = graph.nodes
.filter(({ kind }) => kind === "endpoint")
.flatMap(({ observations }) =>
observations.map(({ properties }) => properties.value),
)
.sort();
const storage = graph.nodes
.filter(({ kind }) => kind === "storage")
.flatMap(({ observations }) =>
observations.map(({ properties }) => properties.storage_kind),
);
expect(endpoints).toEqual(["/api", "/dav"]);
expect(storage).toEqual(["indexed-db"]);
});
@@ -1,3 +1,4 @@
import * as t from "@babel/types";
import { expect, it } from "vitest";
import {
@@ -13,7 +14,12 @@ import {
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js";
import {
analyzeJavaScriptSemantics,
analyzeParsedJavaScriptSemantics,
} from "../../domain/javascript/javascriptSemanticAnalysis.js";
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
import { semanticCoverageResourceLimits } from "../../domain/javascript/javascriptSemanticCoverage.js";
const SHA256 = "a".repeat(64);
const GRAPH_ID = `jag_${"b".repeat(64)}`;
@@ -86,6 +92,70 @@ it("keeps dynamic calls explicit and returns complete deterministic results", ()
expect(repeated).toEqual(first);
});
it("preserves primitive candidate budget unknowns in semantic graph coverage", () => {
const term = '(true ? "a" : "b")';
const source = `const answer = ${Array.from({ length: 20 }, () => term).join(" + ")};`;
const ir = analyzeJavaScriptSemantics(source);
const graph = graphFor(source, ir);
expect(ir.coverage).toMatchObject({
status: "partial",
resourceLimits: ["primitive-candidates"],
});
expect(graph.unknowns).toContainEqual(
expect.objectContaining({
family: "data-flow",
reason: "resource-limit",
detail: expect.stringMatching(/primitive candidate budget exceeded/i),
}),
);
});
it("retains resource-limit reasons at nested object property slots", () => {
const term = '(true ? "a" : "b")';
const source = `const answer = { nested: { value: ${Array.from({ length: 20 }, () => term).join(" + ")} } };`;
const ir = analyzeJavaScriptSemantics(source);
const graph = graphFor(source, ir);
expect(semanticCoverageResourceLimits(ir.coverage)).toEqual([
"primitive-candidates",
]);
expect(graph.unknowns).toContainEqual(
expect.objectContaining({
family: "object-flow",
reason: "resource-limit",
detail: expect.stringMatching(/Unknown value at property:value/u),
}),
);
});
it("preserves deep expression resource reasons through graph projection", () => {
const source = "const answer = true;";
const parsed = parseJavaScriptSource(source);
const declaration = parsed?.program.body[0];
const declarator = t.isVariableDeclaration(declaration)
? declaration.declarations[0]
: undefined;
if (parsed === null || !t.isVariableDeclarator(declarator))
throw new Error("Expected parsed binding initializer");
let expression: t.Expression = t.booleanLiteral(true);
for (let index = 0; index < 5_000; index += 1)
expression = t.unaryExpression("!", expression, true);
declarator.init = expression;
const ir = analyzeParsedJavaScriptSemantics(parsed);
const graph = graphFor(source, ir);
expect(semanticCoverageResourceLimits(ir.coverage)).toEqual([
"expression-depth",
]);
expect(graph.unknowns).toContainEqual(
expect.objectContaining({
reason: "resource-limit",
detail: expect.stringMatching(/expression depth budget exceeded/i),
}),
);
});
it("keeps ambiguous interprocedural flow out of the default traversal", () => {
const graph = graphFor(`
function left(value) { return value; }
@@ -257,7 +327,7 @@ it("keeps duplicate function fingerprints ambiguous", () => {
expect(query.summary.total_seed_matches).toBe(2);
});
const graphFor = (source: string) => {
const graphFor = (source: string, ir = analyzeJavaScriptSemantics(source)) => {
const file: JavaScriptArtifactFile = {
path: "app.js",
container_sha256: SHA256,
@@ -273,9 +343,7 @@ const graphFor = (source: string) => {
{
file,
javascript: null,
semantic: {
ir: analyzeJavaScriptSemantics(source),
},
semantic: { ir },
},
],
packages: [],
@@ -3,11 +3,14 @@ import type {
JavaScriptSemanticBinding,
JavaScriptSemanticValue,
} from "../../domain/javascript/javascriptSemanticIr.js";
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import {
retainSemanticGraphNode,
addSemanticGraphRelation,
addSemanticGraphUnknown,
} from "./JavaScriptSemanticGraphConstruction.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
import { observedSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
/** Project bounded literal values and object slots for exact query seeds. */
export const projectSemanticValues = (
@@ -76,6 +79,29 @@ const projectValue = (input: ValueProjectionInput): void => {
role: `property:${property.name}`,
});
}
else if (value.status === "unknown" && value.resourceLimit !== undefined) {
const location = binding.definitions[0]?.location ?? null;
const evidence = observedSemanticEvidence(context.file, location);
const isPropertyValue = role.startsWith("property:");
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown({
node_id: target.node_id,
family: isPropertyValue ? "object-flow" : "data-flow",
relation_kinds: [isPropertyValue ? "writes-property" : "defines"],
reason: "resource-limit",
detail: `${value.reason} Unknown value at ${role}.`,
candidate_node_ids: [target.node_id],
evidence: {
...evidence,
authority: "unknown",
state: "unknown",
confidence: "unknown",
limitations: [value.reason],
},
}),
);
}
};
const addLiteralNode = (