fix(javascript): preserve semantic evidence through satisfies

This commit is contained in:
Rudy Celekli 2026-10-09 12:20:29 -04:00 committed by morluto
parent 4896adcd0f
commit e995b19f83
4 changed files with 63 additions and 3 deletions

View File

@ -0,0 +1,57 @@
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 { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js";
import { topLevelBinding } from "../../domain/javascript/javascriptSemanticAnalysis.fixture.js";
import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js";
it.each([
'"ready" satisfies string',
'("ready" satisfies string) as string',
'("ready" satisfies string)!',
])("retains the runtime primitive through %s", (expression) => {
const ir = analyzeJavaScriptSemantics(`const value = ${expression};`);
expect(topLevelBinding(ir, "value").value).toEqual({
status: "literal",
value: "ready",
});
});
it("retains exported constant evidence through a type-only satisfies expression", async () => {
const root = await createTestTempDirectory("rea-ts-satisfies-");
await writeFile(
join(root, "app.ts"),
'export const value = "ready" satisfies string;\n',
);
const result = await analyzeJavaScriptApplication({
input_path: root,
format: "directory",
});
if (!result.ok) throw result.error;
const analysis = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
expect(
analysis.semantic_graph.nodes.filter(
({ kind, properties }) =>
kind === "literal" && properties.value === "ready",
),
).toHaveLength(1);
});
it("retains local call targets and exported callable links through satisfies", () => {
const ir = analyzeJavaScriptSemantics(
'export const render = (() => "ready") satisfies (() => string); render();',
);
const callable = ir.callables[0];
if (callable === undefined) throw new Error("Missing callable");
expect(
ir.moduleLinks.find(({ localName }) => localName === "render")?.callableId,
).toBe(callable.callableId);
expect(ir.callSites[0]?.calleeCallableIds).toEqual([callable.callableId]);
expect(ir.callSites[0]?.resolution).toBe("exact");
});

View File

@ -62,7 +62,8 @@ const resolveCallables = (
if (
t.isTSAsExpression(node) ||
t.isTSTypeAssertion(node) ||
t.isTSNonNullExpression(node)
t.isTSNonNullExpression(node) ||
t.isTSSatisfiesExpression(node)
)
return resolveCallables(node.expression, context);
if (t.isConditionalExpression(node) || t.isLogicalExpression(node))

View File

@ -188,7 +188,8 @@ const callableIdsForNode = (
if (
t.isTSAsExpression(node) ||
t.isTSTypeAssertion(node) ||
t.isTSNonNullExpression(node)
t.isTSNonNullExpression(node) ||
t.isTSSatisfiesExpression(node)
)
return callableIdsForNode(node.expression, state, admitted, seen);
return [];

View File

@ -199,7 +199,8 @@ const evaluateExpression = (
if (
(t.isTSAsExpression(node) ||
t.isTSTypeAssertion(node) ||
t.isTSNonNullExpression(node)) &&
t.isTSNonNullExpression(node) ||
t.isTSSatisfiesExpression(node)) &&
t.isExpression(node.expression)
)
return evaluateExpression(node.expression, nestedContext(context));