mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 06:46:10 +00:00
fix(javascript): bound semantic expansion and resolve lexical receivers
This commit is contained in:
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 = (
|
||||
|
||||
Reference in new issue
Block a user