diff --git a/AUTHORS b/AUTHORS index 40df3589c02..6cafad023aa 100644 --- a/AUTHORS +++ b/AUTHORS @@ -62,6 +62,7 @@ answer newbie questions, and generally made Django that much better: Aljaž Košir Aljosa Mohorovic Alokik Vijay + Amir Karimi Amit Chakradeo Amit Ramon Amit Upadhyay diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py index b021673772a..432bf50076e 100644 --- a/django/urls/resolvers.py +++ b/django/urls/resolvers.py @@ -318,7 +318,10 @@ class RoutePattern(CheckURLMixin): return None def check(self): - warnings = self._check_pattern_startswith_slash() + warnings = [ + *self._check_pattern_startswith_slash(), + *self._check_pattern_unmatched_angle_brackets(), + ] route = self._route if "(?P<" in route or route.startswith("^") or route.endswith("$"): warnings.append( @@ -331,6 +334,34 @@ class RoutePattern(CheckURLMixin): ) return warnings + def _check_pattern_unmatched_angle_brackets(self): + warnings = [] + segments = self._route.split("/") + for segment in segments: + open_bracket_counter = 0 + unmatched_angle_bracket = None + for char in segment: + if char == "<": + open_bracket_counter += 1 + elif char == ">": + open_bracket_counter -= 1 + if open_bracket_counter < 0: + unmatched_angle_bracket = ">" + break + else: + if open_bracket_counter > 0: + unmatched_angle_bracket = "<" + + if unmatched_angle_bracket is not None: + warnings.append( + Warning( + "Your URL pattern %s has an unmatched '%s' bracket." + % (self.describe(), unmatched_angle_bracket), + id="urls.W010", + ) + ) + return warnings + def _compile(self, route): return re.compile(_route_to_regex(route, self._is_endpoint)[0]) diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index de7b0dc3829..49bc9cb0a2f 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -609,6 +609,8 @@ The following checks are performed on your URL configuration: imported. * **urls.E009**: Your URL pattern ```` has an invalid view, pass ``.as_view()`` instead of ````. +* **urls.W010**: Your URL pattern ``` has an unmatched + ````. ``contrib`` app checks ====================== diff --git a/tests/check_framework/test_urls.py b/tests/check_framework/test_urls.py index a9038c6f13c..4b6a4a6f3eb 100644 --- a/tests/check_framework/test_urls.py +++ b/tests/check_framework/test_urls.py @@ -161,6 +161,47 @@ class CheckUrlConfigTests(SimpleTestCase): ], ) + @override_settings( + ROOT_URLCONF="check_framework.urls.path_compatibility.matched_angle_brackets" + ) + def test_no_warnings_matched_angle_brackets(self): + self.assertEqual(check_url_config(None), []) + + @override_settings( + ROOT_URLCONF="check_framework.urls.path_compatibility.unmatched_angle_brackets" + ) + def test_warning_unmatched_angle_brackets(self): + self.assertEqual( + check_url_config(None), + [ + Warning( + "Your URL pattern 'beginning-with/' has an unmatched " + "'>' bracket.", + id="urls.W010", + ), + Warning( + "Your URL pattern 'closed_angle>/x/' bracket.", + id="urls.W010", + ), + Warning( + "Your URL pattern 'closed_angle>/x/angle_bracket>' has an unmatched '>' " + "bracket.", + id="urls.W010", + ), + ], + ) + class UpdatedToPathTests(SimpleTestCase): @override_settings( diff --git a/tests/check_framework/urls/path_compatibility/matched_angle_brackets.py b/tests/check_framework/urls/path_compatibility/matched_angle_brackets.py new file mode 100644 index 00000000000..de3076942d9 --- /dev/null +++ b/tests/check_framework/urls/path_compatibility/matched_angle_brackets.py @@ -0,0 +1,5 @@ +from django.urls import path + +urlpatterns = [ + path("", lambda x: x), +] diff --git a/tests/check_framework/urls/path_compatibility/unmatched_angle_brackets.py b/tests/check_framework/urls/path_compatibility/unmatched_angle_brackets.py new file mode 100644 index 00000000000..e53685ec7a3 --- /dev/null +++ b/tests/check_framework/urls/path_compatibility/unmatched_angle_brackets.py @@ -0,0 +1,8 @@ +from django.urls import path + +urlpatterns = [ + path("beginning-with/", lambda x: x), + path("closed_angle>/x/angle_bracket>", lambda x: x), +]