mirror of https://github.com/astral-sh/ruff
implement DJ014
This commit is contained in:
parent
bb40c34361
commit
a2fcf0eb3f
|
|
@ -0,0 +1,40 @@
|
|||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
# Errors - missing trailing slash
|
||||
urlpatterns = [
|
||||
path("help", views.help_view), # DJ014
|
||||
path("about", views.about_view), # DJ014
|
||||
path("contact", views.contact_view), # DJ014
|
||||
path("api/users", views.users_view), # DJ014
|
||||
path("blog/posts", views.posts_view), # DJ014
|
||||
]
|
||||
|
||||
# OK - has trailing 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), # DJ014
|
||||
path("also-good/", views.also_good_view),
|
||||
path("also-bad", views.also_bad_view), # DJ014
|
||||
]
|
||||
|
|
@ -1181,6 +1181,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
if checker.is_rule_enabled(Rule::DjangoLocalsInRenderFunction) {
|
||||
flake8_django::rules::locals_in_render_function(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::DjangoURLPathWithoutTrailingSlash) {
|
||||
flake8_django::rules::url_path_without_trailing_slash(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::UnsupportedMethodCallOnAll) {
|
||||
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1100,6 +1100,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Flake8Django, "008") => rules::flake8_django::rules::DjangoModelWithoutDunderStr,
|
||||
(Flake8Django, "012") => rules::flake8_django::rules::DjangoUnorderedBodyContentInModel,
|
||||
(Flake8Django, "013") => rules::flake8_django::rules::DjangoNonLeadingReceiverDecorator,
|
||||
(Flake8Django, "014") => rules::flake8_django::rules::DjangoURLPathWithoutTrailingSlash,
|
||||
|
||||
// flynt
|
||||
// Reserved: (Flynt, "001") => Rule: :StringConcatenationToFString,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ mod tests {
|
|||
#[test_case(Rule::DjangoExcludeWithModelForm, Path::new("DJ006.py"))]
|
||||
#[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::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_without_trailing_slash::*;
|
||||
|
||||
mod all_with_model_form;
|
||||
mod exclude_with_model_form;
|
||||
|
|
@ -13,3 +14,4 @@ mod model_without_dunder_str;
|
|||
mod non_leading_receiver_decorator;
|
||||
mod nullable_model_string_field;
|
||||
mod unordered_body_content_in_model;
|
||||
mod url_path_without_trailing_slash;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
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;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that all Django URL route definitions using `django.urls.path()`
|
||||
/// end with a trailing slash.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Django's convention is to use trailing slashes in URL patterns. This is
|
||||
/// enforced by the `APPEND_SLASH` setting (enabled by default), which
|
||||
/// redirects requests without trailing slashes to URLs with them. Omitting
|
||||
/// the trailing slash can lead to unnecessary redirects and inconsistent URL
|
||||
/// patterns throughout your application.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from django.urls import path
|
||||
/// from . import views
|
||||
///
|
||||
/// urlpatterns = [
|
||||
/// path("help", views.help_view), # Missing trailing slash
|
||||
/// path("about", views.about_view), # Missing trailing 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 DjangoURLPathWithoutTrailingSlash {
|
||||
url_pattern: String,
|
||||
}
|
||||
|
||||
impl Violation for DjangoURLPathWithoutTrailingSlash {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let DjangoURLPathWithoutTrailingSlash { url_pattern } = self;
|
||||
format!("URL route `{url_pattern}` is missing a trailing slash")
|
||||
}
|
||||
}
|
||||
|
||||
/// DJ014
|
||||
pub(crate) fn url_path_without_trailing_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
|
||||
if route.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip route parameters (routes with angle brackets like "<int:id>")
|
||||
// These are often at the end and shouldn't require trailing slashes
|
||||
// Also skip routes that are just "/" or already end with "/"
|
||||
if route == "/" || route.ends_with('/') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Report diagnostic for routes without trailing slash
|
||||
checker.report_diagnostic(
|
||||
DjangoURLPathWithoutTrailingSlash {
|
||||
url_pattern: route.to_string(),
|
||||
},
|
||||
route_arg.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_django/mod.rs
|
||||
---
|
||||
DJ014 URL route `help` is missing a trailing slash
|
||||
--> DJ014.py:6:10
|
||||
|
|
||||
4 | # Errors - missing trailing slash
|
||||
5 | urlpatterns = [
|
||||
6 | path("help", views.help_view), # DJ014
|
||||
| ^^^^^^
|
||||
7 | path("about", views.about_view), # DJ014
|
||||
8 | path("contact", views.contact_view), # DJ014
|
||||
|
|
||||
|
||||
DJ014 URL route `about` is missing a trailing slash
|
||||
--> DJ014.py:7:10
|
||||
|
|
||||
5 | urlpatterns = [
|
||||
6 | path("help", views.help_view), # DJ014
|
||||
7 | path("about", views.about_view), # DJ014
|
||||
| ^^^^^^^
|
||||
8 | path("contact", views.contact_view), # DJ014
|
||||
9 | path("api/users", views.users_view), # DJ014
|
||||
|
|
||||
|
||||
DJ014 URL route `contact` is missing a trailing slash
|
||||
--> DJ014.py:8:10
|
||||
|
|
||||
6 | path("help", views.help_view), # DJ014
|
||||
7 | path("about", views.about_view), # DJ014
|
||||
8 | path("contact", views.contact_view), # DJ014
|
||||
| ^^^^^^^^^
|
||||
9 | path("api/users", views.users_view), # DJ014
|
||||
10 | path("blog/posts", views.posts_view), # DJ014
|
||||
|
|
||||
|
||||
DJ014 URL route `api/users` is missing a trailing slash
|
||||
--> DJ014.py:9:10
|
||||
|
|
||||
7 | path("about", views.about_view), # DJ014
|
||||
8 | path("contact", views.contact_view), # DJ014
|
||||
9 | path("api/users", views.users_view), # DJ014
|
||||
| ^^^^^^^^^^^
|
||||
10 | path("blog/posts", views.posts_view), # DJ014
|
||||
11 | ]
|
||||
|
|
||||
|
||||
DJ014 URL route `blog/posts` is missing a trailing slash
|
||||
--> DJ014.py:10:10
|
||||
|
|
||||
8 | path("contact", views.contact_view), # DJ014
|
||||
9 | path("api/users", views.users_view), # DJ014
|
||||
10 | path("blog/posts", views.posts_view), # DJ014
|
||||
| ^^^^^^^^^^^^
|
||||
11 | ]
|
||||
|
|
||||
|
||||
DJ014 URL route `bad` is missing a trailing slash
|
||||
--> DJ014.py:37:10
|
||||
|
|
||||
35 | urlpatterns_mixed = [
|
||||
36 | path("good/", views.good_view),
|
||||
37 | path("bad", views.bad_view), # DJ014
|
||||
| ^^^^^
|
||||
38 | path("also-good/", views.also_good_view),
|
||||
39 | path("also-bad", views.also_bad_view), # DJ014
|
||||
|
|
||||
|
||||
DJ014 URL route `also-bad` is missing a trailing slash
|
||||
--> DJ014.py:39:10
|
||||
|
|
||||
37 | path("bad", views.bad_view), # DJ014
|
||||
38 | path("also-good/", views.also_good_view),
|
||||
39 | path("also-bad", views.also_bad_view), # DJ014
|
||||
| ^^^^^^^^^^
|
||||
40 | ]
|
||||
|
|
||||
|
|
@ -3157,6 +3157,7 @@
|
|||
"DJ01",
|
||||
"DJ012",
|
||||
"DJ013",
|
||||
"DJ014",
|
||||
"DOC",
|
||||
"DOC1",
|
||||
"DOC10",
|
||||
|
|
|
|||
Loading…
Reference in New Issue