Compare commits

..

40 Commits

Author SHA1 Message Date
Charlie Marsh
e9fc63331a Reverts 2023-05-21 15:22:52 -04:00
Charlie Marsh
df3b95a73d clean up 2023-05-21 14:52:04 -04:00
Charlie Marsh
a4432102f1 Theres enough here for a proposal 2023-05-21 14:19:41 -04:00
Charlie Marsh
3295ccfbc4 Rename some stuff 2023-05-21 14:19:41 -04:00
Charlie Marsh
7b315b84e2 Make generic 2023-05-21 14:19:41 -04:00
Charlie Marsh
bb2adb3017 Introduce traits 2023-05-21 14:19:41 -04:00
Charlie Marsh
5536d2befc Benchmark UP 2023-05-21 14:19:41 -04:00
Charlie Marsh
b4824979b0 Separate struct 2023-05-21 14:19:41 -04:00
Charlie Marsh
8a2f58065e Dispatch rules off a vector 2023-05-21 14:19:41 -04:00
Arne de Laat
8ca3977602 Fix false-positive for TRY302 if exception cause is given (#4559) 2023-05-21 11:49:53 -04:00
Jacob Coffee
6db05d8cc6 Starlite -> Litestar (#4554) 2023-05-21 09:55:26 -04:00
Jonathan Plasse
fc63c6f2e2 Fix PLE01310 typo (#4550) 2023-05-20 19:34:03 +00:00
Jonathan Plasse
f7f5bc9085 Fix SIM401 snapshot (#4547) 2023-05-20 14:18:19 -04:00
Charlie Marsh
6b85430a14 Ignore #region code folding marks in eradicate rules (#4546) 2023-05-20 16:45:49 +00:00
Jonathan Plasse
a68c865010 Fix SIM110 and SIM111 ranges (#4545) 2023-05-20 12:40:35 -04:00
Charlie Marsh
fe7f2e2e4d Move submodule alias resolution into Context (#4543) 2023-05-20 16:34:10 +00:00
Felipe Peter
0a3cf8ba11 Fix typos in docs (#4540) 2023-05-20 07:23:17 -04:00
Charlie Marsh
bf5b463c0d Include empty success test in JUnit output (#4537) 2023-05-20 03:38:51 +00:00
Charlie Marsh
6aa9900c03 Improve handling of __qualname__, __module__, and __class__ (#4512) 2023-05-20 03:03:45 +00:00
Charlie Marsh
9e21414294 Improve reference resolution for deferred-annotations-within-classes (#4509) 2023-05-20 02:54:18 +00:00
Charlie Marsh
bb4e674415 Move reference-resolution into Context (#4510) 2023-05-20 02:47:15 +00:00
Charlie Marsh
b42ff08612 Parenthesize more sub-expressions in f-string conversion (#4535) 2023-05-19 19:41:30 +00:00
Jonathan Plasse
03fb62c174 Fix RUF010 auto-fix with parenthesis (#4524) 2023-05-19 19:05:51 +00:00
Jonathan Plasse
2dfc645ea9 Fix UP032 auto-fix with integers (#4525) 2023-05-19 18:53:50 +00:00
Hoël Bagard
fe8e2bb237 [pylint] Add named_expr_without_context (W0131) (#4531) 2023-05-19 18:00:01 +00:00
Tom Kuson
a9ed8d5391 Add Pylint docs (#4530) 2023-05-19 17:40:18 +00:00
Aaron Cunningham
41a681531d Support new extend-per-file-ignores setting (#4265) 2023-05-19 12:24:04 -04:00
Justin Prieto
837e70677b [flake8-pyi] Implement PYI013 (#4517) 2023-05-19 15:39:55 +00:00
Hoël Bagard
7ebe372122 [pylint] Add duplicate-value (W0130) (#4515) 2023-05-19 15:03:47 +00:00
konstin
625849b846 Ecosystem CI: Optionally diff fixes (#4193)
* Generate fixes when using --show-fixes

Example command: `cargo run --bin ruff -- --no-cache --select F401
--show-source --show-fixes
crates/ruff/resources/test/fixtures/pyflakes/F401_9.py`

Before, `--show-fixes` was ignored:

```
crates/ruff/resources/test/fixtures/pyflakes/F401_9.py:4:22: F401 [*] `foo.baz` imported but unused
  |
4 | __all__ = ("bar",)
5 | from foo import bar, baz
  |                      ^^^ F401
  |
  = help: Remove unused import: `foo.baz`

Found 1 error.
[*] 1 potentially fixable with the --fix option.
```

After:

```
crates/ruff/resources/test/fixtures/pyflakes/F401_9.py:4:22: F401 [*] `foo.baz` imported but unused
  |
4 | __all__ = ("bar",)
5 | from foo import bar, baz
  |                      ^^^ F401
  |
  = help: Remove unused import: `foo.baz`

ℹ Suggested fix
1 1 | """Test: late-binding of `__all__`."""
2 2 |
3 3 | __all__ = ("bar",)
4   |-from foo import bar, baz
  4 |+from foo import bar

Found 1 error.
[*] 1 potentially fixable with the --fix option.
```

Also fixes git clone
2023-05-19 09:49:57 +00:00
konstin
32f1edc555 Create dummy format CLI (#4453)
* Create dummy format CLI

* Hide format from clap, too

Missed that this is a separate option from `#[doc(hidden)]`

* Remove cargo feature and replace with warning

* No-alloc files parameter matching

* beta warning: warn -> warn_user_once

* Rephrase warning
2023-05-19 11:45:52 +02:00
Micha Reiser
2f35099f81 Remove regex dependency from ruff_python_ast (#4518) 2023-05-19 06:44:18 +00:00
Hoël Bagard
ce8fd31a8f Updated contributing documentation (#4516) 2023-05-19 08:39:15 +02:00
Ville Skyttä
fdb241cad2 [flake8-bandit] Implement paramiko-call (S601) (#4500) 2023-05-19 03:40:50 +00:00
Charlie Marsh
ab303f4e09 Gate schemars skip under feature flag (#4514) 2023-05-19 03:01:31 +00:00
Charlie Marsh
15cb21a6f4 Implement --extend-fixable option (#4297) 2023-05-18 22:20:19 -04:00
Ville Skyttä
2e2ba2cb16 Avoid some false positives in dunder variable assigments (#4508) 2023-05-19 02:11:20 +00:00
Charlie Marsh
d4c0a41b00 Bump version to 0.0.269 (#4506) 2023-05-18 19:45:20 +00:00
Charlie Marsh
8702b5a40a Bump version to 0.0.268 (#4501) 2023-05-18 15:35:46 -04:00
figsoda
bab818e801 Update RustPython dependencies (#4503) 2023-05-18 15:28:13 -04:00
106 changed files with 3027 additions and 627 deletions

View File

@@ -183,18 +183,8 @@ jobs:
- name: "Install cargo-udeps"
uses: taiki-e/install-action@cargo-udeps
- name: "Run cargo-udeps"
run: |
unused_dependencies=$(cargo +nightly-2023-03-30 udeps > unused.txt && cat unused.txt | cut -d $'\n' -f 2-)
if [ -z "$unused_dependencies" ]; then
echo "No unused dependencies found" > $GITHUB_STEP_SUMMARY
exit 0
else
echo "Found unused dependencies" > $GITHUB_STEP_SUMMARY
echo '```console' >> $GITHUB_STEP_SUMMARY
echo "$unused_dependencies" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
run: cargo +nightly-2023-03-30 udeps
python-package:
name: "python package"

View File

@@ -134,7 +134,7 @@ Run `cargo dev generate-all` to generate the code for your new fixture. Then run
locally with (e.g.) `cargo run -p ruff_cli -- check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
`test_case` macro in the relevant `crates/ruff/src/[linter]/mod.rs` file. Then, run `cargo test`.
`test_case` macro in the relevant `crates/ruff/src/rules/[linter]/mod.rs` file. Then, run `cargo test`.
Your test will fail, but you'll be prompted to follow-up with `cargo insta review`. Accept the
generated snapshot, then commit the snapshot file alongside the rest of your changes.
@@ -148,7 +148,7 @@ This implies that rule names:
- should state the bad thing being checked for
- should not contain instructions on what you what you should use instead
- should not contain instructions on what you should use instead
(these belong in the rule documentation and the `autofix_title` for rules that have autofix)
When re-implementing rules from other linters, this convention is given more importance than

33
Cargo.lock generated
View File

@@ -193,9 +193,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.2.1"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84"
[[package]]
name = "bstr"
@@ -711,7 +711,7 @@ dependencies = [
[[package]]
name = "flake8-to-ruff"
version = "0.0.267"
version = "0.0.269"
dependencies = [
"anyhow",
"clap 4.2.7",
@@ -1723,11 +1723,11 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.267"
version = "0.0.269"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
"bitflags 2.2.1",
"bitflags 2.3.1",
"chrono",
"clap 4.2.7",
"colored",
@@ -1812,7 +1812,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.267"
version = "0.0.269"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1820,7 +1820,7 @@ dependencies = [
"assert_cmd",
"atty",
"bincode",
"bitflags 2.2.1",
"bitflags 2.3.1",
"cachedir",
"chrono",
"clap 4.2.7",
@@ -1919,7 +1919,7 @@ name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.2.1",
"bitflags 2.3.1",
"is-macro",
"itertools",
"log",
@@ -1927,7 +1927,6 @@ dependencies = [
"num-bigint",
"num-traits",
"once_cell",
"regex",
"ruff_text_size",
"rustc-hash",
"rustpython-literal",
@@ -1961,7 +1960,7 @@ dependencies = [
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.2.1",
"bitflags 2.3.1",
"is-macro",
"nohash-hasher",
"ruff_python_ast",
@@ -2001,7 +2000,7 @@ dependencies = [
[[package]]
name = "ruff_text_size"
version = "0.0.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
dependencies = [
"schemars",
"serde",
@@ -2072,7 +2071,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
dependencies = [
"is-macro",
"num-bigint",
@@ -2083,9 +2082,9 @@ dependencies = [
[[package]]
name = "rustpython-format"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
dependencies = [
"bitflags 2.2.1",
"bitflags 2.3.1",
"itertools",
"num-bigint",
"num-traits",
@@ -2095,7 +2094,7 @@ dependencies = [
[[package]]
name = "rustpython-literal"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
dependencies = [
"hexf-parse",
"is-macro",
@@ -2107,7 +2106,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
dependencies = [
"anyhow",
"is-macro",
@@ -2130,7 +2129,7 @@ dependencies = [
[[package]]
name = "rustpython-parser-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
dependencies = [
"is-macro",
"ruff_text_size",

View File

@@ -11,7 +11,7 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
[workspace.dependencies]
anyhow = { version = "1.0.69" }
bitflags = { version = "2.2.1" }
bitflags = { version = "2.3.1" }
chrono = { version = "0.4.23", default-features = false, features = ["clock"] }
clap = { version = "4.1.8", features = ["derive"] }
colored = { version = "2.0.0" }
@@ -31,10 +31,10 @@ proc-macro2 = { version = "1.0.51" }
quote = { version = "1.0.23" }
regex = { version = "1.7.1" }
rustc-hash = { version = "1.1.0" }
ruff_text_size = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73" }
rustpython-format = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73" }
rustpython-literal = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73" }
rustpython-parser = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
ruff_text_size = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082" }
rustpython-format = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082" }
rustpython-literal = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082" }
rustpython-parser = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
schemars = { version = "0.8.12" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93", features = ["preserve_order"] }

View File

@@ -137,7 +137,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.267'
rev: 'v0.0.269'
hooks:
- id: ruff
```
@@ -388,7 +388,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [SciPy](https://github.com/scipy/scipy)
- [Sphinx](https://github.com/sphinx-doc/sphinx)
- [Stable Baselines3](https://github.com/DLR-RM/stable-baselines3)
- [Starlite](https://github.com/starlite-api/starlite)
- [Litestar](https://litestar.dev/)
- [The Algorithms](https://github.com/TheAlgorithms/Python)
- [Vega-Altair](https://github.com/altair-viz/altair)
- WordPress ([Openverse](https://github.com/WordPress/openverse))

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.267"
version = "0.0.269"
edition = { workspace = true }
rust-version = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.267"
version = "0.0.269"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true

View File

@@ -0,0 +1,3 @@
import paramiko
paramiko.exec_command('something; really; unsafe')

View File

@@ -0,0 +1,65 @@
class OneAttributeClass:
value: int
...
class OneAttributeClass2:
...
value: int
class TwoEllipsesClass:
...
...
class DocstringClass:
"""
My body only contains an ellipsis.
"""
...
class NonEmptyChild(Exception):
value: int
...
class NonEmptyChild2(Exception):
...
value: int
class NonEmptyWithInit:
value: int
...
def __init__():
pass
class EmptyClass:
...
class EmptyEllipsis:
...
class Dog:
eyes: int = 2
class WithInit:
value: int = 0
def __init__():
...
def function():
...
...

View File

@@ -0,0 +1,56 @@
# Violations of PYI013
class OneAttributeClass:
value: int
... # Error
class OneAttributeClass2:
... # Error
value: int
class MyClass:
...
value: int
class TwoEllipsesClass:
...
... # Error
class DocstringClass:
"""
My body only contains an ellipsis.
"""
... # Error
class NonEmptyChild(Exception):
value: int
... # Error
class NonEmptyChild2(Exception):
... # Error
value: int
class NonEmptyWithInit:
value: int
... # Error
def __init__():
pass
# Not violations
class EmptyClass: ...
class EmptyEllipsis: ...
class Dog:
eyes: int = 2
class WithInit:
value: int = 0
def __init__(): ...
def function(): ...
...

View File

@@ -14,7 +14,7 @@ if key not in a_dict:
else:
var = a_dict[key]
# SIM401 (default with a complex expression)
# OK (default contains effect)
if key in a_dict:
var = a_dict[key]
else:

View File

@@ -19,7 +19,7 @@ if x > 0:
else:
import e
y = x + 1
__some__magic = 1
import f

View File

@@ -0,0 +1,10 @@
"""Test: module bindings are preferred over local bindings, for deferred annotations."""
from __future__ import annotations
import datetime
from typing import Optional
class Class:
datetime: Optional[datetime.datetime]

View File

@@ -0,0 +1,12 @@
"""Test: module bindings are preferred over local bindings, for deferred annotations."""
from __future__ import annotations
from typing import TypeAlias, List
class Class:
List: TypeAlias = List
def bar(self) -> List:
pass

View File

@@ -0,0 +1,8 @@
"""Test: module bindings are preferred over local bindings, for deferred annotations."""
import datetime
from typing import Optional
class Class:
datetime: "Optional[datetime.datetime]"

View File

@@ -0,0 +1,11 @@
###
# Errors.
###
incorrect_set = {"value1", 23, 5, "value1"}
incorrect_set = {1, 1}
###
# Non-errors.
###
correct_set = {"value1", 23, 5}
correct_set = {5, "5"}

View File

@@ -0,0 +1,19 @@
# Errors
(a := 42)
if True:
(b := 1)
class Foo:
(c := 1)
# OK
if a := 42:
print("Success")
a = 0
while (a := a + 1) < 10:
print("Correct")
a = (b := 1)

View File

@@ -0,0 +1,28 @@
# Errors
"{.real}".format(1)
"{0.real}".format(1)
"{a.real}".format(a=1)
"{.real}".format(1.0)
"{0.real}".format(1.0)
"{a.real}".format(a=1.0)
"{.real}".format(1j)
"{0.real}".format(1j)
"{a.real}".format(a=1j)
"{.real}".format(0b01)
"{0.real}".format(0b01)
"{a.real}".format(a=0b01)
"{}".format(1 + 2)
"{}".format([1, 2])
"{}".format({1, 2})
"{}".format({1: 2, 3: 4})
"{}".format((i for i in range(2)))
"{.real}".format(1 + 2)
"{.real}".format([1, 2])
"{.real}".format({1, 2})
"{.real}".format({1: 2, 3: 4})
"{}".format((i for i in range(2)))

View File

@@ -10,6 +10,8 @@ f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010
f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
f"{foo(bla)}" # OK
f"{str(bla, 'ascii')}, {str(bla, encoding='cp1255')}" # OK

View File

@@ -68,6 +68,18 @@ def bad():
except Exception as e:
raise e
def fine():
try:
process()
except Exception as e:
raise e from None
def fine():
try:
process()
except Exception as e:
raise e from Exception
def fine():
try:
process()

View File

@@ -27,7 +27,7 @@ use ruff_python_semantic::binding::{
Binding, BindingId, BindingKind, Exceptions, ExecutionContext, Export, FromImportation,
Importation, StarImportation, SubmoduleImportation,
};
use ruff_python_semantic::context::{Context, ContextFlags};
use ruff_python_semantic::context::{Context, ContextFlags, ResolvedReference};
use ruff_python_semantic::definition::{ContextualizedDefinition, Module, ModuleKind};
use ruff_python_semantic::node::NodeId;
use ruff_python_semantic::scope::{ClassDef, FunctionDef, Lambda, Scope, ScopeId, ScopeKind};
@@ -35,6 +35,7 @@ use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python_stdlib::path::is_python_stub_file;
use crate::checkers::ast::deferred::Deferred;
use crate::checkers::ast::traits::RegisteredRule;
use crate::docstrings::extraction::ExtractionTarget;
use crate::docstrings::Docstring;
use crate::fs::relativize_path;
@@ -56,6 +57,7 @@ use crate::settings::{flags, Settings};
use crate::{autofix, docstrings, noqa, warn_user};
mod deferred;
pub(crate) mod traits;
pub(crate) struct Checker<'a> {
// Settings, static metadata, etc.
@@ -77,6 +79,16 @@ pub(crate) struct Checker<'a> {
deferred: Deferred<'a>,
// Check-specific state.
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
// Dispatchers
call_rules: Vec<RegisteredRule<ast::ExprCall>>,
}
pub(crate) struct RuleContext<'a> {
pub(crate) settings: &'a Settings,
pub(crate) locator: &'a Locator<'a>,
pub(crate) stylist: &'a Stylist<'a>,
pub(crate) indexer: &'a Indexer,
pub(crate) ctx: &'a Context<'a>,
}
impl<'a> Checker<'a> {
@@ -110,10 +122,63 @@ impl<'a> Checker<'a> {
diagnostics: Vec::default(),
deletions: FxHashSet::default(),
flake8_bugbear_seen: Vec::default(),
call_rules: [
RegisteredRule::new::<flake8_django::rules::DjangoLocalsInRenderFunction>(),
RegisteredRule::new::<pyupgrade::rules::DeprecatedUnittestAlias>(),
RegisteredRule::new::<pyupgrade::rules::SuperCallWithParameters>(),
RegisteredRule::new::<pyupgrade::rules::UnnecessaryEncodeUTF8>(),
RegisteredRule::new::<pyupgrade::rules::RedundantOpenModes>(),
RegisteredRule::new::<pyupgrade::rules::NativeLiterals>(),
RegisteredRule::new::<pyupgrade::rules::OpenAlias>(),
RegisteredRule::new::<pyupgrade::rules::ReplaceUniversalNewlines>(),
RegisteredRule::new::<pyupgrade::rules::ReplaceStdoutStderr>(),
RegisteredRule::new::<pyupgrade::rules::OSErrorAlias>(),
RegisteredRule::new::<pyupgrade::rules::NonPEP604Isinstance>(),
RegisteredRule::new::<pyupgrade::rules::TypeOfPrimitive>(),
]
.into_iter()
.filter(|rule| rule.enabled(settings))
.collect(),
}
}
}
// TODO(charlie): Remove these methods from `Checker`, use the immutable `RuleContext` everywhere.
impl<'a> RuleContext<'a> {
/// Return `true` if a patch should be generated under the given autofix
/// `Mode`.
pub(crate) fn patch(&self, code: Rule) -> bool {
self.settings.rules.should_fix(code)
}
/// Create a [`Generator`] to generate source code based on the current AST state.
pub(crate) fn generator(&self) -> Generator {
fn quote_style(context: &Context, locator: &Locator, indexer: &Indexer) -> Option<Quote> {
if !context.in_f_string() {
return None;
}
// Find the quote character used to start the containing f-string.
let expr = context.expr()?;
let string_range = indexer.f_string_range(expr.start())?;
let trailing_quote = trailing_quote(locator.slice(string_range))?;
// Invert the quote character, if it's a single quote.
match *trailing_quote {
"'" => Some(Quote::Double),
"\"" => Some(Quote::Single),
_ => None,
}
}
Generator::new(
self.stylist.indentation(),
quote_style(self.ctx, self.locator, self.indexer).unwrap_or(self.stylist.quote()),
self.stylist.line_ending(),
)
}
}
impl<'a> Checker<'a> {
/// Return `true` if a patch should be generated under the given autofix
/// `Mode`.
@@ -742,6 +807,13 @@ where
if self.settings.rules.enabled(Rule::PassInClassBody) {
flake8_pyi::rules::pass_in_class_body(self, stmt, body);
}
if self
.settings
.rules
.enabled(Rule::EllipsisInNonEmptyClassBody)
{
flake8_pyi::rules::ellipsis_in_non_empty_class_body(self, stmt, body);
}
}
if self
@@ -1888,6 +1960,9 @@ where
if self.settings.rules.enabled(Rule::InvalidMockAccess) {
pygrep_hooks::rules::uncalled_mock_method(self, value);
}
if self.settings.rules.enabled(Rule::NamedExprWithoutContext) {
pylint::rules::named_expr_without_context(self, value);
}
if self.settings.rules.enabled(Rule::AsyncioDanglingTask) {
if let Some(diagnostic) = ruff::rules::asyncio_dangling_task(value, |expr| {
self.ctx.resolve_call_path(expr)
@@ -2557,12 +2632,26 @@ where
}
pandas_vet::rules::attr(self, attr, value, expr);
}
Expr::Call(ast::ExprCall {
func,
args,
keywords,
range: _,
}) => {
Expr::Call(call) => {
let context = RuleContext {
settings: self.settings,
locator: self.locator,
stylist: self.stylist,
indexer: self.indexer,
ctx: &self.ctx,
};
for rule in &self.call_rules {
rule.run(&mut self.diagnostics, &context, call);
}
// Destructure for the rest of the rules, for now.
let ast::ExprCall {
func,
args,
keywords,
range: _,
} = call;
if self.settings.rules.any_enabled(&[
// pyflakes
Rule::StringDotFormatInvalidFormat,
@@ -2662,43 +2751,6 @@ where
}
}
// pyupgrade
if self.settings.rules.enabled(Rule::TypeOfPrimitive) {
pyupgrade::rules::type_of_primitive(self, expr, func, args);
}
if self.settings.rules.enabled(Rule::DeprecatedUnittestAlias) {
pyupgrade::rules::deprecated_unittest_alias(self, func);
}
if self.settings.rules.enabled(Rule::SuperCallWithParameters) {
pyupgrade::rules::super_call_with_parameters(self, expr, func, args);
}
if self.settings.rules.enabled(Rule::UnnecessaryEncodeUTF8) {
pyupgrade::rules::unnecessary_encode_utf8(self, expr, func, args, keywords);
}
if self.settings.rules.enabled(Rule::RedundantOpenModes) {
pyupgrade::rules::redundant_open_modes(self, expr);
}
if self.settings.rules.enabled(Rule::NativeLiterals) {
pyupgrade::rules::native_literals(self, expr, func, args, keywords);
}
if self.settings.rules.enabled(Rule::OpenAlias) {
pyupgrade::rules::open_alias(self, expr, func);
}
if self.settings.rules.enabled(Rule::ReplaceUniversalNewlines) {
pyupgrade::rules::replace_universal_newlines(self, func, keywords);
}
if self.settings.rules.enabled(Rule::ReplaceStdoutStderr) {
pyupgrade::rules::replace_stdout_stderr(self, expr, func, args, keywords);
}
if self.settings.rules.enabled(Rule::OSErrorAlias) {
pyupgrade::rules::os_error_alias_call(self, func);
}
if self.settings.rules.enabled(Rule::NonPEP604Isinstance)
&& self.settings.target_version >= PythonVersion::Py310
{
pyupgrade::rules::use_pep604_isinstance(self, expr, func, args);
}
// flake8-async
if self
.settings
@@ -2846,6 +2898,9 @@ where
if self.settings.rules.enabled(Rule::RequestWithoutTimeout) {
flake8_bandit::rules::request_without_timeout(self, func, args, keywords);
}
if self.settings.rules.enabled(Rule::ParamikoCall) {
flake8_bandit::rules::paramiko_call(self, func);
}
if self
.settings
.rules
@@ -3281,15 +3336,6 @@ where
{
pylint::rules::logging_call(self, func, args, keywords);
}
// flake8-django
if self
.settings
.rules
.enabled(Rule::DjangoLocalsInRenderFunction)
{
flake8_django::rules::locals_in_render_function(self, func, args, keywords);
}
}
Expr::Dict(ast::ExprDict {
keys,
@@ -3307,6 +3353,11 @@ where
flake8_pie::rules::unnecessary_spread(self, keys, values);
}
}
Expr::Set(ast::ExprSet { elts, range: _ }) => {
if self.settings.rules.enabled(Rule::DuplicateValue) {
pylint::rules::duplicate_value(self, elts);
}
}
Expr::Yield(_) => {
if self.settings.rules.enabled(Rule::YieldOutsideFunction) {
pyflakes::rules::yield_outside_function(self, expr);
@@ -3342,6 +3393,13 @@ where
if self.settings.rules.enabled(Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
if self
.settings
.rules
.enabled(Rule::ExplicitFStringTypeConversion)
{
ruff::rules::explicit_f_string_type_conversion(self, expr, values);
}
}
Expr::BinOp(ast::ExprBinOp {
left,
@@ -3841,17 +3899,6 @@ where
flake8_simplify::rules::expr_and_false(self, expr);
}
}
Expr::FormattedValue(ast::ExprFormattedValue {
value, conversion, ..
}) => {
if self
.settings
.rules
.enabled(Rule::ExplicitFStringTypeConversion)
{
ruff::rules::explicit_f_string_type_conversion(self, value, *conversion);
}
}
_ => {}
};
@@ -4732,144 +4779,63 @@ impl<'a> Checker<'a> {
let Expr::Name(ast::ExprName { id, .. } )= expr else {
return;
};
let id = id.as_str();
let mut first_iter = true;
let mut import_starred = false;
for scope in self.ctx.scopes.ancestors(self.ctx.scope_id) {
if scope.kind.is_class() {
if id == "__class__" {
return;
} else if !first_iter {
continue;
}
match self.ctx.resolve_reference(id, expr.range()) {
ResolvedReference::Resolved(..) | ResolvedReference::ImplicitGlobal => {
// Nothing to do.
}
if let Some(index) = scope.get(id) {
// Mark the binding as used.
let context = self.ctx.execution_context();
self.ctx.bindings[*index].mark_used(self.ctx.scope_id, expr.range(), context);
if !self.ctx.in_deferred_type_definition()
&& self.ctx.bindings[*index].kind.is_annotation()
ResolvedReference::StarImport => {
// F405
if self
.settings
.rules
.enabled(Rule::UndefinedLocalWithImportStarUsage)
{
continue;
let sources: Vec<String> = self
.ctx
.scopes
.iter()
.flat_map(Scope::star_imports)
.map(|StarImportation { level, module }| {
helpers::format_import_from(*level, *module)
})
.sorted()
.dedup()
.collect();
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: id.to_string(),
sources,
},
expr.range(),
));
}
}
ResolvedReference::NotFound => {
// F821
if self.settings.rules.enabled(Rule::UndefinedName) {
// Allow __path__.
if self.path.ends_with("__init__.py") && id == "__path__" {
return;
}
// If the name of the sub-importation is the same as an alias of another
// importation and the alias is used, that sub-importation should be
// marked as used too.
//
// This handles code like:
// import pyarrow as pa
// import pyarrow.csv
// print(pa.csv.read_csv("test.csv"))
match &self.ctx.bindings[*index].kind {
BindingKind::Importation(Importation { name, full_name })
| BindingKind::SubmoduleImportation(SubmoduleImportation { name, full_name }) =>
// Avoid flagging if `NameError` is handled.
if self
.ctx
.handled_exceptions
.iter()
.any(|handler_names| handler_names.contains(Exceptions::NAME_ERROR))
{
let has_alias = full_name
.split('.')
.last()
.map(|segment| &segment != name)
.unwrap_or_default();
if has_alias {
// Mark the sub-importation as used.
if let Some(index) = scope.get(full_name) {
self.ctx.bindings[*index].mark_used(
self.ctx.scope_id,
expr.range(),
context,
);
}
}
return;
}
BindingKind::FromImportation(FromImportation { name, full_name }) => {
let has_alias = full_name
.split('.')
.last()
.map(|segment| &segment != name)
.unwrap_or_default();
if has_alias {
// Mark the sub-importation as used.
if let Some(index) = scope.get(full_name.as_str()) {
self.ctx.bindings[*index].mark_used(
self.ctx.scope_id,
expr.range(),
context,
);
}
}
}
_ => {}
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName {
name: id.to_string(),
},
expr.range(),
));
}
return;
}
first_iter = false;
import_starred = import_starred || scope.uses_star_imports();
}
if import_starred {
// F405
if self
.settings
.rules
.enabled(Rule::UndefinedLocalWithImportStarUsage)
{
let sources: Vec<String> = self
.ctx
.scopes
.iter()
.flat_map(Scope::star_imports)
.map(|StarImportation { level, module }| {
helpers::format_import_from(*level, *module)
})
.sorted()
.dedup()
.collect();
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: id.to_string(),
sources,
},
expr.range(),
));
}
return;
}
if self.settings.rules.enabled(Rule::UndefinedName) {
// Allow __path__.
if self.path.ends_with("__init__.py") && id == "__path__" {
return;
}
// Allow "__module__" and "__qualname__" in class scopes.
if (id == "__module__" || id == "__qualname__")
&& matches!(self.ctx.scope().kind, ScopeKind::Class(..))
{
return;
}
// Avoid flagging if NameError is handled.
if self
.ctx
.handled_exceptions
.iter()
.any(|handler_names| handler_names.contains(Exceptions::NAME_ERROR))
{
return;
}
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName {
name: id.to_string(),
},
expr.range(),
));
}
}

View File

@@ -0,0 +1,42 @@
use ruff_diagnostics::Diagnostic;
use crate::checkers::ast::RuleContext;
use crate::registry::Rule;
use crate::settings::Settings;
/// Trait for a lint rule that can be run on an AST node of type `T`.
pub(crate) trait Analyzer<T>: Sized {
/// The [`Rule`] that this analyzer implements.
fn rule() -> Rule;
/// Run the analyzer on the given node.
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &T);
}
/// Internal representation of a single [`Rule`] that can be run on an AST node of type `T`.
pub(super) struct RegisteredRule<T> {
rule: Rule,
run: Executor<T>,
}
impl<T> RegisteredRule<T> {
pub(super) fn new<R: Analyzer<T> + 'static>() -> Self {
Self {
rule: R::rule(),
run: R::run,
}
}
#[inline]
pub(super) fn enabled(&self, settings: &Settings) -> bool {
settings.rules.enabled(self.rule)
}
#[inline]
pub(super) fn run(&self, diagnostics: &mut Vec<Diagnostic>, context: &RuleContext, node: &T) {
(self.run)(diagnostics, context, node);
}
}
/// Executor for an [`Analyzer`] as a generic function pointer.
type Executor<T> = fn(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &T);

View File

@@ -185,6 +185,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R5501") => (RuleGroup::Unspecified, Rule::CollapsibleElseIf),
(Pylint, "W0120") => (RuleGroup::Unspecified, Rule::UselessElseOnLoop),
(Pylint, "W0129") => (RuleGroup::Unspecified, Rule::AssertOnStringLiteral),
(Pylint, "W0131") => (RuleGroup::Unspecified, Rule::NamedExprWithoutContext),
(Pylint, "W0406") => (RuleGroup::Unspecified, Rule::ImportSelf),
(Pylint, "W0602") => (RuleGroup::Unspecified, Rule::GlobalVariableNotAssigned),
(Pylint, "W0603") => (RuleGroup::Unspecified, Rule::GlobalStatement),
@@ -192,6 +193,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "W1508") => (RuleGroup::Unspecified, Rule::InvalidEnvvarDefault),
(Pylint, "W2901") => (RuleGroup::Unspecified, Rule::RedefinedLoopName),
(Pylint, "W3301") => (RuleGroup::Unspecified, Rule::NestedMinMax),
(Pylint, "W0130") => (RuleGroup::Unspecified, Rule::DuplicateValue),
// flake8-async
(Flake8Async, "100") => (RuleGroup::Unspecified, Rule::BlockingHttpCallInAsyncFunction),
@@ -507,6 +509,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bandit, "506") => (RuleGroup::Unspecified, Rule::UnsafeYAMLLoad),
(Flake8Bandit, "508") => (RuleGroup::Unspecified, Rule::SnmpInsecureVersion),
(Flake8Bandit, "509") => (RuleGroup::Unspecified, Rule::SnmpWeakCryptography),
(Flake8Bandit, "601") => (RuleGroup::Unspecified, Rule::ParamikoCall),
(Flake8Bandit, "602") => (RuleGroup::Unspecified, Rule::SubprocessPopenWithShellEqualsTrue),
(Flake8Bandit, "603") => (RuleGroup::Unspecified, Rule::SubprocessWithoutShellEqualsTrue),
(Flake8Bandit, "604") => (RuleGroup::Unspecified, Rule::CallWithShellEqualsTrue),
@@ -580,6 +583,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pyi, "010") => (RuleGroup::Unspecified, Rule::NonEmptyStubBody),
(Flake8Pyi, "011") => (RuleGroup::Unspecified, Rule::TypedArgumentDefaultInStub),
(Flake8Pyi, "012") => (RuleGroup::Unspecified, Rule::PassInClassBody),
(Flake8Pyi, "013") => (RuleGroup::Unspecified, Rule::EllipsisInNonEmptyClassBody),
(Flake8Pyi, "014") => (RuleGroup::Unspecified, Rule::ArgumentDefaultInStub),
(Flake8Pyi, "015") => (RuleGroup::Unspecified, Rule::AssignmentDefaultInStub),
(Flake8Pyi, "016") => (RuleGroup::Unspecified, Rule::DuplicateUnionMember),

View File

@@ -1,7 +1,8 @@
use anyhow::{bail, Result};
use libcst_native::{
Attribute, Call, Comparison, Dict, Expr, Expression, Import, ImportAlias, ImportFrom,
ImportNames, Module, SimpleString, SmallStatement, Statement,
Attribute, Call, Comparison, Dict, Expr, Expression, FormattedString, FormattedStringContent,
FormattedStringExpression, Import, ImportAlias, ImportFrom, ImportNames, Module, Name,
SimpleString, SmallStatement, Statement,
};
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
@@ -111,3 +112,33 @@ pub(crate) fn match_simple_string<'a, 'b>(
bail!("Expected Expression::SimpleString")
}
}
pub(crate) fn match_formatted_string<'a, 'b>(
expression: &'a mut Expression<'b>,
) -> Result<&'a mut FormattedString<'b>> {
if let Expression::FormattedString(formatted_string) = expression {
Ok(formatted_string)
} else {
bail!("Expected Expression::FormattedString")
}
}
pub(crate) fn match_formatted_string_expression<'a, 'b>(
formatted_string_content: &'a mut FormattedStringContent<'b>,
) -> Result<&'a mut FormattedStringExpression<'b>> {
if let FormattedStringContent::Expression(formatted_string_expression) =
formatted_string_content
{
Ok(formatted_string_expression)
} else {
bail!("Expected FormattedStringContent::Expression")
}
}
pub(crate) fn match_name<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Name<'b>> {
if let Expression::Name(name) = expression {
Ok(name)
} else {
bail!("Expected Expression::Name")
}
}

View File

@@ -19,49 +19,60 @@ impl Emitter for JunitEmitter {
) -> anyhow::Result<()> {
let mut report = Report::new("ruff");
for (filename, messages) in group_messages_by_filename(messages) {
let mut test_suite = TestSuite::new(filename);
if messages.is_empty() {
let mut test_suite = TestSuite::new("ruff");
test_suite
.extra
.insert("package".to_string(), "org.ruff".to_string());
for message in messages {
let MessageWithLocation {
message,
start_location,
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.kind.body.clone());
let location = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
SourceLocation::default()
} else {
start_location
};
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.row,
col = location.column,
body = message.kind.body
));
let mut case = TestCase::new(
format!("org.ruff.{}", message.kind.rule().noqa_code()),
status,
);
let file_path = Path::new(filename);
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
let classname = file_path.parent().unwrap().join(file_stem);
case.set_classname(classname.to_str().unwrap());
case.extra
.insert("line".to_string(), location.row.to_string());
case.extra
.insert("column".to_string(), location.column.to_string());
test_suite.add_test_case(case);
}
let mut case = TestCase::new("No errors found", TestCaseStatus::success());
case.set_classname("ruff");
test_suite.add_test_case(case);
report.add_test_suite(test_suite);
} else {
for (filename, messages) in group_messages_by_filename(messages) {
let mut test_suite = TestSuite::new(filename);
test_suite
.extra
.insert("package".to_string(), "org.ruff".to_string());
for message in messages {
let MessageWithLocation {
message,
start_location,
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.kind.body.clone());
let location = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
SourceLocation::default()
} else {
start_location
};
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.row,
col = location.column,
body = message.kind.body
));
let mut case = TestCase::new(
format!("org.ruff.{}", message.kind.rule().noqa_code()),
status,
);
let file_path = Path::new(filename);
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
let classname = file_path.parent().unwrap().join(file_stem);
case.set_classname(classname.to_str().unwrap());
case.extra
.insert("line".to_string(), location.row.to_string());
case.extra
.insert("column".to_string(), location.column.to_string());
test_suite.add_test_case(case);
}
report.add_test_suite(test_suite);
}
}
report.serialize(writer)?;

View File

@@ -159,7 +159,9 @@ ruff_macros::register_rules!(
rules::pylint::rules::LoggingTooManyArgs,
rules::pylint::rules::UnexpectedSpecialMethodSignature,
rules::pylint::rules::NestedMinMax,
rules::pylint::rules::DuplicateValue,
rules::pylint::rules::DuplicateBases,
rules::pylint::rules::NamedExprWithoutContext,
// flake8-async
rules::flake8_async::rules::BlockingHttpCallInAsyncFunction,
rules::flake8_async::rules::OpenSleepOrSubprocessInAsyncFunction,
@@ -422,6 +424,7 @@ ruff_macros::register_rules!(
rules::flake8_bandit::rules::HardcodedTempFile,
rules::flake8_bandit::rules::HashlibInsecureHashFunction,
rules::flake8_bandit::rules::Jinja2AutoescapeFalse,
rules::flake8_bandit::rules::ParamikoCall,
rules::flake8_bandit::rules::LoggingConfigInsecureListen,
rules::flake8_bandit::rules::RequestWithNoCertValidation,
rules::flake8_bandit::rules::RequestWithoutTimeout,
@@ -510,6 +513,7 @@ ruff_macros::register_rules!(
rules::flake8_pyi::rules::BadVersionInfoComparison,
rules::flake8_pyi::rules::DocstringInStub,
rules::flake8_pyi::rules::DuplicateUnionMember,
rules::flake8_pyi::rules::EllipsisInNonEmptyClassBody,
rules::flake8_pyi::rules::NonEmptyStubBody,
rules::flake8_pyi::rules::PassInClassBody,
rules::flake8_pyi::rules::PassStatementStubBody,

View File

@@ -5,7 +5,7 @@ use rustpython_parser as parser;
static ALLOWLIST_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"^(?i)(?:pylint|pyright|noqa|nosec|type:\s*ignore|fmt:\s*(on|off)|isort:\s*(on|off|skip|skip_file|split|dont-add-imports(:\s*\[.*?])?)|mypy:|SPDX-License-Identifier:)"
r"^(?i)(?:pylint|pyright|noqa|nosec|region|endregion|type:\s*ignore|fmt:\s*(on|off)|isort:\s*(on|off|skip|skip_file|split|dont-add-imports(:\s*\[.*?])?)|mypy:|SPDX-License-Identifier:)"
).unwrap()
});
static BRACKET_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[()\[\]{}\s]+$").unwrap());
@@ -224,6 +224,11 @@ mod tests {
assert!(!comment_contains_code("# noqa: A123", &[]));
assert!(!comment_contains_code("# noqa:A123", &[]));
assert!(!comment_contains_code("# nosec", &[]));
assert!(!comment_contains_code("# region", &[]));
assert!(!comment_contains_code("# endregion", &[]));
assert!(!comment_contains_code("# region.name", &[]));
assert!(!comment_contains_code("# region name", &[]));
assert!(!comment_contains_code("# region: name", &[]));
assert!(!comment_contains_code("# fmt: on", &[]));
assert!(!comment_contains_code("# fmt: off", &[]));
assert!(!comment_contains_code("# fmt:on", &[]));

View File

@@ -43,6 +43,7 @@ mod tests {
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"); "S112")]
#[test_case(Rule::TryExceptPass, Path::new("S110.py"); "S110")]
#[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"); "S506")]
#[test_case(Rule::ParamikoCall, Path::new("S601.py"); "S601")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -20,6 +20,7 @@ pub(crate) use jinja2_autoescape_false::{jinja2_autoescape_false, Jinja2Autoesca
pub(crate) use logging_config_insecure_listen::{
logging_config_insecure_listen, LoggingConfigInsecureListen,
};
pub(crate) use paramiko_calls::{paramiko_call, ParamikoCall};
pub(crate) use request_with_no_cert_validation::{
request_with_no_cert_validation, RequestWithNoCertValidation,
};
@@ -57,6 +58,7 @@ mod hardcoded_tmp_directory;
mod hashlib_insecure_hash_functions;
mod jinja2_autoescape_false;
mod logging_config_insecure_listen;
mod paramiko_calls;
mod request_with_no_cert_validation;
mod request_without_timeout;
mod shell_injection;

View File

@@ -0,0 +1,31 @@
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[violation]
pub struct ParamikoCall;
impl Violation for ParamikoCall {
#[derive_message_formats]
fn message(&self) -> String {
format!("Possible shell injection via Paramiko call; check inputs are properly sanitized")
}
}
/// S601
pub(crate) fn paramiko_call(checker: &mut Checker, func: &Expr) {
if checker
.ctx
.resolve_call_path(func)
.map_or(false, |call_path| {
call_path.as_slice() == ["paramiko", "exec_command"]
})
{
checker
.diagnostics
.push(Diagnostic::new(ParamikoCall, func.range()));
}
}

View File

@@ -0,0 +1,12 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
---
S601.py:3:1: S601 Possible shell injection via Paramiko call; check inputs are properly sanitized
|
3 | import paramiko
4 |
5 | paramiko.exec_command('something; really; unsafe')
| ^^^^^^^^^^^^^^^^^^^^^ S601
|

View File

@@ -1,9 +1,11 @@
use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
use rustpython_parser::ast::{self, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::Rule;
/// ## What it does
/// Checks for the use of `locals()` in `render` functions.
@@ -43,11 +45,25 @@ impl Violation for DjangoLocalsInRenderFunction {
}
/// DJ003
impl Analyzer<ast::ExprCall> for DjangoLocalsInRenderFunction {
fn rule() -> Rule {
Rule::DjangoLocalsInRenderFunction
}
fn run(diagnostics: &mut Vec<Diagnostic>, context: &RuleContext, node: &ast::ExprCall) {
locals_in_render_function(diagnostics, context, node);
}
}
pub(crate) fn locals_in_render_function(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
ast::ExprCall {
func,
args,
keywords,
..
}: &ast::ExprCall,
) {
if !checker
.ctx
@@ -76,13 +92,13 @@ pub(crate) fn locals_in_render_function(
return;
};
checker.diagnostics.push(Diagnostic::new(
diagnostics.push(Diagnostic::new(
DjangoLocalsInRenderFunction,
locals.range(),
));
}
fn is_locals_call(checker: &Checker, expr: &Expr) -> bool {
fn is_locals_call(checker: &RuleContext, expr: &Expr) -> bool {
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return false
};

View File

@@ -1,8 +1,6 @@
pub(crate) use all_with_model_form::{all_with_model_form, DjangoAllWithModelForm};
pub(crate) use exclude_with_model_form::{exclude_with_model_form, DjangoExcludeWithModelForm};
pub(crate) use locals_in_render_function::{
locals_in_render_function, DjangoLocalsInRenderFunction,
};
pub(crate) use locals_in_render_function::DjangoLocalsInRenderFunction;
pub(crate) use model_without_dunder_str::{model_without_dunder_str, DjangoModelWithoutDunderStr};
pub(crate) use non_leading_receiver_decorator::{
non_leading_receiver_decorator, DjangoNonLeadingReceiverDecorator,

View File

@@ -23,6 +23,8 @@ mod tests {
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))]
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))]
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.pyi"))]
#[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.py"))]
#[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.pyi"))]
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.py"))]
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))]
#[test_case(Rule::PassInClassBody, Path::new("PYI012.py"))]

View File

@@ -0,0 +1,93 @@
use rustpython_parser::ast::{Expr, ExprConstant, Ranged, Stmt, StmtExpr};
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::RefEquality;
use crate::autofix::actions::delete_stmt;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Removes ellipses (`...`) in otherwise non-empty class bodies.
///
/// ## Why is this bad?
/// An ellipsis in a class body is only necessary if the class body is
/// otherwise empty. If the class body is non-empty, then the ellipsis
/// is redundant.
///
/// ## Example
/// ```python
/// class Foo:
/// ...
/// value: int
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// value: int
/// ```
#[violation]
pub struct EllipsisInNonEmptyClassBody;
impl Violation for EllipsisInNonEmptyClassBody {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Non-empty class body must not contain `...`")
}
fn autofix_title(&self) -> Option<String> {
Some("Remove unnecessary `...`".to_string())
}
}
/// PYI013
pub(crate) fn ellipsis_in_non_empty_class_body<'a>(
checker: &mut Checker<'a>,
parent: &'a Stmt,
body: &'a [Stmt],
) {
// If the class body contains a single statement, then it's fine for it to be an ellipsis.
if body.len() == 1 {
return;
}
for stmt in body {
if let Stmt::Expr(StmtExpr { value, .. }) = &stmt {
if let Expr::Constant(ExprConstant { value, .. }) = value.as_ref() {
if value.is_ellipsis() {
let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
let deleted: Vec<&Stmt> =
checker.deletions.iter().map(Into::into).collect();
let edit = delete_stmt(
stmt,
Some(parent),
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
)?;
// In the unlikely event the class body consists solely of several
// consecutive ellipses, `delete_stmt` can actually result in a
// `pass`.
if edit.is_deletion() || edit.content() == Some("pass") {
checker.deletions.insert(RefEquality(stmt));
}
Ok(Fix::automatic(edit))
});
}
checker.diagnostics.push(diagnostic);
}
}
}
}
}

View File

@@ -3,6 +3,9 @@ pub(crate) use bad_version_info_comparison::{
};
pub(crate) use docstring_in_stubs::{docstring_in_stubs, DocstringInStub};
pub(crate) use duplicate_union_member::{duplicate_union_member, DuplicateUnionMember};
pub(crate) use ellipsis_in_non_empty_class_body::{
ellipsis_in_non_empty_class_body, EllipsisInNonEmptyClassBody,
};
pub(crate) use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody};
pub(crate) use pass_in_class_body::{pass_in_class_body, PassInClassBody};
pub(crate) use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody};
@@ -24,6 +27,7 @@ pub(crate) use unrecognized_platform::{
mod bad_version_info_comparison;
mod docstring_in_stubs;
mod duplicate_union_member;
mod ellipsis_in_non_empty_class_body;
mod non_empty_stub_body;
mod pass_in_class_body;
mod pass_statement_stub_body;

View File

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

View File

@@ -0,0 +1,177 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...`
|
5 | class OneAttributeClass:
6 | value: int
7 | ... # Error
| ^^^ PYI013
8 |
9 | class OneAttributeClass2:
|
= help: Remove unnecessary `...`
Fix
2 2 |
3 3 | class OneAttributeClass:
4 4 | value: int
5 |- ... # Error
6 5 |
7 6 | class OneAttributeClass2:
8 7 | ... # Error
PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
|
8 | class OneAttributeClass2:
9 | ... # Error
| ^^^ PYI013
10 | value: int
|
= help: Remove unnecessary `...`
Fix
5 5 | ... # Error
6 6 |
7 7 | class OneAttributeClass2:
8 |- ... # Error
9 8 | value: int
10 9 |
11 10 | class MyClass:
PYI013.pyi:12:5: PYI013 [*] Non-empty class body must not contain `...`
|
12 | class MyClass:
13 | ...
| ^^^ PYI013
14 | value: int
|
= help: Remove unnecessary `...`
Fix
9 9 | value: int
10 10 |
11 11 | class MyClass:
12 |- ...
13 12 | value: int
14 13 |
15 14 | class TwoEllipsesClass:
PYI013.pyi:16:5: PYI013 [*] Non-empty class body must not contain `...`
|
16 | class TwoEllipsesClass:
17 | ...
| ^^^ PYI013
18 | ... # Error
|
= help: Remove unnecessary `...`
Fix
13 13 | value: int
14 14 |
15 15 | class TwoEllipsesClass:
16 |- ...
17 16 | ... # Error
18 17 |
19 18 | class DocstringClass:
PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...`
|
17 | class TwoEllipsesClass:
18 | ...
19 | ... # Error
| ^^^ PYI013
20 |
21 | class DocstringClass:
|
= help: Remove unnecessary `...`
Fix
14 14 |
15 15 | class TwoEllipsesClass:
16 16 | ...
17 |- ... # Error
17 |+ pass # Error
18 18 |
19 19 | class DocstringClass:
20 20 | """
PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
|
24 | """
25 |
26 | ... # Error
| ^^^ PYI013
27 |
28 | class NonEmptyChild(Exception):
|
= help: Remove unnecessary `...`
Fix
21 21 | My body only contains an ellipsis.
22 22 | """
23 23 |
24 |- ... # Error
25 24 |
26 25 | class NonEmptyChild(Exception):
27 26 | value: int
PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
|
28 | class NonEmptyChild(Exception):
29 | value: int
30 | ... # Error
| ^^^ PYI013
31 |
32 | class NonEmptyChild2(Exception):
|
= help: Remove unnecessary `...`
Fix
25 25 |
26 26 | class NonEmptyChild(Exception):
27 27 | value: int
28 |- ... # Error
29 28 |
30 29 | class NonEmptyChild2(Exception):
31 30 | ... # Error
PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
|
31 | class NonEmptyChild2(Exception):
32 | ... # Error
| ^^^ PYI013
33 | value: int
|
= help: Remove unnecessary `...`
Fix
28 28 | ... # Error
29 29 |
30 30 | class NonEmptyChild2(Exception):
31 |- ... # Error
32 31 | value: int
33 32 |
34 33 | class NonEmptyWithInit:
PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
|
36 | class NonEmptyWithInit:
37 | value: int
38 | ... # Error
| ^^^ PYI013
39 |
40 | def __init__():
|
= help: Remove unnecessary `...`
Fix
33 33 |
34 34 | class NonEmptyWithInit:
35 35 | value: int
36 |- ... # Error
37 36 |
38 37 | def __init__():
39 38 | pass

View File

@@ -236,7 +236,7 @@ pub(crate) fn convert_for_loop_to_any_all(
ReimplementedBuiltin {
repl: contents.clone(),
},
stmt.range(),
TextRange::new(stmt.start(), loop_info.terminal),
);
if checker.patch(diagnostic.kind.rule()) && checker.ctx.is_builtin("any") {
#[allow(deprecated)]
@@ -326,7 +326,7 @@ pub(crate) fn convert_for_loop_to_any_all(
ReimplementedBuiltin {
repl: contents.clone(),
},
stmt.range(),
TextRange::new(stmt.start(), loop_info.terminal),
);
if checker.patch(diagnostic.kind.rule()) && checker.ctx.is_builtin("all") {
#[allow(deprecated)]

View File

@@ -9,8 +9,8 @@ SIM110.py:3:5: SIM110 [*] Use `return any(check(x) for x in iterable)` instead o
| _____^
6 | | if check(x):
7 | | return True
| |_______________________^ SIM110
8 | return False
8 | | return False
| |________________^ SIM110
|
= help: Replace with `return any(check(x) for x in iterable)`
@@ -34,8 +34,8 @@ SIM110.py:25:5: SIM110 [*] Use `return all(not check(x) for x in iterable)` inst
| _____^
28 | | if check(x):
29 | | return False
| |________________________^ SIM110
30 | return True
30 | | return True
| |_______________^ SIM110
|
= help: Replace with `return all(not check(x) for x in iterable)`
@@ -60,8 +60,8 @@ SIM110.py:33:5: SIM110 [*] Use `return all(x.is_empty() for x in iterable)` inst
| _____^
36 | | if not x.is_empty():
37 | | return False
| |________________________^ SIM110
38 | return True
38 | | return True
| |_______________^ SIM110
|
= help: Replace with `return all(x.is_empty() for x in iterable)`
@@ -200,8 +200,8 @@ SIM110.py:124:5: SIM110 Use `return any(check(x) for x in iterable)` instead of
| _____^
127 | | if check(x):
128 | | return True
| |_______________________^ SIM110
129 | return False
129 | | return False
| |________________^ SIM110
|
= help: Replace with `return any(check(x) for x in iterable)`
@@ -213,8 +213,8 @@ SIM110.py:134:5: SIM110 Use `return all(not check(x) for x in iterable)` instead
| _____^
137 | | if check(x):
138 | | return False
| |________________________^ SIM110
139 | return True
139 | | return True
| |_______________^ SIM110
|
= help: Replace with `return all(not check(x) for x in iterable)`
@@ -225,8 +225,8 @@ SIM110.py:144:5: SIM110 [*] Use `return any(check(x) for x in iterable)` instead
| _____^
146 | | if check(x):
147 | | return True
| |_______________________^ SIM110
148 | return False
148 | | return False
| |________________^ SIM110
|
= help: Replace with `return any(check(x) for x in iterable)`
@@ -250,8 +250,8 @@ SIM110.py:154:5: SIM110 [*] Use `return all(not check(x) for x in iterable)` ins
| _____^
156 | | if check(x):
157 | | return False
| |________________________^ SIM110
158 | return True
158 | | return True
| |_______________^ SIM110
|
= help: Replace with `return all(not check(x) for x in iterable)`

View File

@@ -9,8 +9,8 @@ SIM111.py:3:5: SIM110 [*] Use `return any(check(x) for x in iterable)` instead o
| _____^
6 | | if check(x):
7 | | return True
| |_______________________^ SIM110
8 | return False
8 | | return False
| |________________^ SIM110
|
= help: Replace with `return any(check(x) for x in iterable)`
@@ -34,8 +34,8 @@ SIM111.py:25:5: SIM110 [*] Use `return all(not check(x) for x in iterable)` inst
| _____^
28 | | if check(x):
29 | | return False
| |________________________^ SIM110
30 | return True
30 | | return True
| |_______________^ SIM110
|
= help: Replace with `return all(not check(x) for x in iterable)`
@@ -60,8 +60,8 @@ SIM111.py:33:5: SIM110 [*] Use `return all(x.is_empty() for x in iterable)` inst
| _____^
36 | | if not x.is_empty():
37 | | return False
| |________________________^ SIM110
38 | return True
38 | | return True
| |_______________^ SIM110
|
= help: Replace with `return all(x.is_empty() for x in iterable)`
@@ -200,8 +200,8 @@ SIM111.py:124:5: SIM110 Use `return any(check(x) for x in iterable)` instead of
| _____^
127 | | if check(x):
128 | | return True
| |_______________________^ SIM110
129 | return False
129 | | return False
| |________________^ SIM110
|
= help: Replace with `return any(check(x) for x in iterable)`
@@ -213,8 +213,8 @@ SIM111.py:134:5: SIM110 Use `return all(not check(x) for x in iterable)` instead
| _____^
137 | | if check(x):
138 | | return False
| |________________________^ SIM110
139 | return True
139 | | return True
| |_______________^ SIM110
|
= help: Replace with `return all(not check(x) for x in iterable)`
@@ -225,8 +225,8 @@ SIM111.py:144:5: SIM110 [*] Use `return any(check(x) for x in iterable)` instead
| _____^
146 | | if check(x):
147 | | return True
| |_______________________^ SIM110
148 | return False
148 | | return False
| |________________^ SIM110
|
= help: Replace with `return any(check(x) for x in iterable)`
@@ -250,8 +250,8 @@ SIM111.py:154:5: SIM110 [*] Use `return all(not check(x) for x in iterable)` ins
| _____^
156 | | if check(x):
157 | | return False
| |________________________^ SIM110
158 | return True
158 | | return True
| |_______________^ SIM110
|
= help: Replace with `return all(not check(x) for x in iterable)`
@@ -276,8 +276,8 @@ SIM111.py:162:5: SIM110 [*] Use `return all(x in y for x in iterable)` instead o
| _____^
165 | | if x not in y:
166 | | return False
| |________________________^ SIM110
167 | return True
167 | | return True
| |_______________^ SIM110
|
= help: Replace with `return all(x in y for x in iterable)`
@@ -302,8 +302,8 @@ SIM111.py:170:5: SIM110 [*] Use `return all(x <= y for x in iterable)` instead o
| _____^
173 | | if x > y:
174 | | return False
| |________________________^ SIM110
175 | return True
175 | | return True
| |_______________^ SIM110
|
= help: Replace with `return all(x <= y for x in iterable)`

View File

@@ -36,7 +36,7 @@ SIM401.py:12:1: SIM401 [*] Use `var = a_dict.get(key, "default2")` instead of an
16 | | var = a_dict[key]
| |_____________________^ SIM401
17 |
18 | # SIM401 (default with a complex expression)
18 | # OK (default contains effect)
|
= help: Replace with `var = a_dict.get(key, "default2")`
@@ -50,7 +50,7 @@ SIM401.py:12:1: SIM401 [*] Use `var = a_dict.get(key, "default2")` instead of an
15 |- var = a_dict[key]
12 |+var = a_dict.get(key, "default2")
16 13 |
17 14 | # SIM401 (default with a complex expression)
17 14 | # OK (default contains effect)
18 15 | if key in a_dict:
SIM401.py:24:1: SIM401 [*] Use `var = a_dict.get(keys[idx], "default")` instead of an `if` block

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/pycodestyle/mod.rs
---
E402.py:24:1: E402 Module level import not at top of file
|
24 | y = x + 1
24 | __some__magic = 1
25 |
26 | import f
| ^^^^^^^^ E402

View File

@@ -36,6 +36,9 @@ mod tests {
#[test_case(Rule::UnusedImport, Path::new("F401_9.py"); "F401_9")]
#[test_case(Rule::UnusedImport, Path::new("F401_10.py"); "F401_10")]
#[test_case(Rule::UnusedImport, Path::new("F401_11.py"); "F401_11")]
#[test_case(Rule::UnusedImport, Path::new("F401_12.py"); "F401_12")]
#[test_case(Rule::UnusedImport, Path::new("F401_13.py"); "F401_13")]
#[test_case(Rule::UnusedImport, Path::new("F401_14.py"); "F401_14")]
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"); "F402")]
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"); "F403")]
#[test_case(Rule::LateFutureImport, Path::new("F404.py"); "F404")]
@@ -470,6 +473,16 @@ mod tests {
"#,
&[Rule::UndefinedName],
);
flakes(
r#"
def f():
__qualname__ = 1
class Foo:
__qualname__
"#,
&[Rule::UnusedVariable],
);
}
#[test]
@@ -1148,6 +1161,40 @@ mod tests {
"#,
&[],
);
flakes(
r#"
class Test(object):
print(__class__.__name__)
def __init__(self):
self.x = 1
t = Test()
"#,
&[Rule::UndefinedName],
);
flakes(
r#"
class Test(object):
X = [__class__ for _ in range(10)]
def __init__(self):
self.x = 1
t = Test()
"#,
&[Rule::UndefinedName],
);
flakes(
r#"
def f(self):
print(__class__.__name__)
self.x = 1
f()
"#,
&[Rule::UndefinedName],
);
}
/// See: <https://github.com/PyCQA/pyflakes/blob/04ecb0c324ef3b61124e2f80f9e1af6c3a4c7b26/pyflakes/test/test_imports.py>

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ mod tests {
#[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async.py"); "PLE1142")]
#[test_case(Rule::AssertOnStringLiteral, Path::new("assert_on_string_literal.py"); "PLW0129")]
#[test_case(Rule::BadStrStripCall, Path::new("bad_str_strip_call.py"); "PLE01310")]
#[test_case(Rule::BadStrStripCall, Path::new("bad_str_strip_call.py"); "PLE1310")]
#[test_case(Rule::BadStringFormatType, Path::new("bad_string_format_type.py"); "PLE1307")]
#[test_case(Rule::BidirectionalUnicode, Path::new("bidirectional_unicode.py"); "PLE2502")]
#[test_case(Rule::BinaryOpException, Path::new("binary_op_exception.py"); "PLW0711")]
@@ -47,6 +47,7 @@ mod tests {
#[test_case(Rule::InvalidAllFormat, Path::new("invalid_all_format.py"); "PLE0605")]
#[test_case(Rule::InvalidAllObject, Path::new("invalid_all_object.py"); "PLE0604")]
#[test_case(Rule::DuplicateBases, Path::new("duplicate_bases.py"); "PLE0241")]
#[test_case(Rule::DuplicateValue, Path::new("duplicate_value.py"); "PLW0130")]
#[test_case(Rule::InvalidCharacterBackspace, Path::new("invalid_characters.py"); "PLE2510")]
#[test_case(Rule::InvalidCharacterEsc, Path::new("invalid_characters.py"); "PLE2513")]
#[test_case(Rule::InvalidCharacterNul, Path::new("invalid_characters.py"); "PLE2514")]
@@ -57,6 +58,7 @@ mod tests {
#[test_case(Rule::LoggingTooFewArgs, Path::new("logging_too_few_args.py"); "PLE1206")]
#[test_case(Rule::LoggingTooManyArgs, Path::new("logging_too_many_args.py"); "PLE1205")]
#[test_case(Rule::MagicValueComparison, Path::new("magic_value_comparison.py"); "PLR2004")]
#[test_case(Rule::NamedExprWithoutContext, Path::new("named_expr_without_context.py"); "PLW0131")]
#[test_case(Rule::NonlocalWithoutBinding, Path::new("nonlocal_without_binding.py"); "PLE0117")]
#[test_case(Rule::PropertyWithParameters, Path::new("property_with_parameters.py"); "PLR0206")]
#[test_case(Rule::RedefinedLoopName, Path::new("redefined_loop_name.py"); "PLW2901")]

View File

@@ -4,13 +4,45 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::source_code::Locator;
/// ## What it does
/// Checks for `else` blocks that consist of a single `if` statement.
///
/// ## Why is this bad?
/// If an `else` block contains a single `if` statement, it can be collapsed
/// into an `elif`, thus reducing the indentation level.
///
/// ## Example
/// ```python
/// def check_sign(value: int) -> None:
/// if value > 0:
/// print("Number is positive.")
/// else:
/// if value < 0:
/// print("Number is negative.")
/// else:
/// print("Number is zero.")
/// ```
///
/// Use instead:
/// ```python
/// def check_sign(value: int) -> None:
/// if value > 0:
/// print("Number is positive.")
/// elif value < 0:
/// print("Number is negative.")
/// else:
/// print("Number is zero.")
/// ```
///
/// ## References
/// - [Python documentation](https://docs.python.org/3/tutorial/controlflow.html#if-statements)
#[violation]
pub struct CollapsibleElseIf;
impl Violation for CollapsibleElseIf {
#[derive_message_formats]
fn message(&self) -> String {
format!("Consider using `elif` instead of `else` then `if` to remove one indentation level")
format!("Use `elif` instead of `else` then `if`, to reduce indentation")
}
}

View File

@@ -57,6 +57,26 @@ impl fmt::Display for ViolationsCmpop {
}
}
/// ## What it does
/// Checks for comparisons between constants.
///
/// ## Why is this bad?
/// Comparing two constants will always resolve to the same value, so the
/// comparison is redundant. Instead, the expression should be replaced
/// with the result of the comparison.
///
/// ## Example
/// ```python
/// foo = 1 == 1
/// ```
///
/// Use instead:
/// ```python
/// foo = True
/// ```
///
/// ## References
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#comparisons)
#[violation]
pub struct ComparisonOfConstant {
left_constant: String,

View File

@@ -8,6 +8,34 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for duplicate base classes in class definitions.
///
/// ## Why is this bad?
/// Including duplicate base classes will raise a `TypeError` at runtime.
///
/// ## Example
/// ```python
/// class Foo:
/// pass
///
///
/// class Bar(Foo, Foo):
/// pass
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// pass
///
///
/// class Bar(Foo):
/// pass
/// ```
///
/// ## References
/// - [Python documentation](https://docs.python.org/3/reference/compound_stmts.html#class-definitions)
#[violation]
pub struct DuplicateBases {
base: String,

View File

@@ -0,0 +1,57 @@
use rustc_hash::FxHashSet;
use rustpython_parser::ast::{self, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for set literals that contain duplicate values.
///
/// ## Why is this bad?
/// In Python, sets are unordered collections of unique elements. Including a
/// duplicate value in a set literal is redundant, and may be indicative of a
/// mistake.
///
/// ## Example
/// ```python
/// {1, 2, 3, 1}
/// ```
///
/// Use instead:
/// ```python
/// {1, 2, 3}
/// ```
#[violation]
pub struct DuplicateValue {
value: String,
}
impl Violation for DuplicateValue {
#[derive_message_formats]
fn message(&self) -> String {
let DuplicateValue { value } = self;
format!("Duplicate value `{value}` in set")
}
}
/// PLW0130
pub(crate) fn duplicate_value(checker: &mut Checker, elts: &Vec<Expr>) {
let mut seen_values: FxHashSet<ComparableExpr> = FxHashSet::default();
for elt in elts {
if let Expr::Constant(ast::ExprConstant { value, .. }) = elt {
let comparable_value: ComparableExpr = elt.into();
if !seen_values.insert(comparable_value) {
checker.diagnostics.push(Diagnostic::new(
DuplicateValue {
value: checker.generator().constant(value),
},
elt.range(),
));
}
};
}
}

View File

@@ -7,6 +7,35 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::rules::pylint::settings::ConstantType;
/// ## What it does
/// Checks for the use of unnamed numerical constants ("magic") values in
/// comparisons.
///
/// ## Why is this bad?
/// The use of "magic" can make code harder to read and maintain, as readers
/// will have to infer the meaning of the value from the context.
///
/// For convenience, this rule excludes a variety of common values from the
/// "magic" value definition, such as `0`, `1`, `""`, and `"__main__"`.
///
/// ## Example
/// ```python
/// def calculate_discount(price: float) -> float:
/// return price * (1 - 0.2)
/// ```
///
/// Use instead:
/// ```python
/// DISCOUNT_RATE = 0.2
///
///
/// def calculate_discount(price: float) -> float:
/// return price * (1 - DISCOUNT_RATE)
/// ```
///
/// ## References
/// - [Wikipedia](https://en.wikipedia.org/wiki/Magic_number_(programming)#Unnamed_numerical_constants)
/// - [PEP 8](https://peps.python.org/pep-0008/#constants)
#[violation]
pub struct MagicValueComparison {
value: String,

View File

@@ -7,6 +7,25 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for submodule imports that are aliased to the submodule name.
///
/// ## Why is this bad?
/// Using the `from` keyword to import the submodule is more concise and
/// readable.
///
/// ## Example
/// ```python
/// import concurrent.futures as futures
/// ```
///
/// Use instead:
/// ```python
/// from concurrent import futures
/// ```
///
/// ## References
/// - [Python documentation](https://docs.python.org/3/reference/import.html#submodules)
#[violation]
pub struct ManualFromImport {
module: String,

View File

@@ -9,6 +9,7 @@ pub(crate) use compare_to_empty_string::{compare_to_empty_string, CompareToEmpty
pub(crate) use comparison_of_constant::{comparison_of_constant, ComparisonOfConstant};
pub(crate) use continue_in_finally::{continue_in_finally, ContinueInFinally};
pub(crate) use duplicate_bases::{duplicate_bases, DuplicateBases};
pub(crate) use duplicate_value::{duplicate_value, DuplicateValue};
pub(crate) use global_statement::{global_statement, GlobalStatement};
pub(crate) use global_variable_not_assigned::GlobalVariableNotAssigned;
pub(crate) use import_self::{import_from_self, import_self, ImportSelf};
@@ -26,6 +27,7 @@ pub(crate) use load_before_global_declaration::{
pub(crate) use logging::{logging_call, LoggingTooFewArgs, LoggingTooManyArgs};
pub(crate) use magic_value_comparison::{magic_value_comparison, MagicValueComparison};
pub(crate) use manual_import_from::{manual_from_import, ManualFromImport};
pub(crate) use named_expr_without_context::{named_expr_without_context, NamedExprWithoutContext};
pub(crate) use nested_min_max::{nested_min_max, NestedMinMax};
pub(crate) use nonlocal_without_binding::NonlocalWithoutBinding;
pub(crate) use property_with_parameters::{property_with_parameters, PropertyWithParameters};
@@ -59,6 +61,7 @@ mod compare_to_empty_string;
mod comparison_of_constant;
mod continue_in_finally;
mod duplicate_bases;
mod duplicate_value;
mod global_statement;
mod global_variable_not_assigned;
mod import_self;
@@ -71,6 +74,7 @@ mod load_before_global_declaration;
mod logging;
mod magic_value_comparison;
mod manual_import_from;
mod named_expr_without_context;
mod nested_min_max;
mod nonlocal_without_binding;
mod property_with_parameters;

View File

@@ -0,0 +1,44 @@
use rustpython_parser::ast::{self, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for usages of named expressions (e.g., `a := 42`) that can be
/// replaced by regular assignment statements (e.g., `a = 42`).
///
/// ## Why is this bad?
/// While a top-level named expression is syntactically and semantically valid,
/// it's less clear than a regular assignment statement. Named expressions are
/// intended to be used in comprehensions and generator expressions, where
/// assignment statements are not allowed.
///
/// ## Example
/// ```python
/// (a := 42)
/// ```
///
/// Use instead:
/// ```python
/// a = 42
/// ```
#[violation]
pub struct NamedExprWithoutContext;
impl Violation for NamedExprWithoutContext {
#[derive_message_formats]
fn message(&self) -> String {
format!("Named expression used without context")
}
}
/// PLW0131
pub(crate) fn named_expr_without_context(checker: &mut Checker, value: &Expr) {
if let Expr::NamedExpr(ast::ExprNamedExpr { range, .. }) = value {
checker
.diagnostics
.push(Diagnostic::new(NamedExprWithoutContext, *range));
}
}

View File

@@ -6,6 +6,36 @@ use ruff_python_ast::helpers::identifier_range;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for property definitions that accept function parameters.
///
/// ## Why is this bad?
/// Properties cannot be called with parameters.
///
/// If you need to pass parameters to a property, create a method with the
/// desired parameters and call that method instead.
///
/// ## Example
/// ```python
/// class Cat:
/// @property
/// def purr(self, volume):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// class Cat:
/// @property
/// def purr(self):
/// ...
///
/// def purr_volume(self, volume):
/// ...
/// ```
///
/// ## References
/// - [Python documentation](https://docs.python.org/3/library/functions.html#property)
#[violation]
pub struct PropertyWithParameters;

View File

@@ -8,6 +8,34 @@ use ruff_python_ast::hashable::HashableExpr;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for repeated `isinstance` calls on the same object.
///
/// ## Why is this bad?
/// Repeated `isinstance` calls on the same object can be merged into a
/// single call.
///
/// ## Example
/// ```python
/// def is_number(x):
/// return isinstance(x, int) or isinstance(x, float) or isinstance(x, complex)
/// ```
///
/// Use instead:
/// ```python
/// def is_number(x):
/// return isinstance(x, (int, float, complex))
/// ```
///
/// Or, for Python 3.10 and later:
///
/// ```python
/// def is_number(x):
/// return isinstance(x, int | float | complex)
/// ```
///
/// ## References
/// - [Python documentation](https://docs.python.org/3/library/functions.html#isinstance)
#[violation]
pub struct RepeatedIsinstanceCalls {
obj: String,

View File

@@ -7,6 +7,35 @@ use crate::autofix::actions::get_or_import_symbol;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of the `exit()` and `quit()`.
///
/// ## Why is this bad?
/// `exit` and `quit` come from the `site` module, which is typically imported
/// automatically during startup. However, it is not _guaranteed_ to be
/// imported, and so using these functions may result in a `NameError` at
/// runtime. Generally, these constants are intended to be used in an interactive
/// interpreter, and not in programs.
///
/// Prefer `sys.exit()`, as the `sys` module is guaranteed to exist in all
/// contexts.
///
/// ## Example
/// ```python
/// if __name__ == "__main__":
/// exit()
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// if __name__ == "__main__":
/// sys.exit()
/// ```
///
/// ## References
/// - [Python documentation](https://docs.python.org/3/library/constants.html#constants-added-by-the-site-module)
#[violation]
pub struct SysExitAlias {
name: String,

View File

@@ -6,6 +6,43 @@ use ruff_python_ast::helpers::identifier_range;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for function definitions that include too many arguments.
///
/// By default, this rule allows up to five arguments, as configured by the
/// `pylint.max-args` option.
///
/// ## Why is this bad?
/// Functions with many arguments are harder to understand, maintain, and call.
/// Consider refactoring functions with many arguments into smaller functions
/// with fewer arguments, or using objects to group related arguments.
///
/// ## Example
/// ```python
/// def calculate_position(x_pos, y_pos, z_pos, x_vel, y_vel, z_vel, time):
/// new_x = x_pos + x_vel * time
/// new_y = y_pos + y_vel * time
/// new_z = z_pos + z_vel * time
/// return new_x, new_y, new_z
/// ```
///
/// Use instead:
/// ```python
/// from typing import NamedTuple
///
///
/// class Vector(NamedTuple):
/// x: float
/// y: float
/// z: float
///
///
/// def calculate_position(pos: Vector, vel: Vector, time: float) -> Vector:
/// return Vector(*(p + v * time for p, v in zip(pos, vel)))
/// ```
///
/// ## Options
/// - `pylint.max-args`
#[violation]
pub struct TooManyArguments {
c_args: usize,

View File

@@ -5,6 +5,70 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::identifier_range;
use ruff_python_ast::source_code::Locator;
/// ## What it does
/// Checks for functions or methods with too many branches.
///
/// By default, this rule allows up to 12 branches. This can be configured
/// using the `max-branches` option.
///
/// ## Why is this bad?
/// Functions or methods with many branches are harder to understand
/// and maintain than functions or methods with fewer branches.
///
/// ## Example
/// ```python
/// def capital(country):
/// if country == "Australia":
/// return "Canberra"
/// elif country == "Brazil":
/// return "Brasilia"
/// elif country == "Canada":
/// return "Ottawa"
/// elif country == "England":
/// return "London"
/// elif country == "France":
/// return "Paris"
/// elif country == "Germany":
/// return "Berlin"
/// elif country == "Poland":
/// return "Warsaw"
/// elif country == "Romania":
/// return "Bucharest"
/// elif country == "Spain":
/// return "Madrid"
/// elif country == "Thailand":
/// return "Bangkok"
/// elif country == "Turkey":
/// return "Ankara"
/// elif country == "United States":
/// return "Washington"
/// else:
/// return "Unknown" # 13th branch
/// ```
///
/// Use instead:
/// ```python
/// def capital(country):
/// capitals = {
/// "Australia": "Canberra",
/// "Brazil": "Brasilia",
/// "Canada": "Ottawa",
/// "England": "London",
/// "France": "Paris",
/// "Germany": "Berlin",
/// "Poland": "Warsaw",
/// "Romania": "Bucharest",
/// "Spain": "Madrid",
/// "Thailand": "Bangkok",
/// "Turkey": "Ankara",
/// "United States": "Washington",
/// }
/// city = capitals.get(country, "Unknown")
/// return city
/// ```
///
/// ## References
/// - [Ruff configuration documentation](https://beta.ruff.rs/docs/settings/#max-branches)
#[violation]
pub struct TooManyBranches {
branches: usize,

View File

@@ -6,6 +6,51 @@ use ruff_python_ast::helpers::{identifier_range, ReturnStatementVisitor};
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::statement_visitor::StatementVisitor;
/// ## What it does
/// Checks for functions or methods with too many return statements.
///
/// By default, this rule allows up to six return statements, as configured by
/// the `pylint.max-returns` option.
///
/// ## Why is this bad?
/// Functions or methods with many return statements are harder to understand
/// and maintain, and often indicative of complex logic.
///
/// ## Example
/// ```python
/// def capital(country: str) -> str | None:
/// if country == "England":
/// return "London"
/// elif country == "France":
/// return "Paris"
/// elif country == "Poland":
/// return "Warsaw"
/// elif country == "Romania":
/// return "Bucharest"
/// elif country == "Spain":
/// return "Madrid"
/// elif country == "Thailand":
/// return "Bangkok"
/// else:
/// return None
/// ```
///
/// Use instead:
/// ```python
/// def capital(country: str) -> str | None:
/// capitals = {
/// "England": "London",
/// "France": "Paris",
/// "Poland": "Warsaw",
/// "Romania": "Bucharest",
/// "Spain": "Madrid",
/// "Thailand": "Bangkok",
/// }
/// return capitals.get(country)
/// ```
///
/// ## Options
/// - `pylint.max-returns`
#[violation]
pub struct TooManyReturnStatements {
returns: usize,

View File

@@ -5,6 +5,47 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::identifier_range;
use ruff_python_ast::source_code::Locator;
/// ## What it does
/// Checks for functions or methods with too many statements.
///
/// By default, this rule allows up to 50 statements, as configured by the
/// `pylint.max-statements` option.
///
/// ## Why is this bad?
/// Functions or methods with many statements are harder to understand
/// and maintain.
///
/// Instead, consider refactoring the function or method into smaller
/// functions or methods, or identifying generalizable patterns and
/// replacing them with generic logic or abstractions.
///
/// ## Example
/// ```python
/// def is_even(number: int) -> bool:
/// if number == 0:
/// return True
/// elif number == 1:
/// return False
/// elif number == 2:
/// return True
/// elif number == 3:
/// return False
/// elif number == 4:
/// return True
/// elif number == 5:
/// return False
/// else:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def is_even(number: int) -> bool:
/// return number % 2 == 0
/// ```
///
/// ## Options
/// - `pylint.max-statements`
#[violation]
pub struct TooManyStatements {
statements: usize,

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/pylint/mod.rs
---
collapsible_else_if.py:38:9: PLR5501 Consider using `elif` instead of `else` then `if` to remove one indentation level
collapsible_else_if.py:38:9: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
38 | pass
39 | else:
@@ -11,7 +11,7 @@ collapsible_else_if.py:38:9: PLR5501 Consider using `elif` instead of `else` the
| |________________^ PLR5501
|
collapsible_else_if.py:46:9: PLR5501 Consider using `elif` instead of `else` then `if` to remove one indentation level
collapsible_else_if.py:46:9: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
46 | pass
47 | else:

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff/src/rules/pylint/mod.rs
---
duplicate_value.py:4:35: PLW0130 Duplicate value `"value1"` in set
|
4 | # Errors.
5 | ###
6 | incorrect_set = {"value1", 23, 5, "value1"}
| ^^^^^^^^ PLW0130
7 | incorrect_set = {1, 1}
|
duplicate_value.py:5:21: PLW0130 Duplicate value `1` in set
|
5 | ###
6 | incorrect_set = {"value1", 23, 5, "value1"}
7 | incorrect_set = {1, 1}
| ^ PLW0130
8 |
9 | ###
|

View File

@@ -0,0 +1,28 @@
---
source: crates/ruff/src/rules/pylint/mod.rs
---
named_expr_without_context.py:2:2: PLW0131 Named expression used without context
|
2 | # Errors
3 | (a := 42)
| ^^^^^^^ PLW0131
4 | if True:
5 | (b := 1)
|
named_expr_without_context.py:4:6: PLW0131 Named expression used without context
|
4 | (a := 42)
5 | if True:
6 | (b := 1)
| ^^^^^^ PLW0131
|
named_expr_without_context.py:8:6: PLW0131 Named expression used without context
|
8 | class Foo:
9 | (c := 1)
| ^^^^^^ PLW0131
|

View File

@@ -4,7 +4,6 @@ use libcst_native::{
SmallStatement, Statement, Suite,
};
use ruff_text_size::{TextRange, TextSize};
use rustpython_parser::ast::{Expr, Ranged};
use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
@@ -51,11 +50,10 @@ pub(crate) fn adjust_indentation(
/// Generate a fix to remove arguments from a `super` call.
pub(crate) fn remove_super_arguments(
range: TextRange,
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
) -> Option<Edit> {
let range = expr.range();
let contents = locator.slice(range);
let mut tree = libcst_native::parse_module(contents, None).ok()?;

View File

@@ -59,6 +59,7 @@ mod tests {
#[test_case(Rule::PrintfStringFormatting, Path::new("UP031_1.py"); "UP031_1")]
#[test_case(Rule::FString, Path::new("UP032_0.py"); "UP032_0")]
#[test_case(Rule::FString, Path::new("UP032_1.py"); "UP032_1")]
#[test_case(Rule::FString, Path::new("UP032_2.py"); "UP032_2")]
#[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_0.py"); "UP033_0")]
#[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_1.py"); "UP033_1")]
#[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"); "UP034")]

View File

@@ -1,12 +1,13 @@
use once_cell::sync::Lazy;
use rustc_hash::FxHashMap;
use rustpython_parser::ast::{self, Expr, Ranged};
use rustpython_parser::ast::{self, Expr};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::{AsRule, Rule};
#[violation]
pub struct DeprecatedUnittestAlias {
@@ -27,6 +28,16 @@ impl AlwaysAutofixableViolation for DeprecatedUnittestAlias {
}
}
impl Analyzer<ast::ExprCall> for DeprecatedUnittestAlias {
fn rule() -> Rule {
Rule::DeprecatedUnittestAlias
}
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
deprecated_unittest_alias(diagnostics, checker, node);
}
}
static DEPRECATED_ALIASES: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::new(|| {
FxHashMap::from_iter([
("failUnlessEqual", "assertEqual"),
@@ -48,11 +59,12 @@ static DEPRECATED_ALIASES: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::n
});
/// UP005
pub(crate) fn deprecated_unittest_alias(checker: &mut Checker, expr: &Expr) {
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = expr else {
return;
};
let Some(target) = DEPRECATED_ALIASES.get(attr.as_str()) else {
pub(crate) fn deprecated_unittest_alias(
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
ast::ExprCall { func, .. }: &ast::ExprCall,
) {
let Expr::Attribute(ast::ExprAttribute { value, attr, range, .. }) = func.as_ref() else {
return;
};
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
@@ -61,19 +73,22 @@ pub(crate) fn deprecated_unittest_alias(checker: &mut Checker, expr: &Expr) {
if id != "self" {
return;
}
let Some(target) = DEPRECATED_ALIASES.get(attr.as_str()) else {
return;
};
let mut diagnostic = Diagnostic::new(
DeprecatedUnittestAlias {
alias: attr.to_string(),
target: (*target).to_string(),
},
expr.range(),
*range,
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
format!("self.{target}"),
expr.range(),
*range,
)));
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}

View File

@@ -1,3 +1,5 @@
use std::borrow::Cow;
use ruff_text_size::TextRange;
use rustc_hash::FxHashMap;
use rustpython_format::{
@@ -7,6 +9,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::str::{is_implicit_concatenation, leading_quote, trailing_quote};
use crate::checkers::ast::Checker;
@@ -34,21 +37,20 @@ impl AlwaysAutofixableViolation for FString {
/// respectively.
#[derive(Debug)]
struct FormatSummaryValues<'a> {
args: Vec<String>,
kwargs: FxHashMap<&'a str, String>,
args: Vec<&'a Expr>,
kwargs: FxHashMap<&'a str, &'a Expr>,
}
impl<'a> FormatSummaryValues<'a> {
fn try_from_expr(checker: &'a Checker, expr: &'a Expr) -> Option<Self> {
let mut extracted_args: Vec<String> = Vec::new();
let mut extracted_kwargs: FxHashMap<&str, String> = FxHashMap::default();
fn try_from_expr(expr: &'a Expr, locator: &'a Locator) -> Option<Self> {
let mut extracted_args: Vec<&Expr> = Vec::new();
let mut extracted_kwargs: FxHashMap<&str, &Expr> = FxHashMap::default();
if let Expr::Call(ast::ExprCall { args, keywords, .. }) = expr {
for arg in args {
let arg = checker.locator.slice(arg.range());
if contains_invalids(arg) {
if contains_invalids(locator.slice(arg.range())) {
return None;
}
extracted_args.push(arg.to_string());
extracted_args.push(arg);
}
for keyword in keywords {
let Keyword {
@@ -57,11 +59,10 @@ impl<'a> FormatSummaryValues<'a> {
range: _,
} = keyword;
if let Some(key) = arg {
let kwarg = checker.locator.slice(value.range());
if contains_invalids(kwarg) {
if contains_invalids(locator.slice(value.range())) {
return None;
}
extracted_kwargs.insert(key, kwarg.to_string());
extracted_kwargs.insert(key, value);
}
}
}
@@ -76,7 +77,7 @@ impl<'a> FormatSummaryValues<'a> {
})
}
fn consume_next(&mut self) -> Option<String> {
fn consume_next(&mut self) -> Option<&Expr> {
if self.args.is_empty() {
None
} else {
@@ -84,7 +85,7 @@ impl<'a> FormatSummaryValues<'a> {
}
}
fn consume_arg(&mut self, index: usize) -> Option<String> {
fn consume_arg(&mut self, index: usize) -> Option<&Expr> {
if self.args.len() > index {
Some(self.args.remove(index))
} else {
@@ -92,13 +93,13 @@ impl<'a> FormatSummaryValues<'a> {
}
}
fn consume_kwarg(&mut self, key: &str) -> Option<String> {
fn consume_kwarg(&mut self, key: &str) -> Option<&Expr> {
self.kwargs.remove(key)
}
}
/// Return `true` if the string contains characters that are forbidden in
/// argument identifier.
/// Return `true` if the string contains characters that are forbidden by
/// argument identifiers.
fn contains_invalids(string: &str) -> bool {
string.contains('*')
|| string.contains('\'')
@@ -106,8 +107,61 @@ fn contains_invalids(string: &str) -> bool {
|| string.contains("await")
}
enum FormatContext {
/// The expression is used as a bare format spec (e.g., `{x}`).
Bare,
/// The expression is used with conversion flags, or attribute or subscript access
/// (e.g., `{x!r}`, `{x.y}`, `{x[y]}`).
Accessed,
}
/// Given an [`Expr`], format it for use in a formatted expression within an f-string.
fn formatted_expr<'a>(expr: &Expr, context: FormatContext, locator: &Locator<'a>) -> Cow<'a, str> {
let text = locator.slice(expr.range());
let parenthesize = match (context, expr) {
// E.g., `x + y` should be parenthesized in `f"{(x + y)[0]}"`.
(
FormatContext::Accessed,
Expr::BinOp(_)
| Expr::UnaryOp(_)
| Expr::BoolOp(_)
| Expr::NamedExpr(_)
| Expr::Compare(_)
| Expr::IfExp(_)
| Expr::Lambda(_)
| Expr::Await(_)
| Expr::Yield(_)
| Expr::YieldFrom(_)
| Expr::Starred(_),
) => true,
// E.g., `12` should be parenthesized in `f"{(12).real}"`.
(
FormatContext::Accessed,
Expr::Constant(ast::ExprConstant {
value: Constant::Int(..),
..
}),
) => text.chars().all(|c| c.is_ascii_digit()),
// E.g., `{x, y}` should be parenthesized in `f"{(x, y)}"`.
(
_,
Expr::GeneratorExp(_)
| Expr::Dict(_)
| Expr::Set(_)
| Expr::SetComp(_)
| Expr::DictComp(_),
) => true,
_ => false,
};
if parenthesize && !text.starts_with('(') && !text.ends_with(')') {
Cow::Owned(format!("({text})"))
} else {
Cow::Borrowed(text)
}
}
/// Generate an f-string from an [`Expr`].
fn try_convert_to_f_string(checker: &Checker, expr: &Expr) -> Option<String> {
fn try_convert_to_f_string(expr: &Expr, locator: &Locator) -> Option<String> {
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return None;
};
@@ -124,11 +178,11 @@ fn try_convert_to_f_string(checker: &Checker, expr: &Expr) -> Option<String> {
return None;
};
let Some(mut summary) = FormatSummaryValues::try_from_expr(checker, expr) else {
let Some(mut summary) = FormatSummaryValues::try_from_expr( expr, locator) else {
return None;
};
let contents = checker.locator.slice(value.range());
let contents = locator.slice(value.range());
// Skip implicit string concatenations.
if is_implicit_concatenation(contents) {
@@ -171,26 +225,20 @@ fn try_convert_to_f_string(checker: &Checker, expr: &Expr) -> Option<String> {
converted.push('{');
let field = FieldName::parse(&field_name).ok()?;
match field.field_type {
FieldType::Auto => {
let Some(arg) = summary.consume_next() else {
return None;
};
converted.push_str(&arg);
}
FieldType::Index(index) => {
let Some(arg) = summary.consume_arg(index) else {
return None;
};
converted.push_str(&arg);
}
FieldType::Keyword(name) => {
let Some(arg) = summary.consume_kwarg(&name) else {
return None;
};
converted.push_str(&arg);
}
}
let arg = match field.field_type {
FieldType::Auto => summary.consume_next(),
FieldType::Index(index) => summary.consume_arg(index),
FieldType::Keyword(name) => summary.consume_kwarg(&name),
}?;
converted.push_str(&formatted_expr(
arg,
if field.parts.is_empty() {
FormatContext::Bare
} else {
FormatContext::Accessed
},
locator,
));
for part in field.parts {
match part {
@@ -258,7 +306,7 @@ pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &E
// Currently, the only issue we know of is in LibCST:
// https://github.com/Instagram/LibCST/issues/846
let Some(mut contents) = try_convert_to_f_string(checker, expr) else {
let Some(mut contents) = try_convert_to_f_string( expr, checker.locator) else {
return;
};

View File

@@ -10,7 +10,7 @@ pub(crate) use deprecated_import::{deprecated_import, DeprecatedImport};
pub(crate) use deprecated_mock_import::{
deprecated_mock_attribute, deprecated_mock_import, DeprecatedMockImport,
};
pub(crate) use deprecated_unittest_alias::{deprecated_unittest_alias, DeprecatedUnittestAlias};
pub(crate) use deprecated_unittest_alias::DeprecatedUnittestAlias;
pub(crate) use extraneous_parentheses::{extraneous_parentheses, ExtraneousParentheses};
pub(crate) use f_strings::{f_strings, FString};
pub(crate) use format_literals::{format_literals, FormatLiterals};
@@ -20,31 +20,29 @@ pub(crate) use lru_cache_with_maxsize_none::{
pub(crate) use lru_cache_without_parameters::{
lru_cache_without_parameters, LRUCacheWithoutParameters,
};
pub(crate) use native_literals::{native_literals, NativeLiterals};
pub(crate) use open_alias::{open_alias, OpenAlias};
pub(crate) use os_error_alias::{
os_error_alias_call, os_error_alias_handlers, os_error_alias_raise, OSErrorAlias,
};
pub(crate) use native_literals::NativeLiterals;
pub(crate) use open_alias::OpenAlias;
pub(crate) use os_error_alias::{os_error_alias_handlers, os_error_alias_raise, OSErrorAlias};
pub(crate) use outdated_version_block::{outdated_version_block, OutdatedVersionBlock};
pub(crate) use printf_string_formatting::{printf_string_formatting, PrintfStringFormatting};
pub(crate) use quoted_annotation::{quoted_annotation, QuotedAnnotation};
pub(crate) use redundant_open_modes::{redundant_open_modes, RedundantOpenModes};
pub(crate) use replace_stdout_stderr::{replace_stdout_stderr, ReplaceStdoutStderr};
pub(crate) use replace_universal_newlines::{replace_universal_newlines, ReplaceUniversalNewlines};
pub(crate) use super_call_with_parameters::{super_call_with_parameters, SuperCallWithParameters};
pub(crate) use type_of_primitive::{type_of_primitive, TypeOfPrimitive};
pub(crate) use redundant_open_modes::RedundantOpenModes;
pub(crate) use replace_stdout_stderr::ReplaceStdoutStderr;
pub(crate) use replace_universal_newlines::ReplaceUniversalNewlines;
pub(crate) use super_call_with_parameters::SuperCallWithParameters;
pub(crate) use type_of_primitive::TypeOfPrimitive;
pub(crate) use typing_text_str_alias::{typing_text_str_alias, TypingTextStrAlias};
pub(crate) use unicode_kind_prefix::{unicode_kind_prefix, UnicodeKindPrefix};
pub(crate) use unnecessary_builtin_import::{unnecessary_builtin_import, UnnecessaryBuiltinImport};
pub(crate) use unnecessary_coding_comment::{unnecessary_coding_comment, UTF8EncodingDeclaration};
pub(crate) use unnecessary_encode_utf8::{unnecessary_encode_utf8, UnnecessaryEncodeUTF8};
pub(crate) use unnecessary_encode_utf8::UnnecessaryEncodeUTF8;
pub(crate) use unnecessary_future_import::{unnecessary_future_import, UnnecessaryFutureImport};
pub(crate) use unpacked_list_comprehension::{
unpacked_list_comprehension, UnpackedListComprehension,
};
pub(crate) use use_pep585_annotation::{use_pep585_annotation, NonPEP585Annotation};
pub(crate) use use_pep604_annotation::{use_pep604_annotation, NonPEP604Annotation};
pub(crate) use use_pep604_isinstance::{use_pep604_isinstance, NonPEP604Isinstance};
pub(crate) use use_pep604_isinstance::NonPEP604Isinstance;
pub(crate) use useless_metaclass_type::{useless_metaclass_type, UselessMetaclassType};
pub(crate) use useless_object_inheritance::{useless_object_inheritance, UselessObjectInheritance};
pub(crate) use yield_in_for_loop::{yield_in_for_loop, YieldInForLoop};

View File

@@ -1,14 +1,14 @@
use std::fmt;
use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use rustpython_parser::ast::{self, Constant, Expr, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::is_implicit_concatenation;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::{AsRule, Rule};
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum LiteralType {
@@ -43,15 +43,30 @@ impl AlwaysAutofixableViolation for NativeLiterals {
}
}
impl Analyzer<ast::ExprCall> for NativeLiterals {
fn rule() -> Rule {
Rule::NativeLiterals
}
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
native_literals(diagnostics, checker, node);
}
}
/// UP018
pub(crate) fn native_literals(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
ast::ExprCall {
func,
args,
keywords,
range,
}: &ast::ExprCall,
) {
let Expr::Name(ast::ExprName { id, .. }) = func else { return; };
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
return;
};
if !keywords.is_empty() || args.len() > 1 {
return;
@@ -63,7 +78,7 @@ pub(crate) fn native_literals(
LiteralType::Str
} else {
LiteralType::Bytes
}}, expr.range());
}}, *range);
if checker.patch(diagnostic.kind.rule()) {
let constant = if id == "bytes" {
Constant::Bytes(vec![])
@@ -74,10 +89,10 @@ pub(crate) fn native_literals(
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
content,
expr.range(),
*range,
)));
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
return;
};
@@ -121,15 +136,15 @@ pub(crate) fn native_literals(
LiteralType::Bytes
},
},
expr.range(),
*range,
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
arg_code.to_string(),
expr.range(),
*range,
)));
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}
}

View File

@@ -1,10 +1,11 @@
use rustpython_parser::ast::{Expr, Ranged};
use rustpython_parser::ast::{self, Ranged};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::{AsRule, Rule};
#[violation]
pub struct OpenAlias;
@@ -22,25 +23,39 @@ impl Violation for OpenAlias {
}
}
impl Analyzer<ast::ExprCall> for OpenAlias {
fn rule() -> Rule {
Rule::OpenAlias
}
fn run(diagnostics: &mut Vec<Diagnostic>, context: &RuleContext, node: &ast::ExprCall) {
open_alias(diagnostics, context, node);
}
}
/// UP020
pub(crate) fn open_alias(checker: &mut Checker, expr: &Expr, func: &Expr) {
if checker
pub(crate) fn open_alias(
diagnostics: &mut Vec<Diagnostic>,
context: &RuleContext,
ast::ExprCall { func, range, .. }: &ast::ExprCall,
) {
if context
.ctx
.resolve_call_path(func)
.map_or(false, |call_path| call_path.as_slice() == ["io", "open"])
{
let fixable = checker
let fixable = context
.ctx
.find_binding("open")
.map_or(true, |binding| binding.kind.is_builtin());
let mut diagnostic = Diagnostic::new(OpenAlias, expr.range());
if fixable && checker.patch(diagnostic.kind.rule()) {
let mut diagnostic = Diagnostic::new(OpenAlias, *range);
if fixable && context.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
"open".to_string(),
func.range(),
)));
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}
}

View File

@@ -4,11 +4,11 @@ use rustpython_parser::ast::{self, Excepthandler, Expr, ExprContext, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::compose_call_path;
use ruff_python_semantic::context::Context;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::{Checker, RuleContext};
use crate::registry::{AsRule, Rule};
#[violation]
pub struct OSErrorAlias {
@@ -30,6 +30,16 @@ impl AlwaysAutofixableViolation for OSErrorAlias {
}
}
impl Analyzer<ast::ExprCall> for OSErrorAlias {
fn rule() -> Rule {
Rule::OSErrorAlias
}
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
os_error_alias_call(diagnostics, checker, node);
}
}
const ALIASES: &[(&str, &str)] = &[
("", "EnvironmentError"),
("", "IOError"),
@@ -73,6 +83,24 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
checker.diagnostics.push(diagnostic);
}
/// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`].
fn immutable_atom_diagnostic(checker: &RuleContext, target: &Expr) -> Diagnostic {
let mut diagnostic = Diagnostic::new(
OSErrorAlias {
name: compose_call_path(target),
},
target.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
"OSError".to_string(),
target.range(),
)));
}
diagnostic
}
/// Create a [`Diagnostic`] for a tuple of expressions.
fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) {
let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, target.range());
@@ -156,9 +184,13 @@ pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[Excepth
}
/// UP024
pub(crate) fn os_error_alias_call(checker: &mut Checker, func: &Expr) {
if is_alias(&checker.ctx, func) {
atom_diagnostic(checker, func);
pub(crate) fn os_error_alias_call(
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
ast::ExprCall { func, .. }: &ast::ExprCall,
) {
if is_alias(checker.ctx, func) {
diagnostics.push(immutable_atom_diagnostic(checker, func));
}
}

View File

@@ -1,6 +1,6 @@
use ruff_text_size::TextRange;
use std::str::FromStr;
use ruff_text_size::TextRange;
use rustpython_format::cformat::{
CConversionFlags, CFormatPart, CFormatPrecision, CFormatQuantity, CFormatString,
};

View File

@@ -1,6 +1,7 @@
use ruff_text_size::TextRange;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::TextRange;
use crate::checkers::ast::Checker;
use crate::registry::Rule;

View File

@@ -10,7 +10,8 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::source_code::Locator;
use crate::checkers::ast::Checker;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::Rule;
#[violation]
@@ -41,6 +42,16 @@ impl AlwaysAutofixableViolation for RedundantOpenModes {
}
}
impl Analyzer<ast::ExprCall> for RedundantOpenModes {
fn rule() -> Rule {
Rule::RedundantOpenModes
}
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
redundant_open_modes(diagnostics, checker, node);
}
}
const OPEN_FUNC_NAME: &str = "open";
const MODE_KEYWORD_ARGUMENT: &str = "mode";
@@ -86,24 +97,24 @@ impl OpenMode {
}
}
fn match_open(expr: &Expr) -> (Option<&Expr>, Vec<Keyword>) {
if let Expr::Call(ast::ExprCall {
fn match_open(
ast::ExprCall {
func,
args,
keywords,
range: _,
}) = expr
{
if matches!(func.as_ref(), Expr::Name(ast::ExprName {id, ..}) if id == OPEN_FUNC_NAME) {
// Return the "open mode" parameter and keywords.
return (args.get(1), keywords.clone());
}
}: &ast::ExprCall,
) -> (Option<&Expr>, Vec<Keyword>) {
if matches!(func.as_ref(), Expr::Name(ast::ExprName {id, ..}) if id == OPEN_FUNC_NAME) {
// Return the "open mode" parameter and keywords.
(args.get(1), keywords.clone())
} else {
(None, vec![])
}
(None, vec![])
}
fn create_check(
expr: &Expr,
expr: &ast::ExprCall,
mode_param: &Expr,
replacement_value: Option<&str>,
locator: &Locator,
@@ -130,7 +141,11 @@ fn create_check(
diagnostic
}
fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) -> Result<Edit> {
fn create_remove_param_fix(
locator: &Locator,
expr: &ast::ExprCall,
mode_param: &Expr,
) -> Result<Edit> {
let content = locator.slice(expr.range());
// Find the last comma before mode_param and create a deletion fix
// starting from the comma and ending after mode_param.
@@ -171,7 +186,11 @@ fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) ->
}
/// UP015
pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
pub(crate) fn redundant_open_modes(
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
expr: &ast::ExprCall,
) {
// If `open` has been rebound, skip this check entirely.
if !checker.ctx.is_builtin(OPEN_FUNC_NAME) {
return;
@@ -185,7 +204,7 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
}) = &keyword.value
{
if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) {
checker.diagnostics.push(create_check(
diagnostics.push(create_check(
expr,
&keyword.value,
mode.replacement_value(),
@@ -202,7 +221,7 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
}) = &mode_param
{
if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) {
checker.diagnostics.push(create_check(
diagnostics.push(create_check(
expr,
mode_param,
mode.replacement_value(),

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use rustpython_parser::ast::{Expr, Keyword, Ranged};
use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -7,8 +7,9 @@ use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::source_code::Locator;
use crate::autofix::actions::remove_argument;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::{AsRule, Rule};
#[violation]
pub struct ReplaceStdoutStderr;
@@ -24,6 +25,16 @@ impl AlwaysAutofixableViolation for ReplaceStdoutStderr {
}
}
impl Analyzer<ast::ExprCall> for ReplaceStdoutStderr {
fn rule() -> Rule {
Rule::ReplaceStdoutStderr
}
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
replace_stdout_stderr(diagnostics, checker, node);
}
}
/// Generate a [`Edit`] for a `stdout` and `stderr` [`Keyword`] pair.
fn generate_fix(
locator: &Locator,
@@ -54,11 +65,14 @@ fn generate_fix(
/// UP022
pub(crate) fn replace_stdout_stderr(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
ast::ExprCall {
func,
args,
keywords,
range,
}: &ast::ExprCall,
) {
if checker
.ctx
@@ -92,12 +106,12 @@ pub(crate) fn replace_stdout_stderr(
return;
}
let mut diagnostic = Diagnostic::new(ReplaceStdoutStderr, expr.range());
let mut diagnostic = Diagnostic::new(ReplaceStdoutStderr, *range);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
generate_fix(checker.locator, func, args, keywords, stdout, stderr)
});
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}
}

View File

@@ -1,12 +1,13 @@
use ruff_text_size::{TextLen, TextRange};
use rustpython_parser::ast::{Expr, Keyword, Ranged};
use rustpython_parser::ast::{self, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::{AsRule, Rule};
#[violation]
pub struct ReplaceUniversalNewlines;
@@ -22,8 +23,22 @@ impl AlwaysAutofixableViolation for ReplaceUniversalNewlines {
}
}
impl Analyzer<ast::ExprCall> for ReplaceUniversalNewlines {
fn rule() -> Rule {
Rule::ReplaceUniversalNewlines
}
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
replace_universal_newlines(diagnostics, checker, node);
}
}
/// UP021
pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwargs: &[Keyword]) {
pub(crate) fn replace_universal_newlines(
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
ast::ExprCall { func, keywords, .. }: &ast::ExprCall,
) {
if checker
.ctx
.resolve_call_path(func)
@@ -31,8 +46,8 @@ pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwa
call_path.as_slice() == ["subprocess", "run"]
})
{
let Some(kwarg) = find_keyword(kwargs, "universal_newlines") else { return; };
let range = TextRange::at(kwarg.start(), "universal_newlines".text_len());
let Some(keyword) = find_keyword(keywords, "universal_newlines") else { return; };
let range = TextRange::at(keyword.start(), "universal_newlines".text_len());
let mut diagnostic = Diagnostic::new(ReplaceUniversalNewlines, range);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
@@ -41,6 +56,6 @@ pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwa
range,
)));
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}
}

View File

@@ -1,11 +1,12 @@
use rustpython_parser::ast::{self, Arg, Expr, Ranged, Stmt};
use rustpython_parser::ast::{self, Arg, Expr, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::scope::ScopeKind;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::{AsRule, Rule};
use crate::rules::pyupgrade::fixes;
#[violation]
@@ -22,6 +23,16 @@ impl AlwaysAutofixableViolation for SuperCallWithParameters {
}
}
impl Analyzer<ast::ExprCall> for SuperCallWithParameters {
fn rule() -> Rule {
Rule::SuperCallWithParameters
}
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
super_call_with_parameters(diagnostics, checker, node);
}
}
/// Returns `true` if a call is an argumented `super` invocation.
fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
if let Expr::Name(ast::ExprName { id, .. }) = func {
@@ -33,10 +44,11 @@ fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
/// UP008
pub(crate) fn super_call_with_parameters(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
ast::ExprCall {
func, args, range, ..
}: &ast::ExprCall,
) {
// Only bother going through the super check at all if we're in a `super` call.
// (We check this in `super_args` too, so this is just an optimization.)
@@ -55,7 +67,7 @@ pub(crate) fn super_call_with_parameters(
// For a `super` invocation to be unnecessary, the first argument needs to match
// the enclosing class, and the second argument needs to match the first
// argument to the enclosing function.
let [first_arg, second_arg] = args else {
let [first_arg, second_arg] = args.as_slice() else {
return;
};
@@ -97,12 +109,13 @@ pub(crate) fn super_call_with_parameters(
return;
}
let mut diagnostic = Diagnostic::new(SuperCallWithParameters, expr.range());
let mut diagnostic = Diagnostic::new(SuperCallWithParameters, *range);
if checker.patch(diagnostic.kind.rule()) {
if let Some(edit) = fixes::remove_super_arguments(checker.locator, checker.stylist, expr) {
if let Some(edit) = fixes::remove_super_arguments(*range, checker.locator, checker.stylist)
{
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(edit));
}
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}

View File

@@ -1,10 +1,11 @@
use rustpython_parser::ast::{self, Expr, Ranged};
use rustpython_parser::ast::{self, Expr};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::{AsRule, Rule};
use super::super::types::Primitive;
@@ -26,8 +27,24 @@ impl AlwaysAutofixableViolation for TypeOfPrimitive {
}
}
impl Analyzer<ast::ExprCall> for TypeOfPrimitive {
fn rule() -> Rule {
Rule::TypeOfPrimitive
}
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
type_of_primitive(diagnostics, checker, node);
}
}
/// UP003
pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
pub(crate) fn type_of_primitive(
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
ast::ExprCall {
func, args, range, ..
}: &ast::ExprCall,
) {
if args.len() != 1 {
return;
}
@@ -44,13 +61,13 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr,
let Some(primitive) = Primitive::from_constant(value) else {
return;
};
let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, expr.range());
let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, *range);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
primitive.builtin(),
expr.range(),
*range,
)));
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}

View File

@@ -7,7 +7,8 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::source_code::Locator;
use crate::autofix::actions::remove_argument;
use crate::checkers::ast::Checker;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::Rule;
#[derive(Debug, PartialEq, Eq)]
@@ -35,6 +36,16 @@ impl AlwaysAutofixableViolation for UnnecessaryEncodeUTF8 {
}
}
impl Analyzer<ast::ExprCall> for UnnecessaryEncodeUTF8 {
fn rule() -> Rule {
Rule::UnnecessaryEncodeUTF8
}
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
unnecessary_encode_utf8(diagnostics, checker, node);
}
}
const UTF8_LITERALS: &[&str] = &["utf-8", "utf8", "utf_8", "u8", "utf", "cp65001"];
fn match_encoded_variable(func: &Expr) -> Option<&Expr> {
@@ -75,8 +86,8 @@ enum EncodingArg<'a> {
/// Return the encoding argument to an `encode` call, if it can be determined to be a
/// UTF-8-equivalent encoding.
fn match_encoding_arg<'a>(args: &'a [Expr], kwargs: &'a [Keyword]) -> Option<EncodingArg<'a>> {
match (args.len(), kwargs.len()) {
fn match_encoding_arg<'a>(args: &'a [Expr], keywords: &'a [Keyword]) -> Option<EncodingArg<'a>> {
match (args.len(), keywords.len()) {
// Ex `"".encode()`
(0, 0) => return Some(EncodingArg::Empty),
// Ex `"".encode(encoding)`
@@ -88,7 +99,7 @@ fn match_encoding_arg<'a>(args: &'a [Expr], kwargs: &'a [Keyword]) -> Option<Enc
}
// Ex `"".encode(kwarg=kwarg)`
(0, 1) => {
let kwarg = &kwargs[0];
let kwarg = &keywords[0];
if kwarg.arg.as_ref().map_or(false, |arg| arg == "encoding") {
if is_utf8_encoding_arg(&kwarg.value) {
return Some(EncodingArg::Keyword(kwarg));
@@ -102,12 +113,12 @@ fn match_encoding_arg<'a>(args: &'a [Expr], kwargs: &'a [Keyword]) -> Option<Enc
}
/// Return a [`Fix`] replacing the call to encode with a byte string.
fn replace_with_bytes_literal(locator: &Locator, expr: &Expr) -> Fix {
fn replace_with_bytes_literal(locator: &Locator, range: TextRange) -> Fix {
// Build up a replacement string by prefixing all string tokens with `b`.
let contents = locator.slice(expr.range());
let contents = locator.slice(range);
let mut replacement = String::with_capacity(contents.len() + 1);
let mut prev = expr.start();
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, expr.start()).flatten() {
let mut prev = range.start();
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, range.start()).flatten() {
match tok {
Tok::Dot => break,
Tok::String { .. } => {
@@ -125,16 +136,19 @@ fn replace_with_bytes_literal(locator: &Locator, expr: &Expr) -> Fix {
prev = range.end();
}
#[allow(deprecated)]
Fix::unspecified(Edit::range_replacement(replacement, expr.range()))
Fix::unspecified(Edit::range_replacement(replacement, range))
}
/// UP012
pub(crate) fn unnecessary_encode_utf8(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
kwargs: &[Keyword],
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
ast::ExprCall {
func,
args,
range,
keywords,
}: &ast::ExprCall,
) {
let Some(variable) = match_encoded_variable(func) else {
return;
@@ -145,19 +159,19 @@ pub(crate) fn unnecessary_encode_utf8(
..
}) => {
// Ex) `"str".encode()`, `"str".encode("utf-8")`
if let Some(encoding_arg) = match_encoding_arg(args, kwargs) {
if let Some(encoding_arg) = match_encoding_arg(args, keywords) {
if literal.is_ascii() {
// Ex) Convert `"foo".encode()` to `b"foo"`.
let mut diagnostic = Diagnostic::new(
UnnecessaryEncodeUTF8 {
reason: Reason::BytesLiteral,
},
expr.range(),
*range,
);
if checker.patch(Rule::UnnecessaryEncodeUTF8) {
diagnostic.set_fix(replace_with_bytes_literal(checker.locator, expr));
diagnostic.set_fix(replace_with_bytes_literal(checker.locator, *range));
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
} else if let EncodingArg::Keyword(kwarg) = encoding_arg {
// Ex) Convert `"unicode text©".encode(encoding="utf-8")` to
// `"unicode text©".encode()`.
@@ -165,7 +179,7 @@ pub(crate) fn unnecessary_encode_utf8(
UnnecessaryEncodeUTF8 {
reason: Reason::DefaultArgument,
},
expr.range(),
*range,
);
if checker.patch(Rule::UnnecessaryEncodeUTF8) {
#[allow(deprecated)]
@@ -175,19 +189,19 @@ pub(crate) fn unnecessary_encode_utf8(
func.start(),
kwarg.range(),
args,
kwargs,
keywords,
false,
)
});
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
} else if let EncodingArg::Positional(arg) = encoding_arg {
// Ex) Convert `"unicode text©".encode("utf-8")` to `"unicode text©".encode()`.
let mut diagnostic = Diagnostic::new(
UnnecessaryEncodeUTF8 {
reason: Reason::DefaultArgument,
},
expr.range(),
*range,
);
if checker.patch(Rule::UnnecessaryEncodeUTF8) {
#[allow(deprecated)]
@@ -197,18 +211,18 @@ pub(crate) fn unnecessary_encode_utf8(
func.start(),
arg.range(),
args,
kwargs,
keywords,
false,
)
});
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}
}
}
// Ex) `f"foo{bar}".encode("utf-8")`
Expr::JoinedStr(_) => {
if let Some(encoding_arg) = match_encoding_arg(args, kwargs) {
if let Some(encoding_arg) = match_encoding_arg(args, keywords) {
if let EncodingArg::Keyword(kwarg) = encoding_arg {
// Ex) Convert `f"unicode text©".encode(encoding="utf-8")` to
// `f"unicode text©".encode()`.
@@ -216,7 +230,7 @@ pub(crate) fn unnecessary_encode_utf8(
UnnecessaryEncodeUTF8 {
reason: Reason::DefaultArgument,
},
expr.range(),
*range,
);
if checker.patch(Rule::UnnecessaryEncodeUTF8) {
#[allow(deprecated)]
@@ -226,19 +240,19 @@ pub(crate) fn unnecessary_encode_utf8(
func.start(),
kwarg.range(),
args,
kwargs,
keywords,
false,
)
});
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
} else if let EncodingArg::Positional(arg) = encoding_arg {
// Ex) Convert `f"unicode text©".encode("utf-8")` to `f"unicode text©".encode()`.
let mut diagnostic = Diagnostic::new(
UnnecessaryEncodeUTF8 {
reason: Reason::DefaultArgument,
},
expr.range(),
*range,
);
if checker.patch(Rule::UnnecessaryEncodeUTF8) {
#[allow(deprecated)]
@@ -248,12 +262,12 @@ pub(crate) fn unnecessary_encode_utf8(
func.start(),
arg.range(),
args,
kwargs,
keywords,
false,
)
});
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}
}
}

View File

@@ -6,8 +6,10 @@ use rustpython_parser::ast::{self, Expr, Operator, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::checkers::ast::traits::Analyzer;
use crate::checkers::ast::RuleContext;
use crate::registry::{AsRule, Rule};
use crate::settings::types::PythonVersion;
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum CallKind {
@@ -50,6 +52,16 @@ impl AlwaysAutofixableViolation for NonPEP604Isinstance {
}
}
impl Analyzer<ast::ExprCall> for NonPEP604Isinstance {
fn rule() -> Rule {
Rule::NonPEP604Isinstance
}
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
use_pep604_isinstance(diagnostics, checker, node);
}
}
fn union(elts: &[Expr]) -> Expr {
if elts.len() == 1 {
elts[0].clone()
@@ -65,12 +77,17 @@ fn union(elts: &[Expr]) -> Expr {
/// UP038
pub(crate) fn use_pep604_isinstance(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
diagnostics: &mut Vec<Diagnostic>,
checker: &RuleContext,
ast::ExprCall {
func, args, range, ..
}: &ast::ExprCall,
) {
if let Expr::Name(ast::ExprName { id, .. }) = func {
if checker.settings.target_version < PythonVersion::Py310 {
return;
}
if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() {
let Some(kind) = CallKind::from_name(id) else {
return;
};
@@ -89,7 +106,7 @@ pub(crate) fn use_pep604_isinstance(
return;
}
let mut diagnostic = Diagnostic::new(NonPEP604Isinstance { kind }, expr.range());
let mut diagnostic = Diagnostic::new(NonPEP604Isinstance { kind }, *range);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
@@ -97,7 +114,7 @@ pub(crate) fn use_pep604_isinstance(
types.range(),
)));
}
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}
}
}

View File

@@ -0,0 +1,443 @@
---
source: crates/ruff/src/rules/pyupgrade/mod.rs
---
UP032_2.py:2:1: UP032 [*] Use f-string instead of `format` call
|
2 | # Errors
3 | "{.real}".format(1)
| ^^^^^^^^^^^^^^^^^^^ UP032
4 | "{0.real}".format(1)
5 | "{a.real}".format(a=1)
|
= help: Convert to f-string
Suggested fix
1 1 | # Errors
2 |-"{.real}".format(1)
2 |+f"{(1).real}"
3 3 | "{0.real}".format(1)
4 4 | "{a.real}".format(a=1)
5 5 |
UP032_2.py:3:1: UP032 [*] Use f-string instead of `format` call
|
3 | # Errors
4 | "{.real}".format(1)
5 | "{0.real}".format(1)
| ^^^^^^^^^^^^^^^^^^^^ UP032
6 | "{a.real}".format(a=1)
|
= help: Convert to f-string
Suggested fix
1 1 | # Errors
2 2 | "{.real}".format(1)
3 |-"{0.real}".format(1)
3 |+f"{(1).real}"
4 4 | "{a.real}".format(a=1)
5 5 |
6 6 | "{.real}".format(1.0)
UP032_2.py:4:1: UP032 [*] Use f-string instead of `format` call
|
4 | "{.real}".format(1)
5 | "{0.real}".format(1)
6 | "{a.real}".format(a=1)
| ^^^^^^^^^^^^^^^^^^^^^^ UP032
7 |
8 | "{.real}".format(1.0)
|
= help: Convert to f-string
Suggested fix
1 1 | # Errors
2 2 | "{.real}".format(1)
3 3 | "{0.real}".format(1)
4 |-"{a.real}".format(a=1)
4 |+f"{(1).real}"
5 5 |
6 6 | "{.real}".format(1.0)
7 7 | "{0.real}".format(1.0)
UP032_2.py:6:1: UP032 [*] Use f-string instead of `format` call
|
6 | "{a.real}".format(a=1)
7 |
8 | "{.real}".format(1.0)
| ^^^^^^^^^^^^^^^^^^^^^ UP032
9 | "{0.real}".format(1.0)
10 | "{a.real}".format(a=1.0)
|
= help: Convert to f-string
Suggested fix
3 3 | "{0.real}".format(1)
4 4 | "{a.real}".format(a=1)
5 5 |
6 |-"{.real}".format(1.0)
6 |+f"{1.0.real}"
7 7 | "{0.real}".format(1.0)
8 8 | "{a.real}".format(a=1.0)
9 9 |
UP032_2.py:7:1: UP032 [*] Use f-string instead of `format` call
|
7 | "{.real}".format(1.0)
8 | "{0.real}".format(1.0)
| ^^^^^^^^^^^^^^^^^^^^^^ UP032
9 | "{a.real}".format(a=1.0)
|
= help: Convert to f-string
Suggested fix
4 4 | "{a.real}".format(a=1)
5 5 |
6 6 | "{.real}".format(1.0)
7 |-"{0.real}".format(1.0)
7 |+f"{1.0.real}"
8 8 | "{a.real}".format(a=1.0)
9 9 |
10 10 | "{.real}".format(1j)
UP032_2.py:8:1: UP032 [*] Use f-string instead of `format` call
|
8 | "{.real}".format(1.0)
9 | "{0.real}".format(1.0)
10 | "{a.real}".format(a=1.0)
| ^^^^^^^^^^^^^^^^^^^^^^^^ UP032
11 |
12 | "{.real}".format(1j)
|
= help: Convert to f-string
Suggested fix
5 5 |
6 6 | "{.real}".format(1.0)
7 7 | "{0.real}".format(1.0)
8 |-"{a.real}".format(a=1.0)
8 |+f"{1.0.real}"
9 9 |
10 10 | "{.real}".format(1j)
11 11 | "{0.real}".format(1j)
UP032_2.py:10:1: UP032 [*] Use f-string instead of `format` call
|
10 | "{a.real}".format(a=1.0)
11 |
12 | "{.real}".format(1j)
| ^^^^^^^^^^^^^^^^^^^^ UP032
13 | "{0.real}".format(1j)
14 | "{a.real}".format(a=1j)
|
= help: Convert to f-string
Suggested fix
7 7 | "{0.real}".format(1.0)
8 8 | "{a.real}".format(a=1.0)
9 9 |
10 |-"{.real}".format(1j)
10 |+f"{1j.real}"
11 11 | "{0.real}".format(1j)
12 12 | "{a.real}".format(a=1j)
13 13 |
UP032_2.py:11:1: UP032 [*] Use f-string instead of `format` call
|
11 | "{.real}".format(1j)
12 | "{0.real}".format(1j)
| ^^^^^^^^^^^^^^^^^^^^^ UP032
13 | "{a.real}".format(a=1j)
|
= help: Convert to f-string
Suggested fix
8 8 | "{a.real}".format(a=1.0)
9 9 |
10 10 | "{.real}".format(1j)
11 |-"{0.real}".format(1j)
11 |+f"{1j.real}"
12 12 | "{a.real}".format(a=1j)
13 13 |
14 14 | "{.real}".format(0b01)
UP032_2.py:12:1: UP032 [*] Use f-string instead of `format` call
|
12 | "{.real}".format(1j)
13 | "{0.real}".format(1j)
14 | "{a.real}".format(a=1j)
| ^^^^^^^^^^^^^^^^^^^^^^^ UP032
15 |
16 | "{.real}".format(0b01)
|
= help: Convert to f-string
Suggested fix
9 9 |
10 10 | "{.real}".format(1j)
11 11 | "{0.real}".format(1j)
12 |-"{a.real}".format(a=1j)
12 |+f"{1j.real}"
13 13 |
14 14 | "{.real}".format(0b01)
15 15 | "{0.real}".format(0b01)
UP032_2.py:14:1: UP032 [*] Use f-string instead of `format` call
|
14 | "{a.real}".format(a=1j)
15 |
16 | "{.real}".format(0b01)
| ^^^^^^^^^^^^^^^^^^^^^^ UP032
17 | "{0.real}".format(0b01)
18 | "{a.real}".format(a=0b01)
|
= help: Convert to f-string
Suggested fix
11 11 | "{0.real}".format(1j)
12 12 | "{a.real}".format(a=1j)
13 13 |
14 |-"{.real}".format(0b01)
14 |+f"{0b01.real}"
15 15 | "{0.real}".format(0b01)
16 16 | "{a.real}".format(a=0b01)
17 17 |
UP032_2.py:15:1: UP032 [*] Use f-string instead of `format` call
|
15 | "{.real}".format(0b01)
16 | "{0.real}".format(0b01)
| ^^^^^^^^^^^^^^^^^^^^^^^ UP032
17 | "{a.real}".format(a=0b01)
|
= help: Convert to f-string
Suggested fix
12 12 | "{a.real}".format(a=1j)
13 13 |
14 14 | "{.real}".format(0b01)
15 |-"{0.real}".format(0b01)
15 |+f"{0b01.real}"
16 16 | "{a.real}".format(a=0b01)
17 17 |
18 18 | "{}".format(1 + 2)
UP032_2.py:16:1: UP032 [*] Use f-string instead of `format` call
|
16 | "{.real}".format(0b01)
17 | "{0.real}".format(0b01)
18 | "{a.real}".format(a=0b01)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
19 |
20 | "{}".format(1 + 2)
|
= help: Convert to f-string
Suggested fix
13 13 |
14 14 | "{.real}".format(0b01)
15 15 | "{0.real}".format(0b01)
16 |-"{a.real}".format(a=0b01)
16 |+f"{0b01.real}"
17 17 |
18 18 | "{}".format(1 + 2)
19 19 | "{}".format([1, 2])
UP032_2.py:18:1: UP032 [*] Use f-string instead of `format` call
|
18 | "{a.real}".format(a=0b01)
19 |
20 | "{}".format(1 + 2)
| ^^^^^^^^^^^^^^^^^^ UP032
21 | "{}".format([1, 2])
22 | "{}".format({1, 2})
|
= help: Convert to f-string
Suggested fix
15 15 | "{0.real}".format(0b01)
16 16 | "{a.real}".format(a=0b01)
17 17 |
18 |-"{}".format(1 + 2)
18 |+f"{1 + 2}"
19 19 | "{}".format([1, 2])
20 20 | "{}".format({1, 2})
21 21 | "{}".format({1: 2, 3: 4})
UP032_2.py:19:1: UP032 [*] Use f-string instead of `format` call
|
19 | "{}".format(1 + 2)
20 | "{}".format([1, 2])
| ^^^^^^^^^^^^^^^^^^^ UP032
21 | "{}".format({1, 2})
22 | "{}".format({1: 2, 3: 4})
|
= help: Convert to f-string
Suggested fix
16 16 | "{a.real}".format(a=0b01)
17 17 |
18 18 | "{}".format(1 + 2)
19 |-"{}".format([1, 2])
19 |+f"{[1, 2]}"
20 20 | "{}".format({1, 2})
21 21 | "{}".format({1: 2, 3: 4})
22 22 | "{}".format((i for i in range(2)))
UP032_2.py:20:1: UP032 [*] Use f-string instead of `format` call
|
20 | "{}".format(1 + 2)
21 | "{}".format([1, 2])
22 | "{}".format({1, 2})
| ^^^^^^^^^^^^^^^^^^^ UP032
23 | "{}".format({1: 2, 3: 4})
24 | "{}".format((i for i in range(2)))
|
= help: Convert to f-string
Suggested fix
17 17 |
18 18 | "{}".format(1 + 2)
19 19 | "{}".format([1, 2])
20 |-"{}".format({1, 2})
20 |+f"{({1, 2})}"
21 21 | "{}".format({1: 2, 3: 4})
22 22 | "{}".format((i for i in range(2)))
23 23 |
UP032_2.py:21:1: UP032 [*] Use f-string instead of `format` call
|
21 | "{}".format([1, 2])
22 | "{}".format({1, 2})
23 | "{}".format({1: 2, 3: 4})
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
24 | "{}".format((i for i in range(2)))
|
= help: Convert to f-string
Suggested fix
18 18 | "{}".format(1 + 2)
19 19 | "{}".format([1, 2])
20 20 | "{}".format({1, 2})
21 |-"{}".format({1: 2, 3: 4})
21 |+f"{({1: 2, 3: 4})}"
22 22 | "{}".format((i for i in range(2)))
23 23 |
24 24 | "{.real}".format(1 + 2)
UP032_2.py:22:1: UP032 [*] Use f-string instead of `format` call
|
22 | "{}".format({1, 2})
23 | "{}".format({1: 2, 3: 4})
24 | "{}".format((i for i in range(2)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
25 |
26 | "{.real}".format(1 + 2)
|
= help: Convert to f-string
Suggested fix
19 19 | "{}".format([1, 2])
20 20 | "{}".format({1, 2})
21 21 | "{}".format({1: 2, 3: 4})
22 |-"{}".format((i for i in range(2)))
22 |+f"{(i for i in range(2))}"
23 23 |
24 24 | "{.real}".format(1 + 2)
25 25 | "{.real}".format([1, 2])
UP032_2.py:24:1: UP032 [*] Use f-string instead of `format` call
|
24 | "{}".format((i for i in range(2)))
25 |
26 | "{.real}".format(1 + 2)
| ^^^^^^^^^^^^^^^^^^^^^^^ UP032
27 | "{.real}".format([1, 2])
28 | "{.real}".format({1, 2})
|
= help: Convert to f-string
Suggested fix
21 21 | "{}".format({1: 2, 3: 4})
22 22 | "{}".format((i for i in range(2)))
23 23 |
24 |-"{.real}".format(1 + 2)
24 |+f"{(1 + 2).real}"
25 25 | "{.real}".format([1, 2])
26 26 | "{.real}".format({1, 2})
27 27 | "{.real}".format({1: 2, 3: 4})
UP032_2.py:25:1: UP032 [*] Use f-string instead of `format` call
|
25 | "{.real}".format(1 + 2)
26 | "{.real}".format([1, 2])
| ^^^^^^^^^^^^^^^^^^^^^^^^ UP032
27 | "{.real}".format({1, 2})
28 | "{.real}".format({1: 2, 3: 4})
|
= help: Convert to f-string
Suggested fix
22 22 | "{}".format((i for i in range(2)))
23 23 |
24 24 | "{.real}".format(1 + 2)
25 |-"{.real}".format([1, 2])
25 |+f"{[1, 2].real}"
26 26 | "{.real}".format({1, 2})
27 27 | "{.real}".format({1: 2, 3: 4})
28 28 | "{}".format((i for i in range(2)))
UP032_2.py:26:1: UP032 [*] Use f-string instead of `format` call
|
26 | "{.real}".format(1 + 2)
27 | "{.real}".format([1, 2])
28 | "{.real}".format({1, 2})
| ^^^^^^^^^^^^^^^^^^^^^^^^ UP032
29 | "{.real}".format({1: 2, 3: 4})
30 | "{}".format((i for i in range(2)))
|
= help: Convert to f-string
Suggested fix
23 23 |
24 24 | "{.real}".format(1 + 2)
25 25 | "{.real}".format([1, 2])
26 |-"{.real}".format({1, 2})
26 |+f"{({1, 2}).real}"
27 27 | "{.real}".format({1: 2, 3: 4})
28 28 | "{}".format((i for i in range(2)))
UP032_2.py:27:1: UP032 [*] Use f-string instead of `format` call
|
27 | "{.real}".format([1, 2])
28 | "{.real}".format({1, 2})
29 | "{.real}".format({1: 2, 3: 4})
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
30 | "{}".format((i for i in range(2)))
|
= help: Convert to f-string
Suggested fix
24 24 | "{.real}".format(1 + 2)
25 25 | "{.real}".format([1, 2])
26 26 | "{.real}".format({1, 2})
27 |-"{.real}".format({1: 2, 3: 4})
27 |+f"{({1: 2, 3: 4}).real}"
28 28 | "{}".format((i for i in range(2)))
UP032_2.py:28:1: UP032 [*] Use f-string instead of `format` call
|
28 | "{.real}".format({1, 2})
29 | "{.real}".format({1: 2, 3: 4})
30 | "{}".format((i for i in range(2)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
= help: Convert to f-string
Suggested fix
25 25 | "{.real}".format([1, 2])
26 26 | "{.real}".format({1, 2})
27 27 | "{.real}".format({1: 2, 3: 4})
28 |-"{}".format((i for i in range(2)))
28 |+f"{(i for i in range(2))}"

View File

@@ -1,9 +1,16 @@
use rustpython_parser::ast::{self, ConversionFlag, Expr, Ranged};
use anyhow::{bail, Result};
use libcst_native::{Codegen, CodegenState};
use rustpython_parser::ast::{self, Expr, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::source_code::{Locator, Stylist};
use crate::checkers::ast::Checker;
use crate::cst::matchers::{
match_call, match_expression, match_formatted_string, match_formatted_string_expression,
match_name,
};
use crate::registry::AsRule;
/// ## What it does
@@ -39,59 +46,101 @@ impl AlwaysAutofixableViolation for ExplicitFStringTypeConversion {
}
}
fn fix_explicit_f_string_type_conversion(
expr: &Expr,
index: usize,
locator: &Locator,
stylist: &Stylist,
) -> Result<Fix> {
// Replace the call node with its argument and a conversion flag.
let range = expr.range();
let content = locator.slice(range);
let mut expression = match_expression(content)?;
let formatted_string = match_formatted_string(&mut expression)?;
// Replace the formatted call expression at `index` with a conversion flag.
let mut formatted_string_expression =
match_formatted_string_expression(&mut formatted_string.parts[index])?;
let call = match_call(&mut formatted_string_expression.expression)?;
let name = match_name(&mut call.func)?;
match name.value {
"str" => {
formatted_string_expression.conversion = Some("s");
}
"repr" => {
formatted_string_expression.conversion = Some("r");
}
"ascii" => {
formatted_string_expression.conversion = Some("a");
}
_ => bail!("Unexpected function call: `{:?}`", name.value),
}
formatted_string_expression.expression = call.args[0].value.clone();
let mut state = CodegenState {
default_newline: &stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
expression.codegen(&mut state);
Ok(Fix::automatic(Edit::range_replacement(
state.to_string(),
range,
)))
}
/// RUF010
pub(crate) fn explicit_f_string_type_conversion(
checker: &mut Checker,
formatted_value: &Expr,
conversion: ConversionFlag,
expr: &Expr,
values: &[Expr],
) {
// Skip if there's already a conversion flag.
if !conversion.is_none() {
return;
for (index, formatted_value) in values.iter().enumerate() {
let Expr::FormattedValue(ast::ExprFormattedValue {
conversion,
value,
..
}) = &formatted_value else {
continue;
};
// Skip if there's already a conversion flag.
if !conversion.is_none() {
return;
}
let Expr::Call(ast::ExprCall {
func,
args,
keywords,
..
}) = value.as_ref() else {
return;
};
// Can't be a conversion otherwise.
if args.len() != 1 || !keywords.is_empty() {
return;
}
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
return;
};
if !matches!(id.as_str(), "str" | "repr" | "ascii") {
return;
};
if !checker.ctx.is_builtin(id) {
return;
}
let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, value.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fix_explicit_f_string_type_conversion(expr, index, checker.locator, checker.stylist)
});
}
checker.diagnostics.push(diagnostic);
}
let Expr::Call(ast::ExprCall {
func,
args,
keywords,
range: _,
}) = formatted_value else {
return;
};
// Can't be a conversion otherwise.
if args.len() != 1 || !keywords.is_empty() {
return;
}
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
return;
};
let conversion = match id.as_str() {
"ascii" => 'a',
"str" => 's',
"repr" => 'r',
_ => return,
};
if !checker.ctx.is_builtin(id) {
return;
}
let formatted_value_range = formatted_value.range();
let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, formatted_value_range);
if checker.patch(diagnostic.kind.rule()) {
let arg_range = args[0].range();
let remove_call = Edit::deletion(formatted_value_range.start(), arg_range.start());
let add_conversion = Edit::replacement(
format!("!{conversion}"),
arg_range.end(),
formatted_value_range.end(),
);
diagnostic.set_fix(Fix::automatic_edits(remove_call, [add_conversion]));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -65,7 +65,7 @@ RUF010.py:11:4: RUF010 [*] Use conversion in f-string
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
| ^^^^^^^^^^^ RUF010
14 |
15 | f"{foo(bla)}" # OK
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
= help: Replace f-string function call with conversion
@@ -76,7 +76,7 @@ RUF010.py:11:4: RUF010 [*] Use conversion in f-string
11 |-f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
11 |+f"{d['a']!s}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
12 12 |
13 13 | f"{foo(bla)}" # OK
13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
14 14 |
RUF010.py:11:19: RUF010 [*] Use conversion in f-string
@@ -86,7 +86,7 @@ RUF010.py:11:19: RUF010 [*] Use conversion in f-string
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
| ^^^^^^^^^^^^ RUF010
14 |
15 | f"{foo(bla)}" # OK
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
= help: Replace f-string function call with conversion
@@ -97,7 +97,7 @@ RUF010.py:11:19: RUF010 [*] Use conversion in f-string
11 |-f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
11 |+f"{str(d['a'])}, {d['b']!r}, {ascii(d['c'])}" # RUF010
12 12 |
13 13 | f"{foo(bla)}" # OK
13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
14 14 |
RUF010.py:11:35: RUF010 [*] Use conversion in f-string
@@ -107,7 +107,7 @@ RUF010.py:11:35: RUF010 [*] Use conversion in f-string
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
| ^^^^^^^^^^^^^ RUF010
14 |
15 | f"{foo(bla)}" # OK
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
= help: Replace f-string function call with conversion
@@ -118,7 +118,70 @@ RUF010.py:11:35: RUF010 [*] Use conversion in f-string
11 |-f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
11 |+f"{str(d['a'])}, {repr(d['b'])}, {d['c']!a}" # RUF010
12 12 |
13 13 | f"{foo(bla)}" # OK
13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
14 14 |
RUF010.py:13:5: RUF010 [*] Use conversion in f-string
|
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
14 |
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
| ^^^^^^^^ RUF010
16 |
17 | f"{foo(bla)}" # OK
|
= help: Replace f-string function call with conversion
Fix
10 10 |
11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
12 12 |
13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
13 |+f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010
14 14 |
15 15 | f"{foo(bla)}" # OK
16 16 |
RUF010.py:13:19: RUF010 [*] Use conversion in f-string
|
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
14 |
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
| ^^^^^^^^^ RUF010
16 |
17 | f"{foo(bla)}" # OK
|
= help: Replace f-string function call with conversion
Fix
10 10 |
11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
12 12 |
13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
13 |+f"{(str(bla))}, {bla!r}, {(ascii(bla))}" # RUF010
14 14 |
15 15 | f"{foo(bla)}" # OK
16 16 |
RUF010.py:13:34: RUF010 [*] Use conversion in f-string
|
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
14 |
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
| ^^^^^^^^^^ RUF010
16 |
17 | f"{foo(bla)}" # OK
|
= help: Replace f-string function call with conversion
Fix
10 10 |
11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
12 12 |
13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
13 |+f"{(str(bla))}, {(repr(bla))}, {bla!a}" # RUF010
14 14 |
15 15 | f"{foo(bla)}" # OK
16 16 |

View File

@@ -44,7 +44,7 @@ pub(crate) fn useless_try_except(checker: &mut Checker, handlers: &[Excepthandle
.iter()
.map(|handler| {
let ExceptHandler(ExcepthandlerExceptHandler { name, body, .. }) = handler;
let Some(Stmt::Raise(ast::StmtRaise { exc, .. })) = &body.first() else {
let Some(Stmt::Raise(ast::StmtRaise { exc, cause: None, .. })) = &body.first() else {
return None;
};
if let Some(expr) = exc {

View File

@@ -32,6 +32,7 @@ pub struct RuleSelection {
pub extend_select: Vec<RuleSelector>,
pub fixable: Option<Vec<RuleSelector>>,
pub unfixable: Vec<RuleSelector>,
pub extend_fixable: Vec<RuleSelector>,
}
#[derive(Debug, Default)]
@@ -47,6 +48,7 @@ pub struct Configuration {
pub extend: Option<PathBuf>,
pub extend_exclude: Vec<FilePattern>,
pub extend_include: Vec<FilePattern>,
pub extend_per_file_ignores: Vec<PerFileIgnore>,
pub external: Option<Vec<String>>,
pub fix: Option<bool>,
pub fix_only: Option<bool>,
@@ -101,7 +103,13 @@ impl Configuration {
.collect(),
extend_select: options.extend_select.unwrap_or_default(),
fixable: options.fixable,
unfixable: options.unfixable.unwrap_or_default(),
unfixable: options
.unfixable
.into_iter()
.flatten()
.chain(options.extend_unfixable.into_iter().flatten())
.collect(),
extend_fixable: options.extend_fixable.unwrap_or_default(),
}],
allowed_confusables: options.allowed_confusables,
builtins: options.builtins,
@@ -159,6 +167,17 @@ impl Configuration {
.collect()
})
.unwrap_or_default(),
extend_per_file_ignores: options
.extend_per_file_ignores
.map(|per_file_ignores| {
per_file_ignores
.into_iter()
.map(|(pattern, prefixes)| {
PerFileIgnore::new(pattern, &prefixes, Some(project_root))
})
.collect()
})
.unwrap_or_default(),
external: options.external,
fix: options.fix,
fix_only: options.fix_only,
@@ -247,6 +266,11 @@ impl Configuration {
.into_iter()
.chain(self.extend_include.into_iter())
.collect(),
extend_per_file_ignores: config
.extend_per_file_ignores
.into_iter()
.chain(self.extend_per_file_ignores.into_iter())
.collect(),
external: self.external.or(config.external),
fix: self.fix.or(config.fix),
fix_only: self.fix_only.or(config.fix_only),

View File

@@ -163,7 +163,12 @@ impl Settings {
line_length: config.line_length.unwrap_or(defaults::LINE_LENGTH),
namespace_packages: config.namespace_packages.unwrap_or_default(),
per_file_ignores: resolve_per_file_ignores(
config.per_file_ignores.unwrap_or_default(),
config
.per_file_ignores
.unwrap_or_default()
.into_iter()
.chain(config.extend_per_file_ignores)
.collect(),
)?,
respect_gitignore: config.respect_gitignore.unwrap_or(true),
src: config
@@ -258,16 +263,11 @@ impl From<&Configuration> for RuleTable {
// across config files (which otherwise wouldn't be possible since ruff
// only has `extended` but no `extended-by`).
let mut carryover_ignores: Option<&[RuleSelector]> = None;
let mut carryover_unfixables: Option<&[RuleSelector]> = None;
let mut redirects = FxHashMap::default();
for selection in &config.rule_selections {
// We do not have an extend-fixable option, so fixable and unfixable
// selectors can simply be applied directly to fixable_set.
if selection.fixable.is_some() {
fixable_set.clear();
}
// If a selection only specifies extend-select we cannot directly
// apply its rule selectors to the select_set because we firstly have
// to resolve the effectively selected rules within the current rule selection
@@ -276,10 +276,13 @@ impl From<&Configuration> for RuleTable {
// We do this via the following HashMap where the bool indicates
// whether to enable or disable the given rule.
let mut select_map_updates: FxHashMap<Rule, bool> = FxHashMap::default();
let mut fixable_map_updates: FxHashMap<Rule, bool> = FxHashMap::default();
let carriedover_ignores = carryover_ignores.take();
let carriedover_unfixables = carryover_unfixables.take();
for spec in Specificity::iter() {
// Iterate over rule selectors in order of specificity.
for selector in selection
.select
.iter()
@@ -301,17 +304,26 @@ impl From<&Configuration> for RuleTable {
select_map_updates.insert(rule, false);
}
}
if let Some(fixable) = &selection.fixable {
fixable_set
.extend(fixable.iter().filter(|s| s.specificity() == spec).flatten());
// Apply the same logic to `fixable` and `unfixable`.
for selector in selection
.fixable
.iter()
.flatten()
.chain(selection.extend_fixable.iter())
.filter(|s| s.specificity() == spec)
{
for rule in selector {
fixable_map_updates.insert(rule, true);
}
}
for selector in selection
.unfixable
.iter()
.chain(carriedover_unfixables.into_iter().flatten())
.filter(|s| s.specificity() == spec)
{
for rule in selector {
fixable_set.remove(rule);
fixable_map_updates.insert(rule, false);
}
}
}
@@ -341,6 +353,29 @@ impl From<&Configuration> for RuleTable {
}
}
// Apply the same logic to `fixable` and `unfixable`.
if let Some(fixable) = &selection.fixable {
fixable_set = fixable_map_updates
.into_iter()
.filter_map(|(rule, enabled)| enabled.then_some(rule))
.collect();
if fixable.is_empty()
&& selection.extend_fixable.is_empty()
&& !selection.unfixable.is_empty()
{
carryover_unfixables = Some(&selection.unfixable);
}
} else {
for (rule, enabled) in fixable_map_updates {
if enabled {
fixable_set.insert(rule);
} else {
fixable_set.remove(rule);
}
}
}
// We insert redirects into the hashmap so that we
// can warn the users about remapped rule codes.
for selector in selection
@@ -351,6 +386,7 @@ impl From<&Configuration> for RuleTable {
.chain(selection.ignore.iter())
.chain(selection.extend_select.iter())
.chain(selection.unfixable.iter())
.chain(selection.extend_fixable.iter())
{
if let RuleSelector::Prefix {
prefix,

View File

@@ -175,6 +175,24 @@ pub struct Options {
/// A list of rule codes or prefixes to enable, in addition to those
/// specified by `select`.
pub extend_select: Option<Vec<RuleSelector>>,
#[option(
default = r#"[]"#,
value_type = "list[RuleSelector]",
example = r#"
# Enable autofix for flake8-bugbear (`B`), on top of any rules specified by `fixable`.
extend-fixable = ["B"]
"#
)]
/// A list of rule codes or prefixes to consider autofixable, in addition to those
/// specified by `fixable`.
pub extend_fixable: Option<Vec<RuleSelector>>,
/// A list of rule codes or prefixes to consider non-auto-fixable, in addition to those
/// specified by `unfixable`.
///
/// This option has been **deprecated** in favor of `unfixable` since its usage is now
/// interchangeable with `unfixable`.
#[cfg_attr(feature = "schemars", schemars(skip))]
pub extend_unfixable: Option<Vec<RuleSelector>>,
#[option(
default = "[]",
value_type = "list[str]",
@@ -523,4 +541,16 @@ pub struct Options {
/// A list of mappings from file pattern to rule codes or prefixes to
/// exclude, when considering any matching files.
pub per_file_ignores: Option<FxHashMap<String, Vec<RuleSelector>>>,
#[option(
default = "{}",
value_type = "dict[str, list[RuleSelector]]",
example = r#"
# Also ignore `E401` in all `__init__.py` files.
[tool.ruff.extend-per-file-ignores]
"__init__.py" = ["E402"]
"#
)]
/// A list of mappings from file pattern to rule codes or prefixes to
/// exclude, in addition to any rules excluded by `per-file-ignores`.
pub extend_per_file_ignores: Option<FxHashMap<String, Vec<RuleSelector>>>,
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.267"
version = "0.0.269"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = { workspace = true }
rust-version = { workspace = true }

View File

@@ -57,6 +57,13 @@ pub enum Command {
/// Generate shell completion.
#[clap(alias = "--generate-shell-completion", hide = true)]
GenerateShellCompletion { shell: clap_complete_command::Shell },
/// Format the given files, or stdin when using `-`.
#[doc(hidden)]
#[clap(hide = true)]
Format {
/// List of files or directories to format or `-` for stdin
files: Vec<PathBuf>,
},
}
#[derive(Debug, clap::Args)]
@@ -126,8 +133,8 @@ pub struct CheckArgs {
hide_possible_values = true
)]
pub ignore: Option<Vec<RuleSelector>>,
/// Like --select, but adds additional rule codes on top of the selected
/// ones.
/// Like --select, but adds additional rule codes on top of those already
/// specified.
#[arg(
long,
value_delimiter = ',',
@@ -147,9 +154,13 @@ pub struct CheckArgs {
hide = true
)]
pub extend_ignore: Option<Vec<RuleSelector>>,
/// List of mappings from file pattern to code to exclude
/// List of mappings from file pattern to code to exclude.
#[arg(long, value_delimiter = ',', help_heading = "Rule selection")]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// Like `--per-file-ignores`, but adds additional ignores on top of
/// those already specified.
#[arg(long, value_delimiter = ',', help_heading = "Rule selection")]
pub extend_per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(
long,
@@ -189,6 +200,27 @@ pub struct CheckArgs {
hide_possible_values = true
)]
pub unfixable: Option<Vec<RuleSelector>>,
/// Like --fixable, but adds additional rule codes on top of those already
/// specified.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = parse_rule_selector,
help_heading = "Rule selection",
hide_possible_values = true
)]
pub extend_fixable: Option<Vec<RuleSelector>>,
/// Like --unfixable. (Deprecated: You can just use --unfixable instead.)
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = parse_rule_selector,
help_heading = "Rule selection",
hide = true
)]
pub extend_unfixable: Option<Vec<RuleSelector>>,
/// Respect file exclusions via `.gitignore` and other standard ignore
/// files.
#[arg(
@@ -375,8 +407,10 @@ impl CheckArgs {
dummy_variable_rgx: self.dummy_variable_rgx,
exclude: self.exclude,
extend_exclude: self.extend_exclude,
extend_fixable: self.extend_fixable,
extend_ignore: self.extend_ignore,
extend_select: self.extend_select,
extend_unfixable: self.extend_unfixable,
fixable: self.fixable,
ignore: self.ignore,
line_length: self.line_length,
@@ -442,8 +476,10 @@ pub struct Overrides {
pub dummy_variable_rgx: Option<Regex>,
pub exclude: Option<Vec<FilePattern>>,
pub extend_exclude: Option<Vec<FilePattern>>,
pub extend_fixable: Option<Vec<RuleSelector>>,
pub extend_ignore: Option<Vec<RuleSelector>>,
pub extend_select: Option<Vec<RuleSelector>>,
pub extend_unfixable: Option<Vec<RuleSelector>>,
pub fixable: Option<Vec<RuleSelector>>,
pub ignore: Option<Vec<RuleSelector>>,
pub line_length: Option<usize>,
@@ -493,7 +529,14 @@ impl ConfigProcessor for &Overrides {
.collect(),
extend_select: self.extend_select.clone().unwrap_or_default(),
fixable: self.fixable.clone(),
unfixable: self.unfixable.clone().unwrap_or_default(),
unfixable: self
.unfixable
.iter()
.cloned()
.chain(self.extend_unfixable.iter().cloned())
.flatten()
.collect(),
extend_fixable: self.extend_fixable.clone().unwrap_or_default(),
});
if let Some(format) = &self.format {
config.format = Some(*format);

View File

@@ -11,7 +11,7 @@ use crate::args::Overrides;
use crate::diagnostics::{lint_stdin, Diagnostics};
/// Read a `String` from `stdin`.
fn read_from_stdin() -> Result<String> {
pub(crate) fn read_from_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
Ok(buffer)

View File

@@ -1,10 +1,11 @@
use std::io::{self, BufWriter};
use std::path::PathBuf;
use std::io::{self, stdout, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
use anyhow::Result;
use anyhow::{Context, Result};
use clap::CommandFactory;
use log::warn;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use ruff::logging::{set_up_logging, LogLevel};
@@ -13,6 +14,7 @@ use ruff::settings::{flags, CliSettings};
use ruff::{fs, warn_user_once};
use crate::args::{Args, CheckArgs, Command};
use crate::commands::run_stdin::read_from_stdin;
use crate::printer::{Flags as PrinterFlags, Printer};
pub mod args;
@@ -117,11 +119,41 @@ quoting the executed command, along with the relevant file contents and `pyproje
shell.generate(&mut Args::command(), &mut io::stdout());
}
Command::Check(args) => return check(args, log_level),
Command::Format { files } => return format(&files),
}
Ok(ExitStatus::Success)
}
fn format(files: &[PathBuf]) -> Result<ExitStatus> {
warn_user_once!(
"`ruff format` is a work-in-progress, subject to change at any time, and intended for \
internal use only."
);
// dummy
let format_code = |code: &str| code.replace("# DEL", "");
match &files {
// Check if we should read from stdin
[path] if path == Path::new("-") => {
let unformatted = read_from_stdin()?;
let formatted = format_code(&unformatted);
stdout().lock().write_all(formatted.as_bytes())?;
}
_ => {
for file in files {
let unformatted = std::fs::read_to_string(file)
.with_context(|| format!("Could not read {}: ", file.display()))?;
let formatted = format_code(&unformatted);
std::fs::write(file, formatted)
.with_context(|| format!("Could not write to {}, exiting", file.display()))?;
}
}
}
Ok(ExitStatus::Success)
}
fn check(args: CheckArgs, log_level: LogLevel) -> Result<ExitStatus> {
#[cfg(feature = "ecosystem_ci")]
let ecosystem_ci = args.ecosystem_ci;

View File

@@ -19,7 +19,6 @@ memchr = "2.5.0"
num-bigint = { version = "0.4.3" }
num-traits = { version = "0.2.15" }
once_cell = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }
rustpython-literal = { workspace = true }
rustpython-parser = { workspace = true }

View File

@@ -4,8 +4,6 @@ use std::path::Path;
use itertools::Itertools;
use log::error;
use num_traits::Zero;
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_parser::ast::{
@@ -542,7 +540,9 @@ where
body.iter().any(|stmt| any_over_stmt(stmt, func))
}
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
fn is_dunder(id: &str) -> bool {
id.starts_with("__") && id.ends_with("__")
}
/// Return `true` if the [`Stmt`] is an assignment to a dunder (like `__all__`).
pub fn is_assignment_to_a_dunder(stmt: &Stmt) -> bool {
@@ -553,15 +553,19 @@ pub fn is_assignment_to_a_dunder(stmt: &Stmt) -> bool {
if targets.len() != 1 {
return false;
}
match &targets[0] {
Expr::Name(ast::ExprName { id, .. }) => DUNDER_REGEX.is_match(id.as_str()),
_ => false,
if let Expr::Name(ast::ExprName { id, .. }) = &targets[0] {
is_dunder(id)
} else {
false
}
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
is_dunder(id)
} else {
false
}
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => match target.as_ref() {
Expr::Name(ast::ExprName { id, .. }) => DUNDER_REGEX.is_match(id.as_str()),
_ => false,
},
_ => false,
}
}

View File

@@ -3,6 +3,7 @@ use std::path::Path;
use bitflags::bitflags;
use nohash_hasher::{BuildNoHashHasher, IntMap};
use ruff_text_size::TextRange;
use rustpython_parser::ast::{Expr, Stmt};
use smallvec::smallvec;
@@ -113,6 +114,141 @@ impl<'a> Context<'a> {
.map_or(false, |binding| binding.kind.is_builtin())
}
/// Resolve a reference to the given symbol.
pub fn resolve_reference(&mut self, symbol: &str, range: TextRange) -> ResolvedReference {
// PEP 563 indicates that if a forward reference can be resolved in the module scope, we
// should prefer it over local resolutions.
if self.in_deferred_type_definition() {
if let Some(binding_id) = self.scopes.global().get(symbol) {
// Mark the binding as used.
let context = self.execution_context();
self.bindings[*binding_id].mark_used(ScopeId::global(), range, context);
// Mark any submodule aliases as used.
if let Some(binding_id) = self.resolve_submodule(ScopeId::global(), *binding_id) {
self.bindings[binding_id].mark_used(ScopeId::global(), range, context);
}
return ResolvedReference::Resolved(*binding_id);
}
}
let mut seen_function = false;
let mut import_starred = false;
for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() {
let scope = &self.scopes[scope_id];
if scope.kind.is_class() {
// Allow usages of `__class__` within methods, e.g.:
//
// ```python
// class Foo:
// def __init__(self):
// print(__class__)
// ```
if seen_function && matches!(symbol, "__class__") {
return ResolvedReference::ImplicitGlobal;
}
if index > 0 {
continue;
}
}
if let Some(binding_id) = scope.get(symbol) {
// Mark the binding as used.
let context = self.execution_context();
self.bindings[*binding_id].mark_used(self.scope_id, range, context);
// Mark any submodule aliases as used.
if let Some(binding_id) = self.resolve_submodule(scope_id, *binding_id) {
self.bindings[binding_id].mark_used(ScopeId::global(), range, context);
}
// But if it's a type annotation, don't treat it as resolved, unless we're in a
// forward reference. For example, given:
//
// ```python
// name: str
// print(name)
// ```
//
// The `name` in `print(name)` should be treated as unresolved, but the `name` in
// `name: str` should be treated as used.
if !self.in_deferred_type_definition()
&& self.bindings[*binding_id].kind.is_annotation()
{
continue;
}
return ResolvedReference::Resolved(*binding_id);
}
// Allow usages of `__module__` and `__qualname__` within class scopes, e.g.:
//
// ```python
// class Foo:
// print(__qualname__)
// ```
//
// Intentionally defer this check to _after_ the standard `scope.get` logic, so that
// we properly attribute reads to overridden class members, e.g.:
//
// ```python
// class Foo:
// __qualname__ = "Bar"
// print(__qualname__)
// ```
if index == 0 && scope.kind.is_class() {
if matches!(symbol, "__module__" | "__qualname__") {
return ResolvedReference::ImplicitGlobal;
}
}
seen_function |= scope.kind.is_function();
import_starred = import_starred || scope.uses_star_imports();
}
if import_starred {
ResolvedReference::StarImport
} else {
ResolvedReference::NotFound
}
}
/// Given a `BindingId`, return the `BindingId` of the submodule import that it aliases.
fn resolve_submodule(&self, scope_id: ScopeId, binding_id: BindingId) -> Option<BindingId> {
// If the name of a submodule import is the same as an alias of another import, and the
// alias is used, then the submodule import should be marked as used too.
//
// For example, mark `pyarrow.csv` as used in:
//
// ```python
// import pyarrow as pa
// import pyarrow.csv
// print(pa.csv.read_csv("test.csv"))
// ```
let (name, full_name) = match &self.bindings[binding_id].kind {
BindingKind::Importation(Importation { name, full_name }) => (*name, *full_name),
BindingKind::SubmoduleImportation(SubmoduleImportation { name, full_name }) => {
(*name, *full_name)
}
BindingKind::FromImportation(FromImportation { name, full_name }) => {
(*name, full_name.as_str())
}
_ => return None,
};
let has_alias = full_name
.split('.')
.last()
.map(|segment| segment != name)
.unwrap_or_default();
if !has_alias {
return None;
}
self.scopes[scope_id].get(full_name).copied()
}
/// Resolves the [`Expr`] to a fully-qualified symbol-name, if `value` resolves to an imported
/// or builtin symbol.
///
@@ -701,10 +837,24 @@ impl ContextFlags {
}
/// A snapshot of the [`Context`] at a given point in the AST traversal.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Snapshot {
scope_id: ScopeId,
stmt_id: Option<NodeId>,
definition_id: DefinitionId,
flags: ContextFlags,
}
#[derive(Debug)]
pub enum ResolvedReference {
/// The reference is resolved to a specific binding.
Resolved(BindingId),
/// The reference is resolved to a context-specific, implicit global (e.g., `__class__` within
/// a class scope).
ImplicitGlobal,
/// The reference is unresolved, but at least one of the containing scopes contains a star
/// import.
StarImport,
/// The reference is definitively unresolved.
NotFound,
}

View File

@@ -96,8 +96,10 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
allowed_confusables: Some(Vec::default()),
builtins: Some(Vec::default()),
dummy_variable_rgx: Some(defaults::DUMMY_VARIABLE_RGX.as_str().to_string()),
extend_fixable: Some(Vec::default()),
extend_ignore: Some(Vec::default()),
extend_select: Some(Vec::default()),
extend_unfixable: Some(Vec::default()),
external: Some(Vec::default()),
ignore: Some(Vec::default()),
line_length: Some(defaults::LINE_LENGTH),
@@ -109,6 +111,7 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
extend: None,
extend_exclude: None,
extend_include: None,
extend_per_file_ignores: None,
fix: None,
fix_only: None,
fixable: None,

View File

@@ -232,13 +232,17 @@ Rule selection:
--ignore <RULE_CODE>
Comma-separated list of rule codes to disable
--extend-select <RULE_CODE>
Like --select, but adds additional rule codes on top of the selected ones
Like --select, but adds additional rule codes on top of those already specified
--per-file-ignores <PER_FILE_IGNORES>
List of mappings from file pattern to code to exclude
--extend-per-file-ignores <EXTEND_PER_FILE_IGNORES>
Like `--per-file-ignores`, but adds additional ignores on top of those already specified
--fixable <RULE_CODE>
List of rule codes to treat as eligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
--unfixable <RULE_CODE>
List of rule codes to treat as ineligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
--extend-fixable <RULE_CODE>
Like --fixable, but adds additional rule codes on top of those already specified
File selection:
--exclude <FILE_PATTERN> List of paths, used to omit files and/or directories from analysis

View File

@@ -371,5 +371,5 @@ Ruff's color output is powered by the [`colored`](https://crates.io/crates/color
attempts to automatically detect whether the output stream supports color. However, you can force
colors off by setting the `NO_COLOR` environment variable to any value (e.g., `NO_COLOR=1`).
[`colored`](https://crates.io/crates/colored) also supports the the `CLICOLOR` and `CLICOLOR_FORCE`
[`colored`](https://crates.io/crates/colored) also supports the `CLICOLOR` and `CLICOLOR_FORCE`
environment variables (see the [spec](https://bixense.com/clicolors/)).

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