fix(javascript): report SVG script references and skipped bodies (#8627)

Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
This commit is contained in:
hhangandhhang authored and GitHub committed 2026-11-02 15:55:56 +08:00
1 parent f95bdbce76
commit 20a282a181
4 files changed
+175 -6

No files matched your search

@@ -106,7 +106,7 @@ it.each([
['<script language="vbscript">run()</script>', []],
["<!-- <script>run();</script> -->", []],
["<template><script>run();</script></template>", []],
["<svg><script>run();</script></svg>", []],
["<svg><script>run();</script></svg>", ["classic"]],
])("classifies inline JavaScript in %s", (text, expected) => {
expect(
htmlArtifactReferences(text).inlineScripts.map(
@@ -115,6 +115,65 @@ it.each([
).toEqual(expected);
});
it.each([
['src="ignored.js"', "classic"],
['language="vbscript"', "classic"],
['xlink:type="simple"', "classic"],
['type=" Text/JavaScript "', "classic"],
['type="MODULE"', "module"],
['type=" module "', null],
['type="application/json"', null],
['type="text/javascript; charset=utf-8"', null],
['href="external.js"', null],
['href=""', null],
['xlink:href="external.js"', null],
] as const)("classifies SVG inline bodies using %s", (attributes, expected) => {
const result = htmlArtifactReferences(
`<svg><script ${attributes}>run();</script></svg>`,
);
expect(result.inlineScripts.map(({ scriptType }) => scriptType)).toEqual(
expected === null ? [] : [expected],
);
expect(result.inlineScriptCount).toBe(expected === null ? 0 : 1);
});
it.each([
['href="external.js"', ["external.js"]],
['xlink:href="external.js"', ["external.js"]],
['href="first.js" xlink:href="second.js"', ["first.js"]],
['xlink:href="second.js" href="first.js"', ["first.js"]],
['href="" xlink:href="ignored.js"', []],
['other:href="ignored.js"', []],
['src="ignored.js"', []],
])("extracts SVG script URL from %s", (attributes, paths) => {
const result = htmlArtifactReferences(
`<svg><script ${attributes}></script></svg>`,
);
expect(result.scripts.map(({ scriptPath }) => scriptPath)).toEqual(paths);
expect(result.inlineScriptCount).toBe(0);
});
it("shares precise body coordinates and location limits across HTML and SVG", () => {
const prefix = '<!doctype html>\n<svg><script src="ignored.js">';
const body = "\nrun();\n";
expect(
htmlArtifactReferences(`${prefix}${body}</script></svg>`).inlineScripts,
).toEqual([
{
scriptType: "classic",
location: { start: { line: 2, column: 30 }, end: { line: 4, column: 0 } },
},
]);
const repeated = "<script>x</script><svg><script>y</script></svg>".repeat(60);
const result = htmlArtifactReferences(repeated);
expect(result.inlineScriptCount).toBe(120);
expect(result.inlineScripts).toHaveLength(HTML_INLINE_SCRIPT_LOCATION_LIMIT);
expect(
htmlArtifactReferences("<template><svg><script>x</script></svg></template>")
.inlineScriptCount,
).toBe(0);
});
it("retains exact inline script body coordinates, including unclosed scripts", () => {
const prefix = "<!doctype html>\n<script>";
const body = "\nfetch('/api');\n";
@@ -80,6 +80,30 @@ const javaScriptType = (
const asciiLowercase = (value: string): string =>
value.replace(/[A-Z]/gu, (letter) => letter.toLowerCase());
/** SVG uses href/XLink and has no HTML language fallback or src attribute. */
const scriptAttributes = (node: DefaultTreeAdapterTypes.Element) => {
const html = node.namespaceURI === "http://www.w3.org/1999/xhtml";
const source = html
? node.attrs.find(({ name }) => name === "src")
: (node.attrs.find(
({ name, namespace }) => name === "href" && namespace === undefined,
) ??
node.attrs.find(
({ name, namespace }) =>
name === "href" && namespace === "http://www.w3.org/1999/xlink",
));
return {
source,
scriptType: javaScriptType(
html
? node.attrs
: node.attrs.filter(
({ name, namespace }) => name === "type" && namespace === undefined,
),
),
};
};
/**
* Extract inert HTML script references, inline JavaScript bodies, and the
* first document base href.
@@ -105,15 +129,19 @@ export const htmlArtifactReferences = (
if (node === undefined) continue;
if (
defaultTreeAdapter.isElementNode(node) &&
node.namespaceURI === "http://www.w3.org/1999/xhtml"
(node.namespaceURI === "http://www.w3.org/1999/xhtml" ||
node.namespaceURI === "http://www.w3.org/2000/svg")
) {
if (node.tagName === "base" && baseHref === null) {
if (
node.namespaceURI === "http://www.w3.org/1999/xhtml" &&
node.tagName === "base" &&
baseHref === null
) {
const href = node.attrs.find(({ name }) => name === "href");
if (href !== undefined) baseHref = href.value;
}
if (node.tagName === "script") {
const scriptType = javaScriptType(node.attrs);
const src = node.attrs.find(({ name }) => name === "src");
const { scriptType, source: src } = scriptAttributes(node);
const location = node.sourceCodeLocation?.startTag;
if (src !== undefined && src.value !== "" && location !== undefined) {
scripts.push({