Compare commits

...

24 Commits

Author SHA1 Message Date
Dhruv Manilawala
ce4d4ae6ac Add methods to iter over format spec elements 2024-05-13 14:17:23 +05:30
Dhruv Manilawala
128414cd95 Add Iterator impl for StringLike parts 2024-05-13 13:48:54 +05:30
Charlie Marsh
be0ccabbaa Add cargo shear to CI (#11393) 2024-05-12 22:23:36 -04:00
Charlie Marsh
6cec82fff8 Get cargo shear passing (#11392)
## Summary

Remove some unused dependencies, add a few ignores.
2024-05-13 01:56:24 +00:00
Tom Kuson
5ab4cc86c2 Reword future-rewritable-type-annotation (FA100) message (#11381)
## Summary

Changes `future-rewritable-type-annotation` (`FA100`) message to be less
confusing. Uses phrasing from the rule documentation to be consistent.
For example,

```
from_typing_import.py:5:13: FA100 Add `from __future__ import annotations` to rewrite `typing.List` more succinctly
```

Closes #10573.

## Test Plan

`cargo nextest run`
2024-05-13 01:38:49 +00:00
renovate[bot]
bc7856e899 Update pre-commit dependencies (#11391) 2024-05-12 21:22:04 -04:00
Rahul Modpur
6a28f3448e Migrate sys.rs generation to stdlibs (#11374)
## Summary

Closes #11347
2024-05-12 21:21:51 -04:00
renovate[bot]
7c824faa88 Update Rust crate thiserror to v1.0.60 (#11390) 2024-05-13 00:36:08 +00:00
renovate[bot]
12da5968a0 Update Rust crate serde_json to v1.0.117 (#11388) 2024-05-13 00:35:46 +00:00
renovate[bot]
a747b3f2a1 Update Rust crate syn to v2.0.63 (#11389) 2024-05-13 00:35:23 +00:00
renovate[bot]
01a0e6cc7e Update Rust crate serde to v1.0.201 (#11387) 2024-05-13 00:34:34 +00:00
renovate[bot]
a8b06537c7 Update Rust crate anyhow to v1.0.83 (#11384) 2024-05-13 00:34:00 +00:00
renovate[bot]
7b8fe25d32 Update Rust crate schemars to v0.8.19 (#11386) 2024-05-13 00:33:29 +00:00
renovate[bot]
a50416a6d7 Update Rust crate proc-macro2 to v1.0.82 (#11385) 2024-05-13 00:33:05 +00:00
renovate[bot]
41e53d59ab Update NPM Development dependencies (#11383) 2024-05-13 00:30:58 +00:00
Dhruv Manilawala
0fc6cf9bee Avoid PLE0237 for property with setter (#11377)
## Summary

Should this consider the decorator only if the name is actually a
property or is the logic in this PR correct?

fixes: #11358

## Test Plan

Add test case.
2024-05-12 20:23:00 -04:00
Dhruv Manilawala
d835b3e218 Avoid TCH005 for if stmt with elif/else block (#11376)
## Summary

This PR fixes a bug where the auto-fix for `TCH005` would delete the
entire `if` statement.

The fix in this PR is to not consider it a violation if there are any
`elif`/`else` blocks. This also matches the behavior of the original
plugin.

fixes: #11368 

## Test plan

Add test cases.
2024-05-12 20:22:25 -04:00
Jane Lewis
d7f093ef9e ruff server: Support noqa comment code action (#11276)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/10594.

Code actions to disable a diagnostic via `noqa` comment are now
available.


https://github.com/astral-sh/ruff/assets/19577865/6d3bcf11-a9d9-499b-8c7f-a10cd39cfbba

`DiagnosticFix` has been changed so that `noqa` code actions appear even
for diagnostics with no available quick fix. It can contain quick fix
edits, `noqa` comment edits, or both.

## Test Plan

The scenarios that need to be tested are as follows:
* A code action to disable a diagnostic should be available for every
diagnostic.
* Using this code action should append to the appropriate line with the
diagnostic, or modify an existing `noqa` comment.
* Adding a `noqa` comment manually should make a diagnostic disappear
* `Fix all auto-fixable problems` should not add `noqa` comments
* Removing a code from a `noqa` comment should make the diagnostic
re-appear
2024-05-12 14:39:46 -07:00
Charlie Marsh
4b330b11c6 [flake8-pie] Preserve parentheses in unnecessary-dict-kwargs (#11372)
## Summary

Closes https://github.com/astral-sh/ruff/issues/11371.
2024-05-11 18:04:54 -04:00
Jane Lewis
890cc325d5 Split add_noqa process into distinctive edit generation and edit application stages (#11265)
## Summary

`--add-noqa` now runs in two stages: first, the linter finds all
diagnostics that need noqa comments and generate edits on a per-line
basis. Second, these edits are applied, in order, to the document.

A public-facing function, `generate_noqa_edits`, has also been
introduced, which returns noqa edits generated on a per-diagnostic
basis. This will be used by `ruff server` for noqa comment quick-fixes.

## Test Plan

Unit tests have been updated.
2024-05-10 23:16:52 +00:00
Douglas Thor
0726e82342 [pyflakes] Update docs to describe WAI behavior (F541) (#11362)
Addresses this comment:
https://github.com/astral-sh/ruff/issues/11357#issuecomment-2104714029


## Summary

The docs for F541 did not mention some surprising, but WAI, behavior
regarding implicit string concatenation. Update the docs to describe the
behavior.

Here's how things rendered for me locally:


![image](https://github.com/astral-sh/ruff/assets/5386897/32067121-b190-4268-b987-ff37df11a618)
2024-05-10 19:10:34 +00:00
Dhruv Manilawala
f79c980e17 Add support for attribute docstring in the semantic model (#11315)
## Summary

This PR adds updates the semantic model to detect attribute docstring.

Refer to [PEP 258](https://peps.python.org/pep-0258/#attribute-docstrings) 
for the definition of an attribute docstring.

This PR doesn't add full support for it but only considers string
literals as attribute docstring for the following cases:
1. A string literal following an assignment statement in the **global
scope**.
2. A global class attribute

For an assignment statement, it's considered an attribute docstring only
if the target expression is a name expression (`x = 1`). So, chained
assignment, multiple assignment or unpacking, and starred expression,
which are all valid in the target position, aren't considered here.

In `__init__` method, an assignment to the `self` variable like `self.x = 1`
is also a candidate for an attribute docstring. **This PR does not
support this position.**

## Test Plan

I used the following source code along with a print statement to verify
that the attribute docstring detection is correct.

Refer to the PR description for the code snippet.

I'll add this in the follow-up PR
(https://github.com/astral-sh/ruff/pull/11302) which uses this method.
2024-05-10 20:27:56 +05:30
Charlie Marsh
35ba3c91ce Use u64 instead of i64 in Int type (#11356)
## Summary

I believe the value here is always unsigned, since we represent `-42` as
a unary operator on `42`.
2024-05-10 13:35:15 +00:00
konsti
1f794077ec Allow clippy map-unwrap-or (#11354)
`map_or` is harder too read than the `.map().unwrap()` version.

See also https://github.com/astral-sh/uv/pull/3498
2024-05-09 21:22:09 +00:00
53 changed files with 1164 additions and 381 deletions

View File

@@ -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"

View File

@@ -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
View File

@@ -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",

View File

@@ -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"

View File

@@ -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]

View File

@@ -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 }

View File

@@ -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(())
}

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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_);

View File

@@ -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);
}
}

View File

@@ -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")]

View File

@@ -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()
))]
);
}
}

View File

@@ -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}`")
}
}

View File

@@ -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")
|

View File

@@ -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")
|

View File

@@ -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]]], {})
|

View File

@@ -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")
|

View File

@@ -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")
|

View File

@@ -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(),

View File

@@ -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

View File

@@ -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),
}
}

View File

@@ -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);

View File

@@ -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);
};
}
}
}
}

View File

@@ -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;
};

View File

@@ -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:

View File

@@ -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]

View File

@@ -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 {

View File

@@ -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(),

View File

@@ -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(_) => (),
}
}

View File

@@ -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"]

View File

@@ -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<'_> {}

View File

@@ -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 {

View File

@@ -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>),
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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));
}

View File

@@ -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,
),
]

View File

@@ -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();

View File

@@ -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"
)
)
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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();

View File

@@ -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
}

View File

@@ -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"]

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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": "*",

View File

@@ -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",

View File

@@ -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

View File

@@ -1,7 +1,7 @@
[project]
name = "scripts"
version = "0.0.1"
dependencies = ["sphinx"]
dependencies = ["stdlibs"]
requires-python = ">=3.8"
[tool.black]