mirror of https://github.com/astral-sh/ruff
[ty] Add 'remove unused ignore comment' code action (#21582)
## Summary This PR adds a code action to remove unused ignore comments. This PR also includes some infrastructure boilerplate to set up code actions in the editor: * Extend `snapshot-diagnostics` to render fixes * Render fixes when using `--output-format=full` * Hook up edits and the code action request in the LSP * Add the `Unnecessary` tag to `unused-ignore-comment` diagnostics * Group multiple unused codes into a single diagnostic The same fix can be used on the CLI once we add `ty fix` Note: `unused-ignore-comment` is currently disabled by default. https://github.com/user-attachments/assets/f9e21087-3513-4156-85d7-a90b1a7a3489
This commit is contained in:
parent
eddb9ad38d
commit
15cb41c1f9
|
|
@ -4474,6 +4474,7 @@ dependencies = [
|
||||||
"quickcheck_macros",
|
"quickcheck_macros",
|
||||||
"ruff_annotate_snippets",
|
"ruff_annotate_snippets",
|
||||||
"ruff_db",
|
"ruff_db",
|
||||||
|
"ruff_diagnostics",
|
||||||
"ruff_index",
|
"ruff_index",
|
||||||
"ruff_macros",
|
"ruff_macros",
|
||||||
"ruff_memory_usage",
|
"ruff_memory_usage",
|
||||||
|
|
@ -4519,6 +4520,7 @@ dependencies = [
|
||||||
"lsp-types",
|
"lsp-types",
|
||||||
"regex",
|
"regex",
|
||||||
"ruff_db",
|
"ruff_db",
|
||||||
|
"ruff_diagnostics",
|
||||||
"ruff_macros",
|
"ruff_macros",
|
||||||
"ruff_notebook",
|
"ruff_notebook",
|
||||||
"ruff_python_ast",
|
"ruff_python_ast",
|
||||||
|
|
|
||||||
|
|
@ -2120,7 +2120,7 @@ old_func() # emits [deprecated] diagnostic
|
||||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ignore-comment-unknown-rule" target="_blank">Related issues</a> ·
|
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ignore-comment-unknown-rule" target="_blank">Related issues</a> ·
|
||||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L43" target="_blank">View source</a>
|
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L47" target="_blank">View source</a>
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2151,7 +2151,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
|
||||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-ignore-comment" target="_blank">Related issues</a> ·
|
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-ignore-comment" target="_blank">Related issues</a> ·
|
||||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L68" target="_blank">View source</a>
|
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L72" target="_blank">View source</a>
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2536,7 +2536,7 @@ print(x) # NameError: name 'x' is not defined
|
||||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unused-ignore-comment" target="_blank">Related issues</a> ·
|
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unused-ignore-comment" target="_blank">Related issues</a> ·
|
||||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L18" target="_blank">View source</a>
|
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L22" target="_blank">View source</a>
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,8 @@ impl MainLoop {
|
||||||
let terminal_settings = db.project().settings(db).terminal();
|
let terminal_settings = db.project().settings(db).terminal();
|
||||||
let display_config = DisplayDiagnosticConfig::default()
|
let display_config = DisplayDiagnosticConfig::default()
|
||||||
.format(terminal_settings.output_format.into())
|
.format(terminal_settings.output_format.into())
|
||||||
.color(colored::control::SHOULD_COLORIZE.should_colorize());
|
.color(colored::control::SHOULD_COLORIZE.should_colorize())
|
||||||
|
.show_fix_diff(true);
|
||||||
|
|
||||||
if check_revision == revision {
|
if check_revision == revision {
|
||||||
if db.project().files(db).is_empty() {
|
if db.project().files(db).is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ license = { workspace = true }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ruff_db = { workspace = true }
|
ruff_db = { workspace = true }
|
||||||
ruff_annotate_snippets = { workspace = true }
|
ruff_annotate_snippets = { workspace = true }
|
||||||
|
ruff_diagnostics = { workspace = true }
|
||||||
ruff_index = { workspace = true, features = ["salsa"] }
|
ruff_index = { workspace = true, features = ["salsa"] }
|
||||||
ruff_macros = { workspace = true }
|
ruff_macros = { workspace = true }
|
||||||
ruff_memory_usage = { workspace = true }
|
ruff_memory_usage = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,15 @@ info[unused-ignore-comment]: Unused blanket `type: ignore` directive
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
73 | ): ...
|
73 | ): ...
|
||||||
|
|
|
|
||||||
|
help: Remove the unused suppression comment
|
||||||
|
69 | class D(
|
||||||
|
70 | A,
|
||||||
|
71 | # error: [unused-ignore-comment]
|
||||||
|
- A, # type: ignore[duplicate-base]
|
||||||
|
72 + A,
|
||||||
|
73 | ): ...
|
||||||
|
74 |
|
||||||
|
75 | # error: [duplicate-base]
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -356,5 +365,13 @@ info[unused-ignore-comment]: Unused blanket `type: ignore` directive
|
||||||
82 |
|
82 |
|
||||||
83 | # fmt: on
|
83 | # fmt: on
|
||||||
|
|
|
|
||||||
|
help: Remove the unused suppression comment
|
||||||
|
78 | A
|
||||||
|
79 | ):
|
||||||
|
80 | # error: [unused-ignore-comment]
|
||||||
|
- x: int # type: ignore[duplicate-base]
|
||||||
|
81 + x: int
|
||||||
|
82 |
|
||||||
|
83 | # fmt: on
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
---
|
||||||
|
source: crates/ty_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: ty_ignore.md - Suppressing errors with `ty: ignore` - Multiple unused comments
|
||||||
|
mdtest path: crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive"
|
||||||
|
2 | a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference]
|
||||||
|
3 |
|
||||||
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
||||||
|
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
|
||||||
|
6 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
|
||||||
|
7 |
|
||||||
|
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
||||||
|
9 | a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
info[unused-ignore-comment]: Unused `ty: ignore` directive
|
||||||
|
--> src/mdtest_snippet.py:2:13
|
||||||
|
|
|
||||||
|
1 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive"
|
||||||
|
2 | a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
3 |
|
||||||
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
||||||
|
|
|
||||||
|
help: Remove the unused suppression comment
|
||||||
|
1 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive"
|
||||||
|
- a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference]
|
||||||
|
2 + a = 10 / 2
|
||||||
|
3 |
|
||||||
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
||||||
|
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment'
|
||||||
|
--> src/mdtest_snippet.py:6:26
|
||||||
|
|
|
||||||
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
||||||
|
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
|
||||||
|
6 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^
|
||||||
|
7 |
|
||||||
|
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
||||||
|
|
|
||||||
|
help: Remove the unused suppression code
|
||||||
|
3 |
|
||||||
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
||||||
|
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
|
||||||
|
- a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
|
||||||
|
6 + a = 10 / 0 # ty: ignore[division-by-zero, unresolved-reference]
|
||||||
|
7 |
|
||||||
|
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
||||||
|
9 | a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'unresolved-reference'
|
||||||
|
--> src/mdtest_snippet.py:6:64
|
||||||
|
|
|
||||||
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
||||||
|
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
|
||||||
|
6 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
7 |
|
||||||
|
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
||||||
|
|
|
||||||
|
help: Remove the unused suppression code
|
||||||
|
3 |
|
||||||
|
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
||||||
|
5 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
|
||||||
|
- a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
|
||||||
|
6 + a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero]
|
||||||
|
7 |
|
||||||
|
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
||||||
|
9 | a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'
|
||||||
|
--> src/mdtest_snippet.py:9:26
|
||||||
|
|
|
||||||
|
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
||||||
|
9 | a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
help: Remove the unused suppression codes
|
||||||
|
6 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
|
||||||
|
7 |
|
||||||
|
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
||||||
|
- a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
|
||||||
|
9 + a = 10 / 0 # ty: ignore[division-by-zero]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
---
|
||||||
|
source: crates/ty_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: type_ignore.md - Suppressing errors with `type: ignore` - Nested comments
|
||||||
|
mdtest path: crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | # fmt: off
|
||||||
|
2 | a = test /
|
||||||
|
3 | + 2 # fmt: skip # type: ignore
|
||||||
|
4 |
|
||||||
|
5 | a = test /
|
||||||
|
6 | + 2 # type: ignore # fmt: skip
|
||||||
|
7 |
|
||||||
|
8 | a = (3
|
||||||
|
9 | # error: [unused-ignore-comment]
|
||||||
|
10 | + 2) # ty:ignore[division-by-zero] # fmt: skip
|
||||||
|
11 |
|
||||||
|
12 | a = (3
|
||||||
|
13 | # error: [unused-ignore-comment]
|
||||||
|
14 | + 2) # fmt: skip # ty:ignore[division-by-zero]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
info[unused-ignore-comment]: Unused `ty: ignore` directive
|
||||||
|
--> src/mdtest_snippet.py:10:9
|
||||||
|
|
|
||||||
|
8 | a = (3
|
||||||
|
9 | # error: [unused-ignore-comment]
|
||||||
|
10 | + 2) # ty:ignore[division-by-zero] # fmt: skip
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
11 |
|
||||||
|
12 | a = (3
|
||||||
|
|
|
||||||
|
help: Remove the unused suppression comment
|
||||||
|
7 |
|
||||||
|
8 | a = (3
|
||||||
|
9 | # error: [unused-ignore-comment]
|
||||||
|
- + 2) # ty:ignore[division-by-zero] # fmt: skip
|
||||||
|
10 + + 2) # fmt: skip
|
||||||
|
11 |
|
||||||
|
12 | a = (3
|
||||||
|
13 | # error: [unused-ignore-comment]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
info[unused-ignore-comment]: Unused `ty: ignore` directive
|
||||||
|
--> src/mdtest_snippet.py:14:21
|
||||||
|
|
|
||||||
|
12 | a = (3
|
||||||
|
13 | # error: [unused-ignore-comment]
|
||||||
|
14 | + 2) # fmt: skip # ty:ignore[division-by-zero]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
help: Remove the unused suppression comment
|
||||||
|
11 |
|
||||||
|
12 | a = (3
|
||||||
|
13 | # error: [unused-ignore-comment]
|
||||||
|
- + 2) # fmt: skip # ty:ignore[division-by-zero]
|
||||||
|
14 + + 2) # fmt: skip
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -117,6 +117,6 @@ from typing import no_type_check
|
||||||
|
|
||||||
@no_type_check
|
@no_type_check
|
||||||
def test():
|
def test():
|
||||||
# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
|
# error: [unused-ignore-comment] "Unused `ty: ignore` directive"
|
||||||
return x + 5 # ty: ignore[unresolved-reference]
|
return x + 5 # ty: ignore[unresolved-reference]
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ a = 4 + test # ty: ignore[unresolved-reference]
|
||||||
|
|
||||||
```py
|
```py
|
||||||
test = 10
|
test = 10
|
||||||
# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'possibly-unresolved-reference'"
|
# error: [unused-ignore-comment] "Unused `ty: ignore` directive"
|
||||||
a = test + 3 # ty: ignore[possibly-unresolved-reference]
|
a = test + 3 # ty: ignore[possibly-unresolved-reference]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ a = test + 3 # ty: ignore[possibly-unresolved-reference]
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# error: [unresolved-reference]
|
# error: [unresolved-reference]
|
||||||
# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'possibly-unresolved-reference'"
|
# error: [unused-ignore-comment] "Unused `ty: ignore` directive"
|
||||||
a = test + 3 # ty: ignore[possibly-unresolved-reference]
|
a = test + 3 # ty: ignore[possibly-unresolved-reference]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -50,17 +50,20 @@ a = 10 / 0 # ty: ignore[division-by-zero, unused-ignore-comment]
|
||||||
|
|
||||||
## Multiple unused comments
|
## Multiple unused comments
|
||||||
|
|
||||||
Today, ty emits a diagnostic for every unused code. We might want to group the codes by comment at
|
ty groups unused codes that are next to each other.
|
||||||
some point in the future.
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'division-by-zero'"
|
# error: [unused-ignore-comment] "Unused `ty: ignore` directive"
|
||||||
# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
|
|
||||||
a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference]
|
a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference]
|
||||||
|
|
||||||
# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
||||||
# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
|
# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'"
|
||||||
a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
|
a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference]
|
||||||
|
|
||||||
|
# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
||||||
|
a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Multiple suppressions
|
## Multiple suppressions
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,8 @@ a = test # type: ignore[name-defined]
|
||||||
|
|
||||||
## Nested comments
|
## Nested comments
|
||||||
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# fmt: off
|
# fmt: off
|
||||||
a = test \
|
a = test \
|
||||||
|
|
@ -103,6 +105,14 @@ a = test \
|
||||||
|
|
||||||
a = test \
|
a = test \
|
||||||
+ 2 # type: ignore # fmt: skip
|
+ 2 # type: ignore # fmt: skip
|
||||||
|
|
||||||
|
a = (3
|
||||||
|
# error: [unused-ignore-comment]
|
||||||
|
+ 2) # ty:ignore[division-by-zero] # fmt: skip
|
||||||
|
|
||||||
|
a = (3
|
||||||
|
# error: [unused-ignore-comment]
|
||||||
|
+ 2) # fmt: skip # ty:ignore[division-by-zero]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Misspelled `type: ignore`
|
## Misspelled `type: ignore`
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,24 @@
|
||||||
use crate::diagnostic::DiagnosticGuard;
|
|
||||||
use crate::lint::{GetLintError, Level, LintMetadata, LintRegistry, LintStatus};
|
|
||||||
use crate::types::TypeCheckDiagnostics;
|
|
||||||
use crate::{Db, declare_lint, lint::LintId};
|
|
||||||
use ruff_db::diagnostic::{
|
|
||||||
Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Severity, Span,
|
|
||||||
};
|
|
||||||
use ruff_db::{files::File, parsed::parsed_module, source::source_text};
|
|
||||||
use ruff_python_parser::TokenKind;
|
|
||||||
use ruff_python_trivia::Cursor;
|
|
||||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fmt::Formatter;
|
use std::fmt::Formatter;
|
||||||
|
use std::fmt::Write as _;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::diagnostic::DiagnosticGuard;
|
||||||
|
use crate::lint::{GetLintError, Level, LintMetadata, LintRegistry, LintStatus};
|
||||||
|
use crate::types::TypeCheckDiagnostics;
|
||||||
|
use crate::{Db, declare_lint, lint::LintId};
|
||||||
|
|
||||||
|
use ruff_db::diagnostic::{
|
||||||
|
Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Severity, Span,
|
||||||
|
};
|
||||||
|
use ruff_db::{files::File, parsed::parsed_module, source::source_text};
|
||||||
|
use ruff_diagnostics::{Edit, Fix};
|
||||||
|
use ruff_python_parser::TokenKind;
|
||||||
|
use ruff_python_trivia::Cursor;
|
||||||
|
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||||
|
|
||||||
declare_lint! {
|
declare_lint! {
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for `type: ignore` or `ty: ignore` directives that are no longer applicable.
|
/// Checks for `type: ignore` or `ty: ignore` directives that are no longer applicable.
|
||||||
|
|
@ -109,7 +113,7 @@ pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions {
|
||||||
for comment in parser {
|
for comment in parser {
|
||||||
match comment {
|
match comment {
|
||||||
Ok(comment) => {
|
Ok(comment) => {
|
||||||
builder.add_comment(&comment, TextRange::new(line_start, token.end()));
|
builder.add_comment(comment, TextRange::new(line_start, token.end()));
|
||||||
}
|
}
|
||||||
Err(error) => match error.kind {
|
Err(error) => match error.kind {
|
||||||
ParseErrorKind::NotASuppression
|
ParseErrorKind::NotASuppression
|
||||||
|
|
@ -222,38 +226,132 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) {
|
||||||
unused.push(suppression);
|
unused.push(suppression);
|
||||||
}
|
}
|
||||||
|
|
||||||
let unused = unused.iter().filter(|suppression| {
|
let mut unused_iter = unused
|
||||||
|
.iter()
|
||||||
|
.filter(|suppression| {
|
||||||
// This looks silly but it's necessary to check again if a `unused-ignore-comment` is indeed unused
|
// This looks silly but it's necessary to check again if a `unused-ignore-comment` is indeed unused
|
||||||
// in case the "unused" directive comes after it:
|
// in case the "unused" directive comes after it:
|
||||||
// ```py
|
// ```py
|
||||||
// a = 10 / 2 # ty: ignore[unused-ignore-comment, division-by-zero]
|
// a = 10 / 2 # ty: ignore[unused-ignore-comment, division-by-zero]
|
||||||
// ```
|
// ```
|
||||||
!context.is_suppression_used(suppression.id())
|
!context.is_suppression_used(suppression.id())
|
||||||
});
|
})
|
||||||
|
.peekable();
|
||||||
|
|
||||||
for suppression in unused {
|
let source = source_text(context.db, context.file);
|
||||||
match suppression.target {
|
|
||||||
|
while let Some(suppression) = unused_iter.next() {
|
||||||
|
let mut diag = match suppression.target {
|
||||||
SuppressionTarget::All => {
|
SuppressionTarget::All => {
|
||||||
let Some(diag) =
|
let Some(diag) =
|
||||||
context.report_unchecked(&UNUSED_IGNORE_COMMENT, suppression.range)
|
context.report_unchecked(&UNUSED_IGNORE_COMMENT, suppression.range)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
diag.into_diagnostic(format_args!(
|
diag.into_diagnostic(format_args!(
|
||||||
"Unused blanket `{}` directive",
|
"Unused blanket `{}` directive",
|
||||||
suppression.kind
|
suppression.kind
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
SuppressionTarget::Lint(lint) => {
|
SuppressionTarget::Lint(lint) => {
|
||||||
|
// A single code in a `ty: ignore[<code1>, <code2>, ...]` directive
|
||||||
|
|
||||||
|
// Is this the first code directly after the `[`?
|
||||||
|
let includes_first_code = source[..suppression.range.start().to_usize()]
|
||||||
|
.trim_end()
|
||||||
|
.ends_with('[');
|
||||||
|
|
||||||
|
let mut current = suppression;
|
||||||
|
let mut unused_codes = Vec::new();
|
||||||
|
|
||||||
|
// Group successive codes together into a single diagnostic,
|
||||||
|
// or report the entire directive if all codes are unused.
|
||||||
|
while let Some(next) = unused_iter.peek() {
|
||||||
|
if let SuppressionTarget::Lint(next_lint) = next.target
|
||||||
|
&& next.comment_range == current.comment_range
|
||||||
|
&& source[TextRange::new(current.range.end(), next.range.start())]
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_whitespace() || c == ',')
|
||||||
|
{
|
||||||
|
unused_codes.push(next_lint);
|
||||||
|
current = *next;
|
||||||
|
unused_iter.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the last suppression code the last code before the closing `]`.
|
||||||
|
let includes_last_code = source[current.range.end().to_usize()..]
|
||||||
|
.trim_start()
|
||||||
|
.starts_with(']');
|
||||||
|
|
||||||
|
// If only some codes are unused
|
||||||
|
if !includes_first_code || !includes_last_code {
|
||||||
|
let mut codes = format!("'{}'", lint.name());
|
||||||
|
for code in &unused_codes {
|
||||||
|
let _ = write!(&mut codes, ", '{code}'", code = code.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(diag) = context.report_unchecked(
|
||||||
|
&UNUSED_IGNORE_COMMENT,
|
||||||
|
TextRange::new(suppression.range.start(), current.range.end()),
|
||||||
|
) {
|
||||||
|
let mut diag = diag.into_diagnostic(format_args!(
|
||||||
|
"Unused `{kind}` directive: {codes}",
|
||||||
|
kind = suppression.kind
|
||||||
|
));
|
||||||
|
|
||||||
|
diag.primary_annotation_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary);
|
||||||
|
|
||||||
|
// Delete everything up to the start of the next code.
|
||||||
|
let trailing_len: TextSize = source[current.range.end().to_usize()..]
|
||||||
|
.chars()
|
||||||
|
.take_while(|c: &char| c.is_whitespace() || *c == ',')
|
||||||
|
.map(TextLen::text_len)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
// If we delete the last codes before `]`, ensure we delete any trailing comma
|
||||||
|
let leading_len: TextSize = if includes_last_code {
|
||||||
|
source[..suppression.range.start().to_usize()]
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.take_while(|c: &char| c.is_whitespace() || *c == ',')
|
||||||
|
.map(TextLen::text_len)
|
||||||
|
.sum()
|
||||||
|
} else {
|
||||||
|
TextSize::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let fix_range = TextRange::new(
|
||||||
|
suppression.range.start() - leading_len,
|
||||||
|
current.range.end() + trailing_len,
|
||||||
|
);
|
||||||
|
diag.set_fix(Fix::safe_edit(Edit::range_deletion(fix_range)));
|
||||||
|
|
||||||
|
if unused_codes.is_empty() {
|
||||||
|
diag.help("Remove the unused suppression code");
|
||||||
|
} else {
|
||||||
|
diag.help("Remove the unused suppression codes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All codes are unused
|
||||||
let Some(diag) =
|
let Some(diag) =
|
||||||
context.report_unchecked(&UNUSED_IGNORE_COMMENT, suppression.range)
|
context.report_unchecked(&UNUSED_IGNORE_COMMENT, suppression.comment_range)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
diag.into_diagnostic(format_args!(
|
diag.into_diagnostic(format_args!(
|
||||||
"Unused `{kind}` directive: '{code}'",
|
"Unused `{kind}` directive",
|
||||||
kind = suppression.kind,
|
kind = suppression.kind
|
||||||
code = lint.name()
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
SuppressionTarget::Empty => {
|
SuppressionTarget::Empty => {
|
||||||
|
|
@ -268,6 +366,12 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
diag.primary_annotation_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary);
|
||||||
|
diag.set_fix(remove_comment_fix(suppression, &source));
|
||||||
|
diag.help("Remove the unused suppression comment");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -469,12 +573,22 @@ pub(crate) struct Suppression {
|
||||||
target: SuppressionTarget,
|
target: SuppressionTarget,
|
||||||
kind: SuppressionKind,
|
kind: SuppressionKind,
|
||||||
|
|
||||||
/// The range of this specific suppression.
|
/// The range of the code in this suppression.
|
||||||
/// This is the same as `comment_range` except for suppression comments that suppress multiple
|
///
|
||||||
/// codes. For those, the range is limited to the specific code.
|
/// This is the same as the `comment_range` for the
|
||||||
|
/// targets [`SuppressionTarget::All`] and [`SuppressionTarget::Empty`].
|
||||||
range: TextRange,
|
range: TextRange,
|
||||||
|
|
||||||
/// The range of the suppression comment.
|
/// The range of the suppression comment.
|
||||||
|
///
|
||||||
|
/// This isn't the range of the entire comment if this is a nested comment:
|
||||||
|
///
|
||||||
|
/// ```py
|
||||||
|
/// a # ty: ignore # fmt: off
|
||||||
|
/// ^^^^^^^^^^^^^
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// It doesn't include the range of the nested `# fmt: off` comment.
|
||||||
comment_range: TextRange,
|
comment_range: TextRange,
|
||||||
|
|
||||||
/// The range for which this suppression applies.
|
/// The range for which this suppression applies.
|
||||||
|
|
@ -572,7 +686,7 @@ impl<'a> SuppressionsBuilder<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_comment(&mut self, comment: &SuppressionComment, line_range: TextRange) {
|
fn add_comment(&mut self, comment: SuppressionComment, line_range: TextRange) {
|
||||||
// `type: ignore` comments at the start of the file apply to the entire range.
|
// `type: ignore` comments at the start of the file apply to the entire range.
|
||||||
// > A # type: ignore comment on a line by itself at the top of a file, before any docstrings,
|
// > A # type: ignore comment on a line by itself at the top of a file, before any docstrings,
|
||||||
// > imports, or other executable code, silences all errors in the file.
|
// > imports, or other executable code, silences all errors in the file.
|
||||||
|
|
@ -595,7 +709,7 @@ impl<'a> SuppressionsBuilder<'a> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match comment.codes.as_deref() {
|
match comment.codes {
|
||||||
// `type: ignore`
|
// `type: ignore`
|
||||||
None => {
|
None => {
|
||||||
push_type_ignore_suppression(Suppression {
|
push_type_ignore_suppression(Suppression {
|
||||||
|
|
@ -621,7 +735,7 @@ impl<'a> SuppressionsBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// `ty: ignore[]`
|
// `ty: ignore[]`
|
||||||
Some([]) => {
|
Some(codes) if codes.is_empty() => {
|
||||||
self.line.push(Suppression {
|
self.line.push(Suppression {
|
||||||
target: SuppressionTarget::Empty,
|
target: SuppressionTarget::Empty,
|
||||||
kind: comment.kind,
|
kind: comment.kind,
|
||||||
|
|
@ -634,25 +748,20 @@ impl<'a> SuppressionsBuilder<'a> {
|
||||||
// `ty: ignore[a, b]`
|
// `ty: ignore[a, b]`
|
||||||
Some(codes) => {
|
Some(codes) => {
|
||||||
for code_range in codes {
|
for code_range in codes {
|
||||||
let code = &self.source[*code_range];
|
let code = &self.source[code_range];
|
||||||
let range = if codes.len() == 1 {
|
|
||||||
comment.range
|
|
||||||
} else {
|
|
||||||
*code_range
|
|
||||||
};
|
|
||||||
|
|
||||||
match self.lint_registry.get(code) {
|
match self.lint_registry.get(code) {
|
||||||
Ok(lint) => {
|
Ok(lint) => {
|
||||||
self.line.push(Suppression {
|
self.line.push(Suppression {
|
||||||
target: SuppressionTarget::Lint(lint),
|
target: SuppressionTarget::Lint(lint),
|
||||||
kind: comment.kind,
|
kind: comment.kind,
|
||||||
range,
|
range: code_range,
|
||||||
comment_range: comment.range,
|
comment_range: comment.range,
|
||||||
suppressed_range,
|
suppressed_range,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(error) => self.unknown.push(UnknownSuppression {
|
Err(error) => self.unknown.push(UnknownSuppression {
|
||||||
range,
|
range: code_range,
|
||||||
comment_range: comment.range,
|
comment_range: comment.range,
|
||||||
reason: error,
|
reason: error,
|
||||||
}),
|
}),
|
||||||
|
|
@ -795,8 +904,6 @@ impl<'src> SuppressionParser<'src> {
|
||||||
self.eat_whitespace();
|
self.eat_whitespace();
|
||||||
|
|
||||||
if !self.cursor.eat_char(',') {
|
if !self.cursor.eat_char(',') {
|
||||||
self.eat_whitespace();
|
|
||||||
|
|
||||||
if self.cursor.eat_char(']') {
|
if self.cursor.eat_char(']') {
|
||||||
break Ok(Some(codes));
|
break Ok(Some(codes));
|
||||||
}
|
}
|
||||||
|
|
@ -963,6 +1070,37 @@ enum ParseErrorKind {
|
||||||
CodesMissingClosingBracket(SuppressionKind),
|
CodesMissingClosingBracket(SuppressionKind),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_comment_fix(suppression: &Suppression, source: &str) -> Fix {
|
||||||
|
let comment_end = suppression.comment_range.end();
|
||||||
|
let comment_start = suppression.comment_range.start();
|
||||||
|
let after_comment = &source[comment_end.to_usize()..];
|
||||||
|
|
||||||
|
if !after_comment.starts_with(['\n', '\r']) {
|
||||||
|
// For example: `# ty: ignore # fmt: off`
|
||||||
|
// Don't remove the trailing whitespace up to the `ty: ignore` comment
|
||||||
|
return Fix::safe_edit(Edit::range_deletion(suppression.comment_range));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any leading whitespace before the comment
|
||||||
|
// to avoid unnecessary trailing whitespace once the comment is removed
|
||||||
|
let before_comment = &source[..comment_start.to_usize()];
|
||||||
|
|
||||||
|
let mut leading_len = TextSize::default();
|
||||||
|
|
||||||
|
for c in before_comment.chars().rev() {
|
||||||
|
match c {
|
||||||
|
'\n' | '\r' => break,
|
||||||
|
c if c.is_whitespace() => leading_len += c.text_len(),
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Fix::safe_edit(Edit::range_deletion(TextRange::new(
|
||||||
|
comment_start - leading_len,
|
||||||
|
comment_end,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::suppression::{SuppressionComment, SuppressionParser};
|
use crate::suppression::{SuppressionComment, SuppressionParser};
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ license = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ruff_db = { workspace = true, features = ["os"] }
|
ruff_db = { workspace = true, features = ["os"] }
|
||||||
|
ruff_diagnostics = { workspace = true }
|
||||||
ruff_macros = { workspace = true }
|
ruff_macros = { workspace = true }
|
||||||
ruff_notebook = { workspace = true }
|
ruff_notebook = { workspace = true }
|
||||||
ruff_python_ast = { workspace = true }
|
ruff_python_ast = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
ClientCapabilities, CompletionOptions, DeclarationCapability, DiagnosticOptions,
|
ClientCapabilities, CodeActionKind, CodeActionOptions, CompletionOptions,
|
||||||
DiagnosticServerCapabilities, HoverProviderCapability, InlayHintOptions,
|
DeclarationCapability, DiagnosticOptions, DiagnosticServerCapabilities,
|
||||||
InlayHintServerCapabilities, MarkupKind, NotebookCellSelector, NotebookSelector, OneOf,
|
HoverProviderCapability, InlayHintOptions, InlayHintServerCapabilities, MarkupKind,
|
||||||
RenameOptions, SelectionRangeProviderCapability, SemanticTokensFullOptions,
|
NotebookCellSelector, NotebookSelector, OneOf, RenameOptions, SelectionRangeProviderCapability,
|
||||||
SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities,
|
SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions,
|
||||||
ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
|
SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelpOptions,
|
||||||
TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions,
|
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
|
||||||
|
TypeDefinitionProviderCapability, WorkDoneProgressOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::PositionEncoding;
|
use crate::PositionEncoding;
|
||||||
|
|
@ -376,6 +377,13 @@ pub(crate) fn server_capabilities(
|
||||||
|
|
||||||
ServerCapabilities {
|
ServerCapabilities {
|
||||||
position_encoding: Some(position_encoding.into()),
|
position_encoding: Some(position_encoding.into()),
|
||||||
|
code_action_provider: Some(types::CodeActionProviderCapability::Options(
|
||||||
|
CodeActionOptions {
|
||||||
|
code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
|
||||||
|
..CodeActionOptions::default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
|
||||||
execute_command_provider: Some(types::ExecuteCommandOptions {
|
execute_command_provider: Some(types::ExecuteCommandOptions {
|
||||||
commands: SupportedCommand::all()
|
commands: SupportedCommand::all()
|
||||||
.map(|command| command.identifier().to_string())
|
.map(|command| command.identifier().to_string())
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ pub(super) fn request(req: server::Request) -> Task {
|
||||||
|
|
||||||
match req.method.as_str() {
|
match req.method.as_str() {
|
||||||
requests::ExecuteCommand::METHOD => sync_request_task::<requests::ExecuteCommand>(req),
|
requests::ExecuteCommand::METHOD => sync_request_task::<requests::ExecuteCommand>(req),
|
||||||
|
requests::CodeActionRequestHandler::METHOD => background_document_request_task::<
|
||||||
|
requests::CodeActionRequestHandler,
|
||||||
|
>(req, BackgroundSchedule::Worker),
|
||||||
requests::DocumentDiagnosticRequestHandler::METHOD => background_document_request_task::<
|
requests::DocumentDiagnosticRequestHandler::METHOD => background_document_request_task::<
|
||||||
requests::DocumentDiagnosticRequestHandler,
|
requests::DocumentDiagnosticRequestHandler,
|
||||||
>(
|
>(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::hash::{DefaultHasher, Hash as _, Hasher as _};
|
use std::hash::{DefaultHasher, Hash as _, Hasher as _};
|
||||||
|
|
||||||
use lsp_types::notification::PublishDiagnostics;
|
use lsp_types::notification::PublishDiagnostics;
|
||||||
|
|
@ -5,18 +6,21 @@ use lsp_types::{
|
||||||
CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
|
CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
|
||||||
NumberOrString, PublishDiagnosticsParams, Url,
|
NumberOrString, PublishDiagnosticsParams, Url,
|
||||||
};
|
};
|
||||||
|
use ruff_diagnostics::Applicability;
|
||||||
|
use ruff_text_size::Ranged;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic};
|
use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic};
|
||||||
use ruff_db::files::{File, FileRange};
|
use ruff_db::files::{File, FileRange};
|
||||||
use ruff_db::system::SystemPathBuf;
|
use ruff_db::system::SystemPathBuf;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use ty_project::{Db as _, ProjectDatabase};
|
use ty_project::{Db as _, ProjectDatabase};
|
||||||
|
|
||||||
use crate::Db;
|
|
||||||
use crate::document::{FileRangeExt, ToRangeExt};
|
use crate::document::{FileRangeExt, ToRangeExt};
|
||||||
use crate::session::DocumentHandle;
|
use crate::session::DocumentHandle;
|
||||||
use crate::session::client::Client;
|
use crate::session::client::Client;
|
||||||
use crate::system::{AnySystemPath, file_to_url};
|
use crate::system::{AnySystemPath, file_to_url};
|
||||||
|
use crate::{DIAGNOSTIC_NAME, Db};
|
||||||
use crate::{PositionEncoding, Session};
|
use crate::{PositionEncoding, Session};
|
||||||
|
|
||||||
pub(super) struct Diagnostics {
|
pub(super) struct Diagnostics {
|
||||||
|
|
@ -351,6 +355,8 @@ pub(super) fn to_lsp_diagnostic(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let data = DiagnosticData::try_from_diagnostic(db, diagnostic, encoding);
|
||||||
|
|
||||||
(
|
(
|
||||||
url,
|
url,
|
||||||
Diagnostic {
|
Diagnostic {
|
||||||
|
|
@ -359,10 +365,10 @@ pub(super) fn to_lsp_diagnostic(
|
||||||
tags,
|
tags,
|
||||||
code: Some(NumberOrString::String(diagnostic.id().to_string())),
|
code: Some(NumberOrString::String(diagnostic.id().to_string())),
|
||||||
code_description,
|
code_description,
|
||||||
source: Some("ty".into()),
|
source: Some(DIAGNOSTIC_NAME.into()),
|
||||||
message: diagnostic.concise_message().to_string(),
|
message: diagnostic.concise_message().to_string(),
|
||||||
related_information: Some(related_information),
|
related_information: Some(related_information),
|
||||||
data: None,
|
data: serde_json::to_value(data).ok(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -402,3 +408,49 @@ fn sub_diagnostic_to_related_information(
|
||||||
message: diagnostic.concise_message().to_string(),
|
message: diagnostic.concise_message().to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub(crate) struct DiagnosticData {
|
||||||
|
pub(crate) fix_title: String,
|
||||||
|
pub(crate) edits: HashMap<Url, Vec<lsp_types::TextEdit>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiagnosticData {
|
||||||
|
fn try_from_diagnostic(
|
||||||
|
db: &dyn Db,
|
||||||
|
diagnostic: &ruff_db::diagnostic::Diagnostic,
|
||||||
|
encoding: PositionEncoding,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let fix = diagnostic
|
||||||
|
.fix()
|
||||||
|
.filter(|fix| fix.applies(Applicability::Unsafe))?;
|
||||||
|
|
||||||
|
let primary_span = diagnostic.primary_span()?;
|
||||||
|
let file = primary_span.expect_ty_file();
|
||||||
|
|
||||||
|
let mut lsp_edits: HashMap<Url, Vec<lsp_types::TextEdit>> = HashMap::new();
|
||||||
|
|
||||||
|
for edit in fix.edits() {
|
||||||
|
let location = edit
|
||||||
|
.range()
|
||||||
|
.to_lsp_range(db, file, encoding)?
|
||||||
|
.to_location()?;
|
||||||
|
|
||||||
|
lsp_edits
|
||||||
|
.entry(location.uri)
|
||||||
|
.or_default()
|
||||||
|
.push(lsp_types::TextEdit {
|
||||||
|
range: location.range,
|
||||||
|
new_text: edit.content().unwrap_or_default().to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
fix_title: diagnostic
|
||||||
|
.first_help_text()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.unwrap_or_else(|| format!("Fix {}", diagnostic.id())),
|
||||||
|
edits: lsp_edits,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod code_action;
|
||||||
mod completion;
|
mod completion;
|
||||||
mod diagnostic;
|
mod diagnostic;
|
||||||
mod doc_highlights;
|
mod doc_highlights;
|
||||||
|
|
@ -19,6 +20,7 @@ mod signature_help;
|
||||||
mod workspace_diagnostic;
|
mod workspace_diagnostic;
|
||||||
mod workspace_symbols;
|
mod workspace_symbols;
|
||||||
|
|
||||||
|
pub(super) use code_action::CodeActionRequestHandler;
|
||||||
pub(super) use completion::CompletionRequestHandler;
|
pub(super) use completion::CompletionRequestHandler;
|
||||||
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
|
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
|
||||||
pub(super) use doc_highlights::DocumentHighlightRequestHandler;
|
pub(super) use doc_highlights::DocumentHighlightRequestHandler;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use lsp_types::{self as types, Url, request as req};
|
||||||
|
use ty_project::ProjectDatabase;
|
||||||
|
use types::{CodeActionKind, CodeActionOrCommand};
|
||||||
|
|
||||||
|
use crate::DIAGNOSTIC_NAME;
|
||||||
|
use crate::server::Result;
|
||||||
|
use crate::server::api::RequestHandler;
|
||||||
|
use crate::server::api::diagnostics::DiagnosticData;
|
||||||
|
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RetriableRequestHandler};
|
||||||
|
use crate::session::DocumentSnapshot;
|
||||||
|
use crate::session::client::Client;
|
||||||
|
|
||||||
|
pub(crate) struct CodeActionRequestHandler;
|
||||||
|
|
||||||
|
impl RequestHandler for CodeActionRequestHandler {
|
||||||
|
type RequestType = req::CodeActionRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundDocumentRequestHandler for CodeActionRequestHandler {
|
||||||
|
fn document_url(params: &types::CodeActionParams) -> Cow<'_, Url> {
|
||||||
|
Cow::Borrowed(¶ms.text_document.uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_with_snapshot(
|
||||||
|
_db: &ProjectDatabase,
|
||||||
|
_snapshot: &DocumentSnapshot,
|
||||||
|
_client: &Client,
|
||||||
|
params: types::CodeActionParams,
|
||||||
|
) -> Result<Option<types::CodeActionResponse>> {
|
||||||
|
let diagnostics = params.context.diagnostics;
|
||||||
|
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
|
||||||
|
for mut diagnostic in diagnostics.into_iter().filter(|diagnostic| {
|
||||||
|
diagnostic.source.as_deref() == Some(DIAGNOSTIC_NAME)
|
||||||
|
&& range_intersect(&diagnostic.range, ¶ms.range)
|
||||||
|
}) {
|
||||||
|
let Some(data) = diagnostic.data.take() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let data: DiagnosticData = match serde_json::from_value(data) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("Failed to deserialize diagnostic data: {err}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
actions.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
|
||||||
|
title: data.fix_title,
|
||||||
|
kind: Some(CodeActionKind::QUICKFIX),
|
||||||
|
diagnostics: Some(vec![diagnostic]),
|
||||||
|
edit: Some(lsp_types::WorkspaceEdit {
|
||||||
|
changes: Some(data.edits),
|
||||||
|
document_changes: None,
|
||||||
|
change_annotations: None,
|
||||||
|
}),
|
||||||
|
is_preferred: Some(true),
|
||||||
|
command: None,
|
||||||
|
disabled: None,
|
||||||
|
data: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if actions.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(actions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_intersect(range: &lsp_types::Range, other: &lsp_types::Range) -> bool {
|
||||||
|
let start = range.start.max(other.start);
|
||||||
|
let end = range.end.min(other.end);
|
||||||
|
end >= start
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetriableRequestHandler for CodeActionRequestHandler {}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use lsp_types::{Position, Range, request::CodeActionRequest};
|
||||||
|
use ruff_db::system::SystemPath;
|
||||||
|
|
||||||
|
use crate::TestServerBuilder;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn code_action() -> Result<()> {
|
||||||
|
let workspace_root = SystemPath::new("src");
|
||||||
|
let foo = SystemPath::new("src/foo.py");
|
||||||
|
let foo_content = "\
|
||||||
|
x = 20 / 2 # ty: ignore[division-by-zero]
|
||||||
|
";
|
||||||
|
|
||||||
|
let ty_toml = SystemPath::new("ty.toml");
|
||||||
|
let ty_toml_content = "\
|
||||||
|
[rules]
|
||||||
|
unused-ignore-comment = \"warn\"
|
||||||
|
";
|
||||||
|
|
||||||
|
let mut server = TestServerBuilder::new()?
|
||||||
|
.with_workspace(workspace_root, None)?
|
||||||
|
.with_file(ty_toml, ty_toml_content)?
|
||||||
|
.with_file(foo, foo_content)?
|
||||||
|
.enable_pull_diagnostics(true)
|
||||||
|
.build()
|
||||||
|
.wait_until_workspaces_are_initialized();
|
||||||
|
|
||||||
|
server.open_text_document(foo, &foo_content, 1);
|
||||||
|
|
||||||
|
// Wait for diagnostics to be computed.
|
||||||
|
let diagnostics = server.document_diagnostic_request(foo, None);
|
||||||
|
|
||||||
|
// Get code actions for the line with the unused ignore comment.
|
||||||
|
let code_action_params = lsp_types::CodeActionParams {
|
||||||
|
text_document: lsp_types::TextDocumentIdentifier {
|
||||||
|
uri: server.file_uri(foo),
|
||||||
|
},
|
||||||
|
range: Range::new(Position::new(0, 0), Position::new(0, 43)),
|
||||||
|
context: lsp_types::CodeActionContext {
|
||||||
|
diagnostics: match diagnostics {
|
||||||
|
lsp_types::DocumentDiagnosticReportResult::Report(
|
||||||
|
lsp_types::DocumentDiagnosticReport::Full(report),
|
||||||
|
) => report.full_document_diagnostic_report.items,
|
||||||
|
_ => panic!("Expected full diagnostic report"),
|
||||||
|
},
|
||||||
|
only: None,
|
||||||
|
trigger_kind: None,
|
||||||
|
},
|
||||||
|
work_done_progress_params: lsp_types::WorkDoneProgressParams::default(),
|
||||||
|
partial_result_params: lsp_types::PartialResultParams::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let code_action_id = server.send_request::<CodeActionRequest>(code_action_params);
|
||||||
|
let code_actions = server.await_response::<CodeActionRequest>(&code_action_id);
|
||||||
|
|
||||||
|
insta::assert_json_snapshot!(code_actions);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_code_action_for_non_overlapping_range_on_same_line() -> Result<()> {
|
||||||
|
let workspace_root = SystemPath::new("src");
|
||||||
|
let foo = SystemPath::new("src/foo.py");
|
||||||
|
let foo_content = "\
|
||||||
|
x = 20 / 2 # ty: ignore[division-by-zero]
|
||||||
|
";
|
||||||
|
|
||||||
|
let ty_toml = SystemPath::new("ty.toml");
|
||||||
|
let ty_toml_content = "\
|
||||||
|
[rules]
|
||||||
|
unused-ignore-comment = \"warn\"
|
||||||
|
";
|
||||||
|
|
||||||
|
let mut server = TestServerBuilder::new()?
|
||||||
|
.with_workspace(workspace_root, None)?
|
||||||
|
.with_file(ty_toml, ty_toml_content)?
|
||||||
|
.with_file(foo, foo_content)?
|
||||||
|
.enable_pull_diagnostics(true)
|
||||||
|
.build()
|
||||||
|
.wait_until_workspaces_are_initialized();
|
||||||
|
|
||||||
|
server.open_text_document(foo, &foo_content, 1);
|
||||||
|
|
||||||
|
// Wait for diagnostics to be computed.
|
||||||
|
let diagnostics = server.document_diagnostic_request(foo, None);
|
||||||
|
|
||||||
|
// Get code actions for a range that doesn't overlap with the diagnostic.
|
||||||
|
// The diagnostic is at characters 12-42, so we request actions for characters 0-10.
|
||||||
|
let code_action_params = lsp_types::CodeActionParams {
|
||||||
|
text_document: lsp_types::TextDocumentIdentifier {
|
||||||
|
uri: server.file_uri(foo),
|
||||||
|
},
|
||||||
|
range: Range::new(Position::new(0, 0), Position::new(0, 10)),
|
||||||
|
context: lsp_types::CodeActionContext {
|
||||||
|
diagnostics: match diagnostics {
|
||||||
|
lsp_types::DocumentDiagnosticReportResult::Report(
|
||||||
|
lsp_types::DocumentDiagnosticReport::Full(report),
|
||||||
|
) => report.full_document_diagnostic_report.items,
|
||||||
|
_ => panic!("Expected full diagnostic report"),
|
||||||
|
},
|
||||||
|
only: None,
|
||||||
|
trigger_kind: None,
|
||||||
|
},
|
||||||
|
work_done_progress_params: lsp_types::WorkDoneProgressParams::default(),
|
||||||
|
partial_result_params: lsp_types::PartialResultParams::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let code_action_id = server.send_request::<CodeActionRequest>(code_action_params);
|
||||||
|
let code_actions = server.await_response::<CodeActionRequest>(&code_action_id);
|
||||||
|
|
||||||
|
// Should return None because the range doesn't overlap with the diagnostic.
|
||||||
|
assert_eq!(code_actions, None);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
//! [`await_request`]: TestServer::await_request
|
//! [`await_request`]: TestServer::await_request
|
||||||
//! [`await_notification`]: TestServer::await_notification
|
//! [`await_notification`]: TestServer::await_notification
|
||||||
|
|
||||||
|
mod code_actions;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod initialize;
|
mod initialize;
|
||||||
mod inlay_hints;
|
mod inlay_hints;
|
||||||
|
|
@ -740,7 +741,7 @@ impl TestServer {
|
||||||
self.initialize_response.as_ref()
|
self.initialize_response.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_uri(&self, path: impl AsRef<SystemPath>) -> Url {
|
pub(crate) fn file_uri(&self, path: impl AsRef<SystemPath>) -> Url {
|
||||||
Url::from_file_path(self.test_context.root().join(path.as_ref()).as_std_path())
|
Url::from_file_path(self.test_context.root().join(path.as_ref()).as_std_path())
|
||||||
.expect("Path must be a valid URL")
|
.expect("Path must be a valid URL")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
source: crates/ty_server/tests/e2e/code_actions.rs
|
||||||
|
expression: code_actions
|
||||||
|
---
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "Remove the unused suppression comment",
|
||||||
|
"kind": "quickfix",
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 12
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 42
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"severity": 2,
|
||||||
|
"code": "unused-ignore-comment",
|
||||||
|
"codeDescription": {
|
||||||
|
"href": "https://ty.dev/rules#unused-ignore-comment"
|
||||||
|
},
|
||||||
|
"source": "ty",
|
||||||
|
"message": "Unused `ty: ignore` directive",
|
||||||
|
"relatedInformation": [],
|
||||||
|
"tags": [
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edit": {
|
||||||
|
"changes": {
|
||||||
|
"file://<temp_dir>/src/foo.py": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 10
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 42
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newText": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isPreferred": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -43,6 +43,11 @@ expression: initialization_result
|
||||||
"documentHighlightProvider": true,
|
"documentHighlightProvider": true,
|
||||||
"documentSymbolProvider": true,
|
"documentSymbolProvider": true,
|
||||||
"workspaceSymbolProvider": true,
|
"workspaceSymbolProvider": true,
|
||||||
|
"codeActionProvider": {
|
||||||
|
"codeActionKinds": [
|
||||||
|
"quickfix"
|
||||||
|
]
|
||||||
|
},
|
||||||
"declarationProvider": true,
|
"declarationProvider": true,
|
||||||
"executeCommandProvider": {
|
"executeCommandProvider": {
|
||||||
"commands": [
|
"commands": [
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ expression: initialization_result
|
||||||
"documentHighlightProvider": true,
|
"documentHighlightProvider": true,
|
||||||
"documentSymbolProvider": true,
|
"documentSymbolProvider": true,
|
||||||
"workspaceSymbolProvider": true,
|
"workspaceSymbolProvider": true,
|
||||||
|
"codeActionProvider": {
|
||||||
|
"codeActionKinds": [
|
||||||
|
"quickfix"
|
||||||
|
]
|
||||||
|
},
|
||||||
"declarationProvider": true,
|
"declarationProvider": true,
|
||||||
"executeCommandProvider": {
|
"executeCommandProvider": {
|
||||||
"commands": [
|
"commands": [
|
||||||
|
|
|
||||||
|
|
@ -641,7 +641,9 @@ fn create_diagnostic_snapshot(
|
||||||
test: &parser::MarkdownTest,
|
test: &parser::MarkdownTest,
|
||||||
diagnostics: impl IntoIterator<Item = Diagnostic>,
|
diagnostics: impl IntoIterator<Item = Diagnostic>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let display_config = DisplayDiagnosticConfig::default().color(false);
|
let display_config = DisplayDiagnosticConfig::default()
|
||||||
|
.color(false)
|
||||||
|
.show_fix_diff(true);
|
||||||
|
|
||||||
let mut snapshot = String::new();
|
let mut snapshot = String::new();
|
||||||
writeln!(snapshot).unwrap();
|
writeln!(snapshot).unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue