mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 20:24:27 +00:00
[ty] Improve diagnostics for invalid exceptions (#21475)
Some checks are pending
CI / cargo fmt (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / cargo fmt (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
## Summary
Not a high-priority task... but it _is_ a weekend :P
This PR improves our diagnostics for invalid exceptions. Specifically:
- We now give a special-cased ``help: Did you mean
`NotImplementedError`` subdiagnostic for `except NotImplemented`, `raise
NotImplemented` and `raise <EXCEPTION> from NotImplemented`
- If the user catches a tuple of exceptions (`except (foo, bar, baz):`)
and multiple elements in the tuple are invalid, we now collect these
into a single diagnostic rather than emitting a separate diagnostic for
each tuple element
- The explanation of why the `except`/`raise` was invalid ("must be a
`BaseException` instance or `BaseException` subclass", etc.) is
relegated to a subdiagnostic. This makes the top-level diagnostic
summary much more concise.
## Test Plan
Lots of snapshots. And here's some screenshots:
<details>
<summary>Screenshots</summary>
<img width="1770" height="1520" alt="image"
src="https://github.com/user-attachments/assets/7f27fd61-c74d-4ddf-ad97-ea4fd24d06fd"
/>
<img width="1916" height="1392" alt="image"
src="https://github.com/user-attachments/assets/83e5027c-8798-48a6-a0ec-1babfc134000"
/>
<img width="1696" height="588" alt="image"
src="https://github.com/user-attachments/assets/1bc16048-6eb4-4dfa-9ace-dd271074530f"
/>
</details>
This commit is contained in:
parent
fb5b8c3653
commit
3065f8dbbc
5 changed files with 374 additions and 26 deletions
|
|
@ -116,17 +116,18 @@ def silence2[T: (
|
|||
|
||||
## Invalid exception handlers
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
# error: [invalid-exception-caught]
|
||||
except 3 as e:
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
# error: [invalid-exception-caught]
|
||||
except (ValueError, OSError, "foo", b"bar") as e:
|
||||
reveal_type(e) # revealed: ValueError | OSError | Unknown
|
||||
|
||||
|
|
@ -278,3 +279,20 @@ def f(x: type[Exception]):
|
|||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(e) # revealed: None
|
||||
```
|
||||
|
||||
## Special-cased diagnostics for `NotImplemented`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
try:
|
||||
# error: [invalid-raise]
|
||||
# error: [invalid-raise]
|
||||
raise NotImplemented from NotImplemented
|
||||
# error: [invalid-exception-caught]
|
||||
except NotImplemented:
|
||||
pass
|
||||
# error: [invalid-exception-caught]
|
||||
except (TypeError, NotImplemented):
|
||||
pass
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: basic.md - Exception Handling - Invalid exception handlers
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/exception/basic.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | try:
|
||||
2 | pass
|
||||
3 | # error: [invalid-exception-caught]
|
||||
4 | except 3 as e:
|
||||
5 | reveal_type(e) # revealed: Unknown
|
||||
6 |
|
||||
7 | try:
|
||||
8 | pass
|
||||
9 | # error: [invalid-exception-caught]
|
||||
10 | except (ValueError, OSError, "foo", b"bar") as e:
|
||||
11 | reveal_type(e) # revealed: ValueError | OSError | Unknown
|
||||
12 |
|
||||
13 | def foo(
|
||||
14 | x: type[str],
|
||||
15 | y: tuple[type[OSError], type[RuntimeError], int],
|
||||
16 | z: tuple[type[str], ...],
|
||||
17 | ):
|
||||
18 | try:
|
||||
19 | help()
|
||||
20 | # error: [invalid-exception-caught]
|
||||
21 | except x as e:
|
||||
22 | reveal_type(e) # revealed: Unknown
|
||||
23 | # error: [invalid-exception-caught]
|
||||
24 | except y as f:
|
||||
25 | reveal_type(f) # revealed: OSError | RuntimeError | Unknown
|
||||
26 | # error: [invalid-exception-caught]
|
||||
27 | except z as g:
|
||||
28 | reveal_type(g) # revealed: Unknown
|
||||
29 |
|
||||
30 | try:
|
||||
31 | {}.get("foo")
|
||||
32 | # error: [invalid-exception-caught]
|
||||
33 | except int:
|
||||
34 | pass
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-exception-caught]: Invalid object caught in an exception handler
|
||||
--> src/mdtest_snippet.py:4:8
|
||||
|
|
||||
2 | pass
|
||||
3 | # error: [invalid-exception-caught]
|
||||
4 | except 3 as e:
|
||||
| ^ Object has type `Literal[3]`
|
||||
5 | reveal_type(e) # revealed: Unknown
|
||||
|
|
||||
info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses
|
||||
info: rule `invalid-exception-caught` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-exception-caught]: Invalid tuple caught in an exception handler
|
||||
--> src/mdtest_snippet.py:10:8
|
||||
|
|
||||
8 | pass
|
||||
9 | # error: [invalid-exception-caught]
|
||||
10 | except (ValueError, OSError, "foo", b"bar") as e:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^-----^^------^
|
||||
| | |
|
||||
| | Invalid element of type `Literal[b"bar"]`
|
||||
| Invalid element of type `Literal["foo"]`
|
||||
11 | reveal_type(e) # revealed: ValueError | OSError | Unknown
|
||||
|
|
||||
info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses
|
||||
info: rule `invalid-exception-caught` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-exception-caught]: Invalid object caught in an exception handler
|
||||
--> src/mdtest_snippet.py:21:12
|
||||
|
|
||||
19 | help()
|
||||
20 | # error: [invalid-exception-caught]
|
||||
21 | except x as e:
|
||||
| ^ Object has type `type[str]`
|
||||
22 | reveal_type(e) # revealed: Unknown
|
||||
23 | # error: [invalid-exception-caught]
|
||||
|
|
||||
info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses
|
||||
info: rule `invalid-exception-caught` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-exception-caught]: Invalid tuple caught in an exception handler
|
||||
--> src/mdtest_snippet.py:24:12
|
||||
|
|
||||
22 | reveal_type(e) # revealed: Unknown
|
||||
23 | # error: [invalid-exception-caught]
|
||||
24 | except y as f:
|
||||
| ^ Object has type `tuple[type[OSError], type[RuntimeError], int]`
|
||||
25 | reveal_type(f) # revealed: OSError | RuntimeError | Unknown
|
||||
26 | # error: [invalid-exception-caught]
|
||||
|
|
||||
info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses
|
||||
info: rule `invalid-exception-caught` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-exception-caught]: Invalid tuple caught in an exception handler
|
||||
--> src/mdtest_snippet.py:27:12
|
||||
|
|
||||
25 | reveal_type(f) # revealed: OSError | RuntimeError | Unknown
|
||||
26 | # error: [invalid-exception-caught]
|
||||
27 | except z as g:
|
||||
| ^ Object has type `tuple[type[str], ...]`
|
||||
28 | reveal_type(g) # revealed: Unknown
|
||||
|
|
||||
info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses
|
||||
info: rule `invalid-exception-caught` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-exception-caught]: Invalid object caught in an exception handler
|
||||
--> src/mdtest_snippet.py:33:8
|
||||
|
|
||||
31 | {}.get("foo")
|
||||
32 | # error: [invalid-exception-caught]
|
||||
33 | except int:
|
||||
| ^^^ Object has type `<class 'int'>`
|
||||
34 | pass
|
||||
|
|
||||
info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses
|
||||
info: rule `invalid-exception-caught` is enabled by default
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: basic.md - Exception Handling - Special-cased diagnostics for `NotImplemented`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/exception/basic.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | try:
|
||||
2 | # error: [invalid-raise]
|
||||
3 | # error: [invalid-raise]
|
||||
4 | raise NotImplemented from NotImplemented
|
||||
5 | # error: [invalid-exception-caught]
|
||||
6 | except NotImplemented:
|
||||
7 | pass
|
||||
8 | # error: [invalid-exception-caught]
|
||||
9 | except (TypeError, NotImplemented):
|
||||
10 | pass
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-raise]: Cannot raise `NotImplemented`
|
||||
--> src/mdtest_snippet.py:4:11
|
||||
|
|
||||
2 | # error: [invalid-raise]
|
||||
3 | # error: [invalid-raise]
|
||||
4 | raise NotImplemented from NotImplemented
|
||||
| ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`?
|
||||
5 | # error: [invalid-exception-caught]
|
||||
6 | except NotImplemented:
|
||||
|
|
||||
info: Can only raise an instance or subclass of `BaseException`
|
||||
info: rule `invalid-raise` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-raise]: Cannot use `NotImplemented` as an exception cause
|
||||
--> src/mdtest_snippet.py:4:31
|
||||
|
|
||||
2 | # error: [invalid-raise]
|
||||
3 | # error: [invalid-raise]
|
||||
4 | raise NotImplemented from NotImplemented
|
||||
| ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`?
|
||||
5 | # error: [invalid-exception-caught]
|
||||
6 | except NotImplemented:
|
||||
|
|
||||
info: An exception cause must be an instance of `BaseException`, subclass of `BaseException`, or `None`
|
||||
info: rule `invalid-raise` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-exception-caught]: Cannot catch `NotImplemented` in an exception handler
|
||||
--> src/mdtest_snippet.py:6:8
|
||||
|
|
||||
4 | raise NotImplemented from NotImplemented
|
||||
5 | # error: [invalid-exception-caught]
|
||||
6 | except NotImplemented:
|
||||
| ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`?
|
||||
7 | pass
|
||||
8 | # error: [invalid-exception-caught]
|
||||
|
|
||||
info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses
|
||||
info: rule `invalid-exception-caught` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-exception-caught]: Invalid tuple caught in an exception handler
|
||||
--> src/mdtest_snippet.py:9:8
|
||||
|
|
||||
7 | pass
|
||||
8 | # error: [invalid-exception-caught]
|
||||
9 | except (TypeError, NotImplemented):
|
||||
| ^^^^^^^^^^^^--------------^
|
||||
| |
|
||||
| Invalid element of type `NotImplementedType`
|
||||
| Did you mean `NotImplementedError`?
|
||||
10 | pass
|
||||
|
|
||||
info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses
|
||||
info: rule `invalid-exception-caught` is enabled by default
|
||||
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue