From f3fb1387724382e5cb0620616baf964643092707 Mon Sep 17 00:00:00 2001 From: pucedoteth <119044801+pucedoteth@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:56:31 +0200 Subject: [PATCH] fix(resolution): remove catastrophic backtracking in the Vapor route regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repeated group was `(?:[^,()]+,\s*)*`. `[^,()]+` also matches whitespace, so every space after a comma could be consumed either by `[^,()]+` on the next iteration or by `\s*` on the current one. On a `.get(...)`/`.post(...)` call whose argument list never reaches `use:`, the match fails and the engine explores every split: args ms 16 1.7 20 23.8 24 379.7 28 6100.1 That is ~4x per additional argument, so a generated Swift file with a few dozen arguments stalls index/sync/MCP indefinitely. Keeping the loop to `,` and matching the gap before `use:` once, outside the loop, leaves exactly one way to split the input: 2000 arguments now match in 0.2ms. Behaviour is unchanged — `segJoin` only pulls `"quoted"` literals out of the captured group, so moving the whitespace out of it cannot alter a route path, and the extracted receiver/method/path/handler are identical across the real Vapor call shapes (bare `use:`, string segments, non-string segments like `BlogUser.parameter`, spaced arguments, and the `Environment.get("X")` / `req.parameters.get("id")` non-matches). Fixes #1544 --- __tests__/frameworks.test.ts | 13 +++++++++++++ src/resolution/frameworks/swift.ts | 9 ++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/__tests__/frameworks.test.ts b/__tests__/frameworks.test.ts index cc7e3555f..4129343a5 100644 --- a/__tests__/frameworks.test.ts +++ b/__tests__/frameworks.test.ts @@ -1436,6 +1436,19 @@ describe('vaporResolver.extract', () => { expect(references[0].referenceName).toBe('listUsers'); }); + it('does not backtrack exponentially on a call that never reaches use: (#1544)', () => { + // `app.get(a, b, c, ... )` with no `use:` label: the route regex must fail + // fast. The previous pattern had `\s*` inside the repeated group, which + // overlapped `[^,()]+`, so each extra argument roughly quadrupled the work + // and a generated file could hang indexing for minutes. + const args = Array.from({ length: 40 }, (_, i) => `arg${i}, `).join(''); + const src = `app.get(${args}x)\n`; + const started = Date.now(); + const { nodes } = vaporResolver.extract!('routes.swift', src); + expect(Date.now() - started).toBeLessThan(1000); + expect(nodes).toEqual([]); + }); + it('extracts grouped RouteCollection routes with the group prefix and no path arg', () => { const src = ` func boot(routes: RoutesBuilder) throws { diff --git a/src/resolution/frameworks/swift.ts b/src/resolution/frameworks/swift.ts index 0dd1513aa..6b7f91f41 100644 --- a/src/resolution/frameworks/swift.ts +++ b/src/resolution/frameworks/swift.ts @@ -367,7 +367,14 @@ export const vaporResolver: FrameworkResolver = { // (`BlogUser.parameter`, `:id`, a path constant) so accept any comma-separated // args before `use:` — the label keeps only the string parts. `use:` // discriminates a real route from Environment.get("X")/req.parameters.get("X"). - const routeRegex = /\b(\w+)\.(get|post|put|patch|delete|head|options)\s*\(\s*((?:[^,()]+,\s*)*)use:\s*([A-Za-z_][\w.]*)/g; + // The `\s*` that used to sit inside the repeated group overlapped + // `[^,()]+` (which also matches whitespace), so every space after a comma + // could be consumed by either branch. On a call whose argument list never + // reaches `use:` that ambiguity backtracks exponentially — ~4x per extra + // argument, seconds by 28 — hanging index/sync/MCP on generated files. + // Keeping the group purely `,` and matching the gap before `use:` + // once, outside the loop, leaves exactly one way to split the input. + const routeRegex = /\b(\w+)\.(get|post|put|patch|delete|head|options)\s*\(\s*((?:[^,()]+,)*)\s*use:\s*([A-Za-z_][\w.]*)/g; let match: RegExpExecArray | null; while ((match = routeRegex.exec(safe)) !== null) { const [, receiver, method, segsStr, handlerExpr] = match;