From 44f440c544a6828f1964f8b000169b3d182ce62b Mon Sep 17 00:00:00 2001
From: Matt Brophy
Date: Mon, 19 Sep 2022 09:03:52 -0400
Subject: [PATCH 1/3] fix: update matchPath to avoid false positives on
dash-separated segments
---
.../__tests__/nav-link-active-test.tsx | 70 +++++++++++++++++++
.../react-router/__tests__/matchPath-test.tsx | 4 ++
packages/router/utils.ts | 29 +++++---
3 files changed, 94 insertions(+), 9 deletions(-)
diff --git a/packages/react-router-dom/__tests__/nav-link-active-test.tsx b/packages/react-router-dom/__tests__/nav-link-active-test.tsx
index 4cc70728d3..34851b7d9d 100644
--- a/packages/react-router-dom/__tests__/nav-link-active-test.tsx
+++ b/packages/react-router-dom/__tests__/nav-link-active-test.tsx
@@ -218,6 +218,76 @@ describe("NavLink", () => {
expect(anchor.props.className).not.toMatch("active");
});
+
+ it("does not match when path is a subset of the active url", () => {
+ let renderer: TestRenderer.ReactTestRenderer;
+ TestRenderer.act(() => {
+ renderer = TestRenderer.create(
+
+
+
+ Go to /user
+
+ Go to /user-preferences
+
+
+
+ }
+ >
+ Index
} />
+ User} />
+ User Preferences}
+ />
+
+
+
+ );
+ });
+
+ let anchors = renderer.root.findAllByType("a");
+
+ expect(anchors.map((a) => a.props.className)).toEqual(["", "active"]);
+ });
+
+ it("does not match when active url is a subset of a segment", () => {
+ let renderer: TestRenderer.ReactTestRenderer;
+ TestRenderer.act(() => {
+ renderer = TestRenderer.create(
+
+
+
+ Go to /user
+
+ Go to /user-preferences
+
+
+
+ }
+ >
+ Index} />
+ User} />
+ User Preferences}
+ />
+
+
+
+ );
+ });
+
+ let anchors = renderer.root.findAllByType("a");
+
+ expect(anchors.map((a) => a.props.className)).toEqual(["active", ""]);
+ });
});
describe("when it matches just the beginning but not to the end", () => {
diff --git a/packages/react-router/__tests__/matchPath-test.tsx b/packages/react-router/__tests__/matchPath-test.tsx
index 795e889ce4..e36f73ed8a 100644
--- a/packages/react-router/__tests__/matchPath-test.tsx
+++ b/packages/react-router/__tests__/matchPath-test.tsx
@@ -156,6 +156,10 @@ describe("matchPath", () => {
it("fails to match a pathname where the segments do not match", () => {
expect(matchPath({ path: "/users", end: false }, "/")).toBeNull();
expect(matchPath({ path: "/users", end: false }, "/users2")).toBeNull();
+ expect(matchPath({ path: "/users", end: false }, "/users-2")).toBeNull();
+ expect(matchPath({ path: "/users", end: false }, "/users~2")).toBeNull();
+ expect(matchPath({ path: "/users", end: false }, "/users@2")).toBeNull();
+ expect(matchPath({ path: "/users", end: false }, "/users.2")).toBeNull();
expect(
matchPath({ path: "/users/mj", end: false }, "/users/mj2")
).toBeNull();
diff --git a/packages/router/utils.ts b/packages/router/utils.ts
index cb3e89b85d..0709e91ec3 100644
--- a/packages/router/utils.ts
+++ b/packages/router/utils.ts
@@ -634,16 +634,27 @@ function compilePath(
path === "*" || path === "/*"
? "(.*)$" // Already matched the initial /, just match the rest
: "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
+ } else if (end) {
+ // When matching to the end, ignore trailing slashes
+ regexpSource += "\\/*$";
+ } else if (path !== "" && path !== "/") {
+ // If our path is non-empty and contains anything beyond an initial slash,
+ // then we have _some_ form of path and we do not want to include the
+ // positive lookahead below, otherwise we incorrectly match things like
+ // /user-preferences for path /user. So, we look for an optional non-captured
+ // trailing slash (to match a portion of the URL) or the end of the path
+ // (if we've matched to the end). We used to do this with a word boundary
+ // but that gives false positives on routes like /user-preferences since
+ // `-` counts as a word boundary.
+ regexpSource += "(?:(?=\\/|$))";
} else {
- regexpSource += end
- ? "\\/*$" // When matching to the end, ignore trailing slashes
- : // Otherwise, match a word boundary or a proceeding /. The word boundary restricts
- // parent routes to matching only their own words and nothing more, e.g. parent
- // route "/home" should not match "/home2".
- // Additionally, allow paths starting with `.`, `-`, `~`, and url-encoded entities,
- // but do not consume the character in the matched path so they can match against
- // nested paths.
- "(?:(?=[@.~-]|%[0-9A-F]{2})|\\b|\\/|$)";
+ // Otherwise, if we are an empty path, match a word boundary or a
+ // proceeding /. The word boundary restricts parent routes to matching only
+ // their own words and nothing more, e.g. parent route "/home" should not
+ // match "/home2". Additionally, allow paths starting with `.`, `-`, `~`,
+ // and url-encoded entities, but do not consume the character in the
+ // matched path so they can match against nested paths.
+ regexpSource += "(?:(?=[@.~-]|%[0-9A-F]{2})|\\b|\\/|$)";
}
let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
From 2aa1581522d94ed35e51ab02d82c8b5836f4020f Mon Sep 17 00:00:00 2001
From: Matt Brophy
Date: Mon, 19 Sep 2022 09:09:29 -0400
Subject: [PATCH 2/3] Add changeset
---
.changeset/sweet-chicken-suffer.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/sweet-chicken-suffer.md
diff --git a/.changeset/sweet-chicken-suffer.md b/.changeset/sweet-chicken-suffer.md
new file mode 100644
index 0000000000..7cbb3004da
--- /dev/null
+++ b/.changeset/sweet-chicken-suffer.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/router": patch
+---
+
+fix: update matchPath to avoid false positives on dash-separated segments (#9300)
From decf69ba68f3c0371d92692fbb3291bb8ba2a4b7 Mon Sep 17 00:00:00 2001
From: Matt Brophy
Date: Thu, 29 Sep 2022 17:06:18 -0400
Subject: [PATCH 3/3] remove uneeded else branch in matching empty paths
---
packages/router/utils.ts | 21 +++++++--------------
1 file changed, 7 insertions(+), 14 deletions(-)
diff --git a/packages/router/utils.ts b/packages/router/utils.ts
index 0709e91ec3..e60d2d901a 100644
--- a/packages/router/utils.ts
+++ b/packages/router/utils.ts
@@ -639,22 +639,15 @@ function compilePath(
regexpSource += "\\/*$";
} else if (path !== "" && path !== "/") {
// If our path is non-empty and contains anything beyond an initial slash,
- // then we have _some_ form of path and we do not want to include the
- // positive lookahead below, otherwise we incorrectly match things like
- // /user-preferences for path /user. So, we look for an optional non-captured
- // trailing slash (to match a portion of the URL) or the end of the path
- // (if we've matched to the end). We used to do this with a word boundary
- // but that gives false positives on routes like /user-preferences since
- // `-` counts as a word boundary.
+ // then we have _some_ form of path in our regex so we should expect to
+ // match only if we find the end of this path segment. Look for an optional
+ // non-captured trailing slash (to match a portion of the URL) or the end
+ // of the path (if we've matched to the end). We used to do this with a
+ // word boundary but that gives false positives on routes like
+ // /user-preferences since `-` counts as a word boundary.
regexpSource += "(?:(?=\\/|$))";
} else {
- // Otherwise, if we are an empty path, match a word boundary or a
- // proceeding /. The word boundary restricts parent routes to matching only
- // their own words and nothing more, e.g. parent route "/home" should not
- // match "/home2". Additionally, allow paths starting with `.`, `-`, `~`,
- // and url-encoded entities, but do not consume the character in the
- // matched path so they can match against nested paths.
- regexpSource += "(?:(?=[@.~-]|%[0-9A-F]{2})|\\b|\\/|$)";
+ // Nothing to match for "" or "/"
}
let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");