@@ -22,7 +22,7 @@ use crate::Locator;
2222///
2323/// ## Why is this bad?
2424/// The `operator` module provides functions that implement the same functionality as the
25- /// corresponding operators. For example, `operator.add` is equivalent to `lambda x, y: x + y`.
25+ /// corresponding operators. For example, `operator.add` is often equivalent to `lambda x, y: x + y`.
2626/// Using the functions from the `operator` module is more concise and communicates the intent of
2727/// the code more clearly.
2828///
@@ -44,10 +44,30 @@ use crate::Locator;
4444/// ```
4545///
4646/// ## Fix safety
47- /// This fix is usually safe, but if the lambda is called with keyword arguments, e.g.,
48- /// `add = lambda x, y: x + y; add(x=1, y=2)`, replacing the lambda with an operator function, e.g.,
49- /// `operator.add`, will cause the call to raise a `TypeError`, as functions in `operator` do not allow
50- /// keyword arguments.
47+ /// The fix offered by this rule is always marked as unsafe. While the changes the fix would make
48+ /// would rarely break your code, there are two ways in which functions from the `operator` module
49+ /// differ from user-defined functions. It would be non-trivial for Ruff to detect whether or not
50+ /// these differences would matter in a specific situation where Ruff is emitting a diagnostic for
51+ /// this rule.
52+ ///
53+ /// The first difference is that `operator` functions cannot be called with keyword arguments, but
54+ /// most user-defined functions can. If an `add` function is defined as `add = lambda x, y: x + y`,
55+ /// replacing this function with `operator.add` will cause the later call to raise `TypeError` if
56+ /// the function is later called with keyword arguments, e.g. `add(x=1, y=2)`.
57+ ///
58+ /// The second difference is that user-defined functions are [descriptors], but this is not true of
59+ /// the functions defined in the `operator` module. Practically speaking, this means that defining
60+ /// a function in a class body (either by using a `def` statement or assigning a `lambda` function
61+ /// to a variable) is a valid way of defining an instance method on that class; monkeypatching a
62+ /// user-defined function onto a class after the class has been created also has the same effect.
63+ /// The same is not true of an `operator` function: assigning an `operator` function to a variable
64+ /// in a class body or monkeypatching one onto a class will not create a valid instance method.
65+ /// Ruff will refrain from emitting diagnostics for this rule on function definitions in class
66+ /// bodies; however, it does not currently have sophisticated enough type inference to avoid
67+ /// emitting this diagnostic if a user-defined function is being monkeypatched onto a class after
68+ /// the class has been constructed.
69+ ///
70+ /// [descriptors]: https://docs.python.org/3/howto/descriptor.html
5171#[ derive( ViolationMetadata ) ]
5272pub ( crate ) struct ReimplementedOperator {
5373 operator : Operator ,
@@ -60,14 +80,7 @@ impl Violation for ReimplementedOperator {
6080 #[ derive_message_formats]
6181 fn message ( & self ) -> String {
6282 let ReimplementedOperator { operator, target } = self ;
63- match target {
64- FunctionLikeKind :: Function => {
65- format ! ( "Use `operator.{operator}` instead of defining a function" )
66- }
67- FunctionLikeKind :: Lambda => {
68- format ! ( "Use `operator.{operator}` instead of defining a lambda" )
69- }
70- }
83+ format ! ( "Use `operator.{operator}` instead of defining a {target}" )
7184 }
7285
7386 fn fix_title ( & self ) -> Option < String > {
@@ -79,10 +92,16 @@ impl Violation for ReimplementedOperator {
7992/// FURB118
8093pub ( crate ) fn reimplemented_operator ( checker : & mut Checker , target : & FunctionLike ) {
8194 // Ignore methods.
82- if target. kind ( ) == FunctionLikeKind :: Function {
83- if checker. semantic ( ) . current_scope ( ) . kind . is_class ( ) {
84- return ;
85- }
95+ // Methods can be defined via a `def` statement in a class scope,
96+ // or via a lambda appearing on the right-hand side of an assignment in a class scope.
97+ if checker. semantic ( ) . current_scope ( ) . kind . is_class ( )
98+ && ( target. is_function_def ( )
99+ || checker
100+ . semantic ( )
101+ . current_statements ( )
102+ . any ( |stmt| matches ! ( stmt, Stmt :: AnnAssign ( _) | Stmt :: Assign ( _) ) ) )
103+ {
104+ return ;
86105 }
87106
88107 let Some ( params) = target. parameters ( ) else {
@@ -141,6 +160,10 @@ impl FunctionLike<'_> {
141160 }
142161 }
143162
163+ const fn is_function_def ( & self ) -> bool {
164+ matches ! ( self , Self :: Function ( _) )
165+ }
166+
144167 /// Return the body of the function-like node.
145168 ///
146169 /// If the node is a function definition that consists of more than a single return statement,
@@ -327,12 +350,27 @@ fn get_operator(expr: &Expr, params: &Parameters, locator: &Locator) -> Option<O
327350 }
328351}
329352
330- #[ derive( Debug , PartialEq , Eq ) ]
353+ #[ derive( Debug , PartialEq , Eq , Copy , Clone ) ]
331354enum FunctionLikeKind {
332355 Lambda ,
333356 Function ,
334357}
335358
359+ impl FunctionLikeKind {
360+ const fn as_str ( self ) -> & ' static str {
361+ match self {
362+ Self :: Lambda => "lambda" ,
363+ Self :: Function => "function" ,
364+ }
365+ }
366+ }
367+
368+ impl Display for FunctionLikeKind {
369+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
370+ f. write_str ( self . as_str ( ) )
371+ }
372+ }
373+
336374/// Return the name of the `operator` implemented by the given unary expression.
337375fn unary_op ( expr : & ast:: ExprUnaryOp , params : & Parameters ) -> Option < & ' static str > {
338376 let [ arg] = params. args . as_slice ( ) else {
0 commit comments