[ty] Treat Callable dunder members as bound method descriptors (#20860)

## Summary

Dunder methods (at least the ones defined in the standard library)
always take an instance of the class as the first parameter. So it seems
reasonable to generally treat them as bound method descriptors if they
are defined via a `Callable` type.

This removes just a few false positives from the ecosystem, but solves
three user-reported issues:

closes https://github.com/astral-sh/ty/issues/908
closes https://github.com/astral-sh/ty/issues/1143
closes https://github.com/astral-sh/ty/issues/1209

In addition to the change here, I also considered [making `ClassVar`s
bound method descriptors](https://github.com/astral-sh/ruff/pull/20861).
However, there was zero ecosystem impact. So I think we can also close
https://github.com/astral-sh/ty/issues/491 with this PR.

closes https://github.com/astral-sh/ty/issues/491

## Test Plan

Added regression test
This commit is contained in:
David Peter
2025-10-14 14:27:52 +02:00
committed by GitHub
parent ac2c530377
commit 6341bb7403
5 changed files with 82 additions and 11 deletions

View File

@@ -11116,6 +11116,23 @@ impl<'db> IntersectionType<'db> {
}
}
/// Map a type transformation over all positive elements of the intersection. Leave the
/// negative elements unchanged.
pub(crate) fn map_positive(
self,
db: &'db dyn Db,
mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>,
) -> Type<'db> {
let mut builder = IntersectionBuilder::new(db);
for ty in self.positive(db) {
builder = builder.add_positive(transform_fn(ty));
}
for ty in self.negative(db) {
builder = builder.add_negative(*ty);
}
builder.build()
}
pub(crate) fn map_with_boundness(
self,
db: &'db dyn Db,

View File

@@ -2014,7 +2014,29 @@ impl<'db> ClassLiteral<'db> {
name: &str,
policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
self.class_member_inner(db, None, name, policy)
fn into_function_like_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> {
match ty {
Type::Callable(callable_ty) => {
Type::Callable(CallableType::new(db, callable_ty.signatures(db), true))
}
Type::Union(union) => {
union.map(db, |element| into_function_like_callable(db, *element))
}
Type::Intersection(intersection) => intersection
.map_positive(db, |element| into_function_like_callable(db, *element)),
_ => ty,
}
}
let mut member = self.class_member_inner(db, None, name, policy);
// We generally treat dunder attributes with `Callable` types as function-like callables.
// See `callables_as_descriptors.md` for more details.
if name.starts_with("__") && name.ends_with("__") {
member = member.map_type(|ty| into_function_like_callable(db, ty));
}
member
}
fn class_member_inner(