Skip to content

Commit cb04343

Browse files
authored
[ty] Split invalid-base error code into two error codes (#18245)
1 parent 02394b8 commit cb04343

9 files changed

+564
-126
lines changed

crates/ty/docs/rules.md

Lines changed: 102 additions & 55 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty_python_semantic/resources/mdtest/mro.md

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ if hasattr(DoesNotExist, "__mro__"):
173173
if not isinstance(DoesNotExist, type):
174174
reveal_type(DoesNotExist) # revealed: Unknown & ~type
175175

176-
class Foo(DoesNotExist): ... # error: [invalid-base]
176+
class Foo(DoesNotExist): ... # error: [unsupported-base]
177177
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
178178
```
179179

@@ -232,11 +232,15 @@ reveal_type(AA.__mro__) # revealed: tuple[<class 'AA'>, <class 'Z'>, Unknown, <
232232

233233
## `__bases__` includes a `Union`
234234

235+
<!-- snapshot-diagnostics -->
236+
235237
We don't support union types in a class's bases; a base must resolve to a single `ClassType`. If we
236238
find a union type in a class's bases, we infer the class's `__mro__` as being
237239
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
238240

239241
```py
242+
from typing_extensions import reveal_type
243+
240244
def returns_bool() -> bool:
241245
return True
242246

@@ -250,7 +254,7 @@ else:
250254

251255
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
252256

253-
# error: 11 [invalid-base] "Invalid class base with type `<class 'A'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
257+
# error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
254258
class Foo(x): ...
255259

256260
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
@@ -259,8 +263,8 @@ reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'obje
259263
## `__bases__` is a union of a dynamic type and valid bases
260264

261265
If a dynamic type such as `Any` or `Unknown` is one of the elements in the union, and all other
262-
types *would be* valid class bases, we do not emit an `invalid-base` diagnostic and use the dynamic
263-
type as a base to prevent further downstream errors.
266+
types *would be* valid class bases, we do not emit an `invalid-base` or `unsupported-base`
267+
diagnostic, and we use the dynamic type as a base to prevent further downstream errors.
264268

265269
```py
266270
from typing import Any
@@ -299,8 +303,8 @@ else:
299303
reveal_type(x) # revealed: <class 'A'> | <class 'B'>
300304
reveal_type(y) # revealed: <class 'C'> | <class 'D'>
301305

302-
# error: 11 [invalid-base] "Invalid class base with type `<class 'A'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
303-
# error: 14 [invalid-base] "Invalid class base with type `<class 'C'> | <class 'D'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
306+
# error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
307+
# error: 14 [unsupported-base] "Unsupported class base with type `<class 'C'> | <class 'D'>`"
304308
class Foo(x, y): ...
305309

306310
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
@@ -321,7 +325,7 @@ if returns_bool():
321325
else:
322326
foo = object
323327

324-
# error: 21 [invalid-base] "Invalid class base with type `<class 'Y'> | <class 'object'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
328+
# error: 21 [unsupported-base] "Unsupported class base with type `<class 'Y'> | <class 'object'>`"
325329
class PossibleError(foo, X): ...
326330

327331
reveal_type(PossibleError.__mro__) # revealed: tuple[<class 'PossibleError'>, Unknown, <class 'object'>]
@@ -339,12 +343,47 @@ else:
339343
# revealed: tuple[<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>] | tuple[<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>]
340344
reveal_type(B.__mro__)
341345

342-
# error: 12 [invalid-base] "Invalid class base with type `<class 'B'> | <class 'B'>` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
346+
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'B'> | <class 'B'>`"
343347
class Z(A, B): ...
344348

345349
reveal_type(Z.__mro__) # revealed: tuple[<class 'Z'>, Unknown, <class 'object'>]
346350
```
347351

352+
## `__bases__` lists that include objects that are not instances of `type`
353+
354+
<!-- snapshot-diagnostics -->
355+
356+
```py
357+
class Foo(2): ... # error: [invalid-base]
358+
```
359+
360+
A base that is not an instance of `type` but does have an `__mro_entries__` method will not raise an
361+
exception at runtime, so we issue `unsupported-base` rather than `invalid-base`:
362+
363+
```py
364+
class Foo:
365+
def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
366+
return ()
367+
368+
class Bar(Foo()): ... # error: [unsupported-base]
369+
```
370+
371+
But for objects that have badly defined `__mro_entries__`, `invalid-base` is emitted rather than
372+
`unsupported-base`:
373+
374+
```py
375+
class Bad1:
376+
def __mro_entries__(self, bases, extra_arg):
377+
return ()
378+
379+
class Bad2:
380+
def __mro_entries__(self, bases) -> int:
381+
return 42
382+
383+
class BadSub1(Bad1()): ... # error: [invalid-base]
384+
class BadSub2(Bad2()): ... # error: [invalid-base]
385+
```
386+
348387
## `__bases__` lists with duplicate bases
349388

350389
<!-- snapshot-diagnostics -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: mro.md - Method Resolution Order tests - `__bases__` includes a `Union`
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | from typing_extensions import reveal_type
16+
2 |
17+
3 | def returns_bool() -> bool:
18+
4 | return True
19+
5 |
20+
6 | class A: ...
21+
7 | class B: ...
22+
8 |
23+
9 | if returns_bool():
24+
10 | x = A
25+
11 | else:
26+
12 | x = B
27+
13 |
28+
14 | reveal_type(x) # revealed: <class 'A'> | <class 'B'>
29+
15 |
30+
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
31+
17 | class Foo(x): ...
32+
18 |
33+
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
34+
```
35+
36+
# Diagnostics
37+
38+
```
39+
info[revealed-type]: Revealed type
40+
--> src/mdtest_snippet.py:14:13
41+
|
42+
12 | x = B
43+
13 |
44+
14 | reveal_type(x) # revealed: <class 'A'> | <class 'B'>
45+
| ^ `<class 'A'> | <class 'B'>`
46+
15 |
47+
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
48+
|
49+
50+
```
51+
52+
```
53+
warning[unsupported-base]: Unsupported class base with type `<class 'A'> | <class 'B'>`
54+
--> src/mdtest_snippet.py:17:11
55+
|
56+
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
57+
17 | class Foo(x): ...
58+
| ^
59+
18 |
60+
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
61+
|
62+
info: ty cannot resolve a consistent MRO for class `Foo` due to this base
63+
info: Only class objects or `Any` are supported as class bases
64+
info: rule `unsupported-base` is enabled by default
65+
66+
```
67+
68+
```
69+
info[revealed-type]: Revealed type
70+
--> src/mdtest_snippet.py:19:13
71+
|
72+
17 | class Foo(x): ...
73+
18 |
74+
19 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
75+
| ^^^^^^^^^^^ `tuple[<class 'Foo'>, Unknown, <class 'object'>]`
76+
|
77+
78+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: mro.md - Method Resolution Order tests - `__bases__` lists that include objects that are not instances of `type`
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | class Foo(2): ... # error: [invalid-base]
16+
2 | class Foo:
17+
3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
18+
4 | return ()
19+
5 |
20+
6 | class Bar(Foo()): ... # error: [unsupported-base]
21+
7 | class Bad1:
22+
8 | def __mro_entries__(self, bases, extra_arg):
23+
9 | return ()
24+
10 |
25+
11 | class Bad2:
26+
12 | def __mro_entries__(self, bases) -> int:
27+
13 | return 42
28+
14 |
29+
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
30+
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
31+
```
32+
33+
# Diagnostics
34+
35+
```
36+
error[invalid-base]: Invalid class base with type `Literal[2]`
37+
--> src/mdtest_snippet.py:1:11
38+
|
39+
1 | class Foo(2): ... # error: [invalid-base]
40+
| ^
41+
2 | class Foo:
42+
3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
43+
|
44+
info: Definition of class `Foo` will raise `TypeError` at runtime
45+
info: rule `invalid-base` is enabled by default
46+
47+
```
48+
49+
```
50+
warning[unsupported-base]: Unsupported class base with type `Foo`
51+
--> src/mdtest_snippet.py:6:11
52+
|
53+
4 | return ()
54+
5 |
55+
6 | class Bar(Foo()): ... # error: [unsupported-base]
56+
| ^^^^^
57+
7 | class Bad1:
58+
8 | def __mro_entries__(self, bases, extra_arg):
59+
|
60+
info: ty cannot resolve a consistent MRO for class `Bar` due to this base
61+
info: Only class objects or `Any` are supported as class bases
62+
info: rule `unsupported-base` is enabled by default
63+
64+
```
65+
66+
```
67+
error[invalid-base]: Invalid class base with type `Bad1`
68+
--> src/mdtest_snippet.py:15:15
69+
|
70+
13 | return 42
71+
14 |
72+
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
73+
| ^^^^^^
74+
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
75+
|
76+
info: Definition of class `BadSub1` will raise `TypeError` at runtime
77+
info: An instance type is only a valid class base if it has a valid `__mro_entries__` method
78+
info: Type `Bad1` has an `__mro_entries__` method, but it cannot be called with the expected arguments
79+
info: Expected a signature at least as permissive as `def __mro_entries__(self, bases: tuple[type, ...], /) -> tuple[type, ...]`
80+
info: rule `invalid-base` is enabled by default
81+
82+
```
83+
84+
```
85+
error[invalid-base]: Invalid class base with type `Bad2`
86+
--> src/mdtest_snippet.py:16:15
87+
|
88+
15 | class BadSub1(Bad1()): ... # error: [invalid-base]
89+
16 | class BadSub2(Bad2()): ... # error: [invalid-base]
90+
| ^^^^^^
91+
|
92+
info: Definition of class `BadSub2` will raise `TypeError` at runtime
93+
info: An instance type is only a valid class base if it has a valid `__mro_entries__` method
94+
info: Type `Bad2` has an `__mro_entries__` method, but it does not return a tuple of types
95+
info: rule `invalid-base` is enabled by default
96+
97+
```

crates/ty_python_semantic/src/types/class_base.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,12 @@ impl<'db> ClassBase<'db> {
164164
}
165165
}
166166
Type::NominalInstance(_) => None, // TODO -- handle `__mro_entries__`?
167-
Type::PropertyInstance(_) => None,
168-
Type::Never
167+
168+
// This likely means that we're in unreachable code,
169+
// in which case we want to treat `Never` in a forgiving way and silence diagnostics
170+
Type::Never => Some(ClassBase::unknown()),
171+
172+
Type::PropertyInstance(_)
169173
| Type::BooleanLiteral(_)
170174
| Type::FunctionLiteral(_)
171175
| Type::Callable(..)

0 commit comments

Comments
 (0)