diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index d6391b9465..766d70decd 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 4000, + 4057, ); static PYDANTIC: Benchmark = Benchmark::new( @@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 13116, + 13253, ); static TANJUN: Benchmark = Benchmark::new( diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 593b1008bf..ac5f934a51 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -3783,7 +3783,7 @@ quux. __module__ :: str __mul__ :: bound method Quux.__mul__(value: SupportsIndex, /) -> tuple[int | str, ...] __ne__ :: bound method Quux.__ne__(value: object, /) -> bool - __new__ :: (x: int, y: str) -> None + __new__ :: (x: int, y: str) -> Quux __orig_bases__ :: tuple[Any, ...] __reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...] __reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...] diff --git a/crates/ty_python_semantic/resources/mdtest/metaclass.md b/crates/ty_python_semantic/resources/mdtest/metaclass.md index cb2a18efe2..363657276a 100644 --- a/crates/ty_python_semantic/resources/mdtest/metaclass.md +++ b/crates/ty_python_semantic/resources/mdtest/metaclass.md @@ -91,6 +91,30 @@ Foo() # error: [missing-argument] reveal_type(Foo(1)) # revealed: Foo ``` +### Metaclass `__call__` with specific parameters + +When the metaclass `__call__` has specific parameters (not just `*args, **kwargs`), we should check +the metaclass `__call__` signature, even if the return type is the instance type. + +```py +from typing import TypeVar + +T = TypeVar("T") + +class Meta(type): + def __call__(cls: type[T], x: int) -> T: + return object.__new__(cls) + +class Foo(metaclass=Meta): + def __init__(self, x: int) -> None: + pass + +# The metaclass __call__ has specific parameters, so we check them. +Foo("wrong") # error: [invalid-argument-type] +Foo() # error: [missing-argument] +reveal_type(Foo(1)) # revealed: Foo +``` + ## Default ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 211a6876f4..2f97c128a7 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7211,13 +7211,11 @@ impl<'db> Type<'db> { _ => (None, None, self), }; - // Check for a custom `__call__` on the metaclass. If present and it returns a type - // that is not assignable to the class instance type, it takes precedence over the - // `__new__`/`__init__` logic (e.g., returning `int | Meta2` instead of `Class2`). - // - // However, if the metaclass `__call__` returns the class instance type (or a type - // variable that resolves to it), we still need to check `__new__`/`__init__` parameters, - // since the metaclass `__call__` is likely just delegating to `super().__call__()`. + // Check for a custom `__call__` on the metaclass. Following pyright's behavior: + // 1. If the metaclass has a custom `__call__` with a declared return type, validate it first + // 2. If there are argument errors, report them and return + // 3. If the return type is not assignable to the instance type, skip `__new__`/`__init__` + // 4. Otherwise, also validate `__new__`/`__init__` let metaclass_dunder_call = self_type.member_lookup_with_policy( db, "__call__".into(), @@ -7225,61 +7223,26 @@ impl<'db> Type<'db> { | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ); - if let Place::Defined(Type::BoundMethod(metaclass_dunder_call), _, boundness, _) = - metaclass_dunder_call.place - { - // Get the declared return type directly from the function signature. - // This avoids needing to perform argument matching just to determine the return type. - let signature = metaclass_dunder_call.function(db).signature(db); - let declared_return_type = signature - .overloads - .first() - .and_then(|sig| sig.return_ty) - .map(|ty| ty.filter_union(db, |elem| !elem.is_dynamic())) - .unwrap_or(Type::unknown()); - - // Get the instance type for comparison. - let instance_ty = self - .to_instance(db) - .expect("type should be convertible to instance type"); - - // Only use the metaclass `__call__` directly if: - // 1. The return type is not dynamic (has an explicit annotation). - // 2. The return type doesn't contain unresolved type variables (like `T`). - // 3. The return type is not assignable to the instance type. - // - // This handles cases like `Meta2.__call__` returning `int | Meta2`. - // If the return type has typevars (like `T` which would resolve to the instance), - // is dynamic, or is assignable to the instance, fall through to `__new__`/`__init__`. - if !declared_return_type.is_dynamic() - && !declared_return_type.has_typevar(db) - && !declared_return_type.is_assignable_to(db, instance_ty) + // Extract metaclass `__call__` info if it exists and has a declared return type. + let metaclass_call_info = + if let Place::Defined(Type::BoundMethod(metaclass_dunder_call), _, boundness, _) = + metaclass_dunder_call.place { - // Use the metaclass `__call__` for argument checking. - let bindings = Type::BoundMethod(metaclass_dunder_call).bindings(db); - let argument_types = infer_argument_types(Some(bindings.clone())); - - let call_result = bindings - .match_parameters(db, &argument_types) - .check_types(db, &argument_types, tcx, &[]) - .map_err(CallDunderError::from) - .and_then(|bindings| { - if boundness == Definedness::PossiblyUndefined { - Err(CallDunderError::PossiblyUnbound(Box::new(bindings))) - } else { - Ok(bindings) - } - }); - - return match call_result { - Ok(bindings) => Ok(bindings.return_type(db)), - Err(error) => Err(ConstructorCallError::MetaclassCall( - error.fallback_return_type(db), - error, - )), - }; - } - } + let signature = metaclass_dunder_call.function(db).signature(db); + // Only use metaclass `__call__` if it has a declared return type. + // If return type is unannotated, fall through to `__new__`/`__init__`. + let has_declared_return = signature + .overloads + .iter() + .any(|sig| sig.return_ty.is_some()); + if has_declared_return { + Some((metaclass_dunder_call, boundness)) + } else { + None + } + } else { + None + }; // The code below deals with interplay between `__new__` and `__init__` methods. // The logic is roughly as follows: @@ -7318,42 +7281,105 @@ impl<'db> Type<'db> { MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ); - // Infer the call argument types, using both `__new__` and `__init__` for type-context. - let bindings = match ( - new_method.as_ref().map(|method| &method.place), - &init_method.place, - ) { - (Some(Place::Defined(new_method, ..)), Place::Undefined) => Some( - new_method - .bindings(db) - .map(|binding| binding.with_bound_type(self_type)), - ), + // Infer the call argument types using the appropriate bindings for type-context. + // If metaclass `__call__` has a declared return type, use its bindings. + // Otherwise, use combined `__new__` and `__init__` bindings. + let bindings = if metaclass_call_info.is_some() { + // Use metaclass `__call__` bindings for argument inference. + metaclass_call_info + .as_ref() + .map(|(method, _)| Type::BoundMethod(*method).bindings(db)) + } else { + // Use `__new__` and `__init__` bindings for argument inference. + match ( + new_method.as_ref().map(|method| &method.place), + &init_method.place, + ) { + (Some(Place::Defined(new_method, ..)), Place::Undefined) => Some( + new_method + .bindings(db) + .map(|binding| binding.with_bound_type(self_type)), + ), - (Some(Place::Undefined) | None, Place::Defined(init_method, ..)) => { - Some(init_method.bindings(db)) + (Some(Place::Undefined) | None, Place::Defined(init_method, ..)) => { + Some(init_method.bindings(db)) + } + + (Some(Place::Defined(new_method, ..)), Place::Defined(init_method, ..)) => { + let callable = UnionBuilder::new(db) + .add(*new_method) + .add(*init_method) + .build(); + + let new_method_bindings = new_method + .bindings(db) + .map(|binding| binding.with_bound_type(self_type)); + + Some(Bindings::from_union( + callable, + [new_method_bindings, init_method.bindings(db)], + )) + } + + _ => None, } - - (Some(Place::Defined(new_method, ..)), Place::Defined(init_method, ..)) => { - let callable = UnionBuilder::new(db) - .add(*new_method) - .add(*init_method) - .build(); - - let new_method_bindings = new_method - .bindings(db) - .map(|binding| binding.with_bound_type(self_type)); - - Some(Bindings::from_union( - callable, - [new_method_bindings, init_method.bindings(db)], - )) - } - - _ => None, }; let argument_types = infer_argument_types(bindings); + // If metaclass `__call__` exists with a declared return type, validate it first. + // Following pyright's behavior: + // - If there are argument errors, return the metaclass error + // - If the return type is not assignable to the instance type, return the metaclass result + // - Otherwise, continue to validate `__new__`/`__init__` + if let Some((metaclass_dunder_call, boundness)) = metaclass_call_info { + let bindings = Type::BoundMethod(metaclass_dunder_call).bindings(db); + + let call_result = bindings + .clone() + .match_parameters(db, &argument_types) + .check_types(db, &argument_types, tcx, &[]); + + let metaclass_return_type = call_result + .as_ref() + .map_or_else(|err| err.1.return_type(db), |b| b.return_type(db)); + + // Get the instance type for comparison. + let instance_ty = self + .to_instance(db) + .expect("type should be convertible to instance type"); + + // Check if we should skip `__new__`/`__init__` evaluation. + // Skip if: return type is not assignable to instance, is Never, or contains Any. + let skip_new_init = !metaclass_return_type.is_assignable_to(db, instance_ty) + || metaclass_return_type.is_never() + || matches!(metaclass_return_type, Type::Dynamic(DynamicType::Any)); + + // If there are argument errors or we should skip `__new__`/`__init__`, return metaclass result. + if call_result.is_err() || skip_new_init { + let call_result = call_result + .map_err(CallDunderError::from) + .and_then(|bindings| { + if boundness == Definedness::PossiblyUndefined { + Err(CallDunderError::PossiblyUnbound(Box::new(bindings))) + } else { + Ok(bindings) + } + }); + + return match call_result { + Ok(bindings) => Ok(bindings.return_type(db)), + Err(error) => Err(ConstructorCallError::MetaclassCall( + error.fallback_return_type(db), + error, + )), + }; + } + + // Metaclass `__call__` succeeded and returns instance type. + // Continue to validate `__new__`/`__init__` below. + } + let new_call_outcome = new_method.and_then(|new_method| { match new_method.place.try_call_dunder_get(db, self_type) { Place::Defined(new_method, _, boundness, _) => {