mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 19:41:34 +00:00
Merge 75d5b7e1dc into 665f68036c
This commit is contained in:
commit
cf90fcd39c
29 changed files with 1185 additions and 3 deletions
|
|
@ -179,6 +179,7 @@ linter.flake8_self.ignore_names = [
|
||||||
_name_,
|
_name_,
|
||||||
_value_,
|
_value_,
|
||||||
]
|
]
|
||||||
|
linter.flake8_django.additional_path_functions = []
|
||||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||||
linter.flake8_tidy_imports.banned_api = {}
|
linter.flake8_tidy_imports.banned_api = {}
|
||||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ linter.flake8_self.ignore_names = [
|
||||||
_name_,
|
_name_,
|
||||||
_value_,
|
_value_,
|
||||||
]
|
]
|
||||||
|
linter.flake8_django.additional_path_functions = []
|
||||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||||
linter.flake8_tidy_imports.banned_api = {}
|
linter.flake8_tidy_imports.banned_api = {}
|
||||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,7 @@ linter.flake8_self.ignore_names = [
|
||||||
_name_,
|
_name_,
|
||||||
_value_,
|
_value_,
|
||||||
]
|
]
|
||||||
|
linter.flake8_django.additional_path_functions = []
|
||||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||||
linter.flake8_tidy_imports.banned_api = {}
|
linter.flake8_tidy_imports.banned_api = {}
|
||||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,7 @@ linter.flake8_self.ignore_names = [
|
||||||
_name_,
|
_name_,
|
||||||
_value_,
|
_value_,
|
||||||
]
|
]
|
||||||
|
linter.flake8_django.additional_path_functions = []
|
||||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||||
linter.flake8_tidy_imports.banned_api = {}
|
linter.flake8_tidy_imports.banned_api = {}
|
||||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,7 @@ linter.flake8_self.ignore_names = [
|
||||||
_name_,
|
_name_,
|
||||||
_value_,
|
_value_,
|
||||||
]
|
]
|
||||||
|
linter.flake8_django.additional_path_functions = []
|
||||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||||
linter.flake8_tidy_imports.banned_api = {}
|
linter.flake8_tidy_imports.banned_api = {}
|
||||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,7 @@ linter.flake8_self.ignore_names = [
|
||||||
_name_,
|
_name_,
|
||||||
_value_,
|
_value_,
|
||||||
]
|
]
|
||||||
|
linter.flake8_django.additional_path_functions = []
|
||||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||||
linter.flake8_tidy_imports.banned_api = {}
|
linter.flake8_tidy_imports.banned_api = {}
|
||||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ linter.flake8_self.ignore_names = [
|
||||||
_name_,
|
_name_,
|
||||||
_value_,
|
_value_,
|
||||||
]
|
]
|
||||||
|
linter.flake8_django.additional_path_functions = []
|
||||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||||
linter.flake8_tidy_imports.banned_api = {}
|
linter.flake8_tidy_imports.banned_api = {}
|
||||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ linter.flake8_self.ignore_names = [
|
||||||
_name_,
|
_name_,
|
||||||
_value_,
|
_value_,
|
||||||
]
|
]
|
||||||
|
linter.flake8_django.additional_path_functions = []
|
||||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||||
linter.flake8_tidy_imports.banned_api = {}
|
linter.flake8_tidy_imports.banned_api = {}
|
||||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ linter.flake8_self.ignore_names = [
|
||||||
_name_,
|
_name_,
|
||||||
_value_,
|
_value_,
|
||||||
]
|
]
|
||||||
|
linter.flake8_django.additional_path_functions = []
|
||||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||||
linter.flake8_tidy_imports.banned_api = {}
|
linter.flake8_tidy_imports.banned_api = {}
|
||||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,7 @@ linter.flake8_self.ignore_names = [
|
||||||
_name_,
|
_name_,
|
||||||
_value_,
|
_value_,
|
||||||
]
|
]
|
||||||
|
linter.flake8_django.additional_path_functions = []
|
||||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||||
linter.flake8_tidy_imports.banned_api = {}
|
linter.flake8_tidy_imports.banned_api = {}
|
||||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||||
|
|
|
||||||
46
crates/ruff_linter/resources/test/fixtures/flake8_django/DJ100.py
vendored
Normal file
46
crates/ruff_linter/resources/test/fixtures/flake8_django/DJ100.py
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
# Errors - missing trailing slash
|
||||||
|
urlpatterns = [
|
||||||
|
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
|
||||||
|
urlpatterns_ok = [
|
||||||
|
path("help/", views.help_view),
|
||||||
|
path("about/", views.about_view),
|
||||||
|
path("contact/", views.contact_view),
|
||||||
|
path("api/users/", views.users_view),
|
||||||
|
path("blog/posts/", views.posts_view),
|
||||||
|
]
|
||||||
|
|
||||||
|
# OK - just root path
|
||||||
|
urlpatterns_root = [
|
||||||
|
path("/", views.index_view),
|
||||||
|
path("", views.home_view),
|
||||||
|
]
|
||||||
|
|
||||||
|
# OK - with path parameters
|
||||||
|
urlpatterns_params = [
|
||||||
|
path("users/<int:id>/", views.user_detail),
|
||||||
|
path("posts/<slug:slug>/", views.post_detail),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mixed cases
|
||||||
|
urlpatterns_mixed = [
|
||||||
|
path("good/", views.good_view),
|
||||||
|
path("bad", views.bad_view), # DJ100
|
||||||
|
path("also-good/", views.also_good_view),
|
||||||
|
path("also-bad", views.also_bad_view), # DJ100
|
||||||
|
]
|
||||||
|
|
||||||
|
# Error - missing trail slash and argument should stay in message
|
||||||
|
urlpatterns_params_bad = [
|
||||||
|
path("bad/<slug:slug>", views.bad_view), # DJ100
|
||||||
|
path("<slug:slug>", views.bad_view), # DJ100
|
||||||
|
]
|
||||||
27
crates/ruff_linter/resources/test/fixtures/flake8_django/DJ100_custom_paths.py
vendored
Normal file
27
crates/ruff_linter/resources/test/fixtures/flake8_django/DJ100_custom_paths.py
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from mytools import path as mypath
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
# Test that custom path functions are also checked
|
||||||
|
urlpatterns_custom = [
|
||||||
|
mypath("help", views.help_view), # DJ100
|
||||||
|
mypath("about", views.about_view), # DJ100
|
||||||
|
]
|
||||||
|
|
||||||
|
# OK - custom path with trailing slash
|
||||||
|
urlpatterns_custom_ok = [
|
||||||
|
mypath("help/", views.help_view),
|
||||||
|
mypath("about/", views.about_view),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test multiple violations in same list
|
||||||
|
urlpatterns_multiple = [
|
||||||
|
mypath("api/users", views.users_view), # DJ100
|
||||||
|
mypath("api/posts", views.posts_view), # DJ100
|
||||||
|
mypath("api/comments/", views.comments_view), # OK
|
||||||
|
]
|
||||||
|
|
||||||
|
# OK - root path and empty string
|
||||||
|
urlpatterns_edge_cases = [
|
||||||
|
mypath("/", views.root_view), # OK - root path
|
||||||
|
mypath("", views.empty_view), # OK - empty string
|
||||||
|
]
|
||||||
54
crates/ruff_linter/resources/test/fixtures/flake8_django/DJ101.py
vendored
Normal file
54
crates/ruff_linter/resources/test/fixtures/flake8_django/DJ101.py
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
# Errors - leading slash
|
||||||
|
urlpatterns = [
|
||||||
|
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
|
||||||
|
urlpatterns_ok = [
|
||||||
|
path("help/", views.help_view),
|
||||||
|
path("about/", views.about_view),
|
||||||
|
path("contact/", views.contact_view),
|
||||||
|
path("api/users/", views.users_view),
|
||||||
|
path("blog/posts/", views.posts_view),
|
||||||
|
]
|
||||||
|
|
||||||
|
# OK - just root path
|
||||||
|
urlpatterns_root = [
|
||||||
|
path("/", views.index_view),
|
||||||
|
path("", views.home_view),
|
||||||
|
]
|
||||||
|
|
||||||
|
# OK - with path parameters
|
||||||
|
urlpatterns_params = [
|
||||||
|
path("users/<int:id>/", views.user_detail),
|
||||||
|
path("posts/<slug:slug>/", views.post_detail),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mixed cases
|
||||||
|
urlpatterns_mixed = [
|
||||||
|
path("good/", views.good_view),
|
||||||
|
path("/bad/", views.bad_view), # DJ101
|
||||||
|
path("also-good/", views.also_good_view),
|
||||||
|
path("/also-bad/", views.also_bad_view), # DJ101
|
||||||
|
]
|
||||||
|
|
||||||
|
# Edge cases with different quote styles
|
||||||
|
urlpatterns_quotes = [
|
||||||
|
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/<slug:slug>/", views.bad_view), # DJ101
|
||||||
|
path("/<slug:slug>", views.bad_view), # DJ101
|
||||||
|
]
|
||||||
29
crates/ruff_linter/resources/test/fixtures/flake8_django/DJ101_custom_paths.py
vendored
Normal file
29
crates/ruff_linter/resources/test/fixtures/flake8_django/DJ101_custom_paths.py
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
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), # DJ101
|
||||||
|
mypath("/about/", views.about_view), # DJ101
|
||||||
|
]
|
||||||
|
|
||||||
|
# OK - custom path without leading slash
|
||||||
|
urlpatterns_custom_ok = [
|
||||||
|
mypath("help/", views.help_view),
|
||||||
|
mypath("about/", views.about_view),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test multiple violations in same list
|
||||||
|
urlpatterns_multiple = [
|
||||||
|
mypath("/api/users/", views.users_view), # DJ101
|
||||||
|
mypath("/api/posts/", views.posts_view), # DJ101
|
||||||
|
mypath("api/comments/", views.comments_view), # OK
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# OK - root path and empty string
|
||||||
|
urlpatterns_edge_cases = [
|
||||||
|
|
||||||
|
mypath("/", views.root_view), # OK - root path
|
||||||
|
mypath("", views.empty_view), # OK - empty string
|
||||||
|
]
|
||||||
|
|
@ -1181,6 +1181,12 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||||
if checker.is_rule_enabled(Rule::DjangoLocalsInRenderFunction) {
|
if checker.is_rule_enabled(Rule::DjangoLocalsInRenderFunction) {
|
||||||
flake8_django::rules::locals_in_render_function(checker, call);
|
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::DjangoURLPathWithLeadingSlash) {
|
||||||
|
flake8_django::rules::url_path_with_leading_slash(checker, call);
|
||||||
|
}
|
||||||
if checker.is_rule_enabled(Rule::UnsupportedMethodCallOnAll) {
|
if checker.is_rule_enabled(Rule::UnsupportedMethodCallOnAll) {
|
||||||
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
|
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1100,6 +1100,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||||
(Flake8Django, "008") => rules::flake8_django::rules::DjangoModelWithoutDunderStr,
|
(Flake8Django, "008") => rules::flake8_django::rules::DjangoModelWithoutDunderStr,
|
||||||
(Flake8Django, "012") => rules::flake8_django::rules::DjangoUnorderedBodyContentInModel,
|
(Flake8Django, "012") => rules::flake8_django::rules::DjangoUnorderedBodyContentInModel,
|
||||||
(Flake8Django, "013") => rules::flake8_django::rules::DjangoNonLeadingReceiverDecorator,
|
(Flake8Django, "013") => rules::flake8_django::rules::DjangoNonLeadingReceiverDecorator,
|
||||||
|
(Flake8Django, "100") => rules::flake8_django::rules::DjangoURLPathWithoutTrailingSlash,
|
||||||
|
(Flake8Django, "101") => rules::flake8_django::rules::DjangoURLPathWithLeadingSlash,
|
||||||
|
|
||||||
// flynt
|
// flynt
|
||||||
// Reserved: (Flynt, "001") => Rule: :StringConcatenationToFString,
|
// Reserved: (Flynt, "001") => Rule: :StringConcatenationToFString,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! Rules from [django-flake8](https://pypi.org/project/flake8-django/)
|
//! Rules from [django-flake8](https://pypi.org/project/flake8-django/)
|
||||||
mod helpers;
|
mod helpers;
|
||||||
pub(crate) mod rules;
|
pub(crate) mod rules;
|
||||||
|
pub mod settings;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
@ -18,6 +19,8 @@ mod tests {
|
||||||
#[test_case(Rule::DjangoExcludeWithModelForm, Path::new("DJ006.py"))]
|
#[test_case(Rule::DjangoExcludeWithModelForm, Path::new("DJ006.py"))]
|
||||||
#[test_case(Rule::DjangoAllWithModelForm, Path::new("DJ007.py"))]
|
#[test_case(Rule::DjangoAllWithModelForm, Path::new("DJ007.py"))]
|
||||||
#[test_case(Rule::DjangoModelWithoutDunderStr, Path::new("DJ008.py"))]
|
#[test_case(Rule::DjangoModelWithoutDunderStr, Path::new("DJ008.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::DjangoUnorderedBodyContentInModel, Path::new("DJ012.py"))]
|
||||||
#[test_case(Rule::DjangoNonLeadingReceiverDecorator, Path::new("DJ013.py"))]
|
#[test_case(Rule::DjangoNonLeadingReceiverDecorator, Path::new("DJ013.py"))]
|
||||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
|
|
@ -29,4 +32,25 @@ mod tests {
|
||||||
assert_diagnostics!(snapshot, diagnostics);
|
assert_diagnostics!(snapshot, diagnostics);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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/DJ100_custom_paths.py"), &settings)?;
|
||||||
|
assert_diagnostics!("DJ100_custom_paths.py", diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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/DJ101_custom_paths.py"), &settings)?;
|
||||||
|
assert_diagnostics!("DJ101_custom_paths.py", diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ pub(crate) use model_without_dunder_str::*;
|
||||||
pub(crate) use non_leading_receiver_decorator::*;
|
pub(crate) use non_leading_receiver_decorator::*;
|
||||||
pub(crate) use nullable_model_string_field::*;
|
pub(crate) use nullable_model_string_field::*;
|
||||||
pub(crate) use unordered_body_content_in_model::*;
|
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;
|
mod all_with_model_form;
|
||||||
mod exclude_with_model_form;
|
mod exclude_with_model_form;
|
||||||
|
|
@ -13,3 +15,5 @@ mod model_without_dunder_str;
|
||||||
mod non_leading_receiver_decorator;
|
mod non_leading_receiver_decorator;
|
||||||
mod nullable_model_string_field;
|
mod nullable_model_string_field;
|
||||||
mod unordered_body_content_in_model;
|
mod unordered_body_content_in_model;
|
||||||
|
mod url_path_with_leading_slash;
|
||||||
|
mod url_path_without_trailing_slash;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||||
|
use ruff_python_ast::{self as ast, Expr};
|
||||||
|
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.4")]
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
.semantic()
|
||||||
|
.resolve_qualified_name(&call.func)
|
||||||
|
.is_some_and(|qualified_name| {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first argument (the route pattern)
|
||||||
|
let Some(route_arg) = call.arguments.args.first() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if it's a string literal
|
||||||
|
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = route_arg {
|
||||||
|
let route = value.to_str();
|
||||||
|
|
||||||
|
// Skip empty strings and root path "/"
|
||||||
|
if route.is_empty() || route == "/" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if route starts with a leading slash
|
||||||
|
if route.starts_with('/') {
|
||||||
|
// Report diagnostic for routes with leading slash
|
||||||
|
let mut diagnostic = checker.report_diagnostic(
|
||||||
|
DjangoURLPathWithLeadingSlash {
|
||||||
|
url_pattern: route.to_string(),
|
||||||
|
},
|
||||||
|
route_arg.range(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine the quote style to find the insertion point for removal
|
||||||
|
let string_content = checker.locator().slice(route_arg.range());
|
||||||
|
let quote_len =
|
||||||
|
if string_content.starts_with("'''") || string_content.starts_with("\"\"\"") {
|
||||||
|
3
|
||||||
|
} else if string_content.starts_with('\'') || string_content.starts_with('"') {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
return; // Invalid string format
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the leading slash (after the opening quote(s))
|
||||||
|
let removal_start = route_arg.range().start() + TextSize::new(quote_len);
|
||||||
|
let removal_end = removal_start + TextSize::new(1); // Remove one character (the slash)
|
||||||
|
|
||||||
|
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(removal_start, removal_end)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||||
|
use ruff_python_ast::{self as ast, Expr};
|
||||||
|
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()`
|
||||||
|
/// 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.4")]
|
||||||
|
pub(crate) struct DjangoURLPathWithoutTrailingSlash {
|
||||||
|
url_pattern: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
.semantic()
|
||||||
|
.resolve_qualified_name(&call.func)
|
||||||
|
.is_some_and(|qualified_name| {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 routes that are just "/" or already end with "/"
|
||||||
|
if route == "/" || route.ends_with('/') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report diagnostic for routes without trailing slash
|
||||||
|
let mut diagnostic = checker.report_diagnostic(
|
||||||
|
DjangoURLPathWithoutTrailingSlash {
|
||||||
|
url_pattern: route.to_string(),
|
||||||
|
},
|
||||||
|
route_arg.range(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return; // Invalid string format
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
23
crates/ruff_linter/src/rules/flake8_django/settings.rs
Normal file
23
crates/ruff_linter/src/rules/flake8_django/settings.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
//! Settings for the `flake8-django` plugin.
|
||||||
|
|
||||||
|
use crate::display_settings;
|
||||||
|
use ruff_macros::CacheKey;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, CacheKey, Default)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub additional_path_functions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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/<slug:slug>` 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/<slug:slug>", views.bad_view), # DJ100
|
||||||
|
| ^^^^^^^^^^^^^^^^^
|
||||||
|
45 | path("<slug:slug>", 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/<slug:slug>", views.bad_view), # DJ100
|
||||||
|
44 + path("bad/<slug:slug>/", views.bad_view), # DJ100
|
||||||
|
45 | path("<slug:slug>", views.bad_view), # DJ100
|
||||||
|
46 | ]
|
||||||
|
|
||||||
|
DJ100 [*] URL route `<slug:slug>` is missing a trailing slash
|
||||||
|
--> DJ100.py:45:10
|
||||||
|
|
|
||||||
|
43 | urlpatterns_params_bad = [
|
||||||
|
44 | path("bad/<slug:slug>", views.bad_view), # DJ100
|
||||||
|
45 | path("<slug:slug>", 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/<slug:slug>", views.bad_view), # DJ100
|
||||||
|
- path("<slug:slug>", views.bad_view), # DJ100
|
||||||
|
45 + path("<slug:slug>/", views.bad_view), # DJ100
|
||||||
|
46 | ]
|
||||||
|
|
@ -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 |
|
||||||
|
|
@ -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/<slug:slug>/` 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/<slug:slug>/", views.bad_view), # DJ101
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^
|
||||||
|
53 | path("/<slug:slug>", 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/<slug:slug>/", views.bad_view), # DJ101
|
||||||
|
52 + path("bad/<slug:slug>/", views.bad_view), # DJ101
|
||||||
|
53 | path("/<slug:slug>", views.bad_view), # DJ101
|
||||||
|
54 | ]
|
||||||
|
|
||||||
|
DJ101 [*] URL route `/<slug:slug>` has an unnecessary leading slash
|
||||||
|
--> DJ101.py:53:10
|
||||||
|
|
|
||||||
|
51 | urlpatterns_params_bad = [
|
||||||
|
52 | path("/bad/<slug:slug>/", views.bad_view), # DJ101
|
||||||
|
53 | path("/<slug:slug>", 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/<slug:slug>/", views.bad_view), # DJ101
|
||||||
|
- path("/<slug:slug>", views.bad_view), # DJ101
|
||||||
|
53 + path("<slug:slug>", views.bad_view), # DJ101
|
||||||
|
54 | ]
|
||||||
|
|
@ -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 |
|
||||||
|
|
@ -17,7 +17,7 @@ use crate::line_width::LineLength;
|
||||||
use crate::registry::{Linter, Rule};
|
use crate::registry::{Linter, Rule};
|
||||||
use crate::rules::{
|
use crate::rules::{
|
||||||
flake8_annotations, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins,
|
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_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes,
|
||||||
flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe,
|
flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe,
|
||||||
pep8_naming, pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff,
|
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_builtins: flake8_builtins::settings::Settings,
|
||||||
pub flake8_comprehensions: flake8_comprehensions::settings::Settings,
|
pub flake8_comprehensions: flake8_comprehensions::settings::Settings,
|
||||||
pub flake8_copyright: flake8_copyright::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_errmsg: flake8_errmsg::settings::Settings,
|
||||||
pub flake8_gettext: flake8_gettext::settings::Settings,
|
pub flake8_gettext: flake8_gettext::settings::Settings,
|
||||||
pub flake8_implicit_str_concat: flake8_implicit_str_concat::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_pytest_style | nested,
|
||||||
self.flake8_quotes | nested,
|
self.flake8_quotes | nested,
|
||||||
self.flake8_self | nested,
|
self.flake8_self | nested,
|
||||||
|
self.flake8_django | nested,
|
||||||
self.flake8_tidy_imports | nested,
|
self.flake8_tidy_imports | nested,
|
||||||
self.flake8_type_checking | nested,
|
self.flake8_type_checking | nested,
|
||||||
self.flake8_unused_arguments | nested,
|
self.flake8_unused_arguments | nested,
|
||||||
|
|
@ -438,6 +440,7 @@ impl LinterSettings {
|
||||||
flake8_pytest_style: flake8_pytest_style::settings::Settings::default(),
|
flake8_pytest_style: flake8_pytest_style::settings::Settings::default(),
|
||||||
flake8_quotes: flake8_quotes::settings::Settings::default(),
|
flake8_quotes: flake8_quotes::settings::Settings::default(),
|
||||||
flake8_self: flake8_self::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_tidy_imports: flake8_tidy_imports::settings::Settings::default(),
|
||||||
flake8_type_checking: flake8_type_checking::settings::Settings::default(),
|
flake8_type_checking: flake8_type_checking::settings::Settings::default(),
|
||||||
flake8_unused_arguments: flake8_unused_arguments::settings::Settings::default(),
|
flake8_unused_arguments: flake8_unused_arguments::settings::Settings::default(),
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ use ruff_python_formatter::{
|
||||||
use crate::options::{
|
use crate::options::{
|
||||||
AnalyzeOptions, Flake8AnnotationsOptions, Flake8BanditOptions, Flake8BooleanTrapOptions,
|
AnalyzeOptions, Flake8AnnotationsOptions, Flake8BanditOptions, Flake8BooleanTrapOptions,
|
||||||
Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ComprehensionsOptions,
|
Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ComprehensionsOptions,
|
||||||
Flake8CopyrightOptions, Flake8ErrMsgOptions, Flake8GetTextOptions,
|
Flake8CopyrightOptions, Flake8DjangoOptions, Flake8ErrMsgOptions, Flake8GetTextOptions,
|
||||||
Flake8ImplicitStrConcatOptions, Flake8ImportConventionsOptions, Flake8PytestStyleOptions,
|
Flake8ImplicitStrConcatOptions, Flake8ImportConventionsOptions, Flake8PytestStyleOptions,
|
||||||
Flake8QuotesOptions, Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions,
|
Flake8QuotesOptions, Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions,
|
||||||
Flake8UnusedArgumentsOptions, FormatOptions, IsortOptions, LintCommonOptions, LintOptions,
|
Flake8UnusedArgumentsOptions, FormatOptions, IsortOptions, LintCommonOptions, LintOptions,
|
||||||
|
|
@ -366,6 +366,10 @@ impl Configuration {
|
||||||
.map(Flake8CopyrightOptions::try_into_settings)
|
.map(Flake8CopyrightOptions::try_into_settings)
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
flake8_django: lint
|
||||||
|
.flake8_django
|
||||||
|
.map(Flake8DjangoOptions::into_settings)
|
||||||
|
.unwrap_or_default(),
|
||||||
flake8_errmsg: lint
|
flake8_errmsg: lint
|
||||||
.flake8_errmsg
|
.flake8_errmsg
|
||||||
.map(Flake8ErrMsgOptions::into_settings)
|
.map(Flake8ErrMsgOptions::into_settings)
|
||||||
|
|
@ -662,6 +666,7 @@ pub struct LintConfiguration {
|
||||||
pub flake8_builtins: Option<Flake8BuiltinsOptions>,
|
pub flake8_builtins: Option<Flake8BuiltinsOptions>,
|
||||||
pub flake8_comprehensions: Option<Flake8ComprehensionsOptions>,
|
pub flake8_comprehensions: Option<Flake8ComprehensionsOptions>,
|
||||||
pub flake8_copyright: Option<Flake8CopyrightOptions>,
|
pub flake8_copyright: Option<Flake8CopyrightOptions>,
|
||||||
|
pub flake8_django: Option<Flake8DjangoOptions>,
|
||||||
pub flake8_errmsg: Option<Flake8ErrMsgOptions>,
|
pub flake8_errmsg: Option<Flake8ErrMsgOptions>,
|
||||||
pub flake8_gettext: Option<Flake8GetTextOptions>,
|
pub flake8_gettext: Option<Flake8GetTextOptions>,
|
||||||
pub flake8_implicit_str_concat: Option<Flake8ImplicitStrConcatOptions>,
|
pub flake8_implicit_str_concat: Option<Flake8ImplicitStrConcatOptions>,
|
||||||
|
|
@ -779,6 +784,7 @@ impl LintConfiguration {
|
||||||
flake8_builtins: options.common.flake8_builtins,
|
flake8_builtins: options.common.flake8_builtins,
|
||||||
flake8_comprehensions: options.common.flake8_comprehensions,
|
flake8_comprehensions: options.common.flake8_comprehensions,
|
||||||
flake8_copyright: options.common.flake8_copyright,
|
flake8_copyright: options.common.flake8_copyright,
|
||||||
|
flake8_django: options.common.flake8_django,
|
||||||
flake8_errmsg: options.common.flake8_errmsg,
|
flake8_errmsg: options.common.flake8_errmsg,
|
||||||
flake8_gettext: options.common.flake8_gettext,
|
flake8_gettext: options.common.flake8_gettext,
|
||||||
flake8_implicit_str_concat: options.common.flake8_implicit_str_concat,
|
flake8_implicit_str_concat: options.common.flake8_implicit_str_concat,
|
||||||
|
|
@ -1168,6 +1174,7 @@ impl LintConfiguration {
|
||||||
.flake8_comprehensions
|
.flake8_comprehensions
|
||||||
.combine(config.flake8_comprehensions),
|
.combine(config.flake8_comprehensions),
|
||||||
flake8_copyright: self.flake8_copyright.combine(config.flake8_copyright),
|
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_errmsg: self.flake8_errmsg.combine(config.flake8_errmsg),
|
||||||
flake8_gettext: self.flake8_gettext.combine(config.flake8_gettext),
|
flake8_gettext: self.flake8_gettext.combine(config.flake8_gettext),
|
||||||
flake8_implicit_str_concat: self
|
flake8_implicit_str_concat: self
|
||||||
|
|
@ -1394,6 +1401,7 @@ fn warn_about_deprecated_top_level_lint_options(
|
||||||
flake8_builtins,
|
flake8_builtins,
|
||||||
flake8_comprehensions,
|
flake8_comprehensions,
|
||||||
flake8_copyright,
|
flake8_copyright,
|
||||||
|
flake8_django,
|
||||||
flake8_errmsg,
|
flake8_errmsg,
|
||||||
flake8_quotes,
|
flake8_quotes,
|
||||||
flake8_self,
|
flake8_self,
|
||||||
|
|
@ -1517,6 +1525,10 @@ fn warn_about_deprecated_top_level_lint_options(
|
||||||
used_options.push("flake8-copyright");
|
used_options.push("flake8-copyright");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if flake8_django.is_some() {
|
||||||
|
used_options.push("flake8-django");
|
||||||
|
}
|
||||||
|
|
||||||
if flake8_errmsg.is_some() {
|
if flake8_errmsg.is_some() {
|
||||||
used_options.push("flake8-errmsg");
|
used_options.push("flake8-errmsg");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use ruff_linter::rules::pep8_naming::settings::IgnoreNames;
|
||||||
use ruff_linter::rules::pydocstyle::settings::Convention;
|
use ruff_linter::rules::pydocstyle::settings::Convention;
|
||||||
use ruff_linter::rules::pylint::settings::ConstantType;
|
use ruff_linter::rules::pylint::settings::ConstantType;
|
||||||
use ruff_linter::rules::{
|
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_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self,
|
||||||
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming,
|
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming,
|
||||||
pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff,
|
pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff,
|
||||||
|
|
@ -900,6 +900,10 @@ pub struct LintCommonOptions {
|
||||||
#[option_group]
|
#[option_group]
|
||||||
pub flake8_copyright: Option<Flake8CopyrightOptions>,
|
pub flake8_copyright: Option<Flake8CopyrightOptions>,
|
||||||
|
|
||||||
|
/// Options for the `flake8-django` plugin.
|
||||||
|
#[option_group]
|
||||||
|
pub flake8_django: Option<Flake8DjangoOptions>,
|
||||||
|
|
||||||
/// Options for the `flake8-errmsg` plugin.
|
/// Options for the `flake8-errmsg` plugin.
|
||||||
#[option_group]
|
#[option_group]
|
||||||
pub flake8_errmsg: Option<Flake8ErrMsgOptions>,
|
pub flake8_errmsg: Option<Flake8ErrMsgOptions>,
|
||||||
|
|
@ -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<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
/// Options for the `flake8-errmsg` plugin.
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
|
Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
|
||||||
|
|
@ -3937,6 +3974,7 @@ pub struct LintOptionsWire {
|
||||||
flake8_builtins: Option<Flake8BuiltinsOptions>,
|
flake8_builtins: Option<Flake8BuiltinsOptions>,
|
||||||
flake8_comprehensions: Option<Flake8ComprehensionsOptions>,
|
flake8_comprehensions: Option<Flake8ComprehensionsOptions>,
|
||||||
flake8_copyright: Option<Flake8CopyrightOptions>,
|
flake8_copyright: Option<Flake8CopyrightOptions>,
|
||||||
|
flake8_django: Option<Flake8DjangoOptions>,
|
||||||
flake8_errmsg: Option<Flake8ErrMsgOptions>,
|
flake8_errmsg: Option<Flake8ErrMsgOptions>,
|
||||||
flake8_quotes: Option<Flake8QuotesOptions>,
|
flake8_quotes: Option<Flake8QuotesOptions>,
|
||||||
flake8_self: Option<Flake8SelfOptions>,
|
flake8_self: Option<Flake8SelfOptions>,
|
||||||
|
|
@ -3994,6 +4032,7 @@ impl From<LintOptionsWire> for LintOptions {
|
||||||
flake8_builtins,
|
flake8_builtins,
|
||||||
flake8_comprehensions,
|
flake8_comprehensions,
|
||||||
flake8_copyright,
|
flake8_copyright,
|
||||||
|
flake8_django,
|
||||||
flake8_errmsg,
|
flake8_errmsg,
|
||||||
flake8_quotes,
|
flake8_quotes,
|
||||||
flake8_self,
|
flake8_self,
|
||||||
|
|
@ -4050,6 +4089,7 @@ impl From<LintOptionsWire> for LintOptions {
|
||||||
flake8_builtins,
|
flake8_builtins,
|
||||||
flake8_comprehensions,
|
flake8_comprehensions,
|
||||||
flake8_copyright,
|
flake8_copyright,
|
||||||
|
flake8_django,
|
||||||
flake8_errmsg,
|
flake8_errmsg,
|
||||||
flake8_quotes,
|
flake8_quotes,
|
||||||
flake8_self,
|
flake8_self,
|
||||||
|
|
|
||||||
44
ruff.schema.json
generated
44
ruff.schema.json
generated
|
|
@ -297,6 +297,18 @@
|
||||||
],
|
],
|
||||||
"deprecated": true
|
"deprecated": true
|
||||||
},
|
},
|
||||||
|
"flake8-django": {
|
||||||
|
"description": "Options for the `flake8-django` plugin.",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Flake8DjangoOptions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deprecated": true
|
||||||
|
},
|
||||||
"flake8-errmsg": {
|
"flake8-errmsg": {
|
||||||
"description": "Options for the `flake8-errmsg` plugin.",
|
"description": "Options for the `flake8-errmsg` plugin.",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
|
|
@ -1145,6 +1157,23 @@
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"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\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"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"Flake8ErrMsgOptions": {
|
"Flake8ErrMsgOptions": {
|
||||||
"description": "Options for the `flake8-errmsg` plugin.",
|
"description": "Options for the `flake8-errmsg` plugin.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -2167,6 +2196,17 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"flake8-django": {
|
||||||
|
"description": "Options for the `flake8-django` plugin.",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Flake8DjangoOptions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"flake8-errmsg": {
|
"flake8-errmsg": {
|
||||||
"description": "Options for the `flake8-errmsg` plugin.",
|
"description": "Options for the `flake8-errmsg` plugin.",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
|
|
@ -3164,6 +3204,10 @@
|
||||||
"DJ01",
|
"DJ01",
|
||||||
"DJ012",
|
"DJ012",
|
||||||
"DJ013",
|
"DJ013",
|
||||||
|
"DJ1",
|
||||||
|
"DJ10",
|
||||||
|
"DJ100",
|
||||||
|
"DJ101",
|
||||||
"DOC",
|
"DOC",
|
||||||
"DOC1",
|
"DOC1",
|
||||||
"DOC10",
|
"DOC10",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue