Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions __tests__/frameworks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,47 @@ func boot(routes: RoutesBuilder) throws {
const { nodes } = vaporResolver.extract!('configure.swift', src);
expect(nodes).toHaveLength(0);
});

// A `.METHOD(...)` call with many comma-separated args and no `use:` used to
// make the route regex backtrack exponentially (60 args hung for minutes).
it('does not backtrack exponentially on a long arg list without use:', () => {
const args = Array.from({ length: 60 }, (_, i) => `arg${i}: value${i}`).join(', ');
const src = `app.get(${args})\n`;
const start = performance.now();
const { nodes } = vaporResolver.extract!('routes.swift', src);
const elapsed = performance.now() - start;
expect(nodes).toHaveLength(0);
expect(elapsed).toBeLessThan(250);
});

it('still parses every Vapor route shape after the arg-list rewrite', () => {
const src = `
admin.get(use: self.list)
app.get("users", use: listUsers)
router.post("users", User.parameter, "edit", use: UserController.edit)
app.patch(":id" , "meta" , use: update)
app.get(
"multi",
"line",
use: multiLine
)
`;
const { nodes, references } = vaporResolver.extract!('routes.swift', src);
expect(nodes.map((n) => n.name)).toEqual([
'GET /',
'GET /users',
'POST /users/edit',
'PATCH /:id/meta',
'GET /multi/line',
]);
expect(references.map((r) => r.referenceName)).toEqual([
'list',
'listUsers',
'edit',
'update',
'multiLine',
]);
});
});

import { reactResolver } from '../src/resolution/frameworks/react';
Expand Down
12 changes: 11 additions & 1 deletion src/resolution/frameworks/swift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,17 @@ 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;
// Each arg repetition must end at a comma, and `,` is outside the char class,
// so the split is unique and matching stays linear. The earlier
// `(?:[^,()]+,\s*)*` was ambiguous — the trailing `\s*` and the next
// iteration's `[^,()]+` could both claim the same spaces — which backtracked
// exponentially on a long arg list that never reaches `use:`.
// The tail is `\s*` rather than a lazy `[^,()]*?` on purpose: both are
// linear, but the lazy form drops the "`use:` is preceded by a comma"
// requirement and widens the match set — `req.get(foo.use: bar)` would then
// be indexed as a route (groups `["req","get","foo.","bar"]`) where both
// this pattern and the original match nothing.
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;
Expand Down