From 77cf75710369acc6c304ec352ab25aa7274e04b1 Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 Date: Tue, 6 Oct 2026 22:58:43 -0700 Subject: [PATCH] fix(javascript): read substitution-free templates as exact strings Static application analysis read call arguments only from quoted string literals. A template literal without substitutions denotes the same exact string, yet `fetch(`https://x`)`, `localStorage.setItem(`k`)`, `new Worker(`./w.js`)`, and `require(`./dep.js`)` produced no endpoint, an unnamed storage node, or an unresolved reference expression instead of the facts their quoted spellings produce. Electron IPC and data-effect readers already accepted these templates. Read substitution-free templates in the shared static string and argument readers so both spellings produce the same graph. Co-Authored-By: Claude Opus 5.5 --- ...aScriptApplicationTemplateLiterals.test.ts | 69 +++++++++++++++++++ src/domain/javascriptStaticAnalysisHelpers.ts | 19 +++-- 2 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 src/application/JavaScriptApplicationTemplateLiterals.test.ts diff --git a/src/application/JavaScriptApplicationTemplateLiterals.test.ts b/src/application/JavaScriptApplicationTemplateLiterals.test.ts new file mode 100644 index 000000000..2402cf672 --- /dev/null +++ b/src/application/JavaScriptApplicationTemplateLiterals.test.ts @@ -0,0 +1,69 @@ +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/javascriptApplicationAnalysis.js"; +import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js"; + +const graphIdentities = async (source: string) => { + const inputPath = await createTestTempDirectory("rea-js-template-literal-"); + await writeFile(join(inputPath, "app.js"), `${source}\n`); + await writeFile(join(inputPath, "dep.js"), "module.exports = 1;\n"); + await writeFile(join(inputPath, "worker.js"), "self.onmessage = null;\n"); + const result = await analyzeJavaScriptApplication({ + input_path: inputPath, + format: "directory", + }); + if (!result.ok) + throw new Error(`Expected analysis success: ${result.error.message}`); + const { graph } = javascriptApplicationAnalysisResultSchema.parse( + result.value.normalized_result, + ); + return ( + graph.nodes + .filter(({ kind }) => kind !== "javascript-asset" && kind !== "artifact") + // Artifact digests differ between spellings; keep the derived keys. + .map(({ kind, identity }) => ({ + kind, + strategy: identity.strategy, + ...(identity.strategy === "artifact-local-key" + ? { + namespace: identity.namespace, + key: identity.key.replace(/^[0-9a-f]{64}:/u, ""), + } + : {}), + })) + .sort((left, right) => + JSON.stringify(left).localeCompare(JSON.stringify(right)), + ) + ); +}; + +// Each quoted literal has the same length as its template spelling, so +// source locations and every derived identity stay comparable. +it.each([ + "fetch('https://api.example.com/v1/users');", + "new WebSocket('wss://ws.example.com/socket');", + "const xhr = new XMLHttpRequest(); xhr.open('GET', 'https://xhr.example.com/data');", + "localStorage.setItem('token', 1);", + "sessionStorage.getItem('session-id');", + "indexedDB.open('records');", + "new Worker('./worker.js');", + "const dep = require('./dep.js'); dep();", + "import('./dep.js');", + "navigator.serviceWorker.register('./worker.js');", +])( + "reads a template without substitutions as its exact string: %s", + async (quoted) => { + const template = quoted.replaceAll("'", "`"); + const expected = await graphIdentities(quoted); + expect( + expected.some(({ kind }) => + ["endpoint", "storage", "worker", "javascript-module"].includes(kind), + ), + ).toBe(true); + await expect(graphIdentities(template)).resolves.toEqual(expected); + }, +); diff --git a/src/domain/javascriptStaticAnalysisHelpers.ts b/src/domain/javascriptStaticAnalysisHelpers.ts index a1353690a..23045c97a 100644 --- a/src/domain/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascriptStaticAnalysisHelpers.ts @@ -406,19 +406,24 @@ const memberPropertyName = ( ? `[computed@${String(node.property.start ?? -1)}]` : semanticStaticPropertyName(node.property, node.computed); -/** Return a string literal value without evaluating an expression. */ +/** + * Return a string literal value without evaluating an expression. A template + * literal without substitutions denotes the same exact string. + */ export const stringValue = ( node: t.Node | null | undefined, -): string | undefined => (t.isStringLiteral(node) ? node.value : undefined); +): string | undefined => { + if (t.isStringLiteral(node)) return node.value; + if (t.isTemplateLiteral(node) && node.expressions.length === 0) + return node.quasis[0]?.value.cooked ?? undefined; + return undefined; +}; /** Return a string or numeric argument literal. */ export const argumentValue = ( node: t.Node | null | undefined, -): string | undefined => { - if (t.isStringLiteral(node) || t.isNumericLiteral(node)) - return String(node.value); - return undefined; -}; +): string | undefined => + t.isNumericLiteral(node) ? String(node.value) : stringValue(node); /** Prefix a static argument value when one is available. */ export const prefixedArgument = (