diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py b/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py index fbebdff4b2..7cfb3fceee 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py @@ -210,6 +210,9 @@ def f(a, b): # Unused arguments on magic methods. ### class C: + def __new__(cls, x): + print("Hello, world!") + def __init__(self, x) -> None: print("Hello, world!") @@ -219,6 +222,12 @@ class C: def __exit__(self, exc_type, exc_value, traceback) -> None: print("Hello, world!") + def __init_subclass__(cls, x) -> None: + print("Hello, world!") + + def __class_getitem__(cls, x): + print("Hello, world!") + ### # Used arguments on chained cast. diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/bad_staticmethod_argument.py b/crates/ruff_linter/resources/test/fixtures/pylint/bad_staticmethod_argument.py index 69c459fa6b..cac78f89f1 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/bad_staticmethod_argument.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/bad_staticmethod_argument.py @@ -48,3 +48,9 @@ class Foo: @staticmethod def __new__(cls, x, y, z): # OK, see https://docs.python.org/3/reference/datamodel.html#basic-customization pass + +# `__new__` is an implicit staticmethod, so this should still trigger (with +# `self` but not with `cls` as first argument - see above). +class Foo: + def __new__(self, x, y, z): # [bad-staticmethod-argument] + pass diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/self_or_cls_assignment.py b/crates/ruff_linter/resources/test/fixtures/pylint/self_or_cls_assignment.py index fe016694fc..201dcb4368 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/self_or_cls_assignment.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/self_or_cls_assignment.py @@ -41,3 +41,10 @@ class Fruit: def list_fruits(self, cls) -> None: self = "apple" # Ok cls = "banana" # Ok + +# `__new__` is implicitly a static method +# but for the purposes of this check we treat +# it as a class method. +class Foo: + def __new__(cls): + cls = "apple" # PLW0642 diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/too_many_arguments.py b/crates/ruff_linter/resources/test/fixtures/pylint/too_many_arguments.py index d2ff54d376..ef0fe15c47 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/too_many_arguments.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/too_many_arguments.py @@ -74,3 +74,8 @@ class C: def f(y, z, a, b, c): # OK pass +class Foo: + # `__new__` counts args like a classmethod + # even though it is an implicit staticmethod + def __new__(cls,a,b,c,d,e): # Ok + ... diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/too_many_positional_arguments.py b/crates/ruff_linter/resources/test/fixtures/pylint/too_many_positional_arguments.py index b769acf637..1547d12594 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/too_many_positional_arguments.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/too_many_positional_arguments.py @@ -59,3 +59,10 @@ class C: def f(): # OK pass + +class Foo: + # `__new__` counts args like a classmethod + # even though it is an implicit staticmethod + def __new__(cls,a,b,c,d,e): # Ok + ... + diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index efe2c4715f..c0299a03e4 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -159,7 +159,7 @@ pub(crate) fn custom_type_var_instead_of_self( // to a type variable, and we emit the diagnostic on some methods that do not have return // annotations. let (method, diagnostic_range) = match function_kind { - FunctionType::ClassMethod => { + FunctionType::ClassMethod | FunctionType::NewMethod => { if checker.settings.preview.is_enabled() { ( Method::PreviewClass(PreviewClassMethod { diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index 8af3769683..c030e4539f 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -422,7 +422,6 @@ pub(crate) fn unused_arguments(checker: &Checker, scope: &Scope) { && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && (!visibility::is_magic(name) || visibility::is_init(name) - || visibility::is_new(name) || visibility::is_call(name)) && !visibility::is_abstract(decorator_list, checker.semantic()) && !visibility::is_override(decorator_list, checker.semantic()) @@ -437,7 +436,6 @@ pub(crate) fn unused_arguments(checker: &Checker, scope: &Scope) { && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && (!visibility::is_magic(name) || visibility::is_init(name) - || visibility::is_new(name) || visibility::is_call(name)) && !visibility::is_abstract(decorator_list, checker.semantic()) && !visibility::is_override(decorator_list, checker.semantic()) @@ -452,7 +450,6 @@ pub(crate) fn unused_arguments(checker: &Checker, scope: &Scope) { && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && (!visibility::is_magic(name) || visibility::is_init(name) - || visibility::is_new(name) || visibility::is_call(name)) && !visibility::is_abstract(decorator_list, checker.semantic()) && !visibility::is_override(decorator_list, checker.semantic()) @@ -461,6 +458,19 @@ pub(crate) fn unused_arguments(checker: &Checker, scope: &Scope) { function(Argumentable::StaticMethod, parameters, scope, checker); } } + function_type::FunctionType::NewMethod => { + if checker.enabled(Argumentable::StaticMethod.rule_code()) + && !function_type::is_stub(function_def, checker.semantic()) + && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) + && !visibility::is_abstract(decorator_list, checker.semantic()) + && !visibility::is_override(decorator_list, checker.semantic()) + && !visibility::is_overload(decorator_list, checker.semantic()) + { + // we use `method()` here rather than `function()`, as although `__new__` is + // an implicit staticmethod, `__new__` methods must always have >= parameter + method(Argumentable::StaticMethod, parameters, scope, checker); + } + } } } ScopeKind::Lambda(ast::ExprLambda { parameters, .. }) => { diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap b/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap index 0bd147a3e9..3e1e19260a 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap @@ -58,11 +58,11 @@ ARG.py:66:17: ARG002 Unused method argument: `x` 68 | raise NotImplementedError("must use msg") | -ARG.py:213:24: ARG002 Unused method argument: `x` +ARG.py:216:24: ARG002 Unused method argument: `x` | -211 | ### -212 | class C: -213 | def __init__(self, x) -> None: - | ^ ARG002 214 | print("Hello, world!") +215 | +216 | def __init__(self, x) -> None: + | ^ ARG002 +217 | print("Hello, world!") | diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG003_ARG.py.snap b/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG003_ARG.py.snap index 1a7e8e356a..c0187e8c67 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG003_ARG.py.snap +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG003_ARG.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_unused_arguments/mod.rs -snapshot_kind: text --- ARG.py:47:16: ARG003 Unused class method argument: `x` | diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG004_ARG.py.snap b/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG004_ARG.py.snap index 47b8107b46..da31a5449a 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG004_ARG.py.snap +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG004_ARG.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_unused_arguments/mod.rs -snapshot_kind: text --- ARG.py:51:11: ARG004 Unused static method argument: `cls` | @@ -25,3 +24,12 @@ ARG.py:55:11: ARG004 Unused static method argument: `x` | ^ ARG004 56 | print("Hello, world!") | + +ARG.py:213:22: ARG004 Unused static method argument: `x` + | +211 | ### +212 | class C: +213 | def __new__(cls, x): + | ^ ARG004 +214 | print("Hello, world!") + | diff --git a/crates/ruff_linter/src/rules/pep8_naming/mod.rs b/crates/ruff_linter/src/rules/pep8_naming/mod.rs index 6bb777d635..15aa305e0d 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/mod.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/mod.rs @@ -90,6 +90,7 @@ mod tests { } #[test_case(Rule::InvalidArgumentName, Path::new("N803.py"))] + #[test_case(Rule::InvalidArgumentName, Path::new("N804.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs index c61a5a7e4d..4387cb48f5 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs @@ -127,6 +127,8 @@ impl Violation for InvalidFirstArgumentNameForMethod { #[derive(ViolationMetadata)] pub(crate) struct InvalidFirstArgumentNameForClassMethod { argument_name: String, + // Whether the method is `__new__` + is_new: bool, } impl Violation for InvalidFirstArgumentNameForClassMethod { @@ -134,12 +136,19 @@ impl Violation for InvalidFirstArgumentNameForClassMethod { ruff_diagnostics::FixAvailability::Sometimes; #[derive_message_formats] + // The first string below is what shows up in the documentation + // in the rule table, and it is the more common case. + #[allow(clippy::if_not_else)] fn message(&self) -> String { - "First argument of a class method should be named `cls`".to_string() + if !self.is_new { + "First argument of a class method should be named `cls`".to_string() + } else { + "First argument of `__new__` method should be named `cls`".to_string() + } } fn fix_title(&self) -> Option { - let Self { argument_name } = self; + let Self { argument_name, .. } = self; Some(format!("Rename `{argument_name}` to `cls`")) } } @@ -150,13 +159,24 @@ enum FunctionType { Method, /// The function is a class method. ClassMethod, + /// The function is the method `__new__` + NewMethod, } impl FunctionType { fn diagnostic_kind(self, argument_name: String) -> DiagnosticKind { match self { Self::Method => InvalidFirstArgumentNameForMethod { argument_name }.into(), - Self::ClassMethod => InvalidFirstArgumentNameForClassMethod { argument_name }.into(), + Self::ClassMethod => InvalidFirstArgumentNameForClassMethod { + argument_name, + is_new: false, + } + .into(), + Self::NewMethod => InvalidFirstArgumentNameForClassMethod { + argument_name, + is_new: true, + } + .into(), } } @@ -164,6 +184,7 @@ impl FunctionType { match self { Self::Method => "self", Self::ClassMethod => "cls", + Self::NewMethod => "cls", } } @@ -171,6 +192,7 @@ impl FunctionType { match self { Self::Method => Rule::InvalidFirstArgumentNameForMethod, Self::ClassMethod => Rule::InvalidFirstArgumentNameForClassMethod, + Self::NewMethod => Rule::InvalidFirstArgumentNameForClassMethod, } } } @@ -214,6 +236,11 @@ pub(crate) fn invalid_first_argument_name(checker: &Checker, scope: &Scope) { IsMetaclass::Maybe => return, }, function_type::FunctionType::ClassMethod => FunctionType::ClassMethod, + // In preview, this violation is caught by `PLW0211` instead + function_type::FunctionType::NewMethod if checker.settings.preview.is_enabled() => { + return; + } + function_type::FunctionType::NewMethod => FunctionType::NewMethod, }; if !checker.enabled(function_type.rule()) { return; diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__preview__N803_N804.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__preview__N803_N804.py.snap new file mode 100644 index 0000000000..719c39c477 --- /dev/null +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__preview__N803_N804.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pep8_naming/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 79bb1eafb7..6f484b72cb 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -442,6 +442,10 @@ mod tests { )] #[test_case(Rule::InvalidEnvvarDefault, Path::new("invalid_envvar_default.py"))] #[test_case(Rule::BadStrStripCall, Path::new("bad_str_strip_call.py"))] + #[test_case( + Rule::BadStaticmethodArgument, + Path::new("bad_staticmethod_argument.py") + )] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs index 21d49f3522..48ce24a798 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs @@ -11,6 +11,9 @@ use crate::checkers::ast::Checker; /// ## What it does /// Checks for static methods that use `self` or `cls` as their first argument. /// +/// If [`preview`] mode is enabled, this rule also applies to +/// `__new__` methods, which are implicitly static. +/// /// ## Why is this bad? /// [PEP 8] recommends the use of `self` and `cls` as the first arguments for /// instance methods and class methods, respectively. Naming the first argument @@ -72,9 +75,14 @@ pub(crate) fn bad_staticmethod_argument(checker: &Checker, scope: &Scope) { &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ); - if !matches!(type_, function_type::FunctionType::StaticMethod) { - return; - } + + match type_ { + function_type::FunctionType::StaticMethod => {} + function_type::FunctionType::NewMethod if checker.settings.preview.is_enabled() => {} + _ => { + return; + } + }; let Some(ParameterWithDefault { parameter: self_or_cls, diff --git a/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs index 4ec58b6a83..2f18de2054 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs @@ -10,6 +10,9 @@ use crate::checkers::ast::Checker; /// ## What it does /// Checks for assignment of `self` and `cls` in instance and class methods respectively. /// +/// This check also applies to `__new__` even though this is technically +/// a static method. +/// /// ## Why is this bad? /// The identifiers `self` and `cls` are conventional in Python for the first parameter of instance /// methods and class methods, respectively. Assigning new values to these variables can be @@ -102,6 +105,7 @@ pub(crate) fn self_or_cls_assignment(checker: &Checker, target: &Expr) { let method_type = match (function_type, self_or_cls.name().as_str()) { (FunctionType::Method { .. }, "self") => MethodType::Instance, (FunctionType::ClassMethod { .. }, "cls") => MethodType::Class, + (FunctionType::NewMethod, "cls") => MethodType::New, _ => return, }; @@ -134,6 +138,7 @@ fn check_expr(checker: &Checker, target: &Expr, method_type: MethodType) { enum MethodType { Instance, Class, + New, } impl MethodType { @@ -141,6 +146,7 @@ impl MethodType { match self { MethodType::Instance => "self", MethodType::Class => "cls", + MethodType::New => "cls", } } } @@ -150,6 +156,7 @@ impl std::fmt::Display for MethodType { match self { MethodType::Instance => f.write_str("instance"), MethodType::Class => f.write_str("class"), + MethodType::New => f.write_str("`__new__`"), } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs index 2e20e80cce..f64be8688d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs @@ -93,7 +93,9 @@ pub(crate) fn too_many_arguments(checker: &Checker, function_def: &ast::StmtFunc &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ), - function_type::FunctionType::Method | function_type::FunctionType::ClassMethod + function_type::FunctionType::Method + | function_type::FunctionType::ClassMethod + | function_type::FunctionType::NewMethod ) { // If so, we need to subtract one from the number of positional arguments, since the first // argument is always `self` or `cls`. diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs index 28300a5cb9..90a047c355 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs @@ -97,7 +97,9 @@ pub(crate) fn too_many_positional_arguments( &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ), - function_type::FunctionType::Method | function_type::FunctionType::ClassMethod + function_type::FunctionType::Method + | function_type::FunctionType::ClassMethod + | function_type::FunctionType::NewMethod ) { // If so, we need to subtract one from the number of positional arguments, since the first // argument is always `self` or `cls`. diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0211_bad_staticmethod_argument.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0211_bad_staticmethod_argument.py.snap index 30531d9efa..add63e311b 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0211_bad_staticmethod_argument.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0211_bad_staticmethod_argument.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pylint/mod.rs -snapshot_kind: text --- bad_staticmethod_argument.py:3:13: PLW0211 First argument of a static method should not be named `self` | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap index 2192975c9a..e7521e3977 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap @@ -150,3 +150,12 @@ self_or_cls_assignment.py:26:9: PLW0642 Reassigned `self` variable in instance m 28 | def ok(self) -> None: | = help: Consider using a different variable name + +self_or_cls_assignment.py:50:9: PLW0642 Reassigned `cls` variable in `__new__` method + | +48 | class Foo: +49 | def __new__(cls): +50 | cls = "apple" # PLW0642 + | ^^^ PLW0642 + | + = help: Consider using a different variable name diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0211_bad_staticmethod_argument.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0211_bad_staticmethod_argument.py.snap new file mode 100644 index 0000000000..9a18d7a33d --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0211_bad_staticmethod_argument.py.snap @@ -0,0 +1,37 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +bad_staticmethod_argument.py:3:13: PLW0211 First argument of a static method should not be named `self` + | +1 | class Wolf: +2 | @staticmethod +3 | def eat(self): # [bad-staticmethod-argument] + | ^^^^ PLW0211 +4 | pass + | + +bad_staticmethod_argument.py:15:13: PLW0211 First argument of a static method should not be named `cls` + | +13 | class Sheep: +14 | @staticmethod +15 | def eat(cls, x, y, z): # [bad-staticmethod-argument] + | ^^^ PLW0211 +16 | pass + | + +bad_staticmethod_argument.py:19:15: PLW0211 First argument of a static method should not be named `self` + | +18 | @staticmethod +19 | def sleep(self, x, y, z): # [bad-staticmethod-argument] + | ^^^^ PLW0211 +20 | pass + | + +bad_staticmethod_argument.py:55:17: PLW0211 First argument of a static method should not be named `self` + | +53 | # `self` but not with `cls` as first argument - see above). +54 | class Foo: +55 | def __new__(self, x, y, z): # [bad-staticmethod-argument] + | ^^^^ PLW0211 +56 | pass + | diff --git a/crates/ruff_python_semantic/src/analyze/function_type.rs b/crates/ruff_python_semantic/src/analyze/function_type.rs index a9ba29c0e5..49dd3d089d 100644 --- a/crates/ruff_python_semantic/src/analyze/function_type.rs +++ b/crates/ruff_python_semantic/src/analyze/function_type.rs @@ -11,6 +11,9 @@ pub enum FunctionType { Method, ClassMethod, StaticMethod, + /// `__new__` is an implicit static method but + /// is treated similarly to class methods for several lint rules + NewMethod, } /// Classify a function based on its scope, name, and decorators. @@ -30,17 +33,22 @@ pub fn classify( .any(|decorator| is_static_method(decorator, semantic, staticmethod_decorators)) { FunctionType::StaticMethod - } else if matches!(name, "__new__" | "__init_subclass__" | "__class_getitem__") // Special-case class method, like `__new__`. - || decorator_list.iter().any(|decorator| is_class_method(decorator, semantic, classmethod_decorators)) + } else if decorator_list + .iter() + .any(|decorator| is_class_method(decorator, semantic, classmethod_decorators)) { FunctionType::ClassMethod } else { - // It's an instance method. - FunctionType::Method + match name { + "__new__" => FunctionType::NewMethod, // Implicit static method. + "__init_subclass__" | "__class_getitem__" => FunctionType::ClassMethod, // Implicit class methods. + _ => FunctionType::Method, // Default to instance method. + } } } /// Return `true` if a [`Decorator`] is indicative of a static method. +/// Note: Implicit static methods like `__new__` are not considered. fn is_static_method( decorator: &Decorator, semantic: &SemanticModel, @@ -81,6 +89,7 @@ fn is_static_method( } /// Return `true` if a [`Decorator`] is indicative of a class method. +/// Note: Implicit class methods like `__init_subclass__` and `__class_getitem__` are not considered. fn is_class_method( decorator: &Decorator, semantic: &SemanticModel,