mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-24 09:06:17 +00:00
[ty] Attribute access on intersections with negative parts (#19524)
## Summary
We currently infer a `@Todo` type whenever we access an attribute on an
intersection type with negative components. This can happen very
naturally. Consequently, this `@Todo` type is rather pervasive and hides
a lot of true positives that ty could otherwise detect:
```py
class Foo:
attr: int = 1
def _(f: Foo | None):
if f:
reveal_type(f) # Foo & ~AlwaysFalsy
reveal_type(f.attr) # now: int, previously: @Todo
```
The changeset here proposes to handle member access on these
intersection types by simply ignoring all negative contributions. This
is not always ideal: a negative contribution like `~<Protocol with
members 'attr'>` could be a hint that `.attr` should not be accessible
on the full intersection type. The behavior can certainly be improved in
the future, but this seems like a reasonable initial step to get rid of
this unnecessary `@Todo` type.
## Ecosystem analysis
There are quite a few changes here. I spot-checked them and found one
bug where attribute access on pure negation types (`~P == object & ~P`)
would not allow attributes on `object` to be accessed. After that was
fixed, I only see true positives and known problems. The fact that a lot
of `unused-ignore-comment` diagnostics go away are also evidence for the
fact that this touches a sensitive area, where static analysis clashes
with dynamically adding attributes to objects:
```py
… # type: ignore # Runtime attribute access
```
## Test Plan
Updated tests.
This commit is contained in:
parent
d4eb4277ad
commit
c0768dfd96
4 changed files with 92 additions and 57 deletions
|
|
@ -8375,21 +8375,27 @@ impl<'db> IntersectionType<'db> {
|
|||
sorted_self == other.normalized(db)
|
||||
}
|
||||
|
||||
/// Returns an iterator over the positive elements of the intersection. If
|
||||
/// there are no positive elements, returns a single `object` type.
|
||||
fn positive_elements_or_object(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {
|
||||
if self.positive(db).is_empty() {
|
||||
Either::Left(std::iter::once(Type::object(db)))
|
||||
} else {
|
||||
Either::Right(self.positive(db).iter().copied())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_with_boundness(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>,
|
||||
) -> Place<'db> {
|
||||
if !self.negative(db).is_empty() {
|
||||
return Place::todo("map_with_boundness: intersections with negative contributions");
|
||||
}
|
||||
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
|
||||
let mut all_unbound = true;
|
||||
let mut any_definitely_bound = false;
|
||||
for ty in self.positive(db) {
|
||||
let ty_member = transform_fn(ty);
|
||||
for ty in self.positive_elements_or_object(db) {
|
||||
let ty_member = transform_fn(&ty);
|
||||
match ty_member {
|
||||
Place::Unbound => {}
|
||||
Place::Type(ty_member, member_boundness) => {
|
||||
|
|
@ -8422,21 +8428,16 @@ impl<'db> IntersectionType<'db> {
|
|||
db: &'db dyn Db,
|
||||
mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
if !self.negative(db).is_empty() {
|
||||
return Place::todo("map_with_boundness: intersections with negative contributions")
|
||||
.into();
|
||||
}
|
||||
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
let mut qualifiers = TypeQualifiers::empty();
|
||||
|
||||
let mut any_unbound = false;
|
||||
let mut any_possibly_unbound = false;
|
||||
for ty in self.positive(db) {
|
||||
for ty in self.positive_elements_or_object(db) {
|
||||
let PlaceAndQualifiers {
|
||||
place: member,
|
||||
qualifiers: new_qualifiers,
|
||||
} = transform_fn(ty);
|
||||
} = transform_fn(&ty);
|
||||
qualifiers |= new_qualifiers;
|
||||
match member {
|
||||
Place::Unbound => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue