mirror of https://github.com/astral-sh/ruff
Add DJ015 for leading slashes as well
This commit is contained in:
parent
aa4228632a
commit
dd7037a042
|
|
@ -0,0 +1,48 @@
|
|||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
# Errors - leading slash
|
||||
urlpatterns = [
|
||||
path("/help/", views.help_view), # DJ015
|
||||
path("/about/", views.about_view), # DJ015
|
||||
path("/contact/", views.contact_view), # DJ015
|
||||
path("/api/users/", views.users_view), # DJ015
|
||||
path("/blog/posts/", views.posts_view), # DJ015
|
||||
]
|
||||
|
||||
# OK - no leading slash
|
||||
urlpatterns_ok = [
|
||||
path("help/", views.help_view),
|
||||
path("about/", views.about_view),
|
||||
path("contact/", views.contact_view),
|
||||
path("api/users/", views.users_view),
|
||||
path("blog/posts/", views.posts_view),
|
||||
]
|
||||
|
||||
# OK - just root path
|
||||
urlpatterns_root = [
|
||||
path("/", views.index_view),
|
||||
path("", views.home_view),
|
||||
]
|
||||
|
||||
# OK - with path parameters
|
||||
urlpatterns_params = [
|
||||
path("users/<int:id>/", views.user_detail),
|
||||
path("posts/<slug:slug>/", views.post_detail),
|
||||
]
|
||||
|
||||
# Mixed cases
|
||||
urlpatterns_mixed = [
|
||||
path("good/", views.good_view),
|
||||
path("/bad/", views.bad_view), # DJ015
|
||||
path("also-good/", views.also_good_view),
|
||||
path("/also-bad/", views.also_bad_view), # DJ015
|
||||
]
|
||||
|
||||
# Edge cases with different quote styles
|
||||
urlpatterns_quotes = [
|
||||
path('/single-quote/', views.single_quote_view), # DJ015
|
||||
path("/double-quote/", views.double_quote_view), # DJ015
|
||||
path('''/triple-single/''', views.triple_single_view), # DJ015
|
||||
path("""/triple-double/""", views.triple_double_view), # DJ015
|
||||
]
|
||||
|
|
@ -1184,6 +1184,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
if checker.is_rule_enabled(Rule::DjangoURLPathWithoutTrailingSlash) {
|
||||
flake8_django::rules::url_path_without_trailing_slash(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::DjangoURLPathWithLeadingSlash) {
|
||||
flake8_django::rules::url_path_with_leading_slash(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::UnsupportedMethodCallOnAll) {
|
||||
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1101,6 +1101,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Flake8Django, "012") => rules::flake8_django::rules::DjangoUnorderedBodyContentInModel,
|
||||
(Flake8Django, "013") => rules::flake8_django::rules::DjangoNonLeadingReceiverDecorator,
|
||||
(Flake8Django, "014") => rules::flake8_django::rules::DjangoURLPathWithoutTrailingSlash,
|
||||
(Flake8Django, "015") => rules::flake8_django::rules::DjangoURLPathWithLeadingSlash,
|
||||
|
||||
// flynt
|
||||
// Reserved: (Flynt, "001") => Rule: :StringConcatenationToFString,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ mod tests {
|
|||
#[test_case(Rule::DjangoAllWithModelForm, Path::new("DJ007.py"))]
|
||||
#[test_case(Rule::DjangoModelWithoutDunderStr, Path::new("DJ008.py"))]
|
||||
#[test_case(Rule::DjangoURLPathWithoutTrailingSlash, Path::new("DJ014.py"))]
|
||||
#[test_case(Rule::DjangoURLPathWithLeadingSlash, Path::new("DJ015.py"))]
|
||||
#[test_case(Rule::DjangoUnorderedBodyContentInModel, Path::new("DJ012.py"))]
|
||||
#[test_case(Rule::DjangoNonLeadingReceiverDecorator, Path::new("DJ013.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ pub(crate) use model_without_dunder_str::*;
|
|||
pub(crate) use non_leading_receiver_decorator::*;
|
||||
pub(crate) use nullable_model_string_field::*;
|
||||
pub(crate) use unordered_body_content_in_model::*;
|
||||
pub(crate) use url_path_with_leading_slash::*;
|
||||
pub(crate) use url_path_without_trailing_slash::*;
|
||||
|
||||
mod all_with_model_form;
|
||||
|
|
@ -14,4 +15,5 @@ mod model_without_dunder_str;
|
|||
mod non_leading_receiver_decorator;
|
||||
mod nullable_model_string_field;
|
||||
mod unordered_body_content_in_model;
|
||||
mod url_path_with_leading_slash;
|
||||
mod url_path_without_trailing_slash;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::{AlwaysFixableViolation, Edit, Fix};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that all Django URL route definitions using `django.urls.path()`
|
||||
/// do not start with a leading slash.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Django's URL patterns should not start with a leading slash. When using
|
||||
/// `include()` or when patterns are combined, leading slashes can cause
|
||||
/// issues with URL resolution. The Django documentation recommends that
|
||||
/// URL patterns should not have leading slashes, as they are not necessary
|
||||
/// and can lead to unexpected behavior.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from django.urls import path
|
||||
/// from . import views
|
||||
///
|
||||
/// urlpatterns = [
|
||||
/// path("/help/", views.help_view), # Leading slash
|
||||
/// path("/about/", views.about_view), # Leading slash
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from django.urls import path
|
||||
/// from . import views
|
||||
///
|
||||
/// urlpatterns = [
|
||||
/// path("help/", views.help_view),
|
||||
/// path("about/", views.about_view),
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Django documentation: URL dispatcher](https://docs.djangoproject.com/en/stable/topics/http/urls/)
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "v0.14.1")]
|
||||
pub(crate) struct DjangoURLPathWithLeadingSlash {
|
||||
url_pattern: String,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for DjangoURLPathWithLeadingSlash {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let DjangoURLPathWithLeadingSlash { url_pattern } = self;
|
||||
format!("URL route `{url_pattern}` has an unnecessary leading slash")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Remove leading slash".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// DJ015
|
||||
pub(crate) fn url_path_with_leading_slash(checker: &Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::DJANGO) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a call to django.urls.path
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(&call.func)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["django", "urls", "path"])
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first argument (the route pattern)
|
||||
let Some(route_arg) = call.arguments.args.first() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Check if it's a string literal
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = route_arg {
|
||||
let route = value.to_str();
|
||||
|
||||
// Skip empty strings and root path "/"
|
||||
if route.is_empty() || route == "/" {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if route starts with a leading slash
|
||||
if route.starts_with('/') {
|
||||
// Report diagnostic for routes with leading slash
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
DjangoURLPathWithLeadingSlash {
|
||||
url_pattern: route.to_string(),
|
||||
},
|
||||
route_arg.range(),
|
||||
);
|
||||
|
||||
// Determine the quote style to find the insertion point for removal
|
||||
let string_content = checker.locator().slice(route_arg.range());
|
||||
let quote_len =
|
||||
if string_content.starts_with("'''") || string_content.starts_with("\"\"\"") {
|
||||
3
|
||||
} else if string_content.starts_with('\'') || string_content.starts_with('"') {
|
||||
1
|
||||
} else {
|
||||
return; // Invalid string format
|
||||
};
|
||||
|
||||
// Remove the leading slash (after the opening quote(s))
|
||||
let removal_start = route_arg.range().start() + TextSize::new(quote_len);
|
||||
let removal_end = removal_start + TextSize::new(1); // Remove one character (the slash)
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(removal_start, removal_end)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_django/mod.rs
|
||||
---
|
||||
DJ015 [*] URL route `/help/` has an unnecessary leading slash
|
||||
--> DJ015.py:6:10
|
||||
|
|
||||
4 | # Errors - leading slash
|
||||
5 | urlpatterns = [
|
||||
6 | path("/help/", views.help_view), # DJ015
|
||||
| ^^^^^^^^
|
||||
7 | path("/about/", views.about_view), # DJ015
|
||||
8 | path("/contact/", views.contact_view), # DJ015
|
||||
|
|
||||
help: Remove leading slash
|
||||
3 |
|
||||
4 | # Errors - leading slash
|
||||
5 | urlpatterns = [
|
||||
- path("/help/", views.help_view), # DJ015
|
||||
6 + path("help/", views.help_view), # DJ015
|
||||
7 | path("/about/", views.about_view), # DJ015
|
||||
8 | path("/contact/", views.contact_view), # DJ015
|
||||
9 | path("/api/users/", views.users_view), # DJ015
|
||||
|
||||
DJ015 [*] URL route `/about/` has an unnecessary leading slash
|
||||
--> DJ015.py:7:10
|
||||
|
|
||||
5 | urlpatterns = [
|
||||
6 | path("/help/", views.help_view), # DJ015
|
||||
7 | path("/about/", views.about_view), # DJ015
|
||||
| ^^^^^^^^^
|
||||
8 | path("/contact/", views.contact_view), # DJ015
|
||||
9 | path("/api/users/", views.users_view), # DJ015
|
||||
|
|
||||
help: Remove leading slash
|
||||
4 | # Errors - leading slash
|
||||
5 | urlpatterns = [
|
||||
6 | path("/help/", views.help_view), # DJ015
|
||||
- path("/about/", views.about_view), # DJ015
|
||||
7 + path("about/", views.about_view), # DJ015
|
||||
8 | path("/contact/", views.contact_view), # DJ015
|
||||
9 | path("/api/users/", views.users_view), # DJ015
|
||||
10 | path("/blog/posts/", views.posts_view), # DJ015
|
||||
|
||||
DJ015 [*] URL route `/contact/` has an unnecessary leading slash
|
||||
--> DJ015.py:8:10
|
||||
|
|
||||
6 | path("/help/", views.help_view), # DJ015
|
||||
7 | path("/about/", views.about_view), # DJ015
|
||||
8 | path("/contact/", views.contact_view), # DJ015
|
||||
| ^^^^^^^^^^^
|
||||
9 | path("/api/users/", views.users_view), # DJ015
|
||||
10 | path("/blog/posts/", views.posts_view), # DJ015
|
||||
|
|
||||
help: Remove leading slash
|
||||
5 | urlpatterns = [
|
||||
6 | path("/help/", views.help_view), # DJ015
|
||||
7 | path("/about/", views.about_view), # DJ015
|
||||
- path("/contact/", views.contact_view), # DJ015
|
||||
8 + path("contact/", views.contact_view), # DJ015
|
||||
9 | path("/api/users/", views.users_view), # DJ015
|
||||
10 | path("/blog/posts/", views.posts_view), # DJ015
|
||||
11 | ]
|
||||
|
||||
DJ015 [*] URL route `/api/users/` has an unnecessary leading slash
|
||||
--> DJ015.py:9:10
|
||||
|
|
||||
7 | path("/about/", views.about_view), # DJ015
|
||||
8 | path("/contact/", views.contact_view), # DJ015
|
||||
9 | path("/api/users/", views.users_view), # DJ015
|
||||
| ^^^^^^^^^^^^^
|
||||
10 | path("/blog/posts/", views.posts_view), # DJ015
|
||||
11 | ]
|
||||
|
|
||||
help: Remove leading slash
|
||||
6 | path("/help/", views.help_view), # DJ015
|
||||
7 | path("/about/", views.about_view), # DJ015
|
||||
8 | path("/contact/", views.contact_view), # DJ015
|
||||
- path("/api/users/", views.users_view), # DJ015
|
||||
9 + path("api/users/", views.users_view), # DJ015
|
||||
10 | path("/blog/posts/", views.posts_view), # DJ015
|
||||
11 | ]
|
||||
12 |
|
||||
|
||||
DJ015 [*] URL route `/blog/posts/` has an unnecessary leading slash
|
||||
--> DJ015.py:10:10
|
||||
|
|
||||
8 | path("/contact/", views.contact_view), # DJ015
|
||||
9 | path("/api/users/", views.users_view), # DJ015
|
||||
10 | path("/blog/posts/", views.posts_view), # DJ015
|
||||
| ^^^^^^^^^^^^^^
|
||||
11 | ]
|
||||
|
|
||||
help: Remove leading slash
|
||||
7 | path("/about/", views.about_view), # DJ015
|
||||
8 | path("/contact/", views.contact_view), # DJ015
|
||||
9 | path("/api/users/", views.users_view), # DJ015
|
||||
- path("/blog/posts/", views.posts_view), # DJ015
|
||||
10 + path("blog/posts/", views.posts_view), # DJ015
|
||||
11 | ]
|
||||
12 |
|
||||
13 | # OK - no leading slash
|
||||
|
||||
DJ015 [*] URL route `/bad/` has an unnecessary leading slash
|
||||
--> DJ015.py:37:10
|
||||
|
|
||||
35 | urlpatterns_mixed = [
|
||||
36 | path("good/", views.good_view),
|
||||
37 | path("/bad/", views.bad_view), # DJ015
|
||||
| ^^^^^^^
|
||||
38 | path("also-good/", views.also_good_view),
|
||||
39 | path("/also-bad/", views.also_bad_view), # DJ015
|
||||
|
|
||||
help: Remove leading slash
|
||||
34 | # Mixed cases
|
||||
35 | urlpatterns_mixed = [
|
||||
36 | path("good/", views.good_view),
|
||||
- path("/bad/", views.bad_view), # DJ015
|
||||
37 + path("bad/", views.bad_view), # DJ015
|
||||
38 | path("also-good/", views.also_good_view),
|
||||
39 | path("/also-bad/", views.also_bad_view), # DJ015
|
||||
40 | ]
|
||||
|
||||
DJ015 [*] URL route `/also-bad/` has an unnecessary leading slash
|
||||
--> DJ015.py:39:10
|
||||
|
|
||||
37 | path("/bad/", views.bad_view), # DJ015
|
||||
38 | path("also-good/", views.also_good_view),
|
||||
39 | path("/also-bad/", views.also_bad_view), # DJ015
|
||||
| ^^^^^^^^^^^^
|
||||
40 | ]
|
||||
|
|
||||
help: Remove leading slash
|
||||
36 | path("good/", views.good_view),
|
||||
37 | path("/bad/", views.bad_view), # DJ015
|
||||
38 | path("also-good/", views.also_good_view),
|
||||
- path("/also-bad/", views.also_bad_view), # DJ015
|
||||
39 + path("also-bad/", views.also_bad_view), # DJ015
|
||||
40 | ]
|
||||
41 |
|
||||
42 | # Edge cases with different quote styles
|
||||
|
||||
DJ015 [*] URL route `/single-quote/` has an unnecessary leading slash
|
||||
--> DJ015.py:44:10
|
||||
|
|
||||
42 | # Edge cases with different quote styles
|
||||
43 | urlpatterns_quotes = [
|
||||
44 | path('/single-quote/', views.single_quote_view), # DJ015
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
45 | path("/double-quote/", views.double_quote_view), # DJ015
|
||||
46 | path('''/triple-single/''', views.triple_single_view), # DJ015
|
||||
|
|
||||
help: Remove leading slash
|
||||
41 |
|
||||
42 | # Edge cases with different quote styles
|
||||
43 | urlpatterns_quotes = [
|
||||
- path('/single-quote/', views.single_quote_view), # DJ015
|
||||
44 + path('single-quote/', views.single_quote_view), # DJ015
|
||||
45 | path("/double-quote/", views.double_quote_view), # DJ015
|
||||
46 | path('''/triple-single/''', views.triple_single_view), # DJ015
|
||||
47 | path("""/triple-double/""", views.triple_double_view), # DJ015
|
||||
|
||||
DJ015 [*] URL route `/double-quote/` has an unnecessary leading slash
|
||||
--> DJ015.py:45:10
|
||||
|
|
||||
43 | urlpatterns_quotes = [
|
||||
44 | path('/single-quote/', views.single_quote_view), # DJ015
|
||||
45 | path("/double-quote/", views.double_quote_view), # DJ015
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
46 | path('''/triple-single/''', views.triple_single_view), # DJ015
|
||||
47 | path("""/triple-double/""", views.triple_double_view), # DJ015
|
||||
|
|
||||
help: Remove leading slash
|
||||
42 | # Edge cases with different quote styles
|
||||
43 | urlpatterns_quotes = [
|
||||
44 | path('/single-quote/', views.single_quote_view), # DJ015
|
||||
- path("/double-quote/", views.double_quote_view), # DJ015
|
||||
45 + path("double-quote/", views.double_quote_view), # DJ015
|
||||
46 | path('''/triple-single/''', views.triple_single_view), # DJ015
|
||||
47 | path("""/triple-double/""", views.triple_double_view), # DJ015
|
||||
48 | ]
|
||||
|
||||
DJ015 [*] URL route `/triple-single/` has an unnecessary leading slash
|
||||
--> DJ015.py:46:10
|
||||
|
|
||||
44 | path('/single-quote/', views.single_quote_view), # DJ015
|
||||
45 | path("/double-quote/", views.double_quote_view), # DJ015
|
||||
46 | path('''/triple-single/''', views.triple_single_view), # DJ015
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
47 | path("""/triple-double/""", views.triple_double_view), # DJ015
|
||||
48 | ]
|
||||
|
|
||||
help: Remove leading slash
|
||||
43 | urlpatterns_quotes = [
|
||||
44 | path('/single-quote/', views.single_quote_view), # DJ015
|
||||
45 | path("/double-quote/", views.double_quote_view), # DJ015
|
||||
- path('''/triple-single/''', views.triple_single_view), # DJ015
|
||||
46 + path('''triple-single/''', views.triple_single_view), # DJ015
|
||||
47 | path("""/triple-double/""", views.triple_double_view), # DJ015
|
||||
48 | ]
|
||||
|
||||
DJ015 [*] URL route `/triple-double/` has an unnecessary leading slash
|
||||
--> DJ015.py:47:10
|
||||
|
|
||||
45 | path("/double-quote/", views.double_quote_view), # DJ015
|
||||
46 | path('''/triple-single/''', views.triple_single_view), # DJ015
|
||||
47 | path("""/triple-double/""", views.triple_double_view), # DJ015
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
48 | ]
|
||||
|
|
||||
help: Remove leading slash
|
||||
44 | path('/single-quote/', views.single_quote_view), # DJ015
|
||||
45 | path("/double-quote/", views.double_quote_view), # DJ015
|
||||
46 | path('''/triple-single/''', views.triple_single_view), # DJ015
|
||||
- path("""/triple-double/""", views.triple_double_view), # DJ015
|
||||
47 + path("""triple-double/""", views.triple_double_view), # DJ015
|
||||
48 | ]
|
||||
|
|
@ -3158,6 +3158,7 @@
|
|||
"DJ012",
|
||||
"DJ013",
|
||||
"DJ014",
|
||||
"DJ015",
|
||||
"DOC",
|
||||
"DOC1",
|
||||
"DOC10",
|
||||
|
|
|
|||
Loading…
Reference in New Issue