Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/loops/for.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,36 @@ def f(x: Literal["foo", b"bar"], y: Literal["foo"] | range):
reveal_type(item) # revealed: Literal["f", "o"] | int
```

## Attribute errors from iterated aliased unions

We should still report missing attributes when a loop variable comes from an aliased union element:

```toml
[environment]
python-version = "3.12"
```

```py
class A:
pass

class B:
def do_b_thing(self) -> None:
pass

type U = A | B

class C:
def __init__(self, values: list[U]) -> None:
self.values = values

def f(self) -> None:
for item in self.values:
reveal_type(item) # revealed: Unknown | A | B
# error: [unresolved-attribute] "Attribute `do_b_thing` is not defined on `A` in union `Unknown | U`"
item.do_b_thing()
```

## Union type as iterable where one union element has no `__iter__` method

<!-- snapshot-diagnostics -->
Expand Down
30 changes: 25 additions & 5 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8347,6 +8347,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}

fn union_elements_missing_attribute<'db>(
db: &'db dyn Db,
ty: Type<'db>,
attr_name: &str,
missing_types: &mut FxIndexSet<Type<'db>>,
) {
if let Some(union) = ty.as_union_like(db) {
for element in union.elements(db) {
union_elements_missing_attribute(db, *element, attr_name, missing_types);
}
} else if ty.member(db, attr_name).place.is_undefined() {
missing_types.insert(ty);
}
}

let ast::ExprAttribute { value, attr, .. } = attribute;

let mut value_type = self.infer_maybe_standalone_expression(value, TypeContext::default());
Expand Down Expand Up @@ -8587,11 +8602,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// we want it to be an error. Use `as_union_like` here to handle type aliases
// of unions and `NewType`s of float/complex in addition to explicit unions.
if let Some(union) = value_type.as_union_like(db) {
let elements_missing_the_attribute: Vec<_> = union
.elements(db)
.iter()
.filter(|element| element.member(db, attr_name).place.is_undefined())
.collect();
let mut elements_missing_the_attribute = FxIndexSet::default();
for element in union.elements(db) {
union_elements_missing_attribute(
db,
*element,
attr_name,
&mut elements_missing_the_attribute,
);
}

if !elements_missing_the_attribute.is_empty() {
if let Some(builder) =
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, attribute)
Expand Down
Loading