fix(javascript): keep recovered invalid templates dynamic in semantic effects (#3815)

dataEffectLiteralString asked the literal owner for an exact value, and
when a template recovered with an invalid escape had no cooked value it
fell back to the raw spelling. Child-process commands, request endpoints
and event names then became resolved values that are not JavaScript
strings.

Use only the owner's cooked value; a template without one stays dynamic.

Fixes #3361

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-14 20:22:16 -07:00
1 parent 0781716788
commit 4135c911d3
2 files changed
+45 -4

No files matched your search

@@ -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"],
});
});
@@ -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;
};