Compare commits

..

1 Commits

Author SHA1 Message Date
Dhruv Manilawala
909798ffa2 Offset syntax error for each cell 2024-05-17 13:36:21 +05:30
124 changed files with 1078 additions and 3128 deletions

View File

@@ -167,9 +167,6 @@ 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

View File

@@ -1,65 +1,5 @@
# 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
View File

@@ -1300,7 +1300,8 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.95.1"
source = "git+https://github.com/astral-sh/lsp-types.git?rev=3512a9f#3512a9f33eadc5402cfab1b8f7340824c8ca1439"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365"
dependencies = [
"bitflags 1.3.2",
"serde",
@@ -1939,7 +1940,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"argfile",
@@ -2100,7 +2101,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2376,7 +2377,6 @@ dependencies = [
"ruff_diagnostics",
"ruff_formatter",
"ruff_linter",
"ruff_notebook",
"ruff_python_ast",
"ruff_python_codegen",
"ruff_python_formatter",

View File

@@ -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 = { git="https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = ["proposed"] }
lsp-types = { version = "0.95.0", features = ["proposed"] }
matchit = { version = "0.8.1" }
memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }

View File

@@ -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.5
rev: v0.4.4
hooks:
# Run the linter.
- id: ruff

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.4.5"
version = "0.4.4"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -401,7 +401,10 @@ pub(crate) fn format_source(
// Format the cell.
let formatted =
format_module_source(unformatted, options.clone()).map_err(|err| {
if let FormatModuleError::ParseError(err) = err {
if let FormatModuleError::ParseError(mut err) = err {
// Offset the error location by the start offset of the cell to report
// the correct cell index.
err.location += start;
DisplayParseError::from_source_kind(
err,
path.map(Path::to_path_buf),
@@ -857,20 +860,12 @@ 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\"`.`");
}

View File

@@ -1038,48 +1038,6 @@ 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()?;

View File

@@ -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 the input file for argfile to expand
// Create a the input file for argfile to expand
let input_file_path = tempdir.path().join("file_paths.txt");
fs::write(
&input_file_path,

View File

@@ -34,29 +34,12 @@ 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
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.
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.
## Example
```python
import numpy as np # unused import
@@ -66,14 +49,12 @@ 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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.4.5"
version = "0.4.4"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,7 +0,0 @@
def main() -> None:
a_list: list[str] | None = []
a_list.append("hello")
def hello(y: "dict[str, int] | None") -> None:
del y

View File

@@ -63,16 +63,3 @@ if (a and
#: Okay
def f():
return 1
# Soft keywords
#: E271
type Number = int
#: E273
type Number = int
#: E275
match(foo):
case(1):
pass

View File

@@ -46,15 +46,3 @@ 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 \."

View File

@@ -82,16 +82,3 @@ 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

View File

@@ -21,8 +21,6 @@ def wrong(): # [too-many-branches]
pass
try:
pass
except Exception:
pass
finally:
pass
if 2:
@@ -58,8 +56,6 @@ def good():
pass
try:
pass
except Exception:
pass
finally:
pass
if 1:
@@ -94,8 +90,6 @@ def with_statement_wrong():
pass
try:
pass
except Exception:
pass
finally:
pass
if 2:

View File

@@ -1,14 +0,0 @@
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)

View File

@@ -38,12 +38,3 @@ z = (
else
y
)
# FURB110
z = (
x
if x
else y
if y > 0
else None
)

View File

@@ -62,8 +62,6 @@ 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(
@@ -1197,8 +1195,6 @@ 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,

View File

@@ -2152,7 +2152,7 @@ impl<'a> Checker<'a> {
self.semantic.restore(snapshot);
if self.semantic.in_annotation() && self.semantic.in_typing_only_annotation() {
if self.semantic.in_annotation() && self.semantic.future_annotations_or_stub() {
if self.enabled(Rule::QuotedAnnotation) {
pyupgrade::rules::quoted_annotation(self, value, range);
}

View File

@@ -253,7 +253,7 @@ impl Display for DisplayParseError {
ErrorLocation::Cell(cell, location) => {
write!(
f,
"{cell}{colon}{row}{colon}{column}{colon} {inner}",
"cell {cell}{colon}{row}{colon}{column}{colon} {inner}",
cell = cell,
row = location.row,
column = location.column,

View File

@@ -43,7 +43,6 @@ 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(

View File

@@ -1,6 +1,21 @@
---
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:
@@ -14,3 +29,5 @@ 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")

View File

@@ -1,6 +1,36 @@
---
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:
@@ -28,3 +58,5 @@ 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")

View File

@@ -1,6 +1,36 @@
---
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:
@@ -30,3 +60,35 @@ 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")

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
---

View File

@@ -384,11 +384,7 @@ pub(crate) fn unittest_raises_assertion(
},
call.func.range(),
);
if !checker
.indexer()
.comment_ranges()
.has_comments(call, checker.locator())
{
if !checker.indexer().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(

View File

@@ -526,11 +526,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
}
// Avoid removing comments.
if checker
.indexer()
.comment_ranges()
.has_comments(expr, checker.locator())
{
if checker.indexer().has_comments(expr, checker.locator()) {
continue;
}

View File

@@ -209,11 +209,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
},
stmt_if.range(),
);
if !checker
.indexer()
.comment_ranges()
.has_comments(stmt_if, checker.locator())
{
if !checker.indexer().has_comments(stmt_if, checker.locator()) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
contents,
stmt_if.range(),
@@ -299,11 +295,7 @@ pub(crate) fn if_exp_instead_of_dict_get(
},
expr.range(),
);
if !checker
.indexer()
.comment_ranges()
.has_comments(expr, checker.locator())
{
if !checker.indexer().has_comments(expr, checker.locator()) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
contents,
expr.range(),

View File

@@ -142,11 +142,7 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &a
},
stmt_if.range(),
);
if !checker
.indexer()
.comment_ranges()
.has_comments(stmt_if, checker.locator())
{
if !checker.indexer().has_comments(stmt_if, checker.locator()) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
contents,
stmt_if.range(),

View File

@@ -193,11 +193,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
}
// Generate the replacement condition.
let condition = if checker
.indexer()
.comment_ranges()
.has_comments(&range, checker.locator())
{
let condition = if checker.indexer().has_comments(&range, checker.locator()) {
None
} else {
// If the return values are inverted, wrap the condition in a `not`.

View File

@@ -125,11 +125,7 @@ pub(crate) fn suppressible_exception(
},
stmt.range(),
);
if !checker
.indexer()
.comment_ranges()
.has_comments(stmt, checker.locator())
{
if !checker.indexer().has_comments(stmt, checker.locator()) {
diagnostic.try_set_fix(|| {
// let range = statement_range(stmt, checker.locator(), checker.indexer());

View File

@@ -181,7 +181,6 @@ fn check(
// If we're at the end of line, skip.
if matches!(next_char, '\n' | '\r') {
contains_valid_escape_sequence = true;
continue;
}

View File

@@ -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_keyword()
if tok0_kind.is_non_soft_keyword()
&& !(tok0_kind.is_singleton()
|| matches!(tok0_kind, TokenKind::Async | TokenKind::Await)
|| tok0_kind == TokenKind::Except && tok1_kind == TokenKind::Star

View File

@@ -445,7 +445,7 @@ impl LogicalLinesBuilder {
if matches!(kind, TokenKind::Comma | TokenKind::Semi | TokenKind::Colon) {
line.flags.insert(TokenFlags::PUNCTUATION);
} else if kind.is_keyword() {
} else if kind.is_non_soft_keyword() {
line.flags.insert(TokenFlags::KEYWORD);
}

View File

@@ -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_keyword = token.kind().is_keyword();
if is_keyword {
let is_non_soft_keyword = token.kind().is_non_soft_keyword();
if is_non_soft_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_keyword;
after_keyword = is_non_soft_keyword;
}
}

View File

@@ -9,10 +9,10 @@ use crate::checkers::ast::Checker;
use crate::registry::Rule;
/// ## What it does
/// Checks for membership tests using `not {element} in {collection}`.
/// Checks for negative comparison using `not {foo} in {bar}`.
///
/// ## Why is this bad?
/// Testing membership with `{element} not in {collection}` is more readable.
/// Negative comparison should be done using `not in`.
///
/// ## Example
/// ```python
@@ -42,11 +42,10 @@ impl AlwaysFixableViolation for NotInTest {
}
/// ## What it does
/// Checks for identity comparisons using `not {foo} is {bar}`.
/// Checks for negative comparison using `not {foo} is {bar}`.
///
/// ## Why is this bad?
/// According to [PEP8], testing for an object's identity with `is not` is more
/// readable.
/// Negative comparison should be done using `is not`.
///
/// ## Example
/// ```python
@@ -61,8 +60,6 @@ impl AlwaysFixableViolation for NotInTest {
/// pass
/// Z = X.B is not Y
/// ```
///
/// [PEP8]: https://peps.python.org/pep-0008/#programming-recommendations
#[violation]
pub struct NotIsTest;

View File

@@ -11,6 +11,18 @@ 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.

View File

@@ -190,22 +190,4 @@ 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

View File

@@ -106,22 +106,4 @@ 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):

View File

@@ -106,39 +106,4 @@ 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

View File

@@ -145,8 +145,6 @@ 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
@@ -156,61 +154,5 @@ 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 \\."

View File

@@ -229,49 +229,6 @@ 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(

View File

@@ -24,7 +24,6 @@ enum UnusedImportContext {
Init {
first_party: bool,
dunder_all_count: usize,
ignore_init_module_imports: bool,
},
}
@@ -47,29 +46,12 @@ 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
///
/// 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.
/// 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.
///
/// ## Example
///
/// ```python
/// import numpy as np # unused import
///
@@ -79,14 +61,12 @@ 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
///
@@ -107,8 +87,6 @@ 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>,
@@ -139,7 +117,6 @@ impl Violation for UnusedImport {
fn fix_title(&self) -> Option<String> {
let UnusedImport {
name,
module,
binding,
multiple,
..
@@ -148,14 +125,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,
ignore_init_module_imports: true,
}) => Some(format!("Use an explicit re-export: `{module} as {module}`")),
}) => Some(format!(
"Use an explicit re-export: `{binding} as {binding}`"
)),
_ => Some(if *multiple {
"Remove unused import".to_string()
@@ -267,8 +244,7 @@ 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.ignore_init_module_imports;
let preview_mode = checker.settings.preview.is_enabled();
let fix_init = 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
@@ -299,7 +275,6 @@ 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
@@ -313,31 +288,30 @@ 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 || 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(),
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,
)
} else {
(None, None)
};
.ok(),
fix_by_reexporting(
checker,
import_statement,
&to_reexport.iter().map(|(b, _)| b).collect::<Vec<_>>(),
&dunder_all_exprs,
)
.ok(),
)
} else {
(None, None)
};
for ((binding, context), fix) in iter::Iterator::chain(
iter::zip(to_remove, iter::repeat(fix_remove)),
@@ -346,7 +320,6 @@ 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,
@@ -371,7 +344,6 @@ 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,

View File

@@ -1,47 +0,0 @@
---
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

View File

@@ -1,50 +0,0 @@
---
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"]

View File

@@ -1,34 +0,0 @@
---
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__ = []

View File

@@ -1,34 +0,0 @@
---
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

View File

@@ -1,34 +0,0 @@
---
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__ = [];

View File

@@ -1,44 +0,0 @@
---
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

View File

@@ -1,23 +0,0 @@
---
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`

View File

@@ -1,23 +0,0 @@
---
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__

View File

@@ -1,16 +0,0 @@
---
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__

View File

@@ -1,16 +0,0 @@
---
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__

View File

@@ -1,16 +0,0 @@
---
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__

View File

@@ -1,24 +0,0 @@
---
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

View File

@@ -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: `renamed as renamed`
= help: Use an explicit re-export: `bees as bees`

View File

@@ -155,11 +155,7 @@ 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()
.comment_ranges()
.has_comments(expr, checker.locator())
{
if !checker.indexer().has_comments(expr, checker.locator()) {
let flattened_expr = Expr::Call(ast::ExprCall {
func: Box::new(func.clone()),
arguments: Arguments {

View File

@@ -98,8 +98,6 @@ 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();
@@ -112,13 +110,7 @@ fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
};
if id == "__slots__" {
for attribute in slots_attributes(value) {
if let Some(attribute) = attribute {
slots.insert(attribute);
} else {
return vec![];
}
}
slots.extend(slots_attributes(value));
}
}
@@ -133,13 +125,7 @@ fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
};
if id == "__slots__" {
for attribute in slots_attributes(value) {
if let Some(attribute) = attribute {
slots.insert(attribute);
} else {
return vec![];
}
}
slots.extend(slots_attributes(value));
}
}
@@ -150,13 +136,7 @@ fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
};
if id == "__slots__" {
for attribute in slots_attributes(value) {
if let Some(attribute) = attribute {
slots.insert(attribute);
} else {
return vec![];
}
}
slots.extend(slots_attributes(value));
}
}
_ => {}
@@ -257,14 +237,12 @@ fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
}
/// Return an iterator over the attributes enumerated in the given `__slots__` value.
///
/// If an attribute can't be statically determined, it will be `None`.
fn slots_attributes(expr: &Expr) -> impl Iterator<Item = Option<&str>> {
fn slots_attributes(expr: &Expr) -> impl Iterator<Item = &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().map(|elt| match elt {
| Expr::Set(ast::ExprSet { elts, .. }) => Some(elts.iter().filter_map(|elt| match elt {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value.to_str()),
_ => None,
})),
@@ -273,7 +251,7 @@ fn slots_attributes(expr: &Expr) -> impl Iterator<Item = Option<&str>> {
// Ex) `__slots__ = {"name": ...}`
let keys_iter = match expr {
Expr::Dict(dict) => Some(dict.iter_keys().map(|key| match key {
Expr::Dict(dict) => Some(dict.iter_keys().filter_map(|key| match key {
Some(Expr::StringLiteral(ast::ExprStringLiteral { value, .. })) => Some(value.to_str()),
_ => None,
})),

View File

@@ -199,9 +199,7 @@ fn num_branches(stmts: &[Stmt]) -> usize {
finalbody,
..
}) => {
// Count each `except` clause as a branch; the `else` and `finally` clauses also
// count, but the `try` clause itself does not.
num_branches(body)
1 + num_branches(body)
+ (if orelse.is_empty() {
0
} else {
@@ -325,47 +323,6 @@ 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"
@@ -381,7 +338,7 @@ finally:
pass
";
test_helper(source, 4)?;
test_helper(source, 5)?;
Ok(())
}

View File

@@ -10,12 +10,12 @@ too_many_branches.py:8:5: PLR0912 Too many branches (13 > 12)
10 | if 1:
|
too_many_branches.py:80:5: PLR0912 Too many branches (13 > 12)
too_many_branches.py:76:5: PLR0912 Too many branches (13 > 12)
|
78 | pass
79 |
80 | def with_statement_wrong():
74 | pass
75 |
76 | def with_statement_wrong():
| ^^^^^^^^^^^^^^^^^^^^ PLR0912
81 | """statements inside the with statement should get counted"""
82 | with suppress(Exception):
77 | """statements inside the with statement should get counted"""
78 | with suppress(Exception):
|

View File

@@ -55,8 +55,7 @@ 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_0.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_1.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037.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"))]

View File

@@ -10,18 +10,11 @@ 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
///
@@ -39,18 +32,6 @@ 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__)

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP037_0.py:18:14: UP037 [*] Remove quotes from type annotation
UP037.py:18:14: UP037 [*] Remove quotes from type annotation
|
18 | def foo(var: "MyClass") -> "MyClass":
| ^^^^^^^^^ UP037
@@ -19,7 +19,7 @@ UP037_0.py:18:14: UP037 [*] Remove quotes from type annotation
20 20 |
21 21 |
UP037_0.py:18:28: UP037 [*] Remove quotes from type annotation
UP037.py:18:28: UP037 [*] Remove quotes from type annotation
|
18 | def foo(var: "MyClass") -> "MyClass":
| ^^^^^^^^^ UP037
@@ -37,7 +37,7 @@ UP037_0.py:18:28: UP037 [*] Remove quotes from type annotation
20 20 |
21 21 |
UP037_0.py:19:8: UP037 [*] Remove quotes from type annotation
UP037.py:19:8: UP037 [*] Remove quotes from type annotation
|
18 | def foo(var: "MyClass") -> "MyClass":
19 | x: "MyClass"
@@ -55,7 +55,7 @@ UP037_0.py:19:8: UP037 [*] Remove quotes from type annotation
21 21 |
22 22 | def foo(*, inplace: "bool"):
UP037_0.py:22:21: UP037 [*] Remove quotes from type annotation
UP037.py:22:21: UP037 [*] Remove quotes from type annotation
|
22 | def foo(*, inplace: "bool"):
| ^^^^^^ UP037
@@ -73,7 +73,7 @@ UP037_0.py:22:21: UP037 [*] Remove quotes from type annotation
24 24 |
25 25 |
UP037_0.py:26:16: UP037 [*] Remove quotes from type annotation
UP037.py:26:16: UP037 [*] Remove quotes from type annotation
|
26 | def foo(*args: "str", **kwargs: "int"):
| ^^^^^ UP037
@@ -91,7 +91,7 @@ UP037_0.py:26:16: UP037 [*] Remove quotes from type annotation
28 28 |
29 29 |
UP037_0.py:26:33: UP037 [*] Remove quotes from type annotation
UP037.py:26:33: UP037 [*] Remove quotes from type annotation
|
26 | def foo(*args: "str", **kwargs: "int"):
| ^^^^^ UP037
@@ -109,7 +109,7 @@ UP037_0.py:26:33: UP037 [*] Remove quotes from type annotation
28 28 |
29 29 |
UP037_0.py:30:10: UP037 [*] Remove quotes from type annotation
UP037.py:30:10: UP037 [*] Remove quotes from type annotation
|
30 | x: Tuple["MyClass"]
| ^^^^^^^^^ UP037
@@ -128,7 +128,7 @@ UP037_0.py:30:10: UP037 [*] Remove quotes from type annotation
32 32 | x: Callable[["MyClass"], None]
33 33 |
UP037_0.py:32:14: UP037 [*] Remove quotes from type annotation
UP037.py:32:14: UP037 [*] Remove quotes from type annotation
|
30 | x: Tuple["MyClass"]
31 |
@@ -147,7 +147,7 @@ UP037_0.py:32:14: UP037 [*] Remove quotes from type annotation
34 34 |
35 35 | class Foo(NamedTuple):
UP037_0.py:36:8: UP037 [*] Remove quotes from type annotation
UP037.py:36:8: UP037 [*] Remove quotes from type annotation
|
35 | class Foo(NamedTuple):
36 | x: "MyClass"
@@ -165,7 +165,7 @@ UP037_0.py:36:8: UP037 [*] Remove quotes from type annotation
38 38 |
39 39 | class D(TypedDict):
UP037_0.py:40:27: UP037 [*] Remove quotes from type annotation
UP037.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_0.py:40:27: UP037 [*] Remove quotes from type annotation
42 42 |
43 43 | class D(TypedDict):
UP037_0.py:44:31: UP037 [*] Remove quotes from type annotation
UP037.py:44:31: UP037 [*] Remove quotes from type annotation
|
43 | class D(TypedDict):
44 | E: TypedDict("E", {"foo": "int"})
@@ -201,7 +201,7 @@ UP037_0.py:44:31: UP037 [*] Remove quotes from type annotation
46 46 |
47 47 | x: Annotated["str", "metadata"]
UP037_0.py:47:14: UP037 [*] Remove quotes from type annotation
UP037.py:47:14: UP037 [*] Remove quotes from type annotation
|
47 | x: Annotated["str", "metadata"]
| ^^^^^ UP037
@@ -220,7 +220,7 @@ UP037_0.py:47:14: UP037 [*] Remove quotes from type annotation
49 49 | x: Arg("str", "name")
50 50 |
UP037_0.py:49:8: UP037 [*] Remove quotes from type annotation
UP037.py:49:8: UP037 [*] Remove quotes from type annotation
|
47 | x: Annotated["str", "metadata"]
48 |
@@ -241,7 +241,7 @@ UP037_0.py:49:8: UP037 [*] Remove quotes from type annotation
51 51 | x: DefaultArg("str", "name")
52 52 |
UP037_0.py:51:15: UP037 [*] Remove quotes from type annotation
UP037.py:51:15: UP037 [*] Remove quotes from type annotation
|
49 | x: Arg("str", "name")
50 |
@@ -262,7 +262,7 @@ UP037_0.py:51:15: UP037 [*] Remove quotes from type annotation
53 53 | x: NamedArg("str", "name")
54 54 |
UP037_0.py:53:13: UP037 [*] Remove quotes from type annotation
UP037.py:53:13: UP037 [*] Remove quotes from type annotation
|
51 | x: DefaultArg("str", "name")
52 |
@@ -283,7 +283,7 @@ UP037_0.py:53:13: UP037 [*] Remove quotes from type annotation
55 55 | x: DefaultNamedArg("str", "name")
56 56 |
UP037_0.py:55:20: UP037 [*] Remove quotes from type annotation
UP037.py:55:20: UP037 [*] Remove quotes from type annotation
|
53 | x: NamedArg("str", "name")
54 |
@@ -304,7 +304,7 @@ UP037_0.py:55:20: UP037 [*] Remove quotes from type annotation
57 57 | x: DefaultNamedArg("str", name="name")
58 58 |
UP037_0.py:57:20: UP037 [*] Remove quotes from type annotation
UP037.py:57:20: UP037 [*] Remove quotes from type annotation
|
55 | x: DefaultNamedArg("str", "name")
56 |
@@ -325,7 +325,7 @@ UP037_0.py:57:20: UP037 [*] Remove quotes from type annotation
59 59 | x: VarArg("str")
60 60 |
UP037_0.py:59:11: UP037 [*] Remove quotes from type annotation
UP037.py:59:11: UP037 [*] Remove quotes from type annotation
|
57 | x: DefaultNamedArg("str", name="name")
58 |
@@ -346,7 +346,7 @@ UP037_0.py:59:11: UP037 [*] Remove quotes from type annotation
61 61 | x: List[List[List["MyClass"]]]
62 62 |
UP037_0.py:61:19: UP037 [*] Remove quotes from type annotation
UP037.py:61:19: UP037 [*] Remove quotes from type annotation
|
59 | x: VarArg("str")
60 |
@@ -367,7 +367,7 @@ UP037_0.py:61:19: UP037 [*] Remove quotes from type annotation
63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 64 |
UP037_0.py:63:29: UP037 [*] Remove quotes from type annotation
UP037.py:63:29: UP037 [*] Remove quotes from type annotation
|
61 | x: List[List[List["MyClass"]]]
62 |
@@ -388,7 +388,7 @@ UP037_0.py:63:29: UP037 [*] Remove quotes from type annotation
65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 66 |
UP037_0.py:63:45: UP037 [*] Remove quotes from type annotation
UP037.py:63:45: UP037 [*] Remove quotes from type annotation
|
61 | x: List[List[List["MyClass"]]]
62 |
@@ -409,7 +409,7 @@ UP037_0.py:63:45: UP037 [*] Remove quotes from type annotation
65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 66 |
UP037_0.py:65:29: UP037 [*] Remove quotes from type annotation
UP037.py:65:29: UP037 [*] Remove quotes from type annotation
|
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 |
@@ -430,7 +430,7 @@ UP037_0.py:65:29: UP037 [*] Remove quotes from type annotation
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
68 68 |
UP037_0.py:65:36: UP037 [*] Remove quotes from type annotation
UP037.py:65:36: UP037 [*] Remove quotes from type annotation
|
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 |
@@ -451,7 +451,7 @@ UP037_0.py:65:36: UP037 [*] Remove quotes from type annotation
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
68 68 |
UP037_0.py:65:45: UP037 [*] Remove quotes from type annotation
UP037.py:65:45: UP037 [*] Remove quotes from type annotation
|
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 |
@@ -472,7 +472,7 @@ UP037_0.py:65:45: UP037 [*] Remove quotes from type annotation
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
68 68 |
UP037_0.py:65:52: UP037 [*] Remove quotes from type annotation
UP037.py:65:52: UP037 [*] Remove quotes from type annotation
|
63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
64 |
@@ -493,7 +493,7 @@ UP037_0.py:65:52: UP037 [*] Remove quotes from type annotation
67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")])
68 68 |
UP037_0.py:67:24: UP037 [*] Remove quotes from type annotation
UP037.py:67:24: UP037 [*] Remove quotes from type annotation
|
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 |
@@ -514,7 +514,7 @@ UP037_0.py:67:24: UP037 [*] Remove quotes from type annotation
69 69 | X: MyCallable("X")
70 70 |
UP037_0.py:67:38: UP037 [*] Remove quotes from type annotation
UP037.py:67:38: UP037 [*] Remove quotes from type annotation
|
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 |
@@ -535,7 +535,7 @@ UP037_0.py:67:38: UP037 [*] Remove quotes from type annotation
69 69 | X: MyCallable("X")
70 70 |
UP037_0.py:67:45: UP037 [*] Remove quotes from type annotation
UP037.py:67:45: UP037 [*] Remove quotes from type annotation
|
65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
66 |
@@ -554,4 +554,6 @@ UP037_0.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 |

View File

@@ -1,22 +0,0 @@
---
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 |

View File

@@ -1,14 +1,9 @@
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;
@@ -69,13 +64,29 @@ 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 {}",
parenthesize_test(test, if_expr, checker.indexer(), checker.locator()),
parenthesize_test(orelse, if_expr, checker.indexer(), checker.locator()),
checker.locator().slice(left),
checker.locator().slice(right),
),
if_expr.range(),
),
@@ -88,30 +99,3 @@ 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()))
}
}

View File

@@ -177,33 +177,3 @@ 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 | )

View File

@@ -198,11 +198,7 @@ pub(crate) fn collection_literal_concatenation(checker: &mut Checker, expr: &Exp
},
expr.range(),
);
if !checker
.indexer()
.comment_ranges()
.has_comments(expr, checker.locator())
{
if !checker.indexer().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(

View File

@@ -23,7 +23,7 @@ impl fmt::Display for SourceValue {
impl Cell {
/// Return the [`SourceValue`] of the cell.
pub fn source(&self) -> &SourceValue {
pub(crate) fn source(&self) -> &SourceValue {
match self {
Cell::Code(cell) => &cell.source,
Cell::Markdown(cell) => &cell.source,

View File

@@ -98,7 +98,7 @@ impl Notebook {
reader.read_exact(&mut buf).is_ok_and(|()| buf[0] == b'\n')
});
reader.rewind()?;
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
let mut raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
Ok(notebook) => notebook,
Err(err) => {
// Translate the error into a diagnostic
@@ -113,13 +113,7 @@ 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

View File

@@ -112,6 +112,25 @@ 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> {

View File

@@ -6,7 +6,7 @@ use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::{has_leading_content, has_trailing_content, is_python_whitespace};
use crate::is_python_whitespace;
/// Stores the ranges of comments sorted by [`TextRange::start`] in increasing order. No two ranges are overlapping.
#[derive(Clone, Default)]
@@ -49,25 +49,6 @@ 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

View File

@@ -21,7 +21,6 @@ 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 }

View File

@@ -1,68 +1,17 @@
## The Ruff Language Server
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.
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.
### 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.
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:
![A screenshot showing an enabled "Native Server" extension setting in the VS Code settings view](assets/nativeServer.png)
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).
- 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).
### 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 [**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 our [**Discord**](https://discord.com/invite/astral-sh) if you have questions.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,85 +0,0 @@
## 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"]
```

View File

@@ -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.
![A screenshot showing an open Python file in Helix with highlighted diagnostics and a code action dropdown menu open](assets/SuccessfulHelixSetup.png)
![image](assets/SuccessfulHelixSetup.png "A screenshot showing an open Python file in Helix with highlighted diagnostics and a code action dropdown menu open")
*This screenshot is using `select=["ALL]"` for demonstration purposes.*
If you want to, as an example, turn on auto-formatting, add `auto-format = true`:

View File

@@ -1,15 +1,17 @@
{
"codeAction": {
"disableRuleComment": {
"enable": false
}
},
"lint": {
"ignore": ["RUF001"],
"run": "onSave"
},
"fixAll": false,
"logLevel": "warn",
"lineLength": 80,
"exclude": ["third_party"]
"settings": {
"codeAction": {
"disableRuleComment": {
"enable": false
}
},
"lint": {
"ignore": ["RUF001"],
"run": "onSave"
},
"fixAll": false,
"logLevel": "warn",
"lineLength": 80,
"exclude": ["third_party"]
}
}

View File

@@ -1,20 +1,18 @@
//! 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, path::PathBuf};
use std::collections::HashMap;
pub use document::Document;
pub(crate) use document::DocumentVersion;
pub use document::TextDocument;
use lsp_types::PositionEncodingKind;
pub(crate) use notebook::NotebookDocument;
pub(crate) use range::{NotebookRange, RangeExt, ToRangeExt};
pub(crate) use range::{RangeExt, ToRangeExt};
pub(crate) use replacement::Replacement;
use crate::{fix::Fixes, session::ResolvedClientCapabilities};
use crate::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.
@@ -31,37 +29,6 @@ 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)]
@@ -105,25 +72,13 @@ 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 {
@@ -139,8 +94,7 @@ impl WorkspaceEditTracker {
document_edits.push(lsp_types::TextDocumentEdit {
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
uri,
// TODO(jane): Re-enable versioned edits after investigating whether it could work with notebook cells
version: None,
version: Some(version),
},
edits: edits.into_iter().map(lsp_types::OneOf::Left).collect(),
});

View File

@@ -7,10 +7,10 @@ use super::RangeExt;
pub(crate) type DocumentVersion = i32;
/// The state of an individual document in the server. Stays up-to-date
/// The state for an individual document in the server. Stays up-to-date
/// with changes made by the user, including unsaved changes.
#[derive(Debug, Clone)]
pub struct TextDocument {
pub struct Document {
/// 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 TextDocument {
version: DocumentVersion,
}
impl TextDocument {
impl Document {
pub fn new(contents: String, version: DocumentVersion) -> Self {
let index = LineIndex::from_source_text(&contents);
Self {

View File

@@ -1,202 +0,0 @@
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),
}
}
}

View File

@@ -1,16 +1,9 @@
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;
@@ -18,13 +11,6 @@ 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 {
@@ -97,54 +83,8 @@ 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: 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(&notebook_index.translate_location(&start));
let end = source_location_to_position(&notebook_index.translate_location(&end));
NotebookRange {
cell: starting_cell
.map(OneIndexed::to_zero_indexed)
.unwrap_or_default(),
range: types::Range { start, end },
start: offset_to_position(self.start(), text, index, encoding),
end: offset_to_position(self.end(), text, index, encoding),
}
}
}
@@ -171,13 +111,13 @@ fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize {
utf8_code_unit_offset
}
fn offset_to_source_location(
fn offset_to_position(
offset: TextSize,
text: &str,
index: &LineIndex,
encoding: PositionEncoding,
) -> SourceLocation {
match encoding {
) -> types::Position {
let location = match encoding {
PositionEncoding::UTF8 => {
let row = index.line_index(offset);
let column = offset - index.line_start(row, text);
@@ -203,10 +143,8 @@ fn offset_to_source_location(
}
}
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())

View File

@@ -2,29 +2,28 @@ use ruff_linter::{
linter::{FixerResult, LinterResult},
packaging::detect_package_root,
settings::{flags, types::UnsafeFixes, LinterSettings},
source_kind::SourceKind,
};
use ruff_notebook::SourceValue;
use ruff_python_ast::PySourceType;
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(
query: &DocumentQuery,
document: &crate::edit::Document,
document_url: &lsp_types::Url,
linter_settings: &LinterSettings,
encoding: PositionEncoding,
) -> crate::Result<Fixes> {
let document_path = query.file_path();
let source_kind = query.make_source_kind();
) -> 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");
let package = detect_package_root(
document_path
@@ -33,7 +32,10 @@ pub(crate) fn fix_all(
&linter_settings.namespace_packages,
);
let source_type = query.source_type();
let source_type = PySourceType::default();
// TODO(jane): Support Jupyter Notebooks
let source_kind = SourceKind::Python(source.to_string());
// 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
@@ -46,7 +48,7 @@ pub(crate) fn fix_all(
result: LinterResult { error, .. },
..
} = ruff_linter::linter::lint_fix(
document_path,
&document_path,
package,
flags::Noqa::Enabled,
UnsafeFixes::Disabled,
@@ -64,79 +66,27 @@ 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(Fixes::default());
return Ok(vec![]);
}
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 = transformed.source_code();
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 modified_index = LineIndex::from_source_text(modified);
let Replacement {
source_range,
modified_range,
} = Replacement::between(
&source,
source_index.line_starts(),
&modified,
modified_index.line_starts(),
);
let source_index = document.index();
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());
let Replacement {
source_range,
modified_range,
} = Replacement::between(
source,
source_index.line_starts(),
modified,
modified_index.line_starts(),
);
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())
}
Ok(vec![lsp_types::TextEdit {
range: source_range.to_range(source, source_index, encoding),
new_text: modified[modified_range].to_owned(),
}])
}

View File

@@ -1,28 +1,29 @@
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::TextDocument;
use crate::edit::Document;
pub(crate) fn format(
document: &TextDocument,
source_type: PySourceType,
document: &Document,
formatter_settings: &FormatterSettings,
) -> crate::Result<String> {
let format_options = formatter_settings.to_format_options(source_type, document.contents());
// TODO(jane): support Jupyter Notebook
let format_options = formatter_settings
.to_format_options(ruff_python_ast::PySourceType::Python, document.contents());
let formatted = format_module_source(document.contents(), format_options)?;
Ok(formatted.into_code())
}
pub(crate) fn format_range(
document: &TextDocument,
source_type: PySourceType,
document: &Document,
formatter_settings: &FormatterSettings,
range: TextRange,
) -> crate::Result<PrintedRange> {
let format_options = formatter_settings.to_format_options(source_type, document.contents());
// TODO(jane): support Jupyter Notebook
let format_options = formatter_settings
.to_format_options(ruff_python_ast::PySourceType::Python, document.contents());
Ok(ruff_python_formatter::format_range(
document.contents(),

View File

@@ -1,6 +1,6 @@
//! ## The Ruff Language Server
pub use edit::{PositionEncoding, TextDocument};
pub use edit::{Document, PositionEncoding};
use lsp_types::CodeActionKind;
pub use server::Server;
@@ -19,10 +19,6 @@ 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.

View File

@@ -10,32 +10,26 @@ use ruff_linter::{
settings::{flags, LinterSettings},
source_kind::SourceKind,
};
use ruff_notebook::Notebook;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::AsMode;
use ruff_source_file::{LineIndex, Locator};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashMap;
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
use serde::{Deserialize, Serialize};
use crate::{
edit::{NotebookRange, ToRangeExt},
session::DocumentQuery,
PositionEncoding, DIAGNOSTIC_NAME,
};
use crate::{edit::ToRangeExt, PositionEncoding, DIAGNOSTIC_NAME};
/// This is serialized on the diagnostic `data` field.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub(crate) struct AssociatedDiagnosticData {
pub(crate) kind: DiagnosticKind,
/// Edits to fix the diagnostic. If this is empty, a fix
/// does not exist.
pub(crate) edits: Vec<lsp_types::TextEdit>,
/// A possible fix for the associated diagnostic.
pub(crate) fix: Option<Fix>,
/// 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<lsp_types::TextEdit>,
pub(crate) noqa_edit: Option<ruff_diagnostics::Edit>,
}
/// Describes a fix for `fixed_diagnostic` that may have quick fix
@@ -55,16 +49,18 @@ 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(
query: &DocumentQuery,
document: &crate::edit::Document,
document_url: &lsp_types::Url,
linter_settings: &LinterSettings,
encoding: PositionEncoding,
) -> Diagnostics {
let document_path = query.file_path();
let source_kind = query.make_source_kind();
) -> 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");
let package = detect_package_root(
document_path
@@ -73,15 +69,16 @@ pub(crate) fn check(
&linter_settings.namespace_packages,
);
let source_type = query.source_type();
let source_type = PySourceType::default();
// TODO(jane): Support Jupyter Notebooks
let source_kind = SourceKind::Python(contents.to_string());
// Tokenize once.
let tokens = ruff_python_parser::tokenize(source_kind.source_code(), source_type.as_mode());
let index = LineIndex::from_source_text(source_kind.source_code());
let tokens = ruff_python_parser::tokenize(contents, source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::with_index(source_kind.source_code(), index.clone());
let locator = Locator::with_index(contents, index);
// Detect the current code style (lazily).
let stylist = Stylist::from_tokens(&tokens, &locator);
@@ -93,8 +90,10 @@ pub(crate) fn check(
let directives = extract_directives(&tokens, Flags::all(), &locator, &indexer);
// Generate checks.
let LinterResult { data, .. } = check_path(
document_path,
let LinterResult {
data: diagnostics, ..
} = check_path(
&document_path,
package,
&locator,
&stylist,
@@ -108,8 +107,8 @@ pub(crate) fn check(
);
let noqa_edits = generate_noqa_edits(
document_path,
data.as_slice(),
&document_path,
diagnostics.as_slice(),
&locator,
indexer.comment_ranges(),
&linter_settings.external,
@@ -117,47 +116,16 @@ pub(crate) fn check(
stylist.line_ending(),
);
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
diagnostics
.into_iter()
.zip(noqa_edits)
.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
.map(|(diagnostic, noqa_edit)| to_lsp_diagnostic(diagnostic, noqa_edit, document, encoding))
.collect()
}
/// 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
@@ -171,6 +139,36 @@ 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,
@@ -178,28 +176,22 @@ pub(crate) fn fixes_for_diagnostics(
.kind
.suggestion
.unwrap_or(associated_data.kind.name),
noqa_edit: associated_data.noqa_edit,
edits: associated_data.edits,
edits,
noqa_edit,
}))
})
.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>,
source_kind: &SourceKind,
index: &LineIndex,
noqa_edit: Option<Edit>,
document: &crate::edit::Document,
encoding: PositionEncoding,
) -> (usize, lsp_types::Diagnostic) {
) -> lsp_types::Diagnostic {
let Diagnostic {
kind,
range: diagnostic_range,
fix,
..
kind, range, fix, ..
} = diagnostic;
let rule = kind.rule();
@@ -208,24 +200,11 @@ fn to_lsp_diagnostic(
let data = (fix.is_some() || noqa_edit.is_some())
.then(|| {
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 {
serde_json::to_value(&AssociatedDiagnosticData {
kind: kind.clone(),
noqa_edit,
edits,
fix,
code: rule.noqa_code().to_string(),
noqa_edit,
})
.ok()
})
@@ -233,53 +212,20 @@ fn to_lsp_diagnostic(
let code = rule.noqa_code().to_string();
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)
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,
}
}

View File

@@ -1,7 +1,6 @@
//! Scheduling, I/O, and API endpoints.
use std::num::NonZeroUsize;
use std::path::PathBuf;
use lsp_server as lsp;
use lsp_types as types;
@@ -11,9 +10,6 @@ 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;
@@ -69,33 +65,28 @@ impl Server {
let AllSettings {
global_settings,
mut workspace_settings,
} = AllSettings::from_value(
init_params
.initialization_options
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())),
);
} = AllSettings::from_value(init_params.initialization_options.unwrap_or_default());
let mut workspace_for_path = |path: PathBuf| {
let mut workspace_for_uri = |uri| {
let Some(workspace_settings) = workspace_settings.as_mut() else {
return (path, ClientSettings::default());
return (uri, ClientSettings::default());
};
let settings = workspace_settings.remove(&path).unwrap_or_else(|| {
tracing::warn!("No workspace settings found for {}", path.display());
let settings = workspace_settings.remove(&uri).unwrap_or_else(|| {
tracing::warn!("No workspace settings found for {uri}");
ClientSettings::default()
});
(path, settings)
(uri, settings)
};
let workspaces = init_params
.workspace_folders
.filter(|folders| !folders.is_empty())
.map(|folders| folders.into_iter().map(|folder| {
workspace_for_path(folder.uri.to_file_path().unwrap())
workspace_for_uri(folder.uri)
}).collect())
.or_else(|| {
tracing::warn!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace...");
tracing::debug!("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_path(uri.to_file_path().unwrap())])
Some(vec![workspace_for_uri(uri)])
})
.ok_or_else(|| {
anyhow::anyhow!("Failed to get the current working directory while creating a default workspace.")
@@ -109,7 +100,7 @@ impl Server {
position_encoding,
global_settings,
workspaces,
),
)?,
client_capabilities,
})
}
@@ -178,14 +169,10 @@ 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(),
@@ -265,16 +252,6 @@ 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),
@@ -301,15 +278,8 @@ 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 {
@@ -319,8 +289,6 @@ 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,
}
}
@@ -336,8 +304,6 @@ impl SupportedCodeAction {
Self::QuickFix,
Self::SourceFixAll,
Self::SourceOrganizeImports,
Self::NotebookSourceFixAll,
Self::NotebookSourceOrganizeImports,
]
.into_iter()
}

View File

@@ -84,15 +84,6 @@ 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();

View File

@@ -1,20 +1,17 @@
use crate::{
lint::Diagnostics,
server::client::Notifier,
session::{DocumentQuery, DocumentSnapshot},
};
use crate::{server::client::Notifier, session::DocumentSnapshot};
use super::LSPResult;
pub(super) fn generate_diagnostics(snapshot: &DocumentSnapshot) -> Diagnostics {
pub(super) fn generate_diagnostics(snapshot: &DocumentSnapshot) -> Vec<lsp_types::Diagnostic> {
if snapshot.client_settings().lint() {
crate::lint::check(
snapshot.query(),
snapshot.query().settings().linter(),
snapshot.document(),
snapshot.url(),
snapshot.settings().linter(),
snapshot.encoding(),
)
} else {
Diagnostics::default()
vec![]
}
}
@@ -22,31 +19,31 @@ pub(super) fn publish_diagnostics_for_document(
snapshot: &DocumentSnapshot,
notifier: &Notifier,
) -> crate::server::Result<()> {
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)?;
}
let diagnostics = generate_diagnostics(snapshot);
Ok(())
}
pub(super) fn clear_diagnostics_for_document(
query: &DocumentQuery,
notifier: &Notifier,
) -> crate::server::Result<()> {
notifier
.notify::<lsp_types::notification::PublishDiagnostics>(
lsp_types::PublishDiagnosticsParams {
uri: query.make_key().into_url(),
diagnostics: vec![],
version: Some(query.version()),
uri: snapshot.url().clone(),
diagnostics,
version: Some(snapshot.document().version()),
},
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
Ok(())
}
pub(super) fn clear_diagnostics_for_document(
snapshot: &DocumentSnapshot,
notifier: &Notifier,
) -> crate::server::Result<()> {
notifier
.notify::<lsp_types::notification::PublishDiagnostics>(
lsp_types::PublishDiagnosticsParams {
uri: snapshot.url().clone(),
diagnostics: vec![],
version: Some(snapshot.document().version()),
},
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;

View File

@@ -1,22 +1,16 @@
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;

View File

@@ -11,6 +11,7 @@ impl super::NotificationHandler for Cancel {
}
impl super::SyncNotificationHandler for Cancel {
#[tracing::instrument(skip_all)]
fn run(
_session: &mut Session,
_notifier: Notifier,

View File

@@ -3,7 +3,6 @@ use crate::server::api::LSPResult;
use crate::server::client::{Notifier, Requester};
use crate::server::Result;
use crate::session::Session;
use lsp_server::ErrorCode;
use lsp_types as types;
use lsp_types::notification as notif;
@@ -14,6 +13,7 @@ impl super::NotificationHandler for DidChange {
}
impl super::SyncNotificationHandler for DidChange {
#[tracing::instrument(skip_all, fields(file=%uri))]
fn run(
session: &mut Session,
notifier: Notifier,
@@ -27,13 +27,19 @@ impl super::SyncNotificationHandler for DidChange {
content_changes,
}: types::DidChangeTextDocumentParams,
) -> Result<()> {
let key = session
.key_from_url(&uri)
.with_failure_code(ErrorCode::InternalError)?;
let encoding = session.encoding();
let document = session
.document_controller(&uri)
.with_failure_code(lsp_server::ErrorCode::InvalidParams)?;
session
.update_text_document(&key, content_changes, new_version)
.with_failure_code(ErrorCode::InternalError)?;
if content_changes.is_empty() {
document.make_mut().update_version(new_version);
return Ok(());
}
document
.make_mut()
.apply_changes(content_changes, new_version, encoding);
// Publish diagnostics if the client doesnt support pull diagnostics
if !session.resolved_client_capabilities().pull_diagnostics {

View File

@@ -1,41 +0,0 @@
use crate::server::api::diagnostics::publish_diagnostics_for_document;
use crate::server::api::LSPResult;
use crate::server::client::{Notifier, Requester};
use crate::server::Result;
use crate::session::Session;
use lsp_server::ErrorCode;
use lsp_types as types;
use lsp_types::notification as notif;
pub(crate) struct DidChangeNotebook;
impl super::NotificationHandler for DidChangeNotebook {
type NotificationType = notif::DidChangeNotebookDocument;
}
impl super::SyncNotificationHandler for DidChangeNotebook {
fn run(
session: &mut Session,
notifier: Notifier,
_requester: &mut Requester,
types::DidChangeNotebookDocumentParams {
notebook_document: types::VersionedNotebookDocumentIdentifier { uri, version },
change: types::NotebookDocumentChangeEvent { cells, metadata },
}: types::DidChangeNotebookDocumentParams,
) -> Result<()> {
let key = session
.key_from_url(&uri)
.with_failure_code(ErrorCode::InternalError)?;
session
.update_notebook_document(&key, cells, metadata, version)
.with_failure_code(ErrorCode::InternalError)?;
// publish new diagnostics
let snapshot = session
.take_snapshot(&uri)
.expect("snapshot should be available");
publish_diagnostics_for_document(&snapshot, &notifier)?;
Ok(())
}
}

View File

@@ -1,4 +1,3 @@
use crate::server::api::diagnostics::publish_diagnostics_for_document;
use crate::server::api::LSPResult;
use crate::server::client::{Notifier, Requester};
use crate::server::schedule::Task;
@@ -16,36 +15,20 @@ impl super::NotificationHandler for DidChangeWatchedFiles {
impl super::SyncNotificationHandler for DidChangeWatchedFiles {
fn run(
session: &mut Session,
notifier: Notifier,
_notifier: Notifier,
requester: &mut Requester,
params: types::DidChangeWatchedFilesParams,
) -> Result<()> {
for change in &params.changes {
session.reload_settings(&change.uri.to_file_path().unwrap());
session
.reload_settings(&change.uri)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
}
if !params.changes.is_empty() {
if session.resolved_client_capabilities().workspace_refresh {
requester
.request::<types::request::WorkspaceDiagnosticRefresh>((), |()| Task::nothing())
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
} else {
// publish diagnostics for text documents
for url in session.text_document_urls() {
let snapshot = session
.take_snapshot(&url)
.expect("snapshot should be available");
publish_diagnostics_for_document(&snapshot, &notifier)?;
}
}
// always publish diagnostics for notebook files (since they don't use pull diagnostics)
for url in session.notebook_document_urls() {
let snapshot = session
.take_snapshot(&url)
.expect("snapshot should be available");
publish_diagnostics_for_document(&snapshot, &notifier)?;
}
if session.resolved_client_capabilities().workspace_refresh && !params.changes.is_empty() {
requester
.request::<types::request::WorkspaceDiagnosticRefresh>((), |()| Task::nothing())
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
}
Ok(())

View File

@@ -2,8 +2,6 @@ use crate::server::api::LSPResult;
use crate::server::client::{Notifier, Requester};
use crate::server::Result;
use crate::session::Session;
use anyhow::anyhow;
use lsp_server::ErrorCode;
use lsp_types as types;
use lsp_types::notification as notif;
@@ -20,21 +18,14 @@ impl super::SyncNotificationHandler for DidChangeWorkspace {
_requester: &mut Requester,
params: types::DidChangeWorkspaceFoldersParams,
) -> Result<()> {
for types::WorkspaceFolder { ref uri, .. } in params.event.added {
let workspace_path = uri
.to_file_path()
.map_err(|()| anyhow!("expected document URI {uri} to be a valid file path"))
.with_failure_code(ErrorCode::InvalidParams)?;
session.open_workspace_folder(workspace_path);
}
for types::WorkspaceFolder { ref uri, .. } in params.event.removed {
let workspace_path = uri
.to_file_path()
.map_err(|()| anyhow!("expected document URI {uri} to be a valid file path"))
.with_failure_code(ErrorCode::InvalidParams)?;
for new in params.event.added {
session
.close_workspace_folder(&workspace_path)
.open_workspace_folder(&new.uri)
.with_failure_code(lsp_server::ErrorCode::InvalidParams)?;
}
for removed in params.event.removed {
session
.close_workspace_folder(&removed.uri)
.with_failure_code(lsp_server::ErrorCode::InvalidParams)?;
}
Ok(())

View File

@@ -1,4 +1,3 @@
use crate::edit::DocumentKey;
use crate::server::api::diagnostics::clear_diagnostics_for_document;
use crate::server::api::LSPResult;
use crate::server::client::{Notifier, Requester};
@@ -14,6 +13,7 @@ impl super::NotificationHandler for DidClose {
}
impl super::SyncNotificationHandler for DidClose {
#[tracing::instrument(skip_all, fields(file=%uri))]
fn run(
session: &mut Session,
notifier: Notifier,
@@ -22,24 +22,20 @@ impl super::SyncNotificationHandler for DidClose {
text_document: types::TextDocumentIdentifier { uri },
}: types::DidCloseTextDocumentParams,
) -> Result<()> {
// Publish an empty diagnostic report for the document. This will de-register any existing diagnostics.
let snapshot = session
.take_snapshot(&uri)
.ok_or_else(|| anyhow::anyhow!("Unable to take snapshot for document with URL {uri}"))
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
clear_diagnostics_for_document(snapshot.query(), &notifier)?;
let key = snapshot.query().make_key();
// Notebook cells will go through the `textDocument/didClose` path.
// We still want to publish empty diagnostics for them, but we
// shouldn't call `session.close_document` on them.
if matches!(key, DocumentKey::NotebookCell(_)) {
return Ok(());
// Publish an empty diagnostic report for the document if the client does not support pull diagnostics.
// This will de-register any existing diagnostics.
if !session.resolved_client_capabilities().pull_diagnostics {
let snapshot = session
.take_snapshot(&uri)
.ok_or_else(|| {
anyhow::anyhow!("Unable to take snapshot for document with URL {uri}")
})
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
clear_diagnostics_for_document(&snapshot, &notifier)?;
}
session
.close_document(&key)
.close_document(&uri)
.with_failure_code(lsp_server::ErrorCode::InternalError)
}
}

View File

@@ -1,35 +0,0 @@
use crate::server::api::LSPResult;
use crate::server::client::{Notifier, Requester};
use crate::server::Result;
use crate::session::Session;
use lsp_server::ErrorCode;
use lsp_types as types;
use lsp_types::notification as notif;
pub(crate) struct DidCloseNotebook;
impl super::NotificationHandler for DidCloseNotebook {
type NotificationType = notif::DidCloseNotebookDocument;
}
impl super::SyncNotificationHandler for DidCloseNotebook {
fn run(
session: &mut Session,
_notifier: Notifier,
_requester: &mut Requester,
types::DidCloseNotebookDocumentParams {
notebook_document: types::NotebookDocumentIdentifier { uri },
..
}: types::DidCloseNotebookDocumentParams,
) -> Result<()> {
let key = session
.key_from_url(&uri)
.with_failure_code(ErrorCode::InternalError)?;
session
.close_document(&key)
.with_failure_code(ErrorCode::InternalError)?;
Ok(())
}
}

View File

@@ -3,9 +3,6 @@ use crate::server::api::LSPResult;
use crate::server::client::{Notifier, Requester};
use crate::server::Result;
use crate::session::Session;
use crate::TextDocument;
use anyhow::anyhow;
use lsp_server::ErrorCode;
use lsp_types as types;
use lsp_types::notification as notif;
@@ -16,6 +13,7 @@ impl super::NotificationHandler for DidOpen {
}
impl super::SyncNotificationHandler for DidOpen {
#[tracing::instrument(skip_all, fields(file=%url))]
fn run(
session: &mut Session,
notifier: Notifier,
@@ -23,28 +21,21 @@ impl super::SyncNotificationHandler for DidOpen {
types::DidOpenTextDocumentParams {
text_document:
types::TextDocumentItem {
ref uri,
uri: ref url,
text,
version,
..
},
}: types::DidOpenTextDocumentParams,
) -> Result<()> {
let document_path: std::path::PathBuf = uri
.to_file_path()
.map_err(|()| anyhow!("expected document URI {uri} to be a valid file path"))
.with_failure_code(ErrorCode::InvalidParams)?;
let document = TextDocument::new(text, version);
session.open_text_document(document_path, document);
session.open_document(url, text, version);
// Publish diagnostics if the client doesnt support pull diagnostics
if !session.resolved_client_capabilities().pull_diagnostics {
let snapshot = session
.take_snapshot(uri)
.take_snapshot(url)
.ok_or_else(|| {
anyhow::anyhow!("Unable to take snapshot for document with URL {uri}")
anyhow::anyhow!("Unable to take snapshot for document with URL {url}")
})
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
publish_diagnostics_for_document(&snapshot, &notifier)?;

View File

@@ -1,58 +0,0 @@
use crate::edit::NotebookDocument;
use crate::server::api::diagnostics::publish_diagnostics_for_document;
use crate::server::api::LSPResult;
use crate::server::client::{Notifier, Requester};
use crate::server::Result;
use crate::session::Session;
use anyhow::anyhow;
use lsp_server::ErrorCode;
use lsp_types as types;
use lsp_types::notification as notif;
pub(crate) struct DidOpenNotebook;
impl super::NotificationHandler for DidOpenNotebook {
type NotificationType = notif::DidOpenNotebookDocument;
}
impl super::SyncNotificationHandler for DidOpenNotebook {
fn run(
session: &mut Session,
notifier: Notifier,
_requester: &mut Requester,
types::DidOpenNotebookDocumentParams {
notebook_document:
types::NotebookDocument {
uri,
version,
cells,
metadata,
..
},
cell_text_documents,
}: types::DidOpenNotebookDocumentParams,
) -> Result<()> {
let notebook = NotebookDocument::new(
version,
cells,
metadata.unwrap_or_default(),
cell_text_documents,
)
.with_failure_code(ErrorCode::InternalError)?;
let notebook_path = uri
.to_file_path()
.map_err(|()| anyhow!("expected notebook URI {uri} to be a valid file path"))
.with_failure_code(ErrorCode::InvalidParams)?;
session.open_notebook_document(notebook_path, notebook);
// publish diagnostics
let snapshot = session
.take_snapshot(&uri)
.expect("snapshot should be available");
publish_diagnostics_for_document(&snapshot, &notifier)?;
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More