diff --git a/src/domain/javascript/javascriptSemanticAnalysis.invalid_templates.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.invalid_templates.test.ts new file mode 100644 index 0000000000..ea50159a9a --- /dev/null +++ b/src/domain/javascript/javascriptSemanticAnalysis.invalid_templates.test.ts @@ -0,0 +1,38 @@ +import { expect, it } from "vitest"; + +import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; + +const effects = (call: (value: string) => string, value: string) => { + const ir = analyzeJavaScriptSemantics(` + const { spawn } = require("child_process"); + const { EventEmitter } = require("events"); + ${call(value)} + `); + return { + commands: ir.childProcessSpawns.map(({ command }) => command), + endpoints: ir.requestOperations.map(({ endpoint }) => endpoint), + events: ir.eventOperations.map(({ eventName }) => eventName), + }; +}; + +const calls = (value: string) => + [ + `spawn(${value});`, + `fetch(${value});`, + `new EventEmitter().on(${value}, () => 1);`, + ].join("\n"); + +it("keeps recovered templates without a cooked value dynamic", () => { + // `\\unicode` is an invalid escape: Babel recovers it with `cooked: null`. + expect(effects(calls, "`\\unicode`")).toEqual({ + commands: [null], + endpoints: [null], + events: [null], + }); + // Valid templates keep their cooked value, including escapes. + expect(effects(calls, "`ready\\n`")).toEqual({ + commands: ["ready\n"], + endpoints: ["ready\n"], + events: ["ready\n"], + }); +}); diff --git a/src/domain/javascript/javascriptSemanticDataEffectHelpers.ts b/src/domain/javascript/javascriptSemanticDataEffectHelpers.ts index 9112eacd16..5c3ed9ddd4 100644 --- a/src/domain/javascript/javascriptSemanticDataEffectHelpers.ts +++ b/src/domain/javascript/javascriptSemanticDataEffectHelpers.ts @@ -924,7 +924,11 @@ export const dataEffectPrimitive = ( return null; }; -/** Read one static string or expression-free template. */ +/** + * Read one static string or expression-free template. A template recovered + * with an invalid escape has no cooked value; its raw spelling is not a + * JavaScript string, so it stays dynamic. + */ export const dataEffectLiteralString = ( node: | t.Expression @@ -935,9 +939,8 @@ export const dataEffectLiteralString = ( ): string | null => { if (node === undefined) return null; const literal = readExactJavaScriptLiteral(node); - if (literal.found && typeof literal.value === "string") return literal.value; - return t.isTemplateLiteral(node) && node.expressions.length === 0 - ? (node.quasis[0]?.value.raw ?? null) + return literal.found && typeof literal.value === "string" + ? literal.value : null; };