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
4 changes: 4 additions & 0 deletions packages/go_router_builder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 4.4.1

- Fixes path parameter regex parsing to support nested parentheses, grouping constructs, and lookahead assertions in `TypedGoRoute` paths.

## 4.4.0

- Adds `hasOverriddenOnExit` parameter to `GoRouteData.$route` and `RelativeGoRouteData.$route` helper methods for type-safe routes. When set to `true`, enables custom `onExit` callback invocation from route data classes extending `GoRouteData` or `RelativeGoRouteData` when the route is removed from the navigation stack.
Expand Down
88 changes: 80 additions & 8 deletions packages/go_router_builder/lib/src/path_utils.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,81 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:collection/collection.dart';

final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?');
final RegExp _parameterNameRegExp = RegExp(r':(\w+)');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The firstOrNull extension getter is used on line 33, but package:collection/collection.dart is not imported in this file. This will cause a compile-time error. Please add the import at the top of the file.

Suggested change
final RegExp _parameterNameRegExp = RegExp(r':(\w+)');
import 'package:collection/collection.dart';
final RegExp _parameterNameRegExp = RegExp(r':(\w+)');


/// A `:name` occurrence in a path pattern, with its optional constraint.
class _PathParameter {
const _PathParameter({required this.start, required this.end, required this.name});

/// Index of the leading `:`.
final int start;

/// Exclusive end of the whole occurrence, constraint included.
final int end;

final String name;
}

/// Scans [pattern] for `:name` occurrences, each optionally followed by a
/// `(...)` constraint.
List<_PathParameter> _pathParametersOf(String pattern) {
final parameters = <_PathParameter>[];
RegExpMatch? match = _parameterNameRegExp.firstMatch(pattern);
while (match != null) {
final int closingParen = _closingParenIndex(pattern, match.end);
final int end = closingParen != -1 ? closingParen + 1 : match.end;
final String? name = match[1];

if (name != null) {
parameters.add(_PathParameter(start: match.start, end: end, name: name));
}
match = _parameterNameRegExp.allMatches(pattern, end).firstOrNull;
}
return parameters;
}

/// The index of the `)` that closes the `(` at [start], or -1 when [start] is
/// not a `(` or the group is never closed.
///
/// Escaped characters and character classes never open or close a group, so
/// `(\()` and `([()])` are both single groups.
int _closingParenIndex(String pattern, int start) {
if (start >= pattern.length || pattern[start] != '(') {
return -1;
}

var depth = 0;
var inCharacterClass = false;
for (var currentIndex = start; currentIndex < pattern.length; currentIndex++) {
final String character = pattern[currentIndex];
if (character == r'\') {
// The next character is a literal: skip it together with the backslash.
currentIndex++;
continue;
}
if (inCharacterClass) {
if (character == ']') {
inCharacterClass = false;
}
continue;
}
switch (character) {
case '[':
inCharacterClass = true;
case '(':
depth++;
case ')':
depth--;
if (depth == 0) {
return currentIndex;
}
}
}

return -1;
}

/// Extracts the path parameters from a [pattern] such as `/user/:id`.
///
Expand All @@ -15,7 +88,7 @@ final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?');
/// final pathParameters = pathParametersFromPattern(pattern); // {'id', 'bookId'}
/// ```
Set<String> pathParametersFromPattern(String pattern) => <String>{
for (final RegExpMatch match in _parameterRegExp.allMatches(pattern)) match[1]!,
for (final _PathParameter parameter in _pathParametersOf(pattern)) parameter.name,
};

/// Reconstructs the full path from a [pattern] and path parameters.
Expand All @@ -29,13 +102,12 @@ Set<String> pathParametersFromPattern(String pattern) => <String>{
String patternToPath(String pattern, Map<String, String> pathParameters) {
final buffer = StringBuffer();
var start = 0;
for (final RegExpMatch match in _parameterRegExp.allMatches(pattern)) {
if (match.start > start) {
buffer.write(pattern.substring(start, match.start));
for (final _PathParameter parameter in _pathParametersOf(pattern)) {
if (parameter.start > start) {
buffer.write(pattern.substring(start, parameter.start));
}
final String name = match[1]!;
buffer.write(pathParameters[name]);
start = match.end;
buffer.write(pathParameters[parameter.name]);
start = parameter.end;
}

if (start < pattern.length) {
Expand Down
2 changes: 1 addition & 1 deletion packages/go_router_builder/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: go_router_builder
description: >-
A builder that supports generated strongly-typed route helpers for
package:go_router
version: 4.4.0
version: 4.4.1
repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22

Expand Down
50 changes: 50 additions & 0 deletions packages/go_router_builder/test/path_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@ void main() {
expect(pathParametersFromPattern('/user/:id/book'), const <String>{'id'});
expect(pathParametersFromPattern('/user/:id/book/:bookId'), const <String>{'id', 'bookId'});
});

test('It should support a nested group in the parameter pattern', () {
expect(
pathParametersFromPattern(r'/user/:id((?!(?:0|1)(?:/|$))[^/]+)/book/:bookId'),
const <String>{'id', 'bookId'},
);
});

test('It should support group constructs in the parameter pattern', () {
expect(pathParametersFromPattern(r'/user/:id((?:0x)?\d+)'), const <String>{'id'});
});

test('It should support parentheses in a character class', () {
expect(pathParametersFromPattern(r'/a/:x([()])/:y'), const <String>{'x', 'y'});
});

test('It should support a nested group containing an alternation', () {
expect(pathParametersFromPattern(r'/details/:id((FOO|BAR)[0-9a-zA-Z]{10})'), const <String>{
'id',
});
});
});

group('patternToPath', () {
Expand All @@ -33,5 +54,34 @@ void main() {
'/user/user-id/book/book-id',
);
});

test('It should support a nested group in the parameter pattern', () {
expect(
patternToPath(r'/tags/:slug((?!(?:admin|new)(?:/|$))[^/]+)', const <String, String>{
'slug': 'flutter',
}),
'/tags/flutter',
);
});

test('It should support group constructs in the parameter pattern', () {
expect(
patternToPath(r'/user/:id((?:0x)?\d+)/book', const <String, String>{'id': '0x42'}),
'/user/0x42/book',
);
});

test('It should support parentheses in a character class', () {
expect(patternToPath(r'/a/:x([()])', const <String, String>{'x': '('}), '/a/(');
});

test('It should support a nested group containing an alternation', () {
expect(
patternToPath(r'/details/:id((FOO|BAR)[0-9a-zA-Z]{10})', const <String, String>{
'id': 'FOO0123456789',
}),
'/details/FOO0123456789',
);
});
});
}
Loading