mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
fix(javascript): derive loop cardinality from exact literal values
Use the canonical cooked literal value for for-in and for-of string cardinality, preserving possible assignments and prototype uncertainty across quoted and template spellings. Validation: 473 focused tests across six files, including public CLI assignment Evidence and exported return shapes. Refs #4617
This commit is contained in:
1 parent
746a044e42
commit
c3e42385e7
3 files changed
+141
-5
No files matched your search
@@ -547,6 +547,62 @@ describe("nonfinite static values", () => {
|
||||
});
|
||||
|
||||
describe("binding write invalidation", () => {
|
||||
it.each(["in", "of"])(
|
||||
"retains prototype uncertainty for an empty template in for-%s",
|
||||
(operator) => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
String.prototype.extra = "possible inherited key";
|
||||
let selected = "before";
|
||||
for (selected ${operator} \`\`) {}
|
||||
const observed = selected;
|
||||
`);
|
||||
expect(
|
||||
topLevelBinding(ir, "selected").definitions.some(
|
||||
({ kind }) => kind === "assignment",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(topLevelBinding(ir, "observed").value.status).not.toBe("literal");
|
||||
},
|
||||
);
|
||||
|
||||
it.each(
|
||||
[
|
||||
{ input: '"abc"', empty: false },
|
||||
{ input: "`abc`", empty: false },
|
||||
{ input: "`\\u0061`", empty: false },
|
||||
{ input: '""', empty: true },
|
||||
{ input: "``", empty: true },
|
||||
{ input: "`\\\n`", empty: true },
|
||||
].flatMap((literal) =>
|
||||
["in", "of"].flatMap((operator) =>
|
||||
["selected", "[selected]"].map((head) => ({
|
||||
...literal,
|
||||
operator,
|
||||
head,
|
||||
})),
|
||||
),
|
||||
),
|
||||
)(
|
||||
"preserves writes for $head $operator $input (empty=$empty)",
|
||||
({ input, empty, operator, head }) => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
let selected = "before";
|
||||
for (${head} ${operator} ${input}) {}
|
||||
const observed = selected;
|
||||
export function result() { return observed; }
|
||||
`);
|
||||
expect(
|
||||
topLevelBinding(ir, "selected").definitions.some(
|
||||
({ kind }) => kind === "assignment",
|
||||
),
|
||||
).toBe(!empty);
|
||||
const value = topLevelBinding(ir, "observed").value;
|
||||
if (empty) expect(value).toEqual({ status: "literal", value: "before" });
|
||||
else expect(value).not.toEqual({ status: "literal", value: "before" });
|
||||
expect(onlyCallable(ir, "result").returnSites[0]?.value).toEqual(value);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["let value = 1; value++; const observed = value;", "unknown"],
|
||||
["let value = 1; --value; const observed = value;", "unknown"],
|
||||
|
||||
@@ -19,6 +19,7 @@ export const semanticIterationCardinality = (
|
||||
prototypeMayAffectEnumeration: boolean | undefined,
|
||||
): JavaScriptSemanticIterationCardinality => {
|
||||
const iterable = unwrapJavaScriptExpression(loop.right).node;
|
||||
const literal = readExactJavaScriptLiteral(iterable);
|
||||
if (t.isForOfStatement(loop)) {
|
||||
if (prototypeMayAffectEnumeration !== false) return "unknown";
|
||||
if (t.isArrayExpression(iterable)) {
|
||||
@@ -31,8 +32,8 @@ export const semanticIterationCardinality = (
|
||||
return "nonempty";
|
||||
return "unknown";
|
||||
}
|
||||
if (t.isStringLiteral(iterable))
|
||||
return iterable.value.length === 0 ? "empty" : "nonempty";
|
||||
if (literal.found && typeof literal.value === "string")
|
||||
return literal.value.length === 0 ? "empty" : "nonempty";
|
||||
return "unknown";
|
||||
}
|
||||
if (t.isObjectExpression(iterable)) {
|
||||
@@ -57,10 +58,14 @@ export const semanticIterationCardinality = (
|
||||
return "unknown";
|
||||
return prototypeMayAffectEnumeration === false ? "empty" : "unknown";
|
||||
}
|
||||
if (t.isNullLiteral(iterable)) return "empty";
|
||||
if (t.isStringLiteral(iterable) && iterable.value.length > 0)
|
||||
if (literal.found && literal.value === null) return "empty";
|
||||
if (
|
||||
literal.found &&
|
||||
typeof literal.value === "string" &&
|
||||
literal.value.length > 0
|
||||
)
|
||||
return "nonempty";
|
||||
if (readExactJavaScriptLiteral(iterable).found)
|
||||
if (literal.found)
|
||||
return prototypeMayAffectEnumeration === false ? "empty" : "unknown";
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"preserves template loop assignments in public semantic Evidence",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-js-iteration-cardinality-");
|
||||
await writeFile(
|
||||
join(root, "main.js"),
|
||||
`
|
||||
export function template() { let selected = "before"; for ([selected] in \`abc\`) {} return selected; }
|
||||
export function quoted() { let selected = "before"; for ([selected] in "abc") {} return selected; }
|
||||
export function empty() { let selected = "before"; for ([selected] in \`\`) {} return selected; }
|
||||
export function ordinaryTemplate() { let selected = "before"; for (selected in \`abc\`) {} return selected; }
|
||||
for (const [templateKey] in \`abc\`) { void templateKey; }
|
||||
for (const [quotedKey] in "abc") { void quotedKey; }
|
||||
`,
|
||||
);
|
||||
const output = await cli.run({
|
||||
arguments: ["analyze-javascript-application", root, "--json"],
|
||||
});
|
||||
expect(output.exitCode).toBe(0);
|
||||
const envelope = z
|
||||
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
|
||||
.parse(output.json);
|
||||
const result = envelope.normalized_result;
|
||||
const shapeSchema = z.object({
|
||||
semantic_role: z.literal("export-return-shapes"),
|
||||
exported_name: z.string(),
|
||||
static_return_shapes: z.array(
|
||||
z.object({
|
||||
fields: z.array(z.object({ state: z.string(), value: z.unknown() })),
|
||||
}),
|
||||
),
|
||||
});
|
||||
const shapes = result.graph.nodes.flatMap(({ observations }) =>
|
||||
observations.flatMap(({ properties }) => {
|
||||
const parsed = shapeSchema.safeParse(properties);
|
||||
return parsed.success ? [parsed.data] : [];
|
||||
}),
|
||||
);
|
||||
for (const name of ["template", "quoted", "ordinaryTemplate"]) {
|
||||
const shape = shapes.find(({ exported_name }) => exported_name === name);
|
||||
expect(shape?.static_return_shapes[0]?.fields[0]).toMatchObject({
|
||||
state: "unknown",
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
expect(
|
||||
shapes.find(({ exported_name }) => exported_name === "empty")
|
||||
?.static_return_shapes[0]?.fields[0],
|
||||
).toMatchObject({ state: "literal", value: "before" });
|
||||
const graph = result.semantic_graph;
|
||||
for (const label of ["templateKey", "quotedKey"]) {
|
||||
const binding = graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === label,
|
||||
);
|
||||
expect(binding).toBeDefined();
|
||||
const assignment = graph.relations.find(
|
||||
(relation) =>
|
||||
relation.relation === "defines" &&
|
||||
relation.target_node_id === binding?.node_id &&
|
||||
graph.nodes.find((node) => node.node_id === relation.source_node_id)
|
||||
?.label === "assignment",
|
||||
);
|
||||
expect(assignment).toBeDefined();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user