From 602d0021d45a6cdab9f806554f3ee12ea670ec97 Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:25:48 -0700 Subject: [PATCH] fix(javascript): keep computed-method XHR request URLs A computed XHR method, as in `xhr.open(method, "/api")`, no longer discarded an endpoint that main reports. Keep the URL when it is an HTTP(S)/WebSocket URL or a path; fs flags and window targets such as `r` and `_blank` still produce no endpoint. Co-Authored-By: Claude Opus 5.5 --- src/domain/javascriptStaticAnalysisHelpers.ts | 33 ++++++++++++------- src/domain/webBundleAnalyzer.test.ts | 9 ++++- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/domain/javascriptStaticAnalysisHelpers.ts b/src/domain/javascriptStaticAnalysisHelpers.ts index 5461e5ba4..8400862a1 100644 --- a/src/domain/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascriptStaticAnalysisHelpers.ts @@ -356,18 +356,31 @@ const XHR_METHODS = new Set([ "TRACE", ]); +/** An absolute HTTP(S)/WebSocket URL, or an absolute or relative path. */ +const isRequestUrlLiteral = (value: string): boolean => + /^(?:https?|wss?):\/\//iu.test(value) || + value.startsWith("/") || + value.startsWith("./") || + value.startsWith("../"); + /** - * Require a literal HTTP method: `fs.open(path, "r")` and - * `window.open(url, "_blank")` share the callee name but take no URL second. + * Read `XMLHttpRequest.open(method, url)`. `fs.open(path, "r")` and + * `window.open(url, "_blank")` share the callee name, so a literal method + * must be an HTTP method, and a computed method needs a request URL literal. * XHR normalizes the standard methods' case; extension methods such as * WebDAV `PROPFIND` are conventionally uppercase tokens. */ -const isXhrMethodArgument = (node: t.Node | null | undefined): boolean => { - const value = stringValue(node); - return ( - value !== undefined && - (XHR_METHODS.has(value.toUpperCase()) || /^[A-Z][A-Z-]*$/u.test(value)) - ); +const xhrOpenUrl = ( + methodNode: t.Node | null | undefined, + urlNode: t.Node | null | undefined, +): string | undefined => { + const url = stringValue(urlNode); + if (url === undefined) return undefined; + const method = stringValue(methodNode); + if (method === undefined) return isRequestUrlLiteral(url) ? url : undefined; + return XHR_METHODS.has(method.toUpperCase()) || /^[A-Z][A-Z-]*$/u.test(method) + ? url + : undefined; }; /** Select the literal URL argument for recognized network callees. */ @@ -382,9 +395,7 @@ export const endpointArgument = ( ): string | undefined => { if (name === "fetch" || name.endsWith(".fetch") || name === "WebSocket") return stringValue(args[0]); - // `XMLHttpRequest.open(method, url)`. - if (name.endsWith(".open") && isXhrMethodArgument(args[0])) - return stringValue(args[1]); + if (name.endsWith(".open")) return xhrOpenUrl(args[0], args[1]); if ( ["get", "post", "put", "patch", "delete", "request"].some( (method) => name === method || name.endsWith(`.${method}`), diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index ed1bee5d3..77c5df458 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -183,11 +183,18 @@ describe("web bundle static-analysis parity", () => { fs.open("/tmp/data.txt", "r", done); window.open("https://help.example.test/", "_blank"); xhr.open(method, "/dynamic-method"); + fs.open(path, "r", done); + window.open(url, "_blank"); `), ); expect( result.observations.endpoints.map(({ value }) => value).sort(), - ).toEqual(["/dav/", "/xhr-get", "https://xhr.example.test/submit"]); + ).toEqual([ + "/dav/", + "/dynamic-method", + "/xhr-get", + "https://xhr.example.test/submit", + ]); }); it("recognizes loadURL endpoints like static analysis", () => {