Compare commits
29 Commits
dhruv/synt
...
charlie/li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dac0858f2 | ||
|
|
ebdaf5765a | ||
|
|
9a93409e1c | ||
|
|
102b9d930f | ||
|
|
550aa871d3 | ||
|
|
3c22a3bdcc | ||
|
|
6263923915 | ||
|
|
94abea4b08 | ||
|
|
519a65007f | ||
|
|
573facd2ba | ||
|
|
3cb2e677aa | ||
|
|
f0046ab28e | ||
|
|
5bb9720a10 | ||
|
|
9ff18bf9d3 | ||
|
|
aa906b9c75 | ||
|
|
3476e2f359 | ||
|
|
8848eca3c6 | ||
|
|
b0731ef9cb | ||
|
|
84531d1644 | ||
|
|
83b8b62e3e | ||
|
|
7225732859 | ||
|
|
403f0dccd8 | ||
|
|
46fcd19ca6 | ||
|
|
d9ec3d56b0 | ||
|
|
cd87b787d9 | ||
|
|
dd6d411026 | ||
|
|
cfceb437a8 | ||
|
|
48b0660228 | ||
|
|
24899efe50 |
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@@ -167,6 +167,9 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
|
||||
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
|
||||
run: |
|
||||
cargo nextest run --all-features --profile ci
|
||||
cargo test --all-features --doc
|
||||
|
||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,5 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## 0.4.5
|
||||
|
||||
### Ruff's language server is now in Beta
|
||||
|
||||
`v0.4.5` marks the official Beta release of `ruff server`, an integrated language server built into Ruff.
|
||||
`ruff server` supports the same feature set as `ruff-lsp`, powering linting, formatting, and
|
||||
code fixes in Ruff's editor integrations -- but with superior performance and
|
||||
no installation required. We'd love your feedback!
|
||||
|
||||
You can enable `ruff server` in the [VS Code extension](https://github.com/astral-sh/ruff-vscode?tab=readme-ov-file#enabling-the-rust-based-language-server) today.
|
||||
|
||||
To read more about this exciting milestone, check out our [blog post](https://astral.sh/blog/ruff-v0.4.5)!
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-future-annotations`\] Reword `future-rewritable-type-annotation` (`FA100`) message ([#11381](https://github.com/astral-sh/ruff/pull/11381))
|
||||
- \[`pycodestyle`\] Consider soft keywords for `E27` rules ([#11446](https://github.com/astral-sh/ruff/pull/11446))
|
||||
- \[`pyflakes`\] Recommend adding unused import bindings to `__all__` ([#11314](https://github.com/astral-sh/ruff/pull/11314))
|
||||
- \[`pyflakes`\] Update documentation and deprecate `ignore_init_module_imports` ([#11436](https://github.com/astral-sh/ruff/pull/11436))
|
||||
- \[`pyupgrade`\] Mark quotes as unnecessary for non-evaluated annotations ([#11485](https://github.com/astral-sh/ruff/pull/11485))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Avoid multiline quotes warning with `quote-style = preserve` ([#11490](https://github.com/astral-sh/ruff/pull/11490))
|
||||
|
||||
### Server
|
||||
|
||||
- Support Jupyter Notebook files ([#11206](https://github.com/astral-sh/ruff/pull/11206))
|
||||
- Support `noqa` comment code actions ([#11276](https://github.com/astral-sh/ruff/pull/11276))
|
||||
- Fix automatic configuration reloading ([#11492](https://github.com/astral-sh/ruff/pull/11492))
|
||||
- Fix several issues with configuration in Neovim and Helix ([#11497](https://github.com/astral-sh/ruff/pull/11497))
|
||||
|
||||
### CLI
|
||||
|
||||
- Add `--output-format` as a CLI option for `ruff config` ([#11438](https://github.com/astral-sh/ruff/pull/11438))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid `PLE0237` for property with setter ([#11377](https://github.com/astral-sh/ruff/pull/11377))
|
||||
- Avoid `TCH005` for `if` stmt with `elif`/`else` block ([#11376](https://github.com/astral-sh/ruff/pull/11376))
|
||||
- Avoid flagging `__future__` annotations as required for non-evaluated type annotations ([#11414](https://github.com/astral-sh/ruff/pull/11414))
|
||||
- Check for ruff executable in 'bin' directory as installed by 'pip install --target'. ([#11450](https://github.com/astral-sh/ruff/pull/11450))
|
||||
- Sort edits prior to deduplicating in quotation fix ([#11452](https://github.com/astral-sh/ruff/pull/11452))
|
||||
- Treat escaped newline as valid sequence ([#11465](https://github.com/astral-sh/ruff/pull/11465))
|
||||
- \[`flake8-pie`\] Preserve parentheses in `unnecessary-dict-kwargs` ([#11372](https://github.com/astral-sh/ruff/pull/11372))
|
||||
- \[`pylint`\] Ignore `__slots__` with dynamic values ([#11488](https://github.com/astral-sh/ruff/pull/11488))
|
||||
- \[`pylint`\] Remove `try` body from branch counting ([#11487](https://github.com/astral-sh/ruff/pull/11487))
|
||||
- \[`refurb`\] Respect operator precedence in `FURB110` ([#11464](https://github.com/astral-sh/ruff/pull/11464))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add `--preview` to the README ([#11395](https://github.com/astral-sh/ruff/pull/11395))
|
||||
- Add Python 3.13 to list of allowed Python versions ([#11411](https://github.com/astral-sh/ruff/pull/11411))
|
||||
- Simplify Neovim setup documentation ([#11489](https://github.com/astral-sh/ruff/pull/11489))
|
||||
- Update CONTRIBUTING.md to reflect the new parser ([#11434](https://github.com/astral-sh/ruff/pull/11434))
|
||||
- Update server documentation with new migration guide ([#11499](https://github.com/astral-sh/ruff/pull/11499))
|
||||
- \[`pycodestyle`\] Clarify motivation for `E713` and `E714` ([#11483](https://github.com/astral-sh/ruff/pull/11483))
|
||||
- \[`pyflakes`\] Update docs to describe WAI behavior (F541) ([#11362](https://github.com/astral-sh/ruff/pull/11362))
|
||||
- \[`pylint`\] Clearly indicate what is counted as a branch ([#11423](https://github.com/astral-sh/ruff/pull/11423))
|
||||
|
||||
## 0.4.4
|
||||
|
||||
### Preview features
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -1300,8 +1300,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.95.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365"
|
||||
source = "git+https://github.com/astral-sh/lsp-types.git?rev=3512a9f#3512a9f33eadc5402cfab1b8f7340824c8ca1439"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"serde",
|
||||
@@ -1940,7 +1939,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2101,7 +2100,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2377,6 +2376,7 @@ dependencies = [
|
||||
"ruff_diagnostics",
|
||||
"ruff_formatter",
|
||||
"ruff_linter",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_codegen",
|
||||
"ruff_python_formatter",
|
||||
|
||||
@@ -81,7 +81,7 @@ libc = { version = "0.2.153" }
|
||||
libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
lsp-server = { version = "0.7.6" }
|
||||
lsp-types = { version = "0.95.0", features = ["proposed"] }
|
||||
lsp-types = { git="https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = ["proposed"] }
|
||||
matchit = { version = "0.8.1" }
|
||||
memchr = { version = "2.7.1" }
|
||||
mimalloc = { version = "0.1.39" }
|
||||
|
||||
@@ -152,7 +152,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.4.4
|
||||
rev: v0.4.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -433,6 +433,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- Modern Treasury ([Python SDK](https://github.com/Modern-Treasury/modern-treasury-python))
|
||||
- Mozilla ([Firefox](https://github.com/mozilla/gecko-dev))
|
||||
- [Mypy](https://github.com/python/mypy)
|
||||
- [Nautobot](https://github.com/nautobot/nautobot)
|
||||
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
|
||||
- [Neon](https://github.com/neondatabase/neon)
|
||||
- [Nokia](https://nokia.com/)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -857,12 +857,20 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
|
||||
|
||||
if setting.linter.rules.enabled(Rule::BadQuotesMultilineString)
|
||||
&& setting.linter.flake8_quotes.multiline_quotes == Quote::Single
|
||||
&& matches!(
|
||||
setting.formatter.quote_style,
|
||||
QuoteStyle::Single | QuoteStyle::Double
|
||||
)
|
||||
{
|
||||
warn_user_once!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `\"double\"`.`");
|
||||
}
|
||||
|
||||
if setting.linter.rules.enabled(Rule::BadQuotesDocstring)
|
||||
&& setting.linter.flake8_quotes.docstring_quotes == Quote::Single
|
||||
&& matches!(
|
||||
setting.formatter.quote_style,
|
||||
QuoteStyle::Single | QuoteStyle::Double
|
||||
)
|
||||
{
|
||||
warn_user_once!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`");
|
||||
}
|
||||
|
||||
@@ -1038,6 +1038,48 @@ def say_hy(name: str):
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_linter_options_preserve() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint]
|
||||
select = ["Q"]
|
||||
|
||||
[lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
docstring-quotes = "single"
|
||||
multiline-quotes = "single"
|
||||
|
||||
[format]
|
||||
quote-style = "preserve"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let test_path = tempdir.path().join("test.py");
|
||||
fs::write(
|
||||
&test_path,
|
||||
r#"
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--no-cache", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg(test_path), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_rules_default_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
@@ -1414,7 +1414,7 @@ fn check_input_from_argfile() -> Result<()> {
|
||||
fs::write(&file_a_path, b"import os")?;
|
||||
fs::write(&file_b_path, b"print('hello, world!')")?;
|
||||
|
||||
// Create a the input file for argfile to expand
|
||||
// Create the input file for argfile to expand
|
||||
let input_file_path = tempdir.path().join("file_paths.txt");
|
||||
fs::write(
|
||||
&input_file_path,
|
||||
|
||||
@@ -34,12 +34,29 @@ marking it as unused, as in:
|
||||
from module import member as member
|
||||
```
|
||||
|
||||
Alternatively, you can use `__all__` to declare a symbol as part of the module's
|
||||
interface, as in:
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
import some_module
|
||||
|
||||
__all__ = [ "some_module"]
|
||||
```
|
||||
|
||||
## Fix safety
|
||||
|
||||
When `ignore_init_module_imports` is disabled, fixes can remove for unused imports in `__init__` files.
|
||||
These fixes are considered unsafe because they can change the public interface.
|
||||
Fixes to remove unused imports are safe, except in `__init__.py` files.
|
||||
|
||||
Applying fixes to `__init__.py` files is currently in preview. The fix offered depends on the
|
||||
type of the unused import. Ruff will suggest a safe fix to export first-party imports with
|
||||
either a redundant alias or, if already present in the file, an `__all__` entry. If multiple
|
||||
`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix
|
||||
to remove third-party and standard library imports -- the fix is unsafe because the module's
|
||||
interface changes.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
import numpy as np # unused import
|
||||
|
||||
@@ -49,12 +66,14 @@ def area(radius):
|
||||
```
|
||||
|
||||
Use instead:
|
||||
|
||||
```python
|
||||
def area(radius):
|
||||
return 3.14 * radius**2
|
||||
```
|
||||
|
||||
To check the availability of a module, use `importlib.util.find_spec`:
|
||||
|
||||
```python
|
||||
from importlib.util import find_spec
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
57
crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC116.py
vendored
Normal file
57
crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC116.py
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# type: ignore
|
||||
# ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
import math
|
||||
from math import inf
|
||||
|
||||
|
||||
async def import_trio():
|
||||
import trio
|
||||
|
||||
# These examples are probably not meant to ever wake up:
|
||||
await trio.sleep(100000) # error: 116, "async"
|
||||
|
||||
# 'inf literal' overflow trick
|
||||
await trio.sleep(1e999) # error: 116, "async"
|
||||
|
||||
await trio.sleep(86399)
|
||||
await trio.sleep(86400)
|
||||
await trio.sleep(86400.01) # error: 116, "async"
|
||||
await trio.sleep(86401) # error: 116, "async"
|
||||
|
||||
await trio.sleep(-1) # will raise a runtime error
|
||||
await trio.sleep(0) # handled by different check
|
||||
|
||||
# these ones _definitely_ never wake up (TODO)
|
||||
await trio.sleep(float("inf"))
|
||||
await trio.sleep(math.inf)
|
||||
await trio.sleep(inf)
|
||||
|
||||
# don't require inf to be in math (TODO)
|
||||
await trio.sleep(np.inf)
|
||||
|
||||
# don't evaluate expressions (TODO)
|
||||
one_day = 86401
|
||||
await trio.sleep(86400 + 1)
|
||||
await trio.sleep(60 * 60 * 24 + 1)
|
||||
await trio.sleep(foo())
|
||||
await trio.sleep(one_day)
|
||||
await trio.sleep(86400 + foo())
|
||||
await trio.sleep(86400 + ...)
|
||||
await trio.sleep("hello")
|
||||
await trio.sleep(...)
|
||||
|
||||
|
||||
def not_async_fun():
|
||||
import trio
|
||||
|
||||
# does not require the call to be awaited, nor in an async fun
|
||||
trio.sleep(86401) # error: 116, "async"
|
||||
# also checks that we don't break visit_Call
|
||||
trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
|
||||
|
||||
async def import_from_trio():
|
||||
from trio import sleep
|
||||
|
||||
# catch from import
|
||||
await sleep(86401) # error: 116, "async"
|
||||
7
crates/ruff_linter/resources/test/fixtures/flake8_future_annotations/ok_quoted_type.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_future_annotations/ok_quoted_type.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
def main() -> None:
|
||||
a_list: list[str] | None = []
|
||||
a_list.append("hello")
|
||||
|
||||
|
||||
def hello(y: "dict[str, int] | None") -> None:
|
||||
del y
|
||||
@@ -63,3 +63,16 @@ if (a and
|
||||
#: Okay
|
||||
def f():
|
||||
return 1
|
||||
|
||||
# Soft keywords
|
||||
|
||||
#: E271
|
||||
type Number = int
|
||||
|
||||
#: E273
|
||||
type Number = int
|
||||
|
||||
#: E275
|
||||
match(foo):
|
||||
case(1):
|
||||
pass
|
||||
|
||||
@@ -46,3 +46,15 @@ regex = '\\\_'
|
||||
|
||||
#: W605:1:7
|
||||
u'foo\ bar'
|
||||
|
||||
#: W605:1:13
|
||||
(
|
||||
"foo \
|
||||
bar \. baz"
|
||||
)
|
||||
|
||||
#: W605:1:6
|
||||
"foo \. bar \t"
|
||||
|
||||
#: W605:1:13
|
||||
"foo \t bar \."
|
||||
|
||||
@@ -82,3 +82,16 @@ class Foo:
|
||||
@qux.setter
|
||||
def qux(self, value):
|
||||
self.bar = value / 2
|
||||
|
||||
|
||||
class StudentG:
|
||||
names = ("surname",)
|
||||
__slots__ = (*names, "a")
|
||||
|
||||
def __init__(self, name, surname):
|
||||
self.name = name
|
||||
self.surname = surname # [assigning-non-slot]
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
@@ -21,6 +21,8 @@ def wrong(): # [too-many-branches]
|
||||
pass
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
pass
|
||||
if 2:
|
||||
@@ -56,6 +58,8 @@ def good():
|
||||
pass
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
pass
|
||||
if 1:
|
||||
@@ -90,6 +94,8 @@ def with_statement_wrong():
|
||||
pass
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
pass
|
||||
if 2:
|
||||
|
||||
14
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_1.py
vendored
Normal file
14
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_1.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def foo():
|
||||
# UP037
|
||||
x: "Tuple[int, int]" = (0, 0)
|
||||
print(x)
|
||||
|
||||
|
||||
# OK
|
||||
X: "Tuple[int, int]" = (0, 0)
|
||||
@@ -38,3 +38,12 @@ z = (
|
||||
else
|
||||
y
|
||||
)
|
||||
|
||||
# FURB110
|
||||
z = (
|
||||
x
|
||||
if x
|
||||
else y
|
||||
if y > 0
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -62,6 +62,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if !checker.semantic.future_annotations_or_stub()
|
||||
&& checker.settings.target_version < PythonVersion::Py39
|
||||
&& checker.semantic.in_annotation()
|
||||
&& checker.semantic.in_runtime_evaluated_annotation()
|
||||
&& !checker.semantic.in_string_type_definition()
|
||||
&& typing::is_pep585_generic(value, &checker.semantic)
|
||||
{
|
||||
flake8_future_annotations::rules::future_required_type_annotation(
|
||||
@@ -506,6 +508,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::BlockingOsCallInAsyncFunction) {
|
||||
flake8_async::rules::blocking_os_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::SleepForeverCall) {
|
||||
flake8_async::rules::sleep_forever_call(checker, call);
|
||||
}
|
||||
if checker.any_enabled(&[Rule::Print, Rule::PPrint]) {
|
||||
flake8_print::rules::print_call(checker, call);
|
||||
}
|
||||
@@ -1195,6 +1200,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if !checker.semantic.future_annotations_or_stub()
|
||||
&& checker.settings.target_version < PythonVersion::Py310
|
||||
&& checker.semantic.in_annotation()
|
||||
&& checker.semantic.in_runtime_evaluated_annotation()
|
||||
&& !checker.semantic.in_string_type_definition()
|
||||
{
|
||||
flake8_future_annotations::rules::future_required_type_annotation(
|
||||
checker,
|
||||
|
||||
@@ -2152,7 +2152,7 @@ impl<'a> Checker<'a> {
|
||||
|
||||
self.semantic.restore(snapshot);
|
||||
|
||||
if self.semantic.in_annotation() && self.semantic.future_annotations_or_stub() {
|
||||
if self.semantic.in_annotation() && self.semantic.in_typing_only_annotation() {
|
||||
if self.enabled(Rule::QuotedAnnotation) {
|
||||
pyupgrade::rules::quoted_annotation(self, value, range);
|
||||
}
|
||||
|
||||
@@ -334,6 +334,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
|
||||
(Flake8Async, "101") => (RuleGroup::Stable, rules::flake8_async::rules::OpenSleepOrSubprocessInAsyncFunction),
|
||||
(Flake8Async, "102") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOsCallInAsyncFunction),
|
||||
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::SleepForeverCall),
|
||||
|
||||
// flake8-trio
|
||||
(Flake8Trio, "100") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioTimeoutWithoutAwait),
|
||||
|
||||
@@ -16,6 +16,7 @@ mod tests {
|
||||
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC100.py"))]
|
||||
#[test_case(Rule::OpenSleepOrSubprocessInAsyncFunction, Path::new("ASYNC101.py"))]
|
||||
#[test_case(Rule::BlockingOsCallInAsyncFunction, Path::new("ASYNC102.py"))]
|
||||
#[test_case(Rule::SleepForeverCall, Path::new("ASYNC116.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pub(crate) use blocking_http_call::*;
|
||||
pub(crate) use blocking_os_call::*;
|
||||
pub(crate) use open_sleep_or_subprocess_call::*;
|
||||
pub(crate) use sleep_forever_call::*;
|
||||
|
||||
mod blocking_http_call;
|
||||
mod blocking_os_call;
|
||||
mod open_sleep_or_subprocess_call;
|
||||
mod sleep_forever_call;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, ExprCall, ExprNumberLiteral, Number};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::{checkers::ast::Checker, importer::ImportRequest};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `trio.sleep()` with an interval greater than 24 hours.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `trio.sleep()` with an interval greater than 24 hours is usually intended
|
||||
/// to sleep indefinitely. Instead of using a large interval,
|
||||
/// `trio.sleep_forever()` better conveys the intent.
|
||||
///
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import trio
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// await trio.sleep(86401)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import trio
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// await trio.sleep_forever()
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SleepForeverCall;
|
||||
|
||||
impl Violation for SleepForeverCall {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!("Replace with `trio.sleep_forever()`"))
|
||||
}
|
||||
}
|
||||
|
||||
/// ASYNC116
|
||||
pub(crate) fn sleep_forever_call(checker: &mut Checker, call: &ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if call.arguments.len() != 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(arg) = call.arguments.find_argument("seconds", 0) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(call.func.as_ref())
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["trio", "sleep"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let Expr::NumberLiteral(ExprNumberLiteral { value, .. }) = arg else {
|
||||
return;
|
||||
};
|
||||
|
||||
// TODO(ekohilas): Replace with Duration::from_days(1).as_secs(); when available.
|
||||
let one_day_in_secs = 60 * 60 * 24;
|
||||
match value {
|
||||
Number::Int(int_value) => {
|
||||
let Some(int_value) = int_value.as_u64() else {
|
||||
return;
|
||||
};
|
||||
if int_value <= one_day_in_secs {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Number::Float(float_value) =>
|
||||
{
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
if *float_value <= one_day_in_secs as f64 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Number::Complex { .. } => return,
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(SleepForeverCall, call.range());
|
||||
let replacement_function = "sleep_forever";
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from("trio", replacement_function),
|
||||
call.func.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let reference_edit = Edit::range_replacement(binding, call.func.range());
|
||||
let arg_edit = Edit::range_replacement("()".to_string(), call.arguments.range());
|
||||
Ok(Fix::unsafe_edits(import_edit, [reference_edit, arg_edit]))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC116.py:11:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
10 | # These examples are probably not meant to ever wake up:
|
||||
11 | await trio.sleep(100000) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
12 |
|
||||
13 | # 'inf literal' overflow trick
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
8 8 | import trio
|
||||
9 9 |
|
||||
10 10 | # These examples are probably not meant to ever wake up:
|
||||
11 |- await trio.sleep(100000) # error: 116, "async"
|
||||
11 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
12 12 |
|
||||
13 13 | # 'inf literal' overflow trick
|
||||
14 14 | await trio.sleep(1e999) # error: 116, "async"
|
||||
|
||||
ASYNC116.py:14:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
13 | # 'inf literal' overflow trick
|
||||
14 | await trio.sleep(1e999) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
15 |
|
||||
16 | await trio.sleep(86399)
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
11 11 | await trio.sleep(100000) # error: 116, "async"
|
||||
12 12 |
|
||||
13 13 | # 'inf literal' overflow trick
|
||||
14 |- await trio.sleep(1e999) # error: 116, "async"
|
||||
14 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
15 15 |
|
||||
16 16 | await trio.sleep(86399)
|
||||
17 17 | await trio.sleep(86400)
|
||||
|
||||
ASYNC116.py:18:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
16 | await trio.sleep(86399)
|
||||
17 | await trio.sleep(86400)
|
||||
18 | await trio.sleep(86400.01) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
19 | await trio.sleep(86401) # error: 116, "async"
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
15 15 |
|
||||
16 16 | await trio.sleep(86399)
|
||||
17 17 | await trio.sleep(86400)
|
||||
18 |- await trio.sleep(86400.01) # error: 116, "async"
|
||||
18 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
19 19 | await trio.sleep(86401) # error: 116, "async"
|
||||
20 20 |
|
||||
21 21 | await trio.sleep(-1) # will raise a runtime error
|
||||
|
||||
ASYNC116.py:19:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
17 | await trio.sleep(86400)
|
||||
18 | await trio.sleep(86400.01) # error: 116, "async"
|
||||
19 | await trio.sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
20 |
|
||||
21 | await trio.sleep(-1) # will raise a runtime error
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
16 16 | await trio.sleep(86399)
|
||||
17 17 | await trio.sleep(86400)
|
||||
18 18 | await trio.sleep(86400.01) # error: 116, "async"
|
||||
19 |- await trio.sleep(86401) # error: 116, "async"
|
||||
19 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
20 20 |
|
||||
21 21 | await trio.sleep(-1) # will raise a runtime error
|
||||
22 22 | await trio.sleep(0) # handled by different check
|
||||
|
||||
ASYNC116.py:48:5: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
47 | # does not require the call to be awaited, nor in an async fun
|
||||
48 | trio.sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
49 | # also checks that we don't break visit_Call
|
||||
50 | trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
45 45 | import trio
|
||||
46 46 |
|
||||
47 47 | # does not require the call to be awaited, nor in an async fun
|
||||
48 |- trio.sleep(86401) # error: 116, "async"
|
||||
48 |+ trio.sleep_forever() # error: 116, "async"
|
||||
49 49 | # also checks that we don't break visit_Call
|
||||
50 50 | trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
51 51 |
|
||||
|
||||
ASYNC116.py:50:14: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
48 | trio.sleep(86401) # error: 116, "async"
|
||||
49 | # also checks that we don't break visit_Call
|
||||
50 | trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
47 47 | # does not require the call to be awaited, nor in an async fun
|
||||
48 48 | trio.sleep(86401) # error: 116, "async"
|
||||
49 49 | # also checks that we don't break visit_Call
|
||||
50 |- trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
50 |+ trio.run(trio.sleep_forever()) # error: 116, "async"
|
||||
51 51 |
|
||||
52 52 |
|
||||
53 53 | async def import_from_trio():
|
||||
|
||||
ASYNC116.py:57:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
56 | # catch from import
|
||||
57 | await sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^ ASYNC116
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
3 3 | import math
|
||||
4 4 | from math import inf
|
||||
5 |+from trio import sleep_forever
|
||||
5 6 |
|
||||
6 7 |
|
||||
7 8 | async def import_trio():
|
||||
--------------------------------------------------------------------------------
|
||||
54 55 | from trio import sleep
|
||||
55 56 |
|
||||
56 57 | # catch from import
|
||||
57 |- await sleep(86401) # error: 116, "async"
|
||||
58 |+ await sleep_forever() # error: 116, "async"
|
||||
@@ -43,6 +43,7 @@ mod tests {
|
||||
#[test_case(Path::new("no_future_import_uses_union_inner.py"))]
|
||||
#[test_case(Path::new("ok_no_types.py"))]
|
||||
#[test_case(Path::new("ok_uses_future.py"))]
|
||||
#[test_case(Path::new("ok_quoted_type.py"))]
|
||||
fn fa102(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("fa102_{}", path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -7,7 +7,6 @@ use ruff_python_ast::Expr;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::Importer;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of PEP 585- and PEP 604-style type annotations in Python
|
||||
@@ -87,13 +86,11 @@ impl AlwaysFixableViolation for FutureRequiredTypeAnnotation {
|
||||
/// FA102
|
||||
pub(crate) fn future_required_type_annotation(checker: &mut Checker, expr: &Expr, reason: Reason) {
|
||||
let mut diagnostic = Diagnostic::new(FutureRequiredTypeAnnotation { reason }, expr.range());
|
||||
if let Some(python_ast) = checker.semantic().definitions.python_ast() {
|
||||
let required_import =
|
||||
AnyImport::ImportFrom(ImportFrom::member("__future__", "annotations"));
|
||||
diagnostic.set_fix(Fix::unsafe_edit(
|
||||
Importer::new(python_ast, checker.locator(), checker.stylist())
|
||||
.add_import(&required_import, TextSize::default()),
|
||||
));
|
||||
}
|
||||
let required_import = AnyImport::ImportFrom(ImportFrom::member("__future__", "annotations"));
|
||||
diagnostic.set_fix(Fix::unsafe_edit(
|
||||
checker
|
||||
.importer()
|
||||
.add_import(&required_import, TextSize::default()),
|
||||
));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
no_future_import_uses_lowercase.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] = []
|
||||
| ^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_lowercase.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str, int]) -> None:
|
||||
@@ -29,5 +14,3 @@ no_future_import_uses_lowercase.py:6:14: FA102 [*] Missing `from __future__ impo
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
|
||||
|
||||
@@ -1,36 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
no_future_import_uses_union.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] | None = []
|
||||
| ^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] | None = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] | None = []
|
||||
| ^^^^^^^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] | None = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str, int] | None) -> None:
|
||||
@@ -58,5 +28,3 @@ no_future_import_uses_union.py:6:14: FA102 [*] Missing `from __future__ import a
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] | None = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
|
||||
|
||||
@@ -1,36 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
no_future_import_uses_union_inner.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str | None] = []
|
||||
| ^^^^^^^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:2:18: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str | None] = []
|
||||
| ^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str | None, int]) -> None:
|
||||
@@ -60,35 +30,3 @@ no_future_import_uses_union_inner.py:6:19: FA102 [*] Missing `from __future__ im
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:7:8: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str | None, int]) -> None:
|
||||
7 | z: tuple[str, str | None, str] = tuple(y)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA102
|
||||
8 | del z
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:7:19: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
6 | def hello(y: dict[str | None, int]) -> None:
|
||||
7 | z: tuple[str, str | None, str] = tuple(y)
|
||||
| ^^^^^^^^^^ FA102
|
||||
8 | del z
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
|
||||
@@ -384,7 +384,11 @@ pub(crate) fn unittest_raises_assertion(
|
||||
},
|
||||
call.func.range(),
|
||||
);
|
||||
if !checker.indexer().has_comments(call, checker.locator()) {
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(call, checker.locator())
|
||||
{
|
||||
if let Some(args) = to_pytest_raises_args(checker, attr.as_str(), &call.arguments) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
|
||||
@@ -526,7 +526,11 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
|
||||
}
|
||||
|
||||
// Avoid removing comments.
|
||||
if checker.indexer().has_comments(expr, checker.locator()) {
|
||||
if checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(expr, checker.locator())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +209,11 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
|
||||
},
|
||||
stmt_if.range(),
|
||||
);
|
||||
if !checker.indexer().has_comments(stmt_if, checker.locator()) {
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(stmt_if, checker.locator())
|
||||
{
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
contents,
|
||||
stmt_if.range(),
|
||||
@@ -295,7 +299,11 @@ pub(crate) fn if_exp_instead_of_dict_get(
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
if !checker.indexer().has_comments(expr, checker.locator()) {
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(expr, checker.locator())
|
||||
{
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
contents,
|
||||
expr.range(),
|
||||
|
||||
@@ -142,7 +142,11 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &a
|
||||
},
|
||||
stmt_if.range(),
|
||||
);
|
||||
if !checker.indexer().has_comments(stmt_if, checker.locator()) {
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(stmt_if, checker.locator())
|
||||
{
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
contents,
|
||||
stmt_if.range(),
|
||||
|
||||
@@ -193,7 +193,11 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
|
||||
}
|
||||
|
||||
// Generate the replacement condition.
|
||||
let condition = if checker.indexer().has_comments(&range, checker.locator()) {
|
||||
let condition = if checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(&range, checker.locator())
|
||||
{
|
||||
None
|
||||
} else {
|
||||
// If the return values are inverted, wrap the condition in a `not`.
|
||||
|
||||
@@ -125,7 +125,11 @@ pub(crate) fn suppressible_exception(
|
||||
},
|
||||
stmt.range(),
|
||||
);
|
||||
if !checker.indexer().has_comments(stmt, checker.locator()) {
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(stmt, checker.locator())
|
||||
{
|
||||
diagnostic.try_set_fix(|| {
|
||||
// let range = statement_range(stmt, checker.locator(), checker.indexer());
|
||||
|
||||
|
||||
@@ -1009,10 +1009,10 @@ impl<'a> BlankLinesChecker<'a> {
|
||||
)));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
self.stylist
|
||||
.line_ending()
|
||||
.repeat(BLANK_LINES_TOP_LEVEL as usize),
|
||||
self.locator.line_start(line.first_token_range.start()),
|
||||
self.stylist.line_ending().repeat(
|
||||
(BLANK_LINES_TOP_LEVEL - line.preceding_blank_lines.count()) as usize,
|
||||
),
|
||||
self.locator.line_start(state.last_non_comment_line_end),
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,7 @@ fn check(
|
||||
|
||||
// If we're at the end of line, skip.
|
||||
if matches!(next_char, '\n' | '\r') {
|
||||
contains_valid_escape_sequence = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(crate) fn missing_whitespace_after_keyword(
|
||||
let tok0_kind = tok0.kind();
|
||||
let tok1_kind = tok1.kind();
|
||||
|
||||
if tok0_kind.is_non_soft_keyword()
|
||||
if tok0_kind.is_keyword()
|
||||
&& !(tok0_kind.is_singleton()
|
||||
|| matches!(tok0_kind, TokenKind::Async | TokenKind::Await)
|
||||
|| tok0_kind == TokenKind::Except && tok1_kind == TokenKind::Star
|
||||
|
||||
@@ -445,7 +445,7 @@ impl LogicalLinesBuilder {
|
||||
|
||||
if matches!(kind, TokenKind::Comma | TokenKind::Semi | TokenKind::Colon) {
|
||||
line.flags.insert(TokenFlags::PUNCTUATION);
|
||||
} else if kind.is_non_soft_keyword() {
|
||||
} else if kind.is_keyword() {
|
||||
line.flags.insert(TokenFlags::KEYWORD);
|
||||
}
|
||||
|
||||
|
||||
@@ -127,8 +127,8 @@ pub(crate) fn whitespace_around_keywords(line: &LogicalLine, context: &mut Logic
|
||||
let mut after_keyword = false;
|
||||
|
||||
for token in line.tokens() {
|
||||
let is_non_soft_keyword = token.kind().is_non_soft_keyword();
|
||||
if is_non_soft_keyword {
|
||||
let is_keyword = token.kind().is_keyword();
|
||||
if is_keyword {
|
||||
if !after_keyword {
|
||||
match line.leading_whitespace(token) {
|
||||
(Whitespace::Tab, offset) => {
|
||||
@@ -184,6 +184,6 @@ pub(crate) fn whitespace_around_keywords(line: &LogicalLine, context: &mut Logic
|
||||
}
|
||||
}
|
||||
|
||||
after_keyword = is_non_soft_keyword;
|
||||
after_keyword = is_keyword;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for negative comparison using `not {foo} in {bar}`.
|
||||
/// Checks for membership tests using `not {element} in {collection}`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Negative comparison should be done using `not in`.
|
||||
/// Testing membership with `{element} not in {collection}` is more readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -42,10 +42,11 @@ impl AlwaysFixableViolation for NotInTest {
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for negative comparison using `not {foo} is {bar}`.
|
||||
/// Checks for identity comparisons using `not {foo} is {bar}`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Negative comparison should be done using `is not`.
|
||||
/// According to [PEP8], testing for an object's identity with `is not` is more
|
||||
/// readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -60,6 +61,8 @@ impl AlwaysFixableViolation for NotInTest {
|
||||
/// pass
|
||||
/// Z = X.B is not Y
|
||||
/// ```
|
||||
///
|
||||
/// [PEP8]: https://peps.python.org/pep-0008/#programming-recommendations
|
||||
#[violation]
|
||||
pub struct NotIsTest;
|
||||
|
||||
|
||||
@@ -11,18 +11,6 @@ use ruff_text_size::{TextRange, TextSize};
|
||||
/// According to [PEP 8], spaces are preferred over tabs (unless used to remain
|
||||
/// consistent with code that is already indented with tabs).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// if True:
|
||||
/// a = 1
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// if True:
|
||||
/// a = 1
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
|
||||
@@ -190,4 +190,22 @@ E27.py:35:14: E271 [*] Multiple spaces after keyword
|
||||
37 37 | from w import(e, f)
|
||||
38 38 | #: E275
|
||||
|
||||
E27.py:70:5: E271 [*] Multiple spaces after keyword
|
||||
|
|
||||
69 | #: E271
|
||||
70 | type Number = int
|
||||
| ^^ E271
|
||||
71 |
|
||||
72 | #: E273
|
||||
|
|
||||
= help: Replace with single space
|
||||
|
||||
ℹ Safe fix
|
||||
67 67 | # Soft keywords
|
||||
68 68 |
|
||||
69 69 | #: E271
|
||||
70 |-type Number = int
|
||||
70 |+type Number = int
|
||||
71 71 |
|
||||
72 72 | #: E273
|
||||
73 73 | type Number = int
|
||||
|
||||
@@ -106,4 +106,22 @@ E27.py:30:10: E273 [*] Tab after keyword
|
||||
32 32 | from u import (a, b)
|
||||
33 33 | from v import c, d
|
||||
|
||||
E27.py:73:5: E273 [*] Tab after keyword
|
||||
|
|
||||
72 | #: E273
|
||||
73 | type Number = int
|
||||
| ^^^^ E273
|
||||
74 |
|
||||
75 | #: E275
|
||||
|
|
||||
= help: Replace with single space
|
||||
|
||||
ℹ Safe fix
|
||||
70 70 | type Number = int
|
||||
71 71 |
|
||||
72 72 | #: E273
|
||||
73 |-type Number = int
|
||||
73 |+type Number = int
|
||||
74 74 |
|
||||
75 75 | #: E275
|
||||
76 76 | match(foo):
|
||||
|
||||
@@ -106,4 +106,39 @@ E27.py:54:5: E275 [*] Missing whitespace after keyword
|
||||
56 56 | def f():
|
||||
57 57 | print((yield))
|
||||
|
||||
E27.py:76:1: E275 [*] Missing whitespace after keyword
|
||||
|
|
||||
75 | #: E275
|
||||
76 | match(foo):
|
||||
| ^^^^^ E275
|
||||
77 | case(1):
|
||||
78 | pass
|
||||
|
|
||||
= help: Added missing whitespace after keyword
|
||||
|
||||
ℹ Safe fix
|
||||
73 73 | type Number = int
|
||||
74 74 |
|
||||
75 75 | #: E275
|
||||
76 |-match(foo):
|
||||
76 |+match (foo):
|
||||
77 77 | case(1):
|
||||
78 78 | pass
|
||||
|
||||
E27.py:77:5: E275 [*] Missing whitespace after keyword
|
||||
|
|
||||
75 | #: E275
|
||||
76 | match(foo):
|
||||
77 | case(1):
|
||||
| ^^^^ E275
|
||||
78 | pass
|
||||
|
|
||||
= help: Added missing whitespace after keyword
|
||||
|
||||
ℹ Safe fix
|
||||
74 74 |
|
||||
75 75 | #: E275
|
||||
76 76 | match(foo):
|
||||
77 |- case(1):
|
||||
77 |+ case (1):
|
||||
78 78 | pass
|
||||
|
||||
@@ -11,14 +11,13 @@ E30.py:806:1: E305 [*] Expected 2 blank lines after class or function definition
|
||||
= help: Add missing blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
803 803 | # comment
|
||||
804 804 |
|
||||
805 805 | # another comment
|
||||
806 |+
|
||||
807 |+
|
||||
806 808 | fn()
|
||||
807 809 | # end
|
||||
808 810 |
|
||||
800 800 | def fn():
|
||||
801 801 | print()
|
||||
802 802 |
|
||||
803 |+
|
||||
803 804 | # comment
|
||||
804 805 |
|
||||
805 806 | # another comment
|
||||
|
||||
E30.py:817:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
||||
|
|
||||
@@ -30,14 +29,13 @@ E30.py:817:1: E305 [*] Expected 2 blank lines after class or function definition
|
||||
= help: Add missing blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
814 814 | # comment
|
||||
815 815 |
|
||||
816 816 | # another comment
|
||||
817 |+
|
||||
818 |+
|
||||
817 819 | a = 1
|
||||
818 820 | # end
|
||||
819 821 |
|
||||
811 811 | class Class():
|
||||
812 812 | pass
|
||||
813 813 |
|
||||
814 |+
|
||||
814 815 | # comment
|
||||
815 816 |
|
||||
816 817 | # another comment
|
||||
|
||||
E30.py:829:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
||||
|
|
||||
@@ -70,14 +68,13 @@ E30.py:841:1: E305 [*] Expected 2 blank lines after class or function definition
|
||||
= help: Add missing blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
837 837 | def a():
|
||||
838 838 | print()
|
||||
839 839 |
|
||||
840 840 | # Two spaces before comments, too.
|
||||
841 |+
|
||||
842 |+
|
||||
841 843 | if a():
|
||||
842 844 | a()
|
||||
843 845 | # end
|
||||
840 |+
|
||||
840 841 | # Two spaces before comments, too.
|
||||
841 842 | if a():
|
||||
842 843 | a()
|
||||
|
||||
E30.py:854:1: E305 [*] Expected 2 blank lines after class or function definition, found (1)
|
||||
|
|
||||
|
||||
@@ -145,6 +145,8 @@ W605_0.py:48:6: W605 [*] Invalid escape sequence: `\ `
|
||||
47 | #: W605:1:7
|
||||
48 | u'foo\ bar'
|
||||
| ^^ W605
|
||||
49 |
|
||||
50 | #: W605:1:13
|
||||
|
|
||||
= help: Use a raw string literal
|
||||
|
||||
@@ -154,5 +156,61 @@ W605_0.py:48:6: W605 [*] Invalid escape sequence: `\ `
|
||||
47 47 | #: W605:1:7
|
||||
48 |-u'foo\ bar'
|
||||
48 |+r'foo\ bar'
|
||||
49 49 |
|
||||
50 50 | #: W605:1:13
|
||||
51 51 | (
|
||||
|
||||
W605_0.py:53:9: W605 [*] Invalid escape sequence: `\.`
|
||||
|
|
||||
51 | (
|
||||
52 | "foo \
|
||||
53 | bar \. baz"
|
||||
| ^^ W605
|
||||
54 | )
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Safe fix
|
||||
50 50 | #: W605:1:13
|
||||
51 51 | (
|
||||
52 52 | "foo \
|
||||
53 |- bar \. baz"
|
||||
53 |+ bar \\. baz"
|
||||
54 54 | )
|
||||
55 55 |
|
||||
56 56 | #: W605:1:6
|
||||
|
||||
W605_0.py:57:6: W605 [*] Invalid escape sequence: `\.`
|
||||
|
|
||||
56 | #: W605:1:6
|
||||
57 | "foo \. bar \t"
|
||||
| ^^ W605
|
||||
58 |
|
||||
59 | #: W605:1:13
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Safe fix
|
||||
54 54 | )
|
||||
55 55 |
|
||||
56 56 | #: W605:1:6
|
||||
57 |-"foo \. bar \t"
|
||||
57 |+"foo \\. bar \t"
|
||||
58 58 |
|
||||
59 59 | #: W605:1:13
|
||||
60 60 | "foo \t bar \."
|
||||
|
||||
W605_0.py:60:13: W605 [*] Invalid escape sequence: `\.`
|
||||
|
|
||||
59 | #: W605:1:13
|
||||
60 | "foo \t bar \."
|
||||
| ^^ W605
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Safe fix
|
||||
57 57 | "foo \. bar \t"
|
||||
58 58 |
|
||||
59 59 | #: W605:1:13
|
||||
60 |-"foo \t bar \."
|
||||
60 |+"foo \t bar \\."
|
||||
|
||||
@@ -12,11 +12,10 @@ E30.ipynb:55:1: E305 [*] Expected 2 blank lines after class or function definiti
|
||||
= help: Add missing blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
52 52 | # comment
|
||||
53 53 |
|
||||
54 54 | # another comment
|
||||
55 |+
|
||||
56 |+
|
||||
55 57 | fn()
|
||||
56 58 | # end
|
||||
57 59 | # E306:3:5
|
||||
49 49 | def fn():
|
||||
50 50 | print()
|
||||
51 51 |
|
||||
52 |+
|
||||
52 53 | # comment
|
||||
53 54 |
|
||||
54 55 | # another comment
|
||||
|
||||
@@ -229,6 +229,49 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_24/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_25__all_nonempty/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_26__all_empty/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_27__all_mistyped/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_28__all_multiple/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_29__all_conditional/__init__.py"))]
|
||||
fn f401_stable(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"{}_stable_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyflakes").join(path).as_path(),
|
||||
&LinterSettings::for_rule(rule_code),
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_24/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_25__all_nonempty/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_26__all_empty/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_27__all_mistyped/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_28__all_multiple/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_29__all_conditional/__init__.py"))]
|
||||
fn f401_deprecated_option(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"{}_deprecated_option_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyflakes").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
ignore_init_module_imports: false,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f841_dummy_variable_rgx() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -24,6 +24,7 @@ enum UnusedImportContext {
|
||||
Init {
|
||||
first_party: bool,
|
||||
dunder_all_count: usize,
|
||||
ignore_init_module_imports: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,12 +47,29 @@ enum UnusedImportContext {
|
||||
/// from module import member as member
|
||||
/// ```
|
||||
///
|
||||
/// Alternatively, you can use `__all__` to declare a symbol as part of the module's
|
||||
/// interface, as in:
|
||||
///
|
||||
/// ```python
|
||||
/// # __init__.py
|
||||
/// import some_module
|
||||
///
|
||||
/// __all__ = [ "some_module"]
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
///
|
||||
/// When `ignore_init_module_imports` is disabled, fixes can remove for unused imports in `__init__` files.
|
||||
/// These fixes are considered unsafe because they can change the public interface.
|
||||
/// Fixes to remove unused imports are safe, except in `__init__.py` files.
|
||||
///
|
||||
/// Applying fixes to `__init__.py` files is currently in preview. The fix offered depends on the
|
||||
/// type of the unused import. Ruff will suggest a safe fix to export first-party imports with
|
||||
/// either a redundant alias or, if already present in the file, an `__all__` entry. If multiple
|
||||
/// `__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix
|
||||
/// to remove third-party and standard library imports -- the fix is unsafe because the module's
|
||||
/// interface changes.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import numpy as np # unused import
|
||||
///
|
||||
@@ -61,12 +79,14 @@ enum UnusedImportContext {
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def area(radius):
|
||||
/// return 3.14 * radius**2
|
||||
/// ```
|
||||
///
|
||||
/// To check the availability of a module, use `importlib.util.find_spec`:
|
||||
///
|
||||
/// ```python
|
||||
/// from importlib.util import find_spec
|
||||
///
|
||||
@@ -87,6 +107,8 @@ enum UnusedImportContext {
|
||||
pub struct UnusedImport {
|
||||
/// Qualified name of the import
|
||||
name: String,
|
||||
/// Unqualified name of the import
|
||||
module: String,
|
||||
/// Name of the import binding
|
||||
binding: String,
|
||||
context: Option<UnusedImportContext>,
|
||||
@@ -117,6 +139,7 @@ impl Violation for UnusedImport {
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let UnusedImport {
|
||||
name,
|
||||
module,
|
||||
binding,
|
||||
multiple,
|
||||
..
|
||||
@@ -125,14 +148,14 @@ impl Violation for UnusedImport {
|
||||
Some(UnusedImportContext::Init {
|
||||
first_party: true,
|
||||
dunder_all_count: 1,
|
||||
ignore_init_module_imports: true,
|
||||
}) => Some(format!("Add unused import `{binding}` to __all__")),
|
||||
|
||||
Some(UnusedImportContext::Init {
|
||||
first_party: true,
|
||||
dunder_all_count: 0,
|
||||
}) => Some(format!(
|
||||
"Use an explicit re-export: `{binding} as {binding}`"
|
||||
)),
|
||||
ignore_init_module_imports: true,
|
||||
}) => Some(format!("Use an explicit re-export: `{module} as {module}`")),
|
||||
|
||||
_ => Some(if *multiple {
|
||||
"Remove unused import".to_string()
|
||||
@@ -244,7 +267,8 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
|
||||
}
|
||||
|
||||
let in_init = checker.path().ends_with("__init__.py");
|
||||
let fix_init = checker.settings.preview.is_enabled();
|
||||
let fix_init = !checker.settings.ignore_init_module_imports;
|
||||
let preview_mode = checker.settings.preview.is_enabled();
|
||||
let dunder_all_exprs = find_dunder_all_exprs(checker.semantic());
|
||||
|
||||
// Generate a diagnostic for every import, but share fixes across all imports within the same
|
||||
@@ -275,6 +299,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
|
||||
checker,
|
||||
),
|
||||
dunder_all_count: dunder_all_exprs.len(),
|
||||
ignore_init_module_imports: !fix_init,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -288,30 +313,31 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
|
||||
first_party: true,
|
||||
..
|
||||
})
|
||||
)
|
||||
) && preview_mode
|
||||
});
|
||||
|
||||
// generate fixes that are shared across bindings in the statement
|
||||
let (fix_remove, fix_reexport) = if (!in_init || fix_init) && !in_except_handler {
|
||||
(
|
||||
fix_by_removing_imports(
|
||||
checker,
|
||||
import_statement,
|
||||
to_remove.iter().map(|(binding, _)| binding),
|
||||
in_init,
|
||||
let (fix_remove, fix_reexport) =
|
||||
if (!in_init || fix_init || preview_mode) && !in_except_handler {
|
||||
(
|
||||
fix_by_removing_imports(
|
||||
checker,
|
||||
import_statement,
|
||||
to_remove.iter().map(|(binding, _)| binding),
|
||||
in_init,
|
||||
)
|
||||
.ok(),
|
||||
fix_by_reexporting(
|
||||
checker,
|
||||
import_statement,
|
||||
&to_reexport.iter().map(|(b, _)| b).collect::<Vec<_>>(),
|
||||
&dunder_all_exprs,
|
||||
)
|
||||
.ok(),
|
||||
)
|
||||
.ok(),
|
||||
fix_by_reexporting(
|
||||
checker,
|
||||
import_statement,
|
||||
&to_reexport.iter().map(|(b, _)| b).collect::<Vec<_>>(),
|
||||
&dunder_all_exprs,
|
||||
)
|
||||
.ok(),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
for ((binding, context), fix) in iter::Iterator::chain(
|
||||
iter::zip(to_remove, iter::repeat(fix_remove)),
|
||||
@@ -320,6 +346,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnusedImport {
|
||||
name: binding.import.qualified_name().to_string(),
|
||||
module: binding.import.member_name().to_string(),
|
||||
binding: binding.name.to_string(),
|
||||
context,
|
||||
multiple,
|
||||
@@ -344,6 +371,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnusedImport {
|
||||
name: binding.import.qualified_name().to_string(),
|
||||
module: binding.import.member_name().to_string(),
|
||||
binding: binding.name.to_string(),
|
||||
context: None,
|
||||
multiple: false,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:19:8: F401 [*] `sys` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
19 | import sys # F401: remove unused
|
||||
| ^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `sys`
|
||||
|
||||
ℹ Unsafe fix
|
||||
16 16 | import argparse as argparse # Ok: is redundant alias
|
||||
17 17 |
|
||||
18 18 |
|
||||
19 |-import sys # F401: remove unused
|
||||
20 19 |
|
||||
21 20 |
|
||||
22 21 | # first-party
|
||||
|
||||
__init__.py:33:15: F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
33 | from . import unused # F401: change to redundant alias
|
||||
| ^^^^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `.unused`
|
||||
|
||||
ℹ Unsafe fix
|
||||
30 30 | from . import aliased as aliased # Ok: is redundant alias
|
||||
31 31 |
|
||||
32 32 |
|
||||
33 |-from . import unused # F401: change to redundant alias
|
||||
34 33 |
|
||||
35 34 |
|
||||
36 35 | from . import renamed as bees # F401: no fix
|
||||
|
||||
__init__.py:36:26: F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
36 | from . import renamed as bees # F401: no fix
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `.renamed`
|
||||
|
||||
ℹ Unsafe fix
|
||||
33 33 | from . import unused # F401: change to redundant alias
|
||||
34 34 |
|
||||
35 35 |
|
||||
36 |-from . import renamed as bees # F401: no fix
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:19:8: F401 [*] `sys` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
19 | import sys # F401: remove unused
|
||||
| ^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `sys`
|
||||
|
||||
ℹ Unsafe fix
|
||||
16 16 | import argparse # Ok: is exported in __all__
|
||||
17 17 |
|
||||
18 18 |
|
||||
19 |-import sys # F401: remove unused
|
||||
20 19 |
|
||||
21 20 |
|
||||
22 21 | # first-party
|
||||
|
||||
__init__.py:36:15: F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
36 | from . import unused # F401: add to __all__
|
||||
| ^^^^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `.unused`
|
||||
|
||||
ℹ Unsafe fix
|
||||
33 33 | from . import exported # Ok: is exported in __all__
|
||||
34 34 |
|
||||
35 35 |
|
||||
36 |-from . import unused # F401: add to __all__
|
||||
37 36 |
|
||||
38 37 |
|
||||
39 38 | from . import renamed as bees # F401: add to __all__
|
||||
|
||||
__init__.py:39:26: F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
39 | from . import renamed as bees # F401: add to __all__
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `.renamed`
|
||||
|
||||
ℹ Unsafe fix
|
||||
36 36 | from . import unused # F401: add to __all__
|
||||
37 37 |
|
||||
38 38 |
|
||||
39 |-from . import renamed as bees # F401: add to __all__
|
||||
40 39 |
|
||||
41 40 |
|
||||
42 41 | __all__ = ["argparse", "exported"]
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:5:15: F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
5 | from . import unused # F401: add to __all__
|
||||
| ^^^^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `.unused`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | """
|
||||
3 3 |
|
||||
4 4 |
|
||||
5 |-from . import unused # F401: add to __all__
|
||||
6 5 |
|
||||
7 6 |
|
||||
8 7 | from . import renamed as bees # F401: add to __all__
|
||||
|
||||
__init__.py:8:26: F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
8 | from . import renamed as bees # F401: add to __all__
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `.renamed`
|
||||
|
||||
ℹ Unsafe fix
|
||||
5 5 | from . import unused # F401: add to __all__
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 |-from . import renamed as bees # F401: add to __all__
|
||||
9 8 |
|
||||
10 9 |
|
||||
11 10 | __all__ = []
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:5:15: F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
5 | from . import unused # F401: recommend add to all w/o fix
|
||||
| ^^^^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `.unused`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | """
|
||||
3 3 |
|
||||
4 4 |
|
||||
5 |-from . import unused # F401: recommend add to all w/o fix
|
||||
6 5 |
|
||||
7 6 |
|
||||
8 7 | from . import renamed as bees # F401: recommend add to all w/o fix
|
||||
|
||||
__init__.py:8:26: F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
8 | from . import renamed as bees # F401: recommend add to all w/o fix
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `.renamed`
|
||||
|
||||
ℹ Unsafe fix
|
||||
5 5 | from . import unused # F401: recommend add to all w/o fix
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 |-from . import renamed as bees # F401: recommend add to all w/o fix
|
||||
9 8 |
|
||||
10 9 |
|
||||
11 10 | __all__ = None
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:5:15: F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
5 | from . import unused, renamed as bees # F401: add to __all__
|
||||
| ^^^^^^ F401
|
||||
|
|
||||
= help: Remove unused import
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | """
|
||||
3 3 |
|
||||
4 4 |
|
||||
5 |-from . import unused, renamed as bees # F401: add to __all__
|
||||
6 5 |
|
||||
7 6 |
|
||||
8 7 | __all__ = [];
|
||||
|
||||
__init__.py:5:34: F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
5 | from . import unused, renamed as bees # F401: add to __all__
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Remove unused import
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | """
|
||||
3 3 |
|
||||
4 4 |
|
||||
5 |-from . import unused, renamed as bees # F401: add to __all__
|
||||
6 5 |
|
||||
7 6 |
|
||||
8 7 | __all__ = [];
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:8:15: F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
6 | import sys
|
||||
7 |
|
||||
8 | from . import unused, exported, renamed as bees
|
||||
| ^^^^^^ F401
|
||||
9 |
|
||||
10 | if sys.version_info > (3, 9):
|
||||
|
|
||||
= help: Remove unused import
|
||||
|
||||
ℹ Unsafe fix
|
||||
5 5 |
|
||||
6 6 | import sys
|
||||
7 7 |
|
||||
8 |-from . import unused, exported, renamed as bees
|
||||
8 |+from . import exported
|
||||
9 9 |
|
||||
10 10 | if sys.version_info > (3, 9):
|
||||
11 11 | from . import also_exported
|
||||
|
||||
__init__.py:8:44: F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
6 | import sys
|
||||
7 |
|
||||
8 | from . import unused, exported, renamed as bees
|
||||
| ^^^^ F401
|
||||
9 |
|
||||
10 | if sys.version_info > (3, 9):
|
||||
|
|
||||
= help: Remove unused import
|
||||
|
||||
ℹ Unsafe fix
|
||||
5 5 |
|
||||
6 6 | import sys
|
||||
7 7 |
|
||||
8 |-from . import unused, exported, renamed as bees
|
||||
8 |+from . import exported
|
||||
9 9 |
|
||||
10 10 | if sys.version_info > (3, 9):
|
||||
11 11 | from . import also_exported
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:19:8: F401 `sys` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
19 | import sys # F401: remove unused
|
||||
| ^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `sys`
|
||||
|
||||
__init__.py:33:15: F401 `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
33 | from . import unused # F401: change to redundant alias
|
||||
| ^^^^^^ F401
|
||||
|
|
||||
= help: Use an explicit re-export: `unused as unused`
|
||||
|
||||
__init__.py:36:26: F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
36 | from . import renamed as bees # F401: no fix
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Use an explicit re-export: `renamed as renamed`
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:19:8: F401 `sys` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
19 | import sys # F401: remove unused
|
||||
| ^^^ F401
|
||||
|
|
||||
= help: Remove unused import: `sys`
|
||||
|
||||
__init__.py:36:15: F401 `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
36 | from . import unused # F401: add to __all__
|
||||
| ^^^^^^ F401
|
||||
|
|
||||
= help: Add unused import `unused` to __all__
|
||||
|
||||
__init__.py:39:26: F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
39 | from . import renamed as bees # F401: add to __all__
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Add unused import `bees` to __all__
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:5:15: F401 `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
5 | from . import unused # F401: add to __all__
|
||||
| ^^^^^^ F401
|
||||
|
|
||||
= help: Add unused import `unused` to __all__
|
||||
|
||||
__init__.py:8:26: F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
8 | from . import renamed as bees # F401: add to __all__
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Add unused import `bees` to __all__
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:5:15: F401 `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
5 | from . import unused # F401: recommend add to all w/o fix
|
||||
| ^^^^^^ F401
|
||||
|
|
||||
= help: Add unused import `unused` to __all__
|
||||
|
||||
__init__.py:8:26: F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
8 | from . import renamed as bees # F401: recommend add to all w/o fix
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Add unused import `bees` to __all__
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:5:15: F401 `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
5 | from . import unused, renamed as bees # F401: add to __all__
|
||||
| ^^^^^^ F401
|
||||
|
|
||||
= help: Add unused import `unused` to __all__
|
||||
|
||||
__init__.py:5:34: F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
5 | from . import unused, renamed as bees # F401: add to __all__
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Add unused import `bees` to __all__
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:8:15: F401 `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
6 | import sys
|
||||
7 |
|
||||
8 | from . import unused, exported, renamed as bees
|
||||
| ^^^^^^ F401
|
||||
9 |
|
||||
10 | if sys.version_info > (3, 9):
|
||||
|
|
||||
= help: Remove unused import
|
||||
|
||||
__init__.py:8:44: F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
||||
|
|
||||
6 | import sys
|
||||
7 |
|
||||
8 | from . import unused, exported, renamed as bees
|
||||
| ^^^^ F401
|
||||
9 |
|
||||
10 | if sys.version_info > (3, 9):
|
||||
|
|
||||
= help: Remove unused import
|
||||
@@ -39,4 +39,4 @@ __init__.py:36:26: F401 `.renamed` imported but unused; consider removing, addin
|
||||
36 | from . import renamed as bees # F401: no fix
|
||||
| ^^^^ F401
|
||||
|
|
||||
= help: Use an explicit re-export: `bees as bees`
|
||||
= help: Use an explicit re-export: `renamed as renamed`
|
||||
|
||||
@@ -155,7 +155,11 @@ pub(crate) fn nested_min_max(
|
||||
MinMax::try_from_call(func.as_ref(), keywords.as_ref(), checker.semantic()) == Some(min_max)
|
||||
}) {
|
||||
let mut diagnostic = Diagnostic::new(NestedMinMax { func: min_max }, expr.range());
|
||||
if !checker.indexer().has_comments(expr, checker.locator()) {
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(expr, checker.locator())
|
||||
{
|
||||
let flattened_expr = Expr::Call(ast::ExprCall {
|
||||
func: Box::new(func.clone()),
|
||||
arguments: Arguments {
|
||||
|
||||
@@ -98,6 +98,8 @@ impl Ranged for AttributeAssignment<'_> {
|
||||
}
|
||||
|
||||
/// Return a list of attributes that are assigned to but not included in `__slots__`.
|
||||
///
|
||||
/// If the `__slots__` attribute cannot be statically determined, returns an empty vector.
|
||||
fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
|
||||
// First, collect all the attributes that are assigned to `__slots__`.
|
||||
let mut slots = FxHashSet::default();
|
||||
@@ -110,7 +112,13 @@ fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
|
||||
};
|
||||
|
||||
if id == "__slots__" {
|
||||
slots.extend(slots_attributes(value));
|
||||
for attribute in slots_attributes(value) {
|
||||
if let Some(attribute) = attribute {
|
||||
slots.insert(attribute);
|
||||
} else {
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +133,13 @@ fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
|
||||
};
|
||||
|
||||
if id == "__slots__" {
|
||||
slots.extend(slots_attributes(value));
|
||||
for attribute in slots_attributes(value) {
|
||||
if let Some(attribute) = attribute {
|
||||
slots.insert(attribute);
|
||||
} else {
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +150,13 @@ fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
|
||||
};
|
||||
|
||||
if id == "__slots__" {
|
||||
slots.extend(slots_attributes(value));
|
||||
for attribute in slots_attributes(value) {
|
||||
if let Some(attribute) = attribute {
|
||||
slots.insert(attribute);
|
||||
} else {
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -237,12 +257,14 @@ fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
|
||||
}
|
||||
|
||||
/// Return an iterator over the attributes enumerated in the given `__slots__` value.
|
||||
fn slots_attributes(expr: &Expr) -> impl Iterator<Item = &str> {
|
||||
///
|
||||
/// If an attribute can't be statically determined, it will be `None`.
|
||||
fn slots_attributes(expr: &Expr) -> impl Iterator<Item = Option<&str>> {
|
||||
// Ex) `__slots__ = ("name",)`
|
||||
let elts_iter = match expr {
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. })
|
||||
| Expr::List(ast::ExprList { elts, .. })
|
||||
| Expr::Set(ast::ExprSet { elts, .. }) => Some(elts.iter().filter_map(|elt| match elt {
|
||||
| Expr::Set(ast::ExprSet { elts, .. }) => Some(elts.iter().map(|elt| match elt {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value.to_str()),
|
||||
_ => None,
|
||||
})),
|
||||
@@ -251,7 +273,7 @@ fn slots_attributes(expr: &Expr) -> impl Iterator<Item = &str> {
|
||||
|
||||
// Ex) `__slots__ = {"name": ...}`
|
||||
let keys_iter = match expr {
|
||||
Expr::Dict(dict) => Some(dict.iter_keys().filter_map(|key| match key {
|
||||
Expr::Dict(dict) => Some(dict.iter_keys().map(|key| match key {
|
||||
Some(Expr::StringLiteral(ast::ExprStringLiteral { value, .. })) => Some(value.to_str()),
|
||||
_ => None,
|
||||
})),
|
||||
|
||||
@@ -199,7 +199,9 @@ fn num_branches(stmts: &[Stmt]) -> usize {
|
||||
finalbody,
|
||||
..
|
||||
}) => {
|
||||
1 + num_branches(body)
|
||||
// Count each `except` clause as a branch; the `else` and `finally` clauses also
|
||||
// count, but the `try` clause itself does not.
|
||||
num_branches(body)
|
||||
+ (if orelse.is_empty() {
|
||||
0
|
||||
} else {
|
||||
@@ -323,6 +325,47 @@ return 1
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_except() -> Result<()> {
|
||||
let source: &str = r"
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
";
|
||||
|
||||
test_helper(source, 1)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_except_else() -> Result<()> {
|
||||
let source: &str = r"
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
";
|
||||
|
||||
test_helper(source, 2)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_finally() -> Result<()> {
|
||||
let source: &str = r"
|
||||
try:
|
||||
pass
|
||||
finally:
|
||||
pass
|
||||
";
|
||||
|
||||
test_helper(source, 1)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_except_except_else_finally() -> Result<()> {
|
||||
let source: &str = r"
|
||||
@@ -338,7 +381,7 @@ finally:
|
||||
pass
|
||||
";
|
||||
|
||||
test_helper(source, 5)?;
|
||||
test_helper(source, 4)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ too_many_branches.py:8:5: PLR0912 Too many branches (13 > 12)
|
||||
10 | if 1:
|
||||
|
|
||||
|
||||
too_many_branches.py:76:5: PLR0912 Too many branches (13 > 12)
|
||||
too_many_branches.py:80:5: PLR0912 Too many branches (13 > 12)
|
||||
|
|
||||
74 | pass
|
||||
75 |
|
||||
76 | def with_statement_wrong():
|
||||
78 | pass
|
||||
79 |
|
||||
80 | def with_statement_wrong():
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PLR0912
|
||||
77 | """statements inside the with statement should get counted"""
|
||||
78 | with suppress(Exception):
|
||||
81 | """statements inside the with statement should get counted"""
|
||||
82 | with suppress(Exception):
|
||||
|
|
||||
|
||||
@@ -55,7 +55,8 @@ mod tests {
|
||||
#[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_5.py"))]
|
||||
#[test_case(Rule::PrintfStringFormatting, Path::new("UP031_0.py"))]
|
||||
#[test_case(Rule::PrintfStringFormatting, Path::new("UP031_1.py"))]
|
||||
#[test_case(Rule::QuotedAnnotation, Path::new("UP037.py"))]
|
||||
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_0.py"))]
|
||||
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_1.py"))]
|
||||
#[test_case(Rule::RedundantOpenModes, Path::new("UP015.py"))]
|
||||
#[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))]
|
||||
#[test_case(Rule::ReplaceUniversalNewlines, Path::new("UP021.py"))]
|
||||
|
||||
@@ -10,11 +10,18 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// In Python, type annotations can be quoted to avoid forward references.
|
||||
///
|
||||
/// However, if `from __future__ import annotations` is present, Python
|
||||
/// will always evaluate type annotations in a deferred manner, making
|
||||
/// the quotes unnecessary.
|
||||
///
|
||||
/// Similarly, if the annotation is located in a typing-only context and
|
||||
/// won't be evaluated by Python at runtime, the quotes will also be
|
||||
/// considered unnecessary. For example, Python does not evaluate type
|
||||
/// annotations on assignments in function bodies.
|
||||
///
|
||||
/// ## Example
|
||||
/// Given:
|
||||
/// ```python
|
||||
/// from __future__ import annotations
|
||||
///
|
||||
@@ -32,6 +39,18 @@ use crate::checkers::ast::Checker;
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Given:
|
||||
/// ```python
|
||||
/// def foo() -> None:
|
||||
/// bar: "Bar"
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo() -> None:
|
||||
/// bar: Bar
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 563](https://peps.python.org/pep-0563/)
|
||||
/// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html#module-__future__)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP037.py:18:14: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:18:14: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
18 | def foo(var: "MyClass") -> "MyClass":
|
||||
| ^^^^^^^^^ UP037
|
||||
@@ -19,7 +19,7 @@ UP037.py:18:14: UP037 [*] Remove quotes from type annotation
|
||||
20 20 |
|
||||
21 21 |
|
||||
|
||||
UP037.py:18:28: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:18:28: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
18 | def foo(var: "MyClass") -> "MyClass":
|
||||
| ^^^^^^^^^ UP037
|
||||
@@ -37,7 +37,7 @@ UP037.py:18:28: UP037 [*] Remove quotes from type annotation
|
||||
20 20 |
|
||||
21 21 |
|
||||
|
||||
UP037.py:19:8: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:19:8: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
18 | def foo(var: "MyClass") -> "MyClass":
|
||||
19 | x: "MyClass"
|
||||
@@ -55,7 +55,7 @@ UP037.py:19:8: UP037 [*] Remove quotes from type annotation
|
||||
21 21 |
|
||||
22 22 | def foo(*, inplace: "bool"):
|
||||
|
||||
UP037.py:22:21: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:22:21: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
22 | def foo(*, inplace: "bool"):
|
||||
| ^^^^^^ UP037
|
||||
@@ -73,7 +73,7 @@ UP037.py:22:21: UP037 [*] Remove quotes from type annotation
|
||||
24 24 |
|
||||
25 25 |
|
||||
|
||||
UP037.py:26:16: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:26:16: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
26 | def foo(*args: "str", **kwargs: "int"):
|
||||
| ^^^^^ UP037
|
||||
@@ -91,7 +91,7 @@ UP037.py:26:16: UP037 [*] Remove quotes from type annotation
|
||||
28 28 |
|
||||
29 29 |
|
||||
|
||||
UP037.py:26:33: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:26:33: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
26 | def foo(*args: "str", **kwargs: "int"):
|
||||
| ^^^^^ UP037
|
||||
@@ -109,7 +109,7 @@ UP037.py:26:33: UP037 [*] Remove quotes from type annotation
|
||||
28 28 |
|
||||
29 29 |
|
||||
|
||||
UP037.py:30:10: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:30:10: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
30 | x: Tuple["MyClass"]
|
||||
| ^^^^^^^^^ UP037
|
||||
@@ -128,7 +128,7 @@ UP037.py:30:10: UP037 [*] Remove quotes from type annotation
|
||||
32 32 | x: Callable[["MyClass"], None]
|
||||
33 33 |
|
||||
|
||||
UP037.py:32:14: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:32:14: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
30 | x: Tuple["MyClass"]
|
||||
31 |
|
||||
@@ -147,7 +147,7 @@ UP037.py:32:14: UP037 [*] Remove quotes from type annotation
|
||||
34 34 |
|
||||
35 35 | class Foo(NamedTuple):
|
||||
|
||||
UP037.py:36:8: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:36:8: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
35 | class Foo(NamedTuple):
|
||||
36 | x: "MyClass"
|
||||
@@ -165,7 +165,7 @@ UP037.py:36:8: UP037 [*] Remove quotes from type annotation
|
||||
38 38 |
|
||||
39 39 | class D(TypedDict):
|
||||
|
||||
UP037.py:40:27: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:40:27: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
39 | class D(TypedDict):
|
||||
40 | E: TypedDict("E", foo="int", total=False)
|
||||
@@ -183,7 +183,7 @@ UP037.py:40:27: UP037 [*] Remove quotes from type annotation
|
||||
42 42 |
|
||||
43 43 | class D(TypedDict):
|
||||
|
||||
UP037.py:44:31: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:44:31: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
43 | class D(TypedDict):
|
||||
44 | E: TypedDict("E", {"foo": "int"})
|
||||
@@ -201,7 +201,7 @@ UP037.py:44:31: UP037 [*] Remove quotes from type annotation
|
||||
46 46 |
|
||||
47 47 | x: Annotated["str", "metadata"]
|
||||
|
||||
UP037.py:47:14: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:47:14: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
47 | x: Annotated["str", "metadata"]
|
||||
| ^^^^^ UP037
|
||||
@@ -220,7 +220,7 @@ UP037.py:47:14: UP037 [*] Remove quotes from type annotation
|
||||
49 49 | x: Arg("str", "name")
|
||||
50 50 |
|
||||
|
||||
UP037.py:49:8: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:49:8: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
47 | x: Annotated["str", "metadata"]
|
||||
48 |
|
||||
@@ -241,7 +241,7 @@ UP037.py:49:8: UP037 [*] Remove quotes from type annotation
|
||||
51 51 | x: DefaultArg("str", "name")
|
||||
52 52 |
|
||||
|
||||
UP037.py:51:15: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:51:15: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
49 | x: Arg("str", "name")
|
||||
50 |
|
||||
@@ -262,7 +262,7 @@ UP037.py:51:15: UP037 [*] Remove quotes from type annotation
|
||||
53 53 | x: NamedArg("str", "name")
|
||||
54 54 |
|
||||
|
||||
UP037.py:53:13: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:53:13: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
51 | x: DefaultArg("str", "name")
|
||||
52 |
|
||||
@@ -283,7 +283,7 @@ UP037.py:53:13: UP037 [*] Remove quotes from type annotation
|
||||
55 55 | x: DefaultNamedArg("str", "name")
|
||||
56 56 |
|
||||
|
||||
UP037.py:55:20: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:55:20: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
53 | x: NamedArg("str", "name")
|
||||
54 |
|
||||
@@ -304,7 +304,7 @@ UP037.py:55:20: UP037 [*] Remove quotes from type annotation
|
||||
57 57 | x: DefaultNamedArg("str", name="name")
|
||||
58 58 |
|
||||
|
||||
UP037.py:57:20: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:57:20: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
55 | x: DefaultNamedArg("str", "name")
|
||||
56 |
|
||||
@@ -325,7 +325,7 @@ UP037.py:57:20: UP037 [*] Remove quotes from type annotation
|
||||
59 59 | x: VarArg("str")
|
||||
60 60 |
|
||||
|
||||
UP037.py:59:11: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:59:11: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
57 | x: DefaultNamedArg("str", name="name")
|
||||
58 |
|
||||
@@ -346,7 +346,7 @@ UP037.py:59:11: UP037 [*] Remove quotes from type annotation
|
||||
61 61 | x: List[List[List["MyClass"]]]
|
||||
62 62 |
|
||||
|
||||
UP037.py:61:19: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:61:19: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
59 | x: VarArg("str")
|
||||
60 |
|
||||
@@ -367,7 +367,7 @@ UP037.py:61:19: UP037 [*] Remove quotes from type annotation
|
||||
63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
|
||||
64 64 |
|
||||
|
||||
UP037.py:63:29: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:63:29: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
61 | x: List[List[List["MyClass"]]]
|
||||
62 |
|
||||
@@ -388,7 +388,7 @@ UP037.py:63:29: UP037 [*] Remove quotes from type annotation
|
||||
65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
|
||||
66 66 |
|
||||
|
||||
UP037.py:63:45: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:63:45: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
61 | x: List[List[List["MyClass"]]]
|
||||
62 |
|
||||
@@ -409,7 +409,7 @@ UP037.py:63:45: UP037 [*] Remove quotes from type annotation
|
||||
65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
|
||||
66 66 |
|
||||
|
||||
UP037.py:65:29: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:65:29: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
|
||||
64 |
|
||||
@@ -430,7 +430,7 @@ UP037.py:65:29: UP037 [*] Remove quotes from type annotation
|
||||
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
|
||||
68 68 |
|
||||
|
||||
UP037.py:65:36: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:65:36: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
|
||||
64 |
|
||||
@@ -451,7 +451,7 @@ UP037.py:65:36: UP037 [*] Remove quotes from type annotation
|
||||
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
|
||||
68 68 |
|
||||
|
||||
UP037.py:65:45: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:65:45: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
|
||||
64 |
|
||||
@@ -472,7 +472,7 @@ UP037.py:65:45: UP037 [*] Remove quotes from type annotation
|
||||
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
|
||||
68 68 |
|
||||
|
||||
UP037.py:65:52: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:65:52: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
|
||||
64 |
|
||||
@@ -493,7 +493,7 @@ UP037.py:65:52: UP037 [*] Remove quotes from type annotation
|
||||
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
|
||||
68 68 |
|
||||
|
||||
UP037.py:67:24: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:67:24: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
|
||||
66 |
|
||||
@@ -514,7 +514,7 @@ UP037.py:67:24: UP037 [*] Remove quotes from type annotation
|
||||
69 69 | X: MyCallable("X")
|
||||
70 70 |
|
||||
|
||||
UP037.py:67:38: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:67:38: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
|
||||
66 |
|
||||
@@ -535,7 +535,7 @@ UP037.py:67:38: UP037 [*] Remove quotes from type annotation
|
||||
69 69 | X: MyCallable("X")
|
||||
70 70 |
|
||||
|
||||
UP037.py:67:45: UP037 [*] Remove quotes from type annotation
|
||||
UP037_0.py:67:45: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
|
||||
66 |
|
||||
@@ -554,6 +554,4 @@ UP037.py:67:45: UP037 [*] Remove quotes from type annotation
|
||||
67 |+x: NamedTuple(typename="X", fields=[("foo", int)])
|
||||
68 68 |
|
||||
69 69 | X: MyCallable("X")
|
||||
70 70 |
|
||||
|
||||
|
||||
70 70 |
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP037_1.py:9:8: UP037 [*] Remove quotes from type annotation
|
||||
|
|
||||
7 | def foo():
|
||||
8 | # UP037
|
||||
9 | x: "Tuple[int, int]" = (0, 0)
|
||||
| ^^^^^^^^^^^^^^^^^ UP037
|
||||
10 | print(x)
|
||||
|
|
||||
= help: Remove quotes
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 |
|
||||
7 7 | def foo():
|
||||
8 8 | # UP037
|
||||
9 |- x: "Tuple[int, int]" = (0, 0)
|
||||
9 |+ x: Tuple[int, int] = (0, 0)
|
||||
10 10 | print(x)
|
||||
11 11 |
|
||||
12 12 |
|
||||
@@ -1,9 +1,14 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::helpers::contains_effect;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -64,29 +69,13 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &mut Checker, if_expr: &ast
|
||||
|
||||
let mut diagnostic = Diagnostic::new(IfExpInsteadOfOrOperator, *range);
|
||||
|
||||
// Grab the range of the `test` and `orelse` expressions.
|
||||
let left = parenthesized_range(
|
||||
test.into(),
|
||||
if_expr.into(),
|
||||
checker.indexer().comment_ranges(),
|
||||
checker.locator().contents(),
|
||||
)
|
||||
.unwrap_or(test.range());
|
||||
let right = parenthesized_range(
|
||||
orelse.into(),
|
||||
if_expr.into(),
|
||||
checker.indexer().comment_ranges(),
|
||||
checker.locator().contents(),
|
||||
)
|
||||
.unwrap_or(orelse.range());
|
||||
|
||||
// Replace with `{test} or {orelse}`.
|
||||
diagnostic.set_fix(Fix::applicable_edit(
|
||||
Edit::range_replacement(
|
||||
format!(
|
||||
"{} or {}",
|
||||
checker.locator().slice(left),
|
||||
checker.locator().slice(right),
|
||||
parenthesize_test(test, if_expr, checker.indexer(), checker.locator()),
|
||||
parenthesize_test(orelse, if_expr, checker.indexer(), checker.locator()),
|
||||
),
|
||||
if_expr.range(),
|
||||
),
|
||||
@@ -99,3 +88,30 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &mut Checker, if_expr: &ast
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Parenthesize an expression for use in an `or` operator (e.g., parenthesize `x` in `x or y`),
|
||||
/// if it's required to maintain the correct order of operations.
|
||||
///
|
||||
/// If the expression is already parenthesized, it will be returned as-is regardless of whether
|
||||
/// the parentheses are required.
|
||||
///
|
||||
/// See: <https://docs.python.org/3/reference/expressions.html#operator-precedence>
|
||||
fn parenthesize_test<'a>(
|
||||
expr: &Expr,
|
||||
if_expr: &ast::ExprIf,
|
||||
indexer: &Indexer,
|
||||
locator: &Locator<'a>,
|
||||
) -> Cow<'a, str> {
|
||||
if let Some(range) = parenthesized_range(
|
||||
expr.into(),
|
||||
if_expr.into(),
|
||||
indexer.comment_ranges(),
|
||||
locator.contents(),
|
||||
) {
|
||||
Cow::Borrowed(locator.slice(range))
|
||||
} else if matches!(expr, Expr::If(_) | Expr::Lambda(_) | Expr::Named(_)) {
|
||||
Cow::Owned(format!("({})", locator.slice(expr.range())))
|
||||
} else {
|
||||
Cow::Borrowed(locator.slice(expr.range()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,3 +177,33 @@ FURB110.py:34:5: FURB110 [*] Replace ternary `if` expression with `or` operator
|
||||
39 |- y
|
||||
34 |+ x or y
|
||||
40 35 | )
|
||||
41 36 |
|
||||
42 37 | # FURB110
|
||||
|
||||
FURB110.py:44:5: FURB110 [*] Replace ternary `if` expression with `or` operator
|
||||
|
|
||||
42 | # FURB110
|
||||
43 | z = (
|
||||
44 | x
|
||||
| _____^
|
||||
45 | | if x
|
||||
46 | | else y
|
||||
47 | | if y > 0
|
||||
48 | | else None
|
||||
| |_____________^ FURB110
|
||||
49 | )
|
||||
|
|
||||
= help: Replace with `or` operator
|
||||
|
||||
ℹ Safe fix
|
||||
41 41 |
|
||||
42 42 | # FURB110
|
||||
43 43 | z = (
|
||||
44 |- x
|
||||
45 |- if x
|
||||
46 |- else y
|
||||
44 |+ x or (y
|
||||
47 45 | if y > 0
|
||||
48 |- else None
|
||||
46 |+ else None)
|
||||
49 47 | )
|
||||
|
||||
@@ -198,7 +198,11 @@ pub(crate) fn collection_literal_concatenation(checker: &mut Checker, expr: &Exp
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
if !checker.indexer().has_comments(expr, checker.locator()) {
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(expr, checker.locator())
|
||||
{
|
||||
// This suggestion could be unsafe if the non-literal expression in the
|
||||
// expression has overridden the `__add__` (or `__radd__`) magic methods.
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
|
||||
@@ -23,7 +23,7 @@ impl fmt::Display for SourceValue {
|
||||
|
||||
impl Cell {
|
||||
/// Return the [`SourceValue`] of the cell.
|
||||
pub(crate) fn source(&self) -> &SourceValue {
|
||||
pub fn source(&self) -> &SourceValue {
|
||||
match self {
|
||||
Cell::Code(cell) => &cell.source,
|
||||
Cell::Markdown(cell) => &cell.source,
|
||||
|
||||
@@ -98,7 +98,7 @@ impl Notebook {
|
||||
reader.read_exact(&mut buf).is_ok_and(|()| buf[0] == b'\n')
|
||||
});
|
||||
reader.rewind()?;
|
||||
let mut raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
|
||||
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
|
||||
Ok(notebook) => notebook,
|
||||
Err(err) => {
|
||||
// Translate the error into a diagnostic
|
||||
@@ -113,7 +113,13 @@ impl Notebook {
|
||||
});
|
||||
}
|
||||
};
|
||||
Self::from_raw_notebook(raw_notebook, trailing_newline)
|
||||
}
|
||||
|
||||
pub fn from_raw_notebook(
|
||||
mut raw_notebook: RawNotebook,
|
||||
trailing_newline: bool,
|
||||
) -> Result<Self, NotebookError> {
|
||||
// v4 is what everybody uses
|
||||
if raw_notebook.nbformat != 4 {
|
||||
// bail because we should have already failed at the json schema stage
|
||||
|
||||
@@ -112,25 +112,6 @@ impl Indexer {
|
||||
self.continuation_lines.binary_search(&line_start).is_ok()
|
||||
}
|
||||
|
||||
/// Returns `true` if a statement or expression includes at least one comment.
|
||||
pub fn has_comments<T>(&self, node: &T, locator: &Locator) -> bool
|
||||
where
|
||||
T: Ranged,
|
||||
{
|
||||
let start = if has_leading_content(node.start(), locator) {
|
||||
node.start()
|
||||
} else {
|
||||
locator.line_start(node.start())
|
||||
};
|
||||
let end = if has_trailing_content(node.end(), locator) {
|
||||
node.end()
|
||||
} else {
|
||||
locator.line_end(node.end())
|
||||
};
|
||||
|
||||
self.comment_ranges().intersects(TextRange::new(start, end))
|
||||
}
|
||||
|
||||
/// Given an offset at the end of a line (including newlines), return the offset of the
|
||||
/// continuation at the end of that line.
|
||||
fn find_continuation(&self, offset: TextSize, locator: &Locator) -> Option<TextSize> {
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_source_file::Locator;
|
||||
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::is_python_whitespace;
|
||||
use crate::{has_leading_content, has_trailing_content, is_python_whitespace};
|
||||
|
||||
/// Stores the ranges of comments sorted by [`TextRange::start`] in increasing order. No two ranges are overlapping.
|
||||
#[derive(Clone, Default)]
|
||||
@@ -49,6 +49,25 @@ impl CommentRanges {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if a statement or expression includes at least one comment.
|
||||
pub fn has_comments<T>(&self, node: &T, locator: &Locator) -> bool
|
||||
where
|
||||
T: Ranged,
|
||||
{
|
||||
let start = if has_leading_content(node.start(), locator) {
|
||||
node.start()
|
||||
} else {
|
||||
locator.line_start(node.start())
|
||||
};
|
||||
let end = if has_trailing_content(node.end(), locator) {
|
||||
node.end()
|
||||
} else {
|
||||
locator.line_end(node.end())
|
||||
};
|
||||
|
||||
self.intersects(TextRange::new(start, end))
|
||||
}
|
||||
|
||||
/// Given a [`CommentRanges`], determine which comments are grouped together
|
||||
/// in "comment blocks". A "comment block" is a sequence of consecutive
|
||||
/// own-line comments in which the comment hash (`#`) appears in the same
|
||||
|
||||
@@ -21,6 +21,7 @@ ruff_python_codegen = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true }
|
||||
ruff_python_index = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_notebook = { path = "../ruff_notebook" }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ruff_workspace = { workspace = true }
|
||||
|
||||
@@ -1,17 +1,68 @@
|
||||
## The Ruff Language Server
|
||||
|
||||
Welcome! `ruff server` is a language server that powers editor integrations with Ruff. The job of the language server is to
|
||||
listen for requests from the client, (in this case, the code editor of your choice) and call into Ruff's linter and formatter
|
||||
crates to create real-time diagnostics or formatted code, which is then sent back to the client. It also tracks configuration
|
||||
files in your editor's workspace, and will refresh its in-memory configuration whenever those files are modified.
|
||||
Welcome!
|
||||
|
||||
`ruff server` is a language server that powers Ruff's editor integrations.
|
||||
|
||||
The job of the language server is to listen for requests from the client (in this case, the code editor of your choice)
|
||||
and call into Ruff's linter and formatter crates to construct real-time diagnostics or formatted code, which is then
|
||||
sent back to the client. It also tracks configuration files in your editor's workspace, and will refresh its in-memory
|
||||
configuration whenever those files are modified.
|
||||
|
||||
### Setup
|
||||
|
||||
We have specific setup instructions depending on your editor. If you don't see your editor on this list and would like a setup guide, please open an issue.
|
||||
We have specific setup instructions depending on your editor. If you don't see your editor on this list and would like a
|
||||
setup guide, please open an issue.
|
||||
|
||||
- Visual Studio Code: Install the [Ruff extension from the VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). The language server used by the extension will be, by default, the one in your actively-installed `ruff` binary. If you don't have `ruff` installed and haven't provided a path to the extension, it comes with a bundled `ruff` version that it will use instead. Since the new Ruff language server has not yet been stabilized, you will need to use the pre-release version of the extension and enable the `Experimental Server` setting.
|
||||
- Neovim: See the [Neovim setup guide](docs/setup/NEOVIM.md).
|
||||
If you're transferring your configuration from [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp), regardless of
|
||||
editor, there are several settings which have changed or are no longer available. See the [migration guide](docs/MIGRATION.md) for
|
||||
more.
|
||||
|
||||
#### VS Code
|
||||
|
||||
Install the Ruff extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff).
|
||||
|
||||
As this server is still in Beta, you will need to enable the "Native Server" extension setting, either in the settings
|
||||
UI:
|
||||
|
||||

|
||||
|
||||
Or in your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ruff.nativeServer": true
|
||||
}
|
||||
```
|
||||
|
||||
From there, you can configure Ruff to format Python code on-save with:
|
||||
|
||||
```json
|
||||
{
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For more, see [_Configuring VS Code_](https://github.com/astral-sh/ruff-vscode?tab=readme-ov-file#configuring-vs-code)
|
||||
in the Ruff extension documentation.
|
||||
|
||||
By default, the extension will run against the `ruff` binary that it discovers in your environment. If you don't have
|
||||
`ruff` installed, the extension will fall back to a bundled version of the binary.
|
||||
|
||||
#### Neovim
|
||||
|
||||
See the [Neovim setup guide](docs/setup/NEOVIM.md).
|
||||
|
||||
#### Helix
|
||||
|
||||
See the [Helix setup guide](docs/setup//HELIX.md).
|
||||
|
||||
### Contributing
|
||||
|
||||
If you're interested in contributing to `ruff server` - well, first of all, thank you! Second of all, you might find the [**contribution guide**](CONTRIBUTING.md) to be a useful resource. Finally, don't hesitate to reach out on our [**Discord**](https://discord.com/invite/astral-sh) if you have questions.
|
||||
If you're interested in contributing to `ruff server` - well, first of all, thank you! Second of all, you might find the
|
||||
[**contribution guide**](CONTRIBUTING.md) to be a useful resource.
|
||||
|
||||
Finally, don't hesitate to reach out on [**Discord**](https://discord.com/invite/astral-sh) if you have questions.
|
||||
|
||||
BIN
crates/ruff_server/assets/nativeServer.png
Normal file
BIN
crates/ruff_server/assets/nativeServer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
85
crates/ruff_server/docs/MIGRATION.md
Normal file
85
crates/ruff_server/docs/MIGRATION.md
Normal file
@@ -0,0 +1,85 @@
|
||||
## Migrating From `ruff-lsp`
|
||||
|
||||
While `ruff server` supports the same feature set as [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp), migrating to
|
||||
`ruff server` may require changes to your Ruff or language server configuration.
|
||||
|
||||
> \[!NOTE\]
|
||||
>
|
||||
> The [VS Code extension](https://github.com/astral-sh/ruff-vscode) settings include documentation to indicate which
|
||||
> settings are supported by `ruff server`. As such, this migration guide is primarily targeted at editors that lack
|
||||
> explicit documentation for `ruff server` settings, such as Helix or Neovim.
|
||||
|
||||
### Unsupported Settings
|
||||
|
||||
Several `ruff-lsp` settings are not supported by `ruff server`. These are, as follows:
|
||||
|
||||
- `format.args`
|
||||
- `ignoreStandardLibrary`
|
||||
- `interpreter`
|
||||
- `lint.args`
|
||||
- `lint.run`
|
||||
- `logLevel`
|
||||
- `path`
|
||||
|
||||
Note that some of these settings, like `interpreter` and `path`, are still accepted by the VS Code extension. `path`,
|
||||
in particular, can be used to specify a dedicated binary to use when initializing `ruff server`. But the language server
|
||||
itself will no longer accept such settings.
|
||||
|
||||
### New Settings
|
||||
|
||||
`ruff server` introduces several new settings that `ruff-lsp` does not have. These are, as follows:
|
||||
|
||||
- `configuration`: A path to a `ruff.toml` or `pyproject.toml` file to use for configuration. By default, Ruff will discover configuration for each project from the filesystem, mirroring the behavior of the Ruff CLI.
|
||||
- `configurationPreference`: Used to specify how you want to resolve server settings with local file configuration. The following values are available:
|
||||
- `"editorFirst"`: The default strategy - configuration set in the server settings takes priority over configuration set in `.toml` files.
|
||||
- `"filesystemFirst"`: An alternative strategy - configuration set in `.toml` files takes priority over configuration set in the server settings.
|
||||
- `"editorOnly"`: An alternative strategy - configuration set in `.toml` files is ignored entirely.
|
||||
- `exclude`: Paths for the linter and formatter to ignore. See [the documentation](https://docs.astral.sh/ruff/settings/#exclude) for more details.
|
||||
- `format.preview`: Enables [preview mode](https://docs.astral.sh/ruff/settings/#format_preview) for the formatter; enables unstable formatting.
|
||||
- `lineLength`: The [line length](https://docs.astral.sh/ruff/settings/#line-length) used by the formatter and linter.
|
||||
- `lint.select`: The rule codes to enable. Use `ALL` to enable all rules. See [the documentation](https://docs.astral.sh/ruff/settings/#lint_select) for more details.
|
||||
- `lint.extendSelect`: Enables additional rule codes on top of existing configuration, instead of overriding it. Use `ALL` to enable all rules.
|
||||
- `lint.ignore`: Sets rule codes to disable. See [the documentation](https://docs.astral.sh/ruff/settings/#lint_ignore) for more details.
|
||||
- `lint.preview`: Enables [preview mode](https://docs.astral.sh/ruff/settings/#lint_preview) for the linter; enables unstable rules and fixes.
|
||||
|
||||
Several of these new settings are replacements for the now-unsupported `format.args` and `lint.args`. For example, if
|
||||
you've been passing `--select=<RULES>` to `lint.args`, you can migrate to the new server by using `lint.select` with a
|
||||
value of `["<RULES>"]`.
|
||||
|
||||
### Examples
|
||||
|
||||
Let's say you have these settings in VS Code:
|
||||
|
||||
```json
|
||||
{
|
||||
"ruff.lint.args": "--select=E,F --line-length 80 --config ~/.config/custom_ruff_config.toml"
|
||||
}
|
||||
```
|
||||
|
||||
After enabling the native server, you can migrate your settings like so:
|
||||
|
||||
```json
|
||||
{
|
||||
"ruff.configuration": "~/.config/custom_ruff_config.toml",
|
||||
"ruff.lineLength": 80,
|
||||
"ruff.lint.select": ["E", "F"]
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, let's say you have these settings in Helix:
|
||||
|
||||
```toml
|
||||
[language-server.ruff.config.lint]
|
||||
args = "--select=E,F --line-length 80 --config ~/.config/custom_ruff_config.toml"
|
||||
```
|
||||
|
||||
These can be migrated like so:
|
||||
|
||||
```toml
|
||||
[language-server.ruff.config]
|
||||
configuration = "~/.config/custom_ruff_config.toml"
|
||||
lineLength = 80
|
||||
|
||||
[language-server.ruff.config.lint]
|
||||
select = ["E", "F"]
|
||||
```
|
||||
@@ -34,7 +34,7 @@ language-servers = ["ruff", "pylsp"]
|
||||
|
||||
Once you've set up the server, you should see diagnostics in your Python files. Code actions and other LSP features should also be available.
|
||||
|
||||

|
||||

|
||||
*This screenshot is using `select=["ALL]"` for demonstration purposes.*
|
||||
|
||||
If you want to, as an example, turn on auto-formatting, add `auto-format = true`:
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
{
|
||||
"settings": {
|
||||
"codeAction": {
|
||||
"disableRuleComment": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"ignore": ["RUF001"],
|
||||
"run": "onSave"
|
||||
},
|
||||
"fixAll": false,
|
||||
"logLevel": "warn",
|
||||
"lineLength": 80,
|
||||
"exclude": ["third_party"]
|
||||
}
|
||||
"codeAction": {
|
||||
"disableRuleComment": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"ignore": ["RUF001"],
|
||||
"run": "onSave"
|
||||
},
|
||||
"fixAll": false,
|
||||
"logLevel": "warn",
|
||||
"lineLength": 80,
|
||||
"exclude": ["third_party"]
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
//! Types and utilities for working with text, modifying source files, and `Ruff <-> LSP` type conversion.
|
||||
|
||||
mod document;
|
||||
mod notebook;
|
||||
mod range;
|
||||
mod replacement;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
pub use document::Document;
|
||||
pub(crate) use document::DocumentVersion;
|
||||
pub use document::TextDocument;
|
||||
use lsp_types::PositionEncodingKind;
|
||||
pub(crate) use range::{RangeExt, ToRangeExt};
|
||||
pub(crate) use notebook::NotebookDocument;
|
||||
pub(crate) use range::{NotebookRange, RangeExt, ToRangeExt};
|
||||
pub(crate) use replacement::Replacement;
|
||||
|
||||
use crate::session::ResolvedClientCapabilities;
|
||||
use crate::{fix::Fixes, session::ResolvedClientCapabilities};
|
||||
|
||||
/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`].
|
||||
// Please maintain the order from least to greatest priority for the derived `Ord` impl.
|
||||
@@ -29,6 +31,37 @@ pub enum PositionEncoding {
|
||||
UTF8,
|
||||
}
|
||||
|
||||
/// A unique document ID, derived from a URL passed as part of an LSP request.
|
||||
/// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum DocumentKey {
|
||||
Notebook(PathBuf),
|
||||
NotebookCell(lsp_types::Url),
|
||||
Text(PathBuf),
|
||||
}
|
||||
|
||||
impl DocumentKey {
|
||||
/// Converts the key back into its original URL.
|
||||
pub(crate) fn into_url(self) -> lsp_types::Url {
|
||||
match self {
|
||||
DocumentKey::NotebookCell(url) => url,
|
||||
DocumentKey::Notebook(path) | DocumentKey::Text(path) => {
|
||||
lsp_types::Url::from_file_path(path)
|
||||
.expect("file path originally from URL should convert back to URL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DocumentKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NotebookCell(url) => url.fmt(f),
|
||||
Self::Notebook(path) | Self::Text(path) => path.display().fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks multi-document edits to eventually merge into a `WorkspaceEdit`.
|
||||
/// Compatible with clients that don't support `workspace.workspaceEdit.documentChanges`.
|
||||
#[derive(Debug)]
|
||||
@@ -72,13 +105,25 @@ impl WorkspaceEditTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a series of [`Fixes`] for a text or notebook document.
|
||||
pub(crate) fn set_fixes_for_document(
|
||||
&mut self,
|
||||
fixes: Fixes,
|
||||
version: DocumentVersion,
|
||||
) -> crate::Result<()> {
|
||||
for (uri, edits) in fixes {
|
||||
self.set_edits_for_document(uri, version, edits)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the edits made to a specific document. This should only be called
|
||||
/// once for each document `uri`, and will fail if this is called for the same `uri`
|
||||
/// multiple times.
|
||||
pub(crate) fn set_edits_for_document(
|
||||
&mut self,
|
||||
uri: lsp_types::Url,
|
||||
version: DocumentVersion,
|
||||
_version: DocumentVersion,
|
||||
edits: Vec<lsp_types::TextEdit>,
|
||||
) -> crate::Result<()> {
|
||||
match self {
|
||||
@@ -94,7 +139,8 @@ impl WorkspaceEditTracker {
|
||||
document_edits.push(lsp_types::TextDocumentEdit {
|
||||
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
||||
uri,
|
||||
version: Some(version),
|
||||
// TODO(jane): Re-enable versioned edits after investigating whether it could work with notebook cells
|
||||
version: None,
|
||||
},
|
||||
edits: edits.into_iter().map(lsp_types::OneOf::Left).collect(),
|
||||
});
|
||||
|
||||
@@ -7,10 +7,10 @@ use super::RangeExt;
|
||||
|
||||
pub(crate) type DocumentVersion = i32;
|
||||
|
||||
/// The state for an individual document in the server. Stays up-to-date
|
||||
/// The state of an individual document in the server. Stays up-to-date
|
||||
/// with changes made by the user, including unsaved changes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Document {
|
||||
pub struct TextDocument {
|
||||
/// The string contents of the document.
|
||||
contents: String,
|
||||
/// A computed line index for the document. This should always reflect
|
||||
@@ -22,7 +22,7 @@ pub struct Document {
|
||||
version: DocumentVersion,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
impl TextDocument {
|
||||
pub fn new(contents: String, version: DocumentVersion) -> Self {
|
||||
let index = LineIndex::from_source_text(&contents);
|
||||
Self {
|
||||
|
||||
202
crates/ruff_server/src/edit/notebook.rs
Normal file
202
crates/ruff_server/src/edit/notebook.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use std::{collections::HashMap, hash::BuildHasherDefault};
|
||||
|
||||
use anyhow::Ok;
|
||||
use lsp_types::{NotebookCellKind, Url};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{PositionEncoding, TextDocument};
|
||||
|
||||
use super::DocumentVersion;
|
||||
|
||||
pub(super) type CellId = usize;
|
||||
|
||||
/// The state of a notebook document in the server. Contains an array of cells whose
|
||||
/// contents are internally represented by [`TextDocument`]s.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct NotebookDocument {
|
||||
cells: Vec<NotebookCell>,
|
||||
metadata: ruff_notebook::RawNotebookMetadata,
|
||||
version: DocumentVersion,
|
||||
// Used to quickly find the index of a cell for a given URL.
|
||||
cell_index: FxHashMap<lsp_types::Url, CellId>,
|
||||
}
|
||||
|
||||
/// A single cell within a notebook, which has text contents represented as a `TextDocument`.
|
||||
#[derive(Clone, Debug)]
|
||||
struct NotebookCell {
|
||||
url: Url,
|
||||
kind: NotebookCellKind,
|
||||
document: TextDocument,
|
||||
}
|
||||
|
||||
impl NotebookDocument {
|
||||
pub(crate) fn new(
|
||||
version: DocumentVersion,
|
||||
cells: Vec<lsp_types::NotebookCell>,
|
||||
metadata: serde_json::Map<String, serde_json::Value>,
|
||||
cell_documents: Vec<lsp_types::TextDocumentItem>,
|
||||
) -> crate::Result<Self> {
|
||||
let mut cell_contents: FxHashMap<_, _> = cell_documents
|
||||
.into_iter()
|
||||
.map(|document| (document.uri, document.text))
|
||||
.collect();
|
||||
|
||||
let cells: Vec<_> = cells
|
||||
.into_iter()
|
||||
.map(|cell| {
|
||||
let contents = cell_contents.remove(&cell.document).unwrap_or_default();
|
||||
NotebookCell::new(cell, contents, version)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
version,
|
||||
cell_index: Self::make_cell_index(cells.as_slice()),
|
||||
metadata: serde_json::from_value(serde_json::Value::Object(metadata))?,
|
||||
cells,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates a pseudo-representation of a notebook that lacks per-cell metadata and contextual information
|
||||
/// but should still work with Ruff's linter.
|
||||
pub(crate) fn make_ruff_notebook(&self) -> ruff_notebook::Notebook {
|
||||
let cells = self
|
||||
.cells
|
||||
.iter()
|
||||
.map(|cell| match cell.kind {
|
||||
NotebookCellKind::Code => ruff_notebook::Cell::Code(ruff_notebook::CodeCell {
|
||||
execution_count: None,
|
||||
id: None,
|
||||
metadata: serde_json::Value::Null,
|
||||
outputs: vec![],
|
||||
source: ruff_notebook::SourceValue::String(
|
||||
cell.document.contents().to_string(),
|
||||
),
|
||||
}),
|
||||
NotebookCellKind::Markup => {
|
||||
ruff_notebook::Cell::Markdown(ruff_notebook::MarkdownCell {
|
||||
attachments: None,
|
||||
id: None,
|
||||
metadata: serde_json::Value::Null,
|
||||
source: ruff_notebook::SourceValue::String(
|
||||
cell.document.contents().to_string(),
|
||||
),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let raw_notebook = ruff_notebook::RawNotebook {
|
||||
cells,
|
||||
metadata: self.metadata.clone(),
|
||||
nbformat: 4,
|
||||
nbformat_minor: 5,
|
||||
};
|
||||
|
||||
ruff_notebook::Notebook::from_raw_notebook(raw_notebook, false)
|
||||
.unwrap_or_else(|err| panic!("Server notebook document could not be converted to Ruff's notebook document format: {err}"))
|
||||
}
|
||||
|
||||
pub(crate) fn update(
|
||||
&mut self,
|
||||
cells: Option<lsp_types::NotebookDocumentCellChange>,
|
||||
metadata_change: Option<serde_json::Map<String, serde_json::Value>>,
|
||||
version: DocumentVersion,
|
||||
encoding: PositionEncoding,
|
||||
) -> crate::Result<()> {
|
||||
self.version = version;
|
||||
|
||||
if let Some(lsp_types::NotebookDocumentCellChange {
|
||||
structure,
|
||||
data,
|
||||
text_content,
|
||||
}) = cells
|
||||
{
|
||||
if let Some(structure) = structure {
|
||||
let start = structure.array.start as usize;
|
||||
let delete = structure.array.delete_count as usize;
|
||||
if delete > 0 {
|
||||
for cell in self.cells.drain(start..start + delete) {
|
||||
self.cell_index.remove(&cell.url);
|
||||
}
|
||||
}
|
||||
for cell in structure.array.cells.into_iter().flatten().rev() {
|
||||
self.cells
|
||||
.insert(start, NotebookCell::new(cell, String::new(), version));
|
||||
}
|
||||
|
||||
// register any new cells in the index and update existing ones that came after the insertion
|
||||
for (i, cell) in self.cells.iter().enumerate().skip(start) {
|
||||
self.cell_index.insert(cell.url.clone(), i);
|
||||
}
|
||||
}
|
||||
if let Some(cell_data) = data {
|
||||
for cell in cell_data {
|
||||
if let Some(existing_cell) = self.cell_by_uri_mut(&cell.document) {
|
||||
existing_cell.kind = cell.kind;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(content_changes) = text_content {
|
||||
for content_change in content_changes {
|
||||
if let Some(cell) = self.cell_by_uri_mut(&content_change.document.uri) {
|
||||
cell.document
|
||||
.apply_changes(content_change.changes, version, encoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(metadata_change) = metadata_change {
|
||||
self.metadata = serde_json::from_value(serde_json::Value::Object(metadata_change))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the current version of the notebook document.
|
||||
pub(crate) fn version(&self) -> DocumentVersion {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Get the URI for a cell by its index within the cell array.
|
||||
pub(crate) fn cell_uri_by_index(&self, index: CellId) -> Option<&lsp_types::Url> {
|
||||
self.cells.get(index).map(|cell| &cell.url)
|
||||
}
|
||||
|
||||
/// Get the text document representing the contents of a cell by the cell URI.
|
||||
pub(crate) fn cell_document_by_uri(&self, uri: &lsp_types::Url) -> Option<&TextDocument> {
|
||||
self.cells
|
||||
.get(*self.cell_index.get(uri)?)
|
||||
.map(|cell| &cell.document)
|
||||
}
|
||||
|
||||
/// Returns a list of cell URIs in the order they appear in the array.
|
||||
pub(crate) fn urls(&self) -> impl Iterator<Item = &lsp_types::Url> {
|
||||
self.cells.iter().map(|cell| &cell.url)
|
||||
}
|
||||
|
||||
fn cell_by_uri_mut(&mut self, uri: &lsp_types::Url) -> Option<&mut NotebookCell> {
|
||||
self.cells.get_mut(*self.cell_index.get(uri)?)
|
||||
}
|
||||
|
||||
fn make_cell_index(cells: &[NotebookCell]) -> FxHashMap<lsp_types::Url, CellId> {
|
||||
let mut index =
|
||||
HashMap::with_capacity_and_hasher(cells.len(), BuildHasherDefault::default());
|
||||
for (i, cell) in cells.iter().enumerate() {
|
||||
index.insert(cell.url.clone(), i);
|
||||
}
|
||||
index
|
||||
}
|
||||
}
|
||||
|
||||
impl NotebookCell {
|
||||
pub(crate) fn new(
|
||||
cell: lsp_types::NotebookCell,
|
||||
contents: String,
|
||||
version: DocumentVersion,
|
||||
) -> Self {
|
||||
Self {
|
||||
url: cell.document,
|
||||
kind: cell.kind,
|
||||
document: TextDocument::new(contents, version),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
use super::notebook;
|
||||
use super::PositionEncoding;
|
||||
use lsp_types as types;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_source_file::{LineIndex, SourceLocation};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
pub(crate) struct NotebookRange {
|
||||
pub(crate) cell: notebook::CellId,
|
||||
pub(crate) range: types::Range,
|
||||
}
|
||||
|
||||
pub(crate) trait RangeExt {
|
||||
fn to_text_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding)
|
||||
-> TextRange;
|
||||
@@ -11,6 +18,13 @@ pub(crate) trait RangeExt {
|
||||
|
||||
pub(crate) trait ToRangeExt {
|
||||
fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range;
|
||||
fn to_notebook_range(
|
||||
&self,
|
||||
text: &str,
|
||||
source_index: &LineIndex,
|
||||
notebook_index: &NotebookIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> NotebookRange;
|
||||
}
|
||||
|
||||
fn u32_index_to_usize(index: u32) -> usize {
|
||||
@@ -83,8 +97,54 @@ impl RangeExt for lsp_types::Range {
|
||||
impl ToRangeExt for TextRange {
|
||||
fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range {
|
||||
types::Range {
|
||||
start: offset_to_position(self.start(), text, index, encoding),
|
||||
end: offset_to_position(self.end(), text, index, encoding),
|
||||
start: source_location_to_position(&offset_to_source_location(
|
||||
self.start(),
|
||||
text,
|
||||
index,
|
||||
encoding,
|
||||
)),
|
||||
end: source_location_to_position(&offset_to_source_location(
|
||||
self.end(),
|
||||
text,
|
||||
index,
|
||||
encoding,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_notebook_range(
|
||||
&self,
|
||||
text: &str,
|
||||
source_index: &LineIndex,
|
||||
notebook_index: &NotebookIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> NotebookRange {
|
||||
let start = offset_to_source_location(self.start(), text, source_index, encoding);
|
||||
let mut end = offset_to_source_location(self.end(), text, source_index, encoding);
|
||||
let starting_cell = notebook_index.cell(start.row);
|
||||
|
||||
// weird edge case here - if the end of the range is where the newline after the cell got added (making it 'out of bounds')
|
||||
// we need to move it one character back (which should place it at the end of the last line).
|
||||
// we test this by checking if the ending offset is in a different (or nonexistent) cell compared to the cell of the starting offset.
|
||||
if notebook_index.cell(end.row) != starting_cell {
|
||||
end.row = end.row.saturating_sub(1);
|
||||
end.column = offset_to_source_location(
|
||||
self.end().checked_sub(1.into()).unwrap_or_default(),
|
||||
text,
|
||||
source_index,
|
||||
encoding,
|
||||
)
|
||||
.column;
|
||||
}
|
||||
|
||||
let start = source_location_to_position(¬ebook_index.translate_location(&start));
|
||||
let end = source_location_to_position(¬ebook_index.translate_location(&end));
|
||||
|
||||
NotebookRange {
|
||||
cell: starting_cell
|
||||
.map(OneIndexed::to_zero_indexed)
|
||||
.unwrap_or_default(),
|
||||
range: types::Range { start, end },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,13 +171,13 @@ fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize {
|
||||
utf8_code_unit_offset
|
||||
}
|
||||
|
||||
fn offset_to_position(
|
||||
fn offset_to_source_location(
|
||||
offset: TextSize,
|
||||
text: &str,
|
||||
index: &LineIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> types::Position {
|
||||
let location = match encoding {
|
||||
) -> SourceLocation {
|
||||
match encoding {
|
||||
PositionEncoding::UTF8 => {
|
||||
let row = index.line_index(offset);
|
||||
let column = offset - index.line_start(row, text);
|
||||
@@ -143,8 +203,10 @@ fn offset_to_position(
|
||||
}
|
||||
}
|
||||
PositionEncoding::UTF32 => index.source_location(offset, text),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn source_location_to_position(location: &SourceLocation) -> types::Position {
|
||||
types::Position {
|
||||
line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"),
|
||||
character: u32::try_from(location.column.to_zero_indexed())
|
||||
|
||||
@@ -2,28 +2,29 @@ use ruff_linter::{
|
||||
linter::{FixerResult, LinterResult},
|
||||
packaging::detect_package_root,
|
||||
settings::{flags, types::UnsafeFixes, LinterSettings},
|
||||
source_kind::SourceKind,
|
||||
};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_notebook::SourceValue;
|
||||
use ruff_source_file::LineIndex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
edit::{Replacement, ToRangeExt},
|
||||
session::DocumentQuery,
|
||||
PositionEncoding,
|
||||
};
|
||||
|
||||
/// A simultaneous fix made across a single text document or among an arbitrary
|
||||
/// number of notebook cells.
|
||||
pub(crate) type Fixes = FxHashMap<lsp_types::Url, Vec<lsp_types::TextEdit>>;
|
||||
|
||||
pub(crate) fn fix_all(
|
||||
document: &crate::edit::Document,
|
||||
document_url: &lsp_types::Url,
|
||||
query: &DocumentQuery,
|
||||
linter_settings: &LinterSettings,
|
||||
encoding: PositionEncoding,
|
||||
) -> crate::Result<Vec<lsp_types::TextEdit>> {
|
||||
let source = document.contents();
|
||||
|
||||
let document_path = document_url
|
||||
.to_file_path()
|
||||
.expect("document URL should be a valid file path");
|
||||
) -> crate::Result<Fixes> {
|
||||
let document_path = query.file_path();
|
||||
let source_kind = query.make_source_kind();
|
||||
|
||||
let package = detect_package_root(
|
||||
document_path
|
||||
@@ -32,10 +33,7 @@ pub(crate) fn fix_all(
|
||||
&linter_settings.namespace_packages,
|
||||
);
|
||||
|
||||
let source_type = PySourceType::default();
|
||||
|
||||
// TODO(jane): Support Jupyter Notebooks
|
||||
let source_kind = SourceKind::Python(source.to_string());
|
||||
let source_type = query.source_type();
|
||||
|
||||
// We need to iteratively apply all safe fixes onto a single file and then
|
||||
// create a diff between the modified file and the original source to use as a single workspace
|
||||
@@ -48,7 +46,7 @@ pub(crate) fn fix_all(
|
||||
result: LinterResult { error, .. },
|
||||
..
|
||||
} = ruff_linter::linter::lint_fix(
|
||||
&document_path,
|
||||
document_path,
|
||||
package,
|
||||
flags::Noqa::Enabled,
|
||||
UnsafeFixes::Disabled,
|
||||
@@ -66,27 +64,79 @@ pub(crate) fn fix_all(
|
||||
|
||||
// fast path: if `transformed` is still borrowed, no changes were made and we can return early
|
||||
if let Cow::Borrowed(_) = transformed {
|
||||
return Ok(vec![]);
|
||||
return Ok(Fixes::default());
|
||||
}
|
||||
|
||||
let modified = transformed.source_code();
|
||||
if let (Some(source_notebook), Some(modified_notebook)) =
|
||||
(source_kind.as_ipy_notebook(), transformed.as_ipy_notebook())
|
||||
{
|
||||
fn cell_source(cell: &ruff_notebook::Cell) -> String {
|
||||
match cell.source() {
|
||||
SourceValue::String(string) => string.clone(),
|
||||
SourceValue::StringArray(array) => array.join(""),
|
||||
}
|
||||
}
|
||||
|
||||
let modified_index = LineIndex::from_source_text(modified);
|
||||
let Some(notebook) = query.as_notebook() else {
|
||||
anyhow::bail!("Notebook document expected from notebook source kind");
|
||||
};
|
||||
let mut fixes = Fixes::default();
|
||||
for ((source, modified), url) in source_notebook
|
||||
.cells()
|
||||
.iter()
|
||||
.map(cell_source)
|
||||
.zip(modified_notebook.cells().iter().map(cell_source))
|
||||
.zip(notebook.urls())
|
||||
{
|
||||
let source_index = LineIndex::from_source_text(&source);
|
||||
let modified_index = LineIndex::from_source_text(&modified);
|
||||
|
||||
let source_index = document.index();
|
||||
let Replacement {
|
||||
source_range,
|
||||
modified_range,
|
||||
} = Replacement::between(
|
||||
&source,
|
||||
source_index.line_starts(),
|
||||
&modified,
|
||||
modified_index.line_starts(),
|
||||
);
|
||||
|
||||
let Replacement {
|
||||
source_range,
|
||||
modified_range,
|
||||
} = Replacement::between(
|
||||
source,
|
||||
source_index.line_starts(),
|
||||
modified,
|
||||
modified_index.line_starts(),
|
||||
);
|
||||
fixes.insert(
|
||||
url.clone(),
|
||||
vec![lsp_types::TextEdit {
|
||||
range: source_range.to_range(
|
||||
source_kind.source_code(),
|
||||
&source_index,
|
||||
encoding,
|
||||
),
|
||||
new_text: modified[modified_range].to_owned(),
|
||||
}],
|
||||
);
|
||||
}
|
||||
Ok(fixes)
|
||||
} else {
|
||||
let source_index = LineIndex::from_source_text(source_kind.source_code());
|
||||
|
||||
Ok(vec![lsp_types::TextEdit {
|
||||
range: source_range.to_range(source, source_index, encoding),
|
||||
new_text: modified[modified_range].to_owned(),
|
||||
}])
|
||||
let modified = transformed.source_code();
|
||||
let modified_index = LineIndex::from_source_text(modified);
|
||||
|
||||
let Replacement {
|
||||
source_range,
|
||||
modified_range,
|
||||
} = Replacement::between(
|
||||
source_kind.source_code(),
|
||||
source_index.line_starts(),
|
||||
modified,
|
||||
modified_index.line_starts(),
|
||||
);
|
||||
Ok([(
|
||||
query.make_key().into_url(),
|
||||
vec![lsp_types::TextEdit {
|
||||
range: source_range.to_range(source_kind.source_code(), &source_index, encoding),
|
||||
new_text: modified[modified_range].to_owned(),
|
||||
}],
|
||||
)]
|
||||
.into_iter()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
use ruff_formatter::PrintedRange;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_formatter::format_module_source;
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::edit::Document;
|
||||
use crate::edit::TextDocument;
|
||||
|
||||
pub(crate) fn format(
|
||||
document: &Document,
|
||||
document: &TextDocument,
|
||||
source_type: PySourceType,
|
||||
formatter_settings: &FormatterSettings,
|
||||
) -> crate::Result<String> {
|
||||
// TODO(jane): support Jupyter Notebook
|
||||
let format_options = formatter_settings
|
||||
.to_format_options(ruff_python_ast::PySourceType::Python, document.contents());
|
||||
let format_options = formatter_settings.to_format_options(source_type, document.contents());
|
||||
let formatted = format_module_source(document.contents(), format_options)?;
|
||||
Ok(formatted.into_code())
|
||||
}
|
||||
|
||||
pub(crate) fn format_range(
|
||||
document: &Document,
|
||||
document: &TextDocument,
|
||||
source_type: PySourceType,
|
||||
formatter_settings: &FormatterSettings,
|
||||
range: TextRange,
|
||||
) -> crate::Result<PrintedRange> {
|
||||
// TODO(jane): support Jupyter Notebook
|
||||
let format_options = formatter_settings
|
||||
.to_format_options(ruff_python_ast::PySourceType::Python, document.contents());
|
||||
let format_options = formatter_settings.to_format_options(source_type, document.contents());
|
||||
|
||||
Ok(ruff_python_formatter::format_range(
|
||||
document.contents(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## The Ruff Language Server
|
||||
|
||||
pub use edit::{Document, PositionEncoding};
|
||||
pub use edit::{PositionEncoding, TextDocument};
|
||||
use lsp_types::CodeActionKind;
|
||||
pub use server::Server;
|
||||
|
||||
@@ -19,6 +19,10 @@ pub(crate) const DIAGNOSTIC_NAME: &str = "Ruff";
|
||||
pub(crate) const SOURCE_FIX_ALL_RUFF: CodeActionKind = CodeActionKind::new("source.fixAll.ruff");
|
||||
pub(crate) const SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind =
|
||||
CodeActionKind::new("source.organizeImports.ruff");
|
||||
pub(crate) const NOTEBOOK_SOURCE_FIX_ALL_RUFF: CodeActionKind =
|
||||
CodeActionKind::new("notebook.source.fixAll.ruff");
|
||||
pub(crate) const NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind =
|
||||
CodeActionKind::new("notebook.source.organizeImports.ruff");
|
||||
|
||||
/// A common result type used in most cases where a
|
||||
/// result type is needed.
|
||||
|
||||
@@ -10,26 +10,32 @@ use ruff_linter::{
|
||||
settings::{flags, LinterSettings},
|
||||
source_kind::SourceKind,
|
||||
};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::AsMode;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_source_file::{LineIndex, Locator};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{edit::ToRangeExt, PositionEncoding, DIAGNOSTIC_NAME};
|
||||
use crate::{
|
||||
edit::{NotebookRange, ToRangeExt},
|
||||
session::DocumentQuery,
|
||||
PositionEncoding, DIAGNOSTIC_NAME,
|
||||
};
|
||||
|
||||
/// This is serialized on the diagnostic `data` field.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub(crate) struct AssociatedDiagnosticData {
|
||||
pub(crate) kind: DiagnosticKind,
|
||||
/// A possible fix for the associated diagnostic.
|
||||
pub(crate) fix: Option<Fix>,
|
||||
/// Edits to fix the diagnostic. If this is empty, a fix
|
||||
/// does not exist.
|
||||
pub(crate) edits: Vec<lsp_types::TextEdit>,
|
||||
/// The NOQA code for the diagnostic.
|
||||
pub(crate) code: String,
|
||||
/// Possible edit to add a `noqa` comment which will disable this diagnostic.
|
||||
pub(crate) noqa_edit: Option<ruff_diagnostics::Edit>,
|
||||
pub(crate) noqa_edit: Option<lsp_types::TextEdit>,
|
||||
}
|
||||
|
||||
/// Describes a fix for `fixed_diagnostic` that may have quick fix
|
||||
@@ -49,18 +55,16 @@ pub(crate) struct DiagnosticFix {
|
||||
pub(crate) noqa_edit: Option<lsp_types::TextEdit>,
|
||||
}
|
||||
|
||||
/// A series of diagnostics across a single text document or an arbitrary number of notebook cells.
|
||||
pub(crate) type Diagnostics = FxHashMap<lsp_types::Url, Vec<lsp_types::Diagnostic>>;
|
||||
|
||||
pub(crate) fn check(
|
||||
document: &crate::edit::Document,
|
||||
document_url: &lsp_types::Url,
|
||||
query: &DocumentQuery,
|
||||
linter_settings: &LinterSettings,
|
||||
encoding: PositionEncoding,
|
||||
) -> Vec<lsp_types::Diagnostic> {
|
||||
let contents = document.contents();
|
||||
let index = document.index().clone();
|
||||
|
||||
let document_path = document_url
|
||||
.to_file_path()
|
||||
.expect("document URL should be a valid file path");
|
||||
) -> Diagnostics {
|
||||
let document_path = query.file_path();
|
||||
let source_kind = query.make_source_kind();
|
||||
|
||||
let package = detect_package_root(
|
||||
document_path
|
||||
@@ -69,16 +73,15 @@ pub(crate) fn check(
|
||||
&linter_settings.namespace_packages,
|
||||
);
|
||||
|
||||
let source_type = PySourceType::default();
|
||||
|
||||
// TODO(jane): Support Jupyter Notebooks
|
||||
let source_kind = SourceKind::Python(contents.to_string());
|
||||
let source_type = query.source_type();
|
||||
|
||||
// Tokenize once.
|
||||
let tokens = ruff_python_parser::tokenize(contents, source_type.as_mode());
|
||||
let tokens = ruff_python_parser::tokenize(source_kind.source_code(), source_type.as_mode());
|
||||
|
||||
let index = LineIndex::from_source_text(source_kind.source_code());
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
let locator = Locator::with_index(contents, index);
|
||||
let locator = Locator::with_index(source_kind.source_code(), index.clone());
|
||||
|
||||
// Detect the current code style (lazily).
|
||||
let stylist = Stylist::from_tokens(&tokens, &locator);
|
||||
@@ -90,10 +93,8 @@ pub(crate) fn check(
|
||||
let directives = extract_directives(&tokens, Flags::all(), &locator, &indexer);
|
||||
|
||||
// Generate checks.
|
||||
let LinterResult {
|
||||
data: diagnostics, ..
|
||||
} = check_path(
|
||||
&document_path,
|
||||
let LinterResult { data, .. } = check_path(
|
||||
document_path,
|
||||
package,
|
||||
&locator,
|
||||
&stylist,
|
||||
@@ -107,8 +108,8 @@ pub(crate) fn check(
|
||||
);
|
||||
|
||||
let noqa_edits = generate_noqa_edits(
|
||||
&document_path,
|
||||
diagnostics.as_slice(),
|
||||
document_path,
|
||||
data.as_slice(),
|
||||
&locator,
|
||||
indexer.comment_ranges(),
|
||||
&linter_settings.external,
|
||||
@@ -116,16 +117,47 @@ pub(crate) fn check(
|
||||
stylist.line_ending(),
|
||||
);
|
||||
|
||||
diagnostics
|
||||
let mut diagnostics = Diagnostics::default();
|
||||
|
||||
// Populates all relevant URLs with an empty diagnostic list.
|
||||
// This ensures that documents without diagnostics still get updated.
|
||||
if let Some(notebook) = query.as_notebook() {
|
||||
for url in notebook.urls() {
|
||||
diagnostics.entry(url.clone()).or_default();
|
||||
}
|
||||
} else {
|
||||
diagnostics.entry(query.make_key().into_url()).or_default();
|
||||
}
|
||||
|
||||
let lsp_diagnostics = data
|
||||
.into_iter()
|
||||
.zip(noqa_edits)
|
||||
.map(|(diagnostic, noqa_edit)| to_lsp_diagnostic(diagnostic, noqa_edit, document, encoding))
|
||||
.collect()
|
||||
.map(|(diagnostic, noqa_edit)| {
|
||||
to_lsp_diagnostic(diagnostic, &noqa_edit, &source_kind, &index, encoding)
|
||||
});
|
||||
|
||||
if let Some(notebook) = query.as_notebook() {
|
||||
for (index, diagnostic) in lsp_diagnostics {
|
||||
let Some(uri) = notebook.cell_uri_by_index(index) else {
|
||||
tracing::warn!("Unable to find notebook cell at index {index}.");
|
||||
continue;
|
||||
};
|
||||
diagnostics.entry(uri.clone()).or_default().push(diagnostic);
|
||||
}
|
||||
} else {
|
||||
for (_, diagnostic) in lsp_diagnostics {
|
||||
diagnostics
|
||||
.entry(query.make_key().into_url())
|
||||
.or_default()
|
||||
.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics
|
||||
}
|
||||
|
||||
/// Converts LSP diagnostics to a list of `DiagnosticFix`es by deserializing associated data on each diagnostic.
|
||||
pub(crate) fn fixes_for_diagnostics(
|
||||
document: &crate::edit::Document,
|
||||
encoding: PositionEncoding,
|
||||
diagnostics: Vec<lsp_types::Diagnostic>,
|
||||
) -> crate::Result<Vec<DiagnosticFix>> {
|
||||
diagnostics
|
||||
@@ -139,36 +171,6 @@ pub(crate) fn fixes_for_diagnostics(
|
||||
serde_json::from_value(data).map_err(|err| {
|
||||
anyhow::anyhow!("failed to deserialize diagnostic data: {err}")
|
||||
})?;
|
||||
let edits = associated_data
|
||||
.fix
|
||||
.map(|fix| {
|
||||
fix.edits()
|
||||
.iter()
|
||||
.map(|edit| lsp_types::TextEdit {
|
||||
range: edit.range().to_range(
|
||||
document.contents(),
|
||||
document.index(),
|
||||
encoding,
|
||||
),
|
||||
new_text: edit.content().unwrap_or_default().to_string(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let noqa_edit =
|
||||
associated_data
|
||||
.noqa_edit
|
||||
.as_ref()
|
||||
.map(|noqa_edit| lsp_types::TextEdit {
|
||||
range: noqa_edit.range().to_range(
|
||||
document.contents(),
|
||||
document.index(),
|
||||
encoding,
|
||||
),
|
||||
new_text: noqa_edit.content().unwrap_or_default().to_string(),
|
||||
});
|
||||
|
||||
Ok(Some(DiagnosticFix {
|
||||
fixed_diagnostic,
|
||||
code: associated_data.code,
|
||||
@@ -176,22 +178,28 @@ pub(crate) fn fixes_for_diagnostics(
|
||||
.kind
|
||||
.suggestion
|
||||
.unwrap_or(associated_data.kind.name),
|
||||
edits,
|
||||
noqa_edit,
|
||||
noqa_edit: associated_data.noqa_edit,
|
||||
edits: associated_data.edits,
|
||||
}))
|
||||
})
|
||||
.filter_map(crate::Result::transpose)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generates an LSP diagnostic with an associated cell index for the diagnostic to go in.
|
||||
/// If the source kind is a text document, the cell index will always be `0`.
|
||||
fn to_lsp_diagnostic(
|
||||
diagnostic: Diagnostic,
|
||||
noqa_edit: Option<Edit>,
|
||||
document: &crate::edit::Document,
|
||||
noqa_edit: &Option<Edit>,
|
||||
source_kind: &SourceKind,
|
||||
index: &LineIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> lsp_types::Diagnostic {
|
||||
) -> (usize, lsp_types::Diagnostic) {
|
||||
let Diagnostic {
|
||||
kind, range, fix, ..
|
||||
kind,
|
||||
range: diagnostic_range,
|
||||
fix,
|
||||
..
|
||||
} = diagnostic;
|
||||
|
||||
let rule = kind.rule();
|
||||
@@ -200,11 +208,24 @@ fn to_lsp_diagnostic(
|
||||
|
||||
let data = (fix.is_some() || noqa_edit.is_some())
|
||||
.then(|| {
|
||||
serde_json::to_value(&AssociatedDiagnosticData {
|
||||
let edits = fix
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(Fix::edits)
|
||||
.map(|edit| lsp_types::TextEdit {
|
||||
range: diagnostic_edit_range(edit.range(), source_kind, index, encoding),
|
||||
new_text: edit.content().unwrap_or_default().to_string(),
|
||||
})
|
||||
.collect();
|
||||
let noqa_edit = noqa_edit.as_ref().map(|noqa_edit| lsp_types::TextEdit {
|
||||
range: diagnostic_edit_range(noqa_edit.range(), source_kind, index, encoding),
|
||||
new_text: noqa_edit.content().unwrap_or_default().to_string(),
|
||||
});
|
||||
serde_json::to_value(AssociatedDiagnosticData {
|
||||
kind: kind.clone(),
|
||||
fix,
|
||||
code: rule.noqa_code().to_string(),
|
||||
noqa_edit,
|
||||
edits,
|
||||
code: rule.noqa_code().to_string(),
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
@@ -212,20 +233,53 @@ fn to_lsp_diagnostic(
|
||||
|
||||
let code = rule.noqa_code().to_string();
|
||||
|
||||
lsp_types::Diagnostic {
|
||||
range: range.to_range(document.contents(), document.index(), encoding),
|
||||
severity: Some(severity(&code)),
|
||||
tags: tags(&code),
|
||||
code: Some(lsp_types::NumberOrString::String(code)),
|
||||
code_description: rule.url().and_then(|url| {
|
||||
Some(lsp_types::CodeDescription {
|
||||
href: lsp_types::Url::parse(&url).ok()?,
|
||||
})
|
||||
}),
|
||||
source: Some(DIAGNOSTIC_NAME.into()),
|
||||
message: kind.body,
|
||||
related_information: None,
|
||||
data,
|
||||
let range: lsp_types::Range;
|
||||
let cell: usize;
|
||||
|
||||
if let Some(notebook_index) = source_kind.as_ipy_notebook().map(Notebook::index) {
|
||||
NotebookRange { cell, range } = diagnostic_range.to_notebook_range(
|
||||
source_kind.source_code(),
|
||||
index,
|
||||
notebook_index,
|
||||
encoding,
|
||||
);
|
||||
} else {
|
||||
cell = usize::default();
|
||||
range = diagnostic_range.to_range(source_kind.source_code(), index, encoding);
|
||||
}
|
||||
|
||||
(
|
||||
cell,
|
||||
lsp_types::Diagnostic {
|
||||
range,
|
||||
severity: Some(severity(&code)),
|
||||
tags: tags(&code),
|
||||
code: Some(lsp_types::NumberOrString::String(code)),
|
||||
code_description: rule.url().and_then(|url| {
|
||||
Some(lsp_types::CodeDescription {
|
||||
href: lsp_types::Url::parse(&url).ok()?,
|
||||
})
|
||||
}),
|
||||
source: Some(DIAGNOSTIC_NAME.into()),
|
||||
message: kind.body,
|
||||
related_information: None,
|
||||
data,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn diagnostic_edit_range(
|
||||
range: TextRange,
|
||||
source_kind: &SourceKind,
|
||||
index: &LineIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> lsp_types::Range {
|
||||
if let Some(notebook_index) = source_kind.as_ipy_notebook().map(Notebook::index) {
|
||||
range
|
||||
.to_notebook_range(source_kind.source_code(), index, notebook_index, encoding)
|
||||
.range
|
||||
} else {
|
||||
range.to_range(source_kind.source_code(), index, encoding)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Scheduling, I/O, and API endpoints.
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use lsp_server as lsp;
|
||||
use lsp_types as types;
|
||||
@@ -10,6 +11,9 @@ use types::CodeActionOptions;
|
||||
use types::DiagnosticOptions;
|
||||
use types::DidChangeWatchedFilesRegistrationOptions;
|
||||
use types::FileSystemWatcher;
|
||||
use types::NotebookCellSelector;
|
||||
use types::NotebookDocumentSyncOptions;
|
||||
use types::NotebookSelector;
|
||||
use types::OneOf;
|
||||
use types::TextDocumentSyncCapability;
|
||||
use types::TextDocumentSyncKind;
|
||||
@@ -65,28 +69,33 @@ impl Server {
|
||||
let AllSettings {
|
||||
global_settings,
|
||||
mut workspace_settings,
|
||||
} = AllSettings::from_value(init_params.initialization_options.unwrap_or_default());
|
||||
} = AllSettings::from_value(
|
||||
init_params
|
||||
.initialization_options
|
||||
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())),
|
||||
);
|
||||
|
||||
let mut workspace_for_uri = |uri| {
|
||||
let mut workspace_for_path = |path: PathBuf| {
|
||||
let Some(workspace_settings) = workspace_settings.as_mut() else {
|
||||
return (uri, ClientSettings::default());
|
||||
return (path, ClientSettings::default());
|
||||
};
|
||||
let settings = workspace_settings.remove(&uri).unwrap_or_else(|| {
|
||||
tracing::warn!("No workspace settings found for {uri}");
|
||||
let settings = workspace_settings.remove(&path).unwrap_or_else(|| {
|
||||
tracing::warn!("No workspace settings found for {}", path.display());
|
||||
ClientSettings::default()
|
||||
});
|
||||
(uri, settings)
|
||||
(path, settings)
|
||||
};
|
||||
|
||||
let workspaces = init_params
|
||||
.workspace_folders
|
||||
.filter(|folders| !folders.is_empty())
|
||||
.map(|folders| folders.into_iter().map(|folder| {
|
||||
workspace_for_uri(folder.uri)
|
||||
workspace_for_path(folder.uri.to_file_path().unwrap())
|
||||
}).collect())
|
||||
.or_else(|| {
|
||||
tracing::debug!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace...");
|
||||
tracing::warn!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace...");
|
||||
let uri = types::Url::from_file_path(std::env::current_dir().ok()?).ok()?;
|
||||
Some(vec![workspace_for_uri(uri)])
|
||||
Some(vec![workspace_for_path(uri.to_file_path().unwrap())])
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Failed to get the current working directory while creating a default workspace.")
|
||||
@@ -100,7 +109,7 @@ impl Server {
|
||||
position_encoding,
|
||||
global_settings,
|
||||
workspaces,
|
||||
)?,
|
||||
),
|
||||
client_capabilities,
|
||||
})
|
||||
}
|
||||
@@ -169,10 +178,14 @@ impl Server {
|
||||
watchers: vec![
|
||||
FileSystemWatcher {
|
||||
glob_pattern: types::GlobPattern::String(
|
||||
"**/.?ruff.toml".into(),
|
||||
"**/.ruff.toml".into(),
|
||||
),
|
||||
kind: None,
|
||||
},
|
||||
FileSystemWatcher {
|
||||
glob_pattern: types::GlobPattern::String("**/ruff.toml".into()),
|
||||
kind: None,
|
||||
},
|
||||
FileSystemWatcher {
|
||||
glob_pattern: types::GlobPattern::String(
|
||||
"**/pyproject.toml".into(),
|
||||
@@ -252,6 +265,16 @@ impl Server {
|
||||
},
|
||||
)),
|
||||
hover_provider: Some(types::HoverProviderCapability::Simple(true)),
|
||||
notebook_document_sync: Some(types::OneOf::Left(NotebookDocumentSyncOptions {
|
||||
save: Some(false),
|
||||
notebook_selector: [NotebookSelector::ByCells {
|
||||
notebook: None,
|
||||
cells: vec![NotebookCellSelector {
|
||||
language: "python".to_string(),
|
||||
}],
|
||||
}]
|
||||
.to_vec(),
|
||||
})),
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||
TextDocumentSyncOptions {
|
||||
open_close: Some(true),
|
||||
@@ -278,8 +301,15 @@ pub(crate) enum SupportedCodeAction {
|
||||
SourceFixAll,
|
||||
/// Maps to `source.organizeImports` and `source.organizeImports.ruff` code action kinds.
|
||||
/// This is a source action that applies import sorting fixes to the currently open document.
|
||||
#[allow(dead_code)] // TODO: remove
|
||||
SourceOrganizeImports,
|
||||
/// Maps to the `notebook.source.fixAll` and `notebook.source.fixAll.ruff` code action kinds.
|
||||
/// This is a source action, specifically for notebooks, that applies all safe fixes
|
||||
/// to the currently open document.
|
||||
NotebookSourceFixAll,
|
||||
/// Maps to `source.organizeImports` and `source.organizeImports.ruff` code action kinds.
|
||||
/// This is a source action, specifically for notebooks, that applies import sorting fixes
|
||||
/// to the currently open document.
|
||||
NotebookSourceOrganizeImports,
|
||||
}
|
||||
|
||||
impl SupportedCodeAction {
|
||||
@@ -289,6 +319,8 @@ impl SupportedCodeAction {
|
||||
Self::QuickFix => CodeActionKind::QUICKFIX,
|
||||
Self::SourceFixAll => crate::SOURCE_FIX_ALL_RUFF,
|
||||
Self::SourceOrganizeImports => crate::SOURCE_ORGANIZE_IMPORTS_RUFF,
|
||||
Self::NotebookSourceFixAll => crate::NOTEBOOK_SOURCE_FIX_ALL_RUFF,
|
||||
Self::NotebookSourceOrganizeImports => crate::NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +336,8 @@ impl SupportedCodeAction {
|
||||
Self::QuickFix,
|
||||
Self::SourceFixAll,
|
||||
Self::SourceOrganizeImports,
|
||||
Self::NotebookSourceFixAll,
|
||||
Self::NotebookSourceOrganizeImports,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
@@ -84,6 +84,15 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
|
||||
}
|
||||
notification::DidClose::METHOD => local_notification_task::<notification::DidClose>(notif),
|
||||
notification::DidOpen::METHOD => local_notification_task::<notification::DidOpen>(notif),
|
||||
notification::DidOpenNotebook::METHOD => {
|
||||
local_notification_task::<notification::DidOpenNotebook>(notif)
|
||||
}
|
||||
notification::DidChangeNotebook::METHOD => {
|
||||
local_notification_task::<notification::DidChangeNotebook>(notif)
|
||||
}
|
||||
notification::DidCloseNotebook::METHOD => {
|
||||
local_notification_task::<notification::DidCloseNotebook>(notif)
|
||||
}
|
||||
method => {
|
||||
tracing::warn!("Received notification {method} which does not have a handler.");
|
||||
return Task::nothing();
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
use crate::{server::client::Notifier, session::DocumentSnapshot};
|
||||
use crate::{
|
||||
lint::Diagnostics,
|
||||
server::client::Notifier,
|
||||
session::{DocumentQuery, DocumentSnapshot},
|
||||
};
|
||||
|
||||
use super::LSPResult;
|
||||
|
||||
pub(super) fn generate_diagnostics(snapshot: &DocumentSnapshot) -> Vec<lsp_types::Diagnostic> {
|
||||
pub(super) fn generate_diagnostics(snapshot: &DocumentSnapshot) -> Diagnostics {
|
||||
if snapshot.client_settings().lint() {
|
||||
crate::lint::check(
|
||||
snapshot.document(),
|
||||
snapshot.url(),
|
||||
snapshot.settings().linter(),
|
||||
snapshot.query(),
|
||||
snapshot.query().settings().linter(),
|
||||
snapshot.encoding(),
|
||||
)
|
||||
} else {
|
||||
vec![]
|
||||
Diagnostics::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,31 +22,31 @@ pub(super) fn publish_diagnostics_for_document(
|
||||
snapshot: &DocumentSnapshot,
|
||||
notifier: &Notifier,
|
||||
) -> crate::server::Result<()> {
|
||||
let diagnostics = generate_diagnostics(snapshot);
|
||||
|
||||
notifier
|
||||
.notify::<lsp_types::notification::PublishDiagnostics>(
|
||||
lsp_types::PublishDiagnosticsParams {
|
||||
uri: snapshot.url().clone(),
|
||||
diagnostics,
|
||||
version: Some(snapshot.document().version()),
|
||||
},
|
||||
)
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
for (uri, diagnostics) in generate_diagnostics(snapshot) {
|
||||
notifier
|
||||
.notify::<lsp_types::notification::PublishDiagnostics>(
|
||||
lsp_types::PublishDiagnosticsParams {
|
||||
uri,
|
||||
diagnostics,
|
||||
version: Some(snapshot.query().version()),
|
||||
},
|
||||
)
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn clear_diagnostics_for_document(
|
||||
snapshot: &DocumentSnapshot,
|
||||
query: &DocumentQuery,
|
||||
notifier: &Notifier,
|
||||
) -> crate::server::Result<()> {
|
||||
notifier
|
||||
.notify::<lsp_types::notification::PublishDiagnostics>(
|
||||
lsp_types::PublishDiagnosticsParams {
|
||||
uri: snapshot.url().clone(),
|
||||
uri: query.make_key().into_url(),
|
||||
diagnostics: vec![],
|
||||
version: Some(snapshot.document().version()),
|
||||
version: Some(query.version()),
|
||||
},
|
||||
)
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
mod cancel;
|
||||
mod did_change;
|
||||
mod did_change_configuration;
|
||||
mod did_change_notebook;
|
||||
mod did_change_watched_files;
|
||||
mod did_change_workspace;
|
||||
mod did_close;
|
||||
mod did_close_notebook;
|
||||
mod did_open;
|
||||
mod did_open_notebook;
|
||||
|
||||
use super::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
pub(super) use cancel::Cancel;
|
||||
pub(super) use did_change::DidChange;
|
||||
pub(super) use did_change_configuration::DidChangeConfiguration;
|
||||
pub(super) use did_change_notebook::DidChangeNotebook;
|
||||
pub(super) use did_change_watched_files::DidChangeWatchedFiles;
|
||||
pub(super) use did_change_workspace::DidChangeWorkspace;
|
||||
pub(super) use did_close::DidClose;
|
||||
pub(super) use did_close_notebook::DidCloseNotebook;
|
||||
pub(super) use did_open::DidOpen;
|
||||
pub(super) use did_open_notebook::DidOpenNotebook;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user