From 1047e895c75eca28bf6dc8fb13d6a5c5ffcbc666 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Sun, 5 Oct 2025 17:00:25 +0200 Subject: [PATCH] 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 | ]