mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 03:36:18 +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_,
|
||||
_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 = []
|
||||
|
|
|
|||
|
|
@ -181,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 = []
|
||||
|
|
|
|||
|
|
@ -183,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 = []
|
||||
|
|
|
|||
|
|
@ -183,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 = []
|
||||
|
|
|
|||
|
|
@ -180,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 = []
|
||||
|
|
|
|||
|
|
@ -180,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 = []
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
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) {
|
||||
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) {
|
||||
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, "012") => rules::flake8_django::rules::DjangoUnorderedBodyContentInModel,
|
||||
(Flake8Django, "013") => rules::flake8_django::rules::DjangoNonLeadingReceiverDecorator,
|
||||
(Flake8Django, "100") => rules::flake8_django::rules::DjangoURLPathWithoutTrailingSlash,
|
||||
(Flake8Django, "101") => rules::flake8_django::rules::DjangoURLPathWithLeadingSlash,
|
||||
|
||||
// flynt
|
||||
// Reserved: (Flynt, "001") => Rule: :StringConcatenationToFString,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -18,6 +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("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<()> {
|
||||
|
|
@ -29,4 +32,25 @@ mod tests {
|
|||
assert_diagnostics!(snapshot, diagnostics);
|
||||
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 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;
|
||||
mod exclude_with_model_form;
|
||||
|
|
@ -13,3 +15,5 @@ mod model_without_dunder_str;
|
|||
mod non_leading_receiver_decorator;
|
||||
mod nullable_model_string_field;
|
||||
mod unordered_body_content_in_model;
|
||||
mod url_path_with_leading_slash;
|
||||
mod url_path_without_trailing_slash;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,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::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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -366,6 +366,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)
|
||||
|
|
@ -662,6 +666,7 @@ pub struct LintConfiguration {
|
|||
pub flake8_builtins: Option<Flake8BuiltinsOptions>,
|
||||
pub flake8_comprehensions: Option<Flake8ComprehensionsOptions>,
|
||||
pub flake8_copyright: Option<Flake8CopyrightOptions>,
|
||||
pub flake8_django: Option<Flake8DjangoOptions>,
|
||||
pub flake8_errmsg: Option<Flake8ErrMsgOptions>,
|
||||
pub flake8_gettext: Option<Flake8GetTextOptions>,
|
||||
pub flake8_implicit_str_concat: Option<Flake8ImplicitStrConcatOptions>,
|
||||
|
|
@ -779,6 +784,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,
|
||||
|
|
@ -1168,6 +1174,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
|
||||
|
|
@ -1394,6 +1401,7 @@ fn warn_about_deprecated_top_level_lint_options(
|
|||
flake8_builtins,
|
||||
flake8_comprehensions,
|
||||
flake8_copyright,
|
||||
flake8_django,
|
||||
flake8_errmsg,
|
||||
flake8_quotes,
|
||||
flake8_self,
|
||||
|
|
@ -1517,6 +1525,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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Flake8CopyrightOptions>,
|
||||
|
||||
/// Options for the `flake8-django` plugin.
|
||||
#[option_group]
|
||||
pub flake8_django: Option<Flake8DjangoOptions>,
|
||||
|
||||
/// Options for the `flake8-errmsg` plugin.
|
||||
#[option_group]
|
||||
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.
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
|
||||
|
|
@ -3937,6 +3974,7 @@ pub struct LintOptionsWire {
|
|||
flake8_builtins: Option<Flake8BuiltinsOptions>,
|
||||
flake8_comprehensions: Option<Flake8ComprehensionsOptions>,
|
||||
flake8_copyright: Option<Flake8CopyrightOptions>,
|
||||
flake8_django: Option<Flake8DjangoOptions>,
|
||||
flake8_errmsg: Option<Flake8ErrMsgOptions>,
|
||||
flake8_quotes: Option<Flake8QuotesOptions>,
|
||||
flake8_self: Option<Flake8SelfOptions>,
|
||||
|
|
@ -3994,6 +4032,7 @@ impl From<LintOptionsWire> for LintOptions {
|
|||
flake8_builtins,
|
||||
flake8_comprehensions,
|
||||
flake8_copyright,
|
||||
flake8_django,
|
||||
flake8_errmsg,
|
||||
flake8_quotes,
|
||||
flake8_self,
|
||||
|
|
@ -4050,6 +4089,7 @@ impl From<LintOptionsWire> for LintOptions {
|
|||
flake8_builtins,
|
||||
flake8_comprehensions,
|
||||
flake8_copyright,
|
||||
flake8_django,
|
||||
flake8_errmsg,
|
||||
flake8_quotes,
|
||||
flake8_self,
|
||||
|
|
|
|||
44
ruff.schema.json
generated
44
ruff.schema.json
generated
|
|
@ -297,6 +297,18 @@
|
|||
],
|
||||
"deprecated": true
|
||||
},
|
||||
"flake8-django": {
|
||||
"description": "Options for the `flake8-django` plugin.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Flake8DjangoOptions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"deprecated": true
|
||||
},
|
||||
"flake8-errmsg": {
|
||||
"description": "Options for the `flake8-errmsg` plugin.",
|
||||
"anyOf": [
|
||||
|
|
@ -1145,6 +1157,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\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": {
|
||||
"description": "Options for the `flake8-errmsg` plugin.",
|
||||
"type": "object",
|
||||
|
|
@ -2167,6 +2196,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": [
|
||||
|
|
@ -3164,6 +3204,10 @@
|
|||
"DJ01",
|
||||
"DJ012",
|
||||
"DJ013",
|
||||
"DJ1",
|
||||
"DJ10",
|
||||
"DJ100",
|
||||
"DJ101",
|
||||
"DOC",
|
||||
"DOC1",
|
||||
"DOC10",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue