diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_dunder_call.py b/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_dunder_call.py new file mode 100644 index 0000000000000..dcac4a8e503df --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_dunder_call.py @@ -0,0 +1,30 @@ +from typing import Any + + +print((3.0).__add__(4.0)) # PLC2801 +print((3.0).__sub__(4.0)) # PLC2801 +print((3.0).__mul__(4.0)) # PLC2801 +print((3.0).__truediv__(4.0)) # PLC2801 +print((3.0).__floordiv__(4.0)) # PLC2801 +print((3.0).__mod__(4.0)) # PLC2801 +print((3.0).__eq__(4.0)) # PLC2801 +print((3.0).__ne__(4.0)) # PLC2801 +print((3.0).__lt__(4.0)) # PLC2801 +print((3.0).__le__(4.0)) # PLC2801 +print((3.0).__gt__(4.0)) # PLC2801 +print((3.0).__ge__(4.0)) # PLC2801 +print((3.0).__str__()) # PLC2801 +print((3.0).__repr__()) # PLC2801 +print([1, 2, 3].__len__()) # PLC2801 +print((1).__neg__()) # PLC2801 + + +class Thing: + def __init__(self, stuff: Any) -> None: + super().__init__() # OK + super().__class__(stuff=(1, 2, 3)) # OK + + +blah = lambda: {"a": 1}.__delitem__("a") # OK + +blah = dict[{"a": 1}.__delitem__("a")] # OK diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index b5955034fd85d..b0f9ce3bc70b1 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -962,6 +962,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::TrioZeroSleepCall) { flake8_trio::rules::zero_sleep_call(checker, call); } + if checker.enabled(Rule::UnnecessaryDunderCall) { + pylint::rules::unnecessary_dunder_call(checker, call); + } } Expr::Dict(dict) => { if checker.any_enabled(&[ diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index d6e79da23a619..f964f7da53fd6 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -214,6 +214,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel), (Pylint, "C2401") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiName), (Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName), + (Pylint, "C2801") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDunderCall), #[allow(deprecated)] (Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString), (Pylint, "C3002") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDirectLambdaCall), diff --git a/crates/ruff_linter/src/rules/pylint/helpers.rs b/crates/ruff_linter/src/rules/pylint/helpers.rs index d68e639b59f56..75fb3a69fec7e 100644 --- a/crates/ruff_linter/src/rules/pylint/helpers.rs +++ b/crates/ruff_linter/src/rules/pylint/helpers.rs @@ -197,3 +197,141 @@ impl<'a> Visitor<'_> for SequenceIndexVisitor<'a> { } } } + +/// Returns `true` if a method is a known dunder method. +pub(super) fn is_known_dunder_method(method: &str) -> bool { + matches!( + method, + "__abs__" + | "__add__" + | "__aenter__" + | "__aexit__" + | "__aiter__" + | "__and__" + | "__anext__" + | "__attrs_init__" + | "__attrs_post_init__" + | "__attrs_pre_init__" + | "__await__" + | "__bool__" + | "__buffer__" + | "__bytes__" + | "__call__" + | "__ceil__" + | "__class__" + | "__class_getitem__" + | "__complex__" + | "__contains__" + | "__copy__" + | "__deepcopy__" + | "__del__" + | "__delattr__" + | "__delete__" + | "__delitem__" + | "__dict__" + | "__dir__" + | "__divmod__" + | "__doc__" + | "__enter__" + | "__eq__" + | "__exit__" + | "__float__" + | "__floor__" + | "__floordiv__" + | "__format__" + | "__fspath__" + | "__ge__" + | "__get__" + | "__getattr__" + | "__getattribute__" + | "__getitem__" + | "__getnewargs__" + | "__getnewargs_ex__" + | "__getstate__" + | "__gt__" + | "__hash__" + | "__html__" + | "__iadd__" + | "__iand__" + | "__ifloordiv__" + | "__ilshift__" + | "__imatmul__" + | "__imod__" + | "__imul__" + | "__index__" + | "__init__" + | "__init_subclass__" + | "__instancecheck__" + | "__int__" + | "__invert__" + | "__ior__" + | "__ipow__" + | "__irshift__" + | "__isub__" + | "__iter__" + | "__itruediv__" + | "__ixor__" + | "__le__" + | "__len__" + | "__length_hint__" + | "__lshift__" + | "__lt__" + | "__matmul__" + | "__missing__" + | "__mod__" + | "__module__" + | "__mul__" + | "__ne__" + | "__neg__" + | "__new__" + | "__next__" + | "__or__" + | "__pos__" + | "__post_init__" + | "__pow__" + | "__radd__" + | "__rand__" + | "__rdivmod__" + | "__reduce__" + | "__reduce_ex__" + | "__release_buffer__" + | "__repr__" + | "__reversed__" + | "__rfloordiv__" + | "__rlshift__" + | "__rmatmul__" + | "__rmod__" + | "__rmul__" + | "__ror__" + | "__round__" + | "__rpow__" + | "__rrshift__" + | "__rshift__" + | "__rsub__" + | "__rtruediv__" + | "__rxor__" + | "__set__" + | "__set_name__" + | "__setattr__" + | "__setitem__" + | "__setstate__" + | "__sizeof__" + | "__str__" + | "__sub__" + | "__subclasscheck__" + | "__subclasses__" + | "__subclasshook__" + | "__truediv__" + | "__trunc__" + | "__weakref__" + | "__xor__" + // Overridable sunder names from the `Enum` class. + // See: https://docs.python.org/3/library/enum.html#supported-sunder-names + | "_name_" + | "_value_" + | "_missing_" + | "_ignore_" + | "_order_" + | "_generate_next_value_" + ) +} diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 66d3fe1c3d07d..4401880645436 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -161,6 +161,7 @@ mod tests { Path::new("unnecessary_list_index_lookup.py") )] #[test_case(Rule::NoClassmethodDecorator, Path::new("no_method_decorator.py"))] + #[test_case(Rule::UnnecessaryDunderCall, Path::new("unnecessary_dunder_call.py"))] #[test_case(Rule::NoStaticmethodDecorator, Path::new("no_method_decorator.py"))] #[test_case(Rule::SuperWithoutBrackets, Path::new("super_without_brackets.py"))] #[test_case( diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs index 87c14839cfc69..5b42ba4e6ef16 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs @@ -5,6 +5,7 @@ use ruff_python_ast::Stmt; use ruff_python_semantic::analyze::visibility; use crate::checkers::ast::Checker; +use crate::rules::pylint::helpers::is_known_dunder_method; /// ## What it does /// Checks for misspelled and unknown dunder names in method definitions. @@ -84,141 +85,3 @@ pub(crate) fn bad_dunder_method_name(checker: &mut Checker, class_body: &[Stmt]) )); } } - -/// Returns `true` if a method is a known dunder method. -fn is_known_dunder_method(method: &str) -> bool { - matches!( - method, - "__abs__" - | "__add__" - | "__aenter__" - | "__aexit__" - | "__aiter__" - | "__and__" - | "__anext__" - | "__attrs_init__" - | "__attrs_post_init__" - | "__attrs_pre_init__" - | "__await__" - | "__bool__" - | "__buffer__" - | "__bytes__" - | "__call__" - | "__ceil__" - | "__class__" - | "__class_getitem__" - | "__complex__" - | "__contains__" - | "__copy__" - | "__deepcopy__" - | "__del__" - | "__delattr__" - | "__delete__" - | "__delitem__" - | "__dict__" - | "__dir__" - | "__divmod__" - | "__doc__" - | "__enter__" - | "__eq__" - | "__exit__" - | "__float__" - | "__floor__" - | "__floordiv__" - | "__format__" - | "__fspath__" - | "__ge__" - | "__get__" - | "__getattr__" - | "__getattribute__" - | "__getitem__" - | "__getnewargs__" - | "__getnewargs_ex__" - | "__getstate__" - | "__gt__" - | "__hash__" - | "__html__" - | "__iadd__" - | "__iand__" - | "__ifloordiv__" - | "__ilshift__" - | "__imatmul__" - | "__imod__" - | "__imul__" - | "__index__" - | "__init__" - | "__init_subclass__" - | "__instancecheck__" - | "__int__" - | "__invert__" - | "__ior__" - | "__ipow__" - | "__irshift__" - | "__isub__" - | "__iter__" - | "__itruediv__" - | "__ixor__" - | "__le__" - | "__len__" - | "__length_hint__" - | "__lshift__" - | "__lt__" - | "__matmul__" - | "__missing__" - | "__mod__" - | "__module__" - | "__mul__" - | "__ne__" - | "__neg__" - | "__new__" - | "__next__" - | "__or__" - | "__pos__" - | "__post_init__" - | "__pow__" - | "__radd__" - | "__rand__" - | "__rdivmod__" - | "__reduce__" - | "__reduce_ex__" - | "__release_buffer__" - | "__repr__" - | "__reversed__" - | "__rfloordiv__" - | "__rlshift__" - | "__rmatmul__" - | "__rmod__" - | "__rmul__" - | "__ror__" - | "__round__" - | "__rpow__" - | "__rrshift__" - | "__rshift__" - | "__rsub__" - | "__rtruediv__" - | "__rxor__" - | "__set__" - | "__set_name__" - | "__setattr__" - | "__setitem__" - | "__setstate__" - | "__sizeof__" - | "__str__" - | "__sub__" - | "__subclasscheck__" - | "__subclasses__" - | "__subclasshook__" - | "__truediv__" - | "__trunc__" - | "__weakref__" - | "__xor__" - // Overridable sunder names from the `Enum` class. - // See: https://docs.python.org/3/library/enum.html#supported-sunder-names - | "_name_" - | "_value_" - | "_missing_" - | "_ignore_" - | "_order_" - | "_generate_next_value_" - ) -} diff --git a/crates/ruff_linter/src/rules/pylint/rules/mod.rs b/crates/ruff_linter/src/rules/pylint/rules/mod.rs index 07344e84a38dc..e22096424260f 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/mod.rs @@ -68,6 +68,7 @@ pub(crate) use type_param_name_mismatch::*; pub(crate) use unexpected_special_method_signature::*; pub(crate) use unnecessary_dict_index_lookup::*; pub(crate) use unnecessary_direct_lambda_call::*; +pub(crate) use unnecessary_dunder_call::*; pub(crate) use unnecessary_lambda::*; pub(crate) use unnecessary_list_index_lookup::*; pub(crate) use unspecified_encoding::*; @@ -148,6 +149,7 @@ mod type_param_name_mismatch; mod unexpected_special_method_signature; mod unnecessary_dict_index_lookup; mod unnecessary_direct_lambda_call; +mod unnecessary_dunder_call; mod unnecessary_lambda; mod unnecessary_list_index_lookup; mod unspecified_encoding; diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs new file mode 100644 index 0000000000000..9c81143a38871 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs @@ -0,0 +1,346 @@ +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{self as ast, Expr}; +use ruff_python_semantic::SemanticModel; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; +use crate::rules::pylint::helpers::is_known_dunder_method; +use crate::settings::types::PythonVersion; + +/// ## What it does +/// Checks for explicit use of dunder methods, like `__str__` and `__add__`. +/// +/// ## Why is this bad? +/// Dunder names are not meant to be called explicitly and, in most cases, can +/// be replaced with builtins or operators. +/// +/// ## Example +/// ```python +/// three = (3.0).__str__() +/// twelve = "1".__add__("2") +/// +/// +/// def is_greater_than_two(x: int) -> bool: +/// return x.__gt__(2) +/// ``` +/// +/// Use instead: +/// ```python +/// three = str(3.0) +/// twelve = "1" + "2" +/// +/// +/// def is_greater_than_two(x: int) -> bool: +/// return x > 2 +/// ``` +/// +#[violation] +pub struct UnnecessaryDunderCall { + method: String, + replacement: Option, +} + +impl Violation for UnnecessaryDunderCall { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let UnnecessaryDunderCall { + method, + replacement, + } = self; + + if let Some(replacement) = replacement { + format!("Unnecessary dunder call to `{method}`. {replacement}.") + } else { + format!("Unnecessary dunder call to `{method}`") + } + } + + fn fix_title(&self) -> Option { + let UnnecessaryDunderCall { replacement, .. } = self; + replacement.clone() + } +} + +/// PLC2801 +pub(crate) fn unnecessary_dunder_call(checker: &mut Checker, call: &ast::ExprCall) { + let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = call.func.as_ref() else { + return; + }; + + // If this isn't a known dunder method, abort. + if !is_known_dunder_method(attr) { + return; + } + + // If this is an allowed dunder method, abort. + if allowed_dunder_constants(attr, checker.settings.target_version) { + return; + } + + // Ignore certain dunder methods used in lambda expressions. + if allow_nested_expression(attr, checker.semantic()) { + return; + } + + // Ignore dunder methods used on `super`. + if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { + if checker.semantic().is_builtin("super") { + if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { + if id == "super" { + return; + } + } + } + } + + // If the call has keywords, abort. + if !call.arguments.keywords.is_empty() { + return; + } + + let mut fixed: Option = None; + let mut title: Option = None; + + if let Some(dunder) = DunderReplacement::from_method(attr) { + match (call.arguments.args.as_slice(), dunder) { + ([], DunderReplacement::Builtin(replacement, message)) => { + if !checker.semantic().is_builtin(replacement) { + return; + } + fixed = Some(format!( + "{}({})", + replacement, + checker.locator().slice(value.as_ref()), + )); + title = Some(message.to_string()); + } + ([arg], DunderReplacement::Operator(replacement, message)) => { + fixed = Some(format!( + "{} {} {}", + checker.locator().slice(value.as_ref()), + replacement, + checker.locator().slice(arg), + )); + title = Some(message.to_string()); + } + ([arg], DunderReplacement::ROperator(replacement, message)) => { + fixed = Some(format!( + "{} {} {}", + checker.locator().slice(arg), + replacement, + checker.locator().slice(value.as_ref()), + )); + title = Some(message.to_string()); + } + (_, DunderReplacement::MessageOnly(message)) => { + title = Some(message.to_string()); + } + _ => {} + } + } + + let mut diagnostic = Diagnostic::new( + UnnecessaryDunderCall { + method: attr.to_string(), + replacement: title, + }, + call.range(), + ); + + if let Some(fixed) = fixed { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(fixed, call.range()))); + }; + + checker.diagnostics.push(diagnostic); +} + +/// Return `true` if this is a dunder method that is allowed to be called explicitly. +fn allowed_dunder_constants(dunder_method: &str, target_version: PythonVersion) -> bool { + if matches!( + dunder_method, + "__aexit__" + | "__await__" + | "__class__" + | "__class_getitem__" + | "__dict__" + | "__doc__" + | "__exit__" + | "__getnewargs__" + | "__getnewargs_ex__" + | "__getstate__" + | "__index__" + | "__init_subclass__" + | "__missing__" + | "__module__" + | "__new__" + | "__post_init__" + | "__reduce__" + | "__reduce_ex__" + | "__set_name__" + | "__setstate__" + | "__sizeof__" + | "__subclasses__" + | "__subclasshook__" + | "__weakref__" + ) { + return true; + } + + if target_version < PythonVersion::Py310 && matches!(dunder_method, "__aiter__" | "__anext__") { + return true; + } + + false +} + +#[derive(Debug, Copy, Clone)] +enum DunderReplacement { + /// A dunder method that is an operator. + Operator(&'static str, &'static str), + /// A dunder method that is a right-side operator. + ROperator(&'static str, &'static str), + /// A dunder method that is a builtin. + Builtin(&'static str, &'static str), + /// A dunder method that is a message only. + MessageOnly(&'static str), +} + +impl DunderReplacement { + fn from_method(dunder_method: &str) -> Option { + match dunder_method { + "__add__" => Some(Self::Operator("+", "Use `+` operator")), + "__and__" => Some(Self::Operator("&", "Use `&` operator")), + "__contains__" => Some(Self::Operator("in", "Use `in` operator")), + "__eq__" => Some(Self::Operator("==", "Use `==` operator")), + "__floordiv__" => Some(Self::Operator("//", "Use `//` operator")), + "__ge__" => Some(Self::Operator(">=", "Use `>=` operator")), + "__gt__" => Some(Self::Operator(">", "Use `>` operator")), + "__iadd__" => Some(Self::Operator("+=", "Use `+=` operator")), + "__iand__" => Some(Self::Operator("&=", "Use `&=` operator")), + "__ifloordiv__" => Some(Self::Operator("//=", "Use `//=` operator")), + "__ilshift__" => Some(Self::Operator("<<=", "Use `<<=` operator")), + "__imod__" => Some(Self::Operator("%=", "Use `%=` operator")), + "__imul__" => Some(Self::Operator("*=", "Use `*=` operator")), + "__ior__" => Some(Self::Operator("|=", "Use `|=` operator")), + "__ipow__" => Some(Self::Operator("**=", "Use `**=` operator")), + "__irshift__" => Some(Self::Operator(">>=", "Use `>>=` operator")), + "__isub__" => Some(Self::Operator("-=", "Use `-=` operator")), + "__itruediv__" => Some(Self::Operator("/=", "Use `/=` operator")), + "__ixor__" => Some(Self::Operator("^=", "Use `^=` operator")), + "__le__" => Some(Self::Operator("<=", "Use `<=` operator")), + "__lshift__" => Some(Self::Operator("<<", "Use `<<` operator")), + "__lt__" => Some(Self::Operator("<", "Use `<` operator")), + "__mod__" => Some(Self::Operator("%", "Use `%` operator")), + "__mul__" => Some(Self::Operator("*", "Use `*` operator")), + "__ne__" => Some(Self::Operator("!=", "Use `!=` operator")), + "__or__" => Some(Self::Operator("|", "Use `|` operator")), + "__rshift__" => Some(Self::Operator(">>", "Use `>>` operator")), + "__sub__" => Some(Self::Operator("-", "Use `-` operator")), + "__truediv__" => Some(Self::Operator("/", "Use `/` operator")), + "__xor__" => Some(Self::Operator("^", "Use `^` operator")), + + "__radd__" => Some(Self::ROperator("+", "Use `+` operator")), + "__rand__" => Some(Self::ROperator("&", "Use `&` operator")), + "__rfloordiv__" => Some(Self::ROperator("//", "Use `//` operator")), + "__rlshift__" => Some(Self::ROperator("<<", "Use `<<` operator")), + "__rmod__" => Some(Self::ROperator("%", "Use `%` operator")), + "__rmul__" => Some(Self::ROperator("*", "Use `*` operator")), + "__ror__" => Some(Self::ROperator("|", "Use `|` operator")), + "__rrshift__" => Some(Self::ROperator(">>", "Use `>>` operator")), + "__rsub__" => Some(Self::ROperator("-", "Use `-` operator")), + "__rtruediv__" => Some(Self::ROperator("/", "Use `/` operator")), + "__rxor__" => Some(Self::ROperator("^", "Use `^` operator")), + + "__aiter__" => Some(Self::Builtin("aiter", "Use `aiter()` builtin")), + "__anext__" => Some(Self::Builtin("anext", "Use `anext()` builtin")), + "__abs__" => Some(Self::Builtin("abs", "Use `abs()` builtin")), + "__bool__" => Some(Self::Builtin("bool", "Use `bool()` builtin")), + "__bytes__" => Some(Self::Builtin("bytes", "Use `bytes()` builtin")), + "__complex__" => Some(Self::Builtin("complex", "Use `complex()` builtin")), + "__dir__" => Some(Self::Builtin("dir", "Use `dir()` builtin")), + "__float__" => Some(Self::Builtin("float", "Use `float()` builtin")), + "__hash__" => Some(Self::Builtin("hash", "Use `hash()` builtin")), + "__int__" => Some(Self::Builtin("int", "Use `int()` builtin")), + "__iter__" => Some(Self::Builtin("iter", "Use `iter()` builtin")), + "__len__" => Some(Self::Builtin("len", "Use `len()` builtin")), + "__next__" => Some(Self::Builtin("next", "Use `next()` builtin")), + "__repr__" => Some(Self::Builtin("repr", "Use `repr()` builtin")), + "__reversed__" => Some(Self::Builtin("reversed", "Use `reversed()` builtin")), + "__round__" => Some(Self::Builtin("round", "Use `round()` builtin")), + "__str__" => Some(Self::Builtin("str", "Use `str()` builtin")), + "__subclasscheck__" => Some(Self::Builtin("issubclass", "Use `issubclass()` builtin")), + + "__aenter__" => Some(Self::MessageOnly("Use `aenter()` builtin")), + "__ceil__" => Some(Self::MessageOnly("Use `math.ceil()` function")), + "__copy__" => Some(Self::MessageOnly("Use `copy.copy()` function")), + "__deepcopy__" => Some(Self::MessageOnly("Use `copy.deepcopy()` function")), + "__del__" => Some(Self::MessageOnly("Use `del` statement")), + "__delattr__" => Some(Self::MessageOnly("Use `del` statement")), + "__delete__" => Some(Self::MessageOnly("Use `del` statement")), + "__delitem__" => Some(Self::MessageOnly("Use `del` statement")), + "__divmod__" => Some(Self::MessageOnly("Use `divmod()` builtin")), + "__format__" => Some(Self::MessageOnly( + "Use `format` builtin, format string method, or f-string.", + )), + "__fspath__" => Some(Self::MessageOnly("Use `os.fspath` function")), + "__get__" => Some(Self::MessageOnly("Use `get` method")), + "__getattr__" => Some(Self::MessageOnly( + "Access attribute directly or use getattr built-in function.", + )), + "__getattribute__" => Some(Self::MessageOnly( + "Access attribute directly or use getattr built-in function.", + )), + "__getitem__" => Some(Self::MessageOnly("Access item via subscript")), + "__init__" => Some(Self::MessageOnly("Instantiate class directly")), + "__instancecheck__" => Some(Self::MessageOnly("Use `isinstance()` builtin")), + "__invert__" => Some(Self::MessageOnly("Use `~` operator")), + "__neg__" => Some(Self::MessageOnly("Multiply by -1 instead")), + "__pos__" => Some(Self::MessageOnly("Multiply by +1 instead")), + "__pow__" => Some(Self::MessageOnly("Use ** operator or `pow()` builtin")), + "__rdivmod__" => Some(Self::MessageOnly("Use `divmod()` builtin")), + "__rpow__" => Some(Self::MessageOnly("Use ** operator or `pow()` builtin")), + "__set__" => Some(Self::MessageOnly("Use subscript assignment")), + "__setattr__" => Some(Self::MessageOnly( + "Mutate attribute directly or use setattr built-in function.", + )), + "__setitem__" => Some(Self::MessageOnly("Use subscript assignment")), + "__truncate__" => Some(Self::MessageOnly("Use `math.trunc()` function")), + + _ => None, + } + } +} + +/// Returns `true` if this is a dunder method that is excusable in a nested expression. Some +/// methods are otherwise unusable in lambda expressions and elsewhere, as they can only be +/// represented as +/// statements. +fn allow_nested_expression(dunder_name: &str, semantic: &SemanticModel) -> bool { + semantic.current_expression_parent().is_some() + && matches!( + dunder_name, + "__init__" + | "__del__" + | "__delattr__" + | "__set__" + | "__delete__" + | "__setitem__" + | "__delitem__" + | "__iadd__" + | "__isub__" + | "__imul__" + | "__imatmul__" + | "__itruediv__" + | "__ifloordiv__" + | "__imod__" + | "__ipow__" + | "__ilshift__" + | "__irshift__" + | "__iand__" + | "__ixor__" + | "__ior__" + ) +} diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2801_unnecessary_dunder_call.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2801_unnecessary_dunder_call.py.snap new file mode 100644 index 0000000000000..c24069340ad2b --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2801_unnecessary_dunder_call.py.snap @@ -0,0 +1,324 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +unnecessary_dunder_call.py:4:7: PLC2801 [*] Unnecessary dunder call to `__add__`. Use `+` operator. + | +4 | print((3.0).__add__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^^ PLC2801 +5 | print((3.0).__sub__(4.0)) # PLC2801 +6 | print((3.0).__mul__(4.0)) # PLC2801 + | + = help: Use `+` operator + +ℹ Safe fix +1 1 | from typing import Any +2 2 | +3 3 | +4 |-print((3.0).__add__(4.0)) # PLC2801 + 4 |+print(3.0 + 4.0) # PLC2801 +5 5 | print((3.0).__sub__(4.0)) # PLC2801 +6 6 | print((3.0).__mul__(4.0)) # PLC2801 +7 7 | print((3.0).__truediv__(4.0)) # PLC2801 + +unnecessary_dunder_call.py:5:7: PLC2801 [*] Unnecessary dunder call to `__sub__`. Use `-` operator. + | +4 | print((3.0).__add__(4.0)) # PLC2801 +5 | print((3.0).__sub__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^^ PLC2801 +6 | print((3.0).__mul__(4.0)) # PLC2801 +7 | print((3.0).__truediv__(4.0)) # PLC2801 + | + = help: Use `-` operator + +ℹ Safe fix +2 2 | +3 3 | +4 4 | print((3.0).__add__(4.0)) # PLC2801 +5 |-print((3.0).__sub__(4.0)) # PLC2801 + 5 |+print(3.0 - 4.0) # PLC2801 +6 6 | print((3.0).__mul__(4.0)) # PLC2801 +7 7 | print((3.0).__truediv__(4.0)) # PLC2801 +8 8 | print((3.0).__floordiv__(4.0)) # PLC2801 + +unnecessary_dunder_call.py:6:7: PLC2801 [*] Unnecessary dunder call to `__mul__`. Use `*` operator. + | +4 | print((3.0).__add__(4.0)) # PLC2801 +5 | print((3.0).__sub__(4.0)) # PLC2801 +6 | print((3.0).__mul__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^^ PLC2801 +7 | print((3.0).__truediv__(4.0)) # PLC2801 +8 | print((3.0).__floordiv__(4.0)) # PLC2801 + | + = help: Use `*` operator + +ℹ Safe fix +3 3 | +4 4 | print((3.0).__add__(4.0)) # PLC2801 +5 5 | print((3.0).__sub__(4.0)) # PLC2801 +6 |-print((3.0).__mul__(4.0)) # PLC2801 + 6 |+print(3.0 * 4.0) # PLC2801 +7 7 | print((3.0).__truediv__(4.0)) # PLC2801 +8 8 | print((3.0).__floordiv__(4.0)) # PLC2801 +9 9 | print((3.0).__mod__(4.0)) # PLC2801 + +unnecessary_dunder_call.py:7:7: PLC2801 [*] Unnecessary dunder call to `__truediv__`. Use `/` operator. + | +5 | print((3.0).__sub__(4.0)) # PLC2801 +6 | print((3.0).__mul__(4.0)) # PLC2801 +7 | print((3.0).__truediv__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^^^^^^ PLC2801 +8 | print((3.0).__floordiv__(4.0)) # PLC2801 +9 | print((3.0).__mod__(4.0)) # PLC2801 + | + = help: Use `/` operator + +ℹ Safe fix +4 4 | print((3.0).__add__(4.0)) # PLC2801 +5 5 | print((3.0).__sub__(4.0)) # PLC2801 +6 6 | print((3.0).__mul__(4.0)) # PLC2801 +7 |-print((3.0).__truediv__(4.0)) # PLC2801 + 7 |+print(3.0 / 4.0) # PLC2801 +8 8 | print((3.0).__floordiv__(4.0)) # PLC2801 +9 9 | print((3.0).__mod__(4.0)) # PLC2801 +10 10 | print((3.0).__eq__(4.0)) # PLC2801 + +unnecessary_dunder_call.py:8:7: PLC2801 [*] Unnecessary dunder call to `__floordiv__`. Use `//` operator. + | + 6 | print((3.0).__mul__(4.0)) # PLC2801 + 7 | print((3.0).__truediv__(4.0)) # PLC2801 + 8 | print((3.0).__floordiv__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^^^^^^^ PLC2801 + 9 | print((3.0).__mod__(4.0)) # PLC2801 +10 | print((3.0).__eq__(4.0)) # PLC2801 + | + = help: Use `//` operator + +ℹ Safe fix +5 5 | print((3.0).__sub__(4.0)) # PLC2801 +6 6 | print((3.0).__mul__(4.0)) # PLC2801 +7 7 | print((3.0).__truediv__(4.0)) # PLC2801 +8 |-print((3.0).__floordiv__(4.0)) # PLC2801 + 8 |+print(3.0 // 4.0) # PLC2801 +9 9 | print((3.0).__mod__(4.0)) # PLC2801 +10 10 | print((3.0).__eq__(4.0)) # PLC2801 +11 11 | print((3.0).__ne__(4.0)) # PLC2801 + +unnecessary_dunder_call.py:9:7: PLC2801 [*] Unnecessary dunder call to `__mod__`. Use `%` operator. + | + 7 | print((3.0).__truediv__(4.0)) # PLC2801 + 8 | print((3.0).__floordiv__(4.0)) # PLC2801 + 9 | print((3.0).__mod__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^^ PLC2801 +10 | print((3.0).__eq__(4.0)) # PLC2801 +11 | print((3.0).__ne__(4.0)) # PLC2801 + | + = help: Use `%` operator + +ℹ Safe fix +6 6 | print((3.0).__mul__(4.0)) # PLC2801 +7 7 | print((3.0).__truediv__(4.0)) # PLC2801 +8 8 | print((3.0).__floordiv__(4.0)) # PLC2801 +9 |-print((3.0).__mod__(4.0)) # PLC2801 + 9 |+print(3.0 % 4.0) # PLC2801 +10 10 | print((3.0).__eq__(4.0)) # PLC2801 +11 11 | print((3.0).__ne__(4.0)) # PLC2801 +12 12 | print((3.0).__lt__(4.0)) # PLC2801 + +unnecessary_dunder_call.py:10:7: PLC2801 [*] Unnecessary dunder call to `__eq__`. Use `==` operator. + | + 8 | print((3.0).__floordiv__(4.0)) # PLC2801 + 9 | print((3.0).__mod__(4.0)) # PLC2801 +10 | print((3.0).__eq__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^ PLC2801 +11 | print((3.0).__ne__(4.0)) # PLC2801 +12 | print((3.0).__lt__(4.0)) # PLC2801 + | + = help: Use `==` operator + +ℹ Safe fix +7 7 | print((3.0).__truediv__(4.0)) # PLC2801 +8 8 | print((3.0).__floordiv__(4.0)) # PLC2801 +9 9 | print((3.0).__mod__(4.0)) # PLC2801 +10 |-print((3.0).__eq__(4.0)) # PLC2801 + 10 |+print(3.0 == 4.0) # PLC2801 +11 11 | print((3.0).__ne__(4.0)) # PLC2801 +12 12 | print((3.0).__lt__(4.0)) # PLC2801 +13 13 | print((3.0).__le__(4.0)) # PLC2801 + +unnecessary_dunder_call.py:11:7: PLC2801 [*] Unnecessary dunder call to `__ne__`. Use `!=` operator. + | + 9 | print((3.0).__mod__(4.0)) # PLC2801 +10 | print((3.0).__eq__(4.0)) # PLC2801 +11 | print((3.0).__ne__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^ PLC2801 +12 | print((3.0).__lt__(4.0)) # PLC2801 +13 | print((3.0).__le__(4.0)) # PLC2801 + | + = help: Use `!=` operator + +ℹ Safe fix +8 8 | print((3.0).__floordiv__(4.0)) # PLC2801 +9 9 | print((3.0).__mod__(4.0)) # PLC2801 +10 10 | print((3.0).__eq__(4.0)) # PLC2801 +11 |-print((3.0).__ne__(4.0)) # PLC2801 + 11 |+print(3.0 != 4.0) # PLC2801 +12 12 | print((3.0).__lt__(4.0)) # PLC2801 +13 13 | print((3.0).__le__(4.0)) # PLC2801 +14 14 | print((3.0).__gt__(4.0)) # PLC2801 + +unnecessary_dunder_call.py:12:7: PLC2801 [*] Unnecessary dunder call to `__lt__`. Use `<` operator. + | +10 | print((3.0).__eq__(4.0)) # PLC2801 +11 | print((3.0).__ne__(4.0)) # PLC2801 +12 | print((3.0).__lt__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^ PLC2801 +13 | print((3.0).__le__(4.0)) # PLC2801 +14 | print((3.0).__gt__(4.0)) # PLC2801 + | + = help: Use `<` operator + +ℹ Safe fix +9 9 | print((3.0).__mod__(4.0)) # PLC2801 +10 10 | print((3.0).__eq__(4.0)) # PLC2801 +11 11 | print((3.0).__ne__(4.0)) # PLC2801 +12 |-print((3.0).__lt__(4.0)) # PLC2801 + 12 |+print(3.0 < 4.0) # PLC2801 +13 13 | print((3.0).__le__(4.0)) # PLC2801 +14 14 | print((3.0).__gt__(4.0)) # PLC2801 +15 15 | print((3.0).__ge__(4.0)) # PLC2801 + +unnecessary_dunder_call.py:13:7: PLC2801 [*] Unnecessary dunder call to `__le__`. Use `<=` operator. + | +11 | print((3.0).__ne__(4.0)) # PLC2801 +12 | print((3.0).__lt__(4.0)) # PLC2801 +13 | print((3.0).__le__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^ PLC2801 +14 | print((3.0).__gt__(4.0)) # PLC2801 +15 | print((3.0).__ge__(4.0)) # PLC2801 + | + = help: Use `<=` operator + +ℹ Safe fix +10 10 | print((3.0).__eq__(4.0)) # PLC2801 +11 11 | print((3.0).__ne__(4.0)) # PLC2801 +12 12 | print((3.0).__lt__(4.0)) # PLC2801 +13 |-print((3.0).__le__(4.0)) # PLC2801 + 13 |+print(3.0 <= 4.0) # PLC2801 +14 14 | print((3.0).__gt__(4.0)) # PLC2801 +15 15 | print((3.0).__ge__(4.0)) # PLC2801 +16 16 | print((3.0).__str__()) # PLC2801 + +unnecessary_dunder_call.py:14:7: PLC2801 [*] Unnecessary dunder call to `__gt__`. Use `>` operator. + | +12 | print((3.0).__lt__(4.0)) # PLC2801 +13 | print((3.0).__le__(4.0)) # PLC2801 +14 | print((3.0).__gt__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^ PLC2801 +15 | print((3.0).__ge__(4.0)) # PLC2801 +16 | print((3.0).__str__()) # PLC2801 + | + = help: Use `>` operator + +ℹ Safe fix +11 11 | print((3.0).__ne__(4.0)) # PLC2801 +12 12 | print((3.0).__lt__(4.0)) # PLC2801 +13 13 | print((3.0).__le__(4.0)) # PLC2801 +14 |-print((3.0).__gt__(4.0)) # PLC2801 + 14 |+print(3.0 > 4.0) # PLC2801 +15 15 | print((3.0).__ge__(4.0)) # PLC2801 +16 16 | print((3.0).__str__()) # PLC2801 +17 17 | print((3.0).__repr__()) # PLC2801 + +unnecessary_dunder_call.py:15:7: PLC2801 [*] Unnecessary dunder call to `__ge__`. Use `>=` operator. + | +13 | print((3.0).__le__(4.0)) # PLC2801 +14 | print((3.0).__gt__(4.0)) # PLC2801 +15 | print((3.0).__ge__(4.0)) # PLC2801 + | ^^^^^^^^^^^^^^^^^ PLC2801 +16 | print((3.0).__str__()) # PLC2801 +17 | print((3.0).__repr__()) # PLC2801 + | + = help: Use `>=` operator + +ℹ Safe fix +12 12 | print((3.0).__lt__(4.0)) # PLC2801 +13 13 | print((3.0).__le__(4.0)) # PLC2801 +14 14 | print((3.0).__gt__(4.0)) # PLC2801 +15 |-print((3.0).__ge__(4.0)) # PLC2801 + 15 |+print(3.0 >= 4.0) # PLC2801 +16 16 | print((3.0).__str__()) # PLC2801 +17 17 | print((3.0).__repr__()) # PLC2801 +18 18 | print([1, 2, 3].__len__()) # PLC2801 + +unnecessary_dunder_call.py:16:7: PLC2801 [*] Unnecessary dunder call to `__str__`. Use `str()` builtin. + | +14 | print((3.0).__gt__(4.0)) # PLC2801 +15 | print((3.0).__ge__(4.0)) # PLC2801 +16 | print((3.0).__str__()) # PLC2801 + | ^^^^^^^^^^^^^^^ PLC2801 +17 | print((3.0).__repr__()) # PLC2801 +18 | print([1, 2, 3].__len__()) # PLC2801 + | + = help: Use `str()` builtin + +ℹ Safe fix +13 13 | print((3.0).__le__(4.0)) # PLC2801 +14 14 | print((3.0).__gt__(4.0)) # PLC2801 +15 15 | print((3.0).__ge__(4.0)) # PLC2801 +16 |-print((3.0).__str__()) # PLC2801 + 16 |+print(str(3.0)) # PLC2801 +17 17 | print((3.0).__repr__()) # PLC2801 +18 18 | print([1, 2, 3].__len__()) # PLC2801 +19 19 | print((1).__neg__()) # PLC2801 + +unnecessary_dunder_call.py:17:7: PLC2801 [*] Unnecessary dunder call to `__repr__`. Use `repr()` builtin. + | +15 | print((3.0).__ge__(4.0)) # PLC2801 +16 | print((3.0).__str__()) # PLC2801 +17 | print((3.0).__repr__()) # PLC2801 + | ^^^^^^^^^^^^^^^^ PLC2801 +18 | print([1, 2, 3].__len__()) # PLC2801 +19 | print((1).__neg__()) # PLC2801 + | + = help: Use `repr()` builtin + +ℹ Safe fix +14 14 | print((3.0).__gt__(4.0)) # PLC2801 +15 15 | print((3.0).__ge__(4.0)) # PLC2801 +16 16 | print((3.0).__str__()) # PLC2801 +17 |-print((3.0).__repr__()) # PLC2801 + 17 |+print(repr(3.0)) # PLC2801 +18 18 | print([1, 2, 3].__len__()) # PLC2801 +19 19 | print((1).__neg__()) # PLC2801 +20 20 | + +unnecessary_dunder_call.py:18:7: PLC2801 [*] Unnecessary dunder call to `__len__`. Use `len()` builtin. + | +16 | print((3.0).__str__()) # PLC2801 +17 | print((3.0).__repr__()) # PLC2801 +18 | print([1, 2, 3].__len__()) # PLC2801 + | ^^^^^^^^^^^^^^^^^^^ PLC2801 +19 | print((1).__neg__()) # PLC2801 + | + = help: Use `len()` builtin + +ℹ Safe fix +15 15 | print((3.0).__ge__(4.0)) # PLC2801 +16 16 | print((3.0).__str__()) # PLC2801 +17 17 | print((3.0).__repr__()) # PLC2801 +18 |-print([1, 2, 3].__len__()) # PLC2801 + 18 |+print(len([1, 2, 3])) # PLC2801 +19 19 | print((1).__neg__()) # PLC2801 +20 20 | +21 21 | + +unnecessary_dunder_call.py:19:7: PLC2801 Unnecessary dunder call to `__neg__`. Multiply by -1 instead. + | +17 | print((3.0).__repr__()) # PLC2801 +18 | print([1, 2, 3].__len__()) # PLC2801 +19 | print((1).__neg__()) # PLC2801 + | ^^^^^^^^^^^^^ PLC2801 + | + = help: Multiply by -1 instead + + diff --git a/ruff.schema.json b/ruff.schema.json index 74b1c0a6d64df..dce12c257b2b2 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3097,6 +3097,9 @@ "PLC240", "PLC2401", "PLC2403", + "PLC28", + "PLC280", + "PLC2801", "PLC3", "PLC30", "PLC300",