mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): retain assignment and nested-default destructuring
Share source identity and one iterative pattern walker across declarations and assignments. Traverse nested default targets without losing dependency paths, preserving existing scalar-default occurrence identities and conservative unresolved slots. Validation: 405 focused tests across five files, including public CLI dependency Evidence. Refs #4609, #4604
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { graphForJavaScript } from "../../../tests/fixtures/javascriptSemanticGraph.js";
|
||||
|
||||
it.each(
|
||||
[
|
||||
{ pattern: "{ a: selected }", source: "{ a: 1 }", path: ["a"] },
|
||||
{ pattern: "[selected]", source: "[1]", path: ["0"] },
|
||||
{
|
||||
pattern: "{ a: [selected] = [] }",
|
||||
source: "{ a: [1] }",
|
||||
path: ["a", "0"],
|
||||
},
|
||||
{
|
||||
pattern: "{ a: { b: selected } = {} }",
|
||||
source: "{ a: { b: 1 } }",
|
||||
path: ["a", "b"],
|
||||
},
|
||||
{ pattern: "[[selected] = []]", source: "[[1]]", path: ["0", "0"] },
|
||||
].flatMap((pattern) =>
|
||||
[false, true].map((assignment) => ({ ...pattern, assignment })),
|
||||
),
|
||||
)(
|
||||
"retains source-property flow for $pattern, assignment=$assignment",
|
||||
({ pattern, source, path, assignment }) => {
|
||||
const graph = graphForJavaScript(`
|
||||
const source = ${source};
|
||||
${assignment ? `let selected; (${pattern} = source);` : `const ${pattern} = source;`}
|
||||
`);
|
||||
const target = graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === "selected",
|
||||
);
|
||||
expect(target).toBeDefined();
|
||||
const relations = graph.relations.filter(
|
||||
(relation) =>
|
||||
relation.relation === "destructures" &&
|
||||
relation.target_node_id === target?.node_id,
|
||||
);
|
||||
expect(relations).toHaveLength(1);
|
||||
expect(relations[0]?.resolution).toBe("resolved");
|
||||
const slot = graph.nodes.find(
|
||||
(node) => node.node_id === relations[0]?.source_node_id,
|
||||
);
|
||||
expect(slot?.properties.property_path).toEqual(path);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["{}", "unknownSource"])(
|
||||
"keeps nested default source flow conservative for %s",
|
||||
(source) => {
|
||||
const graph = graphForJavaScript(
|
||||
`const source = ${source}; const { a: [selected] = [1] } = source;`,
|
||||
);
|
||||
const target = graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === "selected",
|
||||
);
|
||||
const relation = graph.relations.find(
|
||||
(item) =>
|
||||
item.relation === "destructures" &&
|
||||
item.target_node_id === target?.node_id,
|
||||
);
|
||||
expect(relation).toMatchObject({ resolution: "candidate" });
|
||||
expect(graph.unknowns).toContainEqual(
|
||||
expect.objectContaining({
|
||||
family: "object-flow",
|
||||
relation_kinds: ["destructures"],
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -265,7 +265,10 @@ describe("JavaScript semantic analysis: read-modify-write", () => {
|
||||
let value;
|
||||
[value = source.count] = [];
|
||||
`);
|
||||
expect(ir.objectOperations.map(({ kind }) => kind)).toEqual(["read"]);
|
||||
expect(ir.objectOperations.map(({ kind }) => kind)).toEqual([
|
||||
"destructure",
|
||||
"read",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not model a compound binding write as a competing replacement", () => {
|
||||
|
||||
@@ -65,7 +65,24 @@ export const collectJavaScriptSemanticObjects = (
|
||||
t.isVariableDeclarator(node) &&
|
||||
(t.isObjectPattern(node.id) || t.isArrayPattern(node.id))
|
||||
)
|
||||
collectDestructuring(node, ownerCallableId, state, output);
|
||||
collectDestructuring(
|
||||
node.id,
|
||||
node.init,
|
||||
ownerCallableId,
|
||||
state,
|
||||
output,
|
||||
);
|
||||
else if (
|
||||
t.isAssignmentExpression(node, { operator: "=" }) &&
|
||||
(t.isObjectPattern(node.left) || t.isArrayPattern(node.left))
|
||||
)
|
||||
collectDestructuring(
|
||||
node.left,
|
||||
node.right,
|
||||
ownerCallableId,
|
||||
state,
|
||||
output,
|
||||
);
|
||||
},
|
||||
exit: (node) => callableStack.exit(node),
|
||||
});
|
||||
@@ -126,46 +143,62 @@ const collectMember = (
|
||||
};
|
||||
|
||||
const collectDestructuring = (
|
||||
node: t.VariableDeclarator,
|
||||
root: t.Node,
|
||||
initializer: t.Node | null | undefined,
|
||||
ownerCallableId: string | null,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
output: JavaScriptSemanticObjectOperation[],
|
||||
): void => {
|
||||
const source = expressionObjectIdentity(node.init, state);
|
||||
const visit = (pattern: t.Node, path: readonly string[]): void => {
|
||||
const source = expressionObjectIdentity(initializer, state);
|
||||
const pending: {
|
||||
readonly pattern: t.Node;
|
||||
readonly path: readonly string[];
|
||||
}[] = [{ pattern: root, path: [] }];
|
||||
while (pending.length > 0) {
|
||||
const next = pending.pop();
|
||||
if (next === undefined) break;
|
||||
const { path } = next;
|
||||
const occurrence = unwrapJavaScriptExpression(next.pattern).node;
|
||||
const pattern = t.isAssignmentPattern(occurrence)
|
||||
? unwrapJavaScriptExpression(occurrence.left).node
|
||||
: occurrence;
|
||||
if (t.isObjectPattern(pattern)) {
|
||||
for (const property of pattern.properties) {
|
||||
for (const property of pattern.properties.toReversed()) {
|
||||
if (!t.isObjectProperty(property)) continue;
|
||||
const name = semanticStaticPropertyKey(property.key, property.computed);
|
||||
if (name !== null) visit(property.value, [...path, name]);
|
||||
if (name !== null)
|
||||
pending.push({ pattern: property.value, path: [...path, name] });
|
||||
}
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if (t.isArrayPattern(pattern)) {
|
||||
pattern.elements.forEach((element, index) => {
|
||||
if (element !== null && !t.isRestElement(element))
|
||||
visit(element, [...path, String(index)]);
|
||||
});
|
||||
return;
|
||||
for (let index = pattern.elements.length - 1; index >= 0; index -= 1) {
|
||||
const element = pattern.elements[index];
|
||||
if (
|
||||
element !== null &&
|
||||
element !== undefined &&
|
||||
!t.isRestElement(element)
|
||||
)
|
||||
pending.push({ pattern: element, path: [...path, String(index)] });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const target = bindingIdentifier(pattern);
|
||||
if (target === null) return;
|
||||
if (!t.isIdentifier(pattern)) continue;
|
||||
addObjectOperation(
|
||||
{
|
||||
node: pattern,
|
||||
node: occurrence,
|
||||
kind: "destructure",
|
||||
ownerCallableId,
|
||||
objectBindingId: source.bindingId,
|
||||
targetBindingId:
|
||||
resolveSemanticBindingState(state, target, target.name)?.bindingId ??
|
||||
null,
|
||||
resolveSemanticBindingState(state, pattern, pattern.name)
|
||||
?.bindingId ?? null,
|
||||
propertyPath: source.path === null ? null : [...source.path, ...path],
|
||||
},
|
||||
state,
|
||||
output,
|
||||
);
|
||||
};
|
||||
visit(node.id, []);
|
||||
}
|
||||
};
|
||||
|
||||
interface AddObjectOperationInput {
|
||||
@@ -240,10 +273,3 @@ const expressionObjectIdentity = (
|
||||
path: staticPath ? path.reverse() : null,
|
||||
};
|
||||
};
|
||||
|
||||
const bindingIdentifier = (node: t.Node): t.Identifier | null => {
|
||||
if (t.isIdentifier(node)) return node;
|
||||
if (t.isAssignmentPattern(node) && t.isIdentifier(node.left))
|
||||
return node.left;
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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(
|
||||
"retains assignment and nested-default destructuring Evidence",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-js-destructuring-evidence-",
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "main.js"),
|
||||
`
|
||||
const src = { a: 1 };
|
||||
const { a: declared } = src;
|
||||
let a;
|
||||
({ a } = src);
|
||||
const nested = { a: [1], b: 2 };
|
||||
const { a: [plain] } = nested;
|
||||
const { a: [fallback] = [] } = nested;
|
||||
const { b: scalar = 0 } = nested;
|
||||
`,
|
||||
);
|
||||
const output = await cli.run({
|
||||
arguments: ["analyze-javascript-application", root, "--json"],
|
||||
});
|
||||
expect(output.exitCode).toBe(0);
|
||||
const { normalized_result: result } = z
|
||||
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
|
||||
.parse(output.json);
|
||||
const graph = result.semantic_graph;
|
||||
for (const [name, path] of [
|
||||
["declared", ["a"]],
|
||||
["a", ["a"]],
|
||||
["plain", ["a", "0"]],
|
||||
["fallback", ["a", "0"]],
|
||||
["scalar", ["b"]],
|
||||
] as const) {
|
||||
const binding = graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === name,
|
||||
);
|
||||
expect(binding).toBeDefined();
|
||||
const relations = graph.relations.filter(
|
||||
(relation) =>
|
||||
relation.relation === "destructures" &&
|
||||
relation.target_node_id === binding?.node_id,
|
||||
);
|
||||
expect(relations).toHaveLength(1);
|
||||
expect(relations[0]?.resolution).toBe("resolved");
|
||||
expect(
|
||||
graph.nodes.find(
|
||||
(node) => node.node_id === relations[0]?.source_node_id,
|
||||
)?.properties.property_path,
|
||||
).toEqual(path);
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user