fix(javascript): preserve uncertain with-scope member writes (#7914)
* fix(javascript): preserve uncertain with-scope member writes * fix: retain nested initializer candidates * fix(javascript): admit initializer values in worker transport * fix(tests): remove duplicate MCP test imports
This commit is contained in:
1 parent
611c30cb20
commit
95f0625781
7 files changed
+317
-20
No files matched your search
@@ -3,6 +3,42 @@ import { expect, it } from "vitest";
|
||||
import { graphForJavaScript } from "../../../tests/fixtures/javascriptSemanticGraph.js";
|
||||
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
|
||||
|
||||
it("retains nested initializer literals as uncertain candidates after a dynamic-scope write", () => {
|
||||
const graph = graphForJavaScript(`
|
||||
const config = { nested: { flag: "before", items: ["inside"] }, stable: "unchanged" };
|
||||
with (env) { config.nested = replacement; }
|
||||
`);
|
||||
for (const [value, pointer] of [
|
||||
["before", "/nested/flag"],
|
||||
["inside", "/nested/items/0"],
|
||||
]) {
|
||||
const trace = queryJavaScriptSemanticGraph(
|
||||
graph,
|
||||
javaScriptSemanticQueryInputSchema.parse({
|
||||
seed: { kind: "literal", value },
|
||||
direction: "forward-influence",
|
||||
include_ambiguous_dynamic_edges: true,
|
||||
}),
|
||||
);
|
||||
const slot = trace.nodes.find(
|
||||
(node) =>
|
||||
node.kind === "property-slot" &&
|
||||
node.properties.property_pointer === pointer,
|
||||
);
|
||||
expect(slot?.properties.presence).toBe("unknown-coverage");
|
||||
expect(trace.relations).toContainEqual(
|
||||
expect.objectContaining({
|
||||
target_node_id: slot?.node_id,
|
||||
relation: "defines",
|
||||
resolution: "candidate",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
trace.unknowns.some((unknown) => unknown.reason === "unknown-value"),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
["binding", "speed = accel; return speed;", "speed"],
|
||||
["property", "state.speed = accel; return state.speed;", "read:/speed"],
|
||||
|
||||
@@ -33,6 +33,150 @@ export const projectSemanticValues = (
|
||||
target: bindingNode,
|
||||
path: [],
|
||||
});
|
||||
if (binding.initializerValue !== undefined)
|
||||
projectInitializerCandidates(
|
||||
context,
|
||||
binding,
|
||||
binding.initializerValue,
|
||||
binding.value,
|
||||
[],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** Preserve possible initializer flow when later effects make a slot unknown. */
|
||||
const projectInitializerCandidates = (
|
||||
context: SemanticFlowProjectionContext,
|
||||
binding: JavaScriptSemanticBinding,
|
||||
initial: JavaScriptSemanticValue,
|
||||
current: JavaScriptSemanticValue,
|
||||
path: readonly string[],
|
||||
): void => {
|
||||
const initialContainer = semanticContainer(initial);
|
||||
const currentContainer = semanticContainer(current);
|
||||
if (initialContainer === null || currentContainer === null) return;
|
||||
const currentByName = new Map<string, JavaScriptSemanticProperty>();
|
||||
for (const property of currentContainer.slots)
|
||||
if (!currentByName.has(property.name))
|
||||
currentByName.set(property.name, property);
|
||||
for (const initialProperty of initialContainer.slots) {
|
||||
const currentProperty = currentByName.get(initialProperty.name);
|
||||
if (
|
||||
initialProperty.presence !== "present" ||
|
||||
currentProperty === undefined ||
|
||||
currentProperty.presence === "absent"
|
||||
)
|
||||
continue;
|
||||
const propertyPath = [...path, initialProperty.name];
|
||||
if (currentProperty.value.status === "unknown") {
|
||||
const slot = semanticPropertySlot(
|
||||
context,
|
||||
binding.bindingId,
|
||||
propertyPath,
|
||||
currentProperty,
|
||||
);
|
||||
projectUnknownInitializer({
|
||||
context,
|
||||
binding,
|
||||
initial: initialProperty.value,
|
||||
unknown: currentProperty.value,
|
||||
target: slot,
|
||||
path: propertyPath,
|
||||
});
|
||||
} else {
|
||||
projectInitializerCandidates(
|
||||
context,
|
||||
binding,
|
||||
initialProperty.value,
|
||||
currentProperty.value,
|
||||
propertyPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Retain initializer descendants as candidates without asserting the unknown receiver still exists. */
|
||||
const projectUnknownInitializer = ({
|
||||
context,
|
||||
binding,
|
||||
initial,
|
||||
unknown,
|
||||
target,
|
||||
path,
|
||||
}: {
|
||||
context: SemanticFlowProjectionContext;
|
||||
binding: JavaScriptSemanticBinding;
|
||||
initial: JavaScriptSemanticValue;
|
||||
unknown: Extract<
|
||||
JavaScriptSemanticValue,
|
||||
{ status: "unknown" | "ambiguous" | "cycle" }
|
||||
>;
|
||||
target: ReturnType<typeof semanticPropertySlot>;
|
||||
path: readonly string[];
|
||||
}): void => {
|
||||
if (target === null) return;
|
||||
const values =
|
||||
initial.status === "literal"
|
||||
? [initial.value]
|
||||
: initial.status === "union"
|
||||
? initial.values
|
||||
: [];
|
||||
for (const value of values) {
|
||||
const literal = addLiteralNode(
|
||||
context,
|
||||
binding,
|
||||
value,
|
||||
`property:${semanticPropertyPointer(path)}`,
|
||||
);
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: literal,
|
||||
target,
|
||||
relation: "defines",
|
||||
resolution: "candidate",
|
||||
});
|
||||
if (literal !== null)
|
||||
addSemanticGraphUnknown(context.state, {
|
||||
node_id: literal.node_id,
|
||||
family: "data-flow",
|
||||
relation_kinds: ["defines"],
|
||||
reason: "unknown-value",
|
||||
detail: `The initializer may no longer determine this property: ${unknown.reason}`,
|
||||
candidate_node_ids: [target.node_id],
|
||||
evidence: unknownSemanticEvidence(
|
||||
context.file,
|
||||
binding.definitions[0]?.location ?? null,
|
||||
),
|
||||
});
|
||||
}
|
||||
const container = semanticContainer(initial);
|
||||
if (container === null) return;
|
||||
for (const property of container.slots) {
|
||||
if (property.presence !== "present") continue;
|
||||
const propertyPath = [...path, property.name];
|
||||
const slot = semanticPropertySlot(
|
||||
context,
|
||||
binding.bindingId,
|
||||
propertyPath,
|
||||
{
|
||||
name: property.name,
|
||||
presence: "unknown-coverage",
|
||||
value: unknown,
|
||||
},
|
||||
);
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: target,
|
||||
target: slot,
|
||||
relation: "writes-property",
|
||||
resolution: "candidate",
|
||||
});
|
||||
projectUnknownInitializer({
|
||||
context,
|
||||
binding,
|
||||
initial: property.value,
|
||||
unknown,
|
||||
target: slot,
|
||||
path: propertyPath,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user