Repository navigation
feat: add route pattern overlap utilities - #183
Conversation
Add routesOverlap() and findOverlappingRoutes() for reasoning about patterns against patterns (do their match-sets intersect), using rou3's own segment/radix semantics so behavior stays consistent with findRoute/findAllRoutes. - routesOverlap(a, b): pure, router-free boolean overlap check - findOverlappingRoutes(ctx, method, pattern): findAllRoutes over a pattern scope, least->most specific - src/_overlap.ts: RouteShape model + shape intersection; reuses the addRoute pipeline (getParamRegexp now exported from add.ts) - static/static and static/regex precise; regex/regex and regex/any over-approximated to "overlaps" (conservative default) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds route-pattern overlap detection with public API, docs, and tests. Separately, wildcard end-of-path matching now filters end-at-path wildcard matches by optionality, and compiler snapshots and root-path normalization were updated. ChangesRoute overlap feature
Wildcard end-of-path matching fix
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 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. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/_overlap.ts (2)
65-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider relocating this internal helper to the end of the file.
segmentsToShapeis non-exported but sits between the exportedrouteToShapesandmethodDataToShape. Repository convention is to keep internal helpers at the bottom.As per coding guidelines: "Place non-exported/internal helpers at the end of the file."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/_overlap.ts` around lines 65 - 112, Move the internal helper segmentsToShape out of the middle of the module and place it at the end of the file so it follows the repository convention for non-exported helpers. Keep the exported routeToShapes and methodDataToShape in their current order, and relocate only the private segmentsToShape implementation without changing its behavior or references.Source: Coding guidelines
76-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer
charCodeAt()over.startsWith()for the wildcard check.To stay consistent with the router's performance-first style, use
charCodeAtfor this prefix test.As per coding guidelines: "Use
charCodeAt()instead of.startsWith()... in performance-sensitive code."♻️ Proposed tweak
- if (seg.startsWith("**")) { + if (seg.charCodeAt(0) === 42 /* '*' */ && seg.charCodeAt(1) === 42) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/_overlap.ts` at line 76, The wildcard prefix check currently uses seg.startsWith("**"), which should be replaced with a charCodeAt()-based test to match the router’s performance-first style. Update the prefix logic in the overlap handling path where seg is inspected so it uses direct character code comparison instead of .startsWith(), keeping the same behavior for the "**" case while avoiding the slower string helper.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 173-190: The README example imports only routesOverlap and
findOverlappingRoutes but also uses createRouter and addRoute, so the snippet is
incomplete. Update the example import statement to include the router helpers
referenced in the snippet, using the existing symbols createRouter and addRoute
so the sample runs as written.
---
Nitpick comments:
In `@src/_overlap.ts`:
- Around line 65-112: Move the internal helper segmentsToShape out of the middle
of the module and place it at the end of the file so it follows the repository
convention for non-exported helpers. Keep the exported routeToShapes and
methodDataToShape in their current order, and relocate only the private
segmentsToShape implementation without changing its behavior or references.
- Line 76: The wildcard prefix check currently uses seg.startsWith("**"), which
should be replaced with a charCodeAt()-based test to match the router’s
performance-first style. Update the prefix logic in the overlap handling path
where seg is inspected so it uses direct character code comparison instead of
.startsWith(), keeping the same behavior for the "**" case while avoiding the
slower string helper.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 51f53b83-ab01-4a48-88ff-53722fdc5e16
📒 Files selected for processing (7)
AGENTS.mdREADME.mdsrc/_overlap.tssrc/index.tssrc/operations/add.tssrc/operations/overlap.tstest/overlap.test.ts
Interpreter findAllRoutes pushed wildcard methods unconditionally, matching /a/**:rest against /a — disagreeing with both findRoute and the compiled matchAll router, which require one segment for non-optional wildcards. Bundle budget raised 5.7k->5.8k for the core fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the parallel shape decoders with a single path: query patterns go
through a throwaway addRoute, and shapes are read back from tree entries via
kind-tagged edges + paramsMap (cached per entry in a WeakMap). Fixes
escaped-literal routes (/a/\* stored as static "*") being classified as
dynamic, dedupes results of expanded routes (:x?, {s}?) by matched data
value, prunes unreachable static subtrees, deletes _overlap.ts, and reverts
the getParamRegexp export — core bundle is no longer affected (add.ts and
types.ts untouched).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keeps both modules under the 200 LoC guideline: _overlap.ts holds the shape model (RouteShape, shapeOf, shapesOverlap), operations/overlap.ts the public API and tree traversal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`p.split("/")` turns "/" into ["",""], a phantom trailing segment that made
the compiled router match required root wildcards and params at "/":
`/**:name` and `/:x` both wrongly matched "/", disagreeing with the
interpreter and findRoute.
Let root collapse to "" in the compiled prologue (drop the `|| "/"` so
`"".split("/")` has no phantom segment). Build-time change, zero added
per-call runtime overhead. Covered by an interpreter-vs-compiled root-path
parity test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
test/overlap.test.ts (1)
159-169: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd exact-scope assertions for expanded optional routes.
/a/**overlaps both expanded shapes, so this test would still pass if the cached/computed shape were accidentally broadened. Add narrow checks like/aand/a/bto exercise both expansion entries independently.Test hardening
// `:x?` / `{/c}?` expand to several tree entries sharing one data reference. + expect(findOverlappingRoutes(router, "GET", "/a").map((m) => m.data)).toEqual([opt]); + expect(findOverlappingRoutes(router, "GET", "/a/b").map((m) => m.data)).toEqual([opt]); expect(findOverlappingRoutes(router, "GET", "/a/**").map((m) => m.data)).toEqual([opt]); + expect(findOverlappingRoutes(router, "GET", "/b").map((m) => m.data)).toEqual([grp]); + expect(findOverlappingRoutes(router, "GET", "/b/c").map((m) => m.data)).toEqual([grp]); expect(findOverlappingRoutes(router, "GET", "/b/**").map((m) => m.data)).toEqual([grp]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/overlap.test.ts` around lines 159 - 169, The optional-route overlap test for findOverlappingRoutes only checks the broad /a/** and /b/** patterns, so it can miss regressions where the expanded shapes are widened incorrectly. Update the test around createRouter, addRoute, and findOverlappingRoutes to add exact-scope assertions for the expanded entries, such as checking /a and /a/b separately for the /a/:x? case (and the equivalent narrow paths for /b{/c}?) so each expansion is exercised independently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@test/overlap.test.ts`:
- Around line 159-169: The optional-route overlap test for findOverlappingRoutes
only checks the broad /a/** and /b/** patterns, so it can miss regressions where
the expanded shapes are widened incorrectly. Update the test around
createRouter, addRoute, and findOverlappingRoutes to add exact-scope assertions
for the expanded entries, such as checking /a and /a/b separately for the /a/:x?
case (and the equivalent narrow paths for /b{/c}?) so each expansion is
exercised independently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 89a0c161-8ae0-465d-9edd-6c8c2fe1adb9
📒 Files selected for processing (10)
AGENTS.mdREADME.mdsrc/_overlap.tssrc/compiler.tssrc/operations/overlap.tstest/.snapshot/compiled-all.mjstest/.snapshot/compiled-aot.mjstest/.snapshot/compiled-jit.mjstest/find-all.test.tstest/overlap.test.ts
✅ Files skipped from review due to trivial changes (3)
- test/.snapshot/compiled-all.mjs
- AGENTS.md
- README.md
🚧 Files skipped from review as they are similar to previous changes (1)
- src/operations/overlap.ts
… root parity Post-review follow-ups: - compiler: root static now matches both "" and the once-stripped "//" so a root `/` route keeps interpreter/compiled parity on the doubled-slash path (root-only condition, no per-call cost on other routers). Broad sweep drops from 54 -> 18 interpreter-vs-compiled divergences; the remaining 18 are all pre-existing (malformed `//` + required root wildcard/param, and the `/:id/**` param-zero-segment findRoute quirk) — none newly introduced. - overlap tests: add coverage for the regex-constrained query pruning branch, any-segment query fan-out across static siblings, tail reaching deep statics + trailing-`*` prune boundary, single-node wildcard->param->static->self ordering, and reference-identity dedup for function data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/compiler.ts`:
- Around line 89-96: The compiled matcher in compiler.ts is still using the raw
path string, so root param/wildcard behavior can diverge from
findRoute/findAllRoutes. Update the matcher generation around the key/nk
condition to use splitPath() the same way the interpreter does, and then base
the compiled route comparison on that normalized path representation so "//" and
"/" resolve consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2b0decce-f858-4459-9f78-c455c467caed
📒 Files selected for processing (2)
src/compiler.tstest/overlap.test.ts
…y segments
Address CodeRabbit review: the compiled matcher counted a phantom trailing
segment for paths that keep a trailing empty after the slash strip ("//",
"/a//", ...), so required root/nested wildcards and params matched where the
interpreter's splitPath() rejects them. Drop the trailing empty segment in the
compiled tree walk (guarded by length>1 so the leading-empty root case keeps
0 real segments without over-collapsing). Root static still matches "//" via
the un-split ctx.static fast path, mirroring the interpreter's dual handling.
Broad interpreter-vs-compiled sweep now diverges only on the pre-existing
/:id/** findRoute bug (tracked in #184). Regression tests added via the
_findAllRoutes dual-validation helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Motivation
rou3only matches a concrete path against registered patterns (findRoute/findAllRoutes). Some use cases need to reason about patterns against patterns: given registered route patterns, know when two patterns can match a common concrete path so you can resolve an "effective" merged config over a whole scope.Example (inherited/merged config):
/protected/feed/**overlaps all three, but there's no single concrete path you can hand tofindAllRoutesthat reliably represents "the whole scope of pattern P". The prior workaround (synthesizing a fake path like/protected/feed/_) silently ignores richer syntax (*.png,:id(\d+),{s}?,:x?,\:) and samples only one representative path.What this adds
Two utilities computed with rou3's own segment/radix semantics (exported from
rou3):routesOverlap(patternA, patternB): boolean— pure, router-free.trueiff there exists a concrete path matched by both patterns (overlap, not subset).findOverlappingRoutes(ctx, method, pattern): MatchedRoute[]— likefindAllRoutes, but the query is a pattern (scope) instead of a concrete path. Returns every registered route whose match-set intersects it, ordered least→most specific, with the same method handling (method→""bucket). Matches carry onlydata(a scope has no single concrete path → noparams). A single route that optional/group syntax expanded into several tree entries (sharing onedatareference) is reported once; distinct routes are always reported separately, even when they carry an equal primitivedatavalue (or none).Design
src/operations/overlap.tsis fully self-contained and tree-shakeable — the core bundle is unaffected (add.ts/types.tsuntouched). Each pattern is modeled as aRouteShape: fixed single-segment matchers (stringliteral |RegExp| any) + a variable tail[min,max]. Tails match any values, so overlap reduces to a shared-prefix value check plus an integer length-range intersection.addRoute, and shapes are read back from tree entries (kind-tagged edges +paramsMap) — so queries and registered routes are classified by the exact same pipeline, including escaped literals (/a/\*is the static segment*). Registered-entry shapes are cached in aWeakMap(only the persistentctxpath benefits — the transient query router computes shapes directly, so it pays no cache overhead).findOverlappingRouteswalks the tree infindAllRoutesorder (least→most specific), prunes static subtrees the query can't reach, and collapses only genuine reference-duplicates (one route expanded by optional/group syntax into several entries sharing adatareference). Distinct routes with equal-or-absent primitivedataare never merged — the dedup key is data identity, not value, so no scope is silently dropped.Semantics (verified against the live router)
**= 0+ segments,**:name= 1+, trailing bare*= 0-or-1, mid*/:name= exactly 1./user/:id(\d+)does not overlap/user/abc); regex/regex and regex/any over-approximate to "overlaps" — the safe conservative default, since exact regex intersection is undecidable.Also fixed
findAllRoutes(interpreter) matched**:nameagainst paths with zero remaining segments, disagreeing with bothfindRouteand the compiledmatchAllrouter. Fixed with a regression test; the core bundle budget moves 5.7k → 5.8k for this fix./:p.split("/")turns/into["", ""], a phantom trailing segment that made the compiled router match required root wildcards and params at/—/**:nameand/:xboth wrongly matched/, disagreeing with the (correct) interpreter andfindRoute. Fixed by letting root collapse to""in the compiled prologue (drop the|| "/"so"".split("/")has no phantom) — a build-time change with zero added per-call runtime overhead. Regression covered by an interpreter-vs-compiled parity test.Tests & docs
test/overlap.test.ts— table-drivenroutesOverlap(literals,**zero-segment,*vs:paramvs literal, nested**, disjoint siblings incl./ab/**vs/a/**, regex precise + conservative,*.png,:x?/:x+/:x*,{s}?, escaping — all asserted symmetric) +findOverlappingRoutes(merged-config example, disjoint scopes, method handling, regex, escaped literals, reference-based dedup, and regressions proving distinct routes with equal-or-absent primitivedataare not dropped,**:nameboundary).test/find-all.test.ts— named-wildcard boundary regression + root-path parity block (required root wildcard/param at/), all interpreter-vs-compiled.AGENTS.mdupdated.Full suite, lint, typecheck, and build all pass. Zero new runtime dependencies.
🤖 Generated with Claude Code
Summary by CodeRabbit
routesOverlapandfindOverlappingRoutesto detect whether two route patterns can match the same paths and to list overlapping registered routes.