[ty] Preserve qualifiers when accessing attributes on unions/intersections (#20114)

## Summary

Properly preserve type qualifiers when accessing attributes on unions
and intersections. This is a prerequisite for
https://github.com/astral-sh/ruff/pull/19579.

Also fix a completely wrong implementation of
`map_with_boundness_and_qualifiers`. It now closely follows
`map_with_boundness` (just above).

## Test Plan

I thought about it, but didn't find any easy way to test this. This only
affected `Type::member`. Things like validation of attribute writes
(where type qualifiers like `ClassVar` and `Final` are important) were
already handling things correctly.
This commit is contained in:
David Peter 2025-08-27 20:01:45 +02:00 committed by GitHub
parent ce1dc21e7e
commit 0b3548755c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 16 additions and 22 deletions

View file

@ -3309,19 +3309,14 @@ impl<'db> Type<'db> {
let name_str = name.as_str();
match self {
Type::Union(union) => union
.map_with_boundness(db, |elem| {
elem.member_lookup_with_policy(db, name_str.into(), policy)
.place
})
.into(),
Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| {
elem.member_lookup_with_policy(db, name_str.into(), policy)
}),
Type::Intersection(intersection) => intersection
.map_with_boundness(db, |elem| {
.map_with_boundness_and_qualifiers(db, |elem| {
elem.member_lookup_with_policy(db, name_str.into(), policy)
.place
})
.into(),
}),
Type::Dynamic(..) | Type::Never => Place::bound(self).into(),
@ -9743,8 +9738,8 @@ impl<'db> IntersectionType<'db> {
let mut builder = IntersectionBuilder::new(db);
let mut qualifiers = TypeQualifiers::empty();
let mut any_unbound = false;
let mut any_possibly_unbound = false;
let mut all_unbound = true;
let mut any_definitely_bound = false;
for ty in self.positive_elements_or_object(db) {
let PlaceAndQualifiers {
place: member,
@ -9752,12 +9747,11 @@ impl<'db> IntersectionType<'db> {
} = transform_fn(&ty);
qualifiers |= new_qualifiers;
match member {
Place::Unbound => {
any_unbound = true;
}
Place::Unbound => {}
Place::Type(ty_member, member_boundness) => {
if member_boundness == Boundness::PossiblyUnbound {
any_possibly_unbound = true;
all_unbound = false;
if member_boundness == Boundness::Bound {
any_definitely_bound = true;
}
builder = builder.add_positive(ty_member);
@ -9766,15 +9760,15 @@ impl<'db> IntersectionType<'db> {
}
PlaceAndQualifiers {
place: if any_unbound {
place: if all_unbound {
Place::Unbound
} else {
Place::Type(
builder.build(),
if any_possibly_unbound {
Boundness::PossiblyUnbound
} else {
if any_definitely_bound {
Boundness::Bound
} else {
Boundness::PossiblyUnbound
},
)
},