From a2fcf0eb3ffa558ac156efd9838415375696e804 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Wed, 29 Oct 2025 19:55:24 +0100 Subject: [PATCH 01/17] implement DJ014 --- .../test/fixtures/flake8_django/DJ014.py | 40 +++++++ .../src/checkers/ast/analyze/expression.rs | 3 + crates/ruff_linter/src/codes.rs | 1 + .../src/rules/flake8_django/mod.rs | 1 + .../src/rules/flake8_django/rules/mod.rs | 2 + .../rules/url_path_without_trailing_slash.rs | 104 ++++++++++++++++++ ..._flake8_django__tests__DJ014_DJ014.py.snap | 77 +++++++++++++ ruff.schema.json | 1 + 8 files changed, 229 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py create mode 100644 crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs create mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py new file mode 100644 index 0000000000..d66e64f4b8 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py @@ -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//", views.user_detail), + path("posts//", 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 +] diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 53081e3681..78f1fd98f1 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -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); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 172841dc7c..44336e1f21 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -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, diff --git a/crates/ruff_linter/src/rules/flake8_django/mod.rs b/crates/ruff_linter/src/rules/flake8_django/mod.rs index 8be904a8e0..d4c31a041b 100644 --- a/crates/ruff_linter/src/rules/flake8_django/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_django/mod.rs @@ -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<()> { diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs index afdb7bdc18..1b86fb9001 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs @@ -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; diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs new file mode 100644 index 0000000000..01f409f472 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs @@ -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 "") + // 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(), + ); + } +} diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap new file mode 100644 index 0000000000..560bcd78ec --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap @@ -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 | ] + | diff --git a/ruff.schema.json b/ruff.schema.json index a16e91fbd7..8bd0d00cf1 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3157,6 +3157,7 @@ "DJ01", "DJ012", "DJ013", + "DJ014", "DOC", "DOC1", "DOC10", From 1047e895c75eca28bf6dc8fb13d6a5c5ffcbc666 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Sun, 5 Oct 2025 17:00:25 +0200 Subject: [PATCH 02/17] add autofix --- .../rules/url_path_without_trailing_slash.rs | 36 ++++++++- ..._flake8_django__tests__DJ014_DJ014.py.snap | 75 +++++++++++++++++-- 2 files changed, 100 insertions(+), 11 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs index 01f409f472..6080d0104f 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs @@ -1,10 +1,10 @@ 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 ruff_text_size::{Ranged, TextSize}; -use crate::Violation; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks that all Django URL route definitions using `django.urls.path()` @@ -47,12 +47,16 @@ pub(crate) struct DjangoURLPathWithoutTrailingSlash { url_pattern: String, } -impl Violation for DjangoURLPathWithoutTrailingSlash { +impl AlwaysFixableViolation for DjangoURLPathWithoutTrailingSlash { #[derive_message_formats] fn message(&self) -> String { let DjangoURLPathWithoutTrailingSlash { url_pattern } = self; format!("URL route `{url_pattern}` is missing a trailing slash") } + + fn fix_title(&self) -> String { + "Add trailing slash".to_string() + } } /// DJ014 @@ -94,11 +98,35 @@ pub(crate) fn url_path_without_trailing_slash(checker: &Checker, call: &ast::Exp } // Report diagnostic for routes without trailing slash - checker.report_diagnostic( + let mut diagnostic = checker.report_diagnostic( DjangoURLPathWithoutTrailingSlash { url_pattern: route.to_string(), }, route_arg.range(), ); + + // Generate fix: add trailing slash to the string content + // We need to find the position of the closing quote and insert "/" before it + let string_range = route_arg.range(); + let locator = checker.locator(); + let string_content = locator.slice(string_range); + + // Find the closing quote(s) by working backwards from the end + // Handle both single quotes, double quotes, and their triple variants + let quote_len = if string_content.ends_with("'''") || string_content.ends_with("\"\"\"") { + 3 + } else if string_content.ends_with('\'') || string_content.ends_with('"') { + 1 + } else { + // Shouldn't happen for a valid string literal + return; + }; + + // Insert "/" before the closing quote(s) + let insertion_point = string_range.end() - TextSize::new(quote_len); + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + "/".to_string(), + insertion_point, + ))); } } diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap index 560bcd78ec..53e1de59ac 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_django/mod.rs --- -DJ014 URL route `help` is missing a trailing slash +DJ014 [*] URL route `help` is missing a trailing slash --> DJ014.py:6:10 | 4 | # Errors - missing trailing slash @@ -11,8 +11,17 @@ DJ014 URL route `help` is missing a trailing slash 7 | path("about", views.about_view), # DJ014 8 | path("contact", views.contact_view), # DJ014 | +help: Add trailing slash +3 | +4 | # Errors - missing trailing slash +5 | urlpatterns = [ + - path("help", views.help_view), # DJ014 +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 `about` is missing a trailing slash +DJ014 [*] URL route `about` is missing a trailing slash --> DJ014.py:7:10 | 5 | urlpatterns = [ @@ -22,8 +31,17 @@ DJ014 URL route `about` is missing a trailing slash 8 | path("contact", views.contact_view), # DJ014 9 | path("api/users", views.users_view), # DJ014 | +help: Add trailing slash +4 | # Errors - missing trailing slash +5 | urlpatterns = [ +6 | path("help", views.help_view), # DJ014 + - path("about", views.about_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 `contact` is missing a trailing slash +DJ014 [*] URL route `contact` is missing a trailing slash --> DJ014.py:8:10 | 6 | path("help", views.help_view), # DJ014 @@ -33,8 +51,17 @@ DJ014 URL route `contact` is missing a trailing slash 9 | path("api/users", views.users_view), # DJ014 10 | path("blog/posts", views.posts_view), # DJ014 | +help: Add trailing slash +5 | urlpatterns = [ +6 | path("help", views.help_view), # DJ014 +7 | path("about", views.about_view), # DJ014 + - path("contact", views.contact_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 `api/users` is missing a trailing slash +DJ014 [*] URL route `api/users` is missing a trailing slash --> DJ014.py:9:10 | 7 | path("about", views.about_view), # DJ014 @@ -44,8 +71,17 @@ DJ014 URL route `api/users` is missing a trailing slash 10 | path("blog/posts", views.posts_view), # DJ014 11 | ] | +help: Add trailing slash +6 | path("help", views.help_view), # DJ014 +7 | path("about", views.about_view), # DJ014 +8 | path("contact", views.contact_view), # DJ014 + - path("api/users", views.users_view), # DJ014 +9 + path("api/users/", views.users_view), # DJ014 +10 | path("blog/posts", views.posts_view), # DJ014 +11 | ] +12 | -DJ014 URL route `blog/posts` is missing a trailing slash +DJ014 [*] URL route `blog/posts` is missing a trailing slash --> DJ014.py:10:10 | 8 | path("contact", views.contact_view), # DJ014 @@ -54,8 +90,17 @@ DJ014 URL route `blog/posts` is missing a trailing slash | ^^^^^^^^^^^^ 11 | ] | +help: Add trailing slash +7 | path("about", views.about_view), # DJ014 +8 | path("contact", views.contact_view), # DJ014 +9 | path("api/users", views.users_view), # DJ014 + - path("blog/posts", views.posts_view), # DJ014 +10 + path("blog/posts/", views.posts_view), # DJ014 +11 | ] +12 | +13 | # OK - has trailing slash -DJ014 URL route `bad` is missing a trailing slash +DJ014 [*] URL route `bad` is missing a trailing slash --> DJ014.py:37:10 | 35 | urlpatterns_mixed = [ @@ -65,8 +110,17 @@ DJ014 URL route `bad` is missing a trailing slash 38 | path("also-good/", views.also_good_view), 39 | path("also-bad", views.also_bad_view), # DJ014 | +help: Add trailing slash +34 | # Mixed cases +35 | urlpatterns_mixed = [ +36 | path("good/", views.good_view), + - path("bad", views.bad_view), # DJ014 +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 | ] -DJ014 URL route `also-bad` is missing a trailing slash +DJ014 [*] URL route `also-bad` is missing a trailing slash --> DJ014.py:39:10 | 37 | path("bad", views.bad_view), # DJ014 @@ -75,3 +129,10 @@ DJ014 URL route `also-bad` is missing a trailing slash | ^^^^^^^^^^ 40 | ] | +help: Add trailing slash +36 | path("good/", views.good_view), +37 | path("bad", views.bad_view), # DJ014 +38 | path("also-good/", views.also_good_view), + - path("also-bad", views.also_bad_view), # DJ014 +39 + path("also-bad/", views.also_bad_view), # DJ014 +40 | ] From aa4228632ac64de43460a8bbc8144f9b1437684a Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Sun, 5 Oct 2025 17:42:41 +0200 Subject: [PATCH 03/17] improve insertion --- .../rules/url_path_without_trailing_slash.rs | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs index 6080d0104f..1e85dc7526 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs @@ -90,9 +90,7 @@ pub(crate) fn url_path_without_trailing_slash(checker: &Checker, call: &ast::Exp return; } - // Skip route parameters (routes with angle brackets like "") - // These are often at the end and shouldn't require trailing slashes - // Also skip routes that are just "/" or already end with "/" + // Skip routes that are just "/" or already end with "/" if route == "/" || route.ends_with('/') { return; } @@ -105,25 +103,19 @@ pub(crate) fn url_path_without_trailing_slash(checker: &Checker, call: &ast::Exp route_arg.range(), ); - // Generate fix: add trailing slash to the string content - // We need to find the position of the closing quote and insert "/" before it - let string_range = route_arg.range(); - let locator = checker.locator(); - let string_content = locator.slice(string_range); - - // Find the closing quote(s) by working backwards from the end - // Handle both single quotes, double quotes, and their triple variants + // Determine the quote style to find the insertion point for the slash + // (just before the closing quotes) + let string_content = checker.locator().slice(route_arg.range()); let quote_len = if string_content.ends_with("'''") || string_content.ends_with("\"\"\"") { 3 } else if string_content.ends_with('\'') || string_content.ends_with('"') { 1 } else { - // Shouldn't happen for a valid string literal - return; + return; // Invalid string format }; - // Insert "/" before the closing quote(s) - let insertion_point = string_range.end() - TextSize::new(quote_len); + // Insert "/" just before the closing quote(s) + let insertion_point = route_arg.range().end() - TextSize::new(quote_len); diagnostic.set_fix(Fix::safe_edit(Edit::insertion( "/".to_string(), insertion_point, From dd7037a0423ea653be3a4938abbaadca071d2461 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Wed, 29 Oct 2025 19:57:02 +0100 Subject: [PATCH 04/17] Add DJ015 for leading slashes as well --- .../test/fixtures/flake8_django/DJ015.py | 48 ++++ .../src/checkers/ast/analyze/expression.rs | 3 + crates/ruff_linter/src/codes.rs | 1 + .../src/rules/flake8_django/mod.rs | 1 + .../src/rules/flake8_django/rules/mod.rs | 2 + .../rules/url_path_with_leading_slash.rs | 121 ++++++++++ ..._flake8_django__tests__DJ015_DJ015.py.snap | 216 ++++++++++++++++++ ruff.schema.json | 1 + 8 files changed, 393 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py create mode 100644 crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs create mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py new file mode 100644 index 0000000000..349d312a67 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py @@ -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//", views.user_detail), + path("posts//", 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 +] diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 78f1fd98f1..8024b7b27f 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -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); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 44336e1f21..3e25d15f4c 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -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, diff --git a/crates/ruff_linter/src/rules/flake8_django/mod.rs b/crates/ruff_linter/src/rules/flake8_django/mod.rs index d4c31a041b..43d3e82bce 100644 --- a/crates/ruff_linter/src/rules/flake8_django/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_django/mod.rs @@ -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<()> { diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs index 1b86fb9001..f2a52d7dce 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs @@ -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; diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs new file mode 100644 index 0000000000..df325f9bcf --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs @@ -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))); + } + } +} diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap new file mode 100644 index 0000000000..1293b0008b --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap @@ -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 | ] diff --git a/ruff.schema.json b/ruff.schema.json index 8bd0d00cf1..fa709fe329 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3158,6 +3158,7 @@ "DJ012", "DJ013", "DJ014", + "DJ015", "DOC", "DOC1", "DOC10", From 95a8ccadfb3a963fb3c848670b0557f2f844a356 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Sun, 5 Oct 2025 18:58:52 +0200 Subject: [PATCH 05/17] Add combo of issue with arguments into the snapshot --- .../test/fixtures/flake8_django/DJ014.py | 5 +++++ .../test/fixtures/flake8_django/DJ015.py | 5 +++++ ..._flake8_django__tests__DJ014_DJ014.py.snap | 19 ++++++++++++++++++ ..._flake8_django__tests__DJ015_DJ015.py.snap | 20 +++++++++++++++++++ 4 files changed, 49 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py index d66e64f4b8..e0943da309 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py @@ -38,3 +38,8 @@ urlpatterns_mixed = [ path("also-good/", views.also_good_view), path("also-bad", views.also_bad_view), # DJ014 ] + +# Error - missing trail slash and argument should stay in message +urlpatterns_params_bad = [ + path("bad/", views.bad_view), # DJ014 +] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py index 349d312a67..23d30b6c05 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py @@ -46,3 +46,8 @@ urlpatterns_quotes = [ path('''/triple-single/''', views.triple_single_view), # DJ015 path("""/triple-double/""", views.triple_double_view), # DJ015 ] + +# Error - leading trail slash and argument should stay in message +urlpatterns_params_bad = [ + path("/bad//", views.bad_view), # DJ015 +] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap index 53e1de59ac..62842d19a0 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap @@ -136,3 +136,22 @@ help: Add trailing slash - path("also-bad", views.also_bad_view), # DJ014 39 + path("also-bad/", views.also_bad_view), # DJ014 40 | ] +41 | +42 | # Error - missing trail slash and argument should stay in message + +DJ014 [*] URL route `bad/` is missing a trailing slash + --> DJ014.py:44:10 + | +42 | # Error - missing trail slash and argument should stay in message +43 | urlpatterns_params_bad = [ +44 | path("bad/", views.bad_view), # DJ014 + | ^^^^^^^^^^^^^^^^^ +45 | ] + | +help: Add trailing slash +41 | +42 | # Error - missing trail slash and argument should stay in message +43 | urlpatterns_params_bad = [ + - path("bad/", views.bad_view), # DJ014 +44 + path("bad//", views.bad_view), # DJ014 +45 | ] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap index 1293b0008b..7e984411dd 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap @@ -197,6 +197,7 @@ help: Remove leading slash 46 + path('''triple-single/''', views.triple_single_view), # DJ015 47 | path("""/triple-double/""", views.triple_double_view), # DJ015 48 | ] +49 | DJ015 [*] URL route `/triple-double/` has an unnecessary leading slash --> DJ015.py:47:10 @@ -214,3 +215,22 @@ help: Remove leading slash - path("""/triple-double/""", views.triple_double_view), # DJ015 47 + path("""triple-double/""", views.triple_double_view), # DJ015 48 | ] +49 | +50 | # Error - leading trail slash and argument should stay in message + +DJ015 [*] URL route `/bad//` has an unnecessary leading slash + --> DJ015.py:52:10 + | +50 | # Error - leading trail slash and argument should stay in message +51 | urlpatterns_params_bad = [ +52 | path("/bad//", views.bad_view), # DJ015 + | ^^^^^^^^^^^^^^^^^^^ +53 | ] + | +help: Remove leading slash +49 | +50 | # Error - leading trail slash and argument should stay in message +51 | urlpatterns_params_bad = [ + - path("/bad//", views.bad_view), # DJ015 +52 + path("bad//", views.bad_view), # DJ015 +53 | ] From 4bf48e04fcf890c3213b98acb9d5faffc1ff6db8 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Mon, 6 Oct 2025 20:43:42 +0200 Subject: [PATCH 06/17] Exlicitly check for just arguments as well --- .../test/fixtures/flake8_django/DJ014.py | 1 + .../test/fixtures/flake8_django/DJ015.py | 1 + ..._flake8_django__tests__DJ014_DJ014.py.snap | 23 +++++++++++++++++-- ..._flake8_django__tests__DJ015_DJ015.py.snap | 23 +++++++++++++++++-- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py index e0943da309..556a3c5495 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py @@ -42,4 +42,5 @@ urlpatterns_mixed = [ # Error - missing trail slash and argument should stay in message urlpatterns_params_bad = [ path("bad/", views.bad_view), # DJ014 + path("", views.bad_view), # DJ014 ] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py index 23d30b6c05..c169fff641 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py @@ -50,4 +50,5 @@ urlpatterns_quotes = [ # Error - leading trail slash and argument should stay in message urlpatterns_params_bad = [ path("/bad//", views.bad_view), # DJ015 + path("/", views.bad_view), # DJ014 ] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap index 62842d19a0..5ce447bfa5 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap @@ -146,7 +146,8 @@ DJ014 [*] URL route `bad/` is missing a trailing slash 43 | urlpatterns_params_bad = [ 44 | path("bad/", views.bad_view), # DJ014 | ^^^^^^^^^^^^^^^^^ -45 | ] +45 | path("", views.bad_view), # DJ014 +46 | ] | help: Add trailing slash 41 | @@ -154,4 +155,22 @@ help: Add trailing slash 43 | urlpatterns_params_bad = [ - path("bad/", views.bad_view), # DJ014 44 + path("bad//", views.bad_view), # DJ014 -45 | ] +45 | path("", views.bad_view), # DJ014 +46 | ] + +DJ014 [*] URL route `` is missing a trailing slash + --> DJ014.py:45:10 + | +43 | urlpatterns_params_bad = [ +44 | path("bad/", views.bad_view), # DJ014 +45 | path("", views.bad_view), # DJ014 + | ^^^^^^^^^^^^^ +46 | ] + | +help: Add trailing slash +42 | # Error - missing trail slash and argument should stay in message +43 | urlpatterns_params_bad = [ +44 | path("bad/", views.bad_view), # DJ014 + - path("", views.bad_view), # DJ014 +45 + path("/", views.bad_view), # DJ014 +46 | ] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap index 7e984411dd..9da395bbf7 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap @@ -225,7 +225,8 @@ DJ015 [*] URL route `/bad//` has an unnecessary leading slash 51 | urlpatterns_params_bad = [ 52 | path("/bad//", views.bad_view), # DJ015 | ^^^^^^^^^^^^^^^^^^^ -53 | ] +53 | path("/", views.bad_view), # DJ014 +54 | ] | help: Remove leading slash 49 | @@ -233,4 +234,22 @@ help: Remove leading slash 51 | urlpatterns_params_bad = [ - path("/bad//", views.bad_view), # DJ015 52 + path("bad//", views.bad_view), # DJ015 -53 | ] +53 | path("/", views.bad_view), # DJ014 +54 | ] + +DJ015 [*] URL route `/` has an unnecessary leading slash + --> DJ015.py:53:10 + | +51 | urlpatterns_params_bad = [ +52 | path("/bad//", views.bad_view), # DJ015 +53 | path("/", views.bad_view), # DJ014 + | ^^^^^^^^^^^^^^ +54 | ] + | +help: Remove leading slash +50 | # Error - leading trail slash and argument should stay in message +51 | urlpatterns_params_bad = [ +52 | path("/bad//", views.bad_view), # DJ015 + - path("/", views.bad_view), # DJ014 +53 + path("", views.bad_view), # DJ014 +54 | ] From 8412263001f8a367bf32d5f16fc844e63f0927b8 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Mon, 6 Oct 2025 21:21:33 +0200 Subject: [PATCH 07/17] Fix code in test --- .../resources/test/fixtures/flake8_django/DJ015.py | 2 +- ...r__rules__flake8_django__tests__DJ015_DJ015.py.snap | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py index c169fff641..dd6c98fb05 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py @@ -50,5 +50,5 @@ urlpatterns_quotes = [ # Error - leading trail slash and argument should stay in message urlpatterns_params_bad = [ path("/bad//", views.bad_view), # DJ015 - path("/", views.bad_view), # DJ014 + path("/", views.bad_view), # DJ015 ] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap index 9da395bbf7..da9ec8f894 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap @@ -225,7 +225,7 @@ DJ015 [*] URL route `/bad//` has an unnecessary leading slash 51 | urlpatterns_params_bad = [ 52 | path("/bad//", views.bad_view), # DJ015 | ^^^^^^^^^^^^^^^^^^^ -53 | path("/", views.bad_view), # DJ014 +53 | path("/", views.bad_view), # DJ015 54 | ] | help: Remove leading slash @@ -234,7 +234,7 @@ help: Remove leading slash 51 | urlpatterns_params_bad = [ - path("/bad//", views.bad_view), # DJ015 52 + path("bad//", views.bad_view), # DJ015 -53 | path("/", views.bad_view), # DJ014 +53 | path("/", views.bad_view), # DJ015 54 | ] DJ015 [*] URL route `/` has an unnecessary leading slash @@ -242,7 +242,7 @@ DJ015 [*] URL route `/` has an unnecessary leading slash | 51 | urlpatterns_params_bad = [ 52 | path("/bad//", views.bad_view), # DJ015 -53 | path("/", views.bad_view), # DJ014 +53 | path("/", views.bad_view), # DJ015 | ^^^^^^^^^^^^^^ 54 | ] | @@ -250,6 +250,6 @@ help: Remove leading slash 50 | # Error - leading trail slash and argument should stay in message 51 | urlpatterns_params_bad = [ 52 | path("/bad//", views.bad_view), # DJ015 - - path("/", views.bad_view), # DJ014 -53 + path("", views.bad_view), # DJ014 + - path("/", views.bad_view), # DJ015 +53 + path("", views.bad_view), # DJ015 54 | ] From e9861177ed61cbdbe0ada42d790231462863bd33 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Tue, 7 Oct 2025 00:10:39 +0200 Subject: [PATCH 08/17] Add custom path functions --- .../flake8_django/DJ014_custom_paths.py | 20 ++++++ .../flake8_django/DJ015_custom_paths.py | 29 +++++++++ .../src/rules/flake8_django/mod.rs | 22 +++++++ .../rules/url_path_with_leading_slash.rs | 25 ++++++-- .../rules/url_path_without_trailing_slash.rs | 25 ++++++-- .../src/rules/flake8_django/settings.rs | 31 ++++++++++ ..._django__tests__DJ014_custom_paths.py.snap | 60 ++++++++++++++++++ ..._django__tests__DJ015_custom_paths.py.snap | 61 +++++++++++++++++++ crates/ruff_linter/src/settings/mod.rs | 5 +- crates/ruff_workspace/src/configuration.rs | 14 ++++- crates/ruff_workspace/src/options.rs | 42 ++++++++++++- ruff.schema.json | 40 ++++++++++++ 12 files changed, 361 insertions(+), 13 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py create mode 100644 crates/ruff_linter/src/rules/flake8_django/settings.rs create mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py new file mode 100644 index 0000000000..4e6e4460e7 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py @@ -0,0 +1,20 @@ +from mytools import path as mypath +from . import views + +# Test that custom path functions are also checked +urlpatterns_custom = [ + mypath("help", views.help_view), # DJ014 + mypath("about", views.about_view), # DJ014 +] + +# OK - custom path with trailing slash +urlpatterns_custom_ok = [ + mypath("help/", views.help_view), + mypath("about/", views.about_view), +] + +# Test that default django.urls.path still works +urlpatterns_default = [ + path("contact", views.contact_view), # DJ014 + path("contact/", views.contact_ok), # OK +] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py new file mode 100644 index 0000000000..f2c143fcdc --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py @@ -0,0 +1,29 @@ +from django.urls import path +from mytools import path as mypath +from . import views + +# Test that custom path functions are also checked for leading slashes +urlpatterns_custom = [ + mypath("/help/", views.help_view), # DJ015 + mypath("/about/", views.about_view), # DJ015 +] + +# OK - custom path without leading slash +urlpatterns_custom_ok = [ + mypath("help/", views.help_view), + mypath("about/", views.about_view), +] + +# Test that default django.urls.path still works +urlpatterns_default = [ + path("/contact/", views.contact_view), # DJ015 + path("contact/", views.contact_ok), # OK +] + +# OK - root path and empty string +urlpatterns_edge_cases = [ + path("/", views.root_view), # OK - root path + mypath("/", views.root_view), # OK - root path + path("", views.empty_view), # OK - empty string + mypath("", views.empty_view), # OK - empty string +] diff --git a/crates/ruff_linter/src/rules/flake8_django/mod.rs b/crates/ruff_linter/src/rules/flake8_django/mod.rs index 43d3e82bce..ac3222d5be 100644 --- a/crates/ruff_linter/src/rules/flake8_django/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_django/mod.rs @@ -1,6 +1,7 @@ //! Rules from [django-flake8](https://pypi.org/project/flake8-django/) mod helpers; pub(crate) mod rules; +pub mod settings; #[cfg(test)] mod tests { @@ -31,4 +32,25 @@ mod tests { assert_diagnostics!(snapshot, diagnostics); Ok(()) } + + #[test] + fn test_additional_path_functions_dj014() -> Result<()> { + let mut settings = + settings::LinterSettings::for_rule(Rule::DjangoURLPathWithoutTrailingSlash); + settings.flake8_django.additional_path_functions = vec!["mytools.path".to_string()]; + + let diagnostics = test_path(Path::new("flake8_django/DJ014_custom_paths.py"), &settings)?; + assert_diagnostics!("DJ014_custom_paths.py", diagnostics); + Ok(()) + } + + #[test] + fn test_additional_path_functions_dj015() -> Result<()> { + let mut settings = settings::LinterSettings::for_rule(Rule::DjangoURLPathWithLeadingSlash); + settings.flake8_django.additional_path_functions = vec!["mytools.path".to_string()]; + + let diagnostics = test_path(Path::new("flake8_django/DJ015_custom_paths.py"), &settings)?; + assert_diagnostics!("DJ015_custom_paths.py", diagnostics); + Ok(()) + } } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs index df325f9bcf..d12499a64d 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs @@ -65,14 +65,29 @@ pub(crate) fn url_path_with_leading_slash(checker: &Checker, call: &ast::ExprCal return; } - // Check if this is a call to django.urls.path - if !checker + // Check if this is a call to django.urls.path or any additional configured path functions + let is_path_function = checker .semantic() .resolve_qualified_name(&call.func) .is_some_and(|qualified_name| { - matches!(qualified_name.segments(), ["django", "urls", "path"]) - }) - { + let segments = qualified_name.segments(); + + // Check if it's the default django.urls.path + if matches!(segments, ["django", "urls", "path"]) { + return true; + } + + // Check if it matches any additional configured path functions + let qualified_name_str = segments.join("."); + checker + .settings() + .flake8_django + .additional_path_functions + .iter() + .any(|path| path == &qualified_name_str) + }); + + if !is_path_function { return; } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs index 1e85dc7526..317426de42 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs @@ -65,14 +65,29 @@ pub(crate) fn url_path_without_trailing_slash(checker: &Checker, call: &ast::Exp return; } - // Check if this is a call to django.urls.path - if !checker + // Check if this is a call to django.urls.path or any additional configured path functions + let is_path_function = checker .semantic() .resolve_qualified_name(&call.func) .is_some_and(|qualified_name| { - matches!(qualified_name.segments(), ["django", "urls", "path"]) - }) - { + let segments = qualified_name.segments(); + + // Check if it's the default django.urls.path + if matches!(segments, ["django", "urls", "path"]) { + return true; + } + + // Check if it matches any additional configured path functions + let qualified_name_str = segments.join("."); + checker + .settings() + .flake8_django + .additional_path_functions + .iter() + .any(|path| path == &qualified_name_str) + }); + + if !is_path_function { return; } diff --git a/crates/ruff_linter/src/rules/flake8_django/settings.rs b/crates/ruff_linter/src/rules/flake8_django/settings.rs new file mode 100644 index 0000000000..53a04ba4fb --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/settings.rs @@ -0,0 +1,31 @@ +//! Settings for the `flake8-django` plugin. + +use crate::display_settings; +use ruff_macros::CacheKey; +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Clone, CacheKey)] +pub struct Settings { + pub additional_path_functions: Vec, +} + +impl Default for Settings { + fn default() -> Self { + Self { + additional_path_functions: vec![], + } + } +} + +impl Display for Settings { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + display_settings! { + formatter = f, + namespace = "linter.flake8_django", + fields = [ + self.additional_path_functions | array + ] + } + Ok(()) + } +} diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap new file mode 100644 index 0000000000..12b3c7af26 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff_linter/src/rules/flake8_django/mod.rs +--- +DJ014 [*] URL route `help` is missing a trailing slash + --> DJ014_custom_paths.py:7:12 + | +5 | # Test that custom path functions are also checked +6 | urlpatterns_custom = [ +7 | mypath("help", views.help_view), # DJ014 + | ^^^^^^ +8 | mypath("about", views.about_view), # DJ014 +9 | ] + | +help: Add trailing slash +4 | +5 | # Test that custom path functions are also checked +6 | urlpatterns_custom = [ + - mypath("help", views.help_view), # DJ014 +7 + mypath("help/", views.help_view), # DJ014 +8 | mypath("about", views.about_view), # DJ014 +9 | ] +10 | + +DJ014 [*] URL route `about` is missing a trailing slash + --> DJ014_custom_paths.py:8:12 + | +6 | urlpatterns_custom = [ +7 | mypath("help", views.help_view), # DJ014 +8 | mypath("about", views.about_view), # DJ014 + | ^^^^^^^ +9 | ] + | +help: Add trailing slash +5 | # Test that custom path functions are also checked +6 | urlpatterns_custom = [ +7 | mypath("help", views.help_view), # DJ014 + - mypath("about", views.about_view), # DJ014 +8 + mypath("about/", views.about_view), # DJ014 +9 | ] +10 | +11 | # OK - custom path with trailing slash + +DJ014 [*] URL route `contact` is missing a trailing slash + --> DJ014_custom_paths.py:19:10 + | +17 | # Test that default django.urls.path still works +18 | urlpatterns_default = [ +19 | path("contact", views.contact_view), # DJ014 + | ^^^^^^^^^ +20 | path("contact/", views.contact_ok), # OK +21 | ] + | +help: Add trailing slash +16 | +17 | # Test that default django.urls.path still works +18 | urlpatterns_default = [ + - path("contact", views.contact_view), # DJ014 +19 + path("contact/", views.contact_view), # DJ014 +20 | path("contact/", views.contact_ok), # OK +21 | ] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap new file mode 100644 index 0000000000..6deafbfa06 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap @@ -0,0 +1,61 @@ +--- +source: crates/ruff_linter/src/rules/flake8_django/mod.rs +--- +DJ015 [*] URL route `/help/` has an unnecessary leading slash + --> DJ015_custom_paths.py:7:12 + | +5 | # Test that custom path functions are also checked for leading slashes +6 | urlpatterns_custom = [ +7 | mypath("/help/", views.help_view), # DJ015 + | ^^^^^^^^ +8 | mypath("/about/", views.about_view), # DJ015 +9 | ] + | +help: Remove leading slash +4 | +5 | # Test that custom path functions are also checked for leading slashes +6 | urlpatterns_custom = [ + - mypath("/help/", views.help_view), # DJ015 +7 + mypath("help/", views.help_view), # DJ015 +8 | mypath("/about/", views.about_view), # DJ015 +9 | ] +10 | + +DJ015 [*] URL route `/about/` has an unnecessary leading slash + --> DJ015_custom_paths.py:8:12 + | +6 | urlpatterns_custom = [ +7 | mypath("/help/", views.help_view), # DJ015 +8 | mypath("/about/", views.about_view), # DJ015 + | ^^^^^^^^^ +9 | ] + | +help: Remove leading slash +5 | # Test that custom path functions are also checked for leading slashes +6 | urlpatterns_custom = [ +7 | mypath("/help/", views.help_view), # DJ015 + - mypath("/about/", views.about_view), # DJ015 +8 + mypath("about/", views.about_view), # DJ015 +9 | ] +10 | +11 | # OK - custom path without leading slash + +DJ015 [*] URL route `/contact/` has an unnecessary leading slash + --> DJ015_custom_paths.py:19:10 + | +17 | # Test that default django.urls.path still works +18 | urlpatterns_default = [ +19 | path("/contact/", views.contact_view), # DJ015 + | ^^^^^^^^^^^ +20 | path("contact/", views.contact_ok), # OK +21 | ] + | +help: Remove leading slash +16 | +17 | # Test that default django.urls.path still works +18 | urlpatterns_default = [ + - path("/contact/", views.contact_view), # DJ015 +19 + path("contact/", views.contact_view), # DJ015 +20 | path("contact/", views.contact_ok), # OK +21 | ] +22 | diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index b94e4edafb..0c8b92f942 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -17,7 +17,7 @@ use crate::line_width::LineLength; use crate::registry::{Linter, Rule}; use crate::rules::{ flake8_annotations, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins, - flake8_comprehensions, flake8_copyright, flake8_errmsg, flake8_gettext, + flake8_comprehensions, flake8_copyright, flake8_django, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, @@ -262,6 +262,7 @@ pub struct LinterSettings { pub flake8_builtins: flake8_builtins::settings::Settings, pub flake8_comprehensions: flake8_comprehensions::settings::Settings, pub flake8_copyright: flake8_copyright::settings::Settings, + pub flake8_django: flake8_django::settings::Settings, pub flake8_errmsg: flake8_errmsg::settings::Settings, pub flake8_gettext: flake8_gettext::settings::Settings, pub flake8_implicit_str_concat: flake8_implicit_str_concat::settings::Settings, @@ -337,6 +338,7 @@ impl Display for LinterSettings { self.flake8_pytest_style | nested, self.flake8_quotes | nested, self.flake8_self | nested, + self.flake8_django | nested, self.flake8_tidy_imports | nested, self.flake8_type_checking | nested, self.flake8_unused_arguments | nested, @@ -438,6 +440,7 @@ impl LinterSettings { flake8_pytest_style: flake8_pytest_style::settings::Settings::default(), flake8_quotes: flake8_quotes::settings::Settings::default(), flake8_self: flake8_self::settings::Settings::default(), + flake8_django: flake8_django::settings::Settings::default(), flake8_tidy_imports: flake8_tidy_imports::settings::Settings::default(), flake8_type_checking: flake8_type_checking::settings::Settings::default(), flake8_unused_arguments: flake8_unused_arguments::settings::Settings::default(), diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 8ca5350d11..ab78faf36c 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -47,7 +47,7 @@ use ruff_python_formatter::{ use crate::options::{ AnalyzeOptions, Flake8AnnotationsOptions, Flake8BanditOptions, Flake8BooleanTrapOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ComprehensionsOptions, - Flake8CopyrightOptions, Flake8ErrMsgOptions, Flake8GetTextOptions, + Flake8CopyrightOptions, Flake8DjangoOptions, Flake8ErrMsgOptions, Flake8GetTextOptions, Flake8ImplicitStrConcatOptions, Flake8ImportConventionsOptions, Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions, Flake8UnusedArgumentsOptions, FormatOptions, IsortOptions, LintCommonOptions, LintOptions, @@ -363,6 +363,10 @@ impl Configuration { .map(Flake8CopyrightOptions::try_into_settings) .transpose()? .unwrap_or_default(), + flake8_django: lint + .flake8_django + .map(Flake8DjangoOptions::into_settings) + .unwrap_or_default(), flake8_errmsg: lint .flake8_errmsg .map(Flake8ErrMsgOptions::into_settings) @@ -659,6 +663,7 @@ pub struct LintConfiguration { pub flake8_builtins: Option, pub flake8_comprehensions: Option, pub flake8_copyright: Option, + pub flake8_django: Option, pub flake8_errmsg: Option, pub flake8_gettext: Option, pub flake8_implicit_str_concat: Option, @@ -776,6 +781,7 @@ impl LintConfiguration { flake8_builtins: options.common.flake8_builtins, flake8_comprehensions: options.common.flake8_comprehensions, flake8_copyright: options.common.flake8_copyright, + flake8_django: options.common.flake8_django, flake8_errmsg: options.common.flake8_errmsg, flake8_gettext: options.common.flake8_gettext, flake8_implicit_str_concat: options.common.flake8_implicit_str_concat, @@ -1165,6 +1171,7 @@ impl LintConfiguration { .flake8_comprehensions .combine(config.flake8_comprehensions), flake8_copyright: self.flake8_copyright.combine(config.flake8_copyright), + flake8_django: self.flake8_django.combine(config.flake8_django), flake8_errmsg: self.flake8_errmsg.combine(config.flake8_errmsg), flake8_gettext: self.flake8_gettext.combine(config.flake8_gettext), flake8_implicit_str_concat: self @@ -1388,6 +1395,7 @@ fn warn_about_deprecated_top_level_lint_options( flake8_builtins, flake8_comprehensions, flake8_copyright, + flake8_django, flake8_errmsg, flake8_quotes, flake8_self, @@ -1511,6 +1519,10 @@ fn warn_about_deprecated_top_level_lint_options( used_options.push("flake8-copyright"); } + if flake8_django.is_some() { + used_options.push("flake8-django"); + } + if flake8_errmsg.is_some() { used_options.push("flake8-errmsg"); } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 708d6dcf0b..a7b18232e7 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -22,7 +22,7 @@ use ruff_linter::rules::pep8_naming::settings::IgnoreNames; use ruff_linter::rules::pydocstyle::settings::Convention; use ruff_linter::rules::pylint::settings::ConstantType; use ruff_linter::rules::{ - flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_copyright, flake8_django, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, @@ -900,6 +900,10 @@ pub struct LintCommonOptions { #[option_group] pub flake8_copyright: Option, + /// Options for the `flake8-django` plugin. + #[option_group] + pub flake8_django: Option, + /// Options for the `flake8-errmsg` plugin. #[option_group] pub flake8_errmsg: Option, @@ -1431,6 +1435,39 @@ impl Flake8CopyrightOptions { } } +/// Options for the `flake8-django` plugin. +#[derive( + Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions, +)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Flake8DjangoOptions { + /// Additional qualified paths to Django URL path functions beyond + /// the default `django.urls.path`. This allows the rule to check + /// URL patterns defined using custom path functions or re-exported + /// path functions from other modules. + /// + /// For example, if you have a custom module `mytools` that re-exports + /// Django's path function, you can add `"mytools.path"` to this list. + #[option( + default = "[]", + value_type = "list[str]", + example = r#" + # Allow checking URL patterns from custom path functions + additional-path-functions = ["mytools.path", "myapp.urls.custom_path"] + "# + )] + pub additional_path_functions: Option>, +} + +impl Flake8DjangoOptions { + pub fn into_settings(self) -> flake8_django::settings::Settings { + flake8_django::settings::Settings { + additional_path_functions: self.additional_path_functions.unwrap_or_default(), + } + } +} + /// Options for the `flake8-errmsg` plugin. #[derive( Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions, @@ -3925,6 +3962,7 @@ pub struct LintOptionsWire { flake8_builtins: Option, flake8_comprehensions: Option, flake8_copyright: Option, + flake8_django: Option, flake8_errmsg: Option, flake8_quotes: Option, flake8_self: Option, @@ -3982,6 +4020,7 @@ impl From for LintOptions { flake8_builtins, flake8_comprehensions, flake8_copyright, + flake8_django, flake8_errmsg, flake8_quotes, flake8_self, @@ -4038,6 +4077,7 @@ impl From for LintOptions { flake8_builtins, flake8_comprehensions, flake8_copyright, + flake8_django, flake8_errmsg, flake8_quotes, flake8_self, diff --git a/ruff.schema.json b/ruff.schema.json index fa709fe329..74170ea4df 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -297,6 +297,18 @@ ], "deprecated": true }, + "flake8-django": { + "description": "Options for the `flake8-django` plugin.", + "deprecated": true, + "anyOf": [ + { + "$ref": "#/definitions/Flake8DjangoOptions" + }, + { + "type": "null" + } + ] + }, "flake8-errmsg": { "description": "Options for the `flake8-errmsg` plugin.", "anyOf": [ @@ -1138,6 +1150,23 @@ }, "additionalProperties": false }, + "Flake8DjangoOptions": { + "description": "Options for the `flake8-django` plugin.", + "type": "object", + "properties": { + "additional-path-functions": { + "description": "Additional qualified paths to Django URL path functions beyond the default `django.urls.path`. This allows the rule to check URL patterns defined using custom path functions or re-exported path functions from other modules.\n\nFor example, if you have a custom module `mytools` that re-exports Django's path function, you can add `\"mytools.path\"` to this list.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "Flake8ErrMsgOptions": { "description": "Options for the `flake8-errmsg` plugin.", "type": "object", @@ -2160,6 +2189,17 @@ } ] }, + "flake8-django": { + "description": "Options for the `flake8-django` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8DjangoOptions" + }, + { + "type": "null" + } + ] + }, "flake8-errmsg": { "description": "Options for the `flake8-errmsg` plugin.", "anyOf": [ From e491de2d2b08061eced8340201fefcbb67aa31a4 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Tue, 7 Oct 2025 10:04:33 +0200 Subject: [PATCH 09/17] cargo clippy --fix --- crates/ruff_linter/src/rules/flake8_django/settings.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_django/settings.rs b/crates/ruff_linter/src/rules/flake8_django/settings.rs index 53a04ba4fb..a1e578d4c1 100644 --- a/crates/ruff_linter/src/rules/flake8_django/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_django/settings.rs @@ -4,19 +4,11 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, Clone, CacheKey)] +#[derive(Debug, Clone, CacheKey, Default)] pub struct Settings { pub additional_path_functions: Vec, } -impl Default for Settings { - fn default() -> Self { - Self { - additional_path_functions: vec![], - } - } -} - impl Display for Settings { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { display_settings! { From 616174f5d64a314dcf368b63c9d2da04ed1d596d Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Tue, 7 Oct 2025 12:44:53 +0200 Subject: [PATCH 10/17] accept more missed snapshot reviewa --- .../tests/snapshots/show_settings__display_default_settings.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index 28a6607816..ef0aab8f98 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -292,6 +292,7 @@ linter.flake8_self.ignore_names = [ _name_, _value_, ] +linter.flake8_django.additional_path_functions = [] linter.flake8_tidy_imports.ban_relative_imports = "parents" linter.flake8_tidy_imports.banned_api = {} linter.flake8_tidy_imports.banned_module_level_imports = [] From 496aad45e1b70cd0e3eeabb1ab7eddcb81fee7dc Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Tue, 7 Oct 2025 13:30:55 +0200 Subject: [PATCH 11/17] Recreate snapshots after rebase --- .../cli__lint__requires_python_extend_from_shared_config.snap | 2 +- .../tests/cli/snapshots/cli__lint__requires_python_no_tool.snap | 2 +- .../cli__lint__requires_python_no_tool_preview_enabled.snap | 2 +- ...__lint__requires_python_no_tool_target_version_override.snap | 2 +- .../cli__lint__requires_python_pyproject_toml_above.snap | 2 +- ...i__lint__requires_python_pyproject_toml_above_with_tool.snap | 2 +- .../snapshots/cli__lint__requires_python_ruff_toml_above.snap | 1 + ...cli__lint__requires_python_ruff_toml_no_target_fallback.snap | 2 +- 8 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index 62bde10fe3..e3d7e4cde4 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -9,7 +9,6 @@ info: - concise - "--show-settings" - test.py -snapshot_kind: text --- success: true exit_code: 0 @@ -180,6 +179,7 @@ linter.flake8_self.ignore_names = [ _name_, _value_, ] +linter.flake8_django.additional_path_functions = [] linter.flake8_tidy_imports.ban_relative_imports = "parents" linter.flake8_tidy_imports.banned_api = {} linter.flake8_tidy_imports.banned_module_level_imports = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index a7b4b2c978..b2ac8b8f16 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -12,7 +12,6 @@ info: - UP007 - test.py - "-" -snapshot_kind: text --- success: true exit_code: 0 @@ -182,6 +181,7 @@ linter.flake8_self.ignore_names = [ _name_, _value_, ] +linter.flake8_django.additional_path_functions = [] linter.flake8_tidy_imports.ban_relative_imports = "parents" linter.flake8_tidy_imports.banned_api = {} linter.flake8_tidy_imports.banned_module_level_imports = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap index 4e33c123a4..c9c0bcdb76 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap @@ -13,7 +13,6 @@ info: - UP007 - test.py - "-" -snapshot_kind: text --- success: true exit_code: 0 @@ -184,6 +183,7 @@ linter.flake8_self.ignore_names = [ _name_, _value_, ] +linter.flake8_django.additional_path_functions = [] linter.flake8_tidy_imports.ban_relative_imports = "parents" linter.flake8_tidy_imports.banned_api = {} linter.flake8_tidy_imports.banned_module_level_imports = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index 929943558e..394f5cce04 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -14,7 +14,6 @@ info: - py310 - test.py - "-" -snapshot_kind: text --- success: true exit_code: 0 @@ -184,6 +183,7 @@ linter.flake8_self.ignore_names = [ _name_, _value_, ] +linter.flake8_django.additional_path_functions = [] linter.flake8_tidy_imports.ban_relative_imports = "parents" linter.flake8_tidy_imports.banned_api = {} linter.flake8_tidy_imports.banned_module_level_imports = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index 291cb62d6e..277609d7d5 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -11,7 +11,6 @@ info: - "--select" - UP007 - foo/test.py -snapshot_kind: text --- success: true exit_code: 0 @@ -181,6 +180,7 @@ linter.flake8_self.ignore_names = [ _name_, _value_, ] +linter.flake8_django.additional_path_functions = [] linter.flake8_tidy_imports.ban_relative_imports = "parents" linter.flake8_tidy_imports.banned_api = {} linter.flake8_tidy_imports.banned_module_level_imports = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index d9f9402895..681e832518 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -11,7 +11,6 @@ info: - "--select" - UP007 - foo/test.py -snapshot_kind: text --- success: true exit_code: 0 @@ -181,6 +180,7 @@ linter.flake8_self.ignore_names = [ _name_, _value_, ] +linter.flake8_django.additional_path_functions = [] linter.flake8_tidy_imports.ban_relative_imports = "parents" linter.flake8_tidy_imports.banned_api = {} linter.flake8_tidy_imports.banned_module_level_imports = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index e9ca7bd400..9ff2479c42 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -179,6 +179,7 @@ linter.flake8_self.ignore_names = [ _name_, _value_, ] +linter.flake8_django.additional_path_functions = [] linter.flake8_tidy_imports.ban_relative_imports = "parents" linter.flake8_tidy_imports.banned_api = {} linter.flake8_tidy_imports.banned_module_level_imports = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index a3f9343314..32d76a7e3a 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -9,7 +9,6 @@ info: - concise - test.py - "--show-settings" -snapshot_kind: text --- success: true exit_code: 0 @@ -180,6 +179,7 @@ linter.flake8_self.ignore_names = [ _name_, _value_, ] +linter.flake8_django.additional_path_functions = [] linter.flake8_tidy_imports.ban_relative_imports = "parents" linter.flake8_tidy_imports.banned_api = {} linter.flake8_tidy_imports.banned_module_level_imports = [] From 331623e48ddb4fbeaa22a867d11a2f4316d311d0 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Tue, 7 Oct 2025 13:59:14 +0200 Subject: [PATCH 12/17] More snapshots for toml --- .../snapshots/cli__lint__requires_python_ruff_toml_above-2.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index da3eb66afc..7986ed4928 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -179,6 +179,7 @@ linter.flake8_self.ignore_names = [ _name_, _value_, ] +linter.flake8_django.additional_path_functions = [] linter.flake8_tidy_imports.ban_relative_imports = "parents" linter.flake8_tidy_imports.banned_api = {} linter.flake8_tidy_imports.banned_module_level_imports = [] From 571d392bd6196ab107f3af015adece0925cc2a90 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Tue, 7 Oct 2025 14:03:31 +0200 Subject: [PATCH 13/17] Add explicit case for root and empty paths --- .../test/fixtures/flake8_django/DJ014_custom_paths.py | 6 ++++++ ..._rules__flake8_django__tests__DJ014_custom_paths.py.snap | 1 + 2 files changed, 7 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py index 4e6e4460e7..55abcf7d98 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py @@ -18,3 +18,9 @@ urlpatterns_default = [ path("contact", views.contact_view), # DJ014 path("contact/", views.contact_ok), # OK ] + +# OK - root path and empty string +urlpatterns_edge_cases = [ + mypath("/", views.root_view), # OK - root path + mypath("", views.empty_view), # OK - empty string +] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap index 12b3c7af26..32e213b14a 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap @@ -58,3 +58,4 @@ help: Add trailing slash 19 + path("contact/", views.contact_view), # DJ014 20 | path("contact/", views.contact_ok), # OK 21 | ] +22 | From abaa162bdc621070e38d5b2a79f039b5a15f3297 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Tue, 7 Oct 2025 14:23:10 +0200 Subject: [PATCH 14/17] Remove "seen Django module" check, rely purely on the qualified path --- .../flake8_django/DJ014_custom_paths.py | 9 +- .../flake8_django/DJ015_custom_paths.py | 14 +-- .../rules/url_path_with_leading_slash.rs | 5 - .../rules/url_path_without_trailing_slash.rs | 5 - ..._django__tests__DJ014_custom_paths.py.snap | 98 +++++++++++-------- ..._django__tests__DJ015_custom_paths.py.snap | 98 +++++++++++-------- 6 files changed, 130 insertions(+), 99 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py index 55abcf7d98..a93e095801 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py @@ -13,10 +13,11 @@ urlpatterns_custom_ok = [ mypath("about/", views.about_view), ] -# Test that default django.urls.path still works -urlpatterns_default = [ - path("contact", views.contact_view), # DJ014 - path("contact/", views.contact_ok), # OK +# Test multiple violations in same list +urlpatterns_multiple = [ + mypath("api/users", views.users_view), # DJ014 + mypath("api/posts", views.posts_view), # DJ014 + mypath("api/comments/", views.comments_view), # OK ] # OK - root path and empty string diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py index f2c143fcdc..af523fbe85 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py @@ -1,4 +1,3 @@ -from django.urls import path from mytools import path as mypath from . import views @@ -14,16 +13,17 @@ urlpatterns_custom_ok = [ mypath("about/", views.about_view), ] -# Test that default django.urls.path still works -urlpatterns_default = [ - path("/contact/", views.contact_view), # DJ015 - path("contact/", views.contact_ok), # OK +# Test multiple violations in same list +urlpatterns_multiple = [ + mypath("/api/users/", views.users_view), # DJ015 + mypath("/api/posts/", views.posts_view), # DJ015 + mypath("api/comments/", views.comments_view), # OK ] + # OK - root path and empty string urlpatterns_edge_cases = [ - path("/", views.root_view), # OK - root path + mypath("/", views.root_view), # OK - root path - path("", views.empty_view), # OK - empty string mypath("", views.empty_view), # OK - empty string ] diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs index d12499a64d..281b45dc85 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs @@ -1,6 +1,5 @@ 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; @@ -61,10 +60,6 @@ impl AlwaysFixableViolation for DjangoURLPathWithLeadingSlash { /// 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 or any additional configured path functions let is_path_function = checker .semantic() diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs index 317426de42..fa1fbdeb88 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs @@ -1,6 +1,5 @@ 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; @@ -61,10 +60,6 @@ impl AlwaysFixableViolation for DjangoURLPathWithoutTrailingSlash { /// 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 or any additional configured path functions let is_path_function = checker .semantic() diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap index 32e213b14a..154f6d4f3d 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap @@ -2,60 +2,80 @@ source: crates/ruff_linter/src/rules/flake8_django/mod.rs --- DJ014 [*] URL route `help` is missing a trailing slash - --> DJ014_custom_paths.py:7:12 + --> DJ014_custom_paths.py:6:12 | -5 | # Test that custom path functions are also checked -6 | urlpatterns_custom = [ -7 | mypath("help", views.help_view), # DJ014 +4 | # Test that custom path functions are also checked +5 | urlpatterns_custom = [ +6 | mypath("help", views.help_view), # DJ014 | ^^^^^^ -8 | mypath("about", views.about_view), # DJ014 -9 | ] +7 | mypath("about", views.about_view), # DJ014 +8 | ] | help: Add trailing slash -4 | -5 | # Test that custom path functions are also checked -6 | urlpatterns_custom = [ - - mypath("help", views.help_view), # DJ014 -7 + mypath("help/", views.help_view), # DJ014 -8 | mypath("about", views.about_view), # DJ014 -9 | ] -10 | +3 | +4 | # Test that custom path functions are also checked +5 | urlpatterns_custom = [ + - mypath("help", views.help_view), # DJ014 +6 + mypath("help/", views.help_view), # DJ014 +7 | mypath("about", views.about_view), # DJ014 +8 | ] +9 | DJ014 [*] URL route `about` is missing a trailing slash - --> DJ014_custom_paths.py:8:12 + --> DJ014_custom_paths.py:7:12 | -6 | urlpatterns_custom = [ -7 | mypath("help", views.help_view), # DJ014 -8 | mypath("about", views.about_view), # DJ014 +5 | urlpatterns_custom = [ +6 | mypath("help", views.help_view), # DJ014 +7 | mypath("about", views.about_view), # DJ014 | ^^^^^^^ -9 | ] +8 | ] | help: Add trailing slash -5 | # Test that custom path functions are also checked -6 | urlpatterns_custom = [ -7 | mypath("help", views.help_view), # DJ014 +4 | # Test that custom path functions are also checked +5 | urlpatterns_custom = [ +6 | mypath("help", views.help_view), # DJ014 - mypath("about", views.about_view), # DJ014 -8 + mypath("about/", views.about_view), # DJ014 -9 | ] -10 | -11 | # OK - custom path with trailing slash +7 + mypath("about/", views.about_view), # DJ014 +8 | ] +9 | +10 | # OK - custom path with trailing slash -DJ014 [*] URL route `contact` is missing a trailing slash - --> DJ014_custom_paths.py:19:10 +DJ014 [*] URL route `api/users` is missing a trailing slash + --> DJ014_custom_paths.py:18:12 | -17 | # Test that default django.urls.path still works -18 | urlpatterns_default = [ -19 | path("contact", views.contact_view), # DJ014 - | ^^^^^^^^^ -20 | path("contact/", views.contact_ok), # OK +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ +18 | mypath("api/users", views.users_view), # DJ014 + | ^^^^^^^^^^^ +19 | mypath("api/posts", views.posts_view), # DJ014 +20 | mypath("api/comments/", views.comments_view), # OK + | +help: Add trailing slash +15 | +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ + - mypath("api/users", views.users_view), # DJ014 +18 + mypath("api/users/", views.users_view), # DJ014 +19 | mypath("api/posts", views.posts_view), # DJ014 +20 | mypath("api/comments/", views.comments_view), # OK +21 | ] + +DJ014 [*] URL route `api/posts` is missing a trailing slash + --> DJ014_custom_paths.py:19:12 + | +17 | urlpatterns_multiple = [ +18 | mypath("api/users", views.users_view), # DJ014 +19 | mypath("api/posts", views.posts_view), # DJ014 + | ^^^^^^^^^^^ +20 | mypath("api/comments/", views.comments_view), # OK 21 | ] | help: Add trailing slash -16 | -17 | # Test that default django.urls.path still works -18 | urlpatterns_default = [ - - path("contact", views.contact_view), # DJ014 -19 + path("contact/", views.contact_view), # DJ014 -20 | path("contact/", views.contact_ok), # OK +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ +18 | mypath("api/users", views.users_view), # DJ014 + - mypath("api/posts", views.posts_view), # DJ014 +19 + mypath("api/posts/", views.posts_view), # DJ014 +20 | mypath("api/comments/", views.comments_view), # OK 21 | ] 22 | diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap index 6deafbfa06..4081d1ae4d 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap @@ -2,60 +2,80 @@ source: crates/ruff_linter/src/rules/flake8_django/mod.rs --- DJ015 [*] URL route `/help/` has an unnecessary leading slash - --> DJ015_custom_paths.py:7:12 + --> DJ015_custom_paths.py:6:12 | -5 | # Test that custom path functions are also checked for leading slashes -6 | urlpatterns_custom = [ -7 | mypath("/help/", views.help_view), # DJ015 +4 | # Test that custom path functions are also checked for leading slashes +5 | urlpatterns_custom = [ +6 | mypath("/help/", views.help_view), # DJ015 | ^^^^^^^^ -8 | mypath("/about/", views.about_view), # DJ015 -9 | ] +7 | mypath("/about/", views.about_view), # DJ015 +8 | ] | help: Remove leading slash -4 | -5 | # Test that custom path functions are also checked for leading slashes -6 | urlpatterns_custom = [ - - mypath("/help/", views.help_view), # DJ015 -7 + mypath("help/", views.help_view), # DJ015 -8 | mypath("/about/", views.about_view), # DJ015 -9 | ] -10 | +3 | +4 | # Test that custom path functions are also checked for leading slashes +5 | urlpatterns_custom = [ + - mypath("/help/", views.help_view), # DJ015 +6 + mypath("help/", views.help_view), # DJ015 +7 | mypath("/about/", views.about_view), # DJ015 +8 | ] +9 | DJ015 [*] URL route `/about/` has an unnecessary leading slash - --> DJ015_custom_paths.py:8:12 + --> DJ015_custom_paths.py:7:12 | -6 | urlpatterns_custom = [ -7 | mypath("/help/", views.help_view), # DJ015 -8 | mypath("/about/", views.about_view), # DJ015 +5 | urlpatterns_custom = [ +6 | mypath("/help/", views.help_view), # DJ015 +7 | mypath("/about/", views.about_view), # DJ015 | ^^^^^^^^^ -9 | ] +8 | ] | help: Remove leading slash -5 | # Test that custom path functions are also checked for leading slashes -6 | urlpatterns_custom = [ -7 | mypath("/help/", views.help_view), # DJ015 +4 | # Test that custom path functions are also checked for leading slashes +5 | urlpatterns_custom = [ +6 | mypath("/help/", views.help_view), # DJ015 - mypath("/about/", views.about_view), # DJ015 -8 + mypath("about/", views.about_view), # DJ015 -9 | ] -10 | -11 | # OK - custom path without leading slash +7 + mypath("about/", views.about_view), # DJ015 +8 | ] +9 | +10 | # OK - custom path without leading slash -DJ015 [*] URL route `/contact/` has an unnecessary leading slash - --> DJ015_custom_paths.py:19:10 +DJ015 [*] URL route `/api/users/` has an unnecessary leading slash + --> DJ015_custom_paths.py:18:12 | -17 | # Test that default django.urls.path still works -18 | urlpatterns_default = [ -19 | path("/contact/", views.contact_view), # DJ015 - | ^^^^^^^^^^^ -20 | path("contact/", views.contact_ok), # OK +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ +18 | mypath("/api/users/", views.users_view), # DJ015 + | ^^^^^^^^^^^^^ +19 | mypath("/api/posts/", views.posts_view), # DJ015 +20 | mypath("api/comments/", views.comments_view), # OK + | +help: Remove leading slash +15 | +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ + - mypath("/api/users/", views.users_view), # DJ015 +18 + mypath("api/users/", views.users_view), # DJ015 +19 | mypath("/api/posts/", views.posts_view), # DJ015 +20 | mypath("api/comments/", views.comments_view), # OK +21 | ] + +DJ015 [*] URL route `/api/posts/` has an unnecessary leading slash + --> DJ015_custom_paths.py:19:12 + | +17 | urlpatterns_multiple = [ +18 | mypath("/api/users/", views.users_view), # DJ015 +19 | mypath("/api/posts/", views.posts_view), # DJ015 + | ^^^^^^^^^^^^^ +20 | mypath("api/comments/", views.comments_view), # OK 21 | ] | help: Remove leading slash -16 | -17 | # Test that default django.urls.path still works -18 | urlpatterns_default = [ - - path("/contact/", views.contact_view), # DJ015 -19 + path("contact/", views.contact_view), # DJ015 -20 | path("contact/", views.contact_ok), # OK +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ +18 | mypath("/api/users/", views.users_view), # DJ015 + - mypath("/api/posts/", views.posts_view), # DJ015 +19 + mypath("api/posts/", views.posts_view), # DJ015 +20 | mypath("api/comments/", views.comments_view), # OK 21 | ] 22 | From bfc179440e26f3bc551be9fd50230241b659094a Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Wed, 29 Oct 2025 20:32:08 +0100 Subject: [PATCH 15/17] update schema --- ruff.schema.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ruff.schema.json b/ruff.schema.json index 74170ea4df..06cee2c048 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -299,7 +299,6 @@ }, "flake8-django": { "description": "Options for the `flake8-django` plugin.", - "deprecated": true, "anyOf": [ { "$ref": "#/definitions/Flake8DjangoOptions" @@ -307,7 +306,8 @@ { "type": "null" } - ] + ], + "deprecated": true }, "flake8-errmsg": { "description": "Options for the `flake8-errmsg` plugin.", @@ -1155,7 +1155,7 @@ "type": "object", "properties": { "additional-path-functions": { - "description": "Additional qualified paths to Django URL path functions beyond the default `django.urls.path`. This allows the rule to check URL patterns defined using custom path functions or re-exported path functions from other modules.\n\nFor example, if you have a custom module `mytools` that re-exports Django's path function, you can add `\"mytools.path\"` to this list.", + "description": "Additional qualified paths to Django URL path functions beyond\nthe default `django.urls.path`. This allows the rule to check\nURL patterns defined using custom path functions or re-exported\npath functions from other modules.\n\nFor example, if you have a custom module `mytools` that re-exports\nDjango's path function, you can add `\"mytools.path\"` to this list.", "type": [ "array", "null" From ec2c18294eee27777a5b273e69b52b580d8185ff Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Fri, 31 Oct 2025 17:02:43 +0100 Subject: [PATCH 16/17] Move DJ014->DJ100 and DJ015->DJ101 --- .../flake8_django/{DJ014.py => DJ100.py} | 18 +- ..._custom_paths.py => DJ100_custom_paths.py} | 8 +- .../flake8_django/{DJ015.py => DJ101.py} | 26 +- ..._custom_paths.py => DJ101_custom_paths.py} | 8 +- crates/ruff_linter/src/codes.rs | 4 +- .../src/rules/flake8_django/mod.rs | 16 +- .../rules/url_path_with_leading_slash.rs | 2 +- .../rules/url_path_without_trailing_slash.rs | 2 +- ..._flake8_django__tests__DJ014_DJ014.py.snap | 176 ------------ ..._django__tests__DJ014_custom_paths.py.snap | 81 ------ ..._flake8_django__tests__DJ015_DJ015.py.snap | 255 ------------------ ..._django__tests__DJ015_custom_paths.py.snap | 81 ------ ..._flake8_django__tests__DJ100_DJ100.py.snap | 176 ++++++++++++ ..._django__tests__DJ100_custom_paths.py.snap | 81 ++++++ ..._flake8_django__tests__DJ101_DJ101.py.snap | 255 ++++++++++++++++++ ..._django__tests__DJ101_custom_paths.py.snap | 81 ++++++ ruff.schema.json | 6 +- 17 files changed, 639 insertions(+), 637 deletions(-) rename crates/ruff_linter/resources/test/fixtures/flake8_django/{DJ014.py => DJ100.py} (65%) rename crates/ruff_linter/resources/test/fixtures/flake8_django/{DJ014_custom_paths.py => DJ100_custom_paths.py} (74%) rename crates/ruff_linter/resources/test/fixtures/flake8_django/{DJ015.py => DJ101.py} (63%) rename crates/ruff_linter/resources/test/fixtures/flake8_django/{DJ015_custom_paths.py => DJ101_custom_paths.py} (74%) delete mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap delete mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap delete mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap delete mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ100_DJ100.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ100_custom_paths.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ101_DJ101.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ101_custom_paths.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ100.py similarity index 65% rename from crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py rename to crates/ruff_linter/resources/test/fixtures/flake8_django/DJ100.py index 556a3c5495..3f64e94f90 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ100.py @@ -3,11 +3,11 @@ 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 + path("help", views.help_view), # DJ100 + path("about", views.about_view), # DJ100 + path("contact", views.contact_view), # DJ100 + path("api/users", views.users_view), # DJ100 + path("blog/posts", views.posts_view), # DJ100 ] # OK - has trailing slash @@ -34,13 +34,13 @@ urlpatterns_params = [ # Mixed cases urlpatterns_mixed = [ path("good/", views.good_view), - path("bad", views.bad_view), # DJ014 + path("bad", views.bad_view), # DJ100 path("also-good/", views.also_good_view), - path("also-bad", views.also_bad_view), # DJ014 + path("also-bad", views.also_bad_view), # DJ100 ] # Error - missing trail slash and argument should stay in message urlpatterns_params_bad = [ - path("bad/", views.bad_view), # DJ014 - path("", views.bad_view), # DJ014 + path("bad/", views.bad_view), # DJ100 + path("", views.bad_view), # DJ100 ] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ100_custom_paths.py similarity index 74% rename from crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py rename to crates/ruff_linter/resources/test/fixtures/flake8_django/DJ100_custom_paths.py index a93e095801..cc7d9bd1c1 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ014_custom_paths.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ100_custom_paths.py @@ -3,8 +3,8 @@ from . import views # Test that custom path functions are also checked urlpatterns_custom = [ - mypath("help", views.help_view), # DJ014 - mypath("about", views.about_view), # DJ014 + mypath("help", views.help_view), # DJ100 + mypath("about", views.about_view), # DJ100 ] # OK - custom path with trailing slash @@ -15,8 +15,8 @@ urlpatterns_custom_ok = [ # Test multiple violations in same list urlpatterns_multiple = [ - mypath("api/users", views.users_view), # DJ014 - mypath("api/posts", views.posts_view), # DJ014 + mypath("api/users", views.users_view), # DJ100 + mypath("api/posts", views.posts_view), # DJ100 mypath("api/comments/", views.comments_view), # OK ] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ101.py similarity index 63% rename from crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py rename to crates/ruff_linter/resources/test/fixtures/flake8_django/DJ101.py index dd6c98fb05..67cdc9f6c3 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ101.py @@ -3,11 +3,11 @@ 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 + path("/help/", views.help_view), # DJ101 + path("/about/", views.about_view), # DJ101 + path("/contact/", views.contact_view), # DJ101 + path("/api/users/", views.users_view), # DJ101 + path("/blog/posts/", views.posts_view), # DJ101 ] # OK - no leading slash @@ -34,21 +34,21 @@ urlpatterns_params = [ # Mixed cases urlpatterns_mixed = [ path("good/", views.good_view), - path("/bad/", views.bad_view), # DJ015 + path("/bad/", views.bad_view), # DJ101 path("also-good/", views.also_good_view), - path("/also-bad/", views.also_bad_view), # DJ015 + path("/also-bad/", views.also_bad_view), # DJ101 ] # 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 + path('/single-quote/', views.single_quote_view), # DJ101 + path("/double-quote/", views.double_quote_view), # DJ101 + path('''/triple-single/''', views.triple_single_view), # DJ101 + path("""/triple-double/""", views.triple_double_view), # DJ101 ] # Error - leading trail slash and argument should stay in message urlpatterns_params_bad = [ - path("/bad//", views.bad_view), # DJ015 - path("/", views.bad_view), # DJ015 + path("/bad//", views.bad_view), # DJ101 + path("/", views.bad_view), # DJ101 ] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ101_custom_paths.py similarity index 74% rename from crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py rename to crates/ruff_linter/resources/test/fixtures/flake8_django/DJ101_custom_paths.py index af523fbe85..7b842c39f2 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ015_custom_paths.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ101_custom_paths.py @@ -3,8 +3,8 @@ from . import views # Test that custom path functions are also checked for leading slashes urlpatterns_custom = [ - mypath("/help/", views.help_view), # DJ015 - mypath("/about/", views.about_view), # DJ015 + mypath("/help/", views.help_view), # DJ101 + mypath("/about/", views.about_view), # DJ101 ] # OK - custom path without leading slash @@ -15,8 +15,8 @@ urlpatterns_custom_ok = [ # Test multiple violations in same list urlpatterns_multiple = [ - mypath("/api/users/", views.users_view), # DJ015 - mypath("/api/posts/", views.posts_view), # DJ015 + mypath("/api/users/", views.users_view), # DJ101 + mypath("/api/posts/", views.posts_view), # DJ101 mypath("api/comments/", views.comments_view), # OK ] diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 3e25d15f4c..0a989742d6 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1100,8 +1100,8 @@ 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, - (Flake8Django, "015") => rules::flake8_django::rules::DjangoURLPathWithLeadingSlash, + (Flake8Django, "100") => rules::flake8_django::rules::DjangoURLPathWithoutTrailingSlash, + (Flake8Django, "101") => rules::flake8_django::rules::DjangoURLPathWithLeadingSlash, // flynt // Reserved: (Flynt, "001") => Rule: :StringConcatenationToFString, diff --git a/crates/ruff_linter/src/rules/flake8_django/mod.rs b/crates/ruff_linter/src/rules/flake8_django/mod.rs index ac3222d5be..9b2f90b5b1 100644 --- a/crates/ruff_linter/src/rules/flake8_django/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_django/mod.rs @@ -19,8 +19,8 @@ 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::DjangoURLPathWithLeadingSlash, Path::new("DJ015.py"))] + #[test_case(Rule::DjangoURLPathWithoutTrailingSlash, Path::new("DJ100.py"))] + #[test_case(Rule::DjangoURLPathWithLeadingSlash, Path::new("DJ101.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<()> { @@ -34,23 +34,23 @@ mod tests { } #[test] - fn test_additional_path_functions_dj014() -> Result<()> { + fn test_additional_path_functions_dj100() -> Result<()> { let mut settings = settings::LinterSettings::for_rule(Rule::DjangoURLPathWithoutTrailingSlash); settings.flake8_django.additional_path_functions = vec!["mytools.path".to_string()]; - let diagnostics = test_path(Path::new("flake8_django/DJ014_custom_paths.py"), &settings)?; - assert_diagnostics!("DJ014_custom_paths.py", diagnostics); + let diagnostics = test_path(Path::new("flake8_django/DJ100_custom_paths.py"), &settings)?; + assert_diagnostics!("DJ100_custom_paths.py", diagnostics); Ok(()) } #[test] - fn test_additional_path_functions_dj015() -> Result<()> { + fn test_additional_path_functions_dj101() -> Result<()> { let mut settings = settings::LinterSettings::for_rule(Rule::DjangoURLPathWithLeadingSlash); settings.flake8_django.additional_path_functions = vec!["mytools.path".to_string()]; - let diagnostics = test_path(Path::new("flake8_django/DJ015_custom_paths.py"), &settings)?; - assert_diagnostics!("DJ015_custom_paths.py", diagnostics); + let diagnostics = test_path(Path::new("flake8_django/DJ101_custom_paths.py"), &settings)?; + assert_diagnostics!("DJ101_custom_paths.py", diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs index 281b45dc85..91cde9f837 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs @@ -58,7 +58,7 @@ impl AlwaysFixableViolation for DjangoURLPathWithLeadingSlash { } } -/// DJ015 +/// DJ101 pub(crate) fn url_path_with_leading_slash(checker: &Checker, call: &ast::ExprCall) { // Check if this is a call to django.urls.path or any additional configured path functions let is_path_function = checker diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs index fa1fbdeb88..07faaf47b5 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs @@ -58,7 +58,7 @@ impl AlwaysFixableViolation for DjangoURLPathWithoutTrailingSlash { } } -/// DJ014 +/// DJ100 pub(crate) fn url_path_without_trailing_slash(checker: &Checker, call: &ast::ExprCall) { // Check if this is a call to django.urls.path or any additional configured path functions let is_path_function = checker diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap deleted file mode 100644 index 5ce447bfa5..0000000000 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_DJ014.py.snap +++ /dev/null @@ -1,176 +0,0 @@ ---- -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 - | -help: Add trailing slash -3 | -4 | # Errors - missing trailing slash -5 | urlpatterns = [ - - path("help", views.help_view), # DJ014 -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 `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 - | -help: Add trailing slash -4 | # Errors - missing trailing slash -5 | urlpatterns = [ -6 | path("help", views.help_view), # DJ014 - - path("about", views.about_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 `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 - | -help: Add trailing slash -5 | urlpatterns = [ -6 | path("help", views.help_view), # DJ014 -7 | path("about", views.about_view), # DJ014 - - path("contact", views.contact_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 `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 | ] - | -help: Add trailing slash -6 | path("help", views.help_view), # DJ014 -7 | path("about", views.about_view), # DJ014 -8 | path("contact", views.contact_view), # DJ014 - - path("api/users", views.users_view), # DJ014 -9 + path("api/users/", views.users_view), # DJ014 -10 | path("blog/posts", views.posts_view), # DJ014 -11 | ] -12 | - -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 | ] - | -help: Add trailing slash -7 | path("about", views.about_view), # DJ014 -8 | path("contact", views.contact_view), # DJ014 -9 | path("api/users", views.users_view), # DJ014 - - path("blog/posts", views.posts_view), # DJ014 -10 + path("blog/posts/", views.posts_view), # DJ014 -11 | ] -12 | -13 | # OK - has trailing slash - -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 - | -help: Add trailing slash -34 | # Mixed cases -35 | urlpatterns_mixed = [ -36 | path("good/", views.good_view), - - path("bad", views.bad_view), # DJ014 -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 | ] - -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 | ] - | -help: Add trailing slash -36 | path("good/", views.good_view), -37 | path("bad", views.bad_view), # DJ014 -38 | path("also-good/", views.also_good_view), - - path("also-bad", views.also_bad_view), # DJ014 -39 + path("also-bad/", views.also_bad_view), # DJ014 -40 | ] -41 | -42 | # Error - missing trail slash and argument should stay in message - -DJ014 [*] URL route `bad/` is missing a trailing slash - --> DJ014.py:44:10 - | -42 | # Error - missing trail slash and argument should stay in message -43 | urlpatterns_params_bad = [ -44 | path("bad/", views.bad_view), # DJ014 - | ^^^^^^^^^^^^^^^^^ -45 | path("", views.bad_view), # DJ014 -46 | ] - | -help: Add trailing slash -41 | -42 | # Error - missing trail slash and argument should stay in message -43 | urlpatterns_params_bad = [ - - path("bad/", views.bad_view), # DJ014 -44 + path("bad//", views.bad_view), # DJ014 -45 | path("", views.bad_view), # DJ014 -46 | ] - -DJ014 [*] URL route `` is missing a trailing slash - --> DJ014.py:45:10 - | -43 | urlpatterns_params_bad = [ -44 | path("bad/", views.bad_view), # DJ014 -45 | path("", views.bad_view), # DJ014 - | ^^^^^^^^^^^^^ -46 | ] - | -help: Add trailing slash -42 | # Error - missing trail slash and argument should stay in message -43 | urlpatterns_params_bad = [ -44 | path("bad/", views.bad_view), # DJ014 - - path("", views.bad_view), # DJ014 -45 + path("/", views.bad_view), # DJ014 -46 | ] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap deleted file mode 100644 index 154f6d4f3d..0000000000 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ014_custom_paths.py.snap +++ /dev/null @@ -1,81 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_django/mod.rs ---- -DJ014 [*] URL route `help` is missing a trailing slash - --> DJ014_custom_paths.py:6:12 - | -4 | # Test that custom path functions are also checked -5 | urlpatterns_custom = [ -6 | mypath("help", views.help_view), # DJ014 - | ^^^^^^ -7 | mypath("about", views.about_view), # DJ014 -8 | ] - | -help: Add trailing slash -3 | -4 | # Test that custom path functions are also checked -5 | urlpatterns_custom = [ - - mypath("help", views.help_view), # DJ014 -6 + mypath("help/", views.help_view), # DJ014 -7 | mypath("about", views.about_view), # DJ014 -8 | ] -9 | - -DJ014 [*] URL route `about` is missing a trailing slash - --> DJ014_custom_paths.py:7:12 - | -5 | urlpatterns_custom = [ -6 | mypath("help", views.help_view), # DJ014 -7 | mypath("about", views.about_view), # DJ014 - | ^^^^^^^ -8 | ] - | -help: Add trailing slash -4 | # Test that custom path functions are also checked -5 | urlpatterns_custom = [ -6 | mypath("help", views.help_view), # DJ014 - - mypath("about", views.about_view), # DJ014 -7 + mypath("about/", views.about_view), # DJ014 -8 | ] -9 | -10 | # OK - custom path with trailing slash - -DJ014 [*] URL route `api/users` is missing a trailing slash - --> DJ014_custom_paths.py:18:12 - | -16 | # Test multiple violations in same list -17 | urlpatterns_multiple = [ -18 | mypath("api/users", views.users_view), # DJ014 - | ^^^^^^^^^^^ -19 | mypath("api/posts", views.posts_view), # DJ014 -20 | mypath("api/comments/", views.comments_view), # OK - | -help: Add trailing slash -15 | -16 | # Test multiple violations in same list -17 | urlpatterns_multiple = [ - - mypath("api/users", views.users_view), # DJ014 -18 + mypath("api/users/", views.users_view), # DJ014 -19 | mypath("api/posts", views.posts_view), # DJ014 -20 | mypath("api/comments/", views.comments_view), # OK -21 | ] - -DJ014 [*] URL route `api/posts` is missing a trailing slash - --> DJ014_custom_paths.py:19:12 - | -17 | urlpatterns_multiple = [ -18 | mypath("api/users", views.users_view), # DJ014 -19 | mypath("api/posts", views.posts_view), # DJ014 - | ^^^^^^^^^^^ -20 | mypath("api/comments/", views.comments_view), # OK -21 | ] - | -help: Add trailing slash -16 | # Test multiple violations in same list -17 | urlpatterns_multiple = [ -18 | mypath("api/users", views.users_view), # DJ014 - - mypath("api/posts", views.posts_view), # DJ014 -19 + mypath("api/posts/", views.posts_view), # DJ014 -20 | mypath("api/comments/", views.comments_view), # OK -21 | ] -22 | diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap deleted file mode 100644 index da9ec8f894..0000000000 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_DJ015.py.snap +++ /dev/null @@ -1,255 +0,0 @@ ---- -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 | ] -49 | - -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 | ] -49 | -50 | # Error - leading trail slash and argument should stay in message - -DJ015 [*] URL route `/bad//` has an unnecessary leading slash - --> DJ015.py:52:10 - | -50 | # Error - leading trail slash and argument should stay in message -51 | urlpatterns_params_bad = [ -52 | path("/bad//", views.bad_view), # DJ015 - | ^^^^^^^^^^^^^^^^^^^ -53 | path("/", views.bad_view), # DJ015 -54 | ] - | -help: Remove leading slash -49 | -50 | # Error - leading trail slash and argument should stay in message -51 | urlpatterns_params_bad = [ - - path("/bad//", views.bad_view), # DJ015 -52 + path("bad//", views.bad_view), # DJ015 -53 | path("/", views.bad_view), # DJ015 -54 | ] - -DJ015 [*] URL route `/` has an unnecessary leading slash - --> DJ015.py:53:10 - | -51 | urlpatterns_params_bad = [ -52 | path("/bad//", views.bad_view), # DJ015 -53 | path("/", views.bad_view), # DJ015 - | ^^^^^^^^^^^^^^ -54 | ] - | -help: Remove leading slash -50 | # Error - leading trail slash and argument should stay in message -51 | urlpatterns_params_bad = [ -52 | path("/bad//", views.bad_view), # DJ015 - - path("/", views.bad_view), # DJ015 -53 + path("", views.bad_view), # DJ015 -54 | ] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap deleted file mode 100644 index 4081d1ae4d..0000000000 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ015_custom_paths.py.snap +++ /dev/null @@ -1,81 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_django/mod.rs ---- -DJ015 [*] URL route `/help/` has an unnecessary leading slash - --> DJ015_custom_paths.py:6:12 - | -4 | # Test that custom path functions are also checked for leading slashes -5 | urlpatterns_custom = [ -6 | mypath("/help/", views.help_view), # DJ015 - | ^^^^^^^^ -7 | mypath("/about/", views.about_view), # DJ015 -8 | ] - | -help: Remove leading slash -3 | -4 | # Test that custom path functions are also checked for leading slashes -5 | urlpatterns_custom = [ - - mypath("/help/", views.help_view), # DJ015 -6 + mypath("help/", views.help_view), # DJ015 -7 | mypath("/about/", views.about_view), # DJ015 -8 | ] -9 | - -DJ015 [*] URL route `/about/` has an unnecessary leading slash - --> DJ015_custom_paths.py:7:12 - | -5 | urlpatterns_custom = [ -6 | mypath("/help/", views.help_view), # DJ015 -7 | mypath("/about/", views.about_view), # DJ015 - | ^^^^^^^^^ -8 | ] - | -help: Remove leading slash -4 | # Test that custom path functions are also checked for leading slashes -5 | urlpatterns_custom = [ -6 | mypath("/help/", views.help_view), # DJ015 - - mypath("/about/", views.about_view), # DJ015 -7 + mypath("about/", views.about_view), # DJ015 -8 | ] -9 | -10 | # OK - custom path without leading slash - -DJ015 [*] URL route `/api/users/` has an unnecessary leading slash - --> DJ015_custom_paths.py:18:12 - | -16 | # Test multiple violations in same list -17 | urlpatterns_multiple = [ -18 | mypath("/api/users/", views.users_view), # DJ015 - | ^^^^^^^^^^^^^ -19 | mypath("/api/posts/", views.posts_view), # DJ015 -20 | mypath("api/comments/", views.comments_view), # OK - | -help: Remove leading slash -15 | -16 | # Test multiple violations in same list -17 | urlpatterns_multiple = [ - - mypath("/api/users/", views.users_view), # DJ015 -18 + mypath("api/users/", views.users_view), # DJ015 -19 | mypath("/api/posts/", views.posts_view), # DJ015 -20 | mypath("api/comments/", views.comments_view), # OK -21 | ] - -DJ015 [*] URL route `/api/posts/` has an unnecessary leading slash - --> DJ015_custom_paths.py:19:12 - | -17 | urlpatterns_multiple = [ -18 | mypath("/api/users/", views.users_view), # DJ015 -19 | mypath("/api/posts/", views.posts_view), # DJ015 - | ^^^^^^^^^^^^^ -20 | mypath("api/comments/", views.comments_view), # OK -21 | ] - | -help: Remove leading slash -16 | # Test multiple violations in same list -17 | urlpatterns_multiple = [ -18 | mypath("/api/users/", views.users_view), # DJ015 - - mypath("/api/posts/", views.posts_view), # DJ015 -19 + mypath("api/posts/", views.posts_view), # DJ015 -20 | mypath("api/comments/", views.comments_view), # OK -21 | ] -22 | diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ100_DJ100.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ100_DJ100.py.snap new file mode 100644 index 0000000000..6ef1ad29c8 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ100_DJ100.py.snap @@ -0,0 +1,176 @@ +--- +source: crates/ruff_linter/src/rules/flake8_django/mod.rs +--- +DJ100 [*] URL route `help` is missing a trailing slash + --> DJ100.py:6:10 + | +4 | # Errors - missing trailing slash +5 | urlpatterns = [ +6 | path("help", views.help_view), # DJ100 + | ^^^^^^ +7 | path("about", views.about_view), # DJ100 +8 | path("contact", views.contact_view), # DJ100 + | +help: Add trailing slash +3 | +4 | # Errors - missing trailing slash +5 | urlpatterns = [ + - path("help", views.help_view), # DJ100 +6 + path("help/", views.help_view), # DJ100 +7 | path("about", views.about_view), # DJ100 +8 | path("contact", views.contact_view), # DJ100 +9 | path("api/users", views.users_view), # DJ100 + +DJ100 [*] URL route `about` is missing a trailing slash + --> DJ100.py:7:10 + | +5 | urlpatterns = [ +6 | path("help", views.help_view), # DJ100 +7 | path("about", views.about_view), # DJ100 + | ^^^^^^^ +8 | path("contact", views.contact_view), # DJ100 +9 | path("api/users", views.users_view), # DJ100 + | +help: Add trailing slash +4 | # Errors - missing trailing slash +5 | urlpatterns = [ +6 | path("help", views.help_view), # DJ100 + - path("about", views.about_view), # DJ100 +7 + path("about/", views.about_view), # DJ100 +8 | path("contact", views.contact_view), # DJ100 +9 | path("api/users", views.users_view), # DJ100 +10 | path("blog/posts", views.posts_view), # DJ100 + +DJ100 [*] URL route `contact` is missing a trailing slash + --> DJ100.py:8:10 + | + 6 | path("help", views.help_view), # DJ100 + 7 | path("about", views.about_view), # DJ100 + 8 | path("contact", views.contact_view), # DJ100 + | ^^^^^^^^^ + 9 | path("api/users", views.users_view), # DJ100 +10 | path("blog/posts", views.posts_view), # DJ100 + | +help: Add trailing slash +5 | urlpatterns = [ +6 | path("help", views.help_view), # DJ100 +7 | path("about", views.about_view), # DJ100 + - path("contact", views.contact_view), # DJ100 +8 + path("contact/", views.contact_view), # DJ100 +9 | path("api/users", views.users_view), # DJ100 +10 | path("blog/posts", views.posts_view), # DJ100 +11 | ] + +DJ100 [*] URL route `api/users` is missing a trailing slash + --> DJ100.py:9:10 + | + 7 | path("about", views.about_view), # DJ100 + 8 | path("contact", views.contact_view), # DJ100 + 9 | path("api/users", views.users_view), # DJ100 + | ^^^^^^^^^^^ +10 | path("blog/posts", views.posts_view), # DJ100 +11 | ] + | +help: Add trailing slash +6 | path("help", views.help_view), # DJ100 +7 | path("about", views.about_view), # DJ100 +8 | path("contact", views.contact_view), # DJ100 + - path("api/users", views.users_view), # DJ100 +9 + path("api/users/", views.users_view), # DJ100 +10 | path("blog/posts", views.posts_view), # DJ100 +11 | ] +12 | + +DJ100 [*] URL route `blog/posts` is missing a trailing slash + --> DJ100.py:10:10 + | + 8 | path("contact", views.contact_view), # DJ100 + 9 | path("api/users", views.users_view), # DJ100 +10 | path("blog/posts", views.posts_view), # DJ100 + | ^^^^^^^^^^^^ +11 | ] + | +help: Add trailing slash +7 | path("about", views.about_view), # DJ100 +8 | path("contact", views.contact_view), # DJ100 +9 | path("api/users", views.users_view), # DJ100 + - path("blog/posts", views.posts_view), # DJ100 +10 + path("blog/posts/", views.posts_view), # DJ100 +11 | ] +12 | +13 | # OK - has trailing slash + +DJ100 [*] URL route `bad` is missing a trailing slash + --> DJ100.py:37:10 + | +35 | urlpatterns_mixed = [ +36 | path("good/", views.good_view), +37 | path("bad", views.bad_view), # DJ100 + | ^^^^^ +38 | path("also-good/", views.also_good_view), +39 | path("also-bad", views.also_bad_view), # DJ100 + | +help: Add trailing slash +34 | # Mixed cases +35 | urlpatterns_mixed = [ +36 | path("good/", views.good_view), + - path("bad", views.bad_view), # DJ100 +37 + path("bad/", views.bad_view), # DJ100 +38 | path("also-good/", views.also_good_view), +39 | path("also-bad", views.also_bad_view), # DJ100 +40 | ] + +DJ100 [*] URL route `also-bad` is missing a trailing slash + --> DJ100.py:39:10 + | +37 | path("bad", views.bad_view), # DJ100 +38 | path("also-good/", views.also_good_view), +39 | path("also-bad", views.also_bad_view), # DJ100 + | ^^^^^^^^^^ +40 | ] + | +help: Add trailing slash +36 | path("good/", views.good_view), +37 | path("bad", views.bad_view), # DJ100 +38 | path("also-good/", views.also_good_view), + - path("also-bad", views.also_bad_view), # DJ100 +39 + path("also-bad/", views.also_bad_view), # DJ100 +40 | ] +41 | +42 | # Error - missing trail slash and argument should stay in message + +DJ100 [*] URL route `bad/` is missing a trailing slash + --> DJ100.py:44:10 + | +42 | # Error - missing trail slash and argument should stay in message +43 | urlpatterns_params_bad = [ +44 | path("bad/", views.bad_view), # DJ100 + | ^^^^^^^^^^^^^^^^^ +45 | path("", views.bad_view), # DJ100 +46 | ] + | +help: Add trailing slash +41 | +42 | # Error - missing trail slash and argument should stay in message +43 | urlpatterns_params_bad = [ + - path("bad/", views.bad_view), # DJ100 +44 + path("bad//", views.bad_view), # DJ100 +45 | path("", views.bad_view), # DJ100 +46 | ] + +DJ100 [*] URL route `` is missing a trailing slash + --> DJ100.py:45:10 + | +43 | urlpatterns_params_bad = [ +44 | path("bad/", views.bad_view), # DJ100 +45 | path("", views.bad_view), # DJ100 + | ^^^^^^^^^^^^^ +46 | ] + | +help: Add trailing slash +42 | # Error - missing trail slash and argument should stay in message +43 | urlpatterns_params_bad = [ +44 | path("bad/", views.bad_view), # DJ100 + - path("", views.bad_view), # DJ100 +45 + path("/", views.bad_view), # DJ100 +46 | ] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ100_custom_paths.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ100_custom_paths.py.snap new file mode 100644 index 0000000000..e8d35792b4 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ100_custom_paths.py.snap @@ -0,0 +1,81 @@ +--- +source: crates/ruff_linter/src/rules/flake8_django/mod.rs +--- +DJ100 [*] URL route `help` is missing a trailing slash + --> DJ100_custom_paths.py:6:12 + | +4 | # Test that custom path functions are also checked +5 | urlpatterns_custom = [ +6 | mypath("help", views.help_view), # DJ100 + | ^^^^^^ +7 | mypath("about", views.about_view), # DJ100 +8 | ] + | +help: Add trailing slash +3 | +4 | # Test that custom path functions are also checked +5 | urlpatterns_custom = [ + - mypath("help", views.help_view), # DJ100 +6 + mypath("help/", views.help_view), # DJ100 +7 | mypath("about", views.about_view), # DJ100 +8 | ] +9 | + +DJ100 [*] URL route `about` is missing a trailing slash + --> DJ100_custom_paths.py:7:12 + | +5 | urlpatterns_custom = [ +6 | mypath("help", views.help_view), # DJ100 +7 | mypath("about", views.about_view), # DJ100 + | ^^^^^^^ +8 | ] + | +help: Add trailing slash +4 | # Test that custom path functions are also checked +5 | urlpatterns_custom = [ +6 | mypath("help", views.help_view), # DJ100 + - mypath("about", views.about_view), # DJ100 +7 + mypath("about/", views.about_view), # DJ100 +8 | ] +9 | +10 | # OK - custom path with trailing slash + +DJ100 [*] URL route `api/users` is missing a trailing slash + --> DJ100_custom_paths.py:18:12 + | +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ +18 | mypath("api/users", views.users_view), # DJ100 + | ^^^^^^^^^^^ +19 | mypath("api/posts", views.posts_view), # DJ100 +20 | mypath("api/comments/", views.comments_view), # OK + | +help: Add trailing slash +15 | +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ + - mypath("api/users", views.users_view), # DJ100 +18 + mypath("api/users/", views.users_view), # DJ100 +19 | mypath("api/posts", views.posts_view), # DJ100 +20 | mypath("api/comments/", views.comments_view), # OK +21 | ] + +DJ100 [*] URL route `api/posts` is missing a trailing slash + --> DJ100_custom_paths.py:19:12 + | +17 | urlpatterns_multiple = [ +18 | mypath("api/users", views.users_view), # DJ100 +19 | mypath("api/posts", views.posts_view), # DJ100 + | ^^^^^^^^^^^ +20 | mypath("api/comments/", views.comments_view), # OK +21 | ] + | +help: Add trailing slash +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ +18 | mypath("api/users", views.users_view), # DJ100 + - mypath("api/posts", views.posts_view), # DJ100 +19 + mypath("api/posts/", views.posts_view), # DJ100 +20 | mypath("api/comments/", views.comments_view), # OK +21 | ] +22 | diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ101_DJ101.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ101_DJ101.py.snap new file mode 100644 index 0000000000..681094e3dd --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ101_DJ101.py.snap @@ -0,0 +1,255 @@ +--- +source: crates/ruff_linter/src/rules/flake8_django/mod.rs +--- +DJ101 [*] URL route `/help/` has an unnecessary leading slash + --> DJ101.py:6:10 + | +4 | # Errors - leading slash +5 | urlpatterns = [ +6 | path("/help/", views.help_view), # DJ101 + | ^^^^^^^^ +7 | path("/about/", views.about_view), # DJ101 +8 | path("/contact/", views.contact_view), # DJ101 + | +help: Remove leading slash +3 | +4 | # Errors - leading slash +5 | urlpatterns = [ + - path("/help/", views.help_view), # DJ101 +6 + path("help/", views.help_view), # DJ101 +7 | path("/about/", views.about_view), # DJ101 +8 | path("/contact/", views.contact_view), # DJ101 +9 | path("/api/users/", views.users_view), # DJ101 + +DJ101 [*] URL route `/about/` has an unnecessary leading slash + --> DJ101.py:7:10 + | +5 | urlpatterns = [ +6 | path("/help/", views.help_view), # DJ101 +7 | path("/about/", views.about_view), # DJ101 + | ^^^^^^^^^ +8 | path("/contact/", views.contact_view), # DJ101 +9 | path("/api/users/", views.users_view), # DJ101 + | +help: Remove leading slash +4 | # Errors - leading slash +5 | urlpatterns = [ +6 | path("/help/", views.help_view), # DJ101 + - path("/about/", views.about_view), # DJ101 +7 + path("about/", views.about_view), # DJ101 +8 | path("/contact/", views.contact_view), # DJ101 +9 | path("/api/users/", views.users_view), # DJ101 +10 | path("/blog/posts/", views.posts_view), # DJ101 + +DJ101 [*] URL route `/contact/` has an unnecessary leading slash + --> DJ101.py:8:10 + | + 6 | path("/help/", views.help_view), # DJ101 + 7 | path("/about/", views.about_view), # DJ101 + 8 | path("/contact/", views.contact_view), # DJ101 + | ^^^^^^^^^^^ + 9 | path("/api/users/", views.users_view), # DJ101 +10 | path("/blog/posts/", views.posts_view), # DJ101 + | +help: Remove leading slash +5 | urlpatterns = [ +6 | path("/help/", views.help_view), # DJ101 +7 | path("/about/", views.about_view), # DJ101 + - path("/contact/", views.contact_view), # DJ101 +8 + path("contact/", views.contact_view), # DJ101 +9 | path("/api/users/", views.users_view), # DJ101 +10 | path("/blog/posts/", views.posts_view), # DJ101 +11 | ] + +DJ101 [*] URL route `/api/users/` has an unnecessary leading slash + --> DJ101.py:9:10 + | + 7 | path("/about/", views.about_view), # DJ101 + 8 | path("/contact/", views.contact_view), # DJ101 + 9 | path("/api/users/", views.users_view), # DJ101 + | ^^^^^^^^^^^^^ +10 | path("/blog/posts/", views.posts_view), # DJ101 +11 | ] + | +help: Remove leading slash +6 | path("/help/", views.help_view), # DJ101 +7 | path("/about/", views.about_view), # DJ101 +8 | path("/contact/", views.contact_view), # DJ101 + - path("/api/users/", views.users_view), # DJ101 +9 + path("api/users/", views.users_view), # DJ101 +10 | path("/blog/posts/", views.posts_view), # DJ101 +11 | ] +12 | + +DJ101 [*] URL route `/blog/posts/` has an unnecessary leading slash + --> DJ101.py:10:10 + | + 8 | path("/contact/", views.contact_view), # DJ101 + 9 | path("/api/users/", views.users_view), # DJ101 +10 | path("/blog/posts/", views.posts_view), # DJ101 + | ^^^^^^^^^^^^^^ +11 | ] + | +help: Remove leading slash +7 | path("/about/", views.about_view), # DJ101 +8 | path("/contact/", views.contact_view), # DJ101 +9 | path("/api/users/", views.users_view), # DJ101 + - path("/blog/posts/", views.posts_view), # DJ101 +10 + path("blog/posts/", views.posts_view), # DJ101 +11 | ] +12 | +13 | # OK - no leading slash + +DJ101 [*] URL route `/bad/` has an unnecessary leading slash + --> DJ101.py:37:10 + | +35 | urlpatterns_mixed = [ +36 | path("good/", views.good_view), +37 | path("/bad/", views.bad_view), # DJ101 + | ^^^^^^^ +38 | path("also-good/", views.also_good_view), +39 | path("/also-bad/", views.also_bad_view), # DJ101 + | +help: Remove leading slash +34 | # Mixed cases +35 | urlpatterns_mixed = [ +36 | path("good/", views.good_view), + - path("/bad/", views.bad_view), # DJ101 +37 + path("bad/", views.bad_view), # DJ101 +38 | path("also-good/", views.also_good_view), +39 | path("/also-bad/", views.also_bad_view), # DJ101 +40 | ] + +DJ101 [*] URL route `/also-bad/` has an unnecessary leading slash + --> DJ101.py:39:10 + | +37 | path("/bad/", views.bad_view), # DJ101 +38 | path("also-good/", views.also_good_view), +39 | path("/also-bad/", views.also_bad_view), # DJ101 + | ^^^^^^^^^^^^ +40 | ] + | +help: Remove leading slash +36 | path("good/", views.good_view), +37 | path("/bad/", views.bad_view), # DJ101 +38 | path("also-good/", views.also_good_view), + - path("/also-bad/", views.also_bad_view), # DJ101 +39 + path("also-bad/", views.also_bad_view), # DJ101 +40 | ] +41 | +42 | # Edge cases with different quote styles + +DJ101 [*] URL route `/single-quote/` has an unnecessary leading slash + --> DJ101.py:44:10 + | +42 | # Edge cases with different quote styles +43 | urlpatterns_quotes = [ +44 | path('/single-quote/', views.single_quote_view), # DJ101 + | ^^^^^^^^^^^^^^^^ +45 | path("/double-quote/", views.double_quote_view), # DJ101 +46 | path('''/triple-single/''', views.triple_single_view), # DJ101 + | +help: Remove leading slash +41 | +42 | # Edge cases with different quote styles +43 | urlpatterns_quotes = [ + - path('/single-quote/', views.single_quote_view), # DJ101 +44 + path('single-quote/', views.single_quote_view), # DJ101 +45 | path("/double-quote/", views.double_quote_view), # DJ101 +46 | path('''/triple-single/''', views.triple_single_view), # DJ101 +47 | path("""/triple-double/""", views.triple_double_view), # DJ101 + +DJ101 [*] URL route `/double-quote/` has an unnecessary leading slash + --> DJ101.py:45:10 + | +43 | urlpatterns_quotes = [ +44 | path('/single-quote/', views.single_quote_view), # DJ101 +45 | path("/double-quote/", views.double_quote_view), # DJ101 + | ^^^^^^^^^^^^^^^^ +46 | path('''/triple-single/''', views.triple_single_view), # DJ101 +47 | path("""/triple-double/""", views.triple_double_view), # DJ101 + | +help: Remove leading slash +42 | # Edge cases with different quote styles +43 | urlpatterns_quotes = [ +44 | path('/single-quote/', views.single_quote_view), # DJ101 + - path("/double-quote/", views.double_quote_view), # DJ101 +45 + path("double-quote/", views.double_quote_view), # DJ101 +46 | path('''/triple-single/''', views.triple_single_view), # DJ101 +47 | path("""/triple-double/""", views.triple_double_view), # DJ101 +48 | ] + +DJ101 [*] URL route `/triple-single/` has an unnecessary leading slash + --> DJ101.py:46:10 + | +44 | path('/single-quote/', views.single_quote_view), # DJ101 +45 | path("/double-quote/", views.double_quote_view), # DJ101 +46 | path('''/triple-single/''', views.triple_single_view), # DJ101 + | ^^^^^^^^^^^^^^^^^^^^^ +47 | path("""/triple-double/""", views.triple_double_view), # DJ101 +48 | ] + | +help: Remove leading slash +43 | urlpatterns_quotes = [ +44 | path('/single-quote/', views.single_quote_view), # DJ101 +45 | path("/double-quote/", views.double_quote_view), # DJ101 + - path('''/triple-single/''', views.triple_single_view), # DJ101 +46 + path('''triple-single/''', views.triple_single_view), # DJ101 +47 | path("""/triple-double/""", views.triple_double_view), # DJ101 +48 | ] +49 | + +DJ101 [*] URL route `/triple-double/` has an unnecessary leading slash + --> DJ101.py:47:10 + | +45 | path("/double-quote/", views.double_quote_view), # DJ101 +46 | path('''/triple-single/''', views.triple_single_view), # DJ101 +47 | path("""/triple-double/""", views.triple_double_view), # DJ101 + | ^^^^^^^^^^^^^^^^^^^^^ +48 | ] + | +help: Remove leading slash +44 | path('/single-quote/', views.single_quote_view), # DJ101 +45 | path("/double-quote/", views.double_quote_view), # DJ101 +46 | path('''/triple-single/''', views.triple_single_view), # DJ101 + - path("""/triple-double/""", views.triple_double_view), # DJ101 +47 + path("""triple-double/""", views.triple_double_view), # DJ101 +48 | ] +49 | +50 | # Error - leading trail slash and argument should stay in message + +DJ101 [*] URL route `/bad//` has an unnecessary leading slash + --> DJ101.py:52:10 + | +50 | # Error - leading trail slash and argument should stay in message +51 | urlpatterns_params_bad = [ +52 | path("/bad//", views.bad_view), # DJ101 + | ^^^^^^^^^^^^^^^^^^^ +53 | path("/", views.bad_view), # DJ101 +54 | ] + | +help: Remove leading slash +49 | +50 | # Error - leading trail slash and argument should stay in message +51 | urlpatterns_params_bad = [ + - path("/bad//", views.bad_view), # DJ101 +52 + path("bad//", views.bad_view), # DJ101 +53 | path("/", views.bad_view), # DJ101 +54 | ] + +DJ101 [*] URL route `/` has an unnecessary leading slash + --> DJ101.py:53:10 + | +51 | urlpatterns_params_bad = [ +52 | path("/bad//", views.bad_view), # DJ101 +53 | path("/", views.bad_view), # DJ101 + | ^^^^^^^^^^^^^^ +54 | ] + | +help: Remove leading slash +50 | # Error - leading trail slash and argument should stay in message +51 | urlpatterns_params_bad = [ +52 | path("/bad//", views.bad_view), # DJ101 + - path("/", views.bad_view), # DJ101 +53 + path("", views.bad_view), # DJ101 +54 | ] diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ101_custom_paths.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ101_custom_paths.py.snap new file mode 100644 index 0000000000..18f1f30a29 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ101_custom_paths.py.snap @@ -0,0 +1,81 @@ +--- +source: crates/ruff_linter/src/rules/flake8_django/mod.rs +--- +DJ101 [*] URL route `/help/` has an unnecessary leading slash + --> DJ101_custom_paths.py:6:12 + | +4 | # Test that custom path functions are also checked for leading slashes +5 | urlpatterns_custom = [ +6 | mypath("/help/", views.help_view), # DJ101 + | ^^^^^^^^ +7 | mypath("/about/", views.about_view), # DJ101 +8 | ] + | +help: Remove leading slash +3 | +4 | # Test that custom path functions are also checked for leading slashes +5 | urlpatterns_custom = [ + - mypath("/help/", views.help_view), # DJ101 +6 + mypath("help/", views.help_view), # DJ101 +7 | mypath("/about/", views.about_view), # DJ101 +8 | ] +9 | + +DJ101 [*] URL route `/about/` has an unnecessary leading slash + --> DJ101_custom_paths.py:7:12 + | +5 | urlpatterns_custom = [ +6 | mypath("/help/", views.help_view), # DJ101 +7 | mypath("/about/", views.about_view), # DJ101 + | ^^^^^^^^^ +8 | ] + | +help: Remove leading slash +4 | # Test that custom path functions are also checked for leading slashes +5 | urlpatterns_custom = [ +6 | mypath("/help/", views.help_view), # DJ101 + - mypath("/about/", views.about_view), # DJ101 +7 + mypath("about/", views.about_view), # DJ101 +8 | ] +9 | +10 | # OK - custom path without leading slash + +DJ101 [*] URL route `/api/users/` has an unnecessary leading slash + --> DJ101_custom_paths.py:18:12 + | +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ +18 | mypath("/api/users/", views.users_view), # DJ101 + | ^^^^^^^^^^^^^ +19 | mypath("/api/posts/", views.posts_view), # DJ101 +20 | mypath("api/comments/", views.comments_view), # OK + | +help: Remove leading slash +15 | +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ + - mypath("/api/users/", views.users_view), # DJ101 +18 + mypath("api/users/", views.users_view), # DJ101 +19 | mypath("/api/posts/", views.posts_view), # DJ101 +20 | mypath("api/comments/", views.comments_view), # OK +21 | ] + +DJ101 [*] URL route `/api/posts/` has an unnecessary leading slash + --> DJ101_custom_paths.py:19:12 + | +17 | urlpatterns_multiple = [ +18 | mypath("/api/users/", views.users_view), # DJ101 +19 | mypath("/api/posts/", views.posts_view), # DJ101 + | ^^^^^^^^^^^^^ +20 | mypath("api/comments/", views.comments_view), # OK +21 | ] + | +help: Remove leading slash +16 | # Test multiple violations in same list +17 | urlpatterns_multiple = [ +18 | mypath("/api/users/", views.users_view), # DJ101 + - mypath("/api/posts/", views.posts_view), # DJ101 +19 + mypath("api/posts/", views.posts_view), # DJ101 +20 | mypath("api/comments/", views.comments_view), # OK +21 | ] +22 | diff --git a/ruff.schema.json b/ruff.schema.json index 06cee2c048..48ca5ed416 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3197,8 +3197,10 @@ "DJ01", "DJ012", "DJ013", - "DJ014", - "DJ015", + "DJ1", + "DJ10", + "DJ100", + "DJ101", "DOC", "DOC1", "DOC10", From 75d5b7e1dc2ad947017c6933c5afdeff201b0600 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Fri, 31 Oct 2025 17:03:27 +0100 Subject: [PATCH 17/17] bump preview_since version --- .../rules/flake8_django/rules/url_path_with_leading_slash.rs | 2 +- .../flake8_django/rules/url_path_without_trailing_slash.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs index 91cde9f837..7f310df11f 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_with_leading_slash.rs @@ -41,7 +41,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Django documentation: URL dispatcher](https://docs.djangoproject.com/en/stable/topics/http/urls/) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.14.1")] +#[violation_metadata(preview_since = "v0.14.4")] pub(crate) struct DjangoURLPathWithLeadingSlash { url_pattern: String, } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs index 07faaf47b5..5ba6f57dd5 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/url_path_without_trailing_slash.rs @@ -41,7 +41,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Django documentation: URL dispatcher](https://docs.djangoproject.com/en/stable/topics/http/urls/) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.14.1")] +#[violation_metadata(preview_since = "v0.14.4")] pub(crate) struct DjangoURLPathWithoutTrailingSlash { url_pattern: String, }