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;