Compare commits
24 Commits
v0.4.4
...
dhruv/form
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce4d4ae6ac | ||
|
|
128414cd95 | ||
|
|
be0ccabbaa | ||
|
|
6cec82fff8 | ||
|
|
5ab4cc86c2 | ||
|
|
bc7856e899 | ||
|
|
6a28f3448e | ||
|
|
7c824faa88 | ||
|
|
12da5968a0 | ||
|
|
a747b3f2a1 | ||
|
|
01a0e6cc7e | ||
|
|
a8b06537c7 | ||
|
|
7b8fe25d32 | ||
|
|
a50416a6d7 | ||
|
|
41e53d59ab | ||
|
|
0fc6cf9bee | ||
|
|
d835b3e218 | ||
|
|
d7f093ef9e | ||
|
|
4b330b11c6 | ||
|
|
890cc325d5 | ||
|
|
0726e82342 | ||
|
|
f79c980e17 | ||
|
|
35ba3c91ce | ||
|
|
1f794077ec |
16
.github/workflows/ci.yaml
vendored
16
.github/workflows/ci.yaml
vendored
@@ -395,22 +395,16 @@ jobs:
|
||||
name: ecosystem-result
|
||||
path: ecosystem-result
|
||||
|
||||
cargo-udeps:
|
||||
name: "cargo udeps"
|
||||
cargo-shear:
|
||||
name: "cargo shear"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install nightly Rust toolchain"
|
||||
# Only pinned to make caching work, update freely
|
||||
run: rustup toolchain install nightly-2023-10-15
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install cargo-udeps"
|
||||
uses: taiki-e/install-action@cargo-udeps
|
||||
- name: "Run cargo-udeps"
|
||||
run: cargo +nightly-2023-10-15 udeps
|
||||
- uses: cargo-bins/cargo-binstall@main
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
python-package:
|
||||
name: "python package"
|
||||
|
||||
@@ -14,7 +14,7 @@ exclude: |
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.16
|
||||
rev: v0.17
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
@@ -56,7 +56,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.4.3
|
||||
rev: v0.4.4
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
45
Cargo.lock
generated
45
Cargo.lock
generated
@@ -129,9 +129,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.82"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
|
||||
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
|
||||
|
||||
[[package]]
|
||||
name = "argfile"
|
||||
@@ -1707,9 +1707,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.81"
|
||||
version = "1.0.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
|
||||
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1821,7 +1821,6 @@ dependencies = [
|
||||
"dashmap",
|
||||
"hashbrown 0.14.5",
|
||||
"indexmap",
|
||||
"log",
|
||||
"notify",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
@@ -1829,10 +1828,8 @@ dependencies = [
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_trivia",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"smallvec",
|
||||
"smol_str",
|
||||
"tempfile",
|
||||
"textwrap",
|
||||
@@ -2200,7 +2197,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bitflags 2.5.0",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"itertools 0.12.1",
|
||||
"once_cell",
|
||||
@@ -2348,7 +2344,6 @@ dependencies = [
|
||||
name = "ruff_python_trivia"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"insta",
|
||||
"itertools 0.12.1",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
@@ -2560,9 +2555,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.17"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f55c82c700538496bdc329bb4918a81f87cc8888811bd123cf325a0f2f8d309"
|
||||
checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive",
|
||||
@@ -2572,9 +2567,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.17"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83263746fe5e32097f06356968a077f96089739c927a61450efa069905eec108"
|
||||
checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2602,9 +2597,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.200"
|
||||
version = "1.0.201"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
|
||||
checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -2622,9 +2617,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.200"
|
||||
version = "1.0.201"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
|
||||
checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2644,9 +2639,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.116"
|
||||
version = "1.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -2819,9 +2814,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.60"
|
||||
version = "2.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
|
||||
checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2909,18 +2904,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.59"
|
||||
version = "1.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
|
||||
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.59"
|
||||
version = "1.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
|
||||
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -37,7 +37,6 @@ drop_bomb = { version = "0.1.5" }
|
||||
env_logger = { version = "0.11.0" }
|
||||
fern = { version = "0.6.1" }
|
||||
filetime = { version = "0.2.23" }
|
||||
fs-err = { version = "2.11.0" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
hashbrown = "0.14.3"
|
||||
@@ -130,6 +129,7 @@ char_lit_as_u8 = "allow"
|
||||
collapsible_else_if = "allow"
|
||||
collapsible_if = "allow"
|
||||
implicit_hasher = "allow"
|
||||
map_unwrap_or = "allow"
|
||||
match_same_arms = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
|
||||
@@ -14,31 +14,28 @@ license.workspace = true
|
||||
[dependencies]
|
||||
ruff_python_parser = { path = "../ruff_python_parser" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast" }
|
||||
ruff_python_trivia = { path = "../ruff_python_trivia" }
|
||||
ruff_text_size = { path = "../ruff_text_size" }
|
||||
ruff_index = { path = "../ruff_index" }
|
||||
ruff_notebook = { path = "../ruff_notebook" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
ctrlc = "3.4.4"
|
||||
crossbeam = { workspace = true }
|
||||
ctrlc = { version = "3.4.4" }
|
||||
dashmap = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
log = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
smol_str = "0.2.1"
|
||||
smol_str = { version = "0.2.1" }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-tree = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
textwrap = "0.16.1"
|
||||
textwrap = { version = "0.16.1" }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -68,6 +68,10 @@ insta-cmd = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
# Used via macro expansion.
|
||||
ignored = ["chrono"]
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
mimalloc = { workspace = true }
|
||||
|
||||
|
||||
@@ -1553,3 +1553,68 @@ def unused(x): # noqa: ANN001, ARG001, D103
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_noqa_multiline_comment() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint]
|
||||
select = ["UP031"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let test_path = tempdir.path().join("noqa.py");
|
||||
|
||||
fs::write(
|
||||
&test_path,
|
||||
r#"
|
||||
print(
|
||||
"""First line
|
||||
second line
|
||||
third line
|
||||
%s"""
|
||||
% name
|
||||
)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
|
||||
.arg(&test_path)
|
||||
.arg("--preview")
|
||||
.args(["--add-noqa"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Added 1 noqa directive.
|
||||
"###);
|
||||
});
|
||||
|
||||
let test_code = std::fs::read_to_string(&test_path).expect("should read test file");
|
||||
|
||||
insta::assert_snapshot!(test_code, @r###"
|
||||
print(
|
||||
"""First line
|
||||
second line
|
||||
third line
|
||||
%s""" # noqa: UP031
|
||||
% name
|
||||
)
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ unicode-width = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
# Used via `CacheKey` macro expansion.
|
||||
ignored = ["ruff_cache"]
|
||||
|
||||
[features]
|
||||
serde = ["dep:serde", "ruff_text_size/serde"]
|
||||
schemars = ["dep:schemars", "ruff_text_size/schemars"]
|
||||
|
||||
@@ -24,3 +24,7 @@ foo(**{},)
|
||||
# Duplicated key names won't be fixed, to avoid syntax errors.
|
||||
abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
|
||||
# Some values need to be parenthesized.
|
||||
abc(foo=1, **{'bar': (bar := 1)}) # PIE804
|
||||
abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
||||
|
||||
@@ -43,3 +43,13 @@ from typing_extensions import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass # TCH005
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/11368
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
elif test:
|
||||
pass
|
||||
|
||||
@@ -66,3 +66,19 @@ class StudentF(object):
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/11358
|
||||
class Foo:
|
||||
__slots__ = ("bar",)
|
||||
|
||||
def __init__(self):
|
||||
self.qux = 2
|
||||
|
||||
@property
|
||||
def qux(self):
|
||||
return self.bar * 2
|
||||
|
||||
@qux.setter
|
||||
def qux(self, value):
|
||||
self.bar = value / 2
|
||||
|
||||
@@ -2,7 +2,6 @@ use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::helpers;
|
||||
use ruff_python_ast::types::Node;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_python_semantic::ScopeKind;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -1098,9 +1097,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
pylint::rules::too_many_nested_blocks(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::EmptyTypeCheckingBlock) {
|
||||
if typing::is_type_checking_block(if_, &checker.semantic) {
|
||||
flake8_type_checking::rules::empty_type_checking_block(checker, if_);
|
||||
}
|
||||
flake8_type_checking::rules::empty_type_checking_block(checker, if_);
|
||||
}
|
||||
if checker.enabled(Rule::IfTuple) {
|
||||
pyflakes::rules::if_tuple(checker, if_);
|
||||
|
||||
@@ -73,7 +73,7 @@ mod annotation;
|
||||
mod deferred;
|
||||
|
||||
/// State representing whether a docstring is expected or not for the next statement.
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
enum DocstringState {
|
||||
/// The next statement is expected to be a docstring, but not necessarily so.
|
||||
///
|
||||
@@ -92,15 +92,84 @@ enum DocstringState {
|
||||
/// For `Foo`, the state is expected when the checker is visiting the class
|
||||
/// body but isn't going to be present. While, for `bar` function, the docstring
|
||||
/// is expected and present.
|
||||
#[default]
|
||||
Expected,
|
||||
Expected(ExpectedDocstringKind),
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Default for DocstringState {
|
||||
/// Returns the default docstring state which is to expect a module-level docstring.
|
||||
fn default() -> Self {
|
||||
Self::Expected(ExpectedDocstringKind::Module)
|
||||
}
|
||||
}
|
||||
|
||||
impl DocstringState {
|
||||
/// Returns `true` if the next statement is expected to be a docstring.
|
||||
const fn is_expected(self) -> bool {
|
||||
matches!(self, DocstringState::Expected)
|
||||
/// Returns the docstring kind if the state is expecting a docstring.
|
||||
const fn expected_kind(self) -> Option<ExpectedDocstringKind> {
|
||||
match self {
|
||||
DocstringState::Expected(kind) => Some(kind),
|
||||
DocstringState::Other => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of an expected docstring.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
enum ExpectedDocstringKind {
|
||||
/// A module-level docstring.
|
||||
///
|
||||
/// For example,
|
||||
/// ```python
|
||||
/// """This is a module-level docstring."""
|
||||
///
|
||||
/// a = 1
|
||||
/// ```
|
||||
Module,
|
||||
|
||||
/// A class-level docstring.
|
||||
///
|
||||
/// For example,
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// """This is the docstring for `Foo` class."""
|
||||
///
|
||||
/// def __init__(self) -> None:
|
||||
/// ...
|
||||
/// ```
|
||||
Class,
|
||||
|
||||
/// A function-level docstring.
|
||||
///
|
||||
/// For example,
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// """This is the docstring for `foo` function."""
|
||||
/// pass
|
||||
/// ```
|
||||
Function,
|
||||
|
||||
/// An attribute-level docstring.
|
||||
///
|
||||
/// For example,
|
||||
/// ```python
|
||||
/// a = 1
|
||||
/// """This is the docstring for `a` variable."""
|
||||
///
|
||||
///
|
||||
/// class Foo:
|
||||
/// b = 1
|
||||
/// """This is the docstring for `Foo.b` class variable."""
|
||||
/// ```
|
||||
Attribute,
|
||||
}
|
||||
|
||||
impl ExpectedDocstringKind {
|
||||
/// Returns the semantic model flag that represents the current docstring state.
|
||||
const fn as_flag(self) -> SemanticModelFlags {
|
||||
match self {
|
||||
ExpectedDocstringKind::Attribute => SemanticModelFlags::ATTRIBUTE_DOCSTRING,
|
||||
_ => SemanticModelFlags::PEP_257_DOCSTRING,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,9 +452,9 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
|
||||
// Update the semantic model if it is in a docstring. This should be done after the
|
||||
// flags snapshot to ensure that it gets reset once the statement is analyzed.
|
||||
if self.docstring_state.is_expected() {
|
||||
if let Some(kind) = self.docstring_state.expected_kind() {
|
||||
if is_docstring_stmt(stmt) {
|
||||
self.semantic.flags |= SemanticModelFlags::DOCSTRING;
|
||||
self.semantic.flags |= kind.as_flag();
|
||||
}
|
||||
// Reset the state irrespective of whether the statement is a docstring or not.
|
||||
self.docstring_state = DocstringState::Other;
|
||||
@@ -709,7 +778,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
}
|
||||
|
||||
// Set the docstring state before visiting the class body.
|
||||
self.docstring_state = DocstringState::Expected;
|
||||
self.docstring_state = DocstringState::Expected(ExpectedDocstringKind::Class);
|
||||
self.visit_body(body);
|
||||
|
||||
let scope_id = self.semantic.scope_id;
|
||||
@@ -874,6 +943,24 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
_ => visitor::walk_stmt(self, stmt),
|
||||
};
|
||||
|
||||
if self.semantic().at_top_level() || self.semantic().current_scope().kind.is_class() {
|
||||
match stmt {
|
||||
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
|
||||
if let [Expr::Name(_)] = targets.as_slice() {
|
||||
self.docstring_state =
|
||||
DocstringState::Expected(ExpectedDocstringKind::Attribute);
|
||||
}
|
||||
}
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
|
||||
if target.is_name_expr() {
|
||||
self.docstring_state =
|
||||
DocstringState::Expected(ExpectedDocstringKind::Attribute);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Clean-up
|
||||
|
||||
// Step 4: Analysis
|
||||
@@ -2122,7 +2209,7 @@ impl<'a> Checker<'a> {
|
||||
|
||||
self.visit_parameters(parameters);
|
||||
// Set the docstring state before visiting the function body.
|
||||
self.docstring_state = DocstringState::Expected;
|
||||
self.docstring_state = DocstringState::Expected(ExpectedDocstringKind::Function);
|
||||
self.visit_body(body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//!
|
||||
//! [Ruff]: https://github.com/astral-sh/ruff
|
||||
|
||||
pub use noqa::generate_noqa_edits;
|
||||
#[cfg(feature = "clap")]
|
||||
pub use registry::clap_completion::RuleParser;
|
||||
#[cfg(feature = "clap")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Write};
|
||||
use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::ops::Add;
|
||||
use std::path::Path;
|
||||
@@ -10,7 +10,7 @@ use itertools::Itertools;
|
||||
use log::warn;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::{Diagnostic, Edit};
|
||||
use ruff_python_trivia::{indentation_at_offset, CommentRanges};
|
||||
use ruff_source_file::{LineEnding, Locator};
|
||||
|
||||
@@ -19,6 +19,27 @@ use crate::fs::relativize_path;
|
||||
use crate::registry::{AsRule, Rule, RuleSet};
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
|
||||
/// Generates an array of edits that matches the length of `diagnostics`.
|
||||
/// Each potential edit in the array is paired, in order, with the associated diagnostic.
|
||||
/// Each edit will add a `noqa` comment to the appropriate line in the source to hide
|
||||
/// the diagnostic. These edits may conflict with each other and should not be applied
|
||||
/// simultaneously.
|
||||
pub fn generate_noqa_edits(
|
||||
path: &Path,
|
||||
diagnostics: &[Diagnostic],
|
||||
locator: &Locator,
|
||||
comment_ranges: &CommentRanges,
|
||||
external: &[String],
|
||||
noqa_line_for: &NoqaMapping,
|
||||
line_ending: LineEnding,
|
||||
) -> Vec<Option<Edit>> {
|
||||
let exemption =
|
||||
FileExemption::try_extract(locator.contents(), comment_ranges, external, path, locator);
|
||||
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator);
|
||||
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
|
||||
build_noqa_edits_by_diagnostic(comments, locator, line_ending)
|
||||
}
|
||||
|
||||
/// A directive to ignore a set of rules for a given line of Python source code (e.g.,
|
||||
/// `# noqa: F401, F841`).
|
||||
#[derive(Debug)]
|
||||
@@ -511,6 +532,7 @@ pub(crate) fn add_noqa(
|
||||
noqa_line_for,
|
||||
line_ending,
|
||||
);
|
||||
|
||||
fs::write(path, output)?;
|
||||
Ok(count)
|
||||
}
|
||||
@@ -524,9 +546,7 @@ fn add_noqa_inner(
|
||||
noqa_line_for: &NoqaMapping,
|
||||
line_ending: LineEnding,
|
||||
) -> (usize, String) {
|
||||
// Map of line start offset to set of (non-ignored) diagnostic codes that are triggered on that line.
|
||||
let mut matches_by_line: BTreeMap<TextSize, (RuleSet, Option<&Directive>)> =
|
||||
BTreeMap::default();
|
||||
let mut count = 0;
|
||||
|
||||
// Whether the file is exempted from all checks.
|
||||
// Codes that are globally exempted (within the current file).
|
||||
@@ -534,16 +554,117 @@ fn add_noqa_inner(
|
||||
FileExemption::try_extract(locator.contents(), comment_ranges, external, path, locator);
|
||||
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator);
|
||||
|
||||
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
|
||||
|
||||
let edits = build_noqa_edits_by_line(comments, locator, line_ending);
|
||||
|
||||
let contents = locator.contents();
|
||||
|
||||
let mut output = String::with_capacity(contents.len());
|
||||
let mut last_append = TextSize::default();
|
||||
|
||||
for (_, edit) in edits {
|
||||
output.push_str(&contents[TextRange::new(last_append, edit.start())]);
|
||||
|
||||
edit.write(&mut output);
|
||||
|
||||
count += 1;
|
||||
|
||||
last_append = edit.end();
|
||||
}
|
||||
|
||||
output.push_str(&contents[TextRange::new(last_append, TextSize::of(contents))]);
|
||||
|
||||
(count, output)
|
||||
}
|
||||
|
||||
fn build_noqa_edits_by_diagnostic(
|
||||
comments: Vec<Option<NoqaComment>>,
|
||||
locator: &Locator,
|
||||
line_ending: LineEnding,
|
||||
) -> Vec<Option<Edit>> {
|
||||
let mut edits = Vec::default();
|
||||
for comment in comments {
|
||||
match comment {
|
||||
Some(comment) => {
|
||||
if let Some(noqa_edit) = generate_noqa_edit(
|
||||
comment.directive,
|
||||
comment.line,
|
||||
RuleSet::from_rule(comment.diagnostic.kind.rule()),
|
||||
locator,
|
||||
line_ending,
|
||||
) {
|
||||
edits.push(Some(noqa_edit.into_edit()));
|
||||
}
|
||||
}
|
||||
None => edits.push(None),
|
||||
}
|
||||
}
|
||||
edits
|
||||
}
|
||||
|
||||
fn build_noqa_edits_by_line<'a>(
|
||||
comments: Vec<Option<NoqaComment<'a>>>,
|
||||
locator: &Locator,
|
||||
line_ending: LineEnding,
|
||||
) -> BTreeMap<TextSize, NoqaEdit<'a>> {
|
||||
let mut comments_by_line = BTreeMap::default();
|
||||
for comment in comments.into_iter().flatten() {
|
||||
comments_by_line
|
||||
.entry(comment.line)
|
||||
.or_insert_with(Vec::default)
|
||||
.push(comment);
|
||||
}
|
||||
let mut edits = BTreeMap::default();
|
||||
for (offset, matches) in comments_by_line {
|
||||
let Some(first_match) = matches.first() else {
|
||||
continue;
|
||||
};
|
||||
let directive = first_match.directive;
|
||||
if let Some(edit) = generate_noqa_edit(
|
||||
directive,
|
||||
offset,
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|NoqaComment { diagnostic, .. }| diagnostic.kind.rule())
|
||||
.collect(),
|
||||
locator,
|
||||
line_ending,
|
||||
) {
|
||||
edits.insert(offset, edit);
|
||||
}
|
||||
}
|
||||
edits
|
||||
}
|
||||
|
||||
struct NoqaComment<'a> {
|
||||
line: TextSize,
|
||||
diagnostic: &'a Diagnostic,
|
||||
directive: Option<&'a Directive<'a>>,
|
||||
}
|
||||
|
||||
fn find_noqa_comments<'a>(
|
||||
diagnostics: &'a [Diagnostic],
|
||||
locator: &'a Locator,
|
||||
exemption: &Option<FileExemption>,
|
||||
directives: &'a NoqaDirectives,
|
||||
noqa_line_for: &NoqaMapping,
|
||||
) -> Vec<Option<NoqaComment<'a>>> {
|
||||
// List of noqa comments, ordered to match up with `diagnostics`
|
||||
let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = vec![];
|
||||
|
||||
// Mark any non-ignored diagnostics.
|
||||
for diagnostic in diagnostics {
|
||||
match &exemption {
|
||||
Some(FileExemption::All) => {
|
||||
// If the file is exempted, don't add any noqa directives.
|
||||
comments_by_line.push(None);
|
||||
continue;
|
||||
}
|
||||
Some(FileExemption::Codes(codes)) => {
|
||||
// If the diagnostic is ignored by a global exemption, don't add a noqa directive.
|
||||
if codes.contains(&diagnostic.kind.rule().noqa_code()) {
|
||||
comments_by_line.push(None);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -557,10 +678,12 @@ fn add_noqa_inner(
|
||||
{
|
||||
match &directive_line.directive {
|
||||
Directive::All(_) => {
|
||||
comments_by_line.push(None);
|
||||
continue;
|
||||
}
|
||||
Directive::Codes(codes) => {
|
||||
if codes.includes(diagnostic.kind.rule()) {
|
||||
comments_by_line.push(None);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -574,18 +697,17 @@ fn add_noqa_inner(
|
||||
if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) {
|
||||
match &directive_line.directive {
|
||||
Directive::All(_) => {
|
||||
comments_by_line.push(None);
|
||||
continue;
|
||||
}
|
||||
Directive::Codes(codes) => {
|
||||
directive @ Directive::Codes(codes) => {
|
||||
let rule = diagnostic.kind.rule();
|
||||
if !codes.includes(rule) {
|
||||
matches_by_line
|
||||
.entry(directive_line.start())
|
||||
.or_insert_with(|| {
|
||||
(RuleSet::default(), Some(&directive_line.directive))
|
||||
})
|
||||
.0
|
||||
.insert(rule);
|
||||
comments_by_line.push(Some(NoqaComment {
|
||||
line: directive_line.start(),
|
||||
diagnostic,
|
||||
directive: Some(directive),
|
||||
}));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -593,87 +715,106 @@ fn add_noqa_inner(
|
||||
}
|
||||
|
||||
// There's no existing noqa directive that suppresses the diagnostic.
|
||||
matches_by_line
|
||||
.entry(locator.line_start(noqa_offset))
|
||||
.or_insert_with(|| (RuleSet::default(), None))
|
||||
.0
|
||||
.insert(diagnostic.kind.rule());
|
||||
comments_by_line.push(Some(NoqaComment {
|
||||
line: locator.line_start(noqa_offset),
|
||||
diagnostic,
|
||||
directive: None,
|
||||
}));
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
let mut output = String::with_capacity(locator.len());
|
||||
let mut prev_end = TextSize::default();
|
||||
comments_by_line
|
||||
}
|
||||
|
||||
for (offset, (rules, directive)) in matches_by_line {
|
||||
output.push_str(locator.slice(TextRange::new(prev_end, offset)));
|
||||
struct NoqaEdit<'a> {
|
||||
edit_range: TextRange,
|
||||
rules: RuleSet,
|
||||
codes: Option<&'a Codes<'a>>,
|
||||
line_ending: LineEnding,
|
||||
}
|
||||
|
||||
let line = locator.full_line(offset);
|
||||
impl<'a> NoqaEdit<'a> {
|
||||
fn into_edit(self) -> Edit {
|
||||
let mut edit_content = String::new();
|
||||
self.write(&mut edit_content);
|
||||
|
||||
match directive {
|
||||
None => {
|
||||
// Add existing content.
|
||||
output.push_str(line.trim_end());
|
||||
Edit::range_replacement(edit_content, self.edit_range)
|
||||
}
|
||||
|
||||
// Add `noqa` directive.
|
||||
output.push_str(" # noqa: ");
|
||||
|
||||
// Add codes.
|
||||
push_codes(&mut output, rules.iter().map(|rule| rule.noqa_code()));
|
||||
output.push_str(&line_ending);
|
||||
count += 1;
|
||||
}
|
||||
Some(Directive::All(_)) => {
|
||||
// Does not get inserted into the map.
|
||||
}
|
||||
Some(Directive::Codes(codes)) => {
|
||||
// Reconstruct the line based on the preserved rule codes.
|
||||
// This enables us to tally the number of edits.
|
||||
let output_start = output.len();
|
||||
|
||||
// Add existing content.
|
||||
output.push_str(
|
||||
locator
|
||||
.slice(TextRange::new(offset, codes.start()))
|
||||
.trim_end(),
|
||||
);
|
||||
|
||||
// Add `noqa` directive.
|
||||
output.push_str(" # noqa: ");
|
||||
|
||||
// Add codes.
|
||||
fn write(&self, writer: &mut impl std::fmt::Write) {
|
||||
write!(writer, " # noqa: ").unwrap();
|
||||
match self.codes {
|
||||
Some(codes) => {
|
||||
push_codes(
|
||||
&mut output,
|
||||
rules
|
||||
writer,
|
||||
self.rules
|
||||
.iter()
|
||||
.map(|rule| rule.noqa_code().to_string())
|
||||
.chain(codes.iter().map(ToString::to_string))
|
||||
.sorted_unstable(),
|
||||
);
|
||||
|
||||
// Only count if the new line is an actual edit.
|
||||
if &output[output_start..] != line.trim_end() {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
output.push_str(&line_ending);
|
||||
}
|
||||
None => {
|
||||
push_codes(
|
||||
writer,
|
||||
self.rules.iter().map(|rule| rule.noqa_code().to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
prev_end = offset + line.text_len();
|
||||
write!(writer, "{}", self.line_ending.as_str()).unwrap();
|
||||
}
|
||||
|
||||
output.push_str(locator.after(prev_end));
|
||||
|
||||
(count, output)
|
||||
}
|
||||
|
||||
fn push_codes<I: Display>(str: &mut String, codes: impl Iterator<Item = I>) {
|
||||
impl<'a> Ranged for NoqaEdit<'a> {
|
||||
fn range(&self) -> TextRange {
|
||||
self.edit_range
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_noqa_edit<'a>(
|
||||
directive: Option<&'a Directive>,
|
||||
offset: TextSize,
|
||||
rules: RuleSet,
|
||||
locator: &Locator,
|
||||
line_ending: LineEnding,
|
||||
) -> Option<NoqaEdit<'a>> {
|
||||
let line_range = locator.full_line_range(offset);
|
||||
|
||||
let edit_range;
|
||||
let codes;
|
||||
|
||||
// Add codes.
|
||||
match directive {
|
||||
None => {
|
||||
let trimmed_line = locator.slice(line_range).trim_end();
|
||||
edit_range = TextRange::new(TextSize::of(trimmed_line), line_range.len()) + offset;
|
||||
codes = None;
|
||||
}
|
||||
Some(Directive::Codes(existing_codes)) => {
|
||||
// find trimmed line without the noqa
|
||||
let trimmed_line = locator
|
||||
.slice(TextRange::new(line_range.start(), existing_codes.start()))
|
||||
.trim_end();
|
||||
edit_range = TextRange::new(TextSize::of(trimmed_line), line_range.len()) + offset;
|
||||
codes = Some(existing_codes);
|
||||
}
|
||||
Some(Directive::All(_)) => return None,
|
||||
};
|
||||
|
||||
Some(NoqaEdit {
|
||||
edit_range,
|
||||
rules,
|
||||
codes,
|
||||
line_ending,
|
||||
})
|
||||
}
|
||||
|
||||
fn push_codes<I: Display>(writer: &mut dyn std::fmt::Write, codes: impl Iterator<Item = I>) {
|
||||
let mut first = true;
|
||||
for code in codes {
|
||||
if !first {
|
||||
str.push_str(", ");
|
||||
write!(writer, ", ").unwrap();
|
||||
}
|
||||
write!(str, "{code}").unwrap();
|
||||
write!(writer, "{code}").unwrap();
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
@@ -846,13 +987,15 @@ mod tests {
|
||||
use insta::assert_debug_snapshot;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::{Diagnostic, Edit};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::{LineEnding, Locator};
|
||||
|
||||
use crate::generate_noqa_edits;
|
||||
use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption};
|
||||
use crate::rules::pycodestyle::rules::AmbiguousVariableName;
|
||||
use crate::rules::pyflakes::rules::UnusedVariable;
|
||||
use crate::rules::pyupgrade::rules::PrintfStringFormatting;
|
||||
|
||||
#[test]
|
||||
fn noqa_all() {
|
||||
@@ -1130,4 +1273,41 @@ mod tests {
|
||||
assert_eq!(count, 0);
|
||||
assert_eq!(output, "x = 1 # noqa");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_comment() {
|
||||
let path = Path::new("/tmp/foo.txt");
|
||||
let source = r#"
|
||||
print(
|
||||
"""First line
|
||||
second line
|
||||
third line
|
||||
%s"""
|
||||
% name
|
||||
)
|
||||
"#;
|
||||
let noqa_line_for = [TextRange::new(8.into(), 68.into())].into_iter().collect();
|
||||
let diagnostics = [Diagnostic::new(
|
||||
PrintfStringFormatting,
|
||||
TextRange::new(12.into(), 79.into()),
|
||||
)];
|
||||
let comment_ranges = CommentRanges::default();
|
||||
let edits = generate_noqa_edits(
|
||||
path,
|
||||
&diagnostics,
|
||||
&Locator::new(source),
|
||||
&comment_ranges,
|
||||
&[],
|
||||
&noqa_line_for,
|
||||
LineEnding::Lf,
|
||||
);
|
||||
assert_eq!(
|
||||
edits,
|
||||
vec![Some(Edit::replacement(
|
||||
" # noqa: UP031\n".to_string(),
|
||||
68.into(),
|
||||
69.into()
|
||||
))]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ impl Violation for FutureRewritableTypeAnnotation {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let FutureRewritableTypeAnnotation { name } = self;
|
||||
format!("Missing `from __future__ import annotations`, but uses `{name}`")
|
||||
format!("Add `from __future__ import annotations` to simplify `{name}`")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
edge_case.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||
edge_case.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
||||
|
|
||||
5 | def main(_: List[int]) -> None:
|
||||
| ^^^^ FA100
|
||||
@@ -9,12 +9,10 @@ edge_case.py:5:13: FA100 Missing `from __future__ import annotations`, but uses
|
||||
7 | a_list.append("hello")
|
||||
|
|
||||
|
||||
edge_case.py:6:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||
edge_case.py:6:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
||||
|
|
||||
5 | def main(_: List[int]) -> None:
|
||||
6 | a_list: t.List[str] = []
|
||||
| ^^^^^^ FA100
|
||||
7 | a_list.append("hello")
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
from_typing_import.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||
from_typing_import.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
||||
|
|
||||
4 | def main() -> None:
|
||||
5 | a_list: List[str] = []
|
||||
| ^^^^ FA100
|
||||
6 | a_list.append("hello")
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
from_typing_import_many.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||
from_typing_import_many.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
||||
|
|
||||
4 | def main() -> None:
|
||||
5 | a_list: List[Optional[str]] = []
|
||||
@@ -10,7 +10,7 @@ from_typing_import_many.py:5:13: FA100 Missing `from __future__ import annotatio
|
||||
7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {})
|
||||
|
|
||||
|
||||
from_typing_import_many.py:5:18: FA100 Missing `from __future__ import annotations`, but uses `typing.Optional`
|
||||
from_typing_import_many.py:5:18: FA100 Add `from __future__ import annotations` to simplify `typing.Optional`
|
||||
|
|
||||
4 | def main() -> None:
|
||||
5 | a_list: List[Optional[str]] = []
|
||||
@@ -18,5 +18,3 @@ from_typing_import_many.py:5:18: FA100 Missing `from __future__ import annotatio
|
||||
6 | a_list.append("hello")
|
||||
7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {})
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
import_typing.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||
import_typing.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
||||
|
|
||||
4 | def main() -> None:
|
||||
5 | a_list: typing.List[str] = []
|
||||
| ^^^^^^^^^^^ FA100
|
||||
6 | a_list.append("hello")
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
import_typing_as.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||
import_typing_as.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
|
||||
|
|
||||
4 | def main() -> None:
|
||||
5 | a_list: t.List[str] = []
|
||||
| ^^^^^^ FA100
|
||||
6 | a_list.append("hello")
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -121,7 +122,19 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, call: &ast::ExprCal
|
||||
.iter()
|
||||
.zip(dict.iter_values())
|
||||
.map(|(kwarg, value)| {
|
||||
format!("{}={}", kwarg, checker.locator().slice(value.range()))
|
||||
format!(
|
||||
"{}={}",
|
||||
kwarg,
|
||||
checker.locator().slice(
|
||||
parenthesized_range(
|
||||
value.into(),
|
||||
dict.into(),
|
||||
checker.indexer().comment_ranges(),
|
||||
checker.locator().contents(),
|
||||
)
|
||||
.unwrap_or(value.range())
|
||||
)
|
||||
)
|
||||
})
|
||||
.join(", "),
|
||||
keyword.range(),
|
||||
|
||||
@@ -166,6 +166,8 @@ PIE804.py:26:10: PIE804 Unnecessary `dict` kwargs
|
||||
25 | abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
| ^^^^^^^^^^ PIE804
|
||||
27 |
|
||||
28 | # Some values need to be parenthesized.
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
@@ -175,6 +177,8 @@ PIE804.py:26:22: PIE804 [*] Unnecessary `dict` kwargs
|
||||
25 | abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
| ^^^^^^^^^^ PIE804
|
||||
27 |
|
||||
28 | # Some values need to be parenthesized.
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
@@ -184,5 +188,39 @@ PIE804.py:26:22: PIE804 [*] Unnecessary `dict` kwargs
|
||||
25 25 | abc(**{'a': b}, **{'a': c}) # PIE804
|
||||
26 |-abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
26 |+abc(a=1, **{'a': c}, b=c) # PIE804
|
||||
27 27 |
|
||||
28 28 | # Some values need to be parenthesized.
|
||||
29 29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
|
||||
|
||||
PIE804.py:29:12: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
28 | # Some values need to be parenthesized.
|
||||
29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ PIE804
|
||||
30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
|
||||
27 27 |
|
||||
28 28 | # Some values need to be parenthesized.
|
||||
29 |-abc(foo=1, **{'bar': (bar := 1)}) # PIE804
|
||||
29 |+abc(foo=1, bar=(bar := 1)) # PIE804
|
||||
30 30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
||||
|
||||
PIE804.py:30:12: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
28 | # Some values need to be parenthesized.
|
||||
29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
|
||||
30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PIE804
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
ℹ Safe fix
|
||||
27 27 |
|
||||
28 28 | # Some values need to be parenthesized.
|
||||
29 29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804
|
||||
30 |-abc(foo=1, **{'bar': (yield 1)}) # PIE804
|
||||
30 |+abc(foo=1, bar=(yield 1)) # PIE804
|
||||
|
||||
@@ -52,7 +52,7 @@ impl AlwaysFixableViolation for AvoidableEscapedQuote {
|
||||
|
||||
/// Q003
|
||||
pub(crate) fn avoidable_escaped_quote(checker: &mut Checker, string_like: StringLike) {
|
||||
if checker.semantic().in_docstring()
|
||||
if checker.semantic().in_pep_257_docstring()
|
||||
|| checker.semantic().in_string_type_definition()
|
||||
// This rule has support for strings nested inside another f-strings but they're checked
|
||||
// via the outermost f-string. This means that we shouldn't be checking any nested string
|
||||
@@ -64,28 +64,15 @@ pub(crate) fn avoidable_escaped_quote(checker: &mut Checker, string_like: String
|
||||
|
||||
let mut rule_checker = AvoidableEscapedQuoteChecker::new(checker.locator(), checker.settings);
|
||||
|
||||
match string_like {
|
||||
StringLike::String(expr) => {
|
||||
for string_literal in &expr.value {
|
||||
for part in string_like.parts() {
|
||||
match part {
|
||||
ast::StringLikePart::String(string_literal) => {
|
||||
rule_checker.visit_string_literal(string_literal);
|
||||
}
|
||||
}
|
||||
StringLike::Bytes(expr) => {
|
||||
for bytes_literal in &expr.value {
|
||||
ast::StringLikePart::Bytes(bytes_literal) => {
|
||||
rule_checker.visit_bytes_literal(bytes_literal);
|
||||
}
|
||||
}
|
||||
StringLike::FString(expr) => {
|
||||
for part in &expr.value {
|
||||
match part {
|
||||
ast::FStringPart::Literal(string_literal) => {
|
||||
rule_checker.visit_string_literal(string_literal);
|
||||
}
|
||||
ast::FStringPart::FString(f_string) => {
|
||||
rule_checker.visit_f_string(f_string);
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::StringLikePart::FString(f_string) => rule_checker.visit_f_string(f_string),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -454,13 +454,12 @@ pub(crate) fn check_string_quotes(checker: &mut Checker, string_like: StringLike
|
||||
return;
|
||||
}
|
||||
|
||||
let ranges: Vec<TextRange> = match string_like {
|
||||
StringLike::String(node) => node.value.iter().map(Ranged::range).collect(),
|
||||
StringLike::Bytes(node) => node.value.iter().map(Ranged::range).collect(),
|
||||
StringLike::FString(node) => node.value.iter().map(Ranged::range).collect(),
|
||||
};
|
||||
let ranges = string_like
|
||||
.parts()
|
||||
.map(|part| part.range())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if checker.semantic().in_docstring() {
|
||||
if checker.semantic().in_pep_257_docstring() {
|
||||
if checker.enabled(Rule::BadQuotesDocstring) {
|
||||
for range in ranges {
|
||||
docstring(checker, range);
|
||||
|
||||
@@ -47,45 +47,36 @@ impl AlwaysFixableViolation for UnnecessaryEscapedQuote {
|
||||
|
||||
/// Q004
|
||||
pub(crate) fn unnecessary_escaped_quote(checker: &mut Checker, string_like: StringLike) {
|
||||
if checker.semantic().in_docstring() {
|
||||
if checker.semantic().in_pep_257_docstring() {
|
||||
return;
|
||||
}
|
||||
|
||||
let locator = checker.locator();
|
||||
|
||||
match string_like {
|
||||
StringLike::String(expr) => {
|
||||
for string in &expr.value {
|
||||
for part in string_like.parts() {
|
||||
match part {
|
||||
ast::StringLikePart::String(string_literal) => {
|
||||
if let Some(diagnostic) = check_string_or_bytes(
|
||||
locator,
|
||||
string.range(),
|
||||
AnyStringKind::from(string.flags),
|
||||
string_literal.range(),
|
||||
AnyStringKind::from(string_literal.flags),
|
||||
) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
StringLike::Bytes(expr) => {
|
||||
for bytes in &expr.value {
|
||||
if let Some(diagnostic) =
|
||||
check_string_or_bytes(locator, bytes.range(), AnyStringKind::from(bytes.flags))
|
||||
{
|
||||
ast::StringLikePart::Bytes(bytes_literal) => {
|
||||
if let Some(diagnostic) = check_string_or_bytes(
|
||||
locator,
|
||||
bytes_literal.range(),
|
||||
AnyStringKind::from(bytes_literal.flags),
|
||||
) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
StringLike::FString(expr) => {
|
||||
for part in &expr.value {
|
||||
if let Some(diagnostic) = match part {
|
||||
ast::FStringPart::Literal(string) => check_string_or_bytes(
|
||||
locator,
|
||||
string.range(),
|
||||
AnyStringKind::from(string.flags),
|
||||
),
|
||||
ast::FStringPart::FString(f_string) => check_f_string(locator, f_string),
|
||||
} {
|
||||
ast::StringLikePart::FString(f_string) => {
|
||||
if let Some(diagnostic) = check_f_string(locator, f_string) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_python_ast as ast;
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -47,6 +48,14 @@ impl AlwaysFixableViolation for EmptyTypeCheckingBlock {
|
||||
|
||||
/// TCH005
|
||||
pub(crate) fn empty_type_checking_block(checker: &mut Checker, stmt: &ast::StmtIf) {
|
||||
if !typing::is_type_checking_block(stmt, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !stmt.elif_else_clauses.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let [stmt] = stmt.body.as_slice() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -101,6 +101,8 @@ TCH005.py:45:5: TCH005 [*] Found empty type-checking block
|
||||
44 | if TYPE_CHECKING:
|
||||
45 | pass # TCH005
|
||||
| ^^^^ TCH005
|
||||
46 |
|
||||
47 | # https://github.com/astral-sh/ruff/issues/11368
|
||||
|
|
||||
= help: Delete empty type-checking block
|
||||
|
||||
@@ -110,5 +112,6 @@ TCH005.py:45:5: TCH005 [*] Found empty type-checking block
|
||||
43 43 |
|
||||
44 |-if TYPE_CHECKING:
|
||||
45 |- pass # TCH005
|
||||
|
||||
|
||||
46 44 |
|
||||
47 45 | # https://github.com/astral-sh/ruff/issues/11368
|
||||
48 46 | if TYPE_CHECKING:
|
||||
|
||||
@@ -29,6 +29,28 @@ use crate::checkers::ast::Checker;
|
||||
/// "Hello, world!"
|
||||
/// ```
|
||||
///
|
||||
/// **Note:** to maintain compatibility with PyFlakes, this rule only flags
|
||||
/// f-strings that are part of an implicit concatenation if _none_ of the
|
||||
/// f-string segments contain placeholder expressions.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```python
|
||||
/// # Will not be flagged.
|
||||
/// (
|
||||
/// f"Hello,"
|
||||
/// f" {name}!"
|
||||
/// )
|
||||
///
|
||||
/// # Will be flagged.
|
||||
/// (
|
||||
/// f"Hello,"
|
||||
/// f" World!"
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// See [#10885](https://github.com/astral-sh/ruff/issues/10885) for more.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 498](https://www.python.org/dev/peps/pep-0498/)
|
||||
#[violation]
|
||||
|
||||
@@ -147,6 +147,28 @@ fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// And, collect all the property name with setter.
|
||||
for statement in body {
|
||||
let Stmt::FunctionDef(ast::StmtFunctionDef { decorator_list, .. }) = statement else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for decorator in decorator_list {
|
||||
let Some(ast::ExprAttribute { value, attr, .. }) =
|
||||
decorator.expression.as_attribute_expr()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if attr == "setter" {
|
||||
let Some(ast::ExprName { id, .. }) = value.as_name_expr() else {
|
||||
continue;
|
||||
};
|
||||
slots.insert(id.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second, find any assignments that aren't included in `__slots__`.
|
||||
let mut assignments = vec![];
|
||||
for statement in body {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::str::FromStr;
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, LiteralExpressionRef, UnaryOp};
|
||||
use ruff_python_ast::{self as ast, Expr, Int, LiteralExpressionRef, UnaryOp};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -38,7 +38,7 @@ impl LiteralType {
|
||||
LiteralType::Str => ast::ExprStringLiteral::default().into(),
|
||||
LiteralType::Bytes => ast::ExprBytesLiteral::default().into(),
|
||||
LiteralType::Int => ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(0.into()),
|
||||
value: ast::Number::Int(Int::from(0u8)),
|
||||
range: TextRange::default(),
|
||||
}
|
||||
.into(),
|
||||
|
||||
@@ -186,54 +186,38 @@ pub(crate) fn ambiguous_unicode_character_comment(
|
||||
|
||||
/// RUF001, RUF002
|
||||
pub(crate) fn ambiguous_unicode_character_string(checker: &mut Checker, string_like: StringLike) {
|
||||
let context = if checker.semantic().in_docstring() {
|
||||
let context = if checker.semantic().in_pep_257_docstring() {
|
||||
Context::Docstring
|
||||
} else {
|
||||
Context::String
|
||||
};
|
||||
|
||||
match string_like {
|
||||
StringLike::String(node) => {
|
||||
for literal in &node.value {
|
||||
let text = checker.locator().slice(literal);
|
||||
for part in string_like.parts() {
|
||||
match part {
|
||||
ast::StringLikePart::String(string_literal) => {
|
||||
let text = checker.locator().slice(string_literal);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
literal.range(),
|
||||
string_literal.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
StringLike::FString(node) => {
|
||||
for part in &node.value {
|
||||
match part {
|
||||
ast::FStringPart::Literal(literal) => {
|
||||
let text = checker.locator().slice(literal);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
literal.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
ast::FStringPart::FString(f_string) => {
|
||||
for literal in f_string.literals() {
|
||||
let text = checker.locator().slice(literal);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
literal.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
ast::StringLikePart::Bytes(_) => {}
|
||||
ast::StringLikePart::FString(f_string) => {
|
||||
for literal in f_string.literals() {
|
||||
let text = checker.locator().slice(literal);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
literal.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
StringLike::Bytes(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,6 @@ once_cell = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
|
||||
[features]
|
||||
serde = ["dep:serde", "ruff_text_size/serde"]
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::AnyNodeRef;
|
||||
@@ -394,9 +396,8 @@ impl LiteralExpressionRef<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum that holds a reference to a string-like literal from the AST.
|
||||
/// This includes string literals, bytes literals, and the literal parts of
|
||||
/// f-strings.
|
||||
/// An enum that holds a reference to a string-like expression from the AST. This includes string
|
||||
/// literals, bytes literals, and f-strings.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum StringLike<'a> {
|
||||
String(&'a ast::ExprStringLiteral),
|
||||
@@ -404,6 +405,17 @@ pub enum StringLike<'a> {
|
||||
FString(&'a ast::ExprFString),
|
||||
}
|
||||
|
||||
impl<'a> StringLike<'a> {
|
||||
/// Returns an iterator over the [`StringLikePart`] contained in this string-like expression.
|
||||
pub fn parts(&self) -> StringLikePartIter<'_> {
|
||||
match self {
|
||||
StringLike::String(expr) => StringLikePartIter::String(expr.value.iter()),
|
||||
StringLike::Bytes(expr) => StringLikePartIter::Bytes(expr.value.iter()),
|
||||
StringLike::FString(expr) => StringLikePartIter::FString(expr.value.iter()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprStringLiteral> for StringLike<'a> {
|
||||
fn from(value: &'a ast::ExprStringLiteral) -> Self {
|
||||
StringLike::String(value)
|
||||
@@ -431,3 +443,81 @@ impl Ranged for StringLike<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum that holds a reference to an individual part of a string-like expression.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum StringLikePart<'a> {
|
||||
String(&'a ast::StringLiteral),
|
||||
Bytes(&'a ast::BytesLiteral),
|
||||
FString(&'a ast::FString),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StringLiteral> for StringLikePart<'a> {
|
||||
fn from(value: &'a ast::StringLiteral) -> Self {
|
||||
StringLikePart::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::BytesLiteral> for StringLikePart<'a> {
|
||||
fn from(value: &'a ast::BytesLiteral) -> Self {
|
||||
StringLikePart::Bytes(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::FString> for StringLikePart<'a> {
|
||||
fn from(value: &'a ast::FString) -> Self {
|
||||
StringLikePart::FString(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for StringLikePart<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
StringLikePart::String(part) => part.range(),
|
||||
StringLikePart::Bytes(part) => part.range(),
|
||||
StringLikePart::FString(part) => part.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over all the [`StringLikePart`] of a string-like expression.
|
||||
///
|
||||
/// This is created by the [`StringLike::parts`] method.
|
||||
pub enum StringLikePartIter<'a> {
|
||||
String(std::slice::Iter<'a, ast::StringLiteral>),
|
||||
Bytes(std::slice::Iter<'a, ast::BytesLiteral>),
|
||||
FString(std::slice::Iter<'a, ast::FStringPart>),
|
||||
}
|
||||
|
||||
impl<'a> Iterator for StringLikePartIter<'a> {
|
||||
type Item = StringLikePart<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let part = match self {
|
||||
StringLikePartIter::String(inner) => StringLikePart::String(inner.next()?),
|
||||
StringLikePartIter::Bytes(inner) => StringLikePart::Bytes(inner.next()?),
|
||||
StringLikePartIter::FString(inner) => {
|
||||
let part = inner.next()?;
|
||||
match part {
|
||||
ast::FStringPart::Literal(string_literal) => {
|
||||
StringLikePart::String(string_literal)
|
||||
}
|
||||
ast::FStringPart::FString(f_string) => StringLikePart::FString(f_string),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(part)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
match self {
|
||||
StringLikePartIter::String(inner) => inner.size_hint(),
|
||||
StringLikePartIter::Bytes(inner) => inner.size_hint(),
|
||||
StringLikePartIter::FString(inner) => inner.size_hint(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for StringLikePartIter<'_> {}
|
||||
impl ExactSizeIterator for StringLikePartIter<'_> {}
|
||||
|
||||
@@ -1582,15 +1582,15 @@ mod tests {
|
||||
ctx: ExprContext::Load,
|
||||
});
|
||||
let constant_one = Expr::NumberLiteral(ExprNumberLiteral {
|
||||
value: Number::Int(1.into()),
|
||||
value: Number::Int(Int::from(1u8)),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let constant_two = Expr::NumberLiteral(ExprNumberLiteral {
|
||||
value: Number::Int(2.into()),
|
||||
value: Number::Int(Int::from(2u8)),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let constant_three = Expr::NumberLiteral(ExprNumberLiteral {
|
||||
value: Number::Int(3.into()),
|
||||
value: Number::Int(Int::from(3u8)),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let type_var_one = TypeParam::TypeVar(TypeParamTypeVar {
|
||||
|
||||
@@ -10,7 +10,7 @@ impl FromStr for Int {
|
||||
|
||||
/// Parse an [`Int`] from a string.
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.parse::<i64>() {
|
||||
match s.parse::<u64>() {
|
||||
Ok(value) => Ok(Int::small(value)),
|
||||
Err(err) => {
|
||||
if matches!(
|
||||
@@ -31,7 +31,7 @@ impl Int {
|
||||
pub const ONE: Int = Int(Number::Small(1));
|
||||
|
||||
/// Create an [`Int`] to represent a value that can be represented as an `i64`.
|
||||
fn small(value: i64) -> Self {
|
||||
fn small(value: u64) -> Self {
|
||||
Self(Number::Small(value))
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ impl Int {
|
||||
radix: u32,
|
||||
token: &str,
|
||||
) -> Result<Self, std::num::ParseIntError> {
|
||||
match i64::from_str_radix(number, radix) {
|
||||
match u64::from_str_radix(number, radix) {
|
||||
Ok(value) => Ok(Int::small(value)),
|
||||
Err(err) => {
|
||||
if matches!(
|
||||
@@ -88,6 +88,14 @@ impl Int {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an u64, if it can be represented as that data type.
|
||||
pub const fn as_u64(&self) -> Option<u64> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => Some(*small),
|
||||
Number::Big(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an i8, if it can be represented as that data type.
|
||||
pub fn as_i8(&self) -> Option<i8> {
|
||||
match &self.0 {
|
||||
@@ -113,9 +121,9 @@ impl Int {
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an i64, if it can be represented as that data type.
|
||||
pub const fn as_i64(&self) -> Option<i64> {
|
||||
pub fn as_i64(&self) -> Option<i64> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => Some(*small),
|
||||
Number::Small(small) => i64::try_from(*small).ok(),
|
||||
Number::Big(_) => None,
|
||||
}
|
||||
}
|
||||
@@ -177,51 +185,33 @@ impl PartialEq<i64> for Int {
|
||||
|
||||
impl From<u8> for Int {
|
||||
fn from(value: u8) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
Self::small(u64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Int {
|
||||
fn from(value: u16) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
Self::small(u64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Int {
|
||||
fn from(value: u32) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
Self::small(u64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i8> for Int {
|
||||
fn from(value: i8) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i16> for Int {
|
||||
fn from(value: i16) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Int {
|
||||
fn from(value: i32) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Int {
|
||||
fn from(value: i64) -> Self {
|
||||
impl From<u64> for Int {
|
||||
fn from(value: u64) -> Self {
|
||||
Self::small(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum Number {
|
||||
/// A "small" number that can be represented as an `i64`.
|
||||
Small(i64),
|
||||
/// A "large" number that cannot be represented as an `i64`.
|
||||
/// A "small" number that can be represented as an `u64`.
|
||||
Small(u64),
|
||||
/// A "large" number that cannot be represented as an `u64`.
|
||||
Big(Box<str>),
|
||||
}
|
||||
|
||||
|
||||
@@ -1087,6 +1087,24 @@ pub struct FStringFormatSpec {
|
||||
pub elements: Vec<FStringElement>,
|
||||
}
|
||||
|
||||
impl FStringFormatSpec {
|
||||
/// Returns an iterator over all the [`FStringLiteralElement`] nodes contained in this format
|
||||
/// spec of the f-string.
|
||||
pub fn literals(&self) -> impl Iterator<Item = &FStringLiteralElement> {
|
||||
self.elements
|
||||
.iter()
|
||||
.filter_map(|element| element.as_literal())
|
||||
}
|
||||
|
||||
/// Returns an iterator over all the [`FStringExpressionElement`] nodes contained in this
|
||||
/// format spec of the f-string.
|
||||
pub fn expressions(&self) -> impl Iterator<Item = &FStringExpressionElement> {
|
||||
self.elements
|
||||
.iter()
|
||||
.filter_map(|element| element.as_expression())
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for FStringFormatSpec {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
|
||||
@@ -11,7 +11,7 @@ repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest= false
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ruff_cache = { path = "../ruff_cache" }
|
||||
@@ -49,6 +49,10 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
similar = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
# Used via `CacheKey` macro expansion.
|
||||
ignored = ["ruff_cache"]
|
||||
|
||||
[[test]]
|
||||
name = "ruff_python_formatter_fixtures"
|
||||
path = "tests/fixtures.rs"
|
||||
|
||||
@@ -1777,7 +1777,7 @@ def f(arg=%timeit a = b):
|
||||
#[test]
|
||||
fn test_numbers() {
|
||||
let source =
|
||||
"0x2f 0o12 0b1101 0 123 123_45_67_890 0.2 1e+2 2.1e3 2j 2.2j 000 0x995DC9BBDF1939FA";
|
||||
"0x2f 0o12 0b1101 0 123 123_45_67_890 0.2 1e+2 2.1e3 2j 2.2j 000 0x995DC9BBDF1939FA 0x995DC9BBDF1939FA995DC9BBDF1939FA";
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
|
||||
@@ -79,12 +79,18 @@ expression: lex_source(source)
|
||||
),
|
||||
(
|
||||
Int {
|
||||
value: 0x995DC9BBDF1939FA,
|
||||
value: 11051210869376104954,
|
||||
},
|
||||
64..82,
|
||||
),
|
||||
(
|
||||
Int {
|
||||
value: 0x995DC9BBDF1939FA995DC9BBDF1939FA,
|
||||
},
|
||||
83..117,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
82..82,
|
||||
117..117,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1643,9 +1643,17 @@ impl<'a> SemanticModel<'a> {
|
||||
.intersects(SemanticModelFlags::TYPE_CHECKING_BLOCK)
|
||||
}
|
||||
|
||||
/// Return `true` if the model is in a docstring.
|
||||
pub const fn in_docstring(&self) -> bool {
|
||||
self.flags.intersects(SemanticModelFlags::DOCSTRING)
|
||||
/// Return `true` if the model is in a docstring as described in [PEP 257].
|
||||
///
|
||||
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
|
||||
pub const fn in_pep_257_docstring(&self) -> bool {
|
||||
self.flags.intersects(SemanticModelFlags::PEP_257_DOCSTRING)
|
||||
}
|
||||
|
||||
/// Return `true` if the model is in an attribute docstring.
|
||||
pub const fn in_attribute_docstring(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::ATTRIBUTE_DOCSTRING)
|
||||
}
|
||||
|
||||
/// Return `true` if the model has traversed past the "top-of-file" import boundary.
|
||||
@@ -2082,7 +2090,7 @@ bitflags! {
|
||||
/// ```
|
||||
const COMPREHENSION_ASSIGNMENT = 1 << 20;
|
||||
|
||||
/// The model is in a module / class / function docstring.
|
||||
/// The model is in a docstring as described in [PEP 257].
|
||||
///
|
||||
/// For example, the model could be visiting either the module, class,
|
||||
/// or function docstring in:
|
||||
@@ -2099,7 +2107,9 @@ bitflags! {
|
||||
/// """Function docstring."""
|
||||
/// pass
|
||||
/// ```
|
||||
const DOCSTRING = 1 << 21;
|
||||
///
|
||||
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
|
||||
const PEP_257_DOCSTRING = 1 << 21;
|
||||
|
||||
/// The model is visiting the r.h.s. of a module-level `__all__` definition.
|
||||
///
|
||||
@@ -2136,6 +2146,31 @@ bitflags! {
|
||||
/// while traversing the AST. (This only happens in stub files.)
|
||||
const DEFERRED_CLASS_BASE = 1 << 25;
|
||||
|
||||
/// The model is in an attribute docstring.
|
||||
///
|
||||
/// An attribute docstring is a string literal immediately following an assignment or an
|
||||
/// annotated assignment statement. The context in which this is valid are:
|
||||
/// 1. At the top level of a module
|
||||
/// 2. At the top level of a class definition i.e., a class attribute
|
||||
///
|
||||
/// For example:
|
||||
/// ```python
|
||||
/// a = 1
|
||||
/// """This is an attribute docstring for `a` variable"""
|
||||
///
|
||||
///
|
||||
/// class Foo:
|
||||
/// b = 1
|
||||
/// """This is an attribute docstring for `Foo.b` class variable"""
|
||||
/// ```
|
||||
///
|
||||
/// Unlike other kinds of docstrings as described in [PEP 257], attribute docstrings are
|
||||
/// discarded at runtime. However, they are used by some documentation renderers and
|
||||
/// static-analysis tools.
|
||||
///
|
||||
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
|
||||
const ATTRIBUTE_DOCSTRING = 1 << 26;
|
||||
|
||||
/// The context is in any type annotation.
|
||||
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();
|
||||
|
||||
|
||||
@@ -5,10 +5,96 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
(minor_version, module),
|
||||
(
|
||||
_,
|
||||
"_ast"
|
||||
"__hello__"
|
||||
| "__phello__"
|
||||
| "_abc"
|
||||
| "_ast"
|
||||
| "_asyncio"
|
||||
| "_bisect"
|
||||
| "_blake2"
|
||||
| "_bz2"
|
||||
| "_codecs"
|
||||
| "_codecs_cn"
|
||||
| "_codecs_hk"
|
||||
| "_codecs_iso2022"
|
||||
| "_codecs_jp"
|
||||
| "_codecs_kr"
|
||||
| "_codecs_tw"
|
||||
| "_collections"
|
||||
| "_collections_abc"
|
||||
| "_compat_pickle"
|
||||
| "_compression"
|
||||
| "_contextvars"
|
||||
| "_crypt"
|
||||
| "_csv"
|
||||
| "_ctypes"
|
||||
| "_ctypes_test"
|
||||
| "_curses"
|
||||
| "_curses_panel"
|
||||
| "_datetime"
|
||||
| "_dbm"
|
||||
| "_decimal"
|
||||
| "_elementtree"
|
||||
| "_frozen_importlib"
|
||||
| "_frozen_importlib_external"
|
||||
| "_functools"
|
||||
| "_gdbm"
|
||||
| "_hashlib"
|
||||
| "_heapq"
|
||||
| "_imp"
|
||||
| "_io"
|
||||
| "_json"
|
||||
| "_locale"
|
||||
| "_lsprof"
|
||||
| "_lzma"
|
||||
| "_markupbase"
|
||||
| "_md5"
|
||||
| "_msi"
|
||||
| "_multibytecodec"
|
||||
| "_multiprocessing"
|
||||
| "_opcode"
|
||||
| "_operator"
|
||||
| "_osx_support"
|
||||
| "_overlapped"
|
||||
| "_pickle"
|
||||
| "_posixsubprocess"
|
||||
| "_py_abc"
|
||||
| "_pydecimal"
|
||||
| "_pyio"
|
||||
| "_queue"
|
||||
| "_random"
|
||||
| "_scproxy"
|
||||
| "_sha1"
|
||||
| "_sha3"
|
||||
| "_signal"
|
||||
| "_sitebuiltins"
|
||||
| "_socket"
|
||||
| "_sqlite3"
|
||||
| "_sre"
|
||||
| "_ssl"
|
||||
| "_stat"
|
||||
| "_string"
|
||||
| "_strptime"
|
||||
| "_struct"
|
||||
| "_symtable"
|
||||
| "_testbuffer"
|
||||
| "_testcapi"
|
||||
| "_testconsole"
|
||||
| "_testimportmultiple"
|
||||
| "_testmultiphase"
|
||||
| "_thread"
|
||||
| "_threading_local"
|
||||
| "_tkinter"
|
||||
| "_tracemalloc"
|
||||
| "_uuid"
|
||||
| "_warnings"
|
||||
| "_weakref"
|
||||
| "_weakrefset"
|
||||
| "_winapi"
|
||||
| "_xxtestfuzz"
|
||||
| "abc"
|
||||
| "aifc"
|
||||
| "antigravity"
|
||||
| "argparse"
|
||||
| "array"
|
||||
| "ast"
|
||||
@@ -65,6 +151,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
| "ftplib"
|
||||
| "functools"
|
||||
| "gc"
|
||||
| "genericpath"
|
||||
| "getopt"
|
||||
| "getpass"
|
||||
| "gettext"
|
||||
@@ -76,6 +163,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
| "hmac"
|
||||
| "html"
|
||||
| "http"
|
||||
| "idlelib"
|
||||
| "imaplib"
|
||||
| "imghdr"
|
||||
| "importlib"
|
||||
@@ -103,8 +191,11 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
| "netrc"
|
||||
| "nis"
|
||||
| "nntplib"
|
||||
| "nt"
|
||||
| "ntpath"
|
||||
| "nturl2path"
|
||||
| "numbers"
|
||||
| "opcode"
|
||||
| "operator"
|
||||
| "optparse"
|
||||
| "os"
|
||||
@@ -128,6 +219,8 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
| "py_compile"
|
||||
| "pyclbr"
|
||||
| "pydoc"
|
||||
| "pydoc_data"
|
||||
| "pyexpat"
|
||||
| "queue"
|
||||
| "quopri"
|
||||
| "random"
|
||||
@@ -173,8 +266,8 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
| "telnetlib"
|
||||
| "tempfile"
|
||||
| "termios"
|
||||
| "test"
|
||||
| "textwrap"
|
||||
| "this"
|
||||
| "threading"
|
||||
| "time"
|
||||
| "timeit"
|
||||
@@ -205,13 +298,19 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
| "xdrlib"
|
||||
| "xml"
|
||||
| "xmlrpc"
|
||||
| "xx"
|
||||
| "xxlimited"
|
||||
| "xxsubtype"
|
||||
| "zipapp"
|
||||
| "zipfile"
|
||||
| "zipimport"
|
||||
| "zlib"
|
||||
) | (
|
||||
7,
|
||||
"_dummy_thread"
|
||||
"_bootlocale"
|
||||
| "_dummy_thread"
|
||||
| "_sha256"
|
||||
| "_sha512"
|
||||
| "asynchat"
|
||||
| "asyncore"
|
||||
| "binhex"
|
||||
@@ -225,7 +324,14 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
| "symbol"
|
||||
) | (
|
||||
8,
|
||||
"_dummy_thread"
|
||||
"_bootlocale"
|
||||
| "_dummy_thread"
|
||||
| "_posixshmem"
|
||||
| "_sha256"
|
||||
| "_sha512"
|
||||
| "_statistics"
|
||||
| "_testinternalcapi"
|
||||
| "_xxsubinterpreters"
|
||||
| "asynchat"
|
||||
| "asyncore"
|
||||
| "binhex"
|
||||
@@ -238,7 +344,18 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
| "symbol"
|
||||
) | (
|
||||
9,
|
||||
"asynchat"
|
||||
"_aix_support"
|
||||
| "_bootlocale"
|
||||
| "_bootsubprocess"
|
||||
| "_peg_parser"
|
||||
| "_posixshmem"
|
||||
| "_sha256"
|
||||
| "_sha512"
|
||||
| "_statistics"
|
||||
| "_testinternalcapi"
|
||||
| "_xxsubinterpreters"
|
||||
| "_zoneinfo"
|
||||
| "asynchat"
|
||||
| "asyncore"
|
||||
| "binhex"
|
||||
| "distutils"
|
||||
@@ -246,31 +363,80 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
| "graphlib"
|
||||
| "imp"
|
||||
| "parser"
|
||||
| "peg_parser"
|
||||
| "smtpd"
|
||||
| "symbol"
|
||||
| "zoneinfo"
|
||||
) | (
|
||||
10,
|
||||
"asynchat"
|
||||
"_aix_support"
|
||||
| "_bootsubprocess"
|
||||
| "_posixshmem"
|
||||
| "_sha256"
|
||||
| "_sha512"
|
||||
| "_statistics"
|
||||
| "_testclinic"
|
||||
| "_testinternalcapi"
|
||||
| "_xxsubinterpreters"
|
||||
| "_zoneinfo"
|
||||
| "asynchat"
|
||||
| "asyncore"
|
||||
| "binhex"
|
||||
| "distutils"
|
||||
| "graphlib"
|
||||
| "idlelib"
|
||||
| "imp"
|
||||
| "smtpd"
|
||||
| "xxlimited_35"
|
||||
| "zoneinfo"
|
||||
) | (
|
||||
11,
|
||||
"asynchat"
|
||||
"__hello_alias__"
|
||||
| "__hello_only__"
|
||||
| "__phello_alias__"
|
||||
| "_aix_support"
|
||||
| "_bootsubprocess"
|
||||
| "_posixshmem"
|
||||
| "_sha256"
|
||||
| "_sha512"
|
||||
| "_statistics"
|
||||
| "_testclinic"
|
||||
| "_testinternalcapi"
|
||||
| "_tokenize"
|
||||
| "_typing"
|
||||
| "_xxsubinterpreters"
|
||||
| "_zoneinfo"
|
||||
| "asynchat"
|
||||
| "asyncore"
|
||||
| "distutils"
|
||||
| "graphlib"
|
||||
| "idlelib"
|
||||
| "imp"
|
||||
| "smtpd"
|
||||
| "tomllib"
|
||||
| "xxlimited_35"
|
||||
| "zoneinfo"
|
||||
) | (12, "graphlib" | "idlelib" | "tomllib" | "zoneinfo")
|
||||
) | (
|
||||
12,
|
||||
"__hello_alias__"
|
||||
| "__hello_only__"
|
||||
| "__phello_alias__"
|
||||
| "_aix_support"
|
||||
| "_posixshmem"
|
||||
| "_pydatetime"
|
||||
| "_pylong"
|
||||
| "_sha2"
|
||||
| "_statistics"
|
||||
| "_testclinic"
|
||||
| "_testinternalcapi"
|
||||
| "_testsinglephase"
|
||||
| "_tokenize"
|
||||
| "_typing"
|
||||
| "_xxinterpchannels"
|
||||
| "_xxsubinterpreters"
|
||||
| "_zoneinfo"
|
||||
| "graphlib"
|
||||
| "tomllib"
|
||||
| "xxlimited_35"
|
||||
| "zoneinfo"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,5 @@ ruff_source_file = { path = "../ruff_source_file" }
|
||||
itertools = { workspace = true }
|
||||
unicode-ident = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Access to the Ruff linting API for the LSP
|
||||
|
||||
use ruff_diagnostics::{Applicability, Diagnostic, DiagnosticKind, Fix};
|
||||
use ruff_diagnostics::{Applicability, Diagnostic, DiagnosticKind, Edit, Fix};
|
||||
use ruff_linter::{
|
||||
directives::{extract_directives, Flags},
|
||||
generate_noqa_edits,
|
||||
linter::{check_path, LinterResult, TokenSource},
|
||||
packaging::detect_package_root,
|
||||
registry::AsRule,
|
||||
@@ -24,17 +25,29 @@ use crate::{edit::ToRangeExt, PositionEncoding, DIAGNOSTIC_NAME};
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub(crate) struct AssociatedDiagnosticData {
|
||||
pub(crate) kind: DiagnosticKind,
|
||||
pub(crate) fix: Fix,
|
||||
/// 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<ruff_diagnostics::Edit>,
|
||||
}
|
||||
|
||||
/// Describes a fix for `fixed_diagnostic` that applies `document_edits` to the source.
|
||||
/// Describes a fix for `fixed_diagnostic` that may have quick fix
|
||||
/// edits available, `noqa` comment edits, or both.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct DiagnosticFix {
|
||||
/// The original diagnostic to be fixed
|
||||
pub(crate) fixed_diagnostic: lsp_types::Diagnostic,
|
||||
/// The message describing what the fix does.
|
||||
pub(crate) title: String,
|
||||
/// The NOQA code for the diagnostic.
|
||||
pub(crate) code: String,
|
||||
/// Edits to fix the diagnostic. If this is empty, a fix
|
||||
/// does not exist.
|
||||
pub(crate) edits: Vec<lsp_types::TextEdit>,
|
||||
/// Possible edit to add a `noqa` comment which will disable this diagnostic.
|
||||
pub(crate) noqa_edit: Option<lsp_types::TextEdit>,
|
||||
}
|
||||
|
||||
pub(crate) fn check(
|
||||
@@ -75,7 +88,7 @@ pub(crate) fn check(
|
||||
let indexer = Indexer::from_tokens(&tokens, &locator);
|
||||
|
||||
// Extract the `# noqa` and `# isort: skip` directives from the source.
|
||||
let directives = extract_directives(&tokens, Flags::empty(), &locator, &indexer);
|
||||
let directives = extract_directives(&tokens, Flags::all(), &locator, &indexer);
|
||||
|
||||
// Generate checks.
|
||||
let LinterResult {
|
||||
@@ -94,9 +107,20 @@ pub(crate) fn check(
|
||||
TokenSource::Tokens(tokens),
|
||||
);
|
||||
|
||||
let noqa_edits = generate_noqa_edits(
|
||||
&document_path,
|
||||
diagnostics.as_slice(),
|
||||
&locator,
|
||||
indexer.comment_ranges(),
|
||||
&linter_settings.external,
|
||||
&directives.noqa_line_for,
|
||||
stylist.line_ending(),
|
||||
);
|
||||
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| to_lsp_diagnostic(diagnostic, document, encoding))
|
||||
.zip(noqa_edits)
|
||||
.map(|(diagnostic, noqa_edit)| to_lsp_diagnostic(diagnostic, noqa_edit, document, encoding))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -118,14 +142,34 @@ pub(crate) fn fixes_for_diagnostics(
|
||||
})?;
|
||||
let edits = associated_data
|
||||
.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(),
|
||||
});
|
||||
.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,
|
||||
@@ -133,7 +177,8 @@ pub(crate) fn fixes_for_diagnostics(
|
||||
.kind
|
||||
.suggestion
|
||||
.unwrap_or(associated_data.kind.name),
|
||||
edits: edits.collect(),
|
||||
edits,
|
||||
noqa_edit,
|
||||
}))
|
||||
})
|
||||
.filter_map(crate::Result::transpose)
|
||||
@@ -142,6 +187,7 @@ pub(crate) fn fixes_for_diagnostics(
|
||||
|
||||
fn to_lsp_diagnostic(
|
||||
diagnostic: Diagnostic,
|
||||
noqa_edit: Option<Edit>,
|
||||
document: &crate::edit::Document,
|
||||
encoding: PositionEncoding,
|
||||
) -> lsp_types::Diagnostic {
|
||||
@@ -151,18 +197,19 @@ fn to_lsp_diagnostic(
|
||||
|
||||
let rule = kind.rule();
|
||||
|
||||
let data = fix.and_then(|fix| {
|
||||
fix.applies(Applicability::Unsafe)
|
||||
.then(|| {
|
||||
serde_json::to_value(&AssociatedDiagnosticData {
|
||||
kind: kind.clone(),
|
||||
fix,
|
||||
code: rule.noqa_code().to_string(),
|
||||
})
|
||||
.ok()
|
||||
let fix = fix.and_then(|fix| fix.applies(Applicability::Unsafe).then_some(fix));
|
||||
|
||||
let data = (fix.is_some() || noqa_edit.is_some())
|
||||
.then(|| {
|
||||
serde_json::to_value(&AssociatedDiagnosticData {
|
||||
kind: kind.clone(),
|
||||
fix,
|
||||
code: rule.noqa_code().to_string(),
|
||||
noqa_edit,
|
||||
})
|
||||
.flatten()
|
||||
});
|
||||
.ok()
|
||||
})
|
||||
.flatten();
|
||||
|
||||
let code = rule.noqa_code().to_string();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::edit::WorkspaceEditTracker;
|
||||
use crate::lint::fixes_for_diagnostics;
|
||||
use crate::lint::{fixes_for_diagnostics, DiagnosticFix};
|
||||
use crate::server::api::LSPResult;
|
||||
use crate::server::SupportedCodeAction;
|
||||
use crate::server::{client::Notifier, Result};
|
||||
@@ -29,13 +29,24 @@ impl super::BackgroundDocumentRequestHandler for CodeActions {
|
||||
|
||||
let supported_code_actions = supported_code_actions(params.context.only.clone());
|
||||
|
||||
let fixes = fixes_for_diagnostics(
|
||||
snapshot.document(),
|
||||
snapshot.encoding(),
|
||||
params.context.diagnostics,
|
||||
)
|
||||
.with_failure_code(ErrorCode::InternalError)?;
|
||||
|
||||
if snapshot.client_settings().fix_violation()
|
||||
&& supported_code_actions.contains(&SupportedCodeAction::QuickFix)
|
||||
{
|
||||
response.extend(
|
||||
quick_fix(&snapshot, params.context.diagnostics.clone())
|
||||
.with_failure_code(ErrorCode::InternalError)?,
|
||||
);
|
||||
response
|
||||
.extend(quick_fix(&snapshot, &fixes).with_failure_code(ErrorCode::InternalError)?);
|
||||
}
|
||||
|
||||
if snapshot.client_settings().noqa_comments()
|
||||
&& supported_code_actions.contains(&SupportedCodeAction::QuickFix)
|
||||
{
|
||||
response.extend(noqa_comments(&snapshot, &fixes));
|
||||
}
|
||||
|
||||
if snapshot.client_settings().fix_all()
|
||||
@@ -56,21 +67,19 @@ impl super::BackgroundDocumentRequestHandler for CodeActions {
|
||||
|
||||
fn quick_fix(
|
||||
snapshot: &DocumentSnapshot,
|
||||
diagnostics: Vec<types::Diagnostic>,
|
||||
fixes: &[DiagnosticFix],
|
||||
) -> crate::Result<Vec<CodeActionOrCommand>> {
|
||||
let document = snapshot.document();
|
||||
|
||||
let fixes = fixes_for_diagnostics(document, snapshot.encoding(), diagnostics)?;
|
||||
|
||||
fixes
|
||||
.into_iter()
|
||||
.iter()
|
||||
.filter(|fix| !fix.edits.is_empty())
|
||||
.map(|fix| {
|
||||
let mut tracker = WorkspaceEditTracker::new(snapshot.resolved_client_capabilities());
|
||||
|
||||
tracker.set_edits_for_document(
|
||||
snapshot.url().clone(),
|
||||
document.version(),
|
||||
fix.edits,
|
||||
fix.edits.clone(),
|
||||
)?;
|
||||
|
||||
Ok(types::CodeActionOrCommand::CodeAction(types::CodeAction {
|
||||
@@ -87,6 +96,36 @@ fn quick_fix(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn noqa_comments(snapshot: &DocumentSnapshot, fixes: &[DiagnosticFix]) -> Vec<CodeActionOrCommand> {
|
||||
fixes
|
||||
.iter()
|
||||
.filter_map(|fix| {
|
||||
let edit = fix.noqa_edit.clone()?;
|
||||
|
||||
let mut tracker = WorkspaceEditTracker::new(snapshot.resolved_client_capabilities());
|
||||
|
||||
tracker
|
||||
.set_edits_for_document(
|
||||
snapshot.url().clone(),
|
||||
snapshot.document().version(),
|
||||
vec![edit],
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
Some(types::CodeActionOrCommand::CodeAction(types::CodeAction {
|
||||
title: format!("{DIAGNOSTIC_NAME} ({}): Disable for this line", fix.code),
|
||||
kind: Some(types::CodeActionKind::QUICKFIX),
|
||||
edit: Some(tracker.into_workspace_edit()),
|
||||
diagnostics: Some(vec![fix.fixed_diagnostic.clone()]),
|
||||
data: Some(
|
||||
serde_json::to_value(snapshot.url()).expect("document url to serialize"),
|
||||
),
|
||||
..Default::default()
|
||||
}))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn fix_all(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCommand> {
|
||||
let document = snapshot.document();
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ pub(crate) struct ResolvedClientSettings {
|
||||
fix_all: bool,
|
||||
organize_imports: bool,
|
||||
lint_enable: bool,
|
||||
// TODO(jane): Remove once noqa auto-fix is implemented
|
||||
#[allow(dead_code)]
|
||||
disable_rule_comment_enable: bool,
|
||||
fix_violation_enable: bool,
|
||||
editor_settings: ResolvedEditorSettings,
|
||||
@@ -323,6 +321,10 @@ impl ResolvedClientSettings {
|
||||
self.lint_enable
|
||||
}
|
||||
|
||||
pub(crate) fn noqa_comments(&self) -> bool {
|
||||
self.disable_rule_comment_enable
|
||||
}
|
||||
|
||||
pub(crate) fn fix_violation(&self) -> bool {
|
||||
self.fix_violation_enable
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ toml = { workspace = true }
|
||||
ruff_linter = { path = "../ruff_linter", features = ["clap", "test-rules"] }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
# Used via macro expansion.
|
||||
ignored = ["colored"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
schemars = ["dep:schemars", "ruff_formatter/schemars", "ruff_python_formatter/schemars"]
|
||||
|
||||
16
playground/api/package-lock.json
generated
16
playground/api/package-lock.json
generated
@@ -16,7 +16,7 @@
|
||||
"@cloudflare/workers-types": "^4.20230801.0",
|
||||
"miniflare": "^3.20230801.1",
|
||||
"typescript": "^5.1.6",
|
||||
"wrangler": "3.53.1"
|
||||
"wrangler": "3.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/kv-asset-handler": {
|
||||
@@ -1070,9 +1070,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/miniflare": {
|
||||
"version": "3.20240419.0",
|
||||
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240419.0.tgz",
|
||||
"integrity": "sha512-fIev1PP4H+fQp5FtvzHqRY2v5s+jxh/a0xAhvM5fBNXvxWX7Zod1OatXfXwYbse3hqO3KeVMhb0osVtrW0NwJg==",
|
||||
"version": "3.20240419.1",
|
||||
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240419.1.tgz",
|
||||
"integrity": "sha512-Q9n0W07uUD/u0c/b03E4iogeXOAMjZnE3P7B5Yi8sPaZAx6TYWwjurGBja+Pg2yILN2iMaliEobfVyAKss33cA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "0.8.1",
|
||||
@@ -1516,9 +1516,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wrangler": {
|
||||
"version": "3.53.1",
|
||||
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.53.1.tgz",
|
||||
"integrity": "sha512-bdMRQdHYdvowIwOhEMFkARIZUh56aDw7HLUZ/2JreBjj760osXE4Fc4L1TCkfRRBWgB6/LKF5LA4OcvORMYmHg==",
|
||||
"version": "3.55.0",
|
||||
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.55.0.tgz",
|
||||
"integrity": "sha512-VhtCioKxOdVqkHa8jQ6C6bX3by2Ko0uM0DKzrA+6lBZvfDUlGDWSOPiG+1fOHBHj2JTVBntxWCztXP6L+Udr8w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@cloudflare/kv-asset-handler": "0.3.2",
|
||||
@@ -1527,7 +1527,7 @@
|
||||
"blake3-wasm": "^2.1.5",
|
||||
"chokidar": "^3.5.3",
|
||||
"esbuild": "0.17.19",
|
||||
"miniflare": "3.20240419.0",
|
||||
"miniflare": "3.20240419.1",
|
||||
"nanoid": "^3.3.3",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"resolve": "^1.22.8",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"@cloudflare/workers-types": "^4.20230801.0",
|
||||
"miniflare": "^3.20230801.1",
|
||||
"typescript": "^5.1.6",
|
||||
"wrangler": "3.53.1"
|
||||
"wrangler": "3.55.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
6
playground/package-lock.json
generated
6
playground/package-lock.json
generated
@@ -1046,9 +1046,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==",
|
||||
"version": "18.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz",
|
||||
"integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
|
||||
@@ -39,6 +39,7 @@ KNOWN_FORMATTING_VIOLATIONS = [
|
||||
"blank-lines-before-nested-definition",
|
||||
"blank-lines-top-level",
|
||||
"explicit-string-concatenation",
|
||||
"f-string-missing-placeholders",
|
||||
"indent-with-spaces",
|
||||
"indentation-with-invalid-multiple",
|
||||
"line-too-long",
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
"""Vendored from scripts/mkstdlibs.py in PyCQA/isort.
|
||||
|
||||
Source:
|
||||
https://github.com/PyCQA/isort/blob/e321a670d0fefdea0e04ed9d8d696434cf49bdec/scripts/mkstdlibs.py
|
||||
|
||||
Only the generation of the file has been modified for use in this project.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from sphinx.ext.intersphinx import fetch_inventory
|
||||
from stdlibs import stdlib_module_names
|
||||
|
||||
URL = "https://docs.python.org/{}/objects.inv"
|
||||
PATH = Path("crates") / "ruff_python_stdlib" / "src" / "sys.rs"
|
||||
VERSIONS: list[tuple[int, int]] = [
|
||||
(3, 7),
|
||||
@@ -23,24 +14,12 @@ VERSIONS: list[tuple[int, int]] = [
|
||||
(3, 12),
|
||||
]
|
||||
|
||||
|
||||
class FakeConfig:
|
||||
intersphinx_timeout = None
|
||||
tls_verify = True
|
||||
user_agent = ""
|
||||
|
||||
|
||||
class FakeApp:
|
||||
srcdir = ""
|
||||
config = FakeConfig()
|
||||
|
||||
|
||||
with PATH.open("w") as f:
|
||||
f.write(
|
||||
"""\
|
||||
//! This file is generated by `scripts/generate_known_standard_library.py`
|
||||
|
||||
pub fn is_known_standard_library(minor_version: u32, module: &str) -> bool {
|
||||
pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
|
||||
matches!((minor_version, module),
|
||||
""",
|
||||
)
|
||||
@@ -48,9 +27,6 @@ pub fn is_known_standard_library(minor_version: u32, module: &str) -> bool {
|
||||
modules_by_version = {}
|
||||
|
||||
for major_version, minor_version in VERSIONS:
|
||||
url = URL.format(f"{major_version}.{minor_version}")
|
||||
invdata = fetch_inventory(FakeApp(), "", url)
|
||||
|
||||
modules = {
|
||||
"_ast",
|
||||
"posixpath",
|
||||
@@ -61,10 +37,9 @@ pub fn is_known_standard_library(minor_version: u32, module: &str) -> bool {
|
||||
"sre",
|
||||
}
|
||||
|
||||
for module in invdata["py:module"]:
|
||||
root, *_ = module.split(".")
|
||||
if root not in ["__future__", "__main__"]:
|
||||
modules.add(root)
|
||||
for module in stdlib_module_names(f"{major_version}.{minor_version}"):
|
||||
if module not in ["__future__", "__main__"]:
|
||||
modules.add(module)
|
||||
|
||||
modules_by_version[minor_version] = modules
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "scripts"
|
||||
version = "0.0.1"
|
||||
dependencies = ["sphinx"]
|
||||
dependencies = ["stdlibs"]
|
||||
requires-python = ">=3.8"
|
||||
|
||||
[tool.black]
|
||||
|
||||
Reference in New Issue
Block a user