From cc7cc8155a245d537ead2b391255bc213a4a1a0d Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:37:10 +0800 Subject: [PATCH] test(javascript): bound route scan work independently of timing (#7016) --- .../javascript/javascriptRouteScan.test.ts | 65 +++++++++++++++++++ ...riptStaticAnalysis.routeProperties.test.ts | 20 +++--- 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/domain/javascript/javascriptRouteScan.test.ts b/src/domain/javascript/javascriptRouteScan.test.ts index 7719076c94..ffa11558b4 100644 --- a/src/domain/javascript/javascriptRouteScan.test.ts +++ b/src/domain/javascript/javascriptRouteScan.test.ts @@ -1,3 +1,4 @@ +import * as t from "@babel/types"; import { expect, it } from "vitest"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js"; @@ -30,6 +31,70 @@ const analyze = (source: string, budget?: number) => { return complete(scan.finishSteps()); }; +it("reads route-record keys at most once beyond inspecting each property", () => { + const count = 128; + const source = ["candidate", "router"] + .map( + (name) => + `const ${name} = { ${Array.from({ length: count }, (_, i) => `path: "${name}${String(i)}"`).join(", ")}${name === "router" ? ", component: Page" : ""} };`, + ) + .join("\n"); + const file = parseJavaScriptSource(source); + if (file === null) throw new Error("fixture must parse"); + const index = complete( + indexJavaScriptBindingsWithReceiversSteps(file.program), + ); + complete(index.indexAmbientWritesSteps()); + const facts: { value: string; interpretation: "candidate" | undefined }[] = + []; + const scan = complete( + JavaScriptRouteScan.createSteps(file, index, (fact) => { + facts.push({ value: fact.value, interpretation: fact.interpretation }); + }), + ); + let inspecting = false; + let propertyCount = 0; + let keyReads = 0; + traverseJavaScriptAst(file, { + enter: (node) => { + if (!t.isObjectProperty(node)) return; + propertyCount += 1; + const key = node.key; + // Observe real parsed keys without changing their values. Count only + // owner work, excluding the AST walk's own reads and setup/indexing. + Object.defineProperty(node, "key", { + enumerable: true, + configurable: true, + get: () => { + if (inspecting) keyReads += 1; + return key; + }, + }); + }, + }); + traverseJavaScriptAst(file, { + enter: (node, parent) => { + inspecting = true; + try { + scan.inspect(node, parent); + } finally { + inspecting = false; + } + }, + }); + expect(facts).toEqual( + ["candidate", "router"].flatMap((name) => + Array.from({ length: count }, (_, i) => ({ + value: `${name}${String(i)}`, + interpretation: name === "candidate" ? "candidate" : undefined, + })), + ), + ); + expect(propertyCount).toBe(count * 2 + 1); + expect(keyReads).toBeGreaterThanOrEqual(propertyCount); + expect(keyReads).toBeLessThanOrEqual(propertyCount * 2); +}); + it.each(["true", "(true as const)", "INDEX"])( "retains an index route with flag %s", (flag) => { diff --git a/src/domain/javascript/javascriptStaticAnalysis.routeProperties.test.ts b/src/domain/javascript/javascriptStaticAnalysis.routeProperties.test.ts index 96dfbe86a6..801591159b 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.routeProperties.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.routeProperties.test.ts @@ -70,14 +70,16 @@ it("reports path properties outside router records only as candidates", () => { ]); }); -it("scans an object's route fields once for repeated path keys", () => { - const start = performance.now(); +it("preserves every repeated path property's candidate and source order", () => { + const count = 256; const endpoints = routeEndpoints( - `const record = { ${Array.from({ length: 20_000 }, (_, i) => `path: "p${String(i)}"`).join(", ")} };`, + `const record = { ${Array.from({ length: count }, (_, i) => `path: "p${String(i)}"`).join(", ")} };`, ); - expect(endpoints).toHaveLength(20_000); - expect( - endpoints.every(({ interpretation }) => interpretation === "candidate"), - ).toBe(true); - expect(performance.now() - start).toBeLessThan(2000); -}, 30000); + expect(endpoints).toEqual( + Array.from({ length: count }, (_, i) => ({ + value: `p${String(i)}`, + mechanism: "property:path", + interpretation: "candidate", + })), + ); +});