From 24404d199e20a0983e376d61f70c409461cc6249 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 22 Dec 2025 13:01:02 -0500 Subject: [PATCH 01/12] Use gradual form (...) for parameters in Callable bottom type --- .../resources/mdtest/narrow/callable.md | 50 +++++++++++++++++++ .../resources/mdtest/narrow/isinstance.md | 3 +- .../src/types/signatures.rs | 5 +- 3 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/narrow/callable.md diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md new file mode 100644 index 00000000000000..260a16c0fc4103 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -0,0 +1,50 @@ +# Narrowing for `callable()` + +## Basic narrowing + +The `callable()` builtin returns `TypeIs[Callable[..., object]]`, which narrows the type to the +intersection with `Callable[..., object]`. + +```py +from typing import Any, Callable + +def f(x: Callable[..., Any] | None): + if callable(x): + # The intersection of `Callable[..., Any]` with `Callable[..., object]` preserves + # the gradual parameters (`...`). Previously this was incorrectly narrowed to + # `((...) -> Any) & (() -> object)` because the top materialization of gradual + # parameters was incorrectly `[]` instead of `...`. + reveal_type(x) # revealed: ((...) -> Any) & ((...) -> object) + else: + reveal_type(x) # revealed: (((...) -> Any) & ~((...) -> object)) | None +``` + +## Narrowing with other callable types + +```py +from typing import Any, Callable + +def g(x: Callable[[int], str] | None): + if callable(x): + reveal_type(x) # revealed: ((int, /) -> str) & ((...) -> object) + else: + reveal_type(x) # revealed: (((int, /) -> str) & ~((...) -> object)) | None + +def h(x: Callable[..., int] | None): + if callable(x): + reveal_type(x) # revealed: ((...) -> int) & ((...) -> object) + else: + reveal_type(x) # revealed: (((...) -> int) & ~((...) -> object)) | None +``` + +## Narrowing from object + +```py +from typing import Callable + +def f(x: object): + if callable(x): + reveal_type(x) # revealed: (...) -> object + else: + reveal_type(x) # revealed: ~((...) -> object) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 4a78fd8982ae74..76c7f6e9835870 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -213,8 +213,7 @@ def f(x: dict[str, int] | list[str], y: object): reveal_type(x) # revealed: list[str] if isinstance(y, t.Callable): - # TODO: a better top-materialization for `Callable`s (https://github.com/astral-sh/ty/issues/1426) - reveal_type(y) # revealed: () -> object + reveal_type(y) # revealed: (...) -> object ``` ## Class types diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 33266f8ddea59c..b9c03322764944 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -2058,11 +2058,8 @@ impl<'db> Parameters<'db> { TypeMapping::Materialize(MaterializationKind::Top) if self.is_gradual() => { Parameters::object() } - // TODO: This is wrong, the empty Parameters is not a subtype of all materializations. - // The bottom materialization is not currently representable and implementing it - // properly requires extending the Parameters struct. TypeMapping::Materialize(MaterializationKind::Bottom) if self.is_gradual() => { - Parameters::empty() + Parameters::gradual_form() } _ => Self { value: self From 9e58e6f739c862dce73d644e0a5e314864796aa4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 22 Dec 2025 19:22:54 -0500 Subject: [PATCH 02/12] Use a top --- .../resources/mdtest/narrow/callable.md | 36 +++++-- .../resources/mdtest/narrow/isinstance.md | 2 +- crates/ty_python_semantic/src/types.rs | 100 +++++++++++++++++- .../ty_python_semantic/src/types/call/bind.rs | 1 + crates/ty_python_semantic/src/types/class.rs | 10 ++ .../ty_python_semantic/src/types/display.rs | 23 +++- .../ty_python_semantic/src/types/function.rs | 2 +- .../src/types/infer/builder.rs | 1 + .../src/types/protocol_class.rs | 1 + .../src/types/signatures.rs | 72 +++++++++---- 10 files changed, 214 insertions(+), 34 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index 260a16c0fc4103..98cfcd751f7d05 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -3,20 +3,21 @@ ## Basic narrowing The `callable()` builtin returns `TypeIs[Callable[..., object]]`, which narrows the type to the -intersection with `Callable[..., object]`. +intersection with `Top[Callable[..., object]]`. The `Top[...]` wrapper indicates this is a fully +static type representing the top materialization of a gradual callable. ```py from typing import Any, Callable def f(x: Callable[..., Any] | None): if callable(x): - # The intersection of `Callable[..., Any]` with `Callable[..., object]` preserves + # The intersection of `Callable[..., Any]` with `Top[Callable[..., object]]` preserves # the gradual parameters (`...`). Previously this was incorrectly narrowed to # `((...) -> Any) & (() -> object)` because the top materialization of gradual # parameters was incorrectly `[]` instead of `...`. - reveal_type(x) # revealed: ((...) -> Any) & ((...) -> object) + reveal_type(x) # revealed: ((...) -> Any) & (Top[(...) -> object]) else: - reveal_type(x) # revealed: (((...) -> Any) & ~((...) -> object)) | None + reveal_type(x) # revealed: (((...) -> Any) & ~(Top[(...) -> object])) | None ``` ## Narrowing with other callable types @@ -26,15 +27,15 @@ from typing import Any, Callable def g(x: Callable[[int], str] | None): if callable(x): - reveal_type(x) # revealed: ((int, /) -> str) & ((...) -> object) + reveal_type(x) # revealed: ((int, /) -> str) & (Top[(...) -> object]) else: - reveal_type(x) # revealed: (((int, /) -> str) & ~((...) -> object)) | None + reveal_type(x) # revealed: (((int, /) -> str) & ~(Top[(...) -> object])) | None def h(x: Callable[..., int] | None): if callable(x): - reveal_type(x) # revealed: ((...) -> int) & ((...) -> object) + reveal_type(x) # revealed: ((...) -> int) & (Top[(...) -> object]) else: - reveal_type(x) # revealed: (((...) -> int) & ~((...) -> object)) | None + reveal_type(x) # revealed: (((...) -> int) & ~(Top[(...) -> object])) | None ``` ## Narrowing from object @@ -44,7 +45,22 @@ from typing import Callable def f(x: object): if callable(x): - reveal_type(x) # revealed: (...) -> object + reveal_type(x) # revealed: Top[(...) -> object] else: - reveal_type(x) # revealed: ~((...) -> object) + reveal_type(x) # revealed: ~(Top[(...) -> object]) +``` + +## Calling narrowed callables + +The narrowed type preserves gradual parameters, so calling with any arguments is valid: + +```py +import typing as t + +def call_with_args(y: object, a: int, b: str) -> object: + if isinstance(y, t.Callable): + # Previously, `y` was incorrectly narrowed to `() -> object`, which caused + # false-positive "too many positional arguments" errors here. + return y(a, b) + return None ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 76c7f6e9835870..eca82c11ffb934 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -213,7 +213,7 @@ def f(x: dict[str, int] | list[str], y: object): reveal_type(x) # revealed: list[str] if isinstance(y, t.Callable): - reveal_type(y) # revealed: (...) -> object + reveal_type(y) # revealed: Top[(...) -> object] ``` ## Class types diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 8ecf8f550c0e53..f47c7b291be909 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1903,6 +1903,7 @@ impl<'db> Type<'db> { db, CallableSignature::from_overloads(method.signatures(db)), CallableTypeKind::Regular, + None, ))), Type::WrapperDescriptor(wrapper_descriptor) => { @@ -1910,6 +1911,7 @@ impl<'db> Type<'db> { db, CallableSignature::from_overloads(wrapper_descriptor.signatures(db)), CallableTypeKind::Regular, + None, ))) } @@ -12310,6 +12312,7 @@ impl<'db> BoundMethodType<'db> { .map(|signature| signature.bind_self(db, Some(self_instance))), ), CallableTypeKind::FunctionLike, + None, ) } @@ -12429,6 +12432,8 @@ pub struct CallableType<'db> { pub(crate) signatures: CallableSignature<'db>, kind: CallableTypeKind, + + materialization_kind: Option, } pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -12473,6 +12478,7 @@ impl<'db> CallableType<'db> { db, CallableSignature::single(signature), CallableTypeKind::Regular, + None, ) } @@ -12481,6 +12487,7 @@ impl<'db> CallableType<'db> { db, CallableSignature::single(signature), CallableTypeKind::FunctionLike, + None, ) } @@ -12492,6 +12499,7 @@ impl<'db> CallableType<'db> { db, CallableSignature::single(Signature::new(parameters, None)), CallableTypeKind::ParamSpecValue, + None, ) } @@ -12521,6 +12529,7 @@ impl<'db> CallableType<'db> { db, self.signatures(db).bind_self(db, self_type), self.kind(db), + self.materialization_kind(db), ) } @@ -12529,6 +12538,7 @@ impl<'db> CallableType<'db> { db, self.signatures(db).apply_self(db, self_type), self.kind(db), + self.materialization_kind(db), ) } @@ -12537,7 +12547,12 @@ 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) + Self::new( + db, + CallableSignature::bottom(), + CallableTypeKind::Regular, + None, + ) } /// Return a "normalized" version of this `Callable` type. @@ -12548,6 +12563,7 @@ impl<'db> CallableType<'db> { db, self.signatures(db).normalized_impl(db, visitor), self.kind(db), + self.materialization_kind(db), ) } @@ -12562,6 +12578,7 @@ impl<'db> CallableType<'db> { self.signatures(db) .recursive_type_normalized_impl(db, div, nested)?, self.kind(db), + self.materialization_kind(db), )) } @@ -12572,11 +12589,34 @@ impl<'db> CallableType<'db> { tcx: TypeContext<'db>, visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { + if let TypeMapping::Materialize(materialization_kind) = type_mapping { + // Top/Bottom materializations are fully static types already, + // so materializing them further does nothing. + if self.materialization_kind(db).is_some() { + return self; + } + + // If we're materializing a callable with gradual parameters, wrap it in + // `Top[...]` or `Bottom[...]` instead of simplifying the parameters. This + // preserves the gradual nature of the type while making it a fully static type. + // We still materialize the return type (which is in covariant position). + if self.signatures(db).has_gradual_parameters() { + return CallableType::new( + db, + self.signatures(db) + .materialize_return_types(db, *materialization_kind), + self.kind(db), + Some(*materialization_kind), + ); + } + } + CallableType::new( db, self.signatures(db) .apply_type_mapping_impl(db, type_mapping, tcx, visitor), self.kind(db), + self.materialization_kind(db), ) } @@ -12606,6 +12646,59 @@ impl<'db> CallableType<'db> { if other.is_function_like(db) && !self.is_function_like(db) { return ConstraintSet::from(false); } + + // Handle materialization kinds: + // - `Top[Callable[..., Any]]` is a supertype of `Callable[..., Any]` and all its materializations. + // - `Bottom[Callable[..., Any]]` is a subtype of `Callable[..., Any]` and all its materializations. + match ( + self.materialization_kind(db), + other.materialization_kind(db), + ) { + // No materialization kinds: use normal signature comparison. + (None, None) => {} + + // self <: Top[...] is true if the signatures are compatible (Top is supertype of all). + (_, Some(MaterializationKind::Top)) => { + return self.signatures(db).has_relation_to_impl( + db, + other.signatures(db), + inferable, + relation, + relation_visitor, + disjointness_visitor, + ); + } + + // Bottom[...] <: other is true if the signatures are compatible (Bottom is subtype of all). + (Some(MaterializationKind::Bottom), _) => { + return self.signatures(db).has_relation_to_impl( + db, + other.signatures(db), + inferable, + relation, + relation_visitor, + disjointness_visitor, + ); + } + + // Top[...] <: non-Top is only true for assignability, not subtyping. + (Some(MaterializationKind::Top), None) => { + if relation.is_subtyping() { + return ConstraintSet::from(false); + } + } + + // non-materialized <: Bottom[...] is false (only Bottom is subtype of Bottom). + (None, Some(MaterializationKind::Bottom)) => { + return ConstraintSet::from(false); + } + + // Top[...] <: Bottom[...] is false (Top is not a subtype of Bottom). + (Some(MaterializationKind::Top), Some(MaterializationKind::Bottom)) => { + return ConstraintSet::from(false); + } + } + self.signatures(db).has_relation_to_impl( db, other.signatures(db), @@ -12630,6 +12723,11 @@ impl<'db> CallableType<'db> { return ConstraintSet::from(true); } + // Callables with different materialization kinds are not equivalent + if self.materialization_kind(db) != other.materialization_kind(db) { + return ConstraintSet::from(false); + } + ConstraintSet::from(self.is_function_like(db) == other.is_function_like(db)).and(db, || { self.signatures(db) .is_equivalent_to_impl(db, other.signatures(db), inferable, visitor) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index b456b912e49aa8..d22e4504fc8360 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -4709,5 +4709,6 @@ fn asynccontextmanager_return_type<'db>(db: &'db dyn Db, func_ty: Type<'db>) -> db, CallableSignature::single(new_signature), CallableTypeKind::FunctionLike, + None, ))) } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 70ba8b3b25a102..faf5312f9e7b15 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1051,6 +1051,7 @@ impl<'db> ClassType<'db> { db, getitem_signature, CallableTypeKind::FunctionLike, + None, )); Member::definitely_declared(getitem_type) }) @@ -1218,6 +1219,7 @@ impl<'db> ClassType<'db> { db, dunder_new_signature.bind_self(db, Some(instance_ty)), CallableTypeKind::FunctionLike, + None, ); if returns_non_subclass { @@ -1288,6 +1290,7 @@ impl<'db> ClassType<'db> { db, synthesized_dunder_init_signature, CallableTypeKind::FunctionLike, + None, )) } else { None @@ -2120,6 +2123,7 @@ impl<'db> ClassLiteral<'db> { db, callable_ty.signatures(db), CallableTypeKind::FunctionLike, + callable_ty.materialization_kind(db), )), Type::Union(union) => { union.map(db, |element| into_function_like_callable(db, *element)) @@ -2764,6 +2768,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::none(db)), )), CallableTypeKind::FunctionLike, + None, ))); } @@ -2790,6 +2795,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, + None, ))) } (CodeGeneratorKind::TypedDict, "__getitem__") => { @@ -2817,6 +2823,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, + None, ))) } (CodeGeneratorKind::TypedDict, "__delitem__") => { @@ -2981,6 +2988,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, + None, ))) } (CodeGeneratorKind::TypedDict, "pop") => { @@ -3041,6 +3049,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, + None, ))) } (CodeGeneratorKind::TypedDict, "setdefault") => { @@ -3069,6 +3078,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, + None, ))) } (CodeGeneratorKind::TypedDict, "update") => { diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 197ed5dca22bd9..0171e8ff77e9e8 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1576,6 +1576,7 @@ impl<'db> CallableType<'db> { DisplayCallableType { signatures: self.signatures(db), kind: self.kind(db), + materialization_kind: self.materialization_kind(db), db, settings, } @@ -1585,23 +1586,35 @@ impl<'db> CallableType<'db> { pub(crate) struct DisplayCallableType<'a, 'db> { signatures: &'a CallableSignature<'db>, kind: CallableTypeKind, + materialization_kind: Option, db: &'db dyn Db, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + // If this callable has a materialization kind, wrap it in Top[...] or Bottom[...] + let prefix_details = match self.materialization_kind { + None => None, + Some(MaterializationKind::Top) => Some(("Top", SpecialFormType::Top)), + Some(MaterializationKind::Bottom) => Some(("Bottom", SpecialFormType::Bottom)), + }; + if let Some((name, form)) = prefix_details { + f.with_type(Type::SpecialForm(form)).write_str(name)?; + f.write_char('[')?; + } + match self.signatures.overloads.as_slice() { [signature] => { if matches!(self.kind, CallableTypeKind::ParamSpecValue) { signature .parameters() .display_with(self.db, self.settings.clone()) - .fmt_detailed(f) + .fmt_detailed(f)?; } else { signature .display_with(self.db, self.settings.clone()) - .fmt_detailed(f) + .fmt_detailed(f)?; } } signatures => { @@ -1621,9 +1634,13 @@ impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { if !self.settings.multiline { f.write_char(']')?; } - Ok(()) } } + + if self.materialization_kind.is_some() { + f.write_char(']')?; + } + Ok(()) } } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 40e4e953ef4cd6..d8ee03a0d2685b 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1094,7 +1094,7 @@ impl<'db> FunctionType<'db> { } else { CallableTypeKind::FunctionLike }; - CallableType::new(db, self.signature(db), kind) + CallableType::new(db, self.signature(db), kind, None) } /// Convert the `FunctionType` into a [`BoundMethodType`]. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3b1ceed9d6ca7a..9ddf3dd919b0c3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2427,6 +2427,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { db, callable.signatures(db), kind, + callable.materialization_kind(db), ))), Type::Union(union) => union .try_map(db, |element| propagate_callable_kind(db, *element, kind)), diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 3c02a3aed898ec..b912b4b34ea648 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -990,5 +990,6 @@ fn protocol_bind_self<'db>( db, callable.signatures(db).bind_self(db, self_type), CallableTypeKind::Regular, + callable.materialization_kind(db), ) } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index b9c03322764944..fc80b790125ec8 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -428,6 +428,7 @@ impl<'db> CallableSignature<'db> { |signature| Signature::new(signature.parameters().clone(), None), )), CallableTypeKind::ParamSpecValue, + None, )); let param_spec_matches = ConstraintSet::constrain_typevar( db, @@ -461,6 +462,7 @@ impl<'db> CallableSignature<'db> { |signature| Signature::new(signature.parameters().clone(), None), )), CallableTypeKind::ParamSpecValue, + None, )); let param_spec_matches = ConstraintSet::constrain_typevar( db, @@ -569,6 +571,28 @@ impl<'db> CallableSignature<'db> { } } } + + /// Returns `true` if any signature in this callable has gradual parameters (`...`). + pub(crate) fn has_gradual_parameters(&self) -> bool { + self.overloads.iter().any(|sig| sig.parameters.is_gradual()) + } + + /// Materialize only the return types of all signatures, preserving parameters as-is. + /// + /// This is used when wrapping gradual callables in `Top[...]` or `Bottom[...]` - we want + /// to preserve the gradual parameters but materialize the return types (which are in + /// covariant position). + pub(crate) fn materialize_return_types( + &self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Self { + Self::from_overloads( + self.overloads + .iter() + .map(|sig| sig.materialize_return_type(db, materialization_kind)), + ) + } } impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { @@ -731,6 +755,26 @@ impl<'db> Signature<'db> { Self::new(Parameters::object(), Some(Type::Never)) } + /// Materialize only the return type, preserving parameters as-is. + pub(crate) fn materialize_return_type( + &self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Self { + Self { + generic_context: self.generic_context, + definition: self.definition, + parameters: self.parameters.clone(), + return_ty: self.return_ty.map(|ty| { + ty.materialize( + db, + materialization_kind, + &ApplyTypeMappingVisitor::default(), + ) + }), + } + } + pub(crate) fn with_inherited_generic_context( mut self, db: &'db dyn Db, @@ -1115,6 +1159,7 @@ impl<'db> Signature<'db> { .map(|signature| Signature::new(signature.parameters().clone(), None)), ), CallableTypeKind::ParamSpecValue, + None, )); let param_spec_matches = ConstraintSet::constrain_typevar(db, self_bound_typevar, Type::Never, upper); @@ -1366,6 +1411,7 @@ impl<'db> Signature<'db> { db, CallableSignature::single(Signature::new(other.parameters.clone(), None)), CallableTypeKind::ParamSpecValue, + None, )); let param_spec_matches = ConstraintSet::constrain_typevar( db, @@ -1382,6 +1428,7 @@ impl<'db> Signature<'db> { db, CallableSignature::single(Signature::new(self.parameters.clone(), None)), CallableTypeKind::ParamSpecValue, + None, )); let param_spec_matches = ConstraintSet::constrain_typevar( db, @@ -2051,24 +2098,13 @@ impl<'db> Parameters<'db> { tcx: TypeContext<'db>, visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { - match type_mapping { - // Note that we've already flipped the materialization in Signature.apply_type_mapping_impl(), - // so the "top" materialization here is the bottom materialization of the whole Signature. - // It might make sense to flip the materialization here instead. - TypeMapping::Materialize(MaterializationKind::Top) if self.is_gradual() => { - Parameters::object() - } - TypeMapping::Materialize(MaterializationKind::Bottom) if self.is_gradual() => { - Parameters::gradual_form() - } - _ => Self { - value: self - .value - .iter() - .map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) - .collect(), - kind: self.kind, - }, + Self { + value: self + .value + .iter() + .map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + .collect(), + kind: self.kind, } } From 280a0a78b0e1c266c09bfd491009feccb4e3d752 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 23 Dec 2025 10:35:48 -0500 Subject: [PATCH 03/12] Use assignability --- .../resources/mdtest/narrow/callable.md | 24 +++++++++++-------- crates/ty_python_semantic/src/types.rs | 10 ++++++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index 98cfcd751f7d05..729041a5615929 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -6,18 +6,21 @@ The `callable()` builtin returns `TypeIs[Callable[..., object]]`, which narrows intersection with `Top[Callable[..., object]]`. The `Top[...]` wrapper indicates this is a fully static type representing the top materialization of a gradual callable. +Since all callable types are subtypes of `Top[Callable[..., object]]`, intersections with `Top[...]` +simplify to just the original callable type. + ```py from typing import Any, Callable def f(x: Callable[..., Any] | None): if callable(x): - # The intersection of `Callable[..., Any]` with `Top[Callable[..., object]]` preserves - # the gradual parameters (`...`). Previously this was incorrectly narrowed to - # `((...) -> Any) & (() -> object)` because the top materialization of gradual - # parameters was incorrectly `[]` instead of `...`. - reveal_type(x) # revealed: ((...) -> Any) & (Top[(...) -> object]) + # The intersection simplifies because `(...) -> Any` is a subtype of + # `Top[(...) -> object]` - all callables are subtypes of the top materialization. + reveal_type(x) # revealed: (...) -> Any else: - reveal_type(x) # revealed: (((...) -> Any) & ~(Top[(...) -> object])) | None + # Since `(...) -> Any` is a subtype of `Top[(...) -> object]`, the intersection + # with the negation is empty (Never), leaving just None. + reveal_type(x) # revealed: None ``` ## Narrowing with other callable types @@ -27,15 +30,16 @@ from typing import Any, Callable def g(x: Callable[[int], str] | None): if callable(x): - reveal_type(x) # revealed: ((int, /) -> str) & (Top[(...) -> object]) + # All callables are subtypes of `Top[(...) -> object]`, so the intersection simplifies. + reveal_type(x) # revealed: (int, /) -> str else: - reveal_type(x) # revealed: (((int, /) -> str) & ~(Top[(...) -> object])) | None + reveal_type(x) # revealed: None def h(x: Callable[..., int] | None): if callable(x): - reveal_type(x) # revealed: ((...) -> int) & (Top[(...) -> object]) + reveal_type(x) # revealed: (...) -> int else: - reveal_type(x) # revealed: (((...) -> int) & ~(Top[(...) -> object])) | None + reveal_type(x) # revealed: None ``` ## Narrowing from object diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f47c7b291be909..031a16b4e71ee8 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -12658,24 +12658,30 @@ impl<'db> CallableType<'db> { (None, None) => {} // self <: Top[...] is true if the signatures are compatible (Top is supertype of all). + // Use Assignability for the comparison because: + // 1. Top is the fully static supertype that accepts any compatible callable + // 2. Gradual parameters (...) should be compatible with Top[(...)...] + // 3. Dynamic return types like Unknown should be compatible with any return type (_, Some(MaterializationKind::Top)) => { return self.signatures(db).has_relation_to_impl( db, other.signatures(db), inferable, - relation, + TypeRelation::Assignability, relation_visitor, disjointness_visitor, ); } // Bottom[...] <: other is true if the signatures are compatible (Bottom is subtype of all). + // Use Assignability for the same reasons as Top (symmetrically, Bottom is the minimal + // static subtype). (Some(MaterializationKind::Bottom), _) => { return self.signatures(db).has_relation_to_impl( db, other.signatures(db), inferable, - relation, + TypeRelation::Assignability, relation_visitor, disjointness_visitor, ); From cc08d112177b394933dc3314eebc68ee334ced25 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 23 Dec 2025 13:15:40 -0500 Subject: [PATCH 04/12] Make call on Top an error --- .../resources/mdtest/narrow/callable.md | 8 ++-- .../ty_python_semantic/src/types/call/bind.rs | 44 +++++++++++++++++-- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index 729041a5615929..0ad2b66671a843 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -56,15 +56,17 @@ def f(x: object): ## Calling narrowed callables -The narrowed type preserves gradual parameters, so calling with any arguments is valid: +The narrowed type `Top[Callable[..., object]]` represents the "infinite union" of all possible +callable types. While such objects *are* callable (they pass `callable()`), any attempt to actually +call them should fail because we don't know the actual signature - we can't know if any specific set +of arguments is valid. ```py import typing as t def call_with_args(y: object, a: int, b: str) -> object: if isinstance(y, t.Callable): - # Previously, `y` was incorrectly narrowed to `() -> object`, which caused - # false-positive "too many positional arguments" errors here. + # error: [call-non-callable] "Object of type `Top[(...) -> object]` is not callable" return y(a, b) return None ``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index d22e4504fc8360..c5efa606bcd9d6 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -44,10 +44,10 @@ use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use crate::types::{ BoundMethodType, BoundTypeVarIdentity, BoundTypeVarInstance, CallableSignature, CallableType, CallableTypeKind, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, - FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy, - NominalInstanceType, PropertyInstanceType, SpecialFormType, TrackedConstraintSet, - TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType, WrapperDescriptorKind, - enums, list_members, todo_type, + FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, MaterializationKind, + MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType, + TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType, + WrapperDescriptorKind, enums, list_members, todo_type, }; use crate::unpack::EvaluationMode; use crate::{DisplaySettings, Program}; @@ -1622,6 +1622,21 @@ impl<'db> CallableBinding<'db> { ) .entered(); + // If the callable is a top materialization (e.g., `Top[Callable[..., object]]`), any call + // should fail because we don't know the actual signature. The type IS callable (it passes + // `callable()`), but it represents an infinite union of all possible callable types, so + // there's no valid set of arguments. + if let Type::Callable(callable) = self.signature_type { + if callable.materialization_kind(db) == Some(MaterializationKind::Top) { + for overload in &mut self.overloads { + overload + .errors + .push(BindingError::UnknownCallableSignature(self.signature_type)); + } + return None; + } + } + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), @@ -4124,6 +4139,10 @@ pub(crate) enum BindingError<'db> { /// This overload binding of the callable does not match the arguments. // TODO: We could expand this with an enum to specify why the overload is unmatched. UnmatchedOverload, + /// The callable type is a top materialization (e.g., `Top[Callable[..., object]]`), which + /// represents an unknown callable signature. While such types *are* callable (they pass + /// `callable()`), any specific call should fail because we don't know the actual signature. + UnknownCallableSignature(Type<'db>), } impl<'db> BindingError<'db> { @@ -4551,6 +4570,23 @@ impl<'db> BindingError<'db> { } Self::UnmatchedOverload => {} + + Self::UnknownCallableSignature(callable_ty) => { + let node = Self::get_node(node, None); + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + let callable_ty_display = callable_ty.display(context.db()); + let mut diag = builder.into_diagnostic(format_args!( + "Object of type `{callable_ty_display}` is not callable" + )); + diag.info( + "An object with an unknown callable signature is not callable, \ + because there is no valid set of arguments for it", + ); + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + } } } From 85d3226d40fe2a58f4a705e939b494dfb6f400ff Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 23 Dec 2025 22:56:33 -0500 Subject: [PATCH 05/12] Address feedback --- .../resources/mdtest/narrow/callable.md | 10 ++--- crates/ty_python_semantic/src/types.rs | 44 +++++++------------ .../ty_python_semantic/src/types/call/bind.rs | 21 ++++----- .../src/types/diagnostic.rs | 26 +++++++++++ .../src/types/signatures.rs | 35 +++++++++++++++ 5 files changed, 93 insertions(+), 43 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index 0ad2b66671a843..714076375df91b 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -56,17 +56,17 @@ def f(x: object): ## Calling narrowed callables -The narrowed type `Top[Callable[..., object]]` represents the "infinite union" of all possible -callable types. While such objects *are* callable (they pass `callable()`), any attempt to actually -call them should fail because we don't know the actual signature - we can't know if any specific set -of arguments is valid. +The narrowed type `Top[Callable[..., object]]` represents the set of all possible callable types +(including, e.g., functions that take no arguments and functions that require arguments). While such +objects *are* callable (they pass `callable()`), no specific set of arguments can be guaranteed to +be valid. ```py import typing as t def call_with_args(y: object, a: int, b: str) -> object: if isinstance(y, t.Callable): - # error: [call-non-callable] "Object of type `Top[(...) -> object]` is not callable" + # error: [call-top-callable] return y(a, b) return None ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 031a16b4e71ee8..b382014a77b7d9 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -12648,8 +12648,12 @@ impl<'db> CallableType<'db> { } // Handle materialization kinds: - // - `Top[Callable[..., Any]]` is a supertype of `Callable[..., Any]` and all its materializations. - // - `Bottom[Callable[..., Any]]` is a subtype of `Callable[..., Any]` and all its materializations. + // - `Top[Callable[..., R]]` is a supertype of all callables with return type subtype of R. + // - `Bottom[Callable[..., R]]` is a subtype of all callables with return type supertype of R. + // + // For Top/Bottom, we only need to compare return types because: + // - Top parameters are a supertype of all possible parameters + // - Bottom parameters are a subtype of all possible parameters match ( self.materialization_kind(db), other.materialization_kind(db), @@ -12657,50 +12661,34 @@ impl<'db> CallableType<'db> { // No materialization kinds: use normal signature comparison. (None, None) => {} - // self <: Top[...] is true if the signatures are compatible (Top is supertype of all). - // Use Assignability for the comparison because: - // 1. Top is the fully static supertype that accepts any compatible callable - // 2. Gradual parameters (...) should be compatible with Top[(...)...] - // 3. Dynamic return types like Unknown should be compatible with any return type + // Anything <: Top[...]: just compare return types. (_, Some(MaterializationKind::Top)) => { - return self.signatures(db).has_relation_to_impl( + return self.signatures(db).return_types_have_relation_to( db, other.signatures(db), inferable, - TypeRelation::Assignability, + relation, relation_visitor, disjointness_visitor, ); } - // Bottom[...] <: other is true if the signatures are compatible (Bottom is subtype of all). - // Use Assignability for the same reasons as Top (symmetrically, Bottom is the minimal - // static subtype). + // Bottom[...] <: anything: just compare return types. (Some(MaterializationKind::Bottom), _) => { - return self.signatures(db).has_relation_to_impl( + return self.signatures(db).return_types_have_relation_to( db, other.signatures(db), inferable, - TypeRelation::Assignability, + relation, relation_visitor, disjointness_visitor, ); } - // Top[...] <: non-Top is only true for assignability, not subtyping. - (Some(MaterializationKind::Top), None) => { - if relation.is_subtyping() { - return ConstraintSet::from(false); - } - } - - // non-materialized <: Bottom[...] is false (only Bottom is subtype of Bottom). - (None, Some(MaterializationKind::Bottom)) => { - return ConstraintSet::from(false); - } - - // Top[...] <: Bottom[...] is false (Top is not a subtype of Bottom). - (Some(MaterializationKind::Top), Some(MaterializationKind::Bottom)) => { + // Top[...] <: non-Top and non-Bottom <: Bottom[...] are always false. + // Top is not a subtype of any specific callable, and nothing except Bottom + // is a subtype of Bottom. + _ => { return ConstraintSet::from(false); } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index c5efa606bcd9d6..950a142d4e6e80 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -27,9 +27,9 @@ use crate::place::{Definedness, Place, known_module_symbol}; use crate::types::call::arguments::{Expansion, is_expandable_type}; use crate::types::constraints::ConstraintSet; use crate::types::diagnostic::{ - CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, - NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, POSITIONAL_ONLY_PARAMETER_AS_KWARG, - TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, + CALL_NON_CALLABLE, CALL_TOP_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, + MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, + POSITIONAL_ONLY_PARAMETER_AS_KWARG, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, }; use crate::types::enums::is_enum_class; use crate::types::function::{ @@ -1631,7 +1631,7 @@ impl<'db> CallableBinding<'db> { for overload in &mut self.overloads { overload .errors - .push(BindingError::UnknownCallableSignature(self.signature_type)); + .push(BindingError::CalledTopCallable(self.signature_type)); } return None; } @@ -4140,9 +4140,9 @@ pub(crate) enum BindingError<'db> { // TODO: We could expand this with an enum to specify why the overload is unmatched. UnmatchedOverload, /// The callable type is a top materialization (e.g., `Top[Callable[..., object]]`), which - /// represents an unknown callable signature. While such types *are* callable (they pass + /// represents the infinite union of all callables. While such types *are* callable (they pass /// `callable()`), any specific call should fail because we don't know the actual signature. - UnknownCallableSignature(Type<'db>), + CalledTopCallable(Type<'db>), } impl<'db> BindingError<'db> { @@ -4571,15 +4571,16 @@ impl<'db> BindingError<'db> { Self::UnmatchedOverload => {} - Self::UnknownCallableSignature(callable_ty) => { + Self::CalledTopCallable(callable_ty) => { let node = Self::get_node(node, None); - if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + if let Some(builder) = context.report_lint(&CALL_TOP_CALLABLE, node) { let callable_ty_display = callable_ty.display(context.db()); let mut diag = builder.into_diagnostic(format_args!( - "Object of type `{callable_ty_display}` is not callable" + "Object of type `{callable_ty_display}` is not safe to call; \ + its signature is not known" )); diag.info( - "An object with an unknown callable signature is not callable, \ + "This type includes all possible callables, so it cannot safely be called \ because there is no valid set of arguments for it", ); if let Some(union_diag) = union_diag { diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index a7a4d8d3f11db8..bcfe4f1709d9ed 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -50,6 +50,7 @@ use ty_module_resolver::{Module, ModuleName}; pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&AMBIGUOUS_PROTOCOL_MEMBER); registry.register_lint(&CALL_NON_CALLABLE); + registry.register_lint(&CALL_TOP_CALLABLE); registry.register_lint(&POSSIBLY_MISSING_IMPLICIT_CALL); registry.register_lint(&CONFLICTING_ARGUMENT_FORMS); registry.register_lint(&CONFLICTING_DECLARATIONS); @@ -151,6 +152,31 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for calls to objects typed as `Top[Callable[..., T]]` (the infinite union of all + /// callable types with return type `T`). + /// + /// ## Why is this bad? + /// When an object is narrowed to `Top[Callable[..., object]]` (e.g., via `callable(x)` or + /// `isinstance(x, Callable)`), we know the object is callable, but we don't know its + /// precise signature. This type represents the set of all possible callable types + /// (including, e.g., functions that take no arguments and functions that require arguments), + /// so no specific set of arguments can be guaranteed to be valid. + /// + /// ## Examples + /// ```python + /// def f(x: object): + /// if callable(x): + /// x() # We know x is callable, but not what arguments it accepts + /// ``` + pub(crate) static CALL_TOP_CALLABLE = { + summary: "detects calls to the top callable type", + status: LintStatus::preview("0.0.1-alpha.1"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for implicit calls to possibly missing methods. diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index fc80b790125ec8..1edcd670ce97bc 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -593,6 +593,41 @@ impl<'db> CallableSignature<'db> { .map(|sig| sig.materialize_return_type(db, materialization_kind)), ) } + + /// Check whether the return types of this callable have the given relation to the return + /// types of another callable. + pub(crate) fn return_types_have_relation_to( + &self, + db: &'db dyn Db, + other: &Self, + inferable: InferableTypeVars<'_, 'db>, + relation: TypeRelation<'db>, + relation_visitor: &HasRelationToVisitor<'db>, + disjointness_visitor: &IsDisjointVisitor<'db>, + ) -> ConstraintSet<'db> { + // For each overload in self, the return type must have the relation to + // the return type of some overload in other. + self.overloads.iter().when_all(db, |self_sig| { + let Some(self_return_ty) = self_sig.return_ty else { + // No return type means Never, which is a subtype of everything + return ConstraintSet::from(true); + }; + other.overloads.iter().when_any(db, |other_sig| { + let Some(other_return_ty) = other_sig.return_ty else { + // other returns Never, self returns something - not a match for subtyping + return ConstraintSet::from(false); + }; + self_return_ty.has_relation_to_impl( + db, + other_return_ty, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + }) + } } impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { From c7f927e7bcaf66f8f7a923659729bd43ca5fd949 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 24 Dec 2025 08:45:03 -0500 Subject: [PATCH 06/12] Review feedback --- crates/ty/docs/rules.md | 181 ++++++++++-------- .../resources/mdtest/narrow/callable.md | 2 +- .../ty_python_semantic/src/types/call/bind.rs | 16 +- crates/ty_python_semantic/src/types/class.rs | 2 + .../src/types/diagnostic.rs | 2 +- .../ty_python_semantic/src/types/display.rs | 9 +- .../src/types/signatures.rs | 10 +- ty.schema.json | 10 + 8 files changed, 137 insertions(+), 95 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index e2b08952a7e8be..92875af33c137e 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -80,7 +80,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -98,13 +98,44 @@ Calling a non-callable object will raise a `TypeError` at runtime. 4() # TypeError: 'int' object is not callable ``` +## `call-top-callable` + + +Default level: error · +Added in 0.0.7 · +Related issues · +View source + + + +**What it does** + +Checks for calls to objects typed as `Top[Callable[..., T]]` (the infinite union of all +callable types with return type `T`). + +**Why is this bad?** + +When an object is narrowed to `Top[Callable[..., object]]` (e.g., via `callable(x)` or +`isinstance(x, Callable)`), we know the object is callable, but we don't know its +precise signature. This type represents the set of all possible callable types +(including, e.g., functions that take no arguments and functions that require arguments), +so no specific set of arguments can be guaranteed to be valid. + +**Examples** + +```python +def f(x: object): + if callable(x): + x() # We know x is callable, but not what arguments it accepts +``` + ## `conflicting-argument-forms` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -136,7 +167,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -167,7 +198,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -199,7 +230,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -231,7 +262,7 @@ class B(A): ... Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -259,7 +290,7 @@ type B = A Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -286,7 +317,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -315,7 +346,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -342,7 +373,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -498,7 +529,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -528,7 +559,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -554,7 +585,7 @@ t[3] # IndexError: tuple index out of range Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -643,7 +674,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -670,7 +701,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -698,7 +729,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -732,7 +763,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -768,7 +799,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -792,7 +823,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -819,7 +850,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -848,7 +879,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -892,7 +923,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -934,7 +965,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -978,7 +1009,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1046,7 +1077,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1085,7 +1116,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1120,7 +1151,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1154,7 +1185,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1261,7 +1292,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1315,7 +1346,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1345,7 +1376,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1395,7 +1426,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1421,7 +1452,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1452,7 +1483,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1486,7 +1517,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1535,7 +1566,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1560,7 +1591,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1656,7 +1687,7 @@ class C: ... Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1683,7 +1714,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1730,7 +1761,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1760,7 +1791,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1790,7 +1821,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1824,7 +1855,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1858,7 +1889,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1893,7 +1924,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1918,7 +1949,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1951,7 +1982,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1980,7 +2011,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2004,7 +2035,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2030,7 +2061,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2063,7 +2094,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2090,7 +2121,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2117,7 +2148,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2145,7 +2176,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2177,7 +2208,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2214,7 +2245,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2278,7 +2309,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2305,7 +2336,7 @@ cast(int, f()) # Redundant Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2335,7 +2366,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2364,7 +2395,7 @@ class B(A): ... # Error raised here Default level: error · Preview (since 0.0.1-alpha.30) · Related issues · -View source +View source @@ -2398,7 +2429,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2425,7 +2456,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2453,7 +2484,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2499,7 +2530,7 @@ class A: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2523,7 +2554,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2550,7 +2581,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2578,7 +2609,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2636,7 +2667,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2661,7 +2692,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2686,7 +2717,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2725,7 +2756,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2762,7 +2793,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2821,7 +2852,7 @@ a = 20 / 2 Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2884,7 +2915,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index 714076375df91b..b7255e0a6ee52f 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -51,7 +51,7 @@ def f(x: object): if callable(x): reveal_type(x) # revealed: Top[(...) -> object] else: - reveal_type(x) # revealed: ~(Top[(...) -> object]) + reveal_type(x) # revealed: ~Top[(...) -> object] ``` ## Calling narrowed callables diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 950a142d4e6e80..be3a62e403a356 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1626,15 +1626,15 @@ impl<'db> CallableBinding<'db> { // should fail because we don't know the actual signature. The type IS callable (it passes // `callable()`), but it represents an infinite union of all possible callable types, so // there's no valid set of arguments. - if let Type::Callable(callable) = self.signature_type { - if callable.materialization_kind(db) == Some(MaterializationKind::Top) { - for overload in &mut self.overloads { - overload - .errors - .push(BindingError::CalledTopCallable(self.signature_type)); - } - return None; + if let Type::Callable(callable) = self.signature_type + && callable.materialization_kind(db) == Some(MaterializationKind::Top) + { + for overload in &mut self.overloads { + overload + .errors + .push(BindingError::CalledTopCallable(self.signature_type)); } + return None; } tracing::trace!( diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index faf5312f9e7b15..d6b8bc2c2e9a93 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2855,6 +2855,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::none(db)), )), CallableTypeKind::FunctionLike, + None, ))); } @@ -2880,6 +2881,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, + None, ))) } (CodeGeneratorKind::TypedDict, "get") => { diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index bcfe4f1709d9ed..7a5174be81587e 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -172,7 +172,7 @@ declare_lint! { /// ``` pub(crate) static CALL_TOP_CALLABLE = { summary: "detects calls to the top callable type", - status: LintStatus::preview("0.0.1-alpha.1"), + status: LintStatus::stable("0.0.7"), default_level: Level::Error, } } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 0171e8ff77e9e8..5571fc3392567a 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -2248,8 +2248,13 @@ impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'db> { f.write_char(')') }; match self.ty { - Type::Callable(_) - | Type::KnownBoundMethod(_) + // Callable types with a materialization kind (Top/Bottom) are displayed as + // `Top[(...) -> T]` or `Bottom[(...) -> T]`, which is already unambiguous + // and doesn't need additional parentheses. + Type::Callable(callable) if callable.materialization_kind(self.db).is_none() => { + write_parentheses(f) + } + Type::KnownBoundMethod(_) | Type::FunctionLiteral(_) | Type::BoundMethod(_) | Type::Union(_) => write_parentheses(f), diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 1edcd670ce97bc..0ad2d8e3a5210a 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -608,15 +608,9 @@ impl<'db> CallableSignature<'db> { // For each overload in self, the return type must have the relation to // the return type of some overload in other. self.overloads.iter().when_all(db, |self_sig| { - let Some(self_return_ty) = self_sig.return_ty else { - // No return type means Never, which is a subtype of everything - return ConstraintSet::from(true); - }; + let self_return_ty = self_sig.return_ty.unwrap_or(Type::unknown()); other.overloads.iter().when_any(db, |other_sig| { - let Some(other_return_ty) = other_sig.return_ty else { - // other returns Never, self returns something - not a match for subtyping - return ConstraintSet::from(false); - }; + let other_return_ty = other_sig.return_ty.unwrap_or(Type::unknown()); self_return_ty.has_relation_to_impl( db, other_return_ty, diff --git a/ty.schema.json b/ty.schema.json index 4d49c27b60dff9..b5edc9332ace29 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -356,6 +356,16 @@ } ] }, + "call-top-callable": { + "title": "detects calls to the top callable type", + "description": "## What it does\nChecks for calls to objects typed as `Top[Callable[..., T]]` (the infinite union of all\ncallable types with return type `T`).\n\n## Why is this bad?\nWhen an object is narrowed to `Top[Callable[..., object]]` (e.g., via `callable(x)` or\n`isinstance(x, Callable)`), we know the object is callable, but we don't know its\nprecise signature. This type represents the set of all possible callable types\n(including, e.g., functions that take no arguments and functions that require arguments),\nso no specific set of arguments can be guaranteed to be valid.\n\n## Examples\n```python\ndef f(x: object):\n if callable(x):\n x() # We know x is callable, but not what arguments it accepts\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "conflicting-argument-forms": { "title": "detects when an argument is used as both a value and a type form in a call", "description": "## What it does\nChecks whether an argument is used as both a value and a type form in a call.\n\n## Why is this bad?\nSuch calls have confusing semantics and often indicate a logic error.\n\n## Examples\n```python\nfrom typing import reveal_type\nfrom ty_extensions import is_singleton\n\nif flag:\n f = repr # Expects a value\nelse:\n f = is_singleton # Expects a type form\n\nf(int) # error\n```", From 2ebc58631968bc472e09c1074a71cd7368180e16 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 24 Dec 2025 09:30:32 -0500 Subject: [PATCH 07/12] Use is_top_materialization --- .../mdtest/type_properties/materialization.md | 38 +++++++ crates/ty_python_semantic/src/types.rs | 107 +++++++++--------- .../ty_python_semantic/src/types/call/bind.rs | 12 +- crates/ty_python_semantic/src/types/class.rs | 24 ++-- .../ty_python_semantic/src/types/display.rs | 25 ++-- .../ty_python_semantic/src/types/function.rs | 2 +- .../src/types/infer/builder.rs | 2 +- .../src/types/protocol_class.rs | 2 +- .../src/types/signatures.rs | 10 +- 9 files changed, 127 insertions(+), 95 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index 01f3402c74d32e..769c61c950e68c 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -178,6 +178,44 @@ def _(top: Top[C3], bottom: Bottom[C3]) -> None: reveal_type(bottom) ``` +## Callable with gradual parameters + +For callables with gradual parameters (the `...` form), the top materialization preserves the +gradual form since we cannot know what parameters are required. The bottom materialization +simplifies to the bottom callable `(*args: object, **kwargs: object) -> Never` since this is the +most specific type that is a subtype of all possible callable materializations. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, Callable, Never, Protocol +from ty_extensions import Bottom, Top, is_equivalent_to, is_subtype_of, static_assert + +type GradualCallable = Callable[..., Any] + +def _(top: Top[GradualCallable], bottom: Bottom[GradualCallable]) -> None: + # The top materialization keeps the gradual parameters wrapped + reveal_type(top) # revealed: Top[(...) -> object] + + # The bottom materialization simplifies to the fully static bottom callable + reveal_type(bottom) # revealed: (*args: object, **kwargs: object) -> Never + +# The bottom materialization of a gradual callable is a subtype of (and supertype of) +# a protocol with `__call__(self, *args: object, **kwargs: object) -> Never` +class EquivalentToBottom(Protocol): + def __call__(self, *args: object, **kwargs: object) -> Never: ... + +static_assert(is_subtype_of(EquivalentToBottom, Bottom[Callable[..., Never]])) +static_assert(is_subtype_of(Bottom[Callable[..., Never]], EquivalentToBottom)) + +# TODO: is_equivalent_to only considers types of the same kind equivalent (Callable vs ProtocolInstance), +# so this fails even though mutual subtyping proves semantic equivalence. +# static_assert(is_equivalent_to(Bottom[Callable[..., Never]], EquivalentToBottom)) +``` + ## Tuple All positions in a tuple are covariant. diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index b382014a77b7d9..7e0b806106c20e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1903,7 +1903,7 @@ impl<'db> Type<'db> { db, CallableSignature::from_overloads(method.signatures(db)), CallableTypeKind::Regular, - None, + false, ))), Type::WrapperDescriptor(wrapper_descriptor) => { @@ -1911,7 +1911,7 @@ impl<'db> Type<'db> { db, CallableSignature::from_overloads(wrapper_descriptor.signatures(db)), CallableTypeKind::Regular, - None, + false, ))) } @@ -12312,7 +12312,7 @@ impl<'db> BoundMethodType<'db> { .map(|signature| signature.bind_self(db, Some(self_instance))), ), CallableTypeKind::FunctionLike, - None, + false, ) } @@ -12433,7 +12433,11 @@ pub struct CallableType<'db> { kind: CallableTypeKind, - materialization_kind: Option, + /// Whether this callable is a top materialization (e.g., `Top[Callable[..., object]]`). + /// + /// Bottom materializations of gradual callables are simplified to the bottom callable + /// `(*args: object, **kwargs: object) -> Never`, so this is always false for them. + is_top_materialization: bool, } pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -12478,7 +12482,7 @@ impl<'db> CallableType<'db> { db, CallableSignature::single(signature), CallableTypeKind::Regular, - None, + false, ) } @@ -12487,7 +12491,7 @@ impl<'db> CallableType<'db> { db, CallableSignature::single(signature), CallableTypeKind::FunctionLike, - None, + false, ) } @@ -12499,7 +12503,7 @@ impl<'db> CallableType<'db> { db, CallableSignature::single(Signature::new(parameters, None)), CallableTypeKind::ParamSpecValue, - None, + false, ) } @@ -12529,7 +12533,7 @@ impl<'db> CallableType<'db> { db, self.signatures(db).bind_self(db, self_type), self.kind(db), - self.materialization_kind(db), + self.is_top_materialization(db), ) } @@ -12538,7 +12542,7 @@ impl<'db> CallableType<'db> { db, self.signatures(db).apply_self(db, self_type), self.kind(db), - self.materialization_kind(db), + self.is_top_materialization(db), ) } @@ -12551,7 +12555,7 @@ impl<'db> CallableType<'db> { db, CallableSignature::bottom(), CallableTypeKind::Regular, - None, + false, ) } @@ -12563,7 +12567,7 @@ impl<'db> CallableType<'db> { db, self.signatures(db).normalized_impl(db, visitor), self.kind(db), - self.materialization_kind(db), + self.is_top_materialization(db), ) } @@ -12578,7 +12582,7 @@ impl<'db> CallableType<'db> { self.signatures(db) .recursive_type_normalized_impl(db, div, nested)?, self.kind(db), - self.materialization_kind(db), + self.is_top_materialization(db), )) } @@ -12590,24 +12594,33 @@ impl<'db> CallableType<'db> { visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { if let TypeMapping::Materialize(materialization_kind) = type_mapping { - // Top/Bottom materializations are fully static types already, + // Top materializations are fully static types already, // so materializing them further does nothing. - if self.materialization_kind(db).is_some() { + if self.is_top_materialization(db) { return self; } - // If we're materializing a callable with gradual parameters, wrap it in - // `Top[...]` or `Bottom[...]` instead of simplifying the parameters. This - // preserves the gradual nature of the type while making it a fully static type. - // We still materialize the return type (which is in covariant position). + // If we're materializing a callable with gradual parameters: + // - For Top materialization: wrap in `Top[...]` to preserve the gradual nature + // - For Bottom materialization: simplify to the bottom callable since + // `Bottom[Callable[..., R]]` is equivalent to `(*args: object, **kwargs: object) -> Bottom[R]` if self.signatures(db).has_gradual_parameters() { - return CallableType::new( - db, - self.signatures(db) - .materialize_return_types(db, *materialization_kind), - self.kind(db), - Some(*materialization_kind), - ); + match materialization_kind { + MaterializationKind::Top => { + return CallableType::new( + db, + self.signatures(db) + .materialize_return_types(db, *materialization_kind), + self.kind(db), + true, + ); + } + MaterializationKind::Bottom => { + // Bottom materialization of a gradual callable simplifies to the + // bottom callable: (*args: object, **kwargs: object) -> Never + return CallableType::bottom(db); + } + } } } @@ -12616,7 +12629,7 @@ impl<'db> CallableType<'db> { self.signatures(db) .apply_type_mapping_impl(db, type_mapping, tcx, visitor), self.kind(db), - self.materialization_kind(db), + self.is_top_materialization(db), ) } @@ -12647,22 +12660,21 @@ impl<'db> CallableType<'db> { return ConstraintSet::from(false); } - // Handle materialization kinds: + // Handle top materialization: // - `Top[Callable[..., R]]` is a supertype of all callables with return type subtype of R. - // - `Bottom[Callable[..., R]]` is a subtype of all callables with return type supertype of R. // - // For Top/Bottom, we only need to compare return types because: - // - Top parameters are a supertype of all possible parameters - // - Bottom parameters are a subtype of all possible parameters + // For Top, we only need to compare return types because Top parameters are a supertype + // of all possible parameters. Bottom materializations are simplified to the bottom + // callable directly, so they use normal signature comparison. match ( - self.materialization_kind(db), - other.materialization_kind(db), + self.is_top_materialization(db), + other.is_top_materialization(db), ) { - // No materialization kinds: use normal signature comparison. - (None, None) => {} + // Neither is a top materialization: use normal signature comparison. + (false, false) => {} // Anything <: Top[...]: just compare return types. - (_, Some(MaterializationKind::Top)) => { + (_, true) => { return self.signatures(db).return_types_have_relation_to( db, other.signatures(db), @@ -12673,22 +12685,9 @@ impl<'db> CallableType<'db> { ); } - // Bottom[...] <: anything: just compare return types. - (Some(MaterializationKind::Bottom), _) => { - return self.signatures(db).return_types_have_relation_to( - db, - other.signatures(db), - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); - } - - // Top[...] <: non-Top and non-Bottom <: Bottom[...] are always false. - // Top is not a subtype of any specific callable, and nothing except Bottom - // is a subtype of Bottom. - _ => { + // Top[...] <: non-Top is always false. + // Top is not a subtype of any specific callable. + (true, false) => { return ConstraintSet::from(false); } } @@ -12717,8 +12716,8 @@ impl<'db> CallableType<'db> { return ConstraintSet::from(true); } - // Callables with different materialization kinds are not equivalent - if self.materialization_kind(db) != other.materialization_kind(db) { + // Callables with different top materialization status are not equivalent + if self.is_top_materialization(db) != other.is_top_materialization(db) { return ConstraintSet::from(false); } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index be3a62e403a356..236a9e11be7861 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -44,10 +44,10 @@ use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use crate::types::{ BoundMethodType, BoundTypeVarIdentity, BoundTypeVarInstance, CallableSignature, CallableType, CallableTypeKind, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, - FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, MaterializationKind, - MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType, - TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType, - WrapperDescriptorKind, enums, list_members, todo_type, + FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy, + NominalInstanceType, PropertyInstanceType, SpecialFormType, TrackedConstraintSet, + TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType, WrapperDescriptorKind, + enums, list_members, todo_type, }; use crate::unpack::EvaluationMode; use crate::{DisplaySettings, Program}; @@ -1627,7 +1627,7 @@ impl<'db> CallableBinding<'db> { // `callable()`), but it represents an infinite union of all possible callable types, so // there's no valid set of arguments. if let Type::Callable(callable) = self.signature_type - && callable.materialization_kind(db) == Some(MaterializationKind::Top) + && callable.is_top_materialization(db) { for overload in &mut self.overloads { overload @@ -4746,6 +4746,6 @@ fn asynccontextmanager_return_type<'db>(db: &'db dyn Db, func_ty: Type<'db>) -> db, CallableSignature::single(new_signature), CallableTypeKind::FunctionLike, - None, + false, ))) } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d6b8bc2c2e9a93..16858e0bccf05b 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1051,7 +1051,7 @@ impl<'db> ClassType<'db> { db, getitem_signature, CallableTypeKind::FunctionLike, - None, + false, )); Member::definitely_declared(getitem_type) }) @@ -1219,7 +1219,7 @@ impl<'db> ClassType<'db> { db, dunder_new_signature.bind_self(db, Some(instance_ty)), CallableTypeKind::FunctionLike, - None, + false, ); if returns_non_subclass { @@ -1290,7 +1290,7 @@ impl<'db> ClassType<'db> { db, synthesized_dunder_init_signature, CallableTypeKind::FunctionLike, - None, + false, )) } else { None @@ -2123,7 +2123,7 @@ impl<'db> ClassLiteral<'db> { db, callable_ty.signatures(db), CallableTypeKind::FunctionLike, - callable_ty.materialization_kind(db), + callable_ty.is_top_materialization(db), )), Type::Union(union) => { union.map(db, |element| into_function_like_callable(db, *element)) @@ -2768,7 +2768,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::none(db)), )), CallableTypeKind::FunctionLike, - None, + false, ))); } @@ -2795,7 +2795,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - None, + false, ))) } (CodeGeneratorKind::TypedDict, "__getitem__") => { @@ -2823,7 +2823,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - None, + false, ))) } (CodeGeneratorKind::TypedDict, "__delitem__") => { @@ -2855,7 +2855,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::none(db)), )), CallableTypeKind::FunctionLike, - None, + false, ))); } @@ -2881,7 +2881,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - None, + false, ))) } (CodeGeneratorKind::TypedDict, "get") => { @@ -2990,7 +2990,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - None, + false, ))) } (CodeGeneratorKind::TypedDict, "pop") => { @@ -3051,7 +3051,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - None, + false, ))) } (CodeGeneratorKind::TypedDict, "setdefault") => { @@ -3080,7 +3080,7 @@ impl<'db> ClassLiteral<'db> { db, CallableSignature::from_overloads(overloads), CallableTypeKind::FunctionLike, - None, + false, ))) } (CodeGeneratorKind::TypedDict, "update") => { diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 5571fc3392567a..5693df9d1b49b4 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1576,7 +1576,7 @@ impl<'db> CallableType<'db> { DisplayCallableType { signatures: self.signatures(db), kind: self.kind(db), - materialization_kind: self.materialization_kind(db), + is_top_materialization: self.is_top_materialization(db), db, settings, } @@ -1586,21 +1586,17 @@ impl<'db> CallableType<'db> { pub(crate) struct DisplayCallableType<'a, 'db> { signatures: &'a CallableSignature<'db>, kind: CallableTypeKind, - materialization_kind: Option, + is_top_materialization: bool, db: &'db dyn Db, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - // If this callable has a materialization kind, wrap it in Top[...] or Bottom[...] - let prefix_details = match self.materialization_kind { - None => None, - Some(MaterializationKind::Top) => Some(("Top", SpecialFormType::Top)), - Some(MaterializationKind::Bottom) => Some(("Bottom", SpecialFormType::Bottom)), - }; - if let Some((name, form)) = prefix_details { - f.with_type(Type::SpecialForm(form)).write_str(name)?; + // If this callable is a top materialization, wrap it in Top[...] + if self.is_top_materialization { + f.with_type(Type::SpecialForm(SpecialFormType::Top)) + .write_str("Top")?; f.write_char('[')?; } @@ -1637,7 +1633,7 @@ impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { } } - if self.materialization_kind.is_some() { + if self.is_top_materialization { f.write_char(']')?; } Ok(()) @@ -2248,10 +2244,9 @@ impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'db> { f.write_char(')') }; match self.ty { - // Callable types with a materialization kind (Top/Bottom) are displayed as - // `Top[(...) -> T]` or `Bottom[(...) -> T]`, which is already unambiguous - // and doesn't need additional parentheses. - Type::Callable(callable) if callable.materialization_kind(self.db).is_none() => { + // Callable types with a top materialization are displayed as `Top[(...) -> T]`, + // which is already unambiguous and doesn't need additional parentheses. + Type::Callable(callable) if !callable.is_top_materialization(self.db) => { write_parentheses(f) } Type::KnownBoundMethod(_) diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index d8ee03a0d2685b..540e8b373f9ff8 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1094,7 +1094,7 @@ impl<'db> FunctionType<'db> { } else { CallableTypeKind::FunctionLike }; - CallableType::new(db, self.signature(db), kind, None) + CallableType::new(db, self.signature(db), kind, false) } /// Convert the `FunctionType` into a [`BoundMethodType`]. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 9ddf3dd919b0c3..0c54a1410e2203 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2427,7 +2427,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { db, callable.signatures(db), kind, - callable.materialization_kind(db), + callable.is_top_materialization(db), ))), Type::Union(union) => union .try_map(db, |element| propagate_callable_kind(db, *element, kind)), diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index b912b4b34ea648..8d45b5a42a2207 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -990,6 +990,6 @@ fn protocol_bind_self<'db>( db, callable.signatures(db).bind_self(db, self_type), CallableTypeKind::Regular, - callable.materialization_kind(db), + callable.is_top_materialization(db), ) } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 0ad2d8e3a5210a..dcc2ffb9b73e6b 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -428,7 +428,7 @@ impl<'db> CallableSignature<'db> { |signature| Signature::new(signature.parameters().clone(), None), )), CallableTypeKind::ParamSpecValue, - None, + false, )); let param_spec_matches = ConstraintSet::constrain_typevar( db, @@ -462,7 +462,7 @@ impl<'db> CallableSignature<'db> { |signature| Signature::new(signature.parameters().clone(), None), )), CallableTypeKind::ParamSpecValue, - None, + false, )); let param_spec_matches = ConstraintSet::constrain_typevar( db, @@ -1188,7 +1188,7 @@ impl<'db> Signature<'db> { .map(|signature| Signature::new(signature.parameters().clone(), None)), ), CallableTypeKind::ParamSpecValue, - None, + false, )); let param_spec_matches = ConstraintSet::constrain_typevar(db, self_bound_typevar, Type::Never, upper); @@ -1440,7 +1440,7 @@ impl<'db> Signature<'db> { db, CallableSignature::single(Signature::new(other.parameters.clone(), None)), CallableTypeKind::ParamSpecValue, - None, + false, )); let param_spec_matches = ConstraintSet::constrain_typevar( db, @@ -1457,7 +1457,7 @@ impl<'db> Signature<'db> { db, CallableSignature::single(Signature::new(self.parameters.clone(), None)), CallableTypeKind::ParamSpecValue, - None, + false, )); let param_spec_matches = ConstraintSet::constrain_typevar( db, From d75a93d0fb83b20e086814de5afba081733fddcc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 24 Dec 2025 09:37:00 -0500 Subject: [PATCH 08/12] Allow Top to be assignable to (...) -> object --- .../resources/mdtest/narrow/callable.md | 20 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 16 +++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index b7255e0a6ee52f..18e0cb22e3602f 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -70,3 +70,23 @@ def call_with_args(y: object, a: int, b: str) -> object: return y(a, b) return None ``` + +## Assignability of narrowed callables + +A narrowed callable `Top[Callable[..., object]]` should be assignable to `Callable[..., Any]`. This +is important for decorators and other patterns where we need to pass the narrowed callable to +functions expecting gradual callables. + +```py +from typing import Any, Callable, TypeVar + +F = TypeVar("F", bound=Callable[..., Any]) + +def wrap(f: F) -> F: + return f + +def f(x: object): + if callable(x): + # x has type `Top[(...) -> object]`, which should be assignable to `Callable[..., Any]` + wrap(x) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 7e0b806106c20e..d2a9d25deaf7d5 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -12685,9 +12685,21 @@ impl<'db> CallableType<'db> { ); } - // Top[...] <: non-Top is always false. - // Top is not a subtype of any specific callable. + // Top[...] <: non-Top: depends on whether target has gradual parameters. + // For assignability, Top[(...) -> R] can be assigned to (...) -> S if R <: S + // (where (...) represents gradual parameters). + // For subtyping, Top is never a subtype of any specific callable. (true, false) => { + if !relation.is_subtyping() && other.signatures(db).has_gradual_parameters() { + return self.signatures(db).return_types_have_relation_to( + db, + other.signatures(db), + inferable, + relation, + relation_visitor, + disjointness_visitor, + ); + } return ConstraintSet::from(false); } } From 5201bc3d0acf29a53d65695f969d3684c19e725f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 24 Dec 2025 12:17:22 -0500 Subject: [PATCH 09/12] Address review comments --- crates/ty/docs/rules.md | 2 +- .../mdtest/type_properties/materialization.md | 6 ++- crates/ty_python_semantic/src/types.rs | 45 ++++--------------- .../src/types/diagnostic.rs | 2 +- .../src/types/signatures.rs | 5 +-- ty.schema.json | 2 +- 6 files changed, 19 insertions(+), 43 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 92875af33c137e..e4fa524b321019 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -126,7 +126,7 @@ so no specific set of arguments can be guaranteed to be valid. ```python def f(x: object): if callable(x): - x() # We know x is callable, but not what arguments it accepts + x() # error: We know x is callable, but not what arguments it accepts ``` ## `conflicting-argument-forms` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index 769c61c950e68c..a87fda36f2d7d3 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -213,7 +213,11 @@ static_assert(is_subtype_of(Bottom[Callable[..., Never]], EquivalentToBottom)) # TODO: is_equivalent_to only considers types of the same kind equivalent (Callable vs ProtocolInstance), # so this fails even though mutual subtyping proves semantic equivalence. -# static_assert(is_equivalent_to(Bottom[Callable[..., Never]], EquivalentToBottom)) +static_assert(is_equivalent_to(Bottom[Callable[..., Never]], EquivalentToBottom)) # error: [static-assert-error] + +# Top-materialized callables are not equivalent to non-top-materialized callables, even if their +# signatures would otherwise be equivalent after materialization. +static_assert(not is_equivalent_to(Top[Callable[..., object]], Callable[..., object])) ``` ## Tuple diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d2a9d25deaf7d5..268a9af27282e6 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -12666,42 +12666,15 @@ impl<'db> CallableType<'db> { // For Top, we only need to compare return types because Top parameters are a supertype // of all possible parameters. Bottom materializations are simplified to the bottom // callable directly, so they use normal signature comparison. - match ( - self.is_top_materialization(db), - other.is_top_materialization(db), - ) { - // Neither is a top materialization: use normal signature comparison. - (false, false) => {} - - // Anything <: Top[...]: just compare return types. - (_, true) => { - return self.signatures(db).return_types_have_relation_to( - db, - other.signatures(db), - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); - } - - // Top[...] <: non-Top: depends on whether target has gradual parameters. - // For assignability, Top[(...) -> R] can be assigned to (...) -> S if R <: S - // (where (...) represents gradual parameters). - // For subtyping, Top is never a subtype of any specific callable. - (true, false) => { - if !relation.is_subtyping() && other.signatures(db).has_gradual_parameters() { - return self.signatures(db).return_types_have_relation_to( - db, - other.signatures(db), - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); - } - return ConstraintSet::from(false); - } + if other.is_top_materialization(db) { + return self.signatures(db).return_types_have_relation_to( + db, + other.signatures(db), + inferable, + relation, + relation_visitor, + disjointness_visitor, + ); } self.signatures(db).has_relation_to_impl( diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 7a5174be81587e..34a8418a7459ca 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -168,7 +168,7 @@ declare_lint! { /// ```python /// def f(x: object): /// if callable(x): - /// x() # We know x is callable, but not what arguments it accepts + /// x() # error: We know x is callable, but not what arguments it accepts /// ``` pub(crate) static CALL_TOP_CALLABLE = { summary: "detects calls to the top callable type", diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index dcc2ffb9b73e6b..efb9914bf591c8 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -579,9 +579,8 @@ impl<'db> CallableSignature<'db> { /// Materialize only the return types of all signatures, preserving parameters as-is. /// - /// This is used when wrapping gradual callables in `Top[...]` or `Bottom[...]` - we want - /// to preserve the gradual parameters but materialize the return types (which are in - /// covariant position). + /// This is used when wrapping gradual callables in `Top[...]`. We want to preserve the gradual + /// parameters but materialize the return types (which are in covariant position). pub(crate) fn materialize_return_types( &self, db: &'db dyn Db, diff --git a/ty.schema.json b/ty.schema.json index b5edc9332ace29..2f4e7b95c4689f 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -358,7 +358,7 @@ }, "call-top-callable": { "title": "detects calls to the top callable type", - "description": "## What it does\nChecks for calls to objects typed as `Top[Callable[..., T]]` (the infinite union of all\ncallable types with return type `T`).\n\n## Why is this bad?\nWhen an object is narrowed to `Top[Callable[..., object]]` (e.g., via `callable(x)` or\n`isinstance(x, Callable)`), we know the object is callable, but we don't know its\nprecise signature. This type represents the set of all possible callable types\n(including, e.g., functions that take no arguments and functions that require arguments),\nso no specific set of arguments can be guaranteed to be valid.\n\n## Examples\n```python\ndef f(x: object):\n if callable(x):\n x() # We know x is callable, but not what arguments it accepts\n```", + "description": "## What it does\nChecks for calls to objects typed as `Top[Callable[..., T]]` (the infinite union of all\ncallable types with return type `T`).\n\n## Why is this bad?\nWhen an object is narrowed to `Top[Callable[..., object]]` (e.g., via `callable(x)` or\n`isinstance(x, Callable)`), we know the object is callable, but we don't know its\nprecise signature. This type represents the set of all possible callable types\n(including, e.g., functions that take no arguments and functions that require arguments),\nso no specific set of arguments can be guaranteed to be valid.\n\n## Examples\n```python\ndef f(x: object):\n if callable(x):\n x() # error: We know x is callable, but not what arguments it accepts\n```", "default": "error", "oneOf": [ { From 2bb397c08fc6ade08157af957d016ef67660e628 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 24 Dec 2025 12:41:41 -0500 Subject: [PATCH 10/12] Update crates/ty_python_semantic/resources/mdtest/narrow/callable.md Co-authored-by: Alex Waygood --- crates/ty_python_semantic/resources/mdtest/narrow/callable.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index 18e0cb22e3602f..9762dc6e6ebed6 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -79,6 +79,9 @@ functions expecting gradual callables. ```py from typing import Any, Callable, TypeVar +from ty_extensions import static_assert, Top, is_assignable_to + +static_assert(is_assignable_to(Top[Callable[..., bool]], Callable[..., int])) F = TypeVar("F", bound=Callable[..., Any]) From f215c39086df77f88af181f713afb82360ad959f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 24 Dec 2025 12:41:48 -0500 Subject: [PATCH 11/12] Update crates/ty_python_semantic/src/types/diagnostic.rs Co-authored-by: Alex Waygood --- crates/ty_python_semantic/src/types/diagnostic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 34a8418a7459ca..6df76743c909a0 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -168,7 +168,7 @@ declare_lint! { /// ```python /// def f(x: object): /// if callable(x): - /// x() # error: We know x is callable, but not what arguments it accepts + /// x() # error: We know `x` is callable, but not what arguments it accepts /// ``` pub(crate) static CALL_TOP_CALLABLE = { summary: "detects calls to the top callable type", From 81475b2944ed6b585924f007bb1712ee1c90cf81 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 24 Dec 2025 12:42:18 -0500 Subject: [PATCH 12/12] Regenerate --- crates/ty/docs/rules.md | 2 +- ty.schema.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index e4fa524b321019..ee581801993283 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -126,7 +126,7 @@ so no specific set of arguments can be guaranteed to be valid. ```python def f(x: object): if callable(x): - x() # error: We know x is callable, but not what arguments it accepts + x() # error: We know `x` is callable, but not what arguments it accepts ``` ## `conflicting-argument-forms` diff --git a/ty.schema.json b/ty.schema.json index 2f4e7b95c4689f..e93fd1a6b9be69 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -358,7 +358,7 @@ }, "call-top-callable": { "title": "detects calls to the top callable type", - "description": "## What it does\nChecks for calls to objects typed as `Top[Callable[..., T]]` (the infinite union of all\ncallable types with return type `T`).\n\n## Why is this bad?\nWhen an object is narrowed to `Top[Callable[..., object]]` (e.g., via `callable(x)` or\n`isinstance(x, Callable)`), we know the object is callable, but we don't know its\nprecise signature. This type represents the set of all possible callable types\n(including, e.g., functions that take no arguments and functions that require arguments),\nso no specific set of arguments can be guaranteed to be valid.\n\n## Examples\n```python\ndef f(x: object):\n if callable(x):\n x() # error: We know x is callable, but not what arguments it accepts\n```", + "description": "## What it does\nChecks for calls to objects typed as `Top[Callable[..., T]]` (the infinite union of all\ncallable types with return type `T`).\n\n## Why is this bad?\nWhen an object is narrowed to `Top[Callable[..., object]]` (e.g., via `callable(x)` or\n`isinstance(x, Callable)`), we know the object is callable, but we don't know its\nprecise signature. This type represents the set of all possible callable types\n(including, e.g., functions that take no arguments and functions that require arguments),\nso no specific set of arguments can be guaranteed to be valid.\n\n## Examples\n```python\ndef f(x: object):\n if callable(x):\n x() # error: We know `x` is callable, but not what arguments it accepts\n```", "default": "error", "oneOf": [ {