Repository navigation
fix(router)!: reject empty segments inside :name+ / :name* like URLPattern - #241
Conversation
…RLPattern every segment a `:name+`, `:name*` or `**:name` takes now needs a value (`[^/]+(?:/[^/]+)*`), in the interpreter, the compiled matchers, `routeToRegExp` and `compareRoutes`.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (5)No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (26)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughNamed catch-all and repeated-parameter captures now reject empty path segments. The change updates matching guards, regular-expression generation and reversal, route overlap comparisons, ordering checks, tests, and documentation. ChangesNamed Catch-All Segment Semantics
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to The stricter named-capture behavior and ordering exceptions are documented and covered by regression tests. No actionable merge-blocking issue is established; merge after normal checks pass. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to Paths containing empty segments can stop matching a named route while still matching an existing wildcard fallback. No access-control bypass was established, but applications using these patterns for access rules should validate fallback behavior before upgrading. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 19 files. (7 skipped: 7 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit checks each path with care, Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Parity across findRoute, findAllRoutes, JIT, AOT and routeToRegExp for `/a/:p(.*)` vs `/a/:p+` (also `:p*`, `**:p`, suffix and in-segment forms) on `/a//b`, `/a/b//`, `/a/b//c`, `/a/`, and side by side (`:p+` wins where both match, both orders). Core bundle budget on top of #241: 13068 B / 5622 B gzip (+636 B / +281 B).
Every segment a
:name+,:name*or**:nametakes now needs a value, as URLPattern's:name+([^/]+(?:/[^/]+)*). This removes the "Empty segments in:name+" row from the README differences table.Warning
Breaking: paths with an empty segment inside the value no longer match these routes. They fall through to less specific routes (a
**, a*), or match nothing.Before / after
/foo/:bar+/foo//a{ bar: "/a" }/foo/:bar+/foo/a//b{ bar: "a//b" }/foo/:bar+/foo///{ bar: "/" }/foo/:bar*/foo/a//{ bar: "a/" }/foo/:bar+/baz/foo/a//b/baz{ bar: "a//b" }/foo/**:bar/foo//a{ bar: "/a" }/foo/:bar+/foo/a/b/{ bar: "a/b" }{ bar: "a/b" }(unchanged: one trailing slash is still ignored)/foo/**/foo//a{ 0: "/a" }{ 0: "/a" }(unchanged:**,*and constraints still take empty segments)A user-written
**:namefollows the same rule. It is the same tree entry as:name+, and its docs already said it "needs a value".Changes
emptyParamrejects a**:namevalue with an empty segment, not just"". It reuses the existing slow path: only paths that contain an empty segment reach it (segments.includes("")), so other paths pay nothing new.**:namematcher is guarded by a small loop helper (VALUES) in a data slot, likeRANK. Only lookups that reach a**:namematcher run it. In a microbenchmark it was faster thanArray#lastIndexOf("")or ap.includes("//")flag computed for every lookup. On lookups resolved by a multi-segment**:nameit costs about 5 to 10% (other lookups are unchanged).routeToRegExp: a**:name/:name+/:name*is now[^/]+(?:/[^/]+)*(lazy…*?before optional segments). It can't end in/, so these routes get a plain/?$ending:(?://|…)ending and its "group unset where the router reports/" trade-off are gone, along withisRequiredSegmentGapin the capture sweep;/a/**:r/:y?,/:x+/{b}?, …) no longer need a look-behind, so they now work in RE2 engines.regExpToRoutereads the new form, and the 0.11 forms still read as:x+/:x*.compareRoutes/ overlap: asometail is nowNON_EMPTYin every segment, not only when it is a single segment. New verdicts:/a/**⊋/a/**:x/a/:x/**⊋/a/:y+/**/:y/p⊋/**:n/p/a/**:restvs/a/b/**is nowpartial(it wassuperset, which is wrong under the new rule)New ordering carve-out (B)
The new containment conflicts with the traversal order.
/p/:x/**(and/p/:x/*,/:a/**/p,/**/:y/p) is now a strict superset of/p/:x+(and/**:n/p). The tree still lists a node's wildcard before its param child, and a suffix trie's shallower entry first. As a result,findAllRouteslists the narrower:x+route first andfindRoutepicks the broader one.This PR does not change the router's priority: on
/p/a/b,/p/:x/**still wins over/p/:x+, as before. The case is documented as carve-out B (README "Result ordering",.agents/matching.md) and pinned infind-all.test.ts. The ordering sweeps infind-all.test.tsandsuffix.test.tsaccept exactly this class. A real fix would be the same global re-sort that carve-out A3 needs.Tests
find.test.ts: interpreter,findAllRoutes, JIT, AOT, matchAll, androuteToRegExpagainst the router, per route.EMPTY_SEGMENT_CASESinwpt.test.ts, checked against Node 24's URLPattern.EMPTY_SEGMENT_DIFFSis gone because no diff is left.router.test.ts,find-all.test.ts, and the regexp fixtures.regexp-to-route.test.ts.pnpm test(lint, types, vitest) passes: 3285 tests.bundle.test.tsexplains why.🤖 Generated with AI assistant
Summary by CodeRabbit
**:name,:name+, and:name*) now reject values containing empty path segments, such as those created by doubled slashes. Bare wildcards retain their distinct matching behavior.