diff --git a/crates/ruff/resources/test/fixtures/pylint/unexpected_special_method_signature.py b/crates/ruff/resources/test/fixtures/pylint/unexpected_special_method_signature.py index f5270e0459..0748872abc 100644 --- a/crates/ruff/resources/test/fixtures/pylint/unexpected_special_method_signature.py +++ b/crates/ruff/resources/test/fixtures/pylint/unexpected_special_method_signature.py @@ -1,19 +1,16 @@ class TestClass: def __bool__(self): ... - + def __bool__(self, x): # too many mandatory args ... - + def __bool__(self, x=1): # additional optional args OK ... - - def __bool__(self, *args): # varargs OK - ... - + def __bool__(): # ignored; should be caughty by E0211/N805 ... - + @staticmethod def __bool__(): ... @@ -21,31 +18,58 @@ class TestClass: @staticmethod def __bool__(x): # too many mandatory args ... - + @staticmethod def __bool__(x=1): # additional optional args OK ... - + def __eq__(self, other): # multiple args ... - + def __eq__(self, other=1): # expected arg is optional ... - + def __eq__(self): # too few mandatory args ... - + def __eq__(self, other, other_other): # too many mandatory args ... - - def __round__(self): # allow zero additional args. + + def __round__(self): # allow zero additional args ... - - def __round__(self, x): # allow one additional args. + + def __round__(self, x): # allow one additional args ... - + def __round__(self, x, y): # disallow 2 args ... - + def __round__(self, x, y, z=2): # disallow 3 args even when one is optional - ... \ No newline at end of file + ... + + def __eq__(self, *args): # ignore *args + ... + + def __eq__(self, x, *args): # extra *args is ok + ... + + def __eq__(self, x, y, *args): # too many args with *args + ... + + def __round__(self, *args): # allow zero additional args + ... + + def __round__(self, x, *args): # allow one additional args + ... + + def __round__(self, x, y, *args): # disallow 2 args + ... + + def __eq__(self, **kwargs): # ignore **kwargs + ... + + def __eq__(self, /, other=42): # ignore positional-only args + ... + + def __eq__(self, *, other=42): # ignore positional-only args + ... diff --git a/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs b/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs index 8d014acbc7..078def1def 100644 --- a/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs +++ b/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs @@ -7,7 +7,6 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::identifier_range; use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::visibility::is_staticmethod; -use ruff_python_semantic::scope::ScopeKind; use crate::checkers::ast::Checker; @@ -109,7 +108,12 @@ pub fn unexpected_special_method_signature( args: &Arguments, locator: &Locator, ) { - if !matches!(checker.ctx.scope().kind, ScopeKind::Class(_)) { + if !checker.ctx.scope().kind.is_class() { + return; + } + + // Ignore methods with positional-only or keyword-only parameters, or variadic parameters. + if !args.posonlyargs.is_empty() || !args.kwonlyargs.is_empty() || args.kwarg.is_some() { return; } @@ -126,18 +130,22 @@ pub fn unexpected_special_method_signature( return; }; - let emit = match expected_params { - ExpectedParams::Range(min, max) => !(min..=max).contains(&actual_params), - ExpectedParams::Fixed(expected) => match expected.cmp(&mandatory_params) { - Ordering::Less => true, - Ordering::Greater => { - args.vararg.is_none() && optional_params < (expected - mandatory_params) + let valid_signature = match expected_params { + ExpectedParams::Range(min, max) => { + if mandatory_params >= min { + mandatory_params <= max + } else { + args.vararg.is_some() || actual_params <= max } - Ordering::Equal => false, + } + ExpectedParams::Fixed(expected) => match expected.cmp(&mandatory_params) { + Ordering::Less => false, + Ordering::Greater => args.vararg.is_some() || actual_params >= expected, + Ordering::Equal => true, }, }; - if emit { + if !valid_signature { checker.diagnostics.push(Diagnostic::new( UnexpectedSpecialMethodSignature { method_name: name.to_owned(), diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0302_unexpected_special_method_signature.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0302_unexpected_special_method_signature.py.snap index 602b0ad4aa..82d3395dd1 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0302_unexpected_special_method_signature.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0302_unexpected_special_method_signature.py.snap @@ -4,54 +4,72 @@ source: crates/ruff/src/rules/pylint/mod.rs unexpected_special_method_signature.py:5:9: PLE0302 The special method `__bool__` expects 1 parameter, 2 were given | 5 | ... -6 | +6 | 7 | def __bool__(self, x): # too many mandatory args | ^^^^^^^^ PLE0302 8 | ... | -unexpected_special_method_signature.py:22:9: PLE0302 The special method `__bool__` expects 0 parameters, 1 was given +unexpected_special_method_signature.py:19:9: PLE0302 The special method `__bool__` expects 0 parameters, 1 was given | -22 | @staticmethod -23 | def __bool__(x): # too many mandatory args +19 | @staticmethod +20 | def __bool__(x): # too many mandatory args | ^^^^^^^^ PLE0302 -24 | ... +21 | ... | -unexpected_special_method_signature.py:35:9: PLE0302 The special method `__eq__` expects 2 parameters, 1 was given +unexpected_special_method_signature.py:32:9: PLE0302 The special method `__eq__` expects 2 parameters, 1 was given + | +32 | ... +33 | +34 | def __eq__(self): # too few mandatory args + | ^^^^^^ PLE0302 +35 | ... + | + +unexpected_special_method_signature.py:35:9: PLE0302 The special method `__eq__` expects 2 parameters, 3 were given | 35 | ... -36 | -37 | def __eq__(self): # too few mandatory args +36 | +37 | def __eq__(self, other, other_other): # too many mandatory args | ^^^^^^ PLE0302 38 | ... | -unexpected_special_method_signature.py:38:9: PLE0302 The special method `__eq__` expects 2 parameters, 3 were given +unexpected_special_method_signature.py:44:9: PLE0302 The special method `__round__` expects between 1 and 2 parameters, 3 were given | -38 | ... -39 | -40 | def __eq__(self, other, other_other): # too many mandatory args - | ^^^^^^ PLE0302 -41 | ... +44 | ... +45 | +46 | def __round__(self, x, y): # disallow 2 args + | ^^^^^^^^^ PLE0302 +47 | ... | -unexpected_special_method_signature.py:47:9: PLE0302 The special method `__round__` expects between 1 and 2 parameters, 3 were given +unexpected_special_method_signature.py:47:9: PLE0302 The special method `__round__` expects between 1 and 2 parameters, 4 were given | 47 | ... -48 | -49 | def __round__(self, x, y): # disallow 2 args +48 | +49 | def __round__(self, x, y, z=2): # disallow 3 args even when one is optional | ^^^^^^^^^ PLE0302 50 | ... | -unexpected_special_method_signature.py:50:9: PLE0302 The special method `__round__` expects between 1 and 2 parameters, 4 were given +unexpected_special_method_signature.py:56:9: PLE0302 The special method `__eq__` expects 2 parameters, 3 were given | -50 | ... -51 | -52 | def __round__(self, x, y, z=2): # disallow 3 args even when one is optional +56 | ... +57 | +58 | def __eq__(self, x, y, *args): # too many args with *args + | ^^^^^^ PLE0302 +59 | ... + | + +unexpected_special_method_signature.py:65:9: PLE0302 The special method `__round__` expects between 1 and 2 parameters, 3 were given + | +65 | ... +66 | +67 | def __round__(self, x, y, *args): # disallow 2 args | ^^^^^^^^^ PLE0302 -53 | ... +68 | ... |