From e995b19f834ba78faa294f5959fd3ad8a97290ca Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Fri, 9 Oct 2026 12:20:29 -0400 Subject: [PATCH] fix(javascript): preserve semantic evidence through satisfies --- .../JavaScriptApplicationSatisfies.test.ts | 57 +++++++++++++++++++ .../javascriptSemanticCallResolution.ts | 3 +- .../javascript/javascriptSemanticReturns.ts | 3 +- .../javascript/javascriptSemanticValues.ts | 3 +- 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 src/application/javascript/JavaScriptApplicationSatisfies.test.ts diff --git a/src/application/javascript/JavaScriptApplicationSatisfies.test.ts b/src/application/javascript/JavaScriptApplicationSatisfies.test.ts new file mode 100644 index 000000000..8507180f1 --- /dev/null +++ b/src/application/javascript/JavaScriptApplicationSatisfies.test.ts @@ -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"); +}); diff --git a/src/domain/javascript/javascriptSemanticCallResolution.ts b/src/domain/javascript/javascriptSemanticCallResolution.ts index 246472301..24673279a 100644 --- a/src/domain/javascript/javascriptSemanticCallResolution.ts +++ b/src/domain/javascript/javascriptSemanticCallResolution.ts @@ -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)) diff --git a/src/domain/javascript/javascriptSemanticReturns.ts b/src/domain/javascript/javascriptSemanticReturns.ts index 5a1a3f415..13e68279a 100644 --- a/src/domain/javascript/javascriptSemanticReturns.ts +++ b/src/domain/javascript/javascriptSemanticReturns.ts @@ -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 []; diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index 791d3c388..2fdebe512 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -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));