From 0d4f7dde9967704767d8515aadcf8c94353dde35 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 3 Sep 2025 19:55:20 +0200 Subject: [PATCH] [ty] Treat `__new__` as a static method (#20212) ## Summary Pull this out of https://github.com/astral-sh/ruff/pull/18473 as an isolated change to make sure it has no adverse effects. The wrong behavior is observable on `main` for something like ```py class C: def __new__(cls) -> "C": cls.x = 1 C.x # previously: Attribute `x` can only be accessed on instances # now: Type `` has no attribute `x` ``` where we currently treat `x` as an *instance* attribute (because we consider `__new__` to be a normal function and `cls` to be the "self" attribute). With this PR, we do not consider `x` to be an attribute, neither on the class nor on instances of `C`. If this turns out to be an important feature, we should add it intentionally, instead of accidentally. ## Test Plan Ecosystem checks. --- crates/ty_python_semantic/src/types/call/bind.rs | 3 +-- crates/ty_python_semantic/src/types/class.rs | 11 +++-------- crates/ty_python_semantic/src/types/function.rs | 6 ++++++ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index f9665cfc98..17bec1416b 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -290,8 +290,7 @@ impl<'db> Bindings<'db> { } _ => {} } - } else if function.has_known_decorator(db, FunctionDecorators::STATICMETHOD) - { + } else if function.is_staticmethod(db) { overload.set_return_type(Type::FunctionLiteral(function)); } else if let [Some(first), _] = overload.parameter_types() { if first.is_none(db) { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d678e749a2..1dbe288b3c 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -3,10 +3,8 @@ use std::sync::{LazyLock, Mutex}; use super::TypeVarVariance; use super::{ BoundTypeVarInstance, IntersectionBuilder, MemberLookupPolicy, Mro, MroError, MroIterator, - SpecialFormType, SubclassOfType, Truthiness, Type, TypeQualifiers, - class_base::ClassBase, - function::{FunctionDecorators, FunctionType}, - infer_expression_type, infer_unpack_types, + SpecialFormType, SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase, + function::FunctionType, infer_expression_type, infer_unpack_types, }; use crate::FxOrderMap; use crate::module_resolver::KnownModule; @@ -1257,10 +1255,7 @@ pub(super) enum MethodDecorator { impl MethodDecorator { fn try_from_fn_type(db: &dyn Db, fn_type: FunctionType) -> Result { - match ( - fn_type.is_classmethod(db), - fn_type.has_known_decorator(db, FunctionDecorators::STATICMETHOD), - ) { + match (fn_type.is_classmethod(db), fn_type.is_staticmethod(db)) { (true, true) => Err(()), // A method can't be static and class method at the same time. (true, false) => Ok(Self::ClassMethod), (false, true) => Ok(Self::StaticMethod), diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 3730439017..a6357d145e 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -731,6 +731,12 @@ impl<'db> FunctionType<'db> { ) } + /// Returns true if this method is decorated with `@staticmethod`, or if it is implicitly a + /// static method. + pub(crate) fn is_staticmethod(self, db: &'db dyn Db) -> bool { + self.has_known_decorator(db, FunctionDecorators::STATICMETHOD) || self.name(db) == "__new__" + } + /// If the implementation of this function is deprecated, returns the `@warnings.deprecated`. /// /// Checking if an overload is deprecated requires deeper call analysis.