mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 11:59:10 +00:00
Update to the new rule architecture (#4589)
This commit is contained in:
parent
fcdc7bdd33
commit
4233f6ec91
108 changed files with 2742 additions and 2374 deletions
|
@ -42,7 +42,7 @@ use crate::fs::relativize_path;
|
|||
use crate::importer::Importer;
|
||||
use crate::noqa::NoqaMapping;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::flake8_builtins::rules::AnyShadowing;
|
||||
use crate::rules::flake8_builtins::helpers::AnyShadowing;
|
||||
use crate::rules::{
|
||||
flake8_2020, flake8_annotations, flake8_async, flake8_bandit, flake8_blind_except,
|
||||
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez,
|
||||
|
@ -894,7 +894,7 @@ where
|
|||
|
||||
// flake8_tidy_imports
|
||||
if self.enabled(Rule::BannedApi) {
|
||||
flake8_tidy_imports::banned_api::name_or_parent_is_banned(
|
||||
flake8_tidy_imports::rules::name_or_parent_is_banned(
|
||||
self,
|
||||
&alias.name,
|
||||
alias,
|
||||
|
@ -1057,15 +1057,13 @@ where
|
|||
if let Some(module) =
|
||||
helpers::resolve_imported_module_path(level, module, self.module_path)
|
||||
{
|
||||
flake8_tidy_imports::banned_api::name_or_parent_is_banned(
|
||||
self, &module, stmt,
|
||||
);
|
||||
flake8_tidy_imports::rules::name_or_parent_is_banned(self, &module, stmt);
|
||||
|
||||
for alias in names {
|
||||
if &alias.name == "*" {
|
||||
continue;
|
||||
}
|
||||
flake8_tidy_imports::banned_api::name_is_banned(
|
||||
flake8_tidy_imports::rules::name_is_banned(
|
||||
self,
|
||||
format!("{module}.{}", alias.name),
|
||||
alias,
|
||||
|
@ -1179,16 +1177,14 @@ where
|
|||
}
|
||||
|
||||
if self.enabled(Rule::RelativeImports) {
|
||||
if let Some(diagnostic) =
|
||||
flake8_tidy_imports::relative_imports::banned_relative_import(
|
||||
self,
|
||||
stmt,
|
||||
level,
|
||||
module,
|
||||
self.module_path,
|
||||
self.settings.flake8_tidy_imports.ban_relative_imports,
|
||||
)
|
||||
{
|
||||
if let Some(diagnostic) = flake8_tidy_imports::rules::banned_relative_import(
|
||||
self,
|
||||
stmt,
|
||||
level,
|
||||
module,
|
||||
self.module_path,
|
||||
self.settings.flake8_tidy_imports.ban_relative_imports,
|
||||
) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
@ -2438,7 +2434,7 @@ where
|
|||
flake8_2020::rules::name_or_attribute(self, expr);
|
||||
}
|
||||
if self.enabled(Rule::BannedApi) {
|
||||
flake8_tidy_imports::banned_api::banned_attribute_access(self, expr);
|
||||
flake8_tidy_imports::rules::banned_attribute_access(self, expr);
|
||||
}
|
||||
if self.enabled(Rule::PrivateMemberAccess) {
|
||||
flake8_self::rules::private_member_access(self, expr);
|
||||
|
@ -3024,7 +3020,7 @@ where
|
|||
Rule::BuiltinOpen,
|
||||
Rule::PyPath,
|
||||
]) {
|
||||
flake8_use_pathlib::helpers::replaceable_by_pathlib(self, func);
|
||||
flake8_use_pathlib::rules::replaceable_by_pathlib(self, func);
|
||||
}
|
||||
|
||||
// numpy
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::rules::flake8_pytest_style::types::{
|
|||
ParametrizeNameType, ParametrizeValuesRowType, ParametrizeValuesType,
|
||||
};
|
||||
use crate::rules::flake8_quotes::settings::Quote;
|
||||
use crate::rules::flake8_tidy_imports::relative_imports::Strictness;
|
||||
use crate::rules::flake8_tidy_imports::settings::Strictness;
|
||||
use crate::rules::pydocstyle::settings::Convention;
|
||||
use crate::rules::{
|
||||
flake8_annotations, flake8_bugbear, flake8_builtins, flake8_errmsg, flake8_pytest_style,
|
||||
|
|
|
@ -230,8 +230,8 @@ ruff_macros::register_rules!(
|
|||
// mccabe
|
||||
rules::mccabe::rules::ComplexStructure,
|
||||
// flake8-tidy-imports
|
||||
rules::flake8_tidy_imports::banned_api::BannedApi,
|
||||
rules::flake8_tidy_imports::relative_imports::RelativeImports,
|
||||
rules::flake8_tidy_imports::rules::BannedApi,
|
||||
rules::flake8_tidy_imports::rules::RelativeImports,
|
||||
// flake8-return
|
||||
rules::flake8_return::rules::UnnecessaryReturnNone,
|
||||
rules::flake8_return::rules::ImplicitReturnValue,
|
||||
|
|
|
@ -5,7 +5,7 @@ use ruff_python_ast::source_code::{Indexer, Locator};
|
|||
use crate::registry::Rule;
|
||||
use crate::settings::Settings;
|
||||
|
||||
use super::detection::comment_contains_code;
|
||||
use super::super::detection::comment_contains_code;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for commented-out Python code.
|
3
crates/ruff/src/rules/eradicate/rules/mod.rs
Normal file
3
crates/ruff/src/rules/eradicate/rules/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub(crate) use commented_out_code::{commented_out_code, CommentedOutCode};
|
||||
|
||||
mod commented_out_code;
|
8
crates/ruff/src/rules/flake8_2020/helpers.rs
Normal file
8
crates/ruff/src/rules/flake8_2020/helpers.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use ruff_python_semantic::model::SemanticModel;
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
pub(super) fn is_sys(model: &SemanticModel, expr: &Expr, target: &str) -> bool {
|
||||
model
|
||||
.resolve_call_path(expr)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["sys", target])
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
//! Rules from [flake8-2020](https://pypi.org/project/flake8-2020/).
|
||||
mod helpers;
|
||||
pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -3,30 +3,11 @@ use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged};
|
|||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::model::SemanticModel;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersionSlice3;
|
||||
|
||||
impl Violation for SysVersionSlice3 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[:3]` referenced (python3.10), use `sys.version_info`")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersion2;
|
||||
|
||||
impl Violation for SysVersion2 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[2]` referenced (python3.10), use `sys.version_info`")
|
||||
}
|
||||
}
|
||||
use super::super::helpers::is_sys;
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersionCmpStr3;
|
||||
|
@ -48,16 +29,6 @@ impl Violation for SysVersionInfo0Eq3 {
|
|||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct SixPY3;
|
||||
|
||||
impl Violation for SixPY3 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`six.PY3` referenced (python4), use `not six.PY2`")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersionInfo1CmpInt;
|
||||
|
||||
|
@ -84,16 +55,6 @@ impl Violation for SysVersionInfoMinorCmpInt {
|
|||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersion0;
|
||||
|
||||
impl Violation for SysVersion0 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[0]` referenced (python10), use `sys.version_info`")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersionCmpStr10;
|
||||
|
||||
|
@ -104,69 +65,6 @@ impl Violation for SysVersionCmpStr10 {
|
|||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersionSlice1;
|
||||
|
||||
impl Violation for SysVersionSlice1 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[:1]` referenced (python10), use `sys.version_info`")
|
||||
}
|
||||
}
|
||||
|
||||
fn is_sys(model: &SemanticModel, expr: &Expr, target: &str) -> bool {
|
||||
model
|
||||
.resolve_call_path(expr)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["sys", target])
|
||||
}
|
||||
|
||||
/// YTT101, YTT102, YTT301, YTT303
|
||||
pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
|
||||
if is_sys(checker.semantic_model(), value, "version") {
|
||||
match slice {
|
||||
Expr::Slice(ast::ExprSlice {
|
||||
lower: None,
|
||||
upper: Some(upper),
|
||||
step: None,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(i),
|
||||
..
|
||||
}) = upper.as_ref()
|
||||
{
|
||||
if *i == BigInt::from(1) && checker.enabled(Rule::SysVersionSlice1) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersionSlice1, value.range()));
|
||||
} else if *i == BigInt::from(3) && checker.enabled(Rule::SysVersionSlice3) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersionSlice3, value.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(i),
|
||||
..
|
||||
}) => {
|
||||
if *i == BigInt::from(2) && checker.enabled(Rule::SysVersion2) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersion2, value.range()));
|
||||
} else if *i == BigInt::from(0) && checker.enabled(Rule::SysVersion0) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersion0, value.range()));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// YTT103, YTT201, YTT203, YTT204, YTT302
|
||||
pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &[Expr]) {
|
||||
match left {
|
||||
|
@ -257,16 +155,3 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// YTT202
|
||||
pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(expr)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["six", "PY3"])
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SixPY3, expr.range()));
|
||||
}
|
||||
}
|
12
crates/ruff/src/rules/flake8_2020/rules/mod.rs
Normal file
12
crates/ruff/src/rules/flake8_2020/rules/mod.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
pub(crate) use compare::{
|
||||
compare, SysVersionCmpStr10, SysVersionCmpStr3, SysVersionInfo0Eq3, SysVersionInfo1CmpInt,
|
||||
SysVersionInfoMinorCmpInt,
|
||||
};
|
||||
pub(crate) use name_or_attribute::{name_or_attribute, SixPY3};
|
||||
pub(crate) use subscript::{
|
||||
subscript, SysVersion0, SysVersion2, SysVersionSlice1, SysVersionSlice3,
|
||||
};
|
||||
|
||||
mod compare;
|
||||
mod name_or_attribute;
|
||||
mod subscript;
|
29
crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs
Normal file
29
crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct SixPY3;
|
||||
|
||||
impl Violation for SixPY3 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`six.PY3` referenced (python4), use `not six.PY2`")
|
||||
}
|
||||
}
|
||||
|
||||
/// YTT202
|
||||
pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(expr)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["six", "PY3"])
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SixPY3, expr.range()));
|
||||
}
|
||||
}
|
96
crates/ruff/src/rules/flake8_2020/rules/subscript.rs
Normal file
96
crates/ruff/src/rules/flake8_2020/rules/subscript.rs
Normal file
|
@ -0,0 +1,96 @@
|
|||
use num_bigint::BigInt;
|
||||
use rustpython_parser::ast::{self, Constant, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::flake8_2020::helpers::is_sys;
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersionSlice3;
|
||||
|
||||
impl Violation for SysVersionSlice3 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[:3]` referenced (python3.10), use `sys.version_info`")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersion2;
|
||||
|
||||
impl Violation for SysVersion2 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[2]` referenced (python3.10), use `sys.version_info`")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersion0;
|
||||
|
||||
impl Violation for SysVersion0 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[0]` referenced (python10), use `sys.version_info`")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct SysVersionSlice1;
|
||||
|
||||
impl Violation for SysVersionSlice1 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[:1]` referenced (python10), use `sys.version_info`")
|
||||
}
|
||||
}
|
||||
|
||||
/// YTT101, YTT102, YTT301, YTT303
|
||||
pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
|
||||
if is_sys(checker.semantic_model(), value, "version") {
|
||||
match slice {
|
||||
Expr::Slice(ast::ExprSlice {
|
||||
lower: None,
|
||||
upper: Some(upper),
|
||||
step: None,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(i),
|
||||
..
|
||||
}) = upper.as_ref()
|
||||
{
|
||||
if *i == BigInt::from(1) && checker.enabled(Rule::SysVersionSlice1) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersionSlice1, value.range()));
|
||||
} else if *i == BigInt::from(3) && checker.enabled(Rule::SysVersionSlice3) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersionSlice3, value.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(i),
|
||||
..
|
||||
}) => {
|
||||
if *i == BigInt::from(2) && checker.enabled(Rule::SysVersion2) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersion2, value.range()));
|
||||
} else if *i == BigInt::from(0) && checker.enabled(Rule::SysVersion0) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersion0, value.range()));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,8 +14,8 @@ use ruff_python_stdlib::typing::SIMPLE_MAGIC_RETURN_TYPES;
|
|||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
use super::fixes;
|
||||
use super::helpers::match_function_def;
|
||||
use super::super::fixes;
|
||||
use super::super::helpers::match_function_def;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that function arguments have type annotations.
|
8
crates/ruff/src/rules/flake8_annotations/rules/mod.rs
Normal file
8
crates/ruff/src/rules/flake8_annotations/rules/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
pub(crate) use definition::{
|
||||
definition, AnyType, MissingReturnTypeClassMethod, MissingReturnTypePrivateFunction,
|
||||
MissingReturnTypeSpecialMethod, MissingReturnTypeStaticMethod,
|
||||
MissingReturnTypeUndocumentedPublicFunction, MissingTypeArgs, MissingTypeCls,
|
||||
MissingTypeFunctionArgument, MissingTypeKwargs, MissingTypeSelf,
|
||||
};
|
||||
|
||||
mod definition;
|
18
crates/ruff/src/rules/flake8_async/helpers.rs
Normal file
18
crates/ruff/src/rules/flake8_async/helpers.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use ruff_python_semantic::{
|
||||
model::SemanticModel,
|
||||
scope::{FunctionDef, ScopeKind},
|
||||
};
|
||||
|
||||
/// Return `true` if the [`SemanticModel`] is inside an async function definition.
|
||||
pub(crate) fn in_async_function(model: &SemanticModel) -> bool {
|
||||
model
|
||||
.scopes()
|
||||
.find_map(|scope| {
|
||||
if let ScopeKind::Function(FunctionDef { async_, .. }) = &scope.kind {
|
||||
Some(*async_)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
//! Rules from [flake8-async](https://pypi.org/project/flake8-async/).
|
||||
mod helpers;
|
||||
pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1,235 +0,0 @@
|
|||
use rustpython_parser::ast;
|
||||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::model::SemanticModel;
|
||||
use ruff_python_semantic::scope::{FunctionDef, ScopeKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that async functions do not contain blocking HTTP calls.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Blocking an async function via a blocking HTTP call will block the entire
|
||||
/// event loop, preventing it from executing other tasks while waiting for the
|
||||
/// HTTP response, negating the benefits of asynchronous programming.
|
||||
///
|
||||
/// Instead of making a blocking HTTP call, use an asynchronous HTTP client
|
||||
/// library such as `aiohttp` or `httpx`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// async def fetch():
|
||||
/// urllib.request.urlopen("https://example.com/foo/bar").read()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// async def fetch():
|
||||
/// async with aiohttp.ClientSession() as session:
|
||||
/// async with session.get("https://example.com/foo/bar") as resp:
|
||||
/// ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct BlockingHttpCallInAsyncFunction;
|
||||
|
||||
impl Violation for BlockingHttpCallInAsyncFunction {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Async functions should not call blocking HTTP methods")
|
||||
}
|
||||
}
|
||||
|
||||
const BLOCKING_HTTP_CALLS: &[&[&str]] = &[
|
||||
&["urllib", "request", "urlopen"],
|
||||
&["httpx", "get"],
|
||||
&["httpx", "post"],
|
||||
&["httpx", "delete"],
|
||||
&["httpx", "patch"],
|
||||
&["httpx", "put"],
|
||||
&["httpx", "head"],
|
||||
&["httpx", "connect"],
|
||||
&["httpx", "options"],
|
||||
&["httpx", "trace"],
|
||||
&["requests", "get"],
|
||||
&["requests", "post"],
|
||||
&["requests", "delete"],
|
||||
&["requests", "patch"],
|
||||
&["requests", "put"],
|
||||
&["requests", "head"],
|
||||
&["requests", "connect"],
|
||||
&["requests", "options"],
|
||||
&["requests", "trace"],
|
||||
];
|
||||
|
||||
/// ASYNC100
|
||||
pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) {
|
||||
if in_async_function(checker.semantic_model()) {
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
|
||||
let call_path = checker.semantic_model().resolve_call_path(func);
|
||||
let is_blocking =
|
||||
call_path.map_or(false, |path| BLOCKING_HTTP_CALLS.contains(&path.as_slice()));
|
||||
|
||||
if is_blocking {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BlockingHttpCallInAsyncFunction,
|
||||
func.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that async functions do not contain calls to `open`, `time.sleep`,
|
||||
/// or `subprocess` methods.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Blocking an async function via a blocking call will block the entire
|
||||
/// event loop, preventing it from executing other tasks while waiting for the
|
||||
/// call to complete, negating the benefits of asynchronous programming.
|
||||
///
|
||||
/// Instead of making a blocking call, use an equivalent asynchronous library
|
||||
/// or function.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// async def foo():
|
||||
/// time.sleep(1000)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// async def foo():
|
||||
/// await asyncio.sleep(1000)
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct OpenSleepOrSubprocessInAsyncFunction;
|
||||
|
||||
impl Violation for OpenSleepOrSubprocessInAsyncFunction {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Async functions should not call `open`, `time.sleep`, or `subprocess` methods")
|
||||
}
|
||||
}
|
||||
|
||||
const OPEN_SLEEP_OR_SUBPROCESS_CALL: &[&[&str]] = &[
|
||||
&["", "open"],
|
||||
&["time", "sleep"],
|
||||
&["subprocess", "run"],
|
||||
&["subprocess", "Popen"],
|
||||
// Deprecated subprocess calls:
|
||||
&["subprocess", "call"],
|
||||
&["subprocess", "check_call"],
|
||||
&["subprocess", "check_output"],
|
||||
&["subprocess", "getoutput"],
|
||||
&["subprocess", "getstatusoutput"],
|
||||
&["os", "wait"],
|
||||
&["os", "wait3"],
|
||||
&["os", "wait4"],
|
||||
&["os", "waitid"],
|
||||
&["os", "waitpid"],
|
||||
];
|
||||
|
||||
/// ASYNC101
|
||||
pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) {
|
||||
if in_async_function(checker.semantic_model()) {
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
|
||||
let is_open_sleep_or_subprocess_call = checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |path| {
|
||||
OPEN_SLEEP_OR_SUBPROCESS_CALL.contains(&path.as_slice())
|
||||
});
|
||||
|
||||
if is_open_sleep_or_subprocess_call {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
OpenSleepOrSubprocessInAsyncFunction,
|
||||
func.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that async functions do not contain calls to blocking synchronous
|
||||
/// process calls via the `os` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Blocking an async function via a blocking call will block the entire
|
||||
/// event loop, preventing it from executing other tasks while waiting for the
|
||||
/// call to complete, negating the benefits of asynchronous programming.
|
||||
///
|
||||
/// Instead of making a blocking call, use an equivalent asynchronous library
|
||||
/// or function.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// async def foo():
|
||||
/// os.popen()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// os.popen()
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct BlockingOsCallInAsyncFunction;
|
||||
|
||||
impl Violation for BlockingOsCallInAsyncFunction {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Async functions should not call synchronous `os` methods")
|
||||
}
|
||||
}
|
||||
|
||||
const UNSAFE_OS_METHODS: &[&[&str]] = &[
|
||||
&["os", "popen"],
|
||||
&["os", "posix_spawn"],
|
||||
&["os", "posix_spawnp"],
|
||||
&["os", "spawnl"],
|
||||
&["os", "spawnle"],
|
||||
&["os", "spawnlp"],
|
||||
&["os", "spawnlpe"],
|
||||
&["os", "spawnv"],
|
||||
&["os", "spawnve"],
|
||||
&["os", "spawnvp"],
|
||||
&["os", "spawnvpe"],
|
||||
&["os", "system"],
|
||||
];
|
||||
|
||||
/// ASYNC102
|
||||
pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) {
|
||||
if in_async_function(checker.semantic_model()) {
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
|
||||
let is_unsafe_os_method = checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |path| UNSAFE_OS_METHODS.contains(&path.as_slice()));
|
||||
|
||||
if is_unsafe_os_method {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(BlockingOsCallInAsyncFunction, func.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the [`SemanticModel`] is inside an async function definition.
|
||||
fn in_async_function(model: &SemanticModel) -> bool {
|
||||
model
|
||||
.scopes()
|
||||
.find_map(|scope| {
|
||||
if let ScopeKind::Function(FunctionDef { async_, .. }) = &scope.kind {
|
||||
Some(*async_)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
use rustpython_parser::ast;
|
||||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::super::helpers::in_async_function;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that async functions do not contain blocking HTTP calls.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Blocking an async function via a blocking HTTP call will block the entire
|
||||
/// event loop, preventing it from executing other tasks while waiting for the
|
||||
/// HTTP response, negating the benefits of asynchronous programming.
|
||||
///
|
||||
/// Instead of making a blocking HTTP call, use an asynchronous HTTP client
|
||||
/// library such as `aiohttp` or `httpx`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// async def fetch():
|
||||
/// urllib.request.urlopen("https://example.com/foo/bar").read()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// async def fetch():
|
||||
/// async with aiohttp.ClientSession() as session:
|
||||
/// async with session.get("https://example.com/foo/bar") as resp:
|
||||
/// ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct BlockingHttpCallInAsyncFunction;
|
||||
|
||||
impl Violation for BlockingHttpCallInAsyncFunction {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Async functions should not call blocking HTTP methods")
|
||||
}
|
||||
}
|
||||
|
||||
const BLOCKING_HTTP_CALLS: &[&[&str]] = &[
|
||||
&["urllib", "request", "urlopen"],
|
||||
&["httpx", "get"],
|
||||
&["httpx", "post"],
|
||||
&["httpx", "delete"],
|
||||
&["httpx", "patch"],
|
||||
&["httpx", "put"],
|
||||
&["httpx", "head"],
|
||||
&["httpx", "connect"],
|
||||
&["httpx", "options"],
|
||||
&["httpx", "trace"],
|
||||
&["requests", "get"],
|
||||
&["requests", "post"],
|
||||
&["requests", "delete"],
|
||||
&["requests", "patch"],
|
||||
&["requests", "put"],
|
||||
&["requests", "head"],
|
||||
&["requests", "connect"],
|
||||
&["requests", "options"],
|
||||
&["requests", "trace"],
|
||||
];
|
||||
|
||||
/// ASYNC100
|
||||
pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) {
|
||||
if in_async_function(checker.semantic_model()) {
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
|
||||
let call_path = checker.semantic_model().resolve_call_path(func);
|
||||
let is_blocking =
|
||||
call_path.map_or(false, |path| BLOCKING_HTTP_CALLS.contains(&path.as_slice()));
|
||||
|
||||
if is_blocking {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BlockingHttpCallInAsyncFunction,
|
||||
func.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
75
crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs
Normal file
75
crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use rustpython_parser::ast;
|
||||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::super::helpers::in_async_function;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that async functions do not contain calls to blocking synchronous
|
||||
/// process calls via the `os` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Blocking an async function via a blocking call will block the entire
|
||||
/// event loop, preventing it from executing other tasks while waiting for the
|
||||
/// call to complete, negating the benefits of asynchronous programming.
|
||||
///
|
||||
/// Instead of making a blocking call, use an equivalent asynchronous library
|
||||
/// or function.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// async def foo():
|
||||
/// os.popen()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// os.popen()
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct BlockingOsCallInAsyncFunction;
|
||||
|
||||
impl Violation for BlockingOsCallInAsyncFunction {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Async functions should not call synchronous `os` methods")
|
||||
}
|
||||
}
|
||||
|
||||
const UNSAFE_OS_METHODS: &[&[&str]] = &[
|
||||
&["os", "popen"],
|
||||
&["os", "posix_spawn"],
|
||||
&["os", "posix_spawnp"],
|
||||
&["os", "spawnl"],
|
||||
&["os", "spawnle"],
|
||||
&["os", "spawnlp"],
|
||||
&["os", "spawnlpe"],
|
||||
&["os", "spawnv"],
|
||||
&["os", "spawnve"],
|
||||
&["os", "spawnvp"],
|
||||
&["os", "spawnvpe"],
|
||||
&["os", "system"],
|
||||
];
|
||||
|
||||
/// ASYNC102
|
||||
pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) {
|
||||
if in_async_function(checker.semantic_model()) {
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
|
||||
let is_unsafe_os_method = checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |path| UNSAFE_OS_METHODS.contains(&path.as_slice()));
|
||||
|
||||
if is_unsafe_os_method {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(BlockingOsCallInAsyncFunction, func.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
crates/ruff/src/rules/flake8_async/rules/mod.rs
Normal file
9
crates/ruff/src/rules/flake8_async/rules/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
pub(crate) use blocking_http_call::{blocking_http_call, BlockingHttpCallInAsyncFunction};
|
||||
pub(crate) use blocking_os_call::{blocking_os_call, BlockingOsCallInAsyncFunction};
|
||||
pub(crate) use open_sleep_or_subprocess_call::{
|
||||
open_sleep_or_subprocess_call, OpenSleepOrSubprocessInAsyncFunction,
|
||||
};
|
||||
|
||||
mod blocking_http_call;
|
||||
mod blocking_os_call;
|
||||
mod open_sleep_or_subprocess_call;
|
|
@ -0,0 +1,81 @@
|
|||
use rustpython_parser::ast;
|
||||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::super::helpers::in_async_function;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that async functions do not contain calls to `open`, `time.sleep`,
|
||||
/// or `subprocess` methods.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Blocking an async function via a blocking call will block the entire
|
||||
/// event loop, preventing it from executing other tasks while waiting for the
|
||||
/// call to complete, negating the benefits of asynchronous programming.
|
||||
///
|
||||
/// Instead of making a blocking call, use an equivalent asynchronous library
|
||||
/// or function.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// async def foo():
|
||||
/// time.sleep(1000)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// async def foo():
|
||||
/// await asyncio.sleep(1000)
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct OpenSleepOrSubprocessInAsyncFunction;
|
||||
|
||||
impl Violation for OpenSleepOrSubprocessInAsyncFunction {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Async functions should not call `open`, `time.sleep`, or `subprocess` methods")
|
||||
}
|
||||
}
|
||||
|
||||
const OPEN_SLEEP_OR_SUBPROCESS_CALL: &[&[&str]] = &[
|
||||
&["", "open"],
|
||||
&["time", "sleep"],
|
||||
&["subprocess", "run"],
|
||||
&["subprocess", "Popen"],
|
||||
// Deprecated subprocess calls:
|
||||
&["subprocess", "call"],
|
||||
&["subprocess", "check_call"],
|
||||
&["subprocess", "check_output"],
|
||||
&["subprocess", "getoutput"],
|
||||
&["subprocess", "getstatusoutput"],
|
||||
&["os", "wait"],
|
||||
&["os", "wait3"],
|
||||
&["os", "wait4"],
|
||||
&["os", "waitid"],
|
||||
&["os", "waitpid"],
|
||||
];
|
||||
|
||||
/// ASYNC101
|
||||
pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) {
|
||||
if in_async_function(checker.semantic_model()) {
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
|
||||
let is_open_sleep_or_subprocess_call = checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |path| {
|
||||
OPEN_SLEEP_OR_SUBPROCESS_CALL.contains(&path.as_slice())
|
||||
});
|
||||
|
||||
if is_open_sleep_or_subprocess_call {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
OpenSleepOrSubprocessInAsyncFunction,
|
||||
func.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
crates/ruff/src/rules/flake8_blind_except/rules/mod.rs
Normal file
3
crates/ruff/src/rules/flake8_blind_except/rules/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub(crate) use blind_except::{blind_except, BlindExcept};
|
||||
|
||||
mod blind_except;
|
66
crates/ruff/src/rules/flake8_boolean_trap/helpers.rs
Normal file
66
crates/ruff/src/rules/flake8_boolean_trap/helpers.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use rustpython_parser::ast::{self, Constant, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
pub(super) const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[
|
||||
"append",
|
||||
"assertEqual",
|
||||
"assertEquals",
|
||||
"assertNotEqual",
|
||||
"assertNotEquals",
|
||||
"bytes",
|
||||
"count",
|
||||
"failIfEqual",
|
||||
"failUnlessEqual",
|
||||
"float",
|
||||
"fromkeys",
|
||||
"get",
|
||||
"getattr",
|
||||
"getboolean",
|
||||
"getfloat",
|
||||
"getint",
|
||||
"index",
|
||||
"insert",
|
||||
"int",
|
||||
"param",
|
||||
"pop",
|
||||
"remove",
|
||||
"setattr",
|
||||
"setdefault",
|
||||
"str",
|
||||
];
|
||||
|
||||
pub(super) const FUNC_DEF_NAME_ALLOWLIST: &[&str] = &["__setitem__"];
|
||||
|
||||
/// Returns `true` if an argument is allowed to use a boolean trap. To return
|
||||
/// `true`, the function name must be explicitly allowed, and the argument must
|
||||
/// be either the first or second argument in the call.
|
||||
pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
|
||||
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func {
|
||||
return FUNC_CALL_NAME_ALLOWLIST.contains(&attr.as_ref());
|
||||
}
|
||||
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = func {
|
||||
return FUNC_CALL_NAME_ALLOWLIST.contains(&id.as_ref());
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
const fn is_boolean_arg(arg: &Expr) -> bool {
|
||||
matches!(
|
||||
&arg,
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Bool(_),
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn add_if_boolean(checker: &mut Checker, arg: &Expr, kind: DiagnosticKind) {
|
||||
if is_boolean_arg(arg) {
|
||||
checker.diagnostics.push(Diagnostic::new(kind, arg.range()));
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
//! Rules from [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/).
|
||||
mod helpers;
|
||||
pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1,301 +0,0 @@
|
|||
use rustpython_parser::ast::{self, Arguments, Constant, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::collect_call_path;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for boolean positional arguments in function definitions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean positional arguments is confusing as the
|
||||
/// meaning of the boolean value is not clear to the caller, and to future
|
||||
/// readers of the code.
|
||||
///
|
||||
/// The use of a boolean will also limit the function to only two possible
|
||||
/// behaviors, which makes the function difficult to extend in the future.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number: float, up: bool) -> int:
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5, True) # What does `True` mean?
|
||||
/// round_number(1.5, False) # What does `False` mean?
|
||||
/// ```
|
||||
///
|
||||
/// Instead, refactor into separate implementations:
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_up(number: float) -> int:
|
||||
/// return ceil(number)
|
||||
///
|
||||
///
|
||||
/// def round_down(number: float) -> int:
|
||||
/// return floor(number)
|
||||
///
|
||||
///
|
||||
/// round_up(1.5)
|
||||
/// round_down(1.5)
|
||||
/// ```
|
||||
///
|
||||
/// Or, refactor to use an `Enum`:
|
||||
/// ```python
|
||||
/// from enum import Enum
|
||||
///
|
||||
///
|
||||
/// class RoundingMethod(Enum):
|
||||
/// UP = 1
|
||||
/// DOWN = 2
|
||||
///
|
||||
///
|
||||
/// def round_number(value: float, method: RoundingMethod) -> float:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
#[violation]
|
||||
pub struct BooleanPositionalArgInFunctionDefinition;
|
||||
|
||||
impl Violation for BooleanPositionalArgInFunctionDefinition {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Boolean positional arg in function definition")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of booleans as default values in function definitions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean default means that the keyword argument
|
||||
/// argument can be omitted, which makes the function call ambiguous.
|
||||
///
|
||||
/// Instead, define the relevant argument as keyword-only.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number: float, *, up: bool = True) -> int:
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5)
|
||||
/// round_number(1.5, up=False)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number: float, *, up: bool) -> int:
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5, up=True)
|
||||
/// round_number(1.5, up=False)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
#[violation]
|
||||
pub struct BooleanDefaultValueInFunctionDefinition;
|
||||
|
||||
impl Violation for BooleanDefaultValueInFunctionDefinition {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Boolean default value in function definition")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for boolean positional arguments in function calls.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean positional arguments is confusing as the
|
||||
/// meaning of the boolean value is not clear to the caller, and to future
|
||||
/// readers of the code.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def foo(flag: bool) -> None:
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// foo(True)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo(flag: bool) -> None:
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// foo(flag=True)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
#[violation]
|
||||
pub struct BooleanPositionalValueInFunctionCall;
|
||||
|
||||
impl Violation for BooleanPositionalValueInFunctionCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Boolean positional value in function call")
|
||||
}
|
||||
}
|
||||
|
||||
const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[
|
||||
"append",
|
||||
"assertEqual",
|
||||
"assertEquals",
|
||||
"assertNotEqual",
|
||||
"assertNotEquals",
|
||||
"bytes",
|
||||
"count",
|
||||
"failIfEqual",
|
||||
"failUnlessEqual",
|
||||
"float",
|
||||
"fromkeys",
|
||||
"get",
|
||||
"getattr",
|
||||
"getboolean",
|
||||
"getfloat",
|
||||
"getint",
|
||||
"index",
|
||||
"insert",
|
||||
"int",
|
||||
"param",
|
||||
"pop",
|
||||
"remove",
|
||||
"setattr",
|
||||
"setdefault",
|
||||
"str",
|
||||
];
|
||||
|
||||
const FUNC_DEF_NAME_ALLOWLIST: &[&str] = &["__setitem__"];
|
||||
|
||||
/// Returns `true` if an argument is allowed to use a boolean trap. To return
|
||||
/// `true`, the function name must be explicitly allowed, and the argument must
|
||||
/// be either the first or second argument in the call.
|
||||
fn allow_boolean_trap(func: &Expr) -> bool {
|
||||
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func {
|
||||
return FUNC_CALL_NAME_ALLOWLIST.contains(&attr.as_ref());
|
||||
}
|
||||
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = func {
|
||||
return FUNC_CALL_NAME_ALLOWLIST.contains(&id.as_ref());
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
const fn is_boolean_arg(arg: &Expr) -> bool {
|
||||
matches!(
|
||||
&arg,
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Bool(_),
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn add_if_boolean(checker: &mut Checker, arg: &Expr, kind: DiagnosticKind) {
|
||||
if is_boolean_arg(arg) {
|
||||
checker.diagnostics.push(Diagnostic::new(kind, arg.range()));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_positional_boolean_in_def(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
decorator_list: &[Expr],
|
||||
arguments: &Arguments,
|
||||
) {
|
||||
if FUNC_DEF_NAME_ALLOWLIST.contains(&name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if decorator_list.iter().any(|expr| {
|
||||
collect_call_path(expr).map_or(false, |call_path| call_path.as_slice() == [name, "setter"])
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
for arg in arguments.posonlyargs.iter().chain(arguments.args.iter()) {
|
||||
if arg.annotation.is_none() {
|
||||
continue;
|
||||
}
|
||||
let Some(expr) = &arg.annotation else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// check for both bool (python class) and 'bool' (string annotation)
|
||||
let hint = match expr.as_ref() {
|
||||
Expr::Name(name) => &name.id == "bool",
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
}) => value == "bool",
|
||||
_ => false,
|
||||
};
|
||||
if !hint {
|
||||
continue;
|
||||
}
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BooleanPositionalArgInFunctionDefinition,
|
||||
arg.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_boolean_default_value_in_function_definition(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
decorator_list: &[Expr],
|
||||
arguments: &Arguments,
|
||||
) {
|
||||
if FUNC_DEF_NAME_ALLOWLIST.contains(&name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if decorator_list.iter().any(|expr| {
|
||||
collect_call_path(expr).map_or(false, |call_path| call_path.as_slice() == [name, "setter"])
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
for arg in &arguments.defaults {
|
||||
add_if_boolean(checker, arg, BooleanDefaultValueInFunctionDefinition.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_boolean_positional_value_in_function_call(
|
||||
checker: &mut Checker,
|
||||
args: &[Expr],
|
||||
func: &Expr,
|
||||
) {
|
||||
if allow_boolean_trap(func) {
|
||||
return;
|
||||
}
|
||||
for arg in args {
|
||||
add_if_boolean(checker, arg, BooleanPositionalValueInFunctionCall.into());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
use rustpython_parser::ast::{Arguments, Expr};
|
||||
|
||||
use ruff_diagnostics::Violation;
|
||||
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::collect_call_path;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_boolean_trap::helpers::add_if_boolean;
|
||||
|
||||
use super::super::helpers::FUNC_DEF_NAME_ALLOWLIST;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of booleans as default values in function definitions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean default means that the keyword argument
|
||||
/// argument can be omitted, which makes the function call ambiguous.
|
||||
///
|
||||
/// Instead, define the relevant argument as keyword-only.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number: float, *, up: bool = True) -> int:
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5)
|
||||
/// round_number(1.5, up=False)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number: float, *, up: bool) -> int:
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5, up=True)
|
||||
/// round_number(1.5, up=False)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
#[violation]
|
||||
pub struct BooleanDefaultValueInFunctionDefinition;
|
||||
|
||||
impl Violation for BooleanDefaultValueInFunctionDefinition {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Boolean default value in function definition")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_boolean_default_value_in_function_definition(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
decorator_list: &[Expr],
|
||||
arguments: &Arguments,
|
||||
) {
|
||||
if FUNC_DEF_NAME_ALLOWLIST.contains(&name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if decorator_list.iter().any(|expr| {
|
||||
collect_call_path(expr).map_or(false, |call_path| call_path.as_slice() == [name, "setter"])
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
for arg in &arguments.defaults {
|
||||
add_if_boolean(checker, arg, BooleanDefaultValueInFunctionDefinition.into());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_diagnostics::Violation;
|
||||
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, allow_boolean_trap};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for boolean positional arguments in function calls.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean positional arguments is confusing as the
|
||||
/// meaning of the boolean value is not clear to the caller, and to future
|
||||
/// readers of the code.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def foo(flag: bool) -> None:
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// foo(True)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo(flag: bool) -> None:
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// foo(flag=True)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
#[violation]
|
||||
pub struct BooleanPositionalValueInFunctionCall;
|
||||
|
||||
impl Violation for BooleanPositionalValueInFunctionCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Boolean positional value in function call")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_boolean_positional_value_in_function_call(
|
||||
checker: &mut Checker,
|
||||
args: &[Expr],
|
||||
func: &Expr,
|
||||
) {
|
||||
if allow_boolean_trap(func) {
|
||||
return;
|
||||
}
|
||||
for arg in args {
|
||||
add_if_boolean(checker, arg, BooleanPositionalValueInFunctionCall.into());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
use rustpython_parser::ast::{self, Arguments, Constant, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::collect_call_path;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_boolean_trap::helpers::FUNC_DEF_NAME_ALLOWLIST;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for boolean positional arguments in function definitions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean positional arguments is confusing as the
|
||||
/// meaning of the boolean value is not clear to the caller, and to future
|
||||
/// readers of the code.
|
||||
///
|
||||
/// The use of a boolean will also limit the function to only two possible
|
||||
/// behaviors, which makes the function difficult to extend in the future.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number: float, up: bool) -> int:
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5, True) # What does `True` mean?
|
||||
/// round_number(1.5, False) # What does `False` mean?
|
||||
/// ```
|
||||
///
|
||||
/// Instead, refactor into separate implementations:
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_up(number: float) -> int:
|
||||
/// return ceil(number)
|
||||
///
|
||||
///
|
||||
/// def round_down(number: float) -> int:
|
||||
/// return floor(number)
|
||||
///
|
||||
///
|
||||
/// round_up(1.5)
|
||||
/// round_down(1.5)
|
||||
/// ```
|
||||
///
|
||||
/// Or, refactor to use an `Enum`:
|
||||
/// ```python
|
||||
/// from enum import Enum
|
||||
///
|
||||
///
|
||||
/// class RoundingMethod(Enum):
|
||||
/// UP = 1
|
||||
/// DOWN = 2
|
||||
///
|
||||
///
|
||||
/// def round_number(value: float, method: RoundingMethod) -> float:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
#[violation]
|
||||
pub struct BooleanPositionalArgInFunctionDefinition;
|
||||
|
||||
impl Violation for BooleanPositionalArgInFunctionDefinition {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Boolean positional arg in function definition")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_positional_boolean_in_def(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
decorator_list: &[Expr],
|
||||
arguments: &Arguments,
|
||||
) {
|
||||
if FUNC_DEF_NAME_ALLOWLIST.contains(&name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if decorator_list.iter().any(|expr| {
|
||||
collect_call_path(expr).map_or(false, |call_path| call_path.as_slice() == [name, "setter"])
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
for arg in arguments.posonlyargs.iter().chain(arguments.args.iter()) {
|
||||
if arg.annotation.is_none() {
|
||||
continue;
|
||||
}
|
||||
let Some(expr) = &arg.annotation else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// check for both bool (python class) and 'bool' (string annotation)
|
||||
let hint = match expr.as_ref() {
|
||||
Expr::Name(name) => &name.id == "bool",
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
}) => value == "bool",
|
||||
_ => false,
|
||||
};
|
||||
if !hint {
|
||||
continue;
|
||||
}
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BooleanPositionalArgInFunctionDefinition,
|
||||
arg.range(),
|
||||
));
|
||||
}
|
||||
}
|
13
crates/ruff/src/rules/flake8_boolean_trap/rules/mod.rs
Normal file
13
crates/ruff/src/rules/flake8_boolean_trap/rules/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
pub(crate) use check_boolean_default_value_in_function_definition::{
|
||||
check_boolean_default_value_in_function_definition, BooleanDefaultValueInFunctionDefinition,
|
||||
};
|
||||
pub(crate) use check_boolean_positional_value_in_function_call::{
|
||||
check_boolean_positional_value_in_function_call, BooleanPositionalValueInFunctionCall,
|
||||
};
|
||||
pub(crate) use check_positional_boolean_in_def::{
|
||||
check_positional_boolean_in_def, BooleanPositionalArgInFunctionDefinition,
|
||||
};
|
||||
|
||||
mod check_boolean_default_value_in_function_definition;
|
||||
mod check_boolean_positional_value_in_function_call;
|
||||
mod check_positional_boolean_in_def;
|
45
crates/ruff/src/rules/flake8_builtins/helpers.rs
Normal file
45
crates/ruff/src/rules/flake8_builtins/helpers.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use rustpython_parser::ast::{Excepthandler, Expr, Ranged, Stmt};
|
||||
|
||||
use ruff_python_ast::helpers::identifier_range;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_stdlib::builtins::BUILTINS;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool {
|
||||
BUILTINS.contains(&name) && ignorelist.iter().all(|ignore| ignore != name)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub(crate) enum AnyShadowing<'a> {
|
||||
Expression(&'a Expr),
|
||||
Statement(&'a Stmt),
|
||||
ExceptHandler(&'a Excepthandler),
|
||||
}
|
||||
|
||||
impl AnyShadowing<'_> {
|
||||
pub(crate) fn range(self, locator: &Locator) -> TextRange {
|
||||
match self {
|
||||
AnyShadowing::Expression(expr) => expr.range(),
|
||||
AnyShadowing::Statement(stmt) => identifier_range(stmt, locator),
|
||||
AnyShadowing::ExceptHandler(handler) => handler.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Stmt> for AnyShadowing<'a> {
|
||||
fn from(value: &'a Stmt) -> Self {
|
||||
AnyShadowing::Statement(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Expr> for AnyShadowing<'a> {
|
||||
fn from(value: &'a Expr) -> Self {
|
||||
AnyShadowing::Expression(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Excepthandler> for AnyShadowing<'a> {
|
||||
fn from(value: &'a Excepthandler) -> Self {
|
||||
AnyShadowing::ExceptHandler(value)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
//! Rules from [flake8-builtins](https://pypi.org/project/flake8-builtins/).
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
|
||||
|
|
|
@ -1,257 +0,0 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{Arg, Excepthandler, Expr, Ranged, Stmt};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::identifier_range;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_stdlib::builtins::BUILTINS;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for variable (and function) assignments that use the same name
|
||||
/// as a builtin.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Reusing a builtin name for the name of a variable increases the
|
||||
/// difficulty of reading and maintaining the code, and can cause
|
||||
/// non-obvious errors, as readers may mistake the variable for the
|
||||
/// builtin and vice versa.
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `flake8-builtins.builtins-ignorelist`
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def find_max(list_of_lists):
|
||||
/// max = 0
|
||||
/// for flat_list in list_of_lists:
|
||||
/// for value in flat_list:
|
||||
/// max = max(max, value) # TypeError: 'int' object is not callable
|
||||
/// return max
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def find_max(list_of_lists):
|
||||
/// result = 0
|
||||
/// for flat_list in list_of_lists:
|
||||
/// for value in flat_list:
|
||||
/// result = max(result, value)
|
||||
/// return result
|
||||
/// ```
|
||||
///
|
||||
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
|
||||
#[violation]
|
||||
pub struct BuiltinVariableShadowing {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for BuiltinVariableShadowing {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BuiltinVariableShadowing { name } = self;
|
||||
format!("Variable `{name}` is shadowing a Python builtin")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for any function arguments that use the same name as a builtin.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Reusing a builtin name for the name of an argument increases the
|
||||
/// difficulty of reading and maintaining the code, and can cause
|
||||
/// non-obvious errors, as readers may mistake the argument for the
|
||||
/// builtin and vice versa.
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `flake8-builtins.builtins-ignorelist`
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def remove_duplicates(list, list2):
|
||||
/// result = set()
|
||||
/// for value in list:
|
||||
/// result.add(value)
|
||||
/// for value in list2:
|
||||
/// result.add(value)
|
||||
/// return list(result) # TypeError: 'list' object is not callable
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def remove_duplicates(list1, list2):
|
||||
/// result = set()
|
||||
/// for value in list1:
|
||||
/// result.add(value)
|
||||
/// for value in list2:
|
||||
/// result.add(value)
|
||||
/// return list(result)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
|
||||
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
|
||||
#[violation]
|
||||
pub struct BuiltinArgumentShadowing {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for BuiltinArgumentShadowing {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BuiltinArgumentShadowing { name } = self;
|
||||
format!("Argument `{name}` is shadowing a Python builtin")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for any class attributes that use the same name as a builtin.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Reusing a builtin name for the name of an attribute increases the
|
||||
/// difficulty of reading and maintaining the code, and can cause
|
||||
/// non-obvious errors, as readers may mistake the attribute for the
|
||||
/// builtin and vice versa.
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option, or
|
||||
/// converted to the appropriate dunder method.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `flake8-builtins.builtins-ignorelist`
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Shadow:
|
||||
/// def int():
|
||||
/// return 0
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Shadow:
|
||||
/// def to_int():
|
||||
/// return 0
|
||||
/// ```
|
||||
///
|
||||
/// Or:
|
||||
/// ```python
|
||||
/// class Shadow:
|
||||
/// # Callable as `int(shadow)`
|
||||
/// def __int__():
|
||||
/// return 0
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
|
||||
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
|
||||
#[violation]
|
||||
pub struct BuiltinAttributeShadowing {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for BuiltinAttributeShadowing {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BuiltinAttributeShadowing { name } = self;
|
||||
format!("Class attribute `{name}` is shadowing a Python builtin")
|
||||
}
|
||||
}
|
||||
|
||||
fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool {
|
||||
BUILTINS.contains(&name) && ignorelist.iter().all(|ignore| ignore != name)
|
||||
}
|
||||
|
||||
/// A001
|
||||
pub(crate) fn builtin_variable_shadowing(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
shadowing: AnyShadowing,
|
||||
) {
|
||||
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinVariableShadowing {
|
||||
name: name.to_string(),
|
||||
},
|
||||
shadowing.range(checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// A002
|
||||
pub(crate) fn builtin_argument_shadowing(checker: &mut Checker, argument: &Arg) {
|
||||
if shadows_builtin(
|
||||
argument.arg.as_str(),
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinArgumentShadowing {
|
||||
name: argument.arg.to_string(),
|
||||
},
|
||||
argument.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// A003
|
||||
pub(crate) fn builtin_attribute_shadowing(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
shadowing: AnyShadowing,
|
||||
) {
|
||||
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinAttributeShadowing {
|
||||
name: name.to_string(),
|
||||
},
|
||||
shadowing.range(checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub(crate) enum AnyShadowing<'a> {
|
||||
Expression(&'a Expr),
|
||||
Statement(&'a Stmt),
|
||||
ExceptHandler(&'a Excepthandler),
|
||||
}
|
||||
|
||||
impl AnyShadowing<'_> {
|
||||
fn range(self, locator: &Locator) -> TextRange {
|
||||
match self {
|
||||
AnyShadowing::Expression(expr) => expr.range(),
|
||||
AnyShadowing::Statement(stmt) => identifier_range(stmt, locator),
|
||||
AnyShadowing::ExceptHandler(handler) => handler.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Stmt> for AnyShadowing<'a> {
|
||||
fn from(value: &'a Stmt) -> Self {
|
||||
AnyShadowing::Statement(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Expr> for AnyShadowing<'a> {
|
||||
fn from(value: &'a Expr) -> Self {
|
||||
AnyShadowing::Expression(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Excepthandler> for AnyShadowing<'a> {
|
||||
fn from(value: &'a Excepthandler) -> Self {
|
||||
AnyShadowing::ExceptHandler(value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
use rustpython_parser::ast::{Arg, Ranged};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::super::helpers::shadows_builtin;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for any function arguments that use the same name as a builtin.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Reusing a builtin name for the name of an argument increases the
|
||||
/// difficulty of reading and maintaining the code, and can cause
|
||||
/// non-obvious errors, as readers may mistake the argument for the
|
||||
/// builtin and vice versa.
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `flake8-builtins.builtins-ignorelist`
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def remove_duplicates(list, list2):
|
||||
/// result = set()
|
||||
/// for value in list:
|
||||
/// result.add(value)
|
||||
/// for value in list2:
|
||||
/// result.add(value)
|
||||
/// return list(result) # TypeError: 'list' object is not callable
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def remove_duplicates(list1, list2):
|
||||
/// result = set()
|
||||
/// for value in list1:
|
||||
/// result.add(value)
|
||||
/// for value in list2:
|
||||
/// result.add(value)
|
||||
/// return list(result)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
|
||||
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
|
||||
#[violation]
|
||||
pub struct BuiltinArgumentShadowing {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for BuiltinArgumentShadowing {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BuiltinArgumentShadowing { name } = self;
|
||||
format!("Argument `{name}` is shadowing a Python builtin")
|
||||
}
|
||||
}
|
||||
|
||||
/// A002
|
||||
pub(crate) fn builtin_argument_shadowing(checker: &mut Checker, argument: &Arg) {
|
||||
if shadows_builtin(
|
||||
argument.arg.as_str(),
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinArgumentShadowing {
|
||||
name: argument.arg.to_string(),
|
||||
},
|
||||
argument.range(),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::super::helpers::{shadows_builtin, AnyShadowing};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for any class attributes that use the same name as a builtin.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Reusing a builtin name for the name of an attribute increases the
|
||||
/// difficulty of reading and maintaining the code, and can cause
|
||||
/// non-obvious errors, as readers may mistake the attribute for the
|
||||
/// builtin and vice versa.
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option, or
|
||||
/// converted to the appropriate dunder method.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `flake8-builtins.builtins-ignorelist`
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Shadow:
|
||||
/// def int():
|
||||
/// return 0
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Shadow:
|
||||
/// def to_int():
|
||||
/// return 0
|
||||
/// ```
|
||||
///
|
||||
/// Or:
|
||||
/// ```python
|
||||
/// class Shadow:
|
||||
/// # Callable as `int(shadow)`
|
||||
/// def __int__():
|
||||
/// return 0
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
|
||||
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
|
||||
#[violation]
|
||||
pub struct BuiltinAttributeShadowing {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for BuiltinAttributeShadowing {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BuiltinAttributeShadowing { name } = self;
|
||||
format!("Class attribute `{name}` is shadowing a Python builtin")
|
||||
}
|
||||
}
|
||||
|
||||
/// A003
|
||||
pub(crate) fn builtin_attribute_shadowing(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
shadowing: AnyShadowing,
|
||||
) {
|
||||
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinAttributeShadowing {
|
||||
name: name.to_string(),
|
||||
},
|
||||
shadowing.range(checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::super::helpers::{shadows_builtin, AnyShadowing};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for variable (and function) assignments that use the same name
|
||||
/// as a builtin.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Reusing a builtin name for the name of a variable increases the
|
||||
/// difficulty of reading and maintaining the code, and can cause
|
||||
/// non-obvious errors, as readers may mistake the variable for the
|
||||
/// builtin and vice versa.
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `flake8-builtins.builtins-ignorelist`
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def find_max(list_of_lists):
|
||||
/// max = 0
|
||||
/// for flat_list in list_of_lists:
|
||||
/// for value in flat_list:
|
||||
/// max = max(max, value) # TypeError: 'int' object is not callable
|
||||
/// return max
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def find_max(list_of_lists):
|
||||
/// result = 0
|
||||
/// for flat_list in list_of_lists:
|
||||
/// for value in flat_list:
|
||||
/// result = max(result, value)
|
||||
/// return result
|
||||
/// ```
|
||||
///
|
||||
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
|
||||
#[violation]
|
||||
pub struct BuiltinVariableShadowing {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for BuiltinVariableShadowing {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BuiltinVariableShadowing { name } = self;
|
||||
format!("Variable `{name}` is shadowing a Python builtin")
|
||||
}
|
||||
}
|
||||
|
||||
/// A001
|
||||
pub(crate) fn builtin_variable_shadowing(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
shadowing: AnyShadowing,
|
||||
) {
|
||||
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinVariableShadowing {
|
||||
name: name.to_string(),
|
||||
},
|
||||
shadowing.range(checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
9
crates/ruff/src/rules/flake8_builtins/rules/mod.rs
Normal file
9
crates/ruff/src/rules/flake8_builtins/rules/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
pub(crate) use builtin_argument_shadowing::{builtin_argument_shadowing, BuiltinArgumentShadowing};
|
||||
pub(crate) use builtin_attribute_shadowing::{
|
||||
builtin_attribute_shadowing, BuiltinAttributeShadowing,
|
||||
};
|
||||
pub(crate) use builtin_variable_shadowing::{builtin_variable_shadowing, BuiltinVariableShadowing};
|
||||
|
||||
mod builtin_argument_shadowing;
|
||||
mod builtin_attribute_shadowing;
|
||||
mod builtin_variable_shadowing;
|
3
crates/ruff/src/rules/flake8_builtins/rules/rules.rs
Normal file
3
crates/ruff/src/rules/flake8_builtins/rules/rules.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
|
5
crates/ruff/src/rules/flake8_commas/rules/mod.rs
Normal file
5
crates/ruff/src/rules/flake8_commas/rules/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub(crate) use trailing_commas::{
|
||||
trailing_commas, MissingTrailingComma, ProhibitedTrailingComma, TrailingCommaOnBareTuple,
|
||||
};
|
||||
|
||||
mod trailing_commas;
|
|
@ -1,403 +0,0 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{self, Constant, Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeWithoutTzinfo;
|
||||
|
||||
impl Violation for CallDatetimeWithoutTzinfo {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("The use of `datetime.datetime()` without `tzinfo` argument is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeToday;
|
||||
|
||||
impl Violation for CallDatetimeToday {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.datetime.today()` is not allowed, use \
|
||||
`datetime.datetime.now(tz=)` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeUtcnow;
|
||||
|
||||
impl Violation for CallDatetimeUtcnow {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.datetime.utcnow()` is not allowed, use \
|
||||
`datetime.datetime.now(tz=)` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeUtcfromtimestamp;
|
||||
|
||||
impl Violation for CallDatetimeUtcfromtimestamp {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.datetime.utcfromtimestamp()` is not allowed, use \
|
||||
`datetime.datetime.fromtimestamp(ts, tz=)` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeNowWithoutTzinfo;
|
||||
|
||||
impl Violation for CallDatetimeNowWithoutTzinfo {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("The use of `datetime.datetime.now()` without `tz` argument is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeFromtimestamp;
|
||||
|
||||
impl Violation for CallDatetimeFromtimestamp {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.datetime.fromtimestamp()` without `tz` argument is not allowed"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeStrptimeWithoutZone;
|
||||
|
||||
impl Violation for CallDatetimeStrptimeWithoutZone {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.datetime.strptime()` without %z must be followed by \
|
||||
`.replace(tzinfo=)` or `.astimezone()`"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct CallDateToday;
|
||||
|
||||
impl Violation for CallDateToday {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.date.today()` is not allowed, use \
|
||||
`datetime.datetime.now(tz=).date()` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct CallDateFromtimestamp;
|
||||
|
||||
impl Violation for CallDateFromtimestamp {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.date.fromtimestamp()` is not allowed, use \
|
||||
`datetime.datetime.fromtimestamp(ts, tz=).date()` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call_datetime_without_tzinfo(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
location: TextRange,
|
||||
) {
|
||||
if !checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime"]
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// No positional arg: keyword is missing or constant None.
|
||||
if args.len() < 8 && !has_non_none_keyword(keywords, "tzinfo") {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeWithoutTzinfo, location));
|
||||
return;
|
||||
}
|
||||
|
||||
// Positional arg: is constant None.
|
||||
if args.len() >= 8 && is_const_none(&args[7]) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeWithoutTzinfo, location));
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for `datetime.datetime.today()`. (DTZ002)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// It uses the system local timezone.
|
||||
/// Use `datetime.datetime.now(tz=)` instead.
|
||||
pub(crate) fn call_datetime_today(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "today"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeToday, location));
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for `datetime.datetime.today()`. (DTZ003)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// Because naive `datetime` objects are treated by many `datetime` methods as
|
||||
/// local times, it is preferred to use aware datetimes to represent times in
|
||||
/// UTC. As such, the recommended way to create an object representing the
|
||||
/// current time in UTC is by calling `datetime.now(timezone.utc)`.
|
||||
pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "utcnow"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeUtcnow, location));
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for `datetime.datetime.utcfromtimestamp()`. (DTZ004)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// Because naive `datetime` objects are treated by many `datetime` methods as
|
||||
/// local times, it is preferred to use aware datetimes to represent times in
|
||||
/// UTC. As such, the recommended way to create an object representing a
|
||||
/// specific timestamp in UTC is by calling `datetime.fromtimestamp(timestamp,
|
||||
/// tz=timezone.utc)`.
|
||||
pub(crate) fn call_datetime_utcfromtimestamp(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
location: TextRange,
|
||||
) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "utcfromtimestamp"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeUtcfromtimestamp, location));
|
||||
}
|
||||
}
|
||||
|
||||
/// DTZ005
|
||||
pub(crate) fn call_datetime_now_without_tzinfo(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
location: TextRange,
|
||||
) {
|
||||
if !checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "now"]
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// no args / no args unqualified
|
||||
if args.is_empty() && keywords.is_empty() {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, location));
|
||||
return;
|
||||
}
|
||||
|
||||
// none args
|
||||
if !args.is_empty() && is_const_none(&args[0]) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, location));
|
||||
return;
|
||||
}
|
||||
|
||||
// wrong keywords / none keyword
|
||||
if !keywords.is_empty() && !has_non_none_keyword(keywords, "tz") {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, location));
|
||||
}
|
||||
}
|
||||
|
||||
/// DTZ006
|
||||
pub(crate) fn call_datetime_fromtimestamp(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
location: TextRange,
|
||||
) {
|
||||
if !checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "fromtimestamp"]
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// no args / no args unqualified
|
||||
if args.len() < 2 && keywords.is_empty() {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeFromtimestamp, location));
|
||||
return;
|
||||
}
|
||||
|
||||
// none args
|
||||
if args.len() > 1 && is_const_none(&args[1]) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeFromtimestamp, location));
|
||||
return;
|
||||
}
|
||||
|
||||
// wrong keywords / none keyword
|
||||
if !keywords.is_empty() && !has_non_none_keyword(keywords, "tz") {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeFromtimestamp, location));
|
||||
}
|
||||
}
|
||||
|
||||
/// DTZ007
|
||||
pub(crate) fn call_datetime_strptime_without_zone(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
location: TextRange,
|
||||
) {
|
||||
if !checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "strptime"]
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Does the `strptime` call contain a format string with a timezone specifier?
|
||||
if let Some(Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(format),
|
||||
kind: None,
|
||||
range: _,
|
||||
})) = args.get(1).as_ref()
|
||||
{
|
||||
if format.contains("%z") {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let (Some(grandparent), Some(parent)) = (checker.semantic_model().expr_grandparent(), checker.semantic_model().expr_parent()) else {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
CallDatetimeStrptimeWithoutZone,
|
||||
location,
|
||||
));
|
||||
return;
|
||||
};
|
||||
|
||||
if let Expr::Call(ast::ExprCall { keywords, .. }) = grandparent {
|
||||
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = parent {
|
||||
let attr = attr.as_str();
|
||||
// Ex) `datetime.strptime(...).astimezone()`
|
||||
if attr == "astimezone" {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ex) `datetime.strptime(...).replace(tzinfo=UTC)`
|
||||
if attr == "replace" {
|
||||
if has_non_none_keyword(keywords, "tzinfo") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeStrptimeWithoutZone, location));
|
||||
}
|
||||
|
||||
/// Checks for `datetime.date.today()`. (DTZ011)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// It uses the system local timezone.
|
||||
/// Use `datetime.datetime.now(tz=).date()` instead.
|
||||
pub(crate) fn call_date_today(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "date", "today"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDateToday, location));
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for `datetime.date.fromtimestamp()`. (DTZ012)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// It uses the system local timezone.
|
||||
/// Use `datetime.datetime.fromtimestamp(, tz=).date()` instead.
|
||||
pub(crate) fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "date", "fromtimestamp"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDateFromtimestamp, location));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct CallDateFromtimestamp;
|
||||
|
||||
impl Violation for CallDateFromtimestamp {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.date.fromtimestamp()` is not allowed, use \
|
||||
`datetime.datetime.fromtimestamp(ts, tz=).date()` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for `datetime.date.fromtimestamp()`. (DTZ012)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// It uses the system local timezone.
|
||||
/// Use `datetime.datetime.fromtimestamp(, tz=).date()` instead.
|
||||
pub(crate) fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "date", "fromtimestamp"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDateFromtimestamp, location));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct CallDateToday;
|
||||
|
||||
impl Violation for CallDateToday {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.date.today()` is not allowed, use \
|
||||
`datetime.datetime.now(tz=).date()` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for `datetime.date.today()`. (DTZ011)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// It uses the system local timezone.
|
||||
/// Use `datetime.datetime.now(tz=).date()` instead.
|
||||
pub(crate) fn call_date_today(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "date", "today"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDateToday, location));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeFromtimestamp;
|
||||
|
||||
impl Violation for CallDatetimeFromtimestamp {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.datetime.fromtimestamp()` without `tz` argument is not allowed"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// DTZ006
|
||||
pub(crate) fn call_datetime_fromtimestamp(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
location: TextRange,
|
||||
) {
|
||||
if !checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "fromtimestamp"]
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// no args / no args unqualified
|
||||
if args.len() < 2 && keywords.is_empty() {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeFromtimestamp, location));
|
||||
return;
|
||||
}
|
||||
|
||||
// none args
|
||||
if args.len() > 1 && is_const_none(&args[1]) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeFromtimestamp, location));
|
||||
return;
|
||||
}
|
||||
|
||||
// wrong keywords / none keyword
|
||||
if !keywords.is_empty() && !has_non_none_keyword(keywords, "tz") {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeFromtimestamp, location));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeNowWithoutTzinfo;
|
||||
|
||||
impl Violation for CallDatetimeNowWithoutTzinfo {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("The use of `datetime.datetime.now()` without `tz` argument is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
/// DTZ005
|
||||
pub(crate) fn call_datetime_now_without_tzinfo(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
location: TextRange,
|
||||
) {
|
||||
if !checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "now"]
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// no args / no args unqualified
|
||||
if args.is_empty() && keywords.is_empty() {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, location));
|
||||
return;
|
||||
}
|
||||
|
||||
// none args
|
||||
if !args.is_empty() && is_const_none(&args[0]) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, location));
|
||||
return;
|
||||
}
|
||||
|
||||
// wrong keywords / none keyword
|
||||
if !keywords.is_empty() && !has_non_none_keyword(keywords, "tz") {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeNowWithoutTzinfo, location));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{self, Constant, Expr};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::has_non_none_keyword;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeStrptimeWithoutZone;
|
||||
|
||||
impl Violation for CallDatetimeStrptimeWithoutZone {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.datetime.strptime()` without %z must be followed by \
|
||||
`.replace(tzinfo=)` or `.astimezone()`"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// DTZ007
|
||||
pub(crate) fn call_datetime_strptime_without_zone(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
location: TextRange,
|
||||
) {
|
||||
if !checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "strptime"]
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Does the `strptime` call contain a format string with a timezone specifier?
|
||||
if let Some(Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(format),
|
||||
kind: None,
|
||||
range: _,
|
||||
})) = args.get(1).as_ref()
|
||||
{
|
||||
if format.contains("%z") {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let (Some(grandparent), Some(parent)) = (checker.semantic_model().expr_grandparent(), checker.semantic_model().expr_parent()) else {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
CallDatetimeStrptimeWithoutZone,
|
||||
location,
|
||||
));
|
||||
return;
|
||||
};
|
||||
|
||||
if let Expr::Call(ast::ExprCall { keywords, .. }) = grandparent {
|
||||
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = parent {
|
||||
let attr = attr.as_str();
|
||||
// Ex) `datetime.strptime(...).astimezone()`
|
||||
if attr == "astimezone" {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ex) `datetime.strptime(...).replace(tzinfo=UTC)`
|
||||
if attr == "replace" {
|
||||
if has_non_none_keyword(keywords, "tzinfo") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeStrptimeWithoutZone, location));
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeToday;
|
||||
|
||||
impl Violation for CallDatetimeToday {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.datetime.today()` is not allowed, use \
|
||||
`datetime.datetime.now(tz=)` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for `datetime.datetime.today()`. (DTZ002)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// It uses the system local timezone.
|
||||
/// Use `datetime.datetime.now(tz=)` instead.
|
||||
pub(crate) fn call_datetime_today(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "today"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeToday, location));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeUtcfromtimestamp;
|
||||
|
||||
impl Violation for CallDatetimeUtcfromtimestamp {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.datetime.utcfromtimestamp()` is not allowed, use \
|
||||
`datetime.datetime.fromtimestamp(ts, tz=)` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for `datetime.datetime.utcfromtimestamp()`. (DTZ004)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// Because naive `datetime` objects are treated by many `datetime` methods as
|
||||
/// local times, it is preferred to use aware datetimes to represent times in
|
||||
/// UTC. As such, the recommended way to create an object representing a
|
||||
/// specific timestamp in UTC is by calling `datetime.fromtimestamp(timestamp,
|
||||
/// tz=timezone.utc)`.
|
||||
pub(crate) fn call_datetime_utcfromtimestamp(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
location: TextRange,
|
||||
) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "utcfromtimestamp"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeUtcfromtimestamp, location));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeUtcnow;
|
||||
|
||||
impl Violation for CallDatetimeUtcnow {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"The use of `datetime.datetime.utcnow()` is not allowed, use \
|
||||
`datetime.datetime.now(tz=)` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for `datetime.datetime.today()`. (DTZ003)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// Because naive `datetime` objects are treated by many `datetime` methods as
|
||||
/// local times, it is preferred to use aware datetimes to represent times in
|
||||
/// UTC. As such, the recommended way to create an object representing the
|
||||
/// current time in UTC is by calling `datetime.now(timezone.utc)`.
|
||||
pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime", "utcnow"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeUtcnow, location));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct CallDatetimeWithoutTzinfo;
|
||||
|
||||
impl Violation for CallDatetimeWithoutTzinfo {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("The use of `datetime.datetime()` without `tzinfo` argument is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call_datetime_without_tzinfo(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
location: TextRange,
|
||||
) {
|
||||
if !checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["datetime", "datetime"]
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// No positional arg: keyword is missing or constant None.
|
||||
if args.len() < 8 && !has_non_none_keyword(keywords, "tzinfo") {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeWithoutTzinfo, location));
|
||||
return;
|
||||
}
|
||||
|
||||
// Positional arg: is constant None.
|
||||
if args.len() >= 8 && is_const_none(&args[7]) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(CallDatetimeWithoutTzinfo, location));
|
||||
}
|
||||
}
|
29
crates/ruff/src/rules/flake8_datetimez/rules/mod.rs
Normal file
29
crates/ruff/src/rules/flake8_datetimez/rules/mod.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
pub(crate) use call_date_fromtimestamp::{call_date_fromtimestamp, CallDateFromtimestamp};
|
||||
pub(crate) use call_date_today::{call_date_today, CallDateToday};
|
||||
pub(crate) use call_datetime_fromtimestamp::{
|
||||
call_datetime_fromtimestamp, CallDatetimeFromtimestamp,
|
||||
};
|
||||
pub(crate) use call_datetime_now_without_tzinfo::{
|
||||
call_datetime_now_without_tzinfo, CallDatetimeNowWithoutTzinfo,
|
||||
};
|
||||
pub(crate) use call_datetime_strptime_without_zone::{
|
||||
call_datetime_strptime_without_zone, CallDatetimeStrptimeWithoutZone,
|
||||
};
|
||||
pub(crate) use call_datetime_today::{call_datetime_today, CallDatetimeToday};
|
||||
pub(crate) use call_datetime_utcfromtimestamp::{
|
||||
call_datetime_utcfromtimestamp, CallDatetimeUtcfromtimestamp,
|
||||
};
|
||||
pub(crate) use call_datetime_utcnow::{call_datetime_utcnow, CallDatetimeUtcnow};
|
||||
pub(crate) use call_datetime_without_tzinfo::{
|
||||
call_datetime_without_tzinfo, CallDatetimeWithoutTzinfo,
|
||||
};
|
||||
|
||||
mod call_date_fromtimestamp;
|
||||
mod call_date_today;
|
||||
mod call_datetime_fromtimestamp;
|
||||
mod call_datetime_now_without_tzinfo;
|
||||
mod call_datetime_strptime_without_zone;
|
||||
mod call_datetime_today;
|
||||
mod call_datetime_utcfromtimestamp;
|
||||
mod call_datetime_utcnow;
|
||||
mod call_datetime_without_tzinfo;
|
3
crates/ruff/src/rules/flake8_debugger/rules/mod.rs
Normal file
3
crates/ruff/src/rules/flake8_debugger/rules/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub(crate) use debugger::{debugger_call, debugger_import, Debugger};
|
||||
|
||||
mod debugger;
|
5
crates/ruff/src/rules/flake8_errmsg/rules/mod.rs
Normal file
5
crates/ruff/src/rules/flake8_errmsg/rules/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub(crate) use string_in_exception::{
|
||||
string_in_exception, DotFormatInException, FStringInException, RawStringInException,
|
||||
};
|
||||
|
||||
mod string_in_exception;
|
|
@ -0,0 +1,5 @@
|
|||
pub(crate) use missing_future_annotations::{
|
||||
missing_future_annotations, MissingFutureAnnotationsImport,
|
||||
};
|
||||
|
||||
mod missing_future_annotations;
|
|
@ -1,87 +0,0 @@
|
|||
use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
#[violation]
|
||||
pub struct FStringInGetTextFuncCall;
|
||||
|
||||
impl Violation for FStringInGetTextFuncCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("f-string is resolved before function call; consider `_(\"string %s\") % arg`")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct FormatInGetTextFuncCall;
|
||||
|
||||
impl Violation for FormatInGetTextFuncCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`format` method argument is resolved before function call; consider `_(\"string %s\") % arg`")
|
||||
}
|
||||
}
|
||||
#[violation]
|
||||
pub struct PrintfInGetTextFuncCall;
|
||||
|
||||
impl Violation for PrintfInGetTextFuncCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("printf-style format is resolved before function call; consider `_(\"string %s\") % arg`")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the [`Expr`] is an internationalization function call.
|
||||
pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = func {
|
||||
functions_names.contains(id.as_ref())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// INT001
|
||||
pub(crate) fn f_string_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
|
||||
if let Some(first) = args.first() {
|
||||
if first.is_joined_str_expr() {
|
||||
return Some(Diagnostic::new(FStringInGetTextFuncCall {}, first.range()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// INT002
|
||||
pub(crate) fn format_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
|
||||
if let Some(first) = args.first() {
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = &first {
|
||||
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() {
|
||||
if attr == "format" {
|
||||
return Some(Diagnostic::new(FormatInGetTextFuncCall {}, first.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// INT003
|
||||
pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
|
||||
if let Some(first) = args.first() {
|
||||
if let Expr::BinOp(ast::ExprBinOp {
|
||||
op: Operator::Mod { .. },
|
||||
left,
|
||||
..
|
||||
}) = &first
|
||||
{
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(_),
|
||||
..
|
||||
}) = left.as_ref()
|
||||
{
|
||||
return Some(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
#[violation]
|
||||
pub struct FStringInGetTextFuncCall;
|
||||
|
||||
impl Violation for FStringInGetTextFuncCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("f-string is resolved before function call; consider `_(\"string %s\") % arg`")
|
||||
}
|
||||
}
|
||||
|
||||
/// INT001
|
||||
pub(crate) fn f_string_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
|
||||
if let Some(first) = args.first() {
|
||||
if first.is_joined_str_expr() {
|
||||
return Some(Diagnostic::new(FStringInGetTextFuncCall {}, first.range()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
use rustpython_parser::ast::{self, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
#[violation]
|
||||
pub struct FormatInGetTextFuncCall;
|
||||
|
||||
impl Violation for FormatInGetTextFuncCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`format` method argument is resolved before function call; consider `_(\"string %s\") % arg`")
|
||||
}
|
||||
}
|
||||
|
||||
/// INT002
|
||||
pub(crate) fn format_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
|
||||
if let Some(first) = args.first() {
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = &first {
|
||||
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() {
|
||||
if attr == "format" {
|
||||
return Some(Diagnostic::new(FormatInGetTextFuncCall {}, first.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
use rustpython_parser::ast::{self, Expr};
|
||||
|
||||
/// Returns true if the [`Expr`] is an internationalization function call.
|
||||
pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = func {
|
||||
functions_names.contains(id.as_ref())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
15
crates/ruff/src/rules/flake8_gettext/rules/mod.rs
Normal file
15
crates/ruff/src/rules/flake8_gettext/rules/mod.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
pub(crate) use f_string_in_gettext_func_call::{
|
||||
f_string_in_gettext_func_call, FStringInGetTextFuncCall,
|
||||
};
|
||||
pub(crate) use format_in_gettext_func_call::{
|
||||
format_in_gettext_func_call, FormatInGetTextFuncCall,
|
||||
};
|
||||
pub(crate) use is_gettext_func_call::is_gettext_func_call;
|
||||
pub(crate) use printf_in_gettext_func_call::{
|
||||
printf_in_gettext_func_call, PrintfInGetTextFuncCall,
|
||||
};
|
||||
|
||||
mod f_string_in_gettext_func_call;
|
||||
mod format_in_gettext_func_call;
|
||||
mod is_gettext_func_call;
|
||||
mod printf_in_gettext_func_call;
|
|
@ -0,0 +1,35 @@
|
|||
use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
#[violation]
|
||||
pub struct PrintfInGetTextFuncCall;
|
||||
|
||||
impl Violation for PrintfInGetTextFuncCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("printf-style format is resolved before function call; consider `_(\"string %s\") % arg`")
|
||||
}
|
||||
}
|
||||
|
||||
/// INT003
|
||||
pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
|
||||
if let Some(first) = args.first() {
|
||||
if let Expr::BinOp(ast::ExprBinOp {
|
||||
op: Operator::Mod { .. },
|
||||
left,
|
||||
..
|
||||
}) = &first
|
||||
{
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(_),
|
||||
..
|
||||
}) = left.as_ref()
|
||||
{
|
||||
return Some(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for string literals that are explicitly concatenated (using the
|
||||
/// `+` operator).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// For string literals that wrap across multiple lines, implicit string
|
||||
/// concatenation within parentheses is preferred over explicit
|
||||
/// concatenation using the `+` operator, as the former is more readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// z = (
|
||||
/// "The quick brown fox jumps over the lazy "
|
||||
/// + "dog"
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// z = (
|
||||
/// "The quick brown fox jumps over the lazy "
|
||||
/// "dog"
|
||||
/// )
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct ExplicitStringConcatenation;
|
||||
|
||||
impl Violation for ExplicitStringConcatenation {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Explicitly concatenated string should be implicitly concatenated")
|
||||
}
|
||||
}
|
||||
|
||||
/// ISC003
|
||||
pub(crate) fn explicit(expr: &Expr) -> Option<Diagnostic> {
|
||||
if let Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op,
|
||||
right,
|
||||
range: _,
|
||||
}) = expr
|
||||
{
|
||||
if matches!(op, Operator::Add) {
|
||||
if matches!(
|
||||
left.as_ref(),
|
||||
Expr::JoinedStr(_)
|
||||
| Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(..) | Constant::Bytes(..),
|
||||
..
|
||||
})
|
||||
) && matches!(
|
||||
right.as_ref(),
|
||||
Expr::JoinedStr(_)
|
||||
| Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(..) | Constant::Bytes(..),
|
||||
..
|
||||
})
|
||||
) {
|
||||
return Some(Diagnostic::new(ExplicitStringConcatenation, expr.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
use itertools::Itertools;
|
||||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged};
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use rustpython_parser::Tok;
|
||||
|
||||
|
@ -84,40 +83,6 @@ impl Violation for MultiLineImplicitStringConcatenation {
|
|||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for string literals that are explicitly concatenated (using the
|
||||
/// `+` operator).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// For string literals that wrap across multiple lines, implicit string
|
||||
/// concatenation within parentheses is preferred over explicit
|
||||
/// concatenation using the `+` operator, as the former is more readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// z = (
|
||||
/// "The quick brown fox jumps over the lazy "
|
||||
/// + "dog"
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// z = (
|
||||
/// "The quick brown fox jumps over the lazy "
|
||||
/// "dog"
|
||||
/// )
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct ExplicitStringConcatenation;
|
||||
|
||||
impl Violation for ExplicitStringConcatenation {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Explicitly concatenated string should be implicitly concatenated")
|
||||
}
|
||||
}
|
||||
|
||||
/// ISC001, ISC002
|
||||
pub(crate) fn implicit(
|
||||
tokens: &[LexResult],
|
||||
|
@ -150,35 +115,3 @@ pub(crate) fn implicit(
|
|||
}
|
||||
diagnostics
|
||||
}
|
||||
|
||||
/// ISC003
|
||||
pub(crate) fn explicit(expr: &Expr) -> Option<Diagnostic> {
|
||||
if let Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op,
|
||||
right,
|
||||
range: _,
|
||||
}) = expr
|
||||
{
|
||||
if matches!(op, Operator::Add) {
|
||||
if matches!(
|
||||
left.as_ref(),
|
||||
Expr::JoinedStr(_)
|
||||
| Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(..) | Constant::Bytes(..),
|
||||
..
|
||||
})
|
||||
) && matches!(
|
||||
right.as_ref(),
|
||||
Expr::JoinedStr(_)
|
||||
| Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(..) | Constant::Bytes(..),
|
||||
..
|
||||
})
|
||||
) {
|
||||
return Some(Diagnostic::new(ExplicitStringConcatenation, expr.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
pub(crate) use explicit::{explicit, ExplicitStringConcatenation};
|
||||
pub(crate) use implicit::{
|
||||
implicit, MultiLineImplicitStringConcatenation, SingleLineImplicitStringConcatenation,
|
||||
};
|
||||
|
||||
mod explicit;
|
||||
mod implicit;
|
3
crates/ruff/src/rules/flake8_logging_format/rules/mod.rs
Normal file
3
crates/ruff/src/rules/flake8_logging_format/rules/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub(crate) use logging_call::logging_call;
|
||||
|
||||
mod logging_call;
|
3
crates/ruff/src/rules/flake8_no_pep420/rules/mod.rs
Normal file
3
crates/ruff/src/rules/flake8_no_pep420/rules/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub(crate) use implicit_namespace_package::{implicit_namespace_package, ImplicitNamespacePackage};
|
||||
|
||||
mod implicit_namespace_package;
|
|
@ -1,652 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::iter;
|
||||
|
||||
use itertools::Either::{Left, Right};
|
||||
use log::error;
|
||||
use ruff_text_size::TextRange;
|
||||
use rustc_hash::FxHashSet;
|
||||
use rustpython_parser::ast::{
|
||||
self, Boolop, Constant, Expr, ExprContext, ExprLambda, Keyword, Ranged, Stmt,
|
||||
};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::helpers::trailing_comment_start_offset;
|
||||
use ruff_python_ast::types::RefEquality;
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
|
||||
use crate::autofix::actions::delete_stmt;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary `pass` statements in class and function bodies.
|
||||
/// where it is not needed syntactically (e.g., when an indented docstring is
|
||||
/// present).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// When a function or class definition contains a docstring, an additional
|
||||
/// `pass` statement is redundant.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// """Placeholder docstring."""
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// """Placeholder docstring."""
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-pass-statement)
|
||||
#[violation]
|
||||
pub struct UnnecessaryPass;
|
||||
|
||||
impl AlwaysAutofixableViolation for UnnecessaryPass {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Unnecessary `pass` statement")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
"Remove unnecessary `pass`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for duplicate field definitions in classes.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Defining a field multiple times in a class body is redundant and likely a
|
||||
/// mistake.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Person:
|
||||
/// name = Tom
|
||||
/// ...
|
||||
/// name = Ben
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Person:
|
||||
/// name = Tom
|
||||
/// ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct DuplicateClassFieldDefinition(pub String);
|
||||
|
||||
impl AlwaysAutofixableViolation for DuplicateClassFieldDefinition {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let DuplicateClassFieldDefinition(name) = self;
|
||||
format!("Class field `{name}` is defined multiple times")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
let DuplicateClassFieldDefinition(name) = self;
|
||||
format!("Remove duplicate field definition for `{name}`")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for enums that contain duplicate values.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Enum values should be unique. Non-unique values are redundant and likely a
|
||||
/// mistake.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from enum import Enum
|
||||
///
|
||||
///
|
||||
/// class Foo(Enum):
|
||||
/// A = 1
|
||||
/// B = 2
|
||||
/// C = 1
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from enum import Enum
|
||||
///
|
||||
///
|
||||
/// class Foo(Enum):
|
||||
/// A = 1
|
||||
/// B = 2
|
||||
/// C = 3
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/library/enum.html#enum.Enum)
|
||||
#[violation]
|
||||
pub struct NonUniqueEnums {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Violation for NonUniqueEnums {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let NonUniqueEnums { value } = self;
|
||||
format!("Enum contains duplicate value: `{value}`")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary dictionary unpacking operators (`**`).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Unpacking a dictionary into another dictionary is redundant. The unpacking
|
||||
/// operator can be removed, making the code more readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// foo = {"A": 1, "B": 2}
|
||||
/// bar = {**foo, **{"C": 3}}
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// foo = {"A": 1, "B": 2}
|
||||
/// bar = {**foo, "C": 3}
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#dictionary-displays)
|
||||
#[violation]
|
||||
pub struct UnnecessarySpread;
|
||||
|
||||
impl Violation for UnnecessarySpread {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Unnecessary spread `**`")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `startswith` or `endswith` calls on the same value with
|
||||
/// different prefixes or suffixes.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `startswith` and `endswith` methods accept tuples of prefixes or
|
||||
/// suffixes respectively. Passing a tuple of prefixes or suffixes is more
|
||||
/// more efficient and readable than calling the method multiple times.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// msg = "Hello, world!"
|
||||
/// if msg.startswith("Hello") or msg.startswith("Hi"):
|
||||
/// print("Greetings!")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// msg = "Hello, world!"
|
||||
/// if msg.startswith(("Hello", "Hi")):
|
||||
/// print("Greetings!")
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.startswith)
|
||||
/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.endswith)
|
||||
#[violation]
|
||||
pub struct MultipleStartsEndsWith {
|
||||
attr: String,
|
||||
}
|
||||
|
||||
impl AlwaysAutofixableViolation for MultipleStartsEndsWith {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let MultipleStartsEndsWith { attr } = self;
|
||||
format!("Call `{attr}` once with a `tuple`")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
let MultipleStartsEndsWith { attr } = self;
|
||||
format!("Merge into a single `{attr}` call")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary `dict` kwargs.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// If the `dict` keys are valid identifiers, they can be passed as keyword
|
||||
/// arguments directly.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def foo(bar):
|
||||
/// return bar + 1
|
||||
///
|
||||
///
|
||||
/// print(foo(**{"bar": 2})) # prints 3
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo(bar):
|
||||
/// return bar + 1
|
||||
///
|
||||
///
|
||||
/// print(foo(bar=2)) # prints 3
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#dictionary-displays)
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
#[violation]
|
||||
pub struct UnnecessaryDictKwargs;
|
||||
|
||||
impl Violation for UnnecessaryDictKwargs {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Unnecessary `dict` kwargs")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for lambdas that can be replaced with the `list` builtin.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using `list` builtin is more readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from dataclasses import dataclass, field
|
||||
///
|
||||
///
|
||||
/// @dataclass
|
||||
/// class Foo:
|
||||
/// bar: list[int] = field(default_factory=lambda: [])
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from dataclasses import dataclass, field
|
||||
///
|
||||
///
|
||||
/// @dataclass
|
||||
/// class Foo:
|
||||
/// bar: list[int] = field(default_factory=list)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/library/functions.html#func-list)
|
||||
#[violation]
|
||||
pub struct ReimplementedListBuiltin;
|
||||
|
||||
impl AlwaysAutofixableViolation for ReimplementedListBuiltin {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Prefer `list` over useless lambda")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
"Replace with `list`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE790
|
||||
pub(crate) fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) {
|
||||
if body.len() > 1 {
|
||||
// This only catches the case in which a docstring makes a `pass` statement
|
||||
// redundant. Consider removing all `pass` statements instead.
|
||||
let docstring_stmt = &body[0];
|
||||
let pass_stmt = &body[1];
|
||||
let Stmt::Expr(ast::StmtExpr { value, range: _ } )= docstring_stmt else {
|
||||
return;
|
||||
};
|
||||
if matches!(
|
||||
value.as_ref(),
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(..),
|
||||
..
|
||||
})
|
||||
) {
|
||||
if pass_stmt.is_pass_stmt() {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryPass, pass_stmt.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(index) = trailing_comment_start_offset(pass_stmt, checker.locator) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(
|
||||
pass_stmt.range().add_end(index),
|
||||
)));
|
||||
} else {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
delete_stmt(
|
||||
pass_stmt,
|
||||
None,
|
||||
&[],
|
||||
checker.locator,
|
||||
checker.indexer,
|
||||
checker.stylist,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE794
|
||||
pub(crate) fn duplicate_class_field_definition<'a, 'b>(
|
||||
checker: &mut Checker<'a>,
|
||||
parent: &'b Stmt,
|
||||
body: &'b [Stmt],
|
||||
) where
|
||||
'b: 'a,
|
||||
{
|
||||
let mut seen_targets: FxHashSet<&str> = FxHashSet::default();
|
||||
for stmt in body {
|
||||
// Extract the property name from the assignment statement.
|
||||
let target = match stmt {
|
||||
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
|
||||
if targets.len() != 1 {
|
||||
continue;
|
||||
}
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = &targets[0] {
|
||||
id
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
|
||||
id
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if !seen_targets.insert(target) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DuplicateClassFieldDefinition(target.to_string()),
|
||||
stmt.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
|
||||
let locator = checker.locator;
|
||||
match delete_stmt(
|
||||
stmt,
|
||||
Some(parent),
|
||||
&deleted,
|
||||
locator,
|
||||
checker.indexer,
|
||||
checker.stylist,
|
||||
) {
|
||||
Ok(fix) => {
|
||||
checker.deletions.insert(RefEquality(stmt));
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix_from_edit(fix);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to remove duplicate class definition: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE796
|
||||
pub(crate) fn non_unique_enums<'a, 'b>(
|
||||
checker: &mut Checker<'a>,
|
||||
parent: &'b Stmt,
|
||||
body: &'b [Stmt],
|
||||
) where
|
||||
'b: 'a,
|
||||
{
|
||||
let Stmt::ClassDef(ast::StmtClassDef { bases, .. }) = parent else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !bases.iter().any(|expr| {
|
||||
checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(expr)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["enum", "Enum"])
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut seen_targets: FxHashSet<ComparableExpr> = FxHashSet::default();
|
||||
for stmt in body {
|
||||
let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["enum", "auto"])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if !seen_targets.insert(ComparableExpr::from(value)) {
|
||||
let diagnostic = Diagnostic::new(
|
||||
NonUniqueEnums {
|
||||
value: checker.generator().expr(value),
|
||||
},
|
||||
stmt.range(),
|
||||
);
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE800
|
||||
pub(crate) fn unnecessary_spread(checker: &mut Checker, keys: &[Option<Expr>], values: &[Expr]) {
|
||||
for item in keys.iter().zip(values.iter()) {
|
||||
if let (None, value) = item {
|
||||
// We only care about when the key is None which indicates a spread `**`
|
||||
// inside a dict.
|
||||
if let Expr::Dict(_) = value {
|
||||
let diagnostic = Diagnostic::new(UnnecessarySpread, value.range());
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if a key is a valid keyword argument name.
|
||||
fn is_valid_kwarg_name(key: &Expr) -> bool {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
}) = key
|
||||
{
|
||||
is_identifier(value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE804
|
||||
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[Keyword]) {
|
||||
for kw in kwargs {
|
||||
// keyword is a spread operator (indicated by None)
|
||||
if kw.arg.is_none() {
|
||||
if let Expr::Dict(ast::ExprDict { keys, .. }) = &kw.value {
|
||||
// ensure foo(**{"bar-bar": 1}) doesn't error
|
||||
if keys.iter().all(|expr| expr.as_ref().map_or(false, is_valid_kwarg_name)) ||
|
||||
// handle case of foo(**{**bar})
|
||||
(keys.len() == 1 && keys[0].is_none())
|
||||
{
|
||||
let diagnostic = Diagnostic::new(UnnecessaryDictKwargs, expr.range());
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE810
|
||||
pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
|
||||
let Expr::BoolOp(ast::ExprBoolOp { op: Boolop::Or, values, range: _ }) = expr else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut duplicates = BTreeMap::new();
|
||||
for (index, call) in values.iter().enumerate() {
|
||||
let Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
range: _
|
||||
}) = &call else {
|
||||
continue
|
||||
};
|
||||
|
||||
if !(args.len() == 1 && keywords.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func.as_ref() else {
|
||||
continue
|
||||
};
|
||||
if attr != "startswith" && attr != "endswith" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Expr::Name(ast::ExprName { id: arg_name, .. } )= value.as_ref() else {
|
||||
continue
|
||||
};
|
||||
|
||||
duplicates
|
||||
.entry((attr.as_str(), arg_name.as_str()))
|
||||
.or_insert_with(Vec::new)
|
||||
.push(index);
|
||||
}
|
||||
|
||||
// Generate a `Diagnostic` for each duplicate.
|
||||
for ((attr_name, arg_name), indices) in duplicates {
|
||||
if indices.len() > 1 {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MultipleStartsEndsWith {
|
||||
attr: attr_name.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let words: Vec<&Expr> = indices
|
||||
.iter()
|
||||
.map(|index| &values[*index])
|
||||
.map(|expr| {
|
||||
let Expr::Call(ast::ExprCall { func: _, args, keywords: _, range: _}) = expr else {
|
||||
unreachable!("{}", format!("Indices should only contain `{attr_name}` calls"))
|
||||
};
|
||||
args.get(0)
|
||||
.unwrap_or_else(|| panic!("`{attr_name}` should have one argument"))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let node = Expr::Tuple(ast::ExprTuple {
|
||||
elts: words
|
||||
.iter()
|
||||
.flat_map(|value| {
|
||||
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = value {
|
||||
Left(elts.iter())
|
||||
} else {
|
||||
Right(iter::once(*value))
|
||||
}
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let node1 = Expr::Name(ast::ExprName {
|
||||
id: arg_name.into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let node2 = Expr::Attribute(ast::ExprAttribute {
|
||||
value: Box::new(node1),
|
||||
attr: attr_name.into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let node3 = Expr::Call(ast::ExprCall {
|
||||
func: Box::new(node2),
|
||||
args: vec![node],
|
||||
keywords: vec![],
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let call = node3;
|
||||
|
||||
// Generate the combined `BoolOp`.
|
||||
let mut call = Some(call);
|
||||
let node = Expr::BoolOp(ast::ExprBoolOp {
|
||||
op: Boolop::Or,
|
||||
values: values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, elt)| {
|
||||
if indices.contains(&index) {
|
||||
std::mem::take(&mut call)
|
||||
} else {
|
||||
Some(elt.clone())
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let bool_op = node;
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
checker.generator().expr(&bool_op),
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE807
|
||||
pub(crate) fn reimplemented_list_builtin(checker: &mut Checker, expr: &ExprLambda) {
|
||||
let ExprLambda {
|
||||
args,
|
||||
body,
|
||||
range: _,
|
||||
} = expr;
|
||||
|
||||
if args.args.is_empty()
|
||||
&& args.kwonlyargs.is_empty()
|
||||
&& args.posonlyargs.is_empty()
|
||||
&& args.vararg.is_none()
|
||||
&& args.kwarg.is_none()
|
||||
{
|
||||
if let Expr::List(ast::ExprList { elts, .. }) = body.as_ref() {
|
||||
if elts.is_empty() {
|
||||
let mut diagnostic = Diagnostic::new(ReimplementedListBuiltin, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
"list".to_string(),
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
use log::error;
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
use rustpython_parser::ast::{self, Expr, Ranged, Stmt};
|
||||
|
||||
use ruff_diagnostics::AlwaysAutofixableViolation;
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::types::RefEquality;
|
||||
|
||||
use crate::autofix::actions::delete_stmt;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for duplicate field definitions in classes.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Defining a field multiple times in a class body is redundant and likely a
|
||||
/// mistake.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Person:
|
||||
/// name = Tom
|
||||
/// ...
|
||||
/// name = Ben
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Person:
|
||||
/// name = Tom
|
||||
/// ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct DuplicateClassFieldDefinition(pub String);
|
||||
|
||||
impl AlwaysAutofixableViolation for DuplicateClassFieldDefinition {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let DuplicateClassFieldDefinition(name) = self;
|
||||
format!("Class field `{name}` is defined multiple times")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
let DuplicateClassFieldDefinition(name) = self;
|
||||
format!("Remove duplicate field definition for `{name}`")
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE794
|
||||
pub(crate) fn duplicate_class_field_definition<'a, 'b>(
|
||||
checker: &mut Checker<'a>,
|
||||
parent: &'b Stmt,
|
||||
body: &'b [Stmt],
|
||||
) where
|
||||
'b: 'a,
|
||||
{
|
||||
let mut seen_targets: FxHashSet<&str> = FxHashSet::default();
|
||||
for stmt in body {
|
||||
// Extract the property name from the assignment statement.
|
||||
let target = match stmt {
|
||||
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
|
||||
if targets.len() != 1 {
|
||||
continue;
|
||||
}
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = &targets[0] {
|
||||
id
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
|
||||
id
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if !seen_targets.insert(target) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DuplicateClassFieldDefinition(target.to_string()),
|
||||
stmt.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
|
||||
let locator = checker.locator;
|
||||
match delete_stmt(
|
||||
stmt,
|
||||
Some(parent),
|
||||
&deleted,
|
||||
locator,
|
||||
checker.indexer,
|
||||
checker.stylist,
|
||||
) {
|
||||
Ok(fix) => {
|
||||
checker.deletions.insert(RefEquality(stmt));
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix_from_edit(fix);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to remove duplicate class definition: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
17
crates/ruff/src/rules/flake8_pie/rules/mod.rs
Normal file
17
crates/ruff/src/rules/flake8_pie/rules/mod.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
pub(crate) use duplicate_class_field_definition::{
|
||||
duplicate_class_field_definition, DuplicateClassFieldDefinition,
|
||||
};
|
||||
pub(crate) use multiple_starts_ends_with::{multiple_starts_ends_with, MultipleStartsEndsWith};
|
||||
pub(crate) use no_unnecessary_pass::{no_unnecessary_pass, UnnecessaryPass};
|
||||
pub(crate) use non_unique_enums::{non_unique_enums, NonUniqueEnums};
|
||||
pub(crate) use reimplemented_list_builtin::{reimplemented_list_builtin, ReimplementedListBuiltin};
|
||||
pub(crate) use unnecessary_dict_kwargs::{unnecessary_dict_kwargs, UnnecessaryDictKwargs};
|
||||
pub(crate) use unnecessary_spread::{unnecessary_spread, UnnecessarySpread};
|
||||
|
||||
mod duplicate_class_field_definition;
|
||||
mod multiple_starts_ends_with;
|
||||
mod no_unnecessary_pass;
|
||||
mod non_unique_enums;
|
||||
mod reimplemented_list_builtin;
|
||||
mod unnecessary_dict_kwargs;
|
||||
mod unnecessary_spread;
|
|
@ -0,0 +1,182 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::iter;
|
||||
|
||||
use itertools::Either::{Left, Right};
|
||||
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use rustpython_parser::ast::{self, Boolop, Expr, ExprContext, Ranged};
|
||||
|
||||
use ruff_diagnostics::AlwaysAutofixableViolation;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `startswith` or `endswith` calls on the same value with
|
||||
/// different prefixes or suffixes.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `startswith` and `endswith` methods accept tuples of prefixes or
|
||||
/// suffixes respectively. Passing a tuple of prefixes or suffixes is more
|
||||
/// more efficient and readable than calling the method multiple times.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// msg = "Hello, world!"
|
||||
/// if msg.startswith("Hello") or msg.startswith("Hi"):
|
||||
/// print("Greetings!")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// msg = "Hello, world!"
|
||||
/// if msg.startswith(("Hello", "Hi")):
|
||||
/// print("Greetings!")
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.startswith)
|
||||
/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.endswith)
|
||||
#[violation]
|
||||
pub struct MultipleStartsEndsWith {
|
||||
attr: String,
|
||||
}
|
||||
|
||||
impl AlwaysAutofixableViolation for MultipleStartsEndsWith {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let MultipleStartsEndsWith { attr } = self;
|
||||
format!("Call `{attr}` once with a `tuple`")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
let MultipleStartsEndsWith { attr } = self;
|
||||
format!("Merge into a single `{attr}` call")
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE810
|
||||
pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
|
||||
let Expr::BoolOp(ast::ExprBoolOp { op: Boolop::Or, values, range: _ }) = expr else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut duplicates = BTreeMap::new();
|
||||
for (index, call) in values.iter().enumerate() {
|
||||
let Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
range: _
|
||||
}) = &call else {
|
||||
continue
|
||||
};
|
||||
|
||||
if !(args.len() == 1 && keywords.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func.as_ref() else {
|
||||
continue
|
||||
};
|
||||
if attr != "startswith" && attr != "endswith" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Expr::Name(ast::ExprName { id: arg_name, .. } )= value.as_ref() else {
|
||||
continue
|
||||
};
|
||||
|
||||
duplicates
|
||||
.entry((attr.as_str(), arg_name.as_str()))
|
||||
.or_insert_with(Vec::new)
|
||||
.push(index);
|
||||
}
|
||||
|
||||
// Generate a `Diagnostic` for each duplicate.
|
||||
for ((attr_name, arg_name), indices) in duplicates {
|
||||
if indices.len() > 1 {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MultipleStartsEndsWith {
|
||||
attr: attr_name.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let words: Vec<&Expr> = indices
|
||||
.iter()
|
||||
.map(|index| &values[*index])
|
||||
.map(|expr| {
|
||||
let Expr::Call(ast::ExprCall { func: _, args, keywords: _, range: _}) = expr else {
|
||||
unreachable!("{}", format!("Indices should only contain `{attr_name}` calls"))
|
||||
};
|
||||
args.get(0)
|
||||
.unwrap_or_else(|| panic!("`{attr_name}` should have one argument"))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let node = Expr::Tuple(ast::ExprTuple {
|
||||
elts: words
|
||||
.iter()
|
||||
.flat_map(|value| {
|
||||
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = value {
|
||||
Left(elts.iter())
|
||||
} else {
|
||||
Right(iter::once(*value))
|
||||
}
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let node1 = Expr::Name(ast::ExprName {
|
||||
id: arg_name.into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let node2 = Expr::Attribute(ast::ExprAttribute {
|
||||
value: Box::new(node1),
|
||||
attr: attr_name.into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let node3 = Expr::Call(ast::ExprCall {
|
||||
func: Box::new(node2),
|
||||
args: vec![node],
|
||||
keywords: vec![],
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let call = node3;
|
||||
|
||||
// Generate the combined `BoolOp`.
|
||||
let mut call = Some(call);
|
||||
let node = Expr::BoolOp(ast::ExprBoolOp {
|
||||
op: Boolop::Or,
|
||||
values: values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, elt)| {
|
||||
if indices.contains(&index) {
|
||||
std::mem::take(&mut call)
|
||||
} else {
|
||||
Some(elt.clone())
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let bool_op = node;
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
checker.generator().expr(&bool_op),
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt};
|
||||
|
||||
use ruff_diagnostics::AlwaysAutofixableViolation;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::helpers::trailing_comment_start_offset;
|
||||
|
||||
use crate::autofix::actions::delete_stmt;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary `pass` statements in class and function bodies.
|
||||
/// where it is not needed syntactically (e.g., when an indented docstring is
|
||||
/// present).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// When a function or class definition contains a docstring, an additional
|
||||
/// `pass` statement is redundant.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// """Placeholder docstring."""
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// """Placeholder docstring."""
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-pass-statement)
|
||||
#[violation]
|
||||
pub struct UnnecessaryPass;
|
||||
|
||||
impl AlwaysAutofixableViolation for UnnecessaryPass {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Unnecessary `pass` statement")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
"Remove unnecessary `pass`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE790
|
||||
pub(crate) fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) {
|
||||
if body.len() > 1 {
|
||||
// This only catches the case in which a docstring makes a `pass` statement
|
||||
// redundant. Consider removing all `pass` statements instead.
|
||||
let docstring_stmt = &body[0];
|
||||
let pass_stmt = &body[1];
|
||||
let Stmt::Expr(ast::StmtExpr { value, range: _ } )= docstring_stmt else {
|
||||
return;
|
||||
};
|
||||
if matches!(
|
||||
value.as_ref(),
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(..),
|
||||
..
|
||||
})
|
||||
) {
|
||||
if pass_stmt.is_pass_stmt() {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryPass, pass_stmt.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(index) = trailing_comment_start_offset(pass_stmt, checker.locator) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(
|
||||
pass_stmt.range().add_end(index),
|
||||
)));
|
||||
} else {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
delete_stmt(
|
||||
pass_stmt,
|
||||
None,
|
||||
&[],
|
||||
checker.locator,
|
||||
checker.indexer,
|
||||
checker.stylist,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
102
crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs
Normal file
102
crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use rustc_hash::FxHashSet;
|
||||
use rustpython_parser::ast::{self, Expr, Ranged, Stmt};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for enums that contain duplicate values.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Enum values should be unique. Non-unique values are redundant and likely a
|
||||
/// mistake.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from enum import Enum
|
||||
///
|
||||
///
|
||||
/// class Foo(Enum):
|
||||
/// A = 1
|
||||
/// B = 2
|
||||
/// C = 1
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from enum import Enum
|
||||
///
|
||||
///
|
||||
/// class Foo(Enum):
|
||||
/// A = 1
|
||||
/// B = 2
|
||||
/// C = 3
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/library/enum.html#enum.Enum)
|
||||
#[violation]
|
||||
pub struct NonUniqueEnums {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Violation for NonUniqueEnums {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let NonUniqueEnums { value } = self;
|
||||
format!("Enum contains duplicate value: `{value}`")
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE796
|
||||
pub(crate) fn non_unique_enums<'a, 'b>(
|
||||
checker: &mut Checker<'a>,
|
||||
parent: &'b Stmt,
|
||||
body: &'b [Stmt],
|
||||
) where
|
||||
'b: 'a,
|
||||
{
|
||||
let Stmt::ClassDef(ast::StmtClassDef { bases, .. }) = parent else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !bases.iter().any(|expr| {
|
||||
checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(expr)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["enum", "Enum"])
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut seen_targets: FxHashSet<ComparableExpr> = FxHashSet::default();
|
||||
for stmt in body {
|
||||
let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
|
||||
if checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["enum", "auto"])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if !seen_targets.insert(ComparableExpr::from(value)) {
|
||||
let diagnostic = Diagnostic::new(
|
||||
NonUniqueEnums {
|
||||
value: checker.generator().expr(value),
|
||||
},
|
||||
stmt.range(),
|
||||
);
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
use rustpython_parser::ast::{self, Expr, ExprLambda, Ranged};
|
||||
|
||||
use ruff_diagnostics::AlwaysAutofixableViolation;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for lambdas that can be replaced with the `list` builtin.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using `list` builtin is more readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from dataclasses import dataclass, field
|
||||
///
|
||||
///
|
||||
/// @dataclass
|
||||
/// class Foo:
|
||||
/// bar: list[int] = field(default_factory=lambda: [])
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from dataclasses import dataclass, field
|
||||
///
|
||||
///
|
||||
/// @dataclass
|
||||
/// class Foo:
|
||||
/// bar: list[int] = field(default_factory=list)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/library/functions.html#func-list)
|
||||
#[violation]
|
||||
pub struct ReimplementedListBuiltin;
|
||||
|
||||
impl AlwaysAutofixableViolation for ReimplementedListBuiltin {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Prefer `list` over useless lambda")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
"Replace with `list`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE807
|
||||
pub(crate) fn reimplemented_list_builtin(checker: &mut Checker, expr: &ExprLambda) {
|
||||
let ExprLambda {
|
||||
args,
|
||||
body,
|
||||
range: _,
|
||||
} = expr;
|
||||
|
||||
if args.args.is_empty()
|
||||
&& args.kwonlyargs.is_empty()
|
||||
&& args.posonlyargs.is_empty()
|
||||
&& args.vararg.is_none()
|
||||
&& args.kwarg.is_none()
|
||||
{
|
||||
if let Expr::List(ast::ExprList { elts, .. }) = body.as_ref() {
|
||||
if elts.is_empty() {
|
||||
let mut diagnostic = Diagnostic::new(ReimplementedListBuiltin, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
"list".to_string(),
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary `dict` kwargs.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// If the `dict` keys are valid identifiers, they can be passed as keyword
|
||||
/// arguments directly.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def foo(bar):
|
||||
/// return bar + 1
|
||||
///
|
||||
///
|
||||
/// print(foo(**{"bar": 2})) # prints 3
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo(bar):
|
||||
/// return bar + 1
|
||||
///
|
||||
///
|
||||
/// print(foo(bar=2)) # prints 3
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#dictionary-displays)
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
#[violation]
|
||||
pub struct UnnecessaryDictKwargs;
|
||||
|
||||
impl Violation for UnnecessaryDictKwargs {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Unnecessary `dict` kwargs")
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE804
|
||||
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[Keyword]) {
|
||||
for kw in kwargs {
|
||||
// keyword is a spread operator (indicated by None)
|
||||
if kw.arg.is_none() {
|
||||
if let Expr::Dict(ast::ExprDict { keys, .. }) = &kw.value {
|
||||
// ensure foo(**{"bar-bar": 1}) doesn't error
|
||||
if keys.iter().all(|expr| expr.as_ref().map_or(false, is_valid_kwarg_name)) ||
|
||||
// handle case of foo(**{**bar})
|
||||
(keys.len() == 1 && keys[0].is_none())
|
||||
{
|
||||
let diagnostic = Diagnostic::new(UnnecessaryDictKwargs, expr.range());
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if a key is a valid keyword argument name.
|
||||
fn is_valid_kwarg_name(key: &Expr) -> bool {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
}) = key
|
||||
{
|
||||
is_identifier(value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
52
crates/ruff/src/rules/flake8_pie/rules/unnecessary_spread.rs
Normal file
52
crates/ruff/src/rules/flake8_pie/rules/unnecessary_spread.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary dictionary unpacking operators (`**`).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Unpacking a dictionary into another dictionary is redundant. The unpacking
|
||||
/// operator can be removed, making the code more readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// foo = {"A": 1, "B": 2}
|
||||
/// bar = {**foo, **{"C": 3}}
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// foo = {"A": 1, "B": 2}
|
||||
/// bar = {**foo, "C": 3}
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#dictionary-displays)
|
||||
#[violation]
|
||||
pub struct UnnecessarySpread;
|
||||
|
||||
impl Violation for UnnecessarySpread {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Unnecessary spread `**`")
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE800
|
||||
pub(crate) fn unnecessary_spread(checker: &mut Checker, keys: &[Option<Expr>], values: &[Expr]) {
|
||||
for item in keys.iter().zip(values.iter()) {
|
||||
if let (None, value) = item {
|
||||
// We only care about when the key is None which indicates a spread `**`
|
||||
// inside a dict.
|
||||
if let Expr::Dict(_) = value {
|
||||
let diagnostic = Diagnostic::new(UnnecessarySpread, value.range());
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ use crate::lex::docstring_detection::StateMachine;
|
|||
use crate::registry::Rule;
|
||||
use crate::settings::Settings;
|
||||
|
||||
use super::settings::Quote;
|
||||
use super::super::settings::Quote;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for inline strings that use single quotes or double quotes,
|
6
crates/ruff/src/rules/flake8_quotes/rules/mod.rs
Normal file
6
crates/ruff/src/rules/flake8_quotes/rules/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub(crate) use from_tokens::{
|
||||
from_tokens, AvoidableEscapedQuote, BadQuotesDocstring, BadQuotesInlineString,
|
||||
BadQuotesMultilineString,
|
||||
};
|
||||
|
||||
mod from_tokens;
|
|
@ -15,9 +15,9 @@ use crate::checkers::ast::Checker;
|
|||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::flake8_return::helpers::end_of_last_statement;
|
||||
|
||||
use super::branch::Branch;
|
||||
use super::helpers::result_exists;
|
||||
use super::visitor::{ReturnVisitor, Stack};
|
||||
use super::super::branch::Branch;
|
||||
use super::super::helpers::result_exists;
|
||||
use super::super::visitor::{ReturnVisitor, Stack};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the presence of a `return None` statement when `None` is the only
|
6
crates/ruff/src/rules/flake8_return/rules/mod.rs
Normal file
6
crates/ruff/src/rules/flake8_return/rules/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub(crate) use function::{
|
||||
function, ImplicitReturn, ImplicitReturnValue, SuperfluousElseBreak, SuperfluousElseContinue,
|
||||
SuperfluousElseRaise, SuperfluousElseReturn, UnnecessaryAssign, UnnecessaryReturnNone,
|
||||
};
|
||||
|
||||
mod function;
|
|
@ -1,13 +1,127 @@
|
|||
//! Rules from [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/).
|
||||
use ruff_macros::CacheKey;
|
||||
|
||||
pub mod options;
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
|
||||
pub mod banned_api;
|
||||
pub mod relative_imports;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, CacheKey, Default)]
|
||||
pub struct Settings {
|
||||
pub ban_relative_imports: relative_imports::Settings,
|
||||
pub banned_api: banned_api::Settings,
|
||||
use anyhow::Result;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::flake8_tidy_imports;
|
||||
use crate::rules::flake8_tidy_imports::settings::{ApiBan, Strictness};
|
||||
use crate::settings::Settings;
|
||||
use crate::test::test_path;
|
||||
|
||||
#[test]
|
||||
fn banned_api() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID251.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
|
||||
banned_api: FxHashMap::from_iter([
|
||||
(
|
||||
"cgi".to_string(),
|
||||
ApiBan {
|
||||
msg: "The cgi module is deprecated.".to_string(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"typing.TypedDict".to_string(),
|
||||
ApiBan {
|
||||
msg: "Use typing_extensions.TypedDict instead.".to_string(),
|
||||
},
|
||||
),
|
||||
]),
|
||||
..Default::default()
|
||||
},
|
||||
..Settings::for_rules(vec![Rule::BannedApi])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banned_api_package() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID/my_package/sublib/api/application.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
|
||||
banned_api: FxHashMap::from_iter([
|
||||
(
|
||||
"attrs".to_string(),
|
||||
ApiBan {
|
||||
msg: "The attrs module is deprecated.".to_string(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"my_package.sublib.protocol".to_string(),
|
||||
ApiBan {
|
||||
msg: "The protocol module is deprecated.".to_string(),
|
||||
},
|
||||
),
|
||||
]),
|
||||
..Default::default()
|
||||
},
|
||||
namespace_packages: vec![Path::new("my_package").to_path_buf()],
|
||||
..Settings::for_rules(vec![Rule::BannedApi])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ban_parent_imports() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID252.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
|
||||
ban_relative_imports: Strictness::Parents,
|
||||
..Default::default()
|
||||
},
|
||||
..Settings::for_rules(vec![Rule::RelativeImports])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ban_all_imports() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID252.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
|
||||
ban_relative_imports: Strictness::All,
|
||||
..Default::default()
|
||||
},
|
||||
..Settings::for_rules(vec![Rule::RelativeImports])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ban_parent_imports_package() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID/my_package/sublib/api/application.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
|
||||
ban_relative_imports: Strictness::Parents,
|
||||
..Default::default()
|
||||
},
|
||||
namespace_packages: vec![Path::new("my_package").to_path_buf()],
|
||||
..Settings::for_rules(vec![Rule::RelativeImports])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use ruff_macros::{CombineOptions, ConfigurationOptions};
|
||||
|
||||
use super::banned_api::ApiBan;
|
||||
use super::relative_imports::Strictness;
|
||||
use super::Settings;
|
||||
use super::settings::{ApiBan, Settings, Strictness};
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, CombineOptions,
|
||||
|
|
|
@ -1,23 +1,11 @@
|
|||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation, CacheKey};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::from_qualified_name;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
pub type Settings = FxHashMap<String, ApiBan>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ApiBan {
|
||||
/// The message to display when the API is used.
|
||||
pub msg: String,
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for banned imports.
|
||||
///
|
||||
|
@ -115,77 +103,3 @@ pub(crate) fn banned_attribute_access(checker: &mut Checker, expr: &Expr) {
|
|||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::Settings;
|
||||
use crate::test::test_path;
|
||||
|
||||
use super::ApiBan;
|
||||
|
||||
#[test]
|
||||
fn banned_api() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID251.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: super::super::Settings {
|
||||
banned_api: FxHashMap::from_iter([
|
||||
(
|
||||
"cgi".to_string(),
|
||||
ApiBan {
|
||||
msg: "The cgi module is deprecated.".to_string(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"typing.TypedDict".to_string(),
|
||||
ApiBan {
|
||||
msg: "Use typing_extensions.TypedDict instead.".to_string(),
|
||||
},
|
||||
),
|
||||
]),
|
||||
..Default::default()
|
||||
},
|
||||
..Settings::for_rules(vec![Rule::BannedApi])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banned_api_package() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID/my_package/sublib/api/application.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: super::super::Settings {
|
||||
banned_api: FxHashMap::from_iter([
|
||||
(
|
||||
"attrs".to_string(),
|
||||
ApiBan {
|
||||
msg: "The attrs module is deprecated.".to_string(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"my_package.sublib.protocol".to_string(),
|
||||
ApiBan {
|
||||
msg: "The protocol module is deprecated.".to_string(),
|
||||
},
|
||||
),
|
||||
]),
|
||||
..Default::default()
|
||||
},
|
||||
namespace_packages: vec![Path::new("my_package").to_path_buf()],
|
||||
..Settings::for_rules(vec![Rule::BannedApi])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
7
crates/ruff/src/rules/flake8_tidy_imports/rules/mod.rs
Normal file
7
crates/ruff/src/rules/flake8_tidy_imports/rules/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub(crate) use banned_api::{
|
||||
banned_attribute_access, name_is_banned, name_or_parent_is_banned, BannedApi,
|
||||
};
|
||||
pub(crate) use relative_imports::{banned_relative_import, RelativeImports};
|
||||
|
||||
mod banned_api;
|
||||
mod relative_imports;
|
|
@ -1,28 +1,15 @@
|
|||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{self, Int, Ranged, Stmt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation, CacheKey};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::resolve_imported_module_path;
|
||||
use ruff_python_ast::source_code::Generator;
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
pub type Settings = Strictness;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey, Default)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum Strictness {
|
||||
/// Ban imports that extend into the parent module or beyond.
|
||||
#[default]
|
||||
Parents,
|
||||
/// Ban all relative imports.
|
||||
All,
|
||||
}
|
||||
use crate::rules::flake8_tidy_imports::settings::Strictness;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for relative imports.
|
||||
|
@ -147,66 +134,3 @@ pub(crate) fn banned_relative_import(
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::Settings;
|
||||
use crate::test::test_path;
|
||||
|
||||
use super::Strictness;
|
||||
|
||||
#[test]
|
||||
fn ban_parent_imports() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID252.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: super::super::Settings {
|
||||
ban_relative_imports: Strictness::Parents,
|
||||
..Default::default()
|
||||
},
|
||||
..Settings::for_rules(vec![Rule::RelativeImports])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ban_all_imports() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID252.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: super::super::Settings {
|
||||
ban_relative_imports: Strictness::All,
|
||||
..Default::default()
|
||||
},
|
||||
..Settings::for_rules(vec![Rule::RelativeImports])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ban_parent_imports_package() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID/my_package/sublib/api/application.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: super::super::Settings {
|
||||
ban_relative_imports: Strictness::Parents,
|
||||
..Default::default()
|
||||
},
|
||||
namespace_packages: vec![Path::new("my_package").to_path_buf()],
|
||||
..Settings::for_rules(vec![Rule::RelativeImports])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
29
crates/ruff/src/rules/flake8_tidy_imports/settings.rs
Normal file
29
crates/ruff/src/rules/flake8_tidy_imports/settings.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use ruff_macros::CacheKey;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ApiBan {
|
||||
/// The message to display when the API is used.
|
||||
pub msg: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey, Default)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum Strictness {
|
||||
/// Ban imports that extend into the parent module or beyond.
|
||||
#[default]
|
||||
Parents,
|
||||
/// Ban all relative imports.
|
||||
All,
|
||||
}
|
||||
|
||||
#[derive(Debug, CacheKey, Default)]
|
||||
pub struct Settings {
|
||||
pub ban_relative_imports: Strictness,
|
||||
pub banned_api: FxHashMap<String, ApiBan>,
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs
|
||||
source: crates/ruff/src/rules/flake8_tidy_imports/mod.rs
|
||||
---
|
||||
TID252.py:7:1: TID252 Relative imports are banned
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs
|
||||
source: crates/ruff/src/rules/flake8_tidy_imports/mod.rs
|
||||
---
|
||||
TID252.py:9:1: TID252 Relative imports from parent modules are banned
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs
|
||||
source: crates/ruff/src/rules/flake8_tidy_imports/mod.rs
|
||||
---
|
||||
application.py:5:1: TID252 Relative imports from parent modules are banned
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_tidy_imports/banned_api.rs
|
||||
source: crates/ruff/src/rules/flake8_tidy_imports/mod.rs
|
||||
---
|
||||
TID251.py:2:8: TID251 `cgi` is banned: The cgi module is deprecated.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_tidy_imports/banned_api.rs
|
||||
source: crates/ruff/src/rules/flake8_tidy_imports/mod.rs
|
||||
---
|
||||
application.py:3:8: TID251 `attrs` is banned: The attrs module is deprecated.
|
||||
|
|
6
crates/ruff/src/rules/flake8_todos/rules/mod.rs
Normal file
6
crates/ruff/src/rules/flake8_todos/rules/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub(crate) use todos::{
|
||||
todos, InvalidTodoCapitalization, InvalidTodoTag, MissingSpaceAfterTodoColon,
|
||||
MissingTodoAuthor, MissingTodoColon, MissingTodoDescription, MissingTodoLink,
|
||||
};
|
||||
|
||||
mod todos;
|
|
@ -2,7 +2,6 @@
|
|||
mod helpers;
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
mod types;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
pub(crate) use unused_arguments::{
|
||||
unused_arguments, UnusedClassMethodArgument, UnusedFunctionArgument, UnusedLambdaArgument,
|
||||
UnusedMethodArgument, UnusedStaticMethodArgument,
|
||||
};
|
||||
|
||||
mod unused_arguments;
|
|
@ -3,6 +3,7 @@ use std::iter;
|
|||
use regex::Regex;
|
||||
use rustpython_parser::ast::{Arg, Arguments};
|
||||
|
||||
use ruff_diagnostics::DiagnosticKind;
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::analyze::function_type;
|
||||
|
@ -11,10 +12,42 @@ use ruff_python_semantic::analyze::visibility;
|
|||
use ruff_python_semantic::binding::Bindings;
|
||||
use ruff_python_semantic::scope::{FunctionDef, Lambda, Scope, ScopeKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use super::super::helpers;
|
||||
|
||||
use super::helpers;
|
||||
use super::types::Argumentable;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
|
||||
/// An AST node that can contain arguments.
|
||||
#[derive(Copy, Clone)]
|
||||
enum Argumentable {
|
||||
Function,
|
||||
Method,
|
||||
ClassMethod,
|
||||
StaticMethod,
|
||||
Lambda,
|
||||
}
|
||||
|
||||
impl Argumentable {
|
||||
pub(crate) fn check_for(self, name: String) -> DiagnosticKind {
|
||||
match self {
|
||||
Self::Function => UnusedFunctionArgument { name }.into(),
|
||||
Self::Method => UnusedMethodArgument { name }.into(),
|
||||
Self::ClassMethod => UnusedClassMethodArgument { name }.into(),
|
||||
Self::StaticMethod => UnusedStaticMethodArgument { name }.into(),
|
||||
Self::Lambda => UnusedLambdaArgument { name }.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn rule_code(self) -> Rule {
|
||||
match self {
|
||||
Self::Function => Rule::UnusedFunctionArgument,
|
||||
Self::Method => Rule::UnusedMethodArgument,
|
||||
Self::ClassMethod => Rule::UnusedClassMethodArgument,
|
||||
Self::StaticMethod => Rule::UnusedStaticMethodArgument,
|
||||
Self::Lambda => Rule::UnusedLambdaArgument,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the presence of unused arguments in function definitions.
|
|
@ -1,37 +0,0 @@
|
|||
use ruff_diagnostics::DiagnosticKind;
|
||||
|
||||
use crate::registry::Rule;
|
||||
|
||||
use super::rules;
|
||||
|
||||
/// An AST node that can contain arguments.
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) enum Argumentable {
|
||||
Function,
|
||||
Method,
|
||||
ClassMethod,
|
||||
StaticMethod,
|
||||
Lambda,
|
||||
}
|
||||
|
||||
impl Argumentable {
|
||||
pub(crate) fn check_for(self, name: String) -> DiagnosticKind {
|
||||
match self {
|
||||
Self::Function => rules::UnusedFunctionArgument { name }.into(),
|
||||
Self::Method => rules::UnusedMethodArgument { name }.into(),
|
||||
Self::ClassMethod => rules::UnusedClassMethodArgument { name }.into(),
|
||||
Self::StaticMethod => rules::UnusedStaticMethodArgument { name }.into(),
|
||||
Self::Lambda => rules::UnusedLambdaArgument { name }.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn rule_code(self) -> Rule {
|
||||
match self {
|
||||
Self::Function => Rule::UnusedFunctionArgument,
|
||||
Self::Method => Rule::UnusedMethodArgument,
|
||||
Self::ClassMethod => Rule::UnusedClassMethodArgument,
|
||||
Self::StaticMethod => Rule::UnusedStaticMethodArgument,
|
||||
Self::Lambda => Rule::UnusedLambdaArgument,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//! Rules from [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/).
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod rules;
|
||||
pub(crate) mod violations;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue