@@ -22,7 +22,7 @@ use crate::Locator;
22
22
///
23
23
/// ## Why is this bad?
24
24
/// 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`.
26
26
/// Using the functions from the `operator` module is more concise and communicates the intent of
27
27
/// the code more clearly.
28
28
///
@@ -44,10 +44,30 @@ use crate::Locator;
44
44
/// ```
45
45
///
46
46
/// ## 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
51
71
#[ derive( ViolationMetadata ) ]
52
72
pub ( crate ) struct ReimplementedOperator {
53
73
operator : Operator ,
@@ -60,14 +80,7 @@ impl Violation for ReimplementedOperator {
60
80
#[ derive_message_formats]
61
81
fn message ( & self ) -> String {
62
82
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}" )
71
84
}
72
85
73
86
fn fix_title ( & self ) -> Option < String > {
@@ -79,10 +92,16 @@ impl Violation for ReimplementedOperator {
79
92
/// FURB118
80
93
pub ( crate ) fn reimplemented_operator ( checker : & mut Checker , target : & FunctionLike ) {
81
94
// 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 ;
86
105
}
87
106
88
107
let Some ( params) = target. parameters ( ) else {
@@ -141,6 +160,10 @@ impl FunctionLike<'_> {
141
160
}
142
161
}
143
162
163
+ const fn is_function_def ( & self ) -> bool {
164
+ matches ! ( self , Self :: Function ( _) )
165
+ }
166
+
144
167
/// Return the body of the function-like node.
145
168
///
146
169
/// 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
327
350
}
328
351
}
329
352
330
- #[ derive( Debug , PartialEq , Eq ) ]
353
+ #[ derive( Debug , PartialEq , Eq , Copy , Clone ) ]
331
354
enum FunctionLikeKind {
332
355
Lambda ,
333
356
Function ,
334
357
}
335
358
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
+
336
374
/// Return the name of the `operator` implemented by the given unary expression.
337
375
fn unary_op ( expr : & ast:: ExprUnaryOp , params : & Parameters ) -> Option < & ' static str > {
338
376
let [ arg] = params. args . as_slice ( ) else {
0 commit comments