Skip to content

Commit 4f449ae

Browse files
authored
[ty] Add error context for intersection types (astral-sh#24772)
## Summary This implementation is basically the dual of what we do for unions (with the assignability direction flipped). ## Test Plan Updated mdtests.
1 parent 5b4e753 commit 4f449ae

3 files changed

Lines changed: 135 additions & 45 deletions

File tree

crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -94,60 +94,89 @@ Assigning an intersection to a non-intersection:
9494

9595
```py
9696
from ty_extensions import Intersection
97+
from typing import Protocol
98+
99+
class SupportsFoo(Protocol):
100+
def foo(self) -> None: ...
101+
102+
class SupportsBar(Protocol):
103+
def bar(self) -> None: ...
104+
105+
class SupportsFooAndBar(Protocol):
106+
def foo(self) -> None: ...
107+
def bar(self) -> None: ...
97108

98-
class P: ...
99-
class Q: ...
100-
class R: ...
109+
class HasFoo:
110+
def foo(self) -> None: ...
101111

102-
def _(source: Intersection[P, Q]):
103-
target: int = source # snapshot
112+
class HasBar:
113+
def bar(self) -> None: ...
114+
115+
class HasNeither: ...
116+
117+
def _(source: Intersection[HasBar, HasNeither]):
118+
target: SupportsFooAndBar = source # snapshot
104119
```
105120

106121
```snapshot
107-
error[invalid-assignment]: Object of type `P & Q` is not assignable to `int`
108-
--> src/mdtest_snippet.py:8:13
109-
|
110-
8 | target: int = source # snapshot
111-
| --- ^^^^^^ Incompatible value of type `P & Q`
112-
| |
113-
| Declared type
114-
|
122+
error[invalid-assignment]: Object of type `HasBar & HasNeither` is not assignable to `SupportsFooAndBar`
123+
--> src/mdtest_snippet.py:23:13
124+
|
125+
23 | target: SupportsFooAndBar = source # snapshot
126+
| ----------------- ^^^^^^ Incompatible value of type `HasBar & HasNeither`
127+
| |
128+
| Declared type
129+
|
130+
info: no element of intersection `HasBar & HasNeither` is assignable to `SupportsFooAndBar`
131+
info: ├── type `HasBar` is not assignable to protocol `SupportsFooAndBar`
132+
info: │ └── protocol member `foo` is not defined on type `HasBar`
133+
info: └── type `HasNeither` is not assignable to protocol `SupportsFooAndBar`
134+
info: └── protocol member `bar` is not defined on type `HasNeither`
115135
```
116136

117137
Assigning a non-intersection to an intersection:
118138

119139
```py
120-
def _(source: P):
121-
target: Intersection[P, Q] = source # snapshot
140+
def _(source: HasFoo):
141+
target: Intersection[SupportsFoo, SupportsBar] = source # snapshot
122142
```
123143

124144
```snapshot
125-
error[invalid-assignment]: Object of type `P` is not assignable to `P & Q`
126-
--> src/mdtest_snippet.py:10:13
145+
error[invalid-assignment]: Object of type `HasFoo` is not assignable to `SupportsFoo & SupportsBar`
146+
--> src/mdtest_snippet.py:25:13
127147
|
128-
10 | target: Intersection[P, Q] = source # snapshot
129-
| ------------------ ^^^^^^ Incompatible value of type `P`
148+
25 | target: Intersection[SupportsFoo, SupportsBar] = source # snapshot
149+
| -------------------------------------- ^^^^^^ Incompatible value of type `HasFoo`
130150
| |
131151
| Declared type
132152
|
153+
info: type `HasFoo` is not assignable to element `SupportsBar` of intersection `SupportsFoo & SupportsBar`
154+
info: └── type `HasFoo` is not assignable to protocol `SupportsBar`
155+
info: └── protocol member `bar` is not defined on type `HasFoo`
133156
```
134157

135158
Assigning an intersection to an intersection:
136159

137160
```py
138-
def _(source: Intersection[P, R]):
139-
target: Intersection[P, Q] = source # snapshot
161+
def _(source: Intersection[HasFoo, HasNeither]):
162+
target: Intersection[SupportsFoo, SupportsBar] = source # snapshot
140163
```
141164

142165
```snapshot
143-
error[invalid-assignment]: Object of type `P & R` is not assignable to `P & Q`
144-
--> src/mdtest_snippet.py:12:13
166+
error[invalid-assignment]: Object of type `HasFoo & HasNeither` is not assignable to `SupportsFoo & SupportsBar`
167+
--> src/mdtest_snippet.py:27:13
145168
|
146-
12 | target: Intersection[P, Q] = source # snapshot
147-
| ------------------ ^^^^^^ Incompatible value of type `P & R`
169+
27 | target: Intersection[SupportsFoo, SupportsBar] = source # snapshot
170+
| -------------------------------------- ^^^^^^ Incompatible value of type `HasFoo & HasNeither`
148171
| |
149172
| Declared type
150173
|
174+
info: type `HasFoo & HasNeither` is not assignable to element `SupportsBar` of intersection `SupportsFoo & SupportsBar`
175+
info: └── no element of intersection `HasFoo & HasNeither` is assignable to `SupportsBar`
176+
info: ├── type `HasFoo` is not assignable to protocol `SupportsBar`
177+
info: │ └── protocol member `bar` is not defined on type `HasFoo`
178+
info: └── type `HasNeither` is not assignable to protocol `SupportsBar`
179+
info: └── protocol member `bar` is not defined on type `HasNeither`
151180
```
152181

153182
## Tuples
@@ -789,6 +818,11 @@ error[invalid-assignment]: Object of type `DoesNotSupportFoo1 & DoesNotSupportFo
789818
| |
790819
| Declared type
791820
|
821+
info: no element of intersection `DoesNotSupportFoo1 & DoesNotSupportFoo2` is assignable to `SupportsFoo`
822+
info: ├── type `DoesNotSupportFoo1` is not assignable to protocol `SupportsFoo`
823+
info: │ └── protocol member `foo` is not defined on type `DoesNotSupportFoo1`
824+
info: └── type `DoesNotSupportFoo2` is not assignable to protocol `SupportsFoo`
825+
info: └── protocol member `foo` is not defined on type `DoesNotSupportFoo2`
792826
```
793827

794828
## Assigning an overload set

crates/ty_python_semantic/src/types/relation.rs

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,7 +1211,15 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> {
12111211
.positive(db)
12121212
.iter()
12131213
.when_all(db, self.constraints, |&pos_ty| {
1214-
self.check_type_pair(db, source, pos_ty)
1214+
let constraint_set = self.check_type_pair(db, source, pos_ty);
1215+
if constraint_set.is_never_satisfied(db) {
1216+
self.provide_context(|| ErrorContext::NotAssignableToIntersectionElement {
1217+
source,
1218+
element: pos_ty,
1219+
intersection: target,
1220+
});
1221+
}
1222+
constraint_set
12151223
})
12161224
.and(db, self.constraints, || {
12171225
// For subtyping, we would want to check whether the *top materialization* of `source`
@@ -1258,27 +1266,48 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> {
12581266
// positive elements is a subtype of that type. If there are no positive elements,
12591267
// we treat `object` as the implicit positive element (e.g., `~str` is semantically
12601268
// `object & ~str`).
1261-
// TODO: Similar to how we do this for unions, we should collect error
1262-
// context for all elements and report it if *all* checks fail.
12631269

1264-
self.without_context_collection(|| {
1265-
intersection
1266-
.positive_elements_or_object(db)
1267-
.when_any(db, self.constraints, |elem_ty| {
1268-
self.check_type_pair(db, elem_ty, target)
1269-
})
1270-
.or(db, self.constraints, || {
1271-
if should_expand_intersection(intersection) {
1272-
self.check_type_pair(
1273-
db,
1274-
intersection.with_expanded_typevars_and_newtypes(db),
1275-
target,
1276-
)
1277-
} else {
1278-
self.never()
1270+
let mut elements_context = vec![];
1271+
let context_collection_enabled = self.is_context_collection_enabled();
1272+
1273+
let result = intersection
1274+
.positive_elements_or_object(db)
1275+
.when_any(db, self.constraints, |elem_ty| {
1276+
let result = self.check_type_pair(db, elem_ty, target);
1277+
if context_collection_enabled {
1278+
let ctx = self.context_tree.take();
1279+
if !ctx.is_empty() {
1280+
elements_context.push(ctx);
12791281
}
1280-
})
1281-
})
1282+
}
1283+
result
1284+
})
1285+
.or(db, self.constraints, || {
1286+
if should_expand_intersection(intersection) {
1287+
self.check_type_pair(
1288+
db,
1289+
intersection.with_expanded_typevars_and_newtypes(db),
1290+
target,
1291+
)
1292+
} else {
1293+
self.never()
1294+
}
1295+
});
1296+
1297+
if context_collection_enabled
1298+
&& !elements_context.is_empty()
1299+
&& result.is_never_satisfied(db)
1300+
{
1301+
self.set_context(
1302+
ErrorContext::NoIntersectionElementAssignableToTarget {
1303+
intersection: source,
1304+
target,
1305+
},
1306+
elements_context,
1307+
);
1308+
}
1309+
1310+
result
12821311
}
12831312

12841313
// `Never` is the bottom type, the empty set.

crates/ty_python_semantic/src/types/relation_error.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ pub(crate) enum ErrorContext<'db> {
5555
NotAssignableToNOtherUnionElements {
5656
n: usize,
5757
},
58+
NotAssignableToIntersectionElement {
59+
source: Type<'db>,
60+
element: Type<'db>,
61+
intersection: Type<'db>,
62+
},
63+
NoIntersectionElementAssignableToTarget {
64+
intersection: Type<'db>,
65+
target: Type<'db>,
66+
},
5867
TypedDictFieldMissing {
5968
field_name: Name,
6069
source: TypedDictType<'db>,
@@ -159,6 +168,24 @@ impl<'db> ErrorContext<'db> {
159168
"... omitted {n} union element{} without additional context",
160169
if *n == 1 { "" } else { "s" }
161170
),
171+
Self::NotAssignableToIntersectionElement {
172+
source,
173+
element,
174+
intersection,
175+
} => format!(
176+
"type `{}` is not assignable to element `{}` of intersection `{}`",
177+
source.display(db),
178+
element.display(db),
179+
intersection.display(db),
180+
),
181+
Self::NoIntersectionElementAssignableToTarget {
182+
intersection,
183+
target,
184+
} => format!(
185+
"no element of intersection `{}` is assignable to `{}`",
186+
intersection.display(db),
187+
target.display(db),
188+
),
162189
Self::TypedDictFieldMissing { field_name, source } => {
163190
format!(
164191
"required field \"{field_name}\" is not present in source {source}",

0 commit comments

Comments
 (0)