From 2891955687248cfc83d1527df3525cc77952a736 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 26 Aug 2026 15:12:51 +0100 Subject: [PATCH] [ty] Preserve inferred types for unknown class-decorator results --- .../resources/mdtest/decorators.md | 95 ++++++- .../mdtest/generics/legacy/classes.md | 46 ++-- .../ty_python_semantic/src/types/callable.rs | 83 +----- crates/ty_python_semantic/src/types/class.rs | 12 +- .../src/types/class/static_literal.rs | 10 +- .../src/types/class/typed_dict.rs | 10 +- .../ty_python_semantic/src/types/function.rs | 12 +- .../ty_python_semantic/src/types/generics.rs | 7 +- .../src/types/infer/builder.rs | 27 +- .../src/types/infer/builder/class.rs | 259 ++---------------- .../src/types/match_pattern.rs | 3 +- crates/ty_python_semantic/src/types/method.rs | 19 +- .../src/types/protocol_class.rs | 1 - .../src/types/signatures.rs | 13 +- 14 files changed, 148 insertions(+), 449 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md index b28e878f16b774..4390d7ba495a95 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators.md @@ -388,6 +388,8 @@ def takes_int(x: int) -> int: # error: [invalid-argument-type] @takes_int class Foo: ... + +reveal_type(Foo) # revealed: int ``` Using `None` as a decorator is an error: @@ -396,6 +398,8 @@ Using `None` as a decorator is an error: # error: [call-non-callable] @None class Bar: ... + +reveal_type(Bar) # revealed: ``` A decorator can enforce type constraints on the class being decorated: @@ -457,7 +461,7 @@ reveal_type(DataclassThenWrapped) # revealed: WrapBackend class WrappedThenDataclass: value: int -reveal_type(WrappedThenDataclass) # revealed: Unknown +reveal_type(WrappedThenDataclass) # revealed: WrapBackend def int_decorator_factory() -> Callable[[type[object]], int]: def decorator(cls: type[object]) -> int: @@ -470,7 +474,7 @@ def int_decorator_factory() -> Callable[[type[object]], int]: class IntThenDataclass: value: int -reveal_type(IntThenDataclass) # revealed: Unknown +reveal_type(IntThenDataclass) # revealed: int @WrapBackend class InvalidWrappedBase(1): ... # error: [invalid-base] @@ -496,8 +500,8 @@ class OverloadedCacheClient: return b"" ``` -Unannotated class decorators are assumed to preserve the class binding. We do not infer returned -classes from decorator bodies: +When a class decorator returns `Unknown`, we preserve the current binding. Unannotated decorators +have an `Unknown` return type because we do not infer returned classes from decorator bodies: ```py def personify(cls): @@ -549,7 +553,13 @@ callable_decorator = CallableDecorator() class CallableInstanceDecorated: ... reveal_type(CallableInstanceDecorated) # revealed: +``` +An explicit return annotation can also produce `Unknown`, for example when a type variable is not +specialized. We preserve the class binding in these cases too, but use the return type when it is +known: + +```py class ExplicitReturnDecorator(Generic[T]): def __call__(self, cls) -> T: raise NotImplementedError @@ -559,7 +569,7 @@ explicit_return_decorator = ExplicitReturnDecorator() @explicit_return_decorator class ExplicitReturnCallableInstanceDecorated: ... -reveal_type(ExplicitReturnCallableInstanceDecorated) # revealed: Unknown +reveal_type(ExplicitReturnCallableInstanceDecorated) # revealed: specialized_explicit_return_decorator = ExplicitReturnDecorator[int]() @@ -578,7 +588,7 @@ def explicit_return_callable_decorator(cls) -> T: @explicit_return_callable_decorator class ExplicitReturnCallableDecorated: ... -reveal_type(ExplicitReturnCallableDecorated) # revealed: Unknown +reveal_type(ExplicitReturnCallableDecorated) # revealed: def regular_callable_replacement_factory() -> Callable[[type[object]], T]: raise NotImplementedError @@ -586,20 +596,28 @@ def regular_callable_replacement_factory() -> Callable[[type[object]], T]: @regular_callable_replacement_factory() class RegularCallableReplacementDecorated: ... -reveal_type(RegularCallableReplacementDecorated) # revealed: Unknown +reveal_type(RegularCallableReplacementDecorated) # revealed: ``` -An unknown class decorator still makes the class binding unknown: +An unknown class decorator preserves the class binding while still reporting the unresolved +reference: ```py # error: [unresolved-reference] "Name `unknown_class_decorator` used when not defined" @unknown_class_decorator -class UnknownDecorated: ... +class UnknownDecorated: + def method(self, value: int) -> str: + return str(value) -reveal_type(UnknownDecorated) # revealed: Unknown +reveal_type(UnknownDecorated) # revealed: +reveal_type(UnknownDecorated()) # revealed: UnknownDecorated +reveal_type(UnknownDecorated().method(1)) # revealed: str +UnknownDecorated().method("a") # error: [invalid-argument-type] ``` -An unannotated class decorator preserves the result of earlier decorators: +If an earlier decorator replaces the class with an instance, an `Unknown` return type preserves that +instance's type. This applies both to unannotated decorators and to decorators whose return +annotations evaluate to `Unknown`: ```py def unannotated_identity(cls): @@ -610,6 +628,12 @@ def unannotated_identity(cls): class WrappedThenUnannotated: ... reveal_type(WrappedThenUnannotated) # revealed: WrapBackend + +@explicit_return_decorator +@WrapBackend +class WrappedThenUnknown: ... + +reveal_type(WrappedThenUnknown) # revealed: WrapBackend ``` Metadata decorators still apply above an unannotated class-preserving decorator: @@ -627,6 +651,55 @@ class DeprecatedThenUnannotated: ... DeprecatedThenUnannotated() # error: [deprecated] "use OtherClass" ``` +## Unknown return annotations on class decorators + +An unresolved return annotation produces `Unknown`. This preserves the class binding and allows +outer metadata decorators to apply to the class: + +```py +from dataclasses import dataclass + +def decorator(cls: type) -> Missing: # error: [unresolved-reference] + return cls + +@dataclass +@decorator +class C: + value: int + +reveal_type(C) # revealed: +reveal_type(C(1).value) # revealed: int +C("a") # error: [invalid-argument-type] +``` + +## Explicitly dynamic class decorators + +Unlike `Unknown`, an explicit `Any` return type replaces the class binding: + +```py +from typing import Any + +def decorator(cls: type) -> Any: + return cls + +@decorator +class C: ... + +reveal_type(C) # revealed: Any +``` + +An explicit `type[Any]` return type also replaces the binding: + +```py +def class_decorator(cls: type) -> type[Any]: + return cls + +@class_decorator +class D: ... + +reveal_type(D) # revealed: type[Any] +``` + ## Preserving the original class object If a class decorator returns the original class object, we preserve the class binding so it can diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 3dd29f7416ee91..64dd47f5c54590 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -190,6 +190,28 @@ class Child(Base[T]): ... child: Child[int] ``` +## Unknown decorators on generic bases + +An unresolved decorator preserves the class binding and its generic context. A subclass can forward +a type variable to the decorated base and be specialized without a cascading error. + +```py +from typing import Generic, TypeVar +from ty_extensions._internal import generic_context + +T = TypeVar("T") + +# error: [unresolved-reference] "Name `unknown_decorator` used when not defined" +@unknown_decorator +class Base(Generic[T]): ... + +reveal_type(generic_context(Base)) # revealed: ty_extensions._internal.GenericContext[T@Base] + +class Child(Base[T]): ... + +child: Child[int] +``` + ## Specializing classes with unavailable generic context When an earlier error prevents ty from determining a class's generic context, specializing the class @@ -217,30 +239,6 @@ class Parser(typing.Generic[T]): ... parser: Parser[int] # error: [invalid-type-form] "Non-generic class `Parser` cannot be specialized in a type expression" ``` -### Decorated generic bases - -An unresolved decorator obscures the generic context of a base class. Specializing a subclass that -forwards a type variable to that base currently produces a cascading error. - -```py -from typing import Generic, TypeVar -from ty_extensions._internal import generic_context - -T = TypeVar("T") - -# error: [unresolved-reference] "Name `unknown_decorator` used when not defined" -@unknown_decorator -class Base(Generic[T]): ... - -reveal_type(generic_context(Base)) # revealed: None - -class Child(Base[T]): ... - -# TODO: Avoid this cascading error when the base's generic context is unavailable. -# error: [invalid-type-form] "Non-generic class `Child` cannot be specialized in a type expression" -child: Child[int] -``` - ### Unresolved generic bases ```py diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index 96f47fbe291f09..e2d6a031383a06 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -190,7 +190,6 @@ impl<'db> Type<'db> { db, CallableSignature::from_overloads(signatures), callable.kind(db), - callable.provenance(db), ) })) } @@ -211,7 +210,6 @@ impl<'db> Type<'db> { db, CallableSignature::from_overloads(signatures), callable.kind(db), - callable.provenance(db), )); } } @@ -256,7 +254,6 @@ impl<'db> Type<'db> { db, CallableSignature::from_overloads(method.signatures(db, env)), CallableTypeKind::Regular, - CallableFunctionProvenance::None, ))), Type::WrapperDescriptor(wrapper_descriptor) => { @@ -264,7 +261,6 @@ impl<'db> Type<'db> { db, CallableSignature::from_overloads(wrapper_descriptor.signatures(db, env)), CallableTypeKind::Regular, - CallableFunctionProvenance::None, ))) } @@ -356,33 +352,6 @@ pub enum CallableTypeKind { ParamSpecValue, } -/// Source-function provenance retained by a callable signature. -/// -/// A [`CallableType`] can describe a bare callable shape, such as one from `Callable[...]`. For -/// function-like sources, such as a [`FunctionType`] upcast to a [`CallableType`] or a lambda, this -/// records whether the source function has an explicit return annotation. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] -pub enum CallableFunctionProvenance { - /// The callable does not retain source-function provenance. - None, - - /// The callable came from a function without an explicit return annotation. - ImplicitReturn, - - /// The callable came from a function with an explicit return annotation. - ExplicitReturn, -} - -impl CallableFunctionProvenance { - pub(crate) fn from_function_return_annotation(has_explicit_return_annotation: bool) -> Self { - if has_explicit_return_annotation { - Self::ExplicitReturn - } else { - Self::ImplicitReturn - } - } -} - /// A "policy" enum that describes how `type[]` types should be upcast /// to `Callable` types. /// @@ -438,21 +407,6 @@ pub struct CallableType<'db> { #[returns(copy)] pub(super) kind: CallableTypeKind, - - /// Source-function return-annotation provenance retained by this callable. - /// - /// Function-like values can retain their source-function provenance when converted to a - /// callable signature: - /// ```python - /// def decorator(cls) -> object: ... - /// ``` - /// - /// Callables that are only known from a callable shape do not retain that provenance: - /// ```python - /// def decorator_factory() -> Callable[[type[object]], object]: ... - /// ``` - #[returns(copy)] - pub(crate) provenance: CallableFunctionProvenance, } pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -474,7 +428,6 @@ impl<'db> CallableType<'db> { db, CallableSignature::single(signature), CallableTypeKind::Regular, - CallableFunctionProvenance::None, ) } @@ -483,7 +436,6 @@ impl<'db> CallableType<'db> { db, CallableSignature::single(signature), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, ) } @@ -492,7 +444,6 @@ impl<'db> CallableType<'db> { db, CallableSignature::single(Signature::new(parameters, Type::unknown())), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, ) } @@ -532,12 +483,7 @@ impl<'db> CallableType<'db> { } pub(crate) fn into_regular(self, db: &'db dyn Db) -> CallableType<'db> { - CallableType::new( - db, - self.signatures(db), - CallableTypeKind::Regular, - self.provenance(db), - ) + CallableType::new(db, self.signatures(db), CallableTypeKind::Regular) } /// Returns the reduced callable produced by partially applying selected overloads. @@ -550,7 +496,6 @@ impl<'db> CallableType<'db> { db, CallableSignature::partially_apply(db, env, overloads)?, CallableTypeKind::Regular, - CallableFunctionProvenance::None, )) } @@ -589,26 +534,15 @@ impl<'db> CallableType<'db> { db, self.signatures(db).bind_self(db, env, self_type), self.kind(db), - self.provenance(db), ) } pub(crate) fn into_function_like(self, db: &'db dyn Db) -> CallableType<'db> { - CallableType::new( - db, - self.signatures(db), - CallableTypeKind::FunctionLike, - self.provenance(db), - ) + CallableType::new(db, self.signatures(db), CallableTypeKind::FunctionLike) } pub(crate) fn into_dunder_paramspec(self, db: &'db dyn Db) -> CallableType<'db> { - CallableType::new( - db, - self.signatures(db), - CallableTypeKind::DunderParamSpec, - self.provenance(db), - ) + CallableType::new(db, self.signatures(db), CallableTypeKind::DunderParamSpec) } pub(crate) fn apply_self( @@ -632,7 +566,6 @@ impl<'db> CallableType<'db> { self.signatures(db) .apply_self_with_receiver(db, env, receiver_type, self_type), self.kind(db), - self.provenance(db), ) } @@ -641,12 +574,7 @@ impl<'db> CallableType<'db> { /// Specifically, this represents a callable type with a single signature: /// `(*args: object, **kwargs: object) -> Never`. pub(crate) fn bottom(db: &'db dyn Db) -> CallableType<'db> { - Self::new( - db, - CallableSignature::bottom(), - CallableTypeKind::Regular, - CallableFunctionProvenance::None, - ) + Self::new(db, CallableSignature::bottom(), CallableTypeKind::Regular) } pub(super) fn recursive_type_normalized_impl( @@ -661,7 +589,6 @@ impl<'db> CallableType<'db> { self.signatures(db) .recursive_type_normalized_impl(db, env, div, nested)?, self.kind(db), - self.provenance(db), )) } @@ -681,7 +608,6 @@ impl<'db> CallableType<'db> { self.signatures(db) .apply_type_mapping_impl(db, type_mapping, tcx, visitor), self.kind(db), - self.provenance(db), ) } @@ -780,7 +706,6 @@ impl<'db> CallableTypes<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::Regular, - CallableFunctionProvenance::None, ) .into_precise_functools_partial_instance(db, wrapped) } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 28d39c412429d5..9d7f0031b03c33 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -26,7 +26,7 @@ use super::{ }; use super::{TypeVarVariance, display}; use crate::place::{DefinedPlace, Provenance, TypeOrigin}; -use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; +use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, }; @@ -2067,7 +2067,6 @@ impl<'db> ClassType<'db> { db, getitem_signature, CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, )); Member::definitely_declared(getitem_type) }) @@ -2383,12 +2382,8 @@ impl<'db> ClassType<'db> { .iter() .any(|signature| !signature.return_ty.is_assignable_to(db, env, instance_type)); - let dunder_new_bound_method = CallableType::new( - db, - bound_signature, - CallableTypeKind::Regular, - CallableFunctionProvenance::None, - ); + let dunder_new_bound_method = + CallableType::new(db, bound_signature, CallableTypeKind::Regular); if returns_non_subclass { return CallableTypes::one(dunder_new_bound_method); @@ -2462,7 +2457,6 @@ impl<'db> ClassType<'db> { db, synthesized_dunder_init_signature, CallableTypeKind::Regular, - CallableFunctionProvenance::None, )) } else { None diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index 1149c3f901f95f..9978290bd3f570 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -29,7 +29,7 @@ use crate::{ UnionBuilder, UnionType, bound_super::BoundSuperType, call::{CallError, CallErrorKind}, - callable::{CallableFunctionProvenance, CallableTypeKind}, + callable::CallableTypeKind, class::{ ClassInstanceFlags, ClassMemberResult, ClassMetaclass, CodeGeneratorKind, DisjointBase, DynamicTypedDictLiteral, Field, FieldKind, InstanceMemberResult, MetaclassError, @@ -1675,12 +1675,7 @@ impl<'db> StaticClassLiteral<'db> { ) }), ); - CallableType::new( - db, - signatures, - CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, - ) + CallableType::new(db, signatures, CallableTypeKind::FunctionLike) }); return Some(synthesized_callables.into_type(db, env)); @@ -2218,7 +2213,6 @@ impl<'db> StaticClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, ))) } diff --git a/crates/ty_python_semantic/src/types/class/typed_dict.rs b/crates/ty_python_semantic/src/types/class/typed_dict.rs index f01eecce5384a7..618c53551430a6 100644 --- a/crates/ty_python_semantic/src/types/class/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/class/typed_dict.rs @@ -10,7 +10,7 @@ use ty_module_resolver::KnownModule; use crate::place::PlaceAndQualifiers; use crate::place::known_module_symbol; -use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; +use crate::types::callable::CallableTypeKind; use crate::types::class::{ DynamicClassHeaderAnchor, DynamicClassScopeOffset, dynamic_class_header_range, }; @@ -220,7 +220,6 @@ fn synthesize_typed_dict_init<'db>( db, CallableSignature::from_overloads([map_overload, keyword_overload]), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, )) } @@ -262,7 +261,6 @@ fn synthesize_typed_dict_getitem<'db>( db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, )) } @@ -322,7 +320,6 @@ fn synthesize_typed_dict_setitem<'db>( db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, )) } @@ -376,7 +373,6 @@ fn synthesize_typed_dict_delitem<'db>( db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, )) } @@ -504,7 +500,6 @@ fn synthesize_typed_dict_get<'db>( db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, )) } @@ -654,7 +649,6 @@ fn synthesize_typed_dict_pop<'db>( db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, )) } @@ -705,7 +699,6 @@ fn synthesize_typed_dict_setdefault<'db>( db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, )) } @@ -826,7 +819,6 @@ fn synthesize_typed_dict_merge<'db>( db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, )) } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index c9d863297a7fcd..3cd8af28903537 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -67,7 +67,7 @@ use ty_module_resolver::{ImportingFile, KnownModule, ModuleName, file_to_module, use crate::place::{DefinedPlace, Definedness, Place, place_from_bindings}; use crate::types::call::{Binding, CallArguments}; -use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; +use crate::types::callable::CallableTypeKind; use crate::types::constraints::ConstraintSet; use crate::types::context::InferContext; use crate::types::cyclic::ActiveRecursionDetector; @@ -1233,7 +1233,6 @@ impl<'db> FunctionType<'db> { .signatures(db) .with_inherited_generic_context(db, inherited_generic_context), callable.kind(db), - callable.provenance(db), ) }) .collect() @@ -1657,14 +1656,7 @@ impl<'db> FunctionType<'db> { /// Convert the `FunctionType` into a [`CallableType`]. pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { - CallableType::new( - db, - self.signature(db), - self.callable_type_kind(db), - CallableFunctionProvenance::from_function_return_annotation( - self.has_explicit_return_annotation(db), - ), - ) + CallableType::new(db, self.signature(db), self.callable_type_kind(db)) } /// Convert the `FunctionType` into a [`BoundMethodType`]. diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 3ce3a996961b3f..1c063397f0cb33 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -730,12 +730,7 @@ impl<'db> GenericContext<'db> { ); let signatures = signatures.with_inherited_generic_context(db, generic_context); - let replacement = CallableType::new( - db, - signatures, - callable.kind(db), - callable.provenance(db), - ); + let replacement = CallableType::new(db, signatures, callable.kind(db)); Some((callable, replacement)) }) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index af6e1e33a52302..e798aee176ee05 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -53,7 +53,7 @@ use crate::types::call::bind::{ ArgumentTypeContext, CheckTypesMode, OverloadSet, requires_overload_evaluation, }; use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind}; -use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; +use crate::types::callable::CallableTypeKind; use crate::types::class::{ ClassLiteral, CodeGeneratorKind, FrozenDataclassDispatch, MethodDecorator, }; @@ -5412,20 +5412,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { env: &ProgramEnvironment<'d>, ty: Type<'d>, kind: CallableTypeKind, - provenance: CallableFunctionProvenance, ) -> Option> { match ty { Type::Callable(callable) => Some(Type::Callable(CallableType::new( db, callable.signatures(db), kind, - provenance, ))), Type::Union(union) => union.try_map(db, env, |element| { - propagate_callable_kind(db, env, *element, kind, provenance) + propagate_callable_kind(db, env, *element, kind) }), Type::TypeAlias(alias) => { - propagate_callable_kind(db, env, alias.value_type(db), kind, provenance) + propagate_callable_kind(db, env, alias.value_type(db), kind) } // Intersections are currently not handled here because that would require // the decorator to be explicitly annotated as returning an intersection. @@ -5470,21 +5468,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // computing the signature requires evaluating those defaults which may trigger // deferred inference. let propagatable_kind = match decorated_ty { - Type::FunctionLiteral(func) => Some(( - func.callable_type_kind(self.db()), - CallableFunctionProvenance::from_function_return_annotation( - func.has_explicit_return_annotation(self.db()), - ), - )), + Type::FunctionLiteral(func) => Some(func.callable_type_kind(db)), _ => decorated_ty .try_upcast_to_callable(db, env) .and_then(CallableTypes::exactly_one) .and_then(|callable| match callable.kind(self.db()) { kind @ (CallableTypeKind::FunctionLike | CallableTypeKind::StaticMethodLike - | CallableTypeKind::ClassMethodLike) => { - Some((kind, callable.provenance(self.db()))) - } + | CallableTypeKind::ClassMethodLike) => Some(kind), _ => None, }), }; @@ -5514,9 +5505,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // a `Callable`-typed decorator" in `callables_as_descriptors.md` for the // extended explanation. let inferred_ty = propagatable_kind - .and_then(|(kind, provenance)| { - propagate_callable_kind(db, env, return_ty, kind, provenance) - }) + .and_then(|kind| propagate_callable_kind(db, env, return_ty, kind)) .unwrap_or(return_ty); if let Some(decorated_function) = decorated_function @@ -6692,7 +6681,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .with_definition(signature.definition()) }), ); - CallableType::new(db, signatures, callable.kind(db), callable.provenance(db)) + CallableType::new(db, signatures, callable.kind(db)) }); let inferable = class_generic_context.inferable_typevars(db); let constraints = ConstraintSetBuilder::new(); @@ -8657,7 +8646,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.db(), CallableSignature::single(Signature::new(parameters, return_ty)), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::ImplicitReturn, )) } @@ -8756,7 +8744,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { db, CallableSignature::from_overloads(getitem_overloads), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, ), )], ); diff --git a/crates/ty_python_semantic/src/types/infer/builder/class.rs b/crates/ty_python_semantic/src/types/infer/builder/class.rs index d239987adfed01..fd75502d832f24 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/class.rs @@ -1,11 +1,9 @@ use crate::Db; use crate::ProgramEnvironment; -use crate::place::Place; use crate::types::{ - CallArguments, DataclassParams, KnownClass, KnownInstanceType, MemberLookupPolicy, - SpecialFormType, StaticClassLiteral, SubclassOfType, Type, TypeContext, TypingModule, + CallArguments, DataclassParams, KnownClass, KnownInstanceType, SpecialFormType, + StaticClassLiteral, SubclassOfType, Type, TypeContext, TypingModule, call::CallError, - callable::CallableFunctionProvenance, function::KnownFunction, infer::{ TypeInferenceBuilder, @@ -166,11 +164,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )), } }; - let decorator_call_ty = |decorator: &ast::Decorator| match &decorator.expression { - ast::Expr::Call(call) => Some(self.expression_type(&call.func)), - _ => None, - }; - // In the first pass, collect metadata decorators that shape the original class object. // Once an inner decorator replaces the public binding, outer decorators are ordinary // runtime applications only: they cannot retroactively add metadata to the original class. @@ -283,17 +276,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Ok(return_ty) => *return_ty, Err(error) => error.return_type(db, env), }; - if is_unknown_decorator_result(db, decorated_ty) { - if !preserve_binding_for_unknown_result( - db, - env, - decorator_ty, - decorator_call_ty(decorator), - decorated_ty, - ) { - metadata_applies_to_original_class = false; - } - } else if !type_retains_original_class(db, env, original_class_ty, decorated_ty) { + if !is_unknown_decorator_result(db, decorated_ty) + && !type_retains_original_class(db, env, original_class_ty, decorated_ty) + { metadata_applies_to_original_class = false; } @@ -340,17 +325,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::DataclassDecorator(_) | Type::DataclassTransformer(_) => Type::unknown(), decorated_ty => decorated_ty, }; - // If a class decorator application loses all precision, preserve the original class - // binding for decorators known to preserve unknown results. - let should_preserve_binding = is_unknown_decorator_result(db, decorated_ty) - && preserve_binding_for_unknown_result( - db, - env, - decorator_ty, - decorator_call_ty(decorator_node), - decorated_ty, - ); - inferred_ty = if should_preserve_binding { + inferred_ty = if is_unknown_decorator_result(db, decorated_ty) { inferred_ty } else if class_decorator_preserves_class_binding( db, @@ -541,218 +516,18 @@ fn type_retains_original_class<'db>( } } -/// Return true if an unknown class-decorator result should leave the current class type in place. -/// -/// This handles both direct decorators and decorator factories: -/// ```python -/// def decorator(cls): -/// return cls -/// -/// def decorator_factory(): -/// return decorator -/// -/// @decorator_factory() -/// class C: ... -/// ``` -/// -/// The factory case needs the type of the call target, because the type of -/// `@decorator_factory()` is the returned decorator, while the expression type of -/// `decorator_factory` carries the static information that tells us whether an unknown result can -/// be preserved. -fn preserve_binding_for_unknown_result<'db>( - db: &'db dyn Db, - env: &ProgramEnvironment<'db>, - decorator_ty: Type<'db>, - decorator_call_ty: Option>, - decorator_result_ty: Type<'db>, -) -> bool { - ClassDecoratorUnknownResultPolicy::from_decorator(db, env, decorator_ty, decorator_result_ty) - == ClassDecoratorUnknownResultPolicy::PreserveBinding - || decorator_call_ty.is_some_and(|ty| { - ClassDecoratorUnknownResultPolicy::from_decorator(db, env, ty, decorator_result_ty) - == ClassDecoratorUnknownResultPolicy::PreserveBinding - }) -} - -/// Return true if applying a class decorator produced no useful replacement type. -fn is_unknown_decorator_result<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { - ty.is_unknown() || is_unknown_class_object_decorator_result(db, ty) -} - -/// Return true if applying a class decorator produced an unknown class-object type. -/// -/// Besides plain `Unknown`, class decorators can produce unknown class-object types such as -/// `type[Any]`. Those are represented as a `SubclassOf` dynamic type, but they should trigger the -/// same preservation fallback as an unknown result: -/// ```python -/// from typing import Any -/// -/// def decorator(cls) -> type[Any]: ... +/// Return true if a class-decorator result should leave the current binding unchanged. /// -/// @decorator -/// class C: ... -/// ``` -fn is_unknown_class_object_decorator_result<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { - let Type::SubclassOf(subclass_of) = ty.resolve_type_alias(db) else { - return false; - }; - - subclass_of - .subclass_of() - .into_dynamic() - .is_some_and(|dynamic| Type::Dynamic(dynamic).is_unknown()) -} - -/// Policy for class decorators whose application result is unknown. -/// -/// This is only consulted after applying the decorator produced no useful replacement type. If the -/// decorator itself statically suggests an unannotated identity-preserving shape, we keep the -/// current class binding; if it explicitly promises a replacement type, or if the decorator is -/// unknown, we let the unknown result replace the binding. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum ClassDecoratorUnknownResultPolicy { - /// Preserve the current class binding when the decorator result is unknown. - PreserveBinding, - /// Use the unknown decorator result as the public binding. - ReplaceBinding, -} - -impl ClassDecoratorUnknownResultPolicy { - /// Infer the unknown-result policy from the decorator's own type. - /// - /// Unannotated function and method decorators are treated as class-preserving when their - /// application result is unknown. Explicit return annotations are trusted as replacement - /// intent. - fn from_decorator<'db>( - db: &'db dyn Db, - env: &ProgramEnvironment<'db>, - decorator_ty: Type<'db>, - decorator_result_ty: Type<'db>, - ) -> Self { - if decorator_ty.is_unknown() { - return Self::ReplaceBinding; - } - - Self::known_from_decorator(db, env, decorator_ty, decorator_result_ty) - .unwrap_or(Self::ReplaceBinding) - } - - /// Return the known preservation policy for a class decorator, if one can be read statically. - /// - /// For unknown decorator results, unannotated functions are treated as likely - /// identity-preserving: - /// ```python - /// def decorator(cls): - /// return cls - /// ``` - /// - /// Explicit return annotations are trusted instead: - /// ```python - /// def decorator(cls) -> object: - /// return object() - /// ``` - /// - /// Callable instances and protocols delegate the decision to their `__call__` member, because - /// the decorator value itself is not the function that receives the class. - fn known_from_decorator<'db>( - db: &'db dyn Db, - env: &ProgramEnvironment<'db>, - decorator_ty: Type<'db>, - decorator_result_ty: Type<'db>, - ) -> Option { - match decorator_ty { - Type::FunctionLiteral(function) => { - Some(if function.has_explicit_return_annotation(db) { - Self::ReplaceBinding - } else { - Self::PreserveBinding - }) - } - Type::BoundMethod(method) => { - Some(if method.function(db).has_explicit_return_annotation(db) { - Self::ReplaceBinding - } else { - Self::PreserveBinding - }) - } - Type::NominalInstance(_) | Type::ProtocolInstance(_) => { - let call_symbol = decorator_ty - .member_lookup_with_policy( - db, - env, - "__call__", - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place; - - if let Place::Defined(place) = call_symbol - && place.is_definitely_defined() - { - Some( - Self::known_from_decorator(db, env, place.ty, decorator_result_ty) - .unwrap_or(Self::ReplaceBinding), - ) - } else { - Some(Self::ReplaceBinding) - } - } - Type::Union(union) => Some( - if union.elements(db).iter().all(|element| { - Self::known_from_decorator(db, env, *element, decorator_result_ty) - == Some(Self::PreserveBinding) - }) { - Self::PreserveBinding - } else { - Self::ReplaceBinding - }, - ), - Type::TypeAlias(alias) => Some( - Self::known_from_decorator(db, env, alias.value_type(db), decorator_result_ty) - .unwrap_or(Self::ReplaceBinding), - ), - Type::Callable(callable) => Some(match callable.provenance(db) { - // An unannotated function preserves the class binding when applying it loses the - // concrete return type: - // ```python - // decorator = lambda cls: cls - // - // @decorator - // class C: ... - // ``` - CallableFunctionProvenance::ImplicitReturn => Self::PreserveBinding, - // An explicit return annotation can intentionally replace the class binding: - // ```python - // def decorator[T](cls) -> T: ... - // - // @decorator - // class C: ... - // ``` - CallableFunctionProvenance::ExplicitReturn => Self::ReplaceBinding, - // Generic class-preserving decorator factories can lose the concrete class in - // their returned `Callable`, while still producing an unknown class-object result: - // ```python - // def identity_factory[T]() -> Callable[[type[T]], type[T]]: ... - // - // @identity_factory() - // class C: ... - // ``` - CallableFunctionProvenance::None - if is_unknown_class_object_decorator_result(db, decorator_result_ty) => - { - Self::PreserveBinding - } - // An ordinary `Callable` replacement result has no function provenance to justify - // the unannotated-function preservation fallback: - // ```python - // def replacement_factory[T]() -> Callable[[type[object]], T]: ... - // - // @replacement_factory() - // class C: ... - // ``` - CallableFunctionProvenance::None => Self::ReplaceBinding, - }), - _ => None, - } +/// This also handles `type[Unknown]` results from generic decorator factories whose type +/// variables are specialized before the returned decorator receives the class. Explicit `Any` +/// results do not trigger this fallback. +fn is_unknown_decorator_result<'db>(db: &'db dyn Db, result_ty: Type<'db>) -> bool { + match result_ty.resolve_type_alias(db) { + Type::SubclassOf(subclass_of) => subclass_of + .subclass_of() + .into_dynamic() + .is_some_and(|dynamic| Type::Dynamic(dynamic).is_unknown()), + result_ty => result_ty.is_unknown(), } } diff --git a/crates/ty_python_semantic/src/types/match_pattern.rs b/crates/ty_python_semantic/src/types/match_pattern.rs index 4c8db1f59f839e..fd7841ea7b7f9d 100644 --- a/crates/ty_python_semantic/src/types/match_pattern.rs +++ b/crates/ty_python_semantic/src/types/match_pattern.rs @@ -9,7 +9,7 @@ use ty_python_core::predicate::{ }; use crate::place::{DefinedPlace, Place}; -use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; +use crate::types::callable::CallableTypeKind; use crate::types::equality::{ ComparisonSoundnessPolicy, evaluate_type_equality, is_same_enum_domain, }; @@ -169,7 +169,6 @@ fn sequence_pattern_getitem_method<'db>( db, CallableSignature::from_overloads(overloads.chain(fallback_overload)), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::None, ) } diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs index 570111fbd008a6..05df72fd936ea4 100644 --- a/crates/ty_python_semantic/src/types/method.rs +++ b/crates/ty_python_semantic/src/types/method.rs @@ -7,13 +7,9 @@ use crate::{ types::{ CallableType, KnownClass, LiteralValueType, LiteralValueTypeKind, Parameter, Parameters, PropertyInstanceType, Signature, StringLiteralType, Type, TypeFormType, UnionType, - callable::{CallableFunctionProvenance, CallableTypeKind}, - constraints::ConstraintSet, - function::FunctionType, - known_instance::InternedConstraintSet, - relation::TypeRelationChecker, - signatures::CallableSignature, - visitor, + callable::CallableTypeKind, constraints::ConstraintSet, function::FunctionType, + known_instance::InternedConstraintSet, relation::TypeRelationChecker, + signatures::CallableSignature, visitor, }, }; @@ -101,14 +97,10 @@ impl<'db> BoundMethodType<'db> { heap_size=ruff_memory_usage::heap_size )] pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { - let function = self.function(db); CallableType::new( db, self.bound_signatures(db), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::from_function_return_annotation( - function.has_explicit_return_annotation(db), - ), ) } @@ -120,15 +112,10 @@ impl<'db> BoundMethodType<'db> { receiver_type: Type<'db>, typing_self_type: Type<'db>, ) -> CallableType<'db> { - let function = self.function(db); - CallableType::new( db, self.bound_signatures_with_receiver(db, env, receiver_type, typing_self_type), CallableTypeKind::FunctionLike, - CallableFunctionProvenance::from_function_return_annotation( - function.has_explicit_return_annotation(db), - ), ) } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index b22cc081c16065..e937ec232a1231 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -1766,7 +1766,6 @@ impl<'db> ProtocolMemberKind<'db> { db, signatures, current_callable.kind(db), - current_callable.provenance(db), ))), kind, ) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index e886418b9f490e..1ddd12f126fc11 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -21,7 +21,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::{SmallVec, smallvec_inline}; use super::{DynamicType, Type, TypeVarVariance, UnionType, semantic_index}; -use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; +use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, OwnedConstraintSet, PathBounds, Solutions, @@ -1915,7 +1915,6 @@ impl<'db> Signature<'db> { ) })), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, @@ -2195,7 +2194,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }, )), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, @@ -2250,7 +2248,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }), ), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_matches = ConstraintSet::constrain_typevar_lower_bound( db, @@ -2837,7 +2834,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { Type::unknown(), )), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_lower_bound( db, @@ -2868,7 +2864,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { Type::unknown(), )), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, @@ -2998,7 +2993,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .with_source_overload_index(source.source_overload_index()), ), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_lower_bound( @@ -3027,7 +3021,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .with_source_overload_index(target.source_overload_index()), ), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_upper_bound( @@ -3067,7 +3060,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .with_source_overload_index(source.source_overload_index()), ), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_matches = ConstraintSet::constrain_typevar_lower_bound( db, @@ -3214,7 +3206,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .with_source_overload_index(source.source_overload_index()), ), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_lower_bound( db, @@ -3242,7 +3233,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .with_source_overload_index(target.source_overload_index()), ), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, @@ -3359,7 +3349,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .with_source_overload_index(target.source_overload_index()), ), CallableTypeKind::ParamSpecValue, - CallableFunctionProvenance::None, )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_upper_bound( db,