Compare commits
36 Commits
v0.0.264
...
charlie/bi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7517d4511 | ||
|
|
da8481c97d | ||
|
|
cd5c69b61c | ||
|
|
3beff29026 | ||
|
|
5ac2c7d293 | ||
|
|
e66fdb83d0 | ||
|
|
a95bafefb0 | ||
|
|
539af34f58 | ||
|
|
983bb31577 | ||
|
|
b98b604071 | ||
|
|
cd27b39aff | ||
|
|
a9fc648faf | ||
|
|
c1f0661225 | ||
|
|
2c91412321 | ||
|
|
11e1380df4 | ||
|
|
e93f378635 | ||
|
|
2124feb0e7 | ||
|
|
c0e7269b07 | ||
|
|
c2921e957b | ||
|
|
93cfce674a | ||
|
|
b71cc3789f | ||
|
|
717128112d | ||
|
|
e9e194ab32 | ||
|
|
890e630c41 | ||
|
|
d78287540d | ||
|
|
494e807315 | ||
|
|
6db1a32eb9 | ||
|
|
bb2cbf1f25 | ||
|
|
badfdab61a | ||
|
|
59d40f9f81 | ||
|
|
37aae666c7 | ||
|
|
460023a959 | ||
|
|
d0e3ca29d9 | ||
|
|
ccfc78e2d5 | ||
|
|
b14358fbfe | ||
|
|
ac600bb3da |
@@ -1,5 +1,16 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.0.265
|
||||
|
||||
### `--fix-only` now exits with a zero exit code, unless `--exit-non-zero-on-fix` is specified ([#4146](https://github.com/charliermarsh/ruff/pull/4146))
|
||||
|
||||
Previously, `--fix-only` would exit with a non-zero exit code if any fixes were applied. This
|
||||
behavior was inconsistent with `--fix`, and further, meant that `--exit-non-zero-on-fix` was
|
||||
effectively ignored when `--fix-only` was specified.
|
||||
|
||||
Now, `--fix-only` will exit with a zero exit code, unless `--exit-non-zero-on-fix` is specified,
|
||||
in which case it will exit with a non-zero exit code if any fixes were applied.
|
||||
|
||||
## 0.0.260
|
||||
|
||||
### Fixes are now represented as a list of edits ([#3709](https://github.com/charliermarsh/ruff/pull/3709))
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -841,7 +841,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.264"
|
||||
version = "0.0.265"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.2.4",
|
||||
@@ -2004,7 +2004,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.264"
|
||||
version = "0.0.265"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -2093,7 +2093,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.264"
|
||||
version = "0.0.265"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
|
||||
@@ -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.264'
|
||||
rev: 'v0.0.265'
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -183,6 +183,7 @@ exclude = [
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.264"
|
||||
version = "0.0.265"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.264"
|
||||
version = "0.0.265"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
@@ -14,3 +14,8 @@ def foo(x, y, z):
|
||||
return False
|
||||
|
||||
#import os # noqa: ERA001
|
||||
|
||||
|
||||
class A():
|
||||
pass
|
||||
# b = c
|
||||
|
||||
39
crates/ruff/resources/test/fixtures/flake8_bugbear/B027_extended.py
vendored
Normal file
39
crates/ruff/resources/test/fixtures/flake8_bugbear/B027_extended.py
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Should emit:
|
||||
B027 - on lines 13, 16, 19, 23
|
||||
"""
|
||||
from abc import ABC
|
||||
|
||||
|
||||
class AbstractClass(ABC):
|
||||
def empty_1(self): # error
|
||||
...
|
||||
|
||||
def empty_2(self): # error
|
||||
pass
|
||||
|
||||
def body_1(self):
|
||||
print("foo")
|
||||
...
|
||||
|
||||
def body_2(self):
|
||||
self.body_1()
|
||||
|
||||
|
||||
def foo():
|
||||
class InnerAbstractClass(ABC):
|
||||
def empty_1(self): # error
|
||||
...
|
||||
|
||||
def empty_2(self): # error
|
||||
pass
|
||||
|
||||
def body_1(self):
|
||||
print("foo")
|
||||
...
|
||||
|
||||
def body_2(self):
|
||||
self.body_1()
|
||||
|
||||
return InnerAbstractClass
|
||||
|
||||
@@ -7,3 +7,12 @@ foo.info("Hello {}".format("World!"))
|
||||
logging.log(logging.INFO, msg="Hello {}".format("World!"))
|
||||
logging.log(level=logging.INFO, msg="Hello {}".format("World!"))
|
||||
logging.log(msg="Hello {}".format("World!"), level=logging.INFO)
|
||||
|
||||
# Flask support
|
||||
import flask
|
||||
from flask import current_app
|
||||
from flask import current_app as app
|
||||
|
||||
flask.current_app.logger.info("Hello {}".format("World!"))
|
||||
current_app.logger.info("Hello {}".format("World!"))
|
||||
app.logger.log(logging.INFO, "Hello {}".format("World!"))
|
||||
|
||||
28
crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.py
vendored
Normal file
28
crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.py
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import sys
|
||||
import typing
|
||||
from typing import Annotated, Literal, TypeAlias, TypeVar
|
||||
|
||||
import typing_extensions
|
||||
|
||||
def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
|
||||
def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
|
||||
|
||||
def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
|
||||
Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
|
||||
"""Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
|
||||
|
||||
if sys.platform == "linux":
|
||||
f: "int" # Y020 Quoted annotations should never be used in stubs
|
||||
elif sys.platform == "win32":
|
||||
f: "str" # Y020 Quoted annotations should never be used in stubs
|
||||
else:
|
||||
f: "bytes" # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
# These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations"
|
||||
k = "" # Y052 Need type annotation for "k"
|
||||
el = r"" # Y052 Need type annotation for "el"
|
||||
28
crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.pyi
vendored
Normal file
28
crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.pyi
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import sys
|
||||
import typing
|
||||
from typing import Annotated, Literal, TypeAlias, TypeVar
|
||||
|
||||
import typing_extensions
|
||||
|
||||
def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
|
||||
def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
|
||||
|
||||
def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
|
||||
Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
|
||||
"""Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
|
||||
|
||||
if sys.platform == "linux":
|
||||
f: "int" # Y020 Quoted annotations should never be used in stubs
|
||||
elif sys.platform == "win32":
|
||||
f: "str" # Y020 Quoted annotations should never be used in stubs
|
||||
else:
|
||||
f: "bytes" # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
# These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations"
|
||||
k = "" # Y052 Need type annotation for "k"
|
||||
el = r"" # Y052 Need type annotation for "el"
|
||||
24
crates/ruff/resources/test/fixtures/flake8_pyi/PYI042.py
vendored
Normal file
24
crates/ruff/resources/test/fixtures/flake8_pyi/PYI042.py
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
just_literals_pipe_union: TypeAlias = (
|
||||
Literal[True] | Literal["idk"]
|
||||
) # not PYI042 (not a stubfile)
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
snake_case_alias1: TypeAlias = str | int # not PYI042 (not a stubfile)
|
||||
_snake_case_alias2: TypeAlias = Literal["whatever"] # not PYI042 (not a stubfile)
|
||||
Snake_case_alias: TypeAlias = int | float # not PYI042 (not a stubfile)
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
24
crates/ruff/resources/test/fixtures/flake8_pyi/PYI042.pyi
vendored
Normal file
24
crates/ruff/resources/test/fixtures/flake8_pyi/PYI042.pyi
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
just_literals_pipe_union: TypeAlias = (
|
||||
Literal[True] | Literal["idk"]
|
||||
) # PYI042, since not camel case
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
snake_case_alias1: TypeAlias = str | int # PYI042, since not camel case
|
||||
_snake_case_alias2: TypeAlias = Literal["whatever"] # PYI042, since not camel case
|
||||
Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
23
crates/ruff/resources/test/fixtures/flake8_pyi/PYI043.py
vendored
Normal file
23
crates/ruff/resources/test/fixtures/flake8_pyi/PYI043.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
_PrivateAliasT: TypeAlias = str | int # not PYI043 (not a stubfile)
|
||||
_PrivateAliasT2: TypeAlias = typing.Any # not PYI043 (not a stubfile)
|
||||
_PrivateAliasT3: TypeAlias = Literal[
|
||||
"not", "a", "chance"
|
||||
] # not PYI043 (not a stubfile)
|
||||
just_literals_pipe_union: TypeAlias = Literal[True] | Literal["idk"]
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
23
crates/ruff/resources/test/fixtures/flake8_pyi/PYI043.pyi
vendored
Normal file
23
crates/ruff/resources/test/fixtures/flake8_pyi/PYI043.pyi
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
_PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
|
||||
_PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
|
||||
_PrivateAliasT3: TypeAlias = Literal[
|
||||
"not", "a", "chance"
|
||||
] # PYI043, since this ends in a T
|
||||
just_literals_pipe_union: TypeAlias = Literal[True] | Literal["idk"]
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
7
crates/ruff/resources/test/fixtures/flake8_simplify/SIM105_1.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/flake8_simplify/SIM105_1.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Case: There's a random import, so it should add `contextlib` after it."""
|
||||
import math
|
||||
|
||||
try:
|
||||
math.sqrt(-1)
|
||||
except ValueError: # SIM105
|
||||
pass
|
||||
12
crates/ruff/resources/test/fixtures/flake8_simplify/SIM105_2.py
vendored
Normal file
12
crates/ruff/resources/test/fixtures/flake8_simplify/SIM105_2.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Case: `contextlib` already imported."""
|
||||
import contextlib
|
||||
|
||||
|
||||
def foo():
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
foo()
|
||||
except ValueError:
|
||||
pass
|
||||
21
crates/ruff/resources/test/fixtures/pydocstyle/D214_module.py
vendored
Normal file
21
crates/ruff/resources/test/fixtures/pydocstyle/D214_module.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
"""A module docstring with D214 violations
|
||||
|
||||
Returns
|
||||
-----
|
||||
valid returns
|
||||
|
||||
Args
|
||||
-----
|
||||
valid args
|
||||
"""
|
||||
|
||||
import os
|
||||
from .expected import Expectation
|
||||
|
||||
expectation = Expectation()
|
||||
expect = expectation.expect
|
||||
|
||||
expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]),
|
||||
"D214: Section is over-indented ('Returns')")
|
||||
expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]),
|
||||
"D214: Section is over-indented ('Args')")
|
||||
0
crates/ruff/resources/test/fixtures/pylint/import_self/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pylint/import_self/__init__.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/pylint/import_self/module.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/pylint/import_self/module.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import import_self.module
|
||||
from import_self import module
|
||||
from . import module
|
||||
21
crates/ruff/resources/test/fixtures/pylint/nested_min_max.py
vendored
Normal file
21
crates/ruff/resources/test/fixtures/pylint/nested_min_max.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
min(1, 2, 3)
|
||||
min(1, min(2, 3))
|
||||
min(1, min(2, min(3, 4)))
|
||||
min(1, foo("a", "b"), min(3, 4))
|
||||
min(1, max(2, 3))
|
||||
max(1, 2, 3)
|
||||
max(1, max(2, 3))
|
||||
max(1, max(2, max(3, 4)))
|
||||
max(1, foo("a", "b"), max(3, 4))
|
||||
|
||||
# These should not trigger; we do not flag cases with keyword args.
|
||||
min(1, min(2, 3), key=test)
|
||||
min(1, min(2, 3, key=test))
|
||||
# This will still trigger, to merge the calls without keyword args.
|
||||
min(1, min(2, 3, key=test), min(4, 5))
|
||||
|
||||
# Don't provide a fix if there are comments within the call.
|
||||
min(
|
||||
1, # This is a comment.
|
||||
min(2, 3),
|
||||
)
|
||||
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_7.py
vendored
Normal file
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_7.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
def main():
|
||||
exit(0)
|
||||
|
||||
|
||||
import functools
|
||||
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_8.py
vendored
Normal file
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_8.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
from sys import argv
|
||||
|
||||
|
||||
def main():
|
||||
exit(0)
|
||||
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_9.py
vendored
Normal file
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_9.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
def main():
|
||||
exit(0)
|
||||
|
||||
|
||||
from sys import argv
|
||||
@@ -435,6 +435,7 @@ pub fn remove_argument(
|
||||
pub fn get_or_import_symbol(
|
||||
module: &str,
|
||||
member: &str,
|
||||
at: TextSize,
|
||||
context: &Context,
|
||||
importer: &Importer,
|
||||
locator: &Locator,
|
||||
@@ -462,7 +463,7 @@ pub fn get_or_import_symbol(
|
||||
Edit::range_replacement(locator.slice(source.range()).to_string(), source.range());
|
||||
Ok((import_edit, binding))
|
||||
} else {
|
||||
if let Some(stmt) = importer.get_import_from(module) {
|
||||
if let Some(stmt) = importer.find_import_from(module, at) {
|
||||
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
|
||||
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
|
||||
// bound name.
|
||||
@@ -485,7 +486,8 @@ pub fn get_or_import_symbol(
|
||||
.find_binding(module)
|
||||
.map_or(true, |binding| binding.kind.is_builtin())
|
||||
{
|
||||
let import_edit = importer.add_import(&AnyImport::Import(Import::module(module)));
|
||||
let import_edit =
|
||||
importer.add_import(&AnyImport::Import(Import::module(module)), at);
|
||||
Ok((import_edit, format!("{module}.{member}")))
|
||||
} else {
|
||||
bail!(
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{Expr, Stmt};
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_python_ast::types::RefEquality;
|
||||
use ruff_python_semantic::analyze::visibility::{Visibility, VisibleScope};
|
||||
use ruff_python_semantic::node::NodeId;
|
||||
use ruff_python_semantic::scope::ScopeId;
|
||||
|
||||
use crate::checkers::ast::AnnotationContext;
|
||||
use crate::docstrings::definition::Definition;
|
||||
|
||||
type Context<'a> = (ScopeId, Vec<RefEquality<'a, Stmt>>);
|
||||
/// A snapshot of the current scope and statement, which will be restored when visiting any
|
||||
/// deferred definitions.
|
||||
type Context<'a> = (ScopeId, Option<NodeId>);
|
||||
|
||||
/// A collection of AST nodes that are deferred for later analysis.
|
||||
/// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all
|
||||
@@ -18,8 +20,8 @@ pub struct Deferred<'a> {
|
||||
pub definitions: Vec<(Definition<'a>, Visibility, Context<'a>)>,
|
||||
pub string_type_definitions: Vec<(TextRange, &'a str, AnnotationContext, Context<'a>)>,
|
||||
pub type_definitions: Vec<(&'a Expr, AnnotationContext, Context<'a>)>,
|
||||
pub functions: Vec<(&'a Stmt, Context<'a>, VisibleScope)>,
|
||||
pub functions: Vec<(Context<'a>, VisibleScope)>,
|
||||
pub lambdas: Vec<(&'a Expr, Context<'a>)>,
|
||||
pub for_loops: Vec<(&'a Stmt, Context<'a>)>,
|
||||
pub for_loops: Vec<Context<'a>>,
|
||||
pub assignments: Vec<Context<'a>>,
|
||||
}
|
||||
|
||||
@@ -17,14 +17,16 @@ use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
use ruff_python_ast::types::{Node, RefEquality};
|
||||
use ruff_python_ast::typing::parse_type_annotation;
|
||||
use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor};
|
||||
use ruff_python_ast::{branch_detection, cast, helpers, str, visitor};
|
||||
use ruff_python_ast::{cast, helpers, str, visitor};
|
||||
use ruff_python_semantic::analyze;
|
||||
use ruff_python_semantic::analyze::branch_detection;
|
||||
use ruff_python_semantic::analyze::typing::{Callable, SubscriptKind};
|
||||
use ruff_python_semantic::binding::{
|
||||
Binding, BindingId, BindingKind, Exceptions, ExecutionContext, Export, FromImportation,
|
||||
Importation, StarImportation, SubmoduleImportation,
|
||||
};
|
||||
use ruff_python_semantic::context::Context;
|
||||
use ruff_python_semantic::node::NodeId;
|
||||
use ruff_python_semantic::scope::{ClassDef, FunctionDef, Lambda, Scope, ScopeId, ScopeKind};
|
||||
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||
use ruff_python_stdlib::path::is_python_stub_file;
|
||||
@@ -175,7 +177,7 @@ where
|
||||
'b: 'a,
|
||||
{
|
||||
fn visit_stmt(&mut self, stmt: &'b Stmt) {
|
||||
self.ctx.push_parent(stmt);
|
||||
self.ctx.push_stmt(stmt);
|
||||
|
||||
// Track whether we've seen docstrings, non-imports, etc.
|
||||
match &stmt.node {
|
||||
@@ -196,7 +198,7 @@ where
|
||||
self.ctx.futures_allowed = false;
|
||||
if !self.ctx.seen_import_boundary
|
||||
&& !helpers::is_assignment_to_a_dunder(stmt)
|
||||
&& !helpers::in_nested_block(self.ctx.parents.iter().rev().map(Into::into))
|
||||
&& !helpers::in_nested_block(self.ctx.parents())
|
||||
{
|
||||
self.ctx.seen_import_boundary = true;
|
||||
}
|
||||
@@ -208,7 +210,7 @@ where
|
||||
&stmt.node,
|
||||
StmtKind::Import { .. } | StmtKind::ImportFrom { .. }
|
||||
) {
|
||||
if self.ctx.scope_id.is_global() && self.ctx.current_stmt_parent().is_none() {
|
||||
if self.ctx.at_top_level() {
|
||||
self.importer.visit_import(stmt);
|
||||
}
|
||||
}
|
||||
@@ -230,7 +232,7 @@ where
|
||||
synthetic_usage: usage,
|
||||
typing_usage: None,
|
||||
range: *range,
|
||||
source: Some(RefEquality(stmt)),
|
||||
source: self.ctx.stmt_id,
|
||||
context,
|
||||
exceptions,
|
||||
});
|
||||
@@ -260,7 +262,7 @@ where
|
||||
synthetic_usage: usage,
|
||||
typing_usage: None,
|
||||
range: *range,
|
||||
source: Some(RefEquality(stmt)),
|
||||
source: self.ctx.stmt_id,
|
||||
context,
|
||||
exceptions,
|
||||
});
|
||||
@@ -303,10 +305,9 @@ where
|
||||
}
|
||||
StmtKind::Break => {
|
||||
if self.settings.rules.enabled(Rule::BreakOutsideLoop) {
|
||||
if let Some(diagnostic) = pyflakes::rules::break_outside_loop(
|
||||
stmt,
|
||||
&mut self.ctx.parents.iter().rev().map(Into::into).skip(1),
|
||||
) {
|
||||
if let Some(diagnostic) =
|
||||
pyflakes::rules::break_outside_loop(stmt, &mut self.ctx.parents().skip(1))
|
||||
{
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -315,7 +316,7 @@ where
|
||||
if self.settings.rules.enabled(Rule::ContinueOutsideLoop) {
|
||||
if let Some(diagnostic) = pyflakes::rules::continue_outside_loop(
|
||||
stmt,
|
||||
&mut self.ctx.parents.iter().rev().map(Into::into).skip(1),
|
||||
&mut self.ctx.parents().skip(1),
|
||||
) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -688,7 +689,7 @@ where
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: stmt.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -904,7 +905,7 @@ where
|
||||
synthetic_usage: Some((self.ctx.scope_id, alias.range())),
|
||||
typing_usage: None,
|
||||
range: alias.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -926,15 +927,14 @@ where
|
||||
self.add_binding(
|
||||
name,
|
||||
Binding {
|
||||
kind: BindingKind::SubmoduleImportation(SubmoduleImportation {
|
||||
name,
|
||||
full_name,
|
||||
}),
|
||||
kind: BindingKind::SubmoduleImportation(Box::new(
|
||||
SubmoduleImportation { name, full_name },
|
||||
)),
|
||||
runtime_usage: None,
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: alias.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -953,7 +953,10 @@ where
|
||||
self.add_binding(
|
||||
name,
|
||||
Binding {
|
||||
kind: BindingKind::Importation(Importation { name, full_name }),
|
||||
kind: BindingKind::Importation(Box::new(Importation {
|
||||
name,
|
||||
full_name,
|
||||
})),
|
||||
runtime_usage: None,
|
||||
synthetic_usage: if is_explicit_reexport {
|
||||
Some((self.ctx.scope_id, alias.range()))
|
||||
@@ -962,7 +965,7 @@ where
|
||||
},
|
||||
typing_usage: None,
|
||||
range: alias.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -1000,6 +1003,13 @@ where
|
||||
if self.settings.rules.enabled(Rule::ManualFromImport) {
|
||||
pylint::rules::manual_from_import(self, stmt, alias, names);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::ImportSelf) {
|
||||
if let Some(diagnostic) =
|
||||
pylint::rules::import_self(alias, self.module_path.as_deref())
|
||||
{
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(asname) = &alias.node.asname {
|
||||
let name = alias.node.name.split('.').last().unwrap();
|
||||
@@ -1215,7 +1225,7 @@ where
|
||||
synthetic_usage: Some((self.ctx.scope_id, alias.range())),
|
||||
typing_usage: None,
|
||||
range: alias.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -1299,10 +1309,10 @@ where
|
||||
self.add_binding(
|
||||
name,
|
||||
Binding {
|
||||
kind: BindingKind::FromImportation(FromImportation {
|
||||
kind: BindingKind::FromImportation(Box::new(FromImportation {
|
||||
name,
|
||||
full_name,
|
||||
}),
|
||||
})),
|
||||
runtime_usage: None,
|
||||
synthetic_usage: if is_explicit_reexport {
|
||||
Some((self.ctx.scope_id, alias.range()))
|
||||
@@ -1311,7 +1321,7 @@ where
|
||||
},
|
||||
typing_usage: None,
|
||||
range: alias.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -1477,6 +1487,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::ImportSelf) {
|
||||
if let Some(diagnostic) = pylint::rules::import_from_self(
|
||||
*level,
|
||||
module.as_deref(),
|
||||
names,
|
||||
self.module_path.as_deref(),
|
||||
) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::BannedImportFrom) {
|
||||
if let Some(diagnostic) = flake8_import_conventions::rules::banned_import_from(
|
||||
stmt,
|
||||
@@ -1552,14 +1573,14 @@ where
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
self.ctx.current_stmt_parent().map(Into::into),
|
||||
self.ctx.current_stmt_parent(),
|
||||
);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::IfWithSameArms) {
|
||||
flake8_simplify::rules::if_with_same_arms(
|
||||
self,
|
||||
stmt,
|
||||
self.ctx.current_stmt_parent().map(Into::into),
|
||||
self.ctx.current_stmt_parent(),
|
||||
);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::NeedlessBool) {
|
||||
@@ -1576,14 +1597,14 @@ where
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
self.ctx.current_stmt_parent().map(Into::into),
|
||||
self.ctx.current_stmt_parent(),
|
||||
);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::IfElseBlockInsteadOfIfExp) {
|
||||
flake8_simplify::rules::use_ternary_operator(
|
||||
self,
|
||||
stmt,
|
||||
self.ctx.current_stmt_parent().map(Into::into),
|
||||
self.ctx.current_stmt_parent(),
|
||||
);
|
||||
}
|
||||
if self
|
||||
@@ -1597,7 +1618,7 @@ where
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
self.ctx.current_stmt_parent().map(Into::into),
|
||||
self.ctx.current_stmt_parent(),
|
||||
);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::TypeCheckWithoutTypeError) {
|
||||
@@ -1606,7 +1627,7 @@ where
|
||||
body,
|
||||
test,
|
||||
orelse,
|
||||
self.ctx.current_stmt_parent().map(Into::into),
|
||||
self.ctx.current_stmt_parent(),
|
||||
);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::OutdatedVersionBlock) {
|
||||
@@ -1664,7 +1685,7 @@ where
|
||||
self,
|
||||
stmt,
|
||||
body,
|
||||
self.ctx.current_stmt_parent().map(Into::into),
|
||||
self.ctx.current_stmt_parent(),
|
||||
);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::RedefinedLoopName) {
|
||||
@@ -1696,7 +1717,7 @@ where
|
||||
if self.settings.rules.enabled(Rule::UnusedLoopControlVariable) {
|
||||
self.deferred
|
||||
.for_loops
|
||||
.push((stmt, (self.ctx.scope_id, self.ctx.parents.clone())));
|
||||
.push((self.ctx.scope_id, self.ctx.stmt_id));
|
||||
}
|
||||
if self
|
||||
.settings
|
||||
@@ -1914,6 +1935,14 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.ctx.match_typing_expr(annotation, "TypeAlias") {
|
||||
if self.settings.rules.enabled(Rule::SnakeCaseTypeAlias) {
|
||||
flake8_pyi::rules::snake_case_type_alias(self, target);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::TSuffixedTypeAlias) {
|
||||
flake8_pyi::rules::t_suffixed_type_alias(self, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::Delete { targets } => {
|
||||
@@ -1977,7 +2006,7 @@ where
|
||||
self.deferred.definitions.push((
|
||||
definition,
|
||||
scope.visibility,
|
||||
(self.ctx.scope_id, self.ctx.parents.clone()),
|
||||
(self.ctx.scope_id, self.ctx.stmt_id),
|
||||
));
|
||||
self.ctx.visible_scope = scope;
|
||||
|
||||
@@ -1996,7 +2025,7 @@ where
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: stmt.range(),
|
||||
source: Some(RefEquality(stmt)),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
});
|
||||
@@ -2014,8 +2043,7 @@ where
|
||||
}));
|
||||
|
||||
self.deferred.functions.push((
|
||||
stmt,
|
||||
(self.ctx.scope_id, self.ctx.parents.clone()),
|
||||
(self.ctx.scope_id, self.ctx.stmt_id),
|
||||
self.ctx.visible_scope,
|
||||
));
|
||||
}
|
||||
@@ -2040,7 +2068,7 @@ where
|
||||
self.deferred.definitions.push((
|
||||
definition,
|
||||
scope.visibility,
|
||||
(self.ctx.scope_id, self.ctx.parents.clone()),
|
||||
(self.ctx.scope_id, self.ctx.stmt_id),
|
||||
));
|
||||
self.ctx.visible_scope = scope;
|
||||
|
||||
@@ -2059,7 +2087,7 @@ where
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: stmt.range(),
|
||||
source: Some(RefEquality(stmt)),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
});
|
||||
@@ -2220,7 +2248,7 @@ where
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: stmt.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -2229,7 +2257,7 @@ where
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.ctx.pop_parent();
|
||||
self.ctx.pop_stmt();
|
||||
}
|
||||
|
||||
fn visit_annotation(&mut self, expr: &'b Expr) {
|
||||
@@ -2255,13 +2283,13 @@ where
|
||||
expr.range(),
|
||||
value,
|
||||
(self.ctx.in_annotation, self.ctx.in_type_checking_block),
|
||||
(self.ctx.scope_id, self.ctx.parents.clone()),
|
||||
(self.ctx.scope_id, self.ctx.stmt_id),
|
||||
));
|
||||
} else {
|
||||
self.deferred.type_definitions.push((
|
||||
expr,
|
||||
(self.ctx.in_annotation, self.ctx.in_type_checking_block),
|
||||
(self.ctx.scope_id, self.ctx.parents.clone()),
|
||||
(self.ctx.scope_id, self.ctx.stmt_id),
|
||||
));
|
||||
}
|
||||
return;
|
||||
@@ -2281,8 +2309,7 @@ where
|
||||
match &expr.node {
|
||||
ExprKind::Subscript { value, slice, .. } => {
|
||||
// Ex) Optional[...], Union[...]
|
||||
if self.ctx.in_type_definition
|
||||
&& !self.settings.pyupgrade.keep_runtime_typing
|
||||
if !self.settings.pyupgrade.keep_runtime_typing
|
||||
&& self.settings.rules.enabled(Rule::NonPEP604Annotation)
|
||||
&& (self.settings.target_version >= PythonVersion::Py310
|
||||
|| (self.settings.target_version >= PythonVersion::Py37
|
||||
@@ -2709,7 +2736,7 @@ where
|
||||
flake8_comprehensions::rules::unnecessary_generator_set(
|
||||
self,
|
||||
expr,
|
||||
self.ctx.current_expr_parent().map(Into::into),
|
||||
self.ctx.current_expr_parent(),
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
@@ -2719,7 +2746,7 @@ where
|
||||
flake8_comprehensions::rules::unnecessary_generator_dict(
|
||||
self,
|
||||
expr,
|
||||
self.ctx.current_expr_parent().map(Into::into),
|
||||
self.ctx.current_expr_parent(),
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
@@ -2824,7 +2851,7 @@ where
|
||||
flake8_comprehensions::rules::unnecessary_map(
|
||||
self,
|
||||
expr,
|
||||
self.ctx.current_expr_parent().map(Into::into),
|
||||
self.ctx.current_expr_parent(),
|
||||
func,
|
||||
args,
|
||||
);
|
||||
@@ -2975,6 +3002,9 @@ where
|
||||
if self.settings.rules.enabled(Rule::InvalidEnvvarValue) {
|
||||
pylint::rules::invalid_envvar_value(self, func, args, keywords);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::NestedMinMax) {
|
||||
pylint::rules::nested_min_max(self, expr, func, args, keywords);
|
||||
}
|
||||
|
||||
// flake8-pytest-style
|
||||
if self.settings.rules.enabled(Rule::PytestPatchWithLambda) {
|
||||
@@ -3489,7 +3519,7 @@ where
|
||||
expr.range(),
|
||||
value,
|
||||
(self.ctx.in_annotation, self.ctx.in_type_checking_block),
|
||||
(self.ctx.scope_id, self.ctx.parents.clone()),
|
||||
(self.ctx.scope_id, self.ctx.stmt_id),
|
||||
));
|
||||
}
|
||||
if self
|
||||
@@ -3612,7 +3642,7 @@ where
|
||||
ExprKind::Lambda { .. } => {
|
||||
self.deferred
|
||||
.lambdas
|
||||
.push((expr, (self.ctx.scope_id, self.ctx.parents.clone())));
|
||||
.push((expr, (self.ctx.scope_id, self.ctx.stmt_id)));
|
||||
}
|
||||
ExprKind::IfExp { test, body, orelse } => {
|
||||
visit_boolean_test!(self, test);
|
||||
@@ -4096,7 +4126,7 @@ where
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: arg.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -4140,7 +4170,7 @@ where
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: pattern.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -4187,12 +4217,7 @@ impl<'a> Checker<'a> {
|
||||
if !existing.kind.is_builtin()
|
||||
&& existing.source.map_or(true, |left| {
|
||||
binding.source.map_or(true, |right| {
|
||||
!branch_detection::different_forks(
|
||||
left,
|
||||
right,
|
||||
&self.ctx.depths,
|
||||
&self.ctx.child_to_parent,
|
||||
)
|
||||
!branch_detection::different_forks(left, right, &self.ctx.stmts)
|
||||
})
|
||||
})
|
||||
{
|
||||
@@ -4223,7 +4248,7 @@ impl<'a> Checker<'a> {
|
||||
&& !(existing.kind.is_function_definition()
|
||||
&& analyze::visibility::is_overload(
|
||||
&self.ctx,
|
||||
cast::decorator_list(existing.source.as_ref().unwrap()),
|
||||
cast::decorator_list(self.ctx.stmts[existing.source.unwrap()]),
|
||||
))
|
||||
{
|
||||
if self.settings.rules.enabled(Rule::RedefinedWhileUnused) {
|
||||
@@ -4240,13 +4265,17 @@ impl<'a> Checker<'a> {
|
||||
BindingKind::ClassDefinition | BindingKind::FunctionDefinition
|
||||
)
|
||||
.then(|| {
|
||||
binding.source.as_ref().map_or(binding.range, |source| {
|
||||
helpers::identifier_range(source, self.locator)
|
||||
binding.source.map_or(binding.range, |source| {
|
||||
helpers::identifier_range(
|
||||
self.ctx.stmts[source],
|
||||
self.locator,
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap_or(binding.range),
|
||||
);
|
||||
if let Some(parent) = binding.source.as_ref() {
|
||||
if let Some(parent) = binding.source {
|
||||
let parent = self.ctx.stmts[parent];
|
||||
if matches!(parent.node, StmtKind::ImportFrom { .. })
|
||||
&& parent.range().contains_range(binding.range)
|
||||
{
|
||||
@@ -4385,13 +4414,13 @@ impl<'a> Checker<'a> {
|
||||
// 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 }) =>
|
||||
{
|
||||
BindingKind::Importation(import) => {
|
||||
let name = import.name;
|
||||
let full_name = import.full_name;
|
||||
let has_alias = full_name
|
||||
.split('.')
|
||||
.last()
|
||||
.map(|segment| &segment != name)
|
||||
.map(|segment| segment != name)
|
||||
.unwrap_or_default();
|
||||
if has_alias {
|
||||
// Mark the sub-importation as used.
|
||||
@@ -4404,11 +4433,32 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
BindingKind::FromImportation(FromImportation { name, full_name }) => {
|
||||
BindingKind::SubmoduleImportation(import) => {
|
||||
let name = import.name;
|
||||
let full_name = import.full_name;
|
||||
let has_alias = full_name
|
||||
.split('.')
|
||||
.last()
|
||||
.map(|segment| &segment != name)
|
||||
.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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
BindingKind::FromImportation(import) => {
|
||||
let name = import.name;
|
||||
let full_name = &import.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.
|
||||
@@ -4492,7 +4542,7 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
|
||||
fn handle_node_store(&mut self, id: &'a str, expr: &Expr) {
|
||||
let parent = self.ctx.current_stmt().0;
|
||||
let parent = self.ctx.current_stmt();
|
||||
|
||||
if self.settings.rules.enabled(Rule::UndefinedLocal) {
|
||||
pyflakes::rules::undefined_local(self, id);
|
||||
@@ -4551,7 +4601,7 @@ impl<'a> Checker<'a> {
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: expr.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -4571,7 +4621,7 @@ impl<'a> Checker<'a> {
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: expr.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -4588,7 +4638,7 @@ impl<'a> Checker<'a> {
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: expr.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -4638,10 +4688,8 @@ impl<'a> Checker<'a> {
|
||||
// Grab the existing bound __all__ values.
|
||||
if let StmtKind::AugAssign { .. } = &parent.node {
|
||||
if let Some(index) = scope.get("__all__") {
|
||||
if let BindingKind::Export(Export { names: existing }) =
|
||||
&self.ctx.bindings[*index].kind
|
||||
{
|
||||
names.extend_from_slice(existing);
|
||||
if let BindingKind::Export(export) = &self.ctx.bindings[*index].kind {
|
||||
names.extend(&export.names);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4666,12 +4714,12 @@ impl<'a> Checker<'a> {
|
||||
self.add_binding(
|
||||
id,
|
||||
Binding {
|
||||
kind: BindingKind::Export(Export { names: all_names }),
|
||||
kind: BindingKind::Export(Box::new(Export { names: all_names })),
|
||||
runtime_usage: None,
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: expr.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -4693,7 +4741,7 @@ impl<'a> Checker<'a> {
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: expr.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -4709,7 +4757,7 @@ impl<'a> Checker<'a> {
|
||||
synthetic_usage: None,
|
||||
typing_usage: None,
|
||||
range: expr.range(),
|
||||
source: Some(*self.ctx.current_stmt()),
|
||||
source: self.ctx.stmt_id,
|
||||
context: self.ctx.execution_context(),
|
||||
exceptions: self.ctx.exceptions(),
|
||||
},
|
||||
@@ -4720,7 +4768,7 @@ impl<'a> Checker<'a> {
|
||||
let ExprKind::Name { id, .. } = &expr.node else {
|
||||
return;
|
||||
};
|
||||
if helpers::on_conditional_branch(&mut self.ctx.parents.iter().rev().map(Into::into)) {
|
||||
if helpers::on_conditional_branch(&mut self.ctx.parents()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4755,7 +4803,7 @@ impl<'a> Checker<'a> {
|
||||
docstring,
|
||||
},
|
||||
self.ctx.visible_scope.visibility,
|
||||
(self.ctx.scope_id, self.ctx.parents.clone()),
|
||||
(self.ctx.scope_id, self.ctx.stmt_id),
|
||||
));
|
||||
docstring.is_some()
|
||||
}
|
||||
@@ -4763,11 +4811,11 @@ impl<'a> Checker<'a> {
|
||||
fn check_deferred_type_definitions(&mut self) {
|
||||
while !self.deferred.type_definitions.is_empty() {
|
||||
let type_definitions = std::mem::take(&mut self.deferred.type_definitions);
|
||||
for (expr, (in_annotation, in_type_checking_block), (scope_id, parents)) in
|
||||
for (expr, (in_annotation, in_type_checking_block), (scope_id, stmt_id)) in
|
||||
type_definitions
|
||||
{
|
||||
self.ctx.scope_id = scope_id;
|
||||
self.ctx.parents = parents;
|
||||
self.ctx.stmt_id = stmt_id;
|
||||
self.ctx.in_annotation = in_annotation;
|
||||
self.ctx.in_type_checking_block = in_type_checking_block;
|
||||
self.ctx.in_type_definition = true;
|
||||
@@ -4782,7 +4830,7 @@ impl<'a> Checker<'a> {
|
||||
fn check_deferred_string_type_definitions(&mut self, allocator: &'a typed_arena::Arena<Expr>) {
|
||||
while !self.deferred.string_type_definitions.is_empty() {
|
||||
let type_definitions = std::mem::take(&mut self.deferred.string_type_definitions);
|
||||
for (range, value, (in_annotation, in_type_checking_block), (scope_id, parents)) in
|
||||
for (range, value, (in_annotation, in_type_checking_block), (scope_id, stmt_id)) in
|
||||
type_definitions
|
||||
{
|
||||
if let Ok((expr, kind)) = parse_type_annotation(value, range, self.locator) {
|
||||
@@ -4791,11 +4839,16 @@ impl<'a> Checker<'a> {
|
||||
pyupgrade::rules::quoted_annotation(self, value, range);
|
||||
}
|
||||
}
|
||||
if self.is_stub {
|
||||
if self.settings.rules.enabled(Rule::QuotedAnnotationInStub) {
|
||||
flake8_pyi::rules::quoted_annotation_in_stub(self, value, range);
|
||||
}
|
||||
}
|
||||
|
||||
let expr = allocator.alloc(expr);
|
||||
|
||||
self.ctx.scope_id = scope_id;
|
||||
self.ctx.parents = parents;
|
||||
self.ctx.stmt_id = stmt_id;
|
||||
self.ctx.in_annotation = in_annotation;
|
||||
self.ctx.in_type_checking_block = in_type_checking_block;
|
||||
self.ctx.in_type_definition = true;
|
||||
@@ -4824,13 +4877,12 @@ impl<'a> Checker<'a> {
|
||||
fn check_deferred_functions(&mut self) {
|
||||
while !self.deferred.functions.is_empty() {
|
||||
let deferred_functions = std::mem::take(&mut self.deferred.functions);
|
||||
for (stmt, (scope_id, parents), visibility) in deferred_functions {
|
||||
let parents_snapshot = parents.len();
|
||||
for ((scope_id, stmt_id), visibility) in deferred_functions {
|
||||
self.ctx.scope_id = scope_id;
|
||||
self.ctx.parents = parents;
|
||||
self.ctx.stmt_id = stmt_id;
|
||||
self.ctx.visible_scope = visibility;
|
||||
|
||||
match &stmt.node {
|
||||
match &self.ctx.current_stmt().node {
|
||||
StmtKind::FunctionDef { body, args, .. }
|
||||
| StmtKind::AsyncFunctionDef { body, args, .. } => {
|
||||
self.visit_arguments(args);
|
||||
@@ -4841,10 +4893,7 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut parents = std::mem::take(&mut self.ctx.parents);
|
||||
parents.truncate(parents_snapshot);
|
||||
|
||||
self.deferred.assignments.push((scope_id, parents));
|
||||
self.deferred.assignments.push((scope_id, stmt_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4852,11 +4901,9 @@ impl<'a> Checker<'a> {
|
||||
fn check_deferred_lambdas(&mut self) {
|
||||
while !self.deferred.lambdas.is_empty() {
|
||||
let lambdas = std::mem::take(&mut self.deferred.lambdas);
|
||||
for (expr, (scope_id, parents)) in lambdas {
|
||||
let parents_snapshot = parents.len();
|
||||
|
||||
for (expr, (scope_id, stmt_id)) in lambdas {
|
||||
self.ctx.scope_id = scope_id;
|
||||
self.ctx.parents = parents;
|
||||
self.ctx.stmt_id = stmt_id;
|
||||
|
||||
if let ExprKind::Lambda { args, body } = &expr.node {
|
||||
self.visit_arguments(args);
|
||||
@@ -4865,9 +4912,7 @@ impl<'a> Checker<'a> {
|
||||
unreachable!("Expected ExprKind::Lambda");
|
||||
}
|
||||
|
||||
let mut parents = std::mem::take(&mut self.ctx.parents);
|
||||
parents.truncate(parents_snapshot);
|
||||
self.deferred.assignments.push((scope_id, parents));
|
||||
self.deferred.assignments.push((scope_id, stmt_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4912,17 +4957,15 @@ impl<'a> Checker<'a> {
|
||||
while !self.deferred.for_loops.is_empty() {
|
||||
let for_loops = std::mem::take(&mut self.deferred.for_loops);
|
||||
|
||||
for (stmt, (scope_id, parents)) in for_loops {
|
||||
for (scope_id, stmt_id) in for_loops {
|
||||
self.ctx.scope_id = scope_id;
|
||||
self.ctx.parents = parents;
|
||||
self.ctx.stmt_id = stmt_id;
|
||||
|
||||
if let StmtKind::For { target, body, .. }
|
||||
| StmtKind::AsyncFor { target, body, .. } = &stmt.node
|
||||
| StmtKind::AsyncFor { target, body, .. } = &self.ctx.current_stmt().node
|
||||
{
|
||||
if self.settings.rules.enabled(Rule::UnusedLoopControlVariable) {
|
||||
flake8_bugbear::rules::unused_loop_control_variable(
|
||||
self, stmt, target, body,
|
||||
);
|
||||
flake8_bugbear::rules::unused_loop_control_variable(self, target, body);
|
||||
}
|
||||
} else {
|
||||
unreachable!("Expected ExprKind::For | ExprKind::AsyncFor");
|
||||
@@ -4959,7 +5002,7 @@ impl<'a> Checker<'a> {
|
||||
.get("__all__")
|
||||
.map(|index| &self.ctx.bindings[*index])
|
||||
.and_then(|binding| match &binding.kind {
|
||||
BindingKind::Export(Export { names }) => Some((names, binding.range)),
|
||||
BindingKind::Export(export) => Some((&export.names, binding.range)),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
@@ -4991,7 +5034,7 @@ impl<'a> Checker<'a> {
|
||||
.get("__all__")
|
||||
.map(|index| &self.ctx.bindings[*index])
|
||||
.and_then(|binding| match &binding.kind {
|
||||
BindingKind::Export(Export { names }) => Some((names.as_slice(), binding.range)),
|
||||
BindingKind::Export(export) => Some((export.names.as_slice(), binding.range)),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
@@ -5072,7 +5115,8 @@ impl<'a> Checker<'a> {
|
||||
for (name, index) in scope.bindings() {
|
||||
let binding = &self.ctx.bindings[*index];
|
||||
if binding.kind.is_global() {
|
||||
if let Some(stmt) = &binding.source {
|
||||
if let Some(source) = binding.source {
|
||||
let stmt = &self.ctx.stmts[source];
|
||||
if matches!(stmt.node, StmtKind::Global { .. }) {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
pylint::rules::GlobalVariableNotAssigned {
|
||||
@@ -5126,13 +5170,17 @@ impl<'a> Checker<'a> {
|
||||
| BindingKind::FunctionDefinition
|
||||
)
|
||||
.then(|| {
|
||||
rebound.source.as_ref().map_or(rebound.range, |source| {
|
||||
helpers::identifier_range(source, self.locator)
|
||||
rebound.source.map_or(rebound.range, |source| {
|
||||
helpers::identifier_range(
|
||||
self.ctx.stmts[source],
|
||||
self.locator,
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap_or(rebound.range),
|
||||
);
|
||||
if let Some(parent) = &rebound.source {
|
||||
if let Some(source) = rebound.source {
|
||||
let parent = &self.ctx.stmts[source];
|
||||
if matches!(parent.node, StmtKind::ImportFrom { .. })
|
||||
&& parent.range().contains_range(rebound.range)
|
||||
{
|
||||
@@ -5186,11 +5234,7 @@ impl<'a> Checker<'a> {
|
||||
// Collect all unused imports by location. (Multiple unused imports at the same
|
||||
// location indicates an `import from`.)
|
||||
type UnusedImport<'a> = (&'a str, &'a TextRange);
|
||||
type BindingContext<'a, 'b> = (
|
||||
&'a RefEquality<'b, Stmt>,
|
||||
Option<&'a RefEquality<'b, Stmt>>,
|
||||
Exceptions,
|
||||
);
|
||||
type BindingContext<'a> = (NodeId, Option<NodeId>, Exceptions);
|
||||
|
||||
let mut unused: FxHashMap<BindingContext, Vec<UnusedImport>> = FxHashMap::default();
|
||||
let mut ignored: FxHashMap<BindingContext, Vec<UnusedImport>> =
|
||||
@@ -5200,14 +5244,9 @@ impl<'a> Checker<'a> {
|
||||
let binding = &self.ctx.bindings[*index];
|
||||
|
||||
let full_name = match &binding.kind {
|
||||
BindingKind::Importation(Importation { full_name, .. }) => full_name,
|
||||
BindingKind::FromImportation(FromImportation { full_name, .. }) => {
|
||||
full_name.as_str()
|
||||
}
|
||||
BindingKind::SubmoduleImportation(SubmoduleImportation {
|
||||
full_name,
|
||||
..
|
||||
}) => full_name,
|
||||
BindingKind::Importation(import) => import.full_name,
|
||||
BindingKind::FromImportation(import) => import.full_name.as_str(),
|
||||
BindingKind::SubmoduleImportation(import) => import.full_name,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
@@ -5215,12 +5254,12 @@ impl<'a> Checker<'a> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let defined_by = binding.source.as_ref().unwrap();
|
||||
let defined_in = self.ctx.child_to_parent.get(defined_by);
|
||||
let exceptions = binding.exceptions;
|
||||
let child: &Stmt = defined_by.into();
|
||||
let child_id = binding.source.unwrap();
|
||||
let parent_id = self.ctx.stmts.parent_id(child_id);
|
||||
|
||||
let exceptions = binding.exceptions;
|
||||
let diagnostic_offset = binding.range.start();
|
||||
let child = &self.ctx.stmts[child_id];
|
||||
let parent_offset = if matches!(child.node, StmtKind::ImportFrom { .. }) {
|
||||
Some(child.start())
|
||||
} else {
|
||||
@@ -5233,12 +5272,12 @@ impl<'a> Checker<'a> {
|
||||
})
|
||||
{
|
||||
ignored
|
||||
.entry((defined_by, defined_in, exceptions))
|
||||
.entry((child_id, parent_id, exceptions))
|
||||
.or_default()
|
||||
.push((full_name, &binding.range));
|
||||
} else {
|
||||
unused
|
||||
.entry((defined_by, defined_in, exceptions))
|
||||
.entry((child_id, parent_id, exceptions))
|
||||
.or_default()
|
||||
.push((full_name, &binding.range));
|
||||
}
|
||||
@@ -5248,10 +5287,10 @@ impl<'a> Checker<'a> {
|
||||
self.settings.ignore_init_module_imports && self.path.ends_with("__init__.py");
|
||||
for ((defined_by, defined_in, exceptions), unused_imports) in unused
|
||||
.into_iter()
|
||||
.sorted_by_key(|((defined_by, ..), ..)| defined_by.start())
|
||||
.sorted_by_key(|((defined_by, ..), ..)| *defined_by)
|
||||
{
|
||||
let child: &Stmt = defined_by.into();
|
||||
let parent: Option<&Stmt> = defined_in.map(Into::into);
|
||||
let child = self.ctx.stmts[defined_by];
|
||||
let parent = defined_in.map(|defined_in| self.ctx.stmts[defined_in]);
|
||||
let multiple = unused_imports.len() > 1;
|
||||
let in_except_handler = exceptions
|
||||
.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR);
|
||||
@@ -5269,7 +5308,7 @@ impl<'a> Checker<'a> {
|
||||
) {
|
||||
Ok(fix) => {
|
||||
if fix.is_deletion() || fix.content() == Some("pass") {
|
||||
self.deletions.insert(*defined_by);
|
||||
self.deletions.insert(RefEquality(child));
|
||||
}
|
||||
Some(fix)
|
||||
}
|
||||
@@ -5306,11 +5345,11 @@ impl<'a> Checker<'a> {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
for ((defined_by, .., exceptions), unused_imports) in ignored
|
||||
for ((child, .., exceptions), unused_imports) in ignored
|
||||
.into_iter()
|
||||
.sorted_by_key(|((defined_by, ..), ..)| defined_by.start())
|
||||
.sorted_by_key(|((defined_by, ..), ..)| *defined_by)
|
||||
{
|
||||
let child: &Stmt = defined_by.into();
|
||||
let child = self.ctx.stmts[child];
|
||||
let multiple = unused_imports.len() > 1;
|
||||
let in_except_handler = exceptions
|
||||
.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR);
|
||||
@@ -5406,9 +5445,9 @@ impl<'a> Checker<'a> {
|
||||
let mut overloaded_name: Option<String> = None;
|
||||
while !self.deferred.definitions.is_empty() {
|
||||
let definitions = std::mem::take(&mut self.deferred.definitions);
|
||||
for (definition, visibility, (scope_id, parents)) in definitions {
|
||||
for (definition, visibility, (scope_id, stmt_id)) in definitions {
|
||||
self.ctx.scope_id = scope_id;
|
||||
self.ctx.parents = parents;
|
||||
self.ctx.stmt_id = stmt_id;
|
||||
|
||||
// flake8-annotations
|
||||
if enforce_annotations {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::registry::{Linter, Rule};
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use crate::registry::{Linter, Rule};
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct NoqaCode(&'static str, &'static str);
|
||||
|
||||
@@ -204,12 +205,14 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Pylint, "R5501") => Rule::CollapsibleElseIf,
|
||||
(Pylint, "W0120") => Rule::UselessElseOnLoop,
|
||||
(Pylint, "W0129") => Rule::AssertOnStringLiteral,
|
||||
(Pylint, "W0406") => Rule::ImportSelf,
|
||||
(Pylint, "W0602") => Rule::GlobalVariableNotAssigned,
|
||||
(Pylint, "W0603") => Rule::GlobalStatement,
|
||||
(Pylint, "W0711") => Rule::BinaryOpException,
|
||||
(Pylint, "W1508") => Rule::InvalidEnvvarDefault,
|
||||
(Pylint, "W2901") => Rule::RedefinedLoopName,
|
||||
(Pylint, "E0302") => Rule::UnexpectedSpecialMethodSignature,
|
||||
(Pylint, "W3301") => Rule::NestedMinMax,
|
||||
|
||||
// flake8-builtins
|
||||
(Flake8Builtins, "001") => Rule::BuiltinVariableShadowing,
|
||||
@@ -592,8 +595,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Flake8Pyi, "014") => Rule::ArgumentDefaultInStub,
|
||||
(Flake8Pyi, "015") => Rule::AssignmentDefaultInStub,
|
||||
(Flake8Pyi, "016") => Rule::DuplicateUnionMember,
|
||||
(Flake8Pyi, "020") => Rule::QuotedAnnotationInStub,
|
||||
(Flake8Pyi, "021") => Rule::DocstringInStub,
|
||||
(Flake8Pyi, "033") => Rule::TypeCommentInStub,
|
||||
(Flake8Pyi, "042") => Rule::SnakeCaseTypeAlias,
|
||||
(Flake8Pyi, "043") => Rule::TSuffixedTypeAlias,
|
||||
|
||||
// flake8-pytest-style
|
||||
(Flake8PytestStyle, "001") => Rule::PytestFixtureIncorrectParenthesesStyle,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use anyhow::Result;
|
||||
use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute};
|
||||
use ruff_text_size::TextSize;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::ast::{Stmt, StmtKind, Suite};
|
||||
use rustpython_parser::{lexer, Mode, Tok};
|
||||
|
||||
@@ -18,10 +17,7 @@ pub struct Importer<'a> {
|
||||
python_ast: &'a Suite,
|
||||
locator: &'a Locator<'a>,
|
||||
stylist: &'a Stylist<'a>,
|
||||
/// A map from module name to top-level `StmtKind::ImportFrom` statements.
|
||||
import_from_map: FxHashMap<&'a str, &'a Stmt>,
|
||||
/// The last top-level import statement.
|
||||
trailing_import: Option<&'a Stmt>,
|
||||
ordered_imports: Vec<&'a Stmt>,
|
||||
}
|
||||
|
||||
impl<'a> Importer<'a> {
|
||||
@@ -30,34 +26,21 @@ impl<'a> Importer<'a> {
|
||||
python_ast,
|
||||
locator,
|
||||
stylist,
|
||||
import_from_map: FxHashMap::default(),
|
||||
trailing_import: None,
|
||||
ordered_imports: Vec::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Visit a top-level import statement.
|
||||
pub fn visit_import(&mut self, import: &'a Stmt) {
|
||||
// Store a reference to the import statement in the appropriate map.
|
||||
match &import.node {
|
||||
StmtKind::Import { .. } => {
|
||||
// Nothing to do here, we don't extend top-level `import` statements at all, so
|
||||
// no need to track them.
|
||||
}
|
||||
StmtKind::ImportFrom { module, level, .. } => {
|
||||
// Store a reverse-map from module name to `import ... from` statement.
|
||||
if level.map_or(true, |level| level == 0) {
|
||||
if let Some(module) = module {
|
||||
self.import_from_map.insert(module.as_str(), import);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected StmtKind::Import | StmtKind::ImportFrom");
|
||||
}
|
||||
}
|
||||
self.ordered_imports.push(import);
|
||||
}
|
||||
|
||||
// Store a reference to the last top-level import statement.
|
||||
self.trailing_import = Some(import);
|
||||
/// Return the import statement that precedes the given position, if any.
|
||||
fn preceding_import(&self, at: TextSize) -> Option<&Stmt> {
|
||||
self.ordered_imports
|
||||
.partition_point(|stmt| stmt.start() < at)
|
||||
.checked_sub(1)
|
||||
.map(|idx| self.ordered_imports[idx])
|
||||
}
|
||||
|
||||
/// Add an import statement to import the given module.
|
||||
@@ -65,9 +48,9 @@ impl<'a> Importer<'a> {
|
||||
/// If there are no existing imports, the new import will be added at the top
|
||||
/// of the file. Otherwise, it will be added after the most recent top-level
|
||||
/// import statement.
|
||||
pub fn add_import(&self, import: &AnyImport) -> Edit {
|
||||
pub fn add_import(&self, import: &AnyImport, at: TextSize) -> Edit {
|
||||
let required_import = import.to_string();
|
||||
if let Some(stmt) = self.trailing_import {
|
||||
if let Some(stmt) = self.preceding_import(at) {
|
||||
// Insert after the last top-level import.
|
||||
let Insertion {
|
||||
prefix,
|
||||
@@ -88,10 +71,28 @@ impl<'a> Importer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the top-level [`Stmt`] that imports the given module using `StmtKind::ImportFrom`.
|
||||
/// if it exists.
|
||||
pub fn get_import_from(&self, module: &str) -> Option<&Stmt> {
|
||||
self.import_from_map.get(module).copied()
|
||||
/// Return the top-level [`Stmt`] that imports the given module using `StmtKind::ImportFrom`
|
||||
/// preceding the given position, if any.
|
||||
pub fn find_import_from(&self, module: &str, at: TextSize) -> Option<&Stmt> {
|
||||
let mut import_from = None;
|
||||
for stmt in &self.ordered_imports {
|
||||
if stmt.start() >= at {
|
||||
break;
|
||||
}
|
||||
if let StmtKind::ImportFrom {
|
||||
module: name,
|
||||
level,
|
||||
..
|
||||
} = &stmt.node
|
||||
{
|
||||
if level.map_or(true, |level| level == 0)
|
||||
&& name.as_ref().map_or(false, |name| name == module)
|
||||
{
|
||||
import_from = Some(*stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
import_from
|
||||
}
|
||||
|
||||
/// Add the given member to an existing `StmtKind::ImportFrom` statement.
|
||||
@@ -156,7 +157,7 @@ fn match_docstring_end(body: &[Stmt]) -> Option<TextSize> {
|
||||
Some(stmt.end())
|
||||
}
|
||||
|
||||
/// Find the location at which a "top-of-file" import should be inserted,
|
||||
/// Find the location at which an "end-of-statement" import should be inserted,
|
||||
/// along with a prefix and suffix to use for the insertion.
|
||||
///
|
||||
/// For example, given the following code:
|
||||
@@ -165,9 +166,15 @@ fn match_docstring_end(body: &[Stmt]) -> Option<TextSize> {
|
||||
/// """Hello, world!"""
|
||||
///
|
||||
/// import os
|
||||
/// import math
|
||||
///
|
||||
///
|
||||
/// def foo():
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// The location returned will be the start of the `import os` statement,
|
||||
/// The location returned will be the start of new line after the last
|
||||
/// import statement, which in this case is the line after `import math`,
|
||||
/// along with a trailing newline suffix.
|
||||
fn end_of_statement_insertion(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Insertion {
|
||||
let location = stmt.end();
|
||||
@@ -180,7 +187,7 @@ fn end_of_statement_insertion(stmt: &Stmt, locator: &Locator, stylist: &Stylist)
|
||||
// Otherwise, insert on the next line.
|
||||
Insertion::new(
|
||||
"",
|
||||
locator.line_end(location),
|
||||
locator.full_line_end(location),
|
||||
stylist.line_ending().as_str(),
|
||||
)
|
||||
}
|
||||
@@ -234,11 +241,11 @@ fn top_of_file_insertion(body: &[Stmt], locator: &Locator, stylist: &Stylist) ->
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use ruff_python_ast::newlines::LineEnding;
|
||||
use ruff_text_size::TextSize;
|
||||
use rustpython_parser as parser;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
|
||||
use ruff_python_ast::newlines::LineEnding;
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
|
||||
use crate::importer::{top_of_file_insertion, Insertion};
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use log::error;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
@@ -453,24 +454,12 @@ pub fn lint_fix<'a>(
|
||||
// longer parseable on a subsequent pass, then we've introduced a
|
||||
// syntax error. Return the original code.
|
||||
if parseable && result.error.is_some() {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!(
|
||||
r#"
|
||||
{}: Autofix introduced a syntax error. Reverting all changes.
|
||||
|
||||
This indicates a bug in `{}`. If you could open an issue at:
|
||||
|
||||
{}/issues/new?title=%5BAutofix%20error%5D
|
||||
|
||||
...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
|
||||
"#,
|
||||
"error".red().bold(),
|
||||
CARGO_PKG_NAME,
|
||||
CARGO_PKG_REPOSITORY,
|
||||
fs::relativize_path(path),
|
||||
);
|
||||
}
|
||||
report_autofix_syntax_error(
|
||||
path,
|
||||
&transformed,
|
||||
&result.error.unwrap(),
|
||||
fixed.keys().copied(),
|
||||
);
|
||||
return Err(anyhow!("Autofix introduced a syntax error"));
|
||||
}
|
||||
}
|
||||
@@ -493,25 +482,7 @@ This indicates a bug in `{}`. If you could open an issue at:
|
||||
continue;
|
||||
}
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!(
|
||||
r#"
|
||||
{}: Failed to converge after {} iterations.
|
||||
|
||||
This indicates a bug in `{}`. If you could open an issue at:
|
||||
|
||||
{}/issues/new?title=%5BInfinite%20loop%5D
|
||||
|
||||
...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
|
||||
"#,
|
||||
"error".red().bold(),
|
||||
MAX_ITERATIONS,
|
||||
CARGO_PKG_NAME,
|
||||
CARGO_PKG_REPOSITORY,
|
||||
fs::relativize_path(path),
|
||||
);
|
||||
}
|
||||
report_failed_to_converge_error(path, &transformed, &result.data.0);
|
||||
}
|
||||
|
||||
return Ok(FixerResult {
|
||||
@@ -526,3 +497,80 @@ This indicates a bug in `{}`. If you could open an issue at:
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_rule_codes(rules: impl IntoIterator<Item = Rule>) -> String {
|
||||
rules
|
||||
.into_iter()
|
||||
.map(|rule| rule.noqa_code().to_string())
|
||||
.sorted_unstable()
|
||||
.dedup()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics: &[Diagnostic]) {
|
||||
if cfg!(debug_assertions) {
|
||||
let codes = collect_rule_codes(diagnostics.iter().map(|diagnostic| diagnostic.kind.rule()));
|
||||
eprintln!(
|
||||
"{}: Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---",
|
||||
"debug error".red().bold(),
|
||||
MAX_ITERATIONS,
|
||||
fs::relativize_path(path),
|
||||
codes,
|
||||
transformed,
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
r#"
|
||||
{}: Failed to converge after {} iterations.
|
||||
|
||||
This indicates a bug in `{}`. If you could open an issue at:
|
||||
|
||||
{}/issues/new?title=%5BInfinite%20loop%5D
|
||||
|
||||
...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
|
||||
"#,
|
||||
"error".red().bold(),
|
||||
MAX_ITERATIONS,
|
||||
CARGO_PKG_NAME,
|
||||
CARGO_PKG_REPOSITORY,
|
||||
fs::relativize_path(path),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
fn report_autofix_syntax_error(
|
||||
path: &Path,
|
||||
transformed: &str,
|
||||
error: &ParseError,
|
||||
rules: impl IntoIterator<Item = Rule>,
|
||||
) {
|
||||
if cfg!(debug_assertions) {
|
||||
let codes = collect_rule_codes(rules);
|
||||
eprintln!(
|
||||
"{}: Autofix introduced a syntax error in `{}` with rule codes {}: {}\n---\n{}\n---",
|
||||
"error".red().bold(),
|
||||
fs::relativize_path(path),
|
||||
codes,
|
||||
error,
|
||||
transformed,
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
r#"
|
||||
{}: Autofix introduced a syntax error. Reverting all changes.
|
||||
|
||||
This indicates a bug in `{}`. If you could open an issue at:
|
||||
|
||||
{}/issues/new?title=%5BAutofix%20error%5D
|
||||
|
||||
...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
|
||||
"#,
|
||||
"error".red().bold(),
|
||||
CARGO_PKG_NAME,
|
||||
CARGO_PKG_REPOSITORY,
|
||||
fs::relativize_path(path),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
use ruff_diagnostics::DiagnosticKind;
|
||||
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::Write;
|
||||
|
||||
@@ -172,6 +173,7 @@ impl Display for MessageCodeFrame<'_> {
|
||||
};
|
||||
|
||||
let source_code = file.to_source_code();
|
||||
|
||||
let content_start_index = source_code.line_index(range.start());
|
||||
let mut start_index = content_start_index.saturating_sub(2);
|
||||
|
||||
@@ -200,26 +202,23 @@ impl Display for MessageCodeFrame<'_> {
|
||||
let start_offset = source_code.line_start(start_index);
|
||||
let end_offset = source_code.line_end(end_index);
|
||||
|
||||
let source_text = source_code.slice(TextRange::new(start_offset, end_offset));
|
||||
let source = replace_whitespace(
|
||||
source_code.slice(TextRange::new(start_offset, end_offset)),
|
||||
range - start_offset,
|
||||
);
|
||||
|
||||
let annotation_start_offset = range.start() - start_offset;
|
||||
let annotation_end_offset = range.end() - start_offset;
|
||||
|
||||
let start_char = source_text[TextRange::up_to(annotation_start_offset)]
|
||||
let start_char = source.text[TextRange::up_to(source.annotation_range.start())]
|
||||
.chars()
|
||||
.count();
|
||||
|
||||
let char_length = source_text
|
||||
[TextRange::new(annotation_start_offset, annotation_end_offset)]
|
||||
.chars()
|
||||
.count();
|
||||
let char_length = source.text[source.annotation_range].chars().count();
|
||||
|
||||
let label = kind.rule().noqa_code().to_string();
|
||||
|
||||
let snippet = Snippet {
|
||||
title: None,
|
||||
slices: vec![Slice {
|
||||
source: source_text,
|
||||
source: &source.text,
|
||||
line_start: content_start_index.get(),
|
||||
annotations: vec![SourceAnnotation {
|
||||
label: &label,
|
||||
@@ -245,6 +244,60 @@ impl Display for MessageCodeFrame<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
|
||||
static TAB_SIZE: TextSize = TextSize::new(4);
|
||||
|
||||
let mut result = String::new();
|
||||
let mut last_end = 0;
|
||||
let mut range = annotation_range;
|
||||
let mut column = 0;
|
||||
|
||||
for (index, m) in source.match_indices(['\t', '\n', '\r']) {
|
||||
match m {
|
||||
"\t" => {
|
||||
let tab_width = TAB_SIZE - TextSize::new(column % 4);
|
||||
|
||||
if index < usize::from(annotation_range.start()) {
|
||||
range += tab_width - TextSize::new(1);
|
||||
} else if index < usize::from(annotation_range.end()) {
|
||||
range = range.add_end(tab_width - TextSize::new(1));
|
||||
}
|
||||
|
||||
result.push_str(&source[last_end..index]);
|
||||
|
||||
for _ in 0..u32::from(tab_width) {
|
||||
result.push(' ');
|
||||
}
|
||||
|
||||
last_end = index + 1;
|
||||
}
|
||||
"\n" | "\r" => {
|
||||
column = 0;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// No tabs
|
||||
if result.is_empty() {
|
||||
SourceCode {
|
||||
annotation_range,
|
||||
text: Cow::Borrowed(source),
|
||||
}
|
||||
} else {
|
||||
result.push_str(&source[last_end..]);
|
||||
SourceCode {
|
||||
annotation_range: range,
|
||||
text: Cow::Owned(result),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SourceCode<'a> {
|
||||
text: Cow<'a, str>,
|
||||
annotation_range: TextRange,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::resolver::{PyprojectDiscovery, Resolver};
|
||||
use crate::resolver::{PyprojectConfig, Resolver};
|
||||
|
||||
// If we have a Python package layout like:
|
||||
// - root/
|
||||
@@ -82,7 +82,7 @@ fn detect_package_root_with_cache<'a>(
|
||||
pub fn detect_package_roots<'a>(
|
||||
files: &[&'a Path],
|
||||
resolver: &'a Resolver,
|
||||
pyproject_strategy: &'a PyprojectDiscovery,
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
) -> FxHashMap<&'a Path, Option<&'a Path>> {
|
||||
// Pre-populate the module cache, since the list of files could (but isn't
|
||||
// required to) contain some `__init__.py` files.
|
||||
@@ -98,9 +98,7 @@ pub fn detect_package_roots<'a>(
|
||||
// Search for the package root for each file.
|
||||
let mut package_roots: FxHashMap<&Path, Option<&Path>> = FxHashMap::default();
|
||||
for file in files {
|
||||
let namespace_packages = &resolver
|
||||
.resolve(file, pyproject_strategy)
|
||||
.namespace_packages;
|
||||
let namespace_packages = &resolver.resolve(file, pyproject_config).namespace_packages;
|
||||
if let Some(package) = file.parent() {
|
||||
if package_roots.contains_key(package) {
|
||||
continue;
|
||||
|
||||
@@ -156,6 +156,7 @@ ruff_macros::register_rules!(
|
||||
rules::pylint::rules::BadStringFormatType,
|
||||
rules::pylint::rules::BidirectionalUnicode,
|
||||
rules::pylint::rules::BinaryOpException,
|
||||
rules::pylint::rules::ImportSelf,
|
||||
rules::pylint::rules::InvalidCharacterBackspace,
|
||||
rules::pylint::rules::InvalidCharacterSub,
|
||||
rules::pylint::rules::InvalidCharacterEsc,
|
||||
@@ -188,6 +189,7 @@ ruff_macros::register_rules!(
|
||||
rules::pylint::rules::LoggingTooFewArgs,
|
||||
rules::pylint::rules::LoggingTooManyArgs,
|
||||
rules::pylint::rules::UnexpectedSpecialMethodSignature,
|
||||
rules::pylint::rules::NestedMinMax,
|
||||
// flake8-builtins
|
||||
rules::flake8_builtins::rules::BuiltinVariableShadowing,
|
||||
rules::flake8_builtins::rules::BuiltinArgumentShadowing,
|
||||
@@ -539,6 +541,9 @@ ruff_macros::register_rules!(
|
||||
rules::flake8_pyi::rules::UnrecognizedPlatformName,
|
||||
rules::flake8_pyi::rules::PassInClassBody,
|
||||
rules::flake8_pyi::rules::DuplicateUnionMember,
|
||||
rules::flake8_pyi::rules::QuotedAnnotationInStub,
|
||||
rules::flake8_pyi::rules::SnakeCaseTypeAlias,
|
||||
rules::flake8_pyi::rules::TSuffixedTypeAlias,
|
||||
// flake8-pytest-style
|
||||
rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle,
|
||||
rules::flake8_pytest_style::rules::PytestFixturePositionalArgs,
|
||||
|
||||
@@ -7,10 +7,10 @@ use std::iter::FusedIterator;
|
||||
///
|
||||
/// Uses a bitset where a bit of one signals that the Rule with that [u16] is in this set.
|
||||
#[derive(Clone, Default, CacheKey, PartialEq, Eq)]
|
||||
pub struct RuleSet([u64; 9]);
|
||||
pub struct RuleSet([u64; 10]);
|
||||
|
||||
impl RuleSet {
|
||||
const EMPTY: [u64; 9] = [0; 9];
|
||||
const EMPTY: [u64; 10] = [0; 10];
|
||||
|
||||
// 64 fits into a u16 without truncation
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
|
||||
@@ -17,25 +17,42 @@ use crate::settings::configuration::Configuration;
|
||||
use crate::settings::pyproject::settings_toml;
|
||||
use crate::settings::{pyproject, AllSettings, Settings};
|
||||
|
||||
/// The configuration information from a `pyproject.toml` file.
|
||||
pub struct PyprojectConfig {
|
||||
/// The strategy used to discover the relevant `pyproject.toml` file for
|
||||
/// each Python file.
|
||||
pub strategy: PyprojectDiscoveryStrategy,
|
||||
/// All settings from the `pyproject.toml` file.
|
||||
pub settings: AllSettings,
|
||||
/// Absolute path to the `pyproject.toml` file. This would be `None` when
|
||||
/// either using the default settings or the `--isolated` flag is set.
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl PyprojectConfig {
|
||||
pub fn new(
|
||||
strategy: PyprojectDiscoveryStrategy,
|
||||
settings: AllSettings,
|
||||
path: Option<PathBuf>,
|
||||
) -> Self {
|
||||
Self {
|
||||
strategy,
|
||||
settings,
|
||||
path: path.map(fs::normalize_path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The strategy used to discover the relevant `pyproject.toml` file for each
|
||||
/// Python file.
|
||||
#[derive(Debug, is_macro::Is)]
|
||||
pub enum PyprojectDiscovery {
|
||||
pub enum PyprojectDiscoveryStrategy {
|
||||
/// Use a fixed `pyproject.toml` file for all Python files (i.e., one
|
||||
/// provided on the command-line).
|
||||
Fixed(AllSettings),
|
||||
Fixed,
|
||||
/// Use the closest `pyproject.toml` file in the filesystem hierarchy, or
|
||||
/// the default settings.
|
||||
Hierarchical(AllSettings),
|
||||
}
|
||||
|
||||
impl PyprojectDiscovery {
|
||||
pub fn top_level_settings(&self) -> &AllSettings {
|
||||
match self {
|
||||
PyprojectDiscovery::Fixed(settings) => settings,
|
||||
PyprojectDiscovery::Hierarchical(settings) => settings,
|
||||
}
|
||||
}
|
||||
Hierarchical,
|
||||
}
|
||||
|
||||
/// The strategy for resolving file paths in a `pyproject.toml`.
|
||||
@@ -75,21 +92,25 @@ impl Resolver {
|
||||
pub fn resolve_all<'a>(
|
||||
&'a self,
|
||||
path: &Path,
|
||||
strategy: &'a PyprojectDiscovery,
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
) -> &'a AllSettings {
|
||||
match strategy {
|
||||
PyprojectDiscovery::Fixed(settings) => settings,
|
||||
PyprojectDiscovery::Hierarchical(default) => self
|
||||
match pyproject_config.strategy {
|
||||
PyprojectDiscoveryStrategy::Fixed => &pyproject_config.settings,
|
||||
PyprojectDiscoveryStrategy::Hierarchical => self
|
||||
.settings
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|(root, settings)| path.starts_with(root).then_some(settings))
|
||||
.unwrap_or(default),
|
||||
.unwrap_or(&pyproject_config.settings),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve<'a>(&'a self, path: &Path, strategy: &'a PyprojectDiscovery) -> &'a Settings {
|
||||
&self.resolve_all(path, strategy).lib
|
||||
pub fn resolve<'a>(
|
||||
&'a self,
|
||||
path: &Path,
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
) -> &'a Settings {
|
||||
&self.resolve_all(path, pyproject_config).lib
|
||||
}
|
||||
|
||||
/// Return an iterator over the resolved [`Settings`] in this [`Resolver`].
|
||||
@@ -200,7 +221,7 @@ fn match_exclusion<P: AsRef<Path>, R: AsRef<Path>>(
|
||||
/// Find all Python (`.py`, `.pyi` and `.ipynb` files) in a set of paths.
|
||||
pub fn python_files_in_path(
|
||||
paths: &[PathBuf],
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
pyproject_config: &PyprojectConfig,
|
||||
processor: impl ConfigProcessor,
|
||||
) -> Result<(Vec<Result<DirEntry, ignore::Error>>, Resolver)> {
|
||||
// Normalize every path (e.g., convert from relative to absolute).
|
||||
@@ -209,7 +230,7 @@ pub fn python_files_in_path(
|
||||
// Search for `pyproject.toml` files in all parent directories.
|
||||
let mut resolver = Resolver::default();
|
||||
let mut seen = FxHashSet::default();
|
||||
if pyproject_strategy.is_hierarchical() {
|
||||
if pyproject_config.strategy.is_hierarchical() {
|
||||
for path in &paths {
|
||||
for ancestor in path.ancestors() {
|
||||
if seen.insert(ancestor) {
|
||||
@@ -224,8 +245,8 @@ pub fn python_files_in_path(
|
||||
}
|
||||
|
||||
// Check if the paths themselves are excluded.
|
||||
if pyproject_strategy.top_level_settings().lib.force_exclude {
|
||||
paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_strategy));
|
||||
if pyproject_config.settings.lib.force_exclude {
|
||||
paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_config));
|
||||
if paths.is_empty() {
|
||||
return Ok((vec![], resolver));
|
||||
}
|
||||
@@ -240,12 +261,7 @@ pub fn python_files_in_path(
|
||||
for path in &paths[1..] {
|
||||
builder.add(path);
|
||||
}
|
||||
builder.standard_filters(
|
||||
pyproject_strategy
|
||||
.top_level_settings()
|
||||
.lib
|
||||
.respect_gitignore,
|
||||
);
|
||||
builder.standard_filters(pyproject_config.settings.lib.respect_gitignore);
|
||||
builder.hidden(false);
|
||||
let walker = builder.build_parallel();
|
||||
|
||||
@@ -261,7 +277,7 @@ pub fn python_files_in_path(
|
||||
if entry.depth() > 0 {
|
||||
let path = entry.path();
|
||||
let resolver = resolver.read().unwrap();
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if !settings.exclude.is_empty()
|
||||
&& match_exclusion(path, file_name, &settings.exclude)
|
||||
@@ -283,7 +299,7 @@ pub fn python_files_in_path(
|
||||
|
||||
// Search for the `pyproject.toml` file in this directory, before we visit any
|
||||
// of its contents.
|
||||
if pyproject_strategy.is_hierarchical() {
|
||||
if pyproject_config.strategy.is_hierarchical() {
|
||||
if let Ok(entry) = &result {
|
||||
if entry
|
||||
.file_type()
|
||||
@@ -321,7 +337,7 @@ pub fn python_files_in_path(
|
||||
// Otherwise, check if the file is included.
|
||||
let path = entry.path();
|
||||
let resolver = resolver.read().unwrap();
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
if settings.include.is_match(path) {
|
||||
debug!("Included path via `include`: {:?}", path);
|
||||
true
|
||||
@@ -348,10 +364,10 @@ pub fn python_files_in_path(
|
||||
/// Return `true` if the Python file at [`Path`] is _not_ excluded.
|
||||
pub fn python_file_at_path(
|
||||
path: &Path,
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
pyproject_config: &PyprojectConfig,
|
||||
processor: impl ConfigProcessor,
|
||||
) -> Result<bool> {
|
||||
if !pyproject_strategy.top_level_settings().lib.force_exclude {
|
||||
if !pyproject_config.settings.lib.force_exclude {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -360,7 +376,7 @@ pub fn python_file_at_path(
|
||||
|
||||
// Search for `pyproject.toml` files in all parent directories.
|
||||
let mut resolver = Resolver::default();
|
||||
if pyproject_strategy.is_hierarchical() {
|
||||
if pyproject_config.strategy.is_hierarchical() {
|
||||
for ancestor in path.ancestors() {
|
||||
if let Some(pyproject) = settings_toml(ancestor)? {
|
||||
let (root, settings) =
|
||||
@@ -371,14 +387,14 @@ pub fn python_file_at_path(
|
||||
}
|
||||
|
||||
// Check exclusions.
|
||||
Ok(!is_file_excluded(&path, &resolver, pyproject_strategy))
|
||||
Ok(!is_file_excluded(&path, &resolver, pyproject_config))
|
||||
}
|
||||
|
||||
/// Return `true` if the given top-level [`Path`] should be excluded.
|
||||
fn is_file_excluded(
|
||||
path: &Path,
|
||||
resolver: &Resolver,
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
pyproject_strategy: &PyprojectConfig,
|
||||
) -> bool {
|
||||
// TODO(charlie): Respect gitignore.
|
||||
for path in path.ancestors() {
|
||||
@@ -419,7 +435,7 @@ mod tests {
|
||||
|
||||
use crate::resolver::{
|
||||
is_file_excluded, match_exclusion, resolve_settings_with_processor, NoOpProcessor,
|
||||
PyprojectDiscovery, Relativity, Resolver,
|
||||
PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, Resolver,
|
||||
};
|
||||
use crate::settings::pyproject::find_settings_toml;
|
||||
use crate::settings::types::FilePattern;
|
||||
@@ -560,25 +576,29 @@ mod tests {
|
||||
fn rooted_exclusion() -> Result<()> {
|
||||
let package_root = test_resource_path("package");
|
||||
let resolver = Resolver::default();
|
||||
let ppd = PyprojectDiscovery::Hierarchical(resolve_settings_with_processor(
|
||||
&find_settings_toml(&package_root)?.unwrap(),
|
||||
&Relativity::Parent,
|
||||
&NoOpProcessor,
|
||||
)?);
|
||||
let pyproject_config = PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Hierarchical,
|
||||
resolve_settings_with_processor(
|
||||
&find_settings_toml(&package_root)?.unwrap(),
|
||||
&Relativity::Parent,
|
||||
&NoOpProcessor,
|
||||
)?,
|
||||
None,
|
||||
);
|
||||
// src/app.py should not be excluded even if it lives in a hierarchy that should
|
||||
// be excluded by virtue of the pyproject.toml having `resources/*` in
|
||||
// it.
|
||||
assert!(!is_file_excluded(
|
||||
&package_root.join("src/app.py"),
|
||||
&resolver,
|
||||
&ppd,
|
||||
&pyproject_config,
|
||||
));
|
||||
// However, resources/ignored.py should be ignored, since that `resources` is
|
||||
// beneath the package root.
|
||||
assert!(is_file_excluded(
|
||||
&package_root.join("resources/ignored.py"),
|
||||
&resolver,
|
||||
&ppd,
|
||||
&pyproject_config,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_text_size::{TextLen, TextRange};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
@@ -58,10 +58,7 @@ pub fn commented_out_code(
|
||||
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
|
||||
let mut diagnostic = Diagnostic::new(CommentedOutCode, range);
|
||||
if autofix.into() && settings.rules.should_fix(Rule::CommentedOutCode) {
|
||||
diagnostic.set_fix(Edit::range_deletion(TextRange::at(
|
||||
range.start(),
|
||||
line.text_len(),
|
||||
)));
|
||||
diagnostic.set_fix(Edit::range_deletion(locator.full_lines_range(range)));
|
||||
}
|
||||
Some(diagnostic)
|
||||
} else {
|
||||
|
||||
@@ -91,4 +91,19 @@ ERA001.py:13:5: ERA001 [*] Found commented-out code
|
||||
15 14 |
|
||||
16 15 | #import os # noqa: ERA001
|
||||
|
||||
ERA001.py:21:5: ERA001 [*] Found commented-out code
|
||||
|
|
||||
21 | class A():
|
||||
22 | pass
|
||||
23 | # b = c
|
||||
| ^^^^^^^ ERA001
|
||||
|
|
||||
= help: Remove commented-out code
|
||||
|
||||
ℹ Suggested fix
|
||||
18 18 |
|
||||
19 19 | class A():
|
||||
20 20 | pass
|
||||
21 |- # b = c
|
||||
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ mod tests {
|
||||
#[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"); "B026")]
|
||||
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.py"); "B027")]
|
||||
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.pyi"); "B027_pyi")]
|
||||
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027_extended.py"); "B027_extended")]
|
||||
#[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"); "B028")]
|
||||
#[test_case(Rule::ExceptWithEmptyTuple, Path::new("B029.py"); "B029")]
|
||||
#[test_case(Rule::ExceptWithNonExceptionClasses, Path::new("B030.py"); "B030")]
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
use ruff_python_ast::whitespace::indentation;
|
||||
use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload};
|
||||
use ruff_python_semantic::context::Context;
|
||||
|
||||
use crate::autofix::actions::get_or_import_symbol;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::Importer;
|
||||
use crate::registry::Rule;
|
||||
|
||||
#[violation]
|
||||
@@ -19,12 +25,13 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod {
|
||||
format!("`{name}` is an abstract base class, but it has no abstract methods")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct EmptyMethodWithoutAbstractDecorator {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Violation for EmptyMethodWithoutAbstractDecorator {
|
||||
impl AlwaysAutofixableViolation for EmptyMethodWithoutAbstractDecorator {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let EmptyMethodWithoutAbstractDecorator { name } = self;
|
||||
@@ -32,24 +39,26 @@ impl Violation for EmptyMethodWithoutAbstractDecorator {
|
||||
"`{name}` is an empty method in an abstract base class, but has no abstract decorator"
|
||||
)
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
"Add the `@abstractmethod` decorator".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_abc_class(checker: &Checker, bases: &[Expr], keywords: &[Keyword]) -> bool {
|
||||
fn is_abc_class(context: &Context, bases: &[Expr], keywords: &[Keyword]) -> bool {
|
||||
keywords.iter().any(|keyword| {
|
||||
keyword
|
||||
.node
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |arg| arg == "metaclass")
|
||||
&& checker
|
||||
.ctx
|
||||
&& context
|
||||
.resolve_call_path(&keyword.node.value)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["abc", "ABCMeta"]
|
||||
})
|
||||
}) || bases.iter().any(|base| {
|
||||
checker
|
||||
.ctx
|
||||
context
|
||||
.resolve_call_path(base)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["abc", "ABC"])
|
||||
})
|
||||
@@ -68,6 +77,34 @@ fn is_empty_body(body: &[Stmt]) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn fix_abstractmethod_missing(
|
||||
context: &Context,
|
||||
importer: &Importer,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
stmt: &Stmt,
|
||||
) -> Result<Fix> {
|
||||
let indent = indentation(locator, stmt).ok_or(anyhow!("Unable to detect indentation"))?;
|
||||
let (import_edit, binding) = get_or_import_symbol(
|
||||
"abc",
|
||||
"abstractmethod",
|
||||
stmt.start(),
|
||||
context,
|
||||
importer,
|
||||
locator,
|
||||
)?;
|
||||
let reference_edit = Edit::insertion(
|
||||
format!(
|
||||
"@{binding}{line_ending}{indent}",
|
||||
line_ending = stylist.line_ending().as_str(),
|
||||
),
|
||||
stmt.range().start(),
|
||||
);
|
||||
Ok(Fix::from_iter([import_edit, reference_edit]))
|
||||
}
|
||||
|
||||
/// B024
|
||||
/// B027
|
||||
pub fn abstract_base_class(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
@@ -79,7 +116,7 @@ pub fn abstract_base_class(
|
||||
if bases.len() + keywords.len() != 1 {
|
||||
return;
|
||||
}
|
||||
if !is_abc_class(checker, bases, keywords) {
|
||||
if !is_abc_class(&checker.ctx, bases, keywords) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,12 +160,24 @@ pub fn abstract_base_class(
|
||||
&& is_empty_body(body)
|
||||
&& !is_overload(&checker.ctx, decorator_list)
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
EmptyMethodWithoutAbstractDecorator {
|
||||
name: format!("{name}.{method_name}"),
|
||||
},
|
||||
stmt.range(),
|
||||
));
|
||||
);
|
||||
if checker.patch(Rule::EmptyMethodWithoutAbstractDecorator) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fix_abstractmethod_missing(
|
||||
&checker.ctx,
|
||||
&checker.importer,
|
||||
checker.locator,
|
||||
checker.stylist,
|
||||
stmt,
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker
|
||||
|
||||
@@ -24,7 +24,6 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::types::RefEquality;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{helpers, visitor};
|
||||
|
||||
@@ -108,12 +107,7 @@ where
|
||||
}
|
||||
|
||||
/// B007
|
||||
pub fn unused_loop_control_variable(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
target: &Expr,
|
||||
body: &[Stmt],
|
||||
) {
|
||||
pub fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body: &[Stmt]) {
|
||||
let control_names = {
|
||||
let mut finder = NameFinder::new();
|
||||
finder.visit_expr(target);
|
||||
@@ -170,8 +164,7 @@ pub fn unused_loop_control_variable(
|
||||
let binding = &checker.ctx.bindings[*index];
|
||||
binding
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|source| (source == &RefEquality(stmt)).then_some(binding))
|
||||
.and_then(|source| (Some(source) == checker.ctx.stmt_id).then_some(binding))
|
||||
});
|
||||
if let Some(binding) = binding {
|
||||
if binding.kind.is_loop_var() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B027.py:13:5: B027 `AbstractClass.empty_1` is an empty method in an abstract base class, but has no abstract decorator
|
||||
B027.py:13:5: B027 [*] `AbstractClass.empty_1` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
13 | class AbstractClass(ABC):
|
||||
14 | def empty_1(self): # error
|
||||
@@ -11,8 +11,18 @@ B027.py:13:5: B027 `AbstractClass.empty_1` is an empty method in an abstract bas
|
||||
16 |
|
||||
17 | def empty_2(self): # error
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
B027.py:16:5: B027 `AbstractClass.empty_2` is an empty method in an abstract base class, but has no abstract decorator
|
||||
ℹ Suggested fix
|
||||
10 10 |
|
||||
11 11 |
|
||||
12 12 | class AbstractClass(ABC):
|
||||
13 |+ @notabstract
|
||||
13 14 | def empty_1(self): # error
|
||||
14 15 | ...
|
||||
15 16 |
|
||||
|
||||
B027.py:16:5: B027 [*] `AbstractClass.empty_2` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
16 | ...
|
||||
17 |
|
||||
@@ -23,8 +33,18 @@ B027.py:16:5: B027 `AbstractClass.empty_2` is an empty method in an abstract bas
|
||||
20 |
|
||||
21 | def empty_3(self): # error
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
B027.py:19:5: B027 `AbstractClass.empty_3` is an empty method in an abstract base class, but has no abstract decorator
|
||||
ℹ Suggested fix
|
||||
13 13 | def empty_1(self): # error
|
||||
14 14 | ...
|
||||
15 15 |
|
||||
16 |+ @notabstract
|
||||
16 17 | def empty_2(self): # error
|
||||
17 18 | pass
|
||||
18 19 |
|
||||
|
||||
B027.py:19:5: B027 [*] `AbstractClass.empty_3` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
19 | pass
|
||||
20 |
|
||||
@@ -36,8 +56,18 @@ B027.py:19:5: B027 `AbstractClass.empty_3` is an empty method in an abstract bas
|
||||
24 |
|
||||
25 | def empty_4(self): # error
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
B027.py:23:5: B027 `AbstractClass.empty_4` is an empty method in an abstract base class, but has no abstract decorator
|
||||
ℹ Suggested fix
|
||||
16 16 | def empty_2(self): # error
|
||||
17 17 | pass
|
||||
18 18 |
|
||||
19 |+ @notabstract
|
||||
19 20 | def empty_3(self): # error
|
||||
20 21 | """docstring"""
|
||||
21 22 | ...
|
||||
|
||||
B027.py:23:5: B027 [*] `AbstractClass.empty_4` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
23 | ...
|
||||
24 |
|
||||
@@ -52,5 +82,15 @@ B027.py:23:5: B027 `AbstractClass.empty_4` is an empty method in an abstract bas
|
||||
31 |
|
||||
32 | @notabstract
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
ℹ Suggested fix
|
||||
20 20 | """docstring"""
|
||||
21 21 | ...
|
||||
22 22 |
|
||||
23 |+ @notabstract
|
||||
23 24 | def empty_4(self): # error
|
||||
24 25 | """multiple ellipsis/pass"""
|
||||
25 26 | ...
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B027_extended.py:9:5: B027 [*] `AbstractClass.empty_1` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
9 | class AbstractClass(ABC):
|
||||
10 | def empty_1(self): # error
|
||||
| _____^
|
||||
11 | | ...
|
||||
| |___________^ B027
|
||||
12 |
|
||||
13 | def empty_2(self): # error
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
ℹ Suggested fix
|
||||
2 2 | Should emit:
|
||||
3 3 | B027 - on lines 13, 16, 19, 23
|
||||
4 4 | """
|
||||
5 |-from abc import ABC
|
||||
5 |+from abc import ABC, abstractmethod
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 8 | class AbstractClass(ABC):
|
||||
9 |+ @abstractmethod
|
||||
9 10 | def empty_1(self): # error
|
||||
10 11 | ...
|
||||
11 12 |
|
||||
|
||||
B027_extended.py:12:5: B027 [*] `AbstractClass.empty_2` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
12 | ...
|
||||
13 |
|
||||
14 | def empty_2(self): # error
|
||||
| _____^
|
||||
15 | | pass
|
||||
| |____________^ B027
|
||||
16 |
|
||||
17 | def body_1(self):
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
ℹ Suggested fix
|
||||
2 2 | Should emit:
|
||||
3 3 | B027 - on lines 13, 16, 19, 23
|
||||
4 4 | """
|
||||
5 |-from abc import ABC
|
||||
5 |+from abc import ABC, abstractmethod
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 8 | class AbstractClass(ABC):
|
||||
9 9 | def empty_1(self): # error
|
||||
10 10 | ...
|
||||
11 11 |
|
||||
12 |+ @abstractmethod
|
||||
12 13 | def empty_2(self): # error
|
||||
13 14 | pass
|
||||
14 15 |
|
||||
|
||||
B027_extended.py:25:9: B027 [*] `InnerAbstractClass.empty_1` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
25 | def foo():
|
||||
26 | class InnerAbstractClass(ABC):
|
||||
27 | def empty_1(self): # error
|
||||
| _________^
|
||||
28 | | ...
|
||||
| |_______________^ B027
|
||||
29 |
|
||||
30 | def empty_2(self): # error
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
ℹ Suggested fix
|
||||
2 2 | Should emit:
|
||||
3 3 | B027 - on lines 13, 16, 19, 23
|
||||
4 4 | """
|
||||
5 |-from abc import ABC
|
||||
5 |+from abc import ABC, abstractmethod
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 8 | class AbstractClass(ABC):
|
||||
--------------------------------------------------------------------------------
|
||||
22 22 |
|
||||
23 23 | def foo():
|
||||
24 24 | class InnerAbstractClass(ABC):
|
||||
25 |+ @abstractmethod
|
||||
25 26 | def empty_1(self): # error
|
||||
26 27 | ...
|
||||
27 28 |
|
||||
|
||||
B027_extended.py:28:9: B027 [*] `InnerAbstractClass.empty_2` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
28 | ...
|
||||
29 |
|
||||
30 | def empty_2(self): # error
|
||||
| _________^
|
||||
31 | | pass
|
||||
| |________________^ B027
|
||||
32 |
|
||||
33 | def body_1(self):
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
ℹ Suggested fix
|
||||
2 2 | Should emit:
|
||||
3 3 | B027 - on lines 13, 16, 19, 23
|
||||
4 4 | """
|
||||
5 |-from abc import ABC
|
||||
5 |+from abc import ABC, abstractmethod
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 8 | class AbstractClass(ABC):
|
||||
--------------------------------------------------------------------------------
|
||||
25 25 | def empty_1(self): # error
|
||||
26 26 | ...
|
||||
27 27 |
|
||||
28 |+ @abstractmethod
|
||||
28 29 | def empty_2(self): # error
|
||||
29 30 | pass
|
||||
30 31 |
|
||||
|
||||
|
||||
@@ -55,6 +55,34 @@ G001.py:9:17: G001 Logging statement uses `string.format()`
|
||||
10 | logging.log(level=logging.INFO, msg="Hello {}".format("World!"))
|
||||
11 | logging.log(msg="Hello {}".format("World!"), level=logging.INFO)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ G001
|
||||
12 |
|
||||
13 | # Flask support
|
||||
|
|
||||
|
||||
G001.py:16:31: G001 Logging statement uses `string.format()`
|
||||
|
|
||||
16 | from flask import current_app as app
|
||||
17 |
|
||||
18 | flask.current_app.logger.info("Hello {}".format("World!"))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ G001
|
||||
19 | current_app.logger.info("Hello {}".format("World!"))
|
||||
20 | app.logger.log(logging.INFO, "Hello {}".format("World!"))
|
||||
|
|
||||
|
||||
G001.py:17:25: G001 Logging statement uses `string.format()`
|
||||
|
|
||||
17 | flask.current_app.logger.info("Hello {}".format("World!"))
|
||||
18 | current_app.logger.info("Hello {}".format("World!"))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ G001
|
||||
19 | app.logger.log(logging.INFO, "Hello {}".format("World!"))
|
||||
|
|
||||
|
||||
G001.py:18:30: G001 Logging statement uses `string.format()`
|
||||
|
|
||||
18 | flask.current_app.logger.info("Hello {}".format("World!"))
|
||||
19 | current_app.logger.info("Hello {}".format("World!"))
|
||||
20 | app.logger.log(logging.INFO, "Hello {}".format("World!"))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ G001
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -35,10 +35,16 @@ mod tests {
|
||||
#[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.pyi"))]
|
||||
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))]
|
||||
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.pyi"))]
|
||||
#[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.py"))]
|
||||
#[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.pyi"))]
|
||||
#[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))]
|
||||
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))]
|
||||
#[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))]
|
||||
#[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))]
|
||||
#[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.py"))]
|
||||
#[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))]
|
||||
#[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))]
|
||||
#[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.pyi"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -5,11 +5,15 @@ pub use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody};
|
||||
pub use pass_in_class_body::{pass_in_class_body, PassInClassBody};
|
||||
pub use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody};
|
||||
pub use prefix_type_params::{prefix_type_params, UnprefixedTypeParam};
|
||||
pub use quoted_annotation_in_stub::{quoted_annotation_in_stub, QuotedAnnotationInStub};
|
||||
pub use simple_defaults::{
|
||||
annotated_assignment_default_in_stub, argument_simple_defaults, assignment_default_in_stub,
|
||||
typed_argument_simple_defaults, ArgumentDefaultInStub, AssignmentDefaultInStub,
|
||||
TypedArgumentDefaultInStub,
|
||||
};
|
||||
pub use type_alias_naming::{
|
||||
snake_case_type_alias, t_suffixed_type_alias, SnakeCaseTypeAlias, TSuffixedTypeAlias,
|
||||
};
|
||||
pub use type_comment_in_stub::{type_comment_in_stub, TypeCommentInStub};
|
||||
pub use unrecognized_platform::{
|
||||
unrecognized_platform, UnrecognizedPlatformCheck, UnrecognizedPlatformName,
|
||||
@@ -22,6 +26,8 @@ mod non_empty_stub_body;
|
||||
mod pass_in_class_body;
|
||||
mod pass_statement_stub_body;
|
||||
mod prefix_type_params;
|
||||
mod quoted_annotation_in_stub;
|
||||
mod simple_defaults;
|
||||
mod type_alias_naming;
|
||||
mod type_comment_in_stub;
|
||||
mod unrecognized_platform;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
|
||||
#[violation]
|
||||
pub struct QuotedAnnotationInStub;
|
||||
|
||||
impl AlwaysAutofixableViolation for QuotedAnnotationInStub {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Quoted annotations should not be included in stubs")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
"Remove quotes".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI020
|
||||
pub fn quoted_annotation_in_stub(checker: &mut Checker, annotation: &str, range: TextRange) {
|
||||
let mut diagnostic = Diagnostic::new(QuotedAnnotationInStub, range);
|
||||
if checker.patch(Rule::QuotedAnnotationInStub) {
|
||||
diagnostic.set_fix(Edit::range_replacement(annotation.to_string(), range));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
91
crates/ruff/src/rules/flake8_pyi/rules/type_alias_naming.rs
Normal file
91
crates/ruff/src/rules/flake8_pyi/rules/type_alias_naming.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use rustpython_parser::ast::{Expr, ExprKind};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct SnakeCaseTypeAlias {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Violation for SnakeCaseTypeAlias {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self { name } = self;
|
||||
format!("Type alias `{name}` should be CamelCase")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct TSuffixedTypeAlias {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Violation for TSuffixedTypeAlias {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self { name } = self;
|
||||
format!("Private type alias `{name}` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the given name is a `snake_case` type alias. In this context, we match against
|
||||
/// any name that begins with an optional underscore, followed by at least one lowercase letter.
|
||||
fn is_snake_case_type_alias(name: &str) -> bool {
|
||||
let mut chars = name.chars();
|
||||
matches!(
|
||||
(chars.next(), chars.next()),
|
||||
(Some('_'), Some('0'..='9' | 'a'..='z')) | (Some('0'..='9' | 'a'..='z'), ..)
|
||||
)
|
||||
}
|
||||
|
||||
/// Return `true` if the given name is a T-suffixed type alias. In this context, we match against
|
||||
/// any name that begins with an underscore, and ends in a lowercase letter, followed by `T`,
|
||||
/// followed by an optional digit.
|
||||
fn is_t_suffixed_type_alias(name: &str) -> bool {
|
||||
// A T-suffixed, private type alias must begin with an underscore.
|
||||
if !name.starts_with('_') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// It must end in a lowercase letter, followed by `T`, and (optionally) a digit.
|
||||
let mut chars = name.chars().rev();
|
||||
matches!(
|
||||
(chars.next(), chars.next(), chars.next()),
|
||||
(Some('0'..='9'), Some('T'), Some('a'..='z')) | (Some('T'), Some('a'..='z'), _)
|
||||
)
|
||||
}
|
||||
|
||||
/// PYI042
|
||||
pub fn snake_case_type_alias(checker: &mut Checker, target: &Expr) {
|
||||
if let ExprKind::Name { id, .. } = target.node() {
|
||||
if !is_snake_case_type_alias(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
SnakeCaseTypeAlias {
|
||||
name: id.to_string(),
|
||||
},
|
||||
target.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI043
|
||||
pub fn t_suffixed_type_alias(checker: &mut Checker, target: &Expr) {
|
||||
if let ExprKind::Name { id, .. } = target.node() {
|
||||
if !is_t_suffixed_type_alias(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
TSuffixedTypeAlias {
|
||||
name: id.to_string(),
|
||||
},
|
||||
target.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI020.pyi:7:10: PYI020 [*] Quoted annotations should not be included in stubs
|
||||
|
|
||||
7 | import typing_extensions
|
||||
8 |
|
||||
9 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
|
||||
| ^^^^^ PYI020
|
||||
10 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
11 | _T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
|
||||
|
|
||||
= help: Remove quotes
|
||||
|
||||
ℹ Suggested fix
|
||||
4 4 |
|
||||
5 5 | import typing_extensions
|
||||
6 6 |
|
||||
7 |-def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
|
||||
7 |+def f(x: int): ... # Y020 Quoted annotations should never be used in stubs
|
||||
8 8 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
9 9 | _T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
|
||||
10 10 |
|
||||
|
||||
PYI020.pyi:8:15: PYI020 [*] Quoted annotations should not be included in stubs
|
||||
|
|
||||
8 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
|
||||
9 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
| ^^^^^ PYI020
|
||||
10 | _T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
|
||||
|
|
||||
= help: Remove quotes
|
||||
|
||||
ℹ Suggested fix
|
||||
5 5 | import typing_extensions
|
||||
6 6 |
|
||||
7 7 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
|
||||
8 |-def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
8 |+def g(x: list[int]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
9 9 | _T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
|
||||
10 10 |
|
||||
11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
|
||||
|
||||
PYI020.pyi:9:26: PYI020 [*] Quoted annotations should not be included in stubs
|
||||
|
|
||||
9 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
|
||||
10 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
11 | _T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
|
||||
| ^^^^^ PYI020
|
||||
12 |
|
||||
13 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
|
||||
|
|
||||
= help: Remove quotes
|
||||
|
||||
ℹ Suggested fix
|
||||
6 6 |
|
||||
7 7 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
|
||||
8 8 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
9 |-_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
|
||||
9 |+_T = TypeVar("_T", bound=int) # Y020 Quoted annotations should never be used in stubs
|
||||
10 10 |
|
||||
11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
|
||||
12 12 |
|
||||
|
||||
PYI020.pyi:13:12: PYI020 [*] Quoted annotations should not be included in stubs
|
||||
|
|
||||
13 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
|
||||
14 |
|
||||
15 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
|
||||
| ^^^^^ PYI020
|
||||
16 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
|
||||
|
|
||||
= help: Remove quotes
|
||||
|
||||
ℹ Suggested fix
|
||||
10 10 |
|
||||
11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
|
||||
12 12 |
|
||||
13 |-def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
|
||||
13 |+def j() -> int: ... # Y020 Quoted annotations should never be used in stubs
|
||||
14 14 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
|
||||
15 15 |
|
||||
16 16 | class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
PYI020.pyi:14:25: PYI020 [*] Quoted annotations should not be included in stubs
|
||||
|
|
||||
14 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
|
||||
15 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
|
||||
| ^^^^^ PYI020
|
||||
16 |
|
||||
17 | class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
|
||||
|
|
||||
= help: Remove quotes
|
||||
|
||||
ℹ Suggested fix
|
||||
11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
|
||||
12 12 |
|
||||
13 13 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
|
||||
14 |-Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
|
||||
14 |+Alias: TypeAlias = list[int] # Y020 Quoted annotations should never be used in stubs
|
||||
15 15 |
|
||||
16 16 | class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
|
||||
17 17 | """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
|
||||
|
||||
PYI020.pyi:16:18: PYI020 [*] Quoted annotations should not be included in stubs
|
||||
|
|
||||
16 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
|
||||
17 |
|
||||
18 | class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
|
||||
| ^^^^^ PYI020
|
||||
19 | """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
|
||||
|
|
||||
= help: Remove quotes
|
||||
|
||||
ℹ Suggested fix
|
||||
13 13 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
|
||||
14 14 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
|
||||
15 15 |
|
||||
16 |-class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
|
||||
16 |+class Child(list[int]): # Y020 Quoted annotations should never be used in stubs
|
||||
17 17 | """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
|
||||
18 18 |
|
||||
19 19 | if sys.platform == "linux":
|
||||
|
||||
PYI020.pyi:20:8: PYI020 [*] Quoted annotations should not be included in stubs
|
||||
|
|
||||
20 | if sys.platform == "linux":
|
||||
21 | f: "int" # Y020 Quoted annotations should never be used in stubs
|
||||
| ^^^^^ PYI020
|
||||
22 | elif sys.platform == "win32":
|
||||
23 | f: "str" # Y020 Quoted annotations should never be used in stubs
|
||||
|
|
||||
= help: Remove quotes
|
||||
|
||||
ℹ Suggested fix
|
||||
17 17 | """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
|
||||
18 18 |
|
||||
19 19 | if sys.platform == "linux":
|
||||
20 |- f: "int" # Y020 Quoted annotations should never be used in stubs
|
||||
20 |+ f: int # Y020 Quoted annotations should never be used in stubs
|
||||
21 21 | elif sys.platform == "win32":
|
||||
22 22 | f: "str" # Y020 Quoted annotations should never be used in stubs
|
||||
23 23 | else:
|
||||
|
||||
PYI020.pyi:22:8: PYI020 [*] Quoted annotations should not be included in stubs
|
||||
|
|
||||
22 | f: "int" # Y020 Quoted annotations should never be used in stubs
|
||||
23 | elif sys.platform == "win32":
|
||||
24 | f: "str" # Y020 Quoted annotations should never be used in stubs
|
||||
| ^^^^^ PYI020
|
||||
25 | else:
|
||||
26 | f: "bytes" # Y020 Quoted annotations should never be used in stubs
|
||||
|
|
||||
= help: Remove quotes
|
||||
|
||||
ℹ Suggested fix
|
||||
19 19 | if sys.platform == "linux":
|
||||
20 20 | f: "int" # Y020 Quoted annotations should never be used in stubs
|
||||
21 21 | elif sys.platform == "win32":
|
||||
22 |- f: "str" # Y020 Quoted annotations should never be used in stubs
|
||||
22 |+ f: str # Y020 Quoted annotations should never be used in stubs
|
||||
23 23 | else:
|
||||
24 24 | f: "bytes" # Y020 Quoted annotations should never be used in stubs
|
||||
25 25 |
|
||||
|
||||
PYI020.pyi:24:8: PYI020 [*] Quoted annotations should not be included in stubs
|
||||
|
|
||||
24 | f: "str" # Y020 Quoted annotations should never be used in stubs
|
||||
25 | else:
|
||||
26 | f: "bytes" # Y020 Quoted annotations should never be used in stubs
|
||||
| ^^^^^^^ PYI020
|
||||
27 |
|
||||
28 | # These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations"
|
||||
|
|
||||
= help: Remove quotes
|
||||
|
||||
ℹ Suggested fix
|
||||
21 21 | elif sys.platform == "win32":
|
||||
22 22 | f: "str" # Y020 Quoted annotations should never be used in stubs
|
||||
23 23 | else:
|
||||
24 |- f: "bytes" # Y020 Quoted annotations should never be used in stubs
|
||||
24 |+ f: bytes # Y020 Quoted annotations should never be used in stubs
|
||||
25 25 |
|
||||
26 26 | # These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations"
|
||||
27 27 | k = "" # Y052 Need type annotation for "k"
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI042.pyi:10:1: PYI042 Type alias `just_literals_pipe_union` should be CamelCase
|
||||
|
|
||||
10 | )
|
||||
11 |
|
||||
12 | just_literals_pipe_union: TypeAlias = (
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ PYI042
|
||||
13 | Literal[True] | Literal["idk"]
|
||||
14 | ) # PYI042, since not camel case
|
||||
|
|
||||
|
||||
PYI042.pyi:19:1: PYI042 Type alias `snake_case_alias1` should be CamelCase
|
||||
|
|
||||
19 | _PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
20 |
|
||||
21 | snake_case_alias1: TypeAlias = str | int # PYI042, since not camel case
|
||||
| ^^^^^^^^^^^^^^^^^ PYI042
|
||||
22 | _snake_case_alias2: TypeAlias = Literal["whatever"] # PYI042, since not camel case
|
||||
23 | Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
||||
|
|
||||
|
||||
PYI042.pyi:20:1: PYI042 Type alias `_snake_case_alias2` should be CamelCase
|
||||
|
|
||||
20 | snake_case_alias1: TypeAlias = str | int # PYI042, since not camel case
|
||||
21 | _snake_case_alias2: TypeAlias = Literal["whatever"] # PYI042, since not camel case
|
||||
| ^^^^^^^^^^^^^^^^^^ PYI042
|
||||
22 | Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI043.pyi:10:1: PYI043 Private type alias `_PrivateAliasT` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)
|
||||
|
|
||||
10 | )
|
||||
11 |
|
||||
12 | _PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
|
||||
| ^^^^^^^^^^^^^^ PYI043
|
||||
13 | _PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
|
||||
14 | _PrivateAliasT3: TypeAlias = Literal[
|
||||
|
|
||||
|
||||
PYI043.pyi:11:1: PYI043 Private type alias `_PrivateAliasT2` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)
|
||||
|
|
||||
11 | _PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
|
||||
12 | _PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
|
||||
| ^^^^^^^^^^^^^^^ PYI043
|
||||
13 | _PrivateAliasT3: TypeAlias = Literal[
|
||||
14 | "not", "a", "chance"
|
||||
|
|
||||
|
||||
PYI043.pyi:12:1: PYI043 Private type alias `_PrivateAliasT3` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)
|
||||
|
|
||||
12 | _PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
|
||||
13 | _PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
|
||||
14 | _PrivateAliasT3: TypeAlias = Literal[
|
||||
| ^^^^^^^^^^^^^^^ PYI043
|
||||
15 | "not", "a", "chance"
|
||||
16 | ] # PYI043, since this ends in a T
|
||||
|
|
||||
|
||||
|
||||
@@ -150,16 +150,12 @@ impl AlwaysAutofixableViolation for ImplicitReturn {
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// bar = 1
|
||||
/// # some code that not using `bar`
|
||||
/// print('test')
|
||||
/// return bar
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// # some code that not using `bar`
|
||||
/// print('test')
|
||||
/// return 1
|
||||
/// ```
|
||||
#[violation]
|
||||
|
||||
@@ -17,6 +17,8 @@ mod tests {
|
||||
#[test_case(Rule::CollapsibleIf, Path::new("SIM102.py"); "SIM102")]
|
||||
#[test_case(Rule::NeedlessBool, Path::new("SIM103.py"); "SIM103")]
|
||||
#[test_case(Rule::SuppressibleException, Path::new("SIM105.py"); "SIM105")]
|
||||
#[test_case(Rule::SuppressibleException, Path::new("SIM105_1.py"); "SIM105_1")]
|
||||
#[test_case(Rule::SuppressibleException, Path::new("SIM105_2.py"); "SIM105_2")]
|
||||
#[test_case(Rule::ReturnInTryExceptFinally, Path::new("SIM107.py"); "SIM107")]
|
||||
#[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"); "SIM108")]
|
||||
#[test_case(Rule::CompareWithTuple, Path::new("SIM109.py"); "SIM109")]
|
||||
|
||||
@@ -33,7 +33,7 @@ fn match_async_exit_stack(checker: &Checker) -> bool {
|
||||
if attr != "enter_async_context" {
|
||||
return false;
|
||||
}
|
||||
for parent in &checker.ctx.parents {
|
||||
for parent in checker.ctx.parents() {
|
||||
if let StmtKind::With { items, .. } = &parent.node {
|
||||
for item in items {
|
||||
if let ExprKind::Call { func, .. } = &item.context_expr.node {
|
||||
@@ -68,7 +68,7 @@ fn match_exit_stack(checker: &Checker) -> bool {
|
||||
if attr != "enter_context" {
|
||||
return false;
|
||||
}
|
||||
for parent in &checker.ctx.parents {
|
||||
for parent in checker.ctx.parents() {
|
||||
if let StmtKind::With { items, .. } = &parent.node {
|
||||
for item in items {
|
||||
if let ExprKind::Call { func, .. } = &item.context_expr.node {
|
||||
|
||||
@@ -94,6 +94,7 @@ pub fn suppressible_exception(
|
||||
let (import_edit, binding) = get_or_import_symbol(
|
||||
"contextlib",
|
||||
"suppress",
|
||||
stmt.start(),
|
||||
&checker.ctx,
|
||||
&checker.importer,
|
||||
checker.locator,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM105_1.py:4:1: SIM105 [*] Use `contextlib.suppress(ValueError)` instead of `try`-`except`-`pass`
|
||||
|
|
||||
4 | import math
|
||||
5 |
|
||||
6 | / try:
|
||||
7 | | math.sqrt(-1)
|
||||
8 | | except ValueError: # SIM105
|
||||
9 | | pass
|
||||
| |________^ SIM105
|
||||
|
|
||||
= help: Replace with `contextlib.suppress(ValueError)`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | """Case: There's a random import, so it should add `contextlib` after it."""
|
||||
2 2 | import math
|
||||
3 |+import contextlib
|
||||
3 4 |
|
||||
4 |-try:
|
||||
5 |+with contextlib.suppress(ValueError):
|
||||
5 6 | math.sqrt(-1)
|
||||
6 |-except ValueError: # SIM105
|
||||
7 |- pass
|
||||
7 |+
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM105_2.py:9:1: SIM105 [*] Use `contextlib.suppress(ValueError)` instead of `try`-`except`-`pass`
|
||||
|
|
||||
9 | / try:
|
||||
10 | | foo()
|
||||
11 | | except ValueError:
|
||||
12 | | pass
|
||||
| |________^ SIM105
|
||||
|
|
||||
= help: Replace with `contextlib.suppress(ValueError)`
|
||||
|
||||
ℹ Suggested fix
|
||||
6 6 | pass
|
||||
7 7 |
|
||||
8 8 |
|
||||
9 |-try:
|
||||
9 |+with contextlib.suppress(ValueError):
|
||||
10 10 | foo()
|
||||
11 |-except ValueError:
|
||||
12 |- pass
|
||||
11 |+
|
||||
|
||||
|
||||
@@ -60,11 +60,7 @@ pub fn empty_type_checking_block<'a, 'b>(
|
||||
|
||||
// Delete the entire type-checking block.
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let parent = checker
|
||||
.ctx
|
||||
.child_to_parent
|
||||
.get(&RefEquality(stmt))
|
||||
.map(Into::into);
|
||||
let parent = checker.ctx.stmts.parent(stmt);
|
||||
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
|
||||
match delete_stmt(
|
||||
stmt,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::binding::{
|
||||
Binding, BindingKind, ExecutionContext, FromImportation, Importation, SubmoduleImportation,
|
||||
};
|
||||
use ruff_python_semantic::binding::{Binding, BindingKind, ExecutionContext};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for runtime imports defined in a type-checking block.
|
||||
@@ -51,9 +49,9 @@ impl Violation for RuntimeImportInTypeCheckingBlock {
|
||||
/// TCH004
|
||||
pub fn runtime_import_in_type_checking_block(binding: &Binding) -> Option<Diagnostic> {
|
||||
let full_name = match &binding.kind {
|
||||
BindingKind::Importation(Importation { full_name, .. }) => full_name,
|
||||
BindingKind::FromImportation(FromImportation { full_name, .. }) => full_name.as_str(),
|
||||
BindingKind::SubmoduleImportation(SubmoduleImportation { full_name, .. }) => full_name,
|
||||
BindingKind::Importation(import) => import.full_name,
|
||||
BindingKind::FromImportation(import) => import.full_name.as_str(),
|
||||
BindingKind::SubmoduleImportation(import) => import.full_name,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ use std::path::Path;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::binding::{
|
||||
Binding, BindingKind, ExecutionContext, FromImportation, Importation, SubmoduleImportation,
|
||||
};
|
||||
use ruff_python_semantic::binding::{Binding, BindingKind, ExecutionContext};
|
||||
|
||||
use crate::rules::isort::{categorize, ImportSection, ImportType};
|
||||
use crate::settings::Settings;
|
||||
@@ -160,67 +158,68 @@ impl Violation for TypingOnlyStandardLibraryImport {
|
||||
}
|
||||
|
||||
/// Return `true` if `this` is implicitly loaded via importing `that`.
|
||||
fn is_implicit_import(this: &Binding, that: &Binding) -> bool {
|
||||
match &this.kind {
|
||||
BindingKind::Importation(Importation {
|
||||
full_name: this_name,
|
||||
..
|
||||
})
|
||||
| BindingKind::SubmoduleImportation(SubmoduleImportation {
|
||||
name: this_name, ..
|
||||
}) => match &that.kind {
|
||||
BindingKind::FromImportation(FromImportation {
|
||||
full_name: that_name,
|
||||
..
|
||||
}) => {
|
||||
// Ex) `pkg.A` vs. `pkg`
|
||||
this_name
|
||||
.rfind('.')
|
||||
.map_or(false, |i| this_name[..i] == *that_name)
|
||||
}
|
||||
BindingKind::Importation(Importation {
|
||||
full_name: that_name,
|
||||
..
|
||||
})
|
||||
| BindingKind::SubmoduleImportation(SubmoduleImportation {
|
||||
name: that_name, ..
|
||||
}) => {
|
||||
// Ex) `pkg.A` vs. `pkg.B`
|
||||
this_name == that_name
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
BindingKind::FromImportation(FromImportation {
|
||||
full_name: this_name,
|
||||
..
|
||||
}) => match &that.kind {
|
||||
BindingKind::Importation(Importation {
|
||||
full_name: that_name,
|
||||
..
|
||||
})
|
||||
| BindingKind::SubmoduleImportation(SubmoduleImportation {
|
||||
name: that_name, ..
|
||||
}) => {
|
||||
// Ex) `pkg.A` vs. `pkg`
|
||||
this_name
|
||||
.rfind('.')
|
||||
.map_or(false, |i| &this_name[..i] == *that_name)
|
||||
}
|
||||
BindingKind::FromImportation(FromImportation {
|
||||
full_name: that_name,
|
||||
..
|
||||
}) => {
|
||||
// Ex) `pkg.A` vs. `pkg.B`
|
||||
this_name.rfind('.').map_or(false, |i| {
|
||||
that_name
|
||||
.rfind('.')
|
||||
.map_or(false, |j| this_name[..i] == that_name[..j])
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
fn is_implicit_import(_this: &Binding, _that: &Binding) -> bool {
|
||||
true
|
||||
// match &this.kind {
|
||||
// BindingKind::Importation(Importation {
|
||||
// full_name: this_name,
|
||||
// ..
|
||||
// })
|
||||
// | BindingKind::SubmoduleImportation(SubmoduleImportation {
|
||||
// name: this_name, ..
|
||||
// }) => match &that.kind {
|
||||
// BindingKind::FromImportation(FromImportation {
|
||||
// full_name: that_name,
|
||||
// ..
|
||||
// }) => {
|
||||
// // Ex) `pkg.A` vs. `pkg`
|
||||
// this_name
|
||||
// .rfind('.')
|
||||
// .map_or(false, |i| this_name[..i] == *that_name)
|
||||
// }
|
||||
// BindingKind::Importation(Importation {
|
||||
// full_name: that_name,
|
||||
// ..
|
||||
// })
|
||||
// | BindingKind::SubmoduleImportation(SubmoduleImportation {
|
||||
// name: that_name, ..
|
||||
// }) => {
|
||||
// // Ex) `pkg.A` vs. `pkg.B`
|
||||
// this_name == that_name
|
||||
// }
|
||||
// _ => false,
|
||||
// },
|
||||
// BindingKind::FromImportation(FromImportation {
|
||||
// full_name: this_name,
|
||||
// ..
|
||||
// }) => match &that.kind {
|
||||
// BindingKind::Importation(Importation {
|
||||
// full_name: that_name,
|
||||
// ..
|
||||
// })
|
||||
// | BindingKind::SubmoduleImportation(SubmoduleImportation {
|
||||
// name: that_name, ..
|
||||
// }) => {
|
||||
// // Ex) `pkg.A` vs. `pkg`
|
||||
// this_name
|
||||
// .rfind('.')
|
||||
// .map_or(false, |i| &this_name[..i] == *that_name)
|
||||
// }
|
||||
// BindingKind::FromImportation(FromImportation {
|
||||
// full_name: that_name,
|
||||
// ..
|
||||
// }) => {
|
||||
// // Ex) `pkg.A` vs. `pkg.B`
|
||||
// this_name.rfind('.').map_or(false, |i| {
|
||||
// that_name
|
||||
// .rfind('.')
|
||||
// .map_or(false, |j| this_name[..i] == that_name[..j])
|
||||
// })
|
||||
// }
|
||||
// _ => false,
|
||||
// },
|
||||
// _ => false,
|
||||
// }
|
||||
}
|
||||
|
||||
/// Return `true` if `name` is exempt from typing-only enforcement.
|
||||
@@ -257,9 +256,9 @@ pub fn typing_only_runtime_import(
|
||||
}
|
||||
|
||||
let full_name = match &binding.kind {
|
||||
BindingKind::Importation(Importation { full_name, .. }) => full_name,
|
||||
BindingKind::FromImportation(FromImportation { full_name, .. }) => full_name.as_str(),
|
||||
BindingKind::SubmoduleImportation(SubmoduleImportation { full_name, .. }) => full_name,
|
||||
BindingKind::Importation(import) => import.full_name,
|
||||
BindingKind::FromImportation(import) => import.full_name.as_str(),
|
||||
BindingKind::SubmoduleImportation(import) => import.full_name,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use log::error;
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustpython_parser as parser;
|
||||
use rustpython_parser::ast::{StmtKind, Suite};
|
||||
|
||||
@@ -120,7 +120,10 @@ fn add_required_import(
|
||||
TextRange::default(),
|
||||
);
|
||||
if autofix.into() && settings.rules.should_fix(Rule::MissingRequiredImport) {
|
||||
diagnostic.set_fix(Importer::new(python_ast, locator, stylist).add_import(required_import));
|
||||
diagnostic.set_fix(
|
||||
Importer::new(python_ast, locator, stylist)
|
||||
.add_import(required_import, TextSize::default()),
|
||||
);
|
||||
}
|
||||
Some(diagnostic)
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ pub struct Options {
|
||||
/// in the order specified.
|
||||
pub forced_separate: Option<Vec<String>>,
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
default = r#"["future", "standard-library", "third-party", "first-party", "local-folder"]"#,
|
||||
value_type = r#"list["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str]"#,
|
||||
example = r#"
|
||||
section-order = ["future", "standard-library", "first-party", "local-folder", "third-party"]
|
||||
|
||||
@@ -3,7 +3,7 @@ use rustpython_parser::ast::{Expr, ExprKind};
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::binding::{BindingKind, Importation};
|
||||
use ruff_python_semantic::binding::BindingKind;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
@@ -85,11 +85,8 @@ pub fn check_call(checker: &mut Checker, func: &Expr) {
|
||||
// irrelevant bindings (like non-Pandas imports).
|
||||
if let ExprKind::Name { id, .. } = &value.node {
|
||||
if checker.ctx.find_binding(id).map_or(true, |binding| {
|
||||
if let BindingKind::Importation(Importation {
|
||||
full_name: module, ..
|
||||
}) = &binding.kind
|
||||
{
|
||||
module != &"pandas"
|
||||
if let BindingKind::Importation(import) = &binding.kind {
|
||||
import.full_name != "pandas"
|
||||
} else {
|
||||
matches!(
|
||||
binding.kind,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use itertools::Itertools;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::checkers::logical_lines::LogicalLinesContext;
|
||||
use crate::rules::pycodestyle::rules::logical_lines::LogicalLine;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::token_kind::TokenKind;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
#[violation]
|
||||
pub struct MissingWhitespaceAfterKeyword;
|
||||
@@ -22,7 +20,10 @@ pub(crate) fn missing_whitespace_after_keyword(
|
||||
line: &LogicalLine,
|
||||
context: &mut LogicalLinesContext,
|
||||
) {
|
||||
for (tok0, tok1) in line.tokens().iter().tuple_windows() {
|
||||
for window in line.tokens().windows(2) {
|
||||
let tok0 = &window[0];
|
||||
let tok1 = &window[1];
|
||||
|
||||
let tok0_kind = tok0.kind();
|
||||
let tok1_kind = tok1.kind();
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::checkers::logical_lines::LogicalLinesContext;
|
||||
use ruff_diagnostics::Violation;
|
||||
use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine, LogicalLineToken};
|
||||
use ruff_diagnostics::{DiagnosticKind, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::token_kind::TokenKind;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::rules::pycodestyle::rules::logical_lines::LogicalLine;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
// E225
|
||||
#[violation]
|
||||
@@ -56,131 +55,179 @@ pub(crate) fn missing_whitespace_around_operator(
|
||||
line: &LogicalLine,
|
||||
context: &mut LogicalLinesContext,
|
||||
) {
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
enum NeedsSpace {
|
||||
Yes,
|
||||
No,
|
||||
Unset,
|
||||
}
|
||||
|
||||
let mut needs_space_main = NeedsSpace::No;
|
||||
let mut needs_space_aux = NeedsSpace::Unset;
|
||||
let mut prev_end_aux = TextSize::default();
|
||||
let mut parens = 0u32;
|
||||
let mut prev_type: TokenKind = TokenKind::EndOfFile;
|
||||
let mut prev_end = TextSize::default();
|
||||
let mut prev_token: Option<&LogicalLineToken> = None;
|
||||
let mut tokens = line.tokens().iter().peekable();
|
||||
|
||||
for token in line.tokens() {
|
||||
while let Some(token) = tokens.next() {
|
||||
let kind = token.kind();
|
||||
|
||||
if kind.is_skip_comment() {
|
||||
if kind.is_trivia() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match kind {
|
||||
TokenKind::Lpar | TokenKind::Lambda => parens += 1,
|
||||
TokenKind::Rpar => parens -= 1,
|
||||
TokenKind::Rpar => parens = parens.saturating_sub(1),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let needs_space = needs_space_main == NeedsSpace::Yes
|
||||
|| needs_space_aux != NeedsSpace::Unset
|
||||
|| prev_end_aux != TextSize::new(0);
|
||||
if needs_space {
|
||||
if token.start() > prev_end {
|
||||
if needs_space_main != NeedsSpace::Yes && needs_space_aux != NeedsSpace::Yes {
|
||||
let needs_space = if kind == TokenKind::Equal && parens > 0 {
|
||||
// Allow keyword args or defaults: foo(bar=None).
|
||||
NeedsSpace::No
|
||||
} else if kind == TokenKind::Slash {
|
||||
// Tolerate the "/" operator in function definition
|
||||
// For more info see PEP570
|
||||
|
||||
// `def f(a, /, b):` or `def f(a, b, /):` or `f = lambda a, /:`
|
||||
// ^ ^ ^
|
||||
let slash_in_func = matches!(
|
||||
tokens.peek().map(|t| t.kind()),
|
||||
Some(TokenKind::Comma | TokenKind::Rpar | TokenKind::Colon)
|
||||
);
|
||||
|
||||
NeedsSpace::from(!slash_in_func)
|
||||
} else if kind.is_unary() || kind == TokenKind::DoubleStar {
|
||||
let is_binary = prev_token.map_or(false, |prev_token| {
|
||||
let prev_kind = prev_token.kind();
|
||||
|
||||
// Check if the operator is used as a binary operator.
|
||||
// Allow unary operators: -123, -x, +1.
|
||||
// Allow argument unpacking: foo(*args, **kwargs)
|
||||
matches!(
|
||||
prev_kind,
|
||||
TokenKind::Rpar | TokenKind::Rsqb | TokenKind::Rbrace
|
||||
) || !(prev_kind.is_operator()
|
||||
|| prev_kind.is_keyword()
|
||||
|| prev_kind.is_soft_keyword())
|
||||
});
|
||||
|
||||
if is_binary {
|
||||
if kind == TokenKind::DoubleStar {
|
||||
// Enforce consistent spacing, but don't enforce whitespaces.
|
||||
NeedsSpace::Optional
|
||||
} else {
|
||||
NeedsSpace::Yes
|
||||
}
|
||||
} else {
|
||||
NeedsSpace::No
|
||||
}
|
||||
} else if is_whitespace_needed(kind) {
|
||||
NeedsSpace::Yes
|
||||
} else {
|
||||
NeedsSpace::No
|
||||
};
|
||||
|
||||
if needs_space != NeedsSpace::No {
|
||||
let has_leading_trivia = prev_token.map_or(true, |prev| {
|
||||
prev.end() < token.start() || prev.kind().is_trivia()
|
||||
});
|
||||
|
||||
let has_trailing_trivia = tokens.peek().map_or(true, |next| {
|
||||
token.end() < next.start() || next.kind().is_trivia()
|
||||
});
|
||||
|
||||
match (has_leading_trivia, has_trailing_trivia) {
|
||||
// Operator with trailing but no leading space, enforce consistent spacing
|
||||
(false, true) => {
|
||||
context.push(
|
||||
MissingWhitespaceAroundOperator,
|
||||
TextRange::empty(prev_end_aux),
|
||||
TextRange::empty(token.start()),
|
||||
);
|
||||
}
|
||||
needs_space_main = NeedsSpace::No;
|
||||
needs_space_aux = NeedsSpace::Unset;
|
||||
prev_end_aux = TextSize::new(0);
|
||||
} else if kind == TokenKind::Greater
|
||||
&& matches!(prev_type, TokenKind::Less | TokenKind::Minus)
|
||||
{
|
||||
// Tolerate the "<>" operator, even if running Python 3
|
||||
// Deal with Python 3's annotated return value "->"
|
||||
} else if prev_type == TokenKind::Slash
|
||||
&& matches!(kind, TokenKind::Comma | TokenKind::Rpar | TokenKind::Colon)
|
||||
|| (prev_type == TokenKind::Rpar && kind == TokenKind::Colon)
|
||||
{
|
||||
// Tolerate the "/" operator in function definition
|
||||
// For more info see PEP570
|
||||
} else {
|
||||
if needs_space_main == NeedsSpace::Yes || needs_space_aux == NeedsSpace::Yes {
|
||||
context.push(MissingWhitespaceAroundOperator, TextRange::empty(prev_end));
|
||||
} else if prev_type != TokenKind::DoubleStar {
|
||||
if prev_type == TokenKind::Percent {
|
||||
// Operator with leading but no trailing space, enforce consistent spacing.
|
||||
(true, false) => {
|
||||
context.push(
|
||||
MissingWhitespaceAroundOperator,
|
||||
TextRange::empty(token.end()),
|
||||
);
|
||||
}
|
||||
// Operator with no space, require spaces if it is required by the operator.
|
||||
(false, false) => {
|
||||
if needs_space == NeedsSpace::Yes {
|
||||
context.push(
|
||||
MissingWhitespaceAroundModuloOperator,
|
||||
TextRange::empty(prev_end_aux),
|
||||
);
|
||||
} else if !prev_type.is_arithmetic() {
|
||||
context.push(
|
||||
MissingWhitespaceAroundBitwiseOrShiftOperator,
|
||||
TextRange::empty(prev_end_aux),
|
||||
);
|
||||
} else {
|
||||
context.push(
|
||||
MissingWhitespaceAroundArithmeticOperator,
|
||||
TextRange::empty(prev_end_aux),
|
||||
diagnostic_kind_for_operator(kind),
|
||||
TextRange::empty(token.start()),
|
||||
);
|
||||
}
|
||||
}
|
||||
needs_space_main = NeedsSpace::No;
|
||||
needs_space_aux = NeedsSpace::Unset;
|
||||
prev_end_aux = TextSize::new(0);
|
||||
}
|
||||
} else if (kind.is_operator() || matches!(kind, TokenKind::Name))
|
||||
&& prev_end != TextSize::default()
|
||||
{
|
||||
if kind == TokenKind::Equal && parens > 0 {
|
||||
// Allow keyword args or defaults: foo(bar=None).
|
||||
} else if kind.is_whitespace_needed() {
|
||||
needs_space_main = NeedsSpace::Yes;
|
||||
needs_space_aux = NeedsSpace::Unset;
|
||||
prev_end_aux = TextSize::new(0);
|
||||
} else if kind.is_unary() {
|
||||
// Check if the operator is used as a binary operator
|
||||
// Allow unary operators: -123, -x, +1.
|
||||
// Allow argument unpacking: foo(*args, **kwargs)
|
||||
if (matches!(
|
||||
prev_type,
|
||||
TokenKind::Rpar | TokenKind::Rsqb | TokenKind::Rbrace
|
||||
)) || (!prev_type.is_operator()
|
||||
&& !prev_type.is_keyword()
|
||||
&& !prev_type.is_soft_keyword())
|
||||
{
|
||||
needs_space_main = NeedsSpace::Unset;
|
||||
needs_space_aux = NeedsSpace::Unset;
|
||||
prev_end_aux = TextSize::new(0);
|
||||
(true, true) => {
|
||||
// Operator has leading and trailing space, all good
|
||||
}
|
||||
} else if kind.is_whitespace_optional() {
|
||||
needs_space_main = NeedsSpace::Unset;
|
||||
needs_space_aux = NeedsSpace::Unset;
|
||||
prev_end_aux = TextSize::new(0);
|
||||
}
|
||||
|
||||
if needs_space_main == NeedsSpace::Unset {
|
||||
// Surrounding space is optional, but ensure that
|
||||
// trailing space matches opening space
|
||||
prev_end_aux = prev_end;
|
||||
needs_space_aux = if token.start() == prev_end {
|
||||
NeedsSpace::No
|
||||
} else {
|
||||
NeedsSpace::Yes
|
||||
};
|
||||
} else if needs_space_main == NeedsSpace::Yes && token.start() == prev_end_aux {
|
||||
// A needed opening space was not found
|
||||
context.push(MissingWhitespaceAroundOperator, TextRange::empty(prev_end));
|
||||
needs_space_main = NeedsSpace::No;
|
||||
needs_space_aux = NeedsSpace::Unset;
|
||||
prev_end_aux = TextSize::new(0);
|
||||
}
|
||||
}
|
||||
prev_type = kind;
|
||||
prev_end = token.end();
|
||||
|
||||
prev_token = Some(token);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
enum NeedsSpace {
|
||||
/// Needs a leading and trailing space.
|
||||
Yes,
|
||||
|
||||
/// Doesn't need a leading or trailing space. Or in other words, we don't care how many
|
||||
/// leading or trailing spaces that token has.
|
||||
No,
|
||||
|
||||
/// Needs consistent leading and trailing spacing. The operator needs spacing if
|
||||
/// * it has a leading space
|
||||
/// * it has a trailing space
|
||||
Optional,
|
||||
}
|
||||
|
||||
impl From<bool> for NeedsSpace {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
NeedsSpace::Yes
|
||||
} else {
|
||||
NeedsSpace::No
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostic_kind_for_operator(operator: TokenKind) -> DiagnosticKind {
|
||||
if operator == TokenKind::Percent {
|
||||
DiagnosticKind::from(MissingWhitespaceAroundModuloOperator)
|
||||
} else if operator.is_bitwise_or_shift() {
|
||||
DiagnosticKind::from(MissingWhitespaceAroundBitwiseOrShiftOperator)
|
||||
} else if operator.is_arithmetic() {
|
||||
DiagnosticKind::from(MissingWhitespaceAroundArithmeticOperator)
|
||||
} else {
|
||||
DiagnosticKind::from(MissingWhitespaceAroundOperator)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_whitespace_needed(kind: TokenKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
TokenKind::DoubleStarEqual
|
||||
| TokenKind::StarEqual
|
||||
| TokenKind::SlashEqual
|
||||
| TokenKind::DoubleSlashEqual
|
||||
| TokenKind::PlusEqual
|
||||
| TokenKind::MinusEqual
|
||||
| TokenKind::NotEqual
|
||||
| TokenKind::Less
|
||||
| TokenKind::Greater
|
||||
| TokenKind::PercentEqual
|
||||
| TokenKind::CircumflexEqual
|
||||
| TokenKind::AmperEqual
|
||||
| TokenKind::VbarEqual
|
||||
| TokenKind::EqEqual
|
||||
| TokenKind::LessEqual
|
||||
| TokenKind::GreaterEqual
|
||||
| TokenKind::LeftShiftEqual
|
||||
| TokenKind::RightShiftEqual
|
||||
| TokenKind::Equal
|
||||
| TokenKind::And
|
||||
| TokenKind::Or
|
||||
| TokenKind::In
|
||||
| TokenKind::Is
|
||||
| TokenKind::Rarrow
|
||||
| TokenKind::ColonEqual
|
||||
| TokenKind::Slash
|
||||
| TokenKind::Percent
|
||||
) || kind.is_arithmetic()
|
||||
|| kind.is_bitwise_or_shift()
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ E101.py:11:1: E101 Indentation contains mixed spaces and tabs
|
||||
|
|
||||
11 | def func_mixed_start_with_tab():
|
||||
12 | # E101
|
||||
13 | print("mixed starts with tab")
|
||||
| ^^ E101
|
||||
13 | print("mixed starts with tab")
|
||||
| ^^^^^^ E101
|
||||
14 |
|
||||
15 | def func_mixed_start_with_space():
|
||||
|
|
||||
@@ -15,8 +15,8 @@ E101.py:15:1: E101 Indentation contains mixed spaces and tabs
|
||||
|
|
||||
15 | def func_mixed_start_with_space():
|
||||
16 | # E101
|
||||
17 | print("mixed starts with space")
|
||||
| ^^^^^^^^ E101
|
||||
17 | print("mixed starts with space")
|
||||
| ^^^^^^^^^^^^^^^^^^^^ E101
|
||||
18 |
|
||||
19 | def xyz():
|
||||
|
|
||||
@@ -25,8 +25,8 @@ E101.py:19:1: E101 Indentation contains mixed spaces and tabs
|
||||
|
|
||||
19 | def xyz():
|
||||
20 | # E101
|
||||
21 | print("xyz");
|
||||
| ^^^ E101
|
||||
21 | print("xyz");
|
||||
| ^^^^^^^ E101
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ E11.py:42:1: E117 Over-indented
|
||||
|
|
||||
42 | #: E117 W191
|
||||
43 | def start():
|
||||
44 | print()
|
||||
| E117
|
||||
44 | print()
|
||||
| ^^^^^^^^ E117
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -27,34 +27,34 @@ E20.py:6:15: E201 Whitespace after '('
|
||||
8 | spam(ham[1], { eggs: 2})
|
||||
| E201
|
||||
9 | #: E201:1:6
|
||||
10 | spam( ham[1], {eggs: 2})
|
||||
10 | spam( ham[1], {eggs: 2})
|
||||
|
|
||||
|
||||
E20.py:8:6: E201 Whitespace after '('
|
||||
|
|
||||
8 | spam(ham[1], { eggs: 2})
|
||||
9 | #: E201:1:6
|
||||
10 | spam( ham[1], {eggs: 2})
|
||||
10 | spam( ham[1], {eggs: 2})
|
||||
| E201
|
||||
11 | #: E201:1:10
|
||||
12 | spam(ham[ 1], {eggs: 2})
|
||||
12 | spam(ham[ 1], {eggs: 2})
|
||||
|
|
||||
|
||||
E20.py:10:10: E201 Whitespace after '('
|
||||
|
|
||||
10 | spam( ham[1], {eggs: 2})
|
||||
10 | spam( ham[1], {eggs: 2})
|
||||
11 | #: E201:1:10
|
||||
12 | spam(ham[ 1], {eggs: 2})
|
||||
12 | spam(ham[ 1], {eggs: 2})
|
||||
| E201
|
||||
13 | #: E201:1:15
|
||||
14 | spam(ham[1], { eggs: 2})
|
||||
14 | spam(ham[1], { eggs: 2})
|
||||
|
|
||||
|
||||
E20.py:12:15: E201 Whitespace after '('
|
||||
|
|
||||
12 | spam(ham[ 1], {eggs: 2})
|
||||
12 | spam(ham[ 1], {eggs: 2})
|
||||
13 | #: E201:1:15
|
||||
14 | spam(ham[1], { eggs: 2})
|
||||
14 | spam(ham[1], { eggs: 2})
|
||||
| E201
|
||||
15 | #: Okay
|
||||
16 | spam(ham[1], {eggs: 2})
|
||||
|
||||
@@ -27,34 +27,34 @@ E20.py:23:11: E202 Whitespace before ')'
|
||||
25 | spam(ham[1 ], {eggs: 2})
|
||||
| E202
|
||||
26 | #: E202:1:23
|
||||
27 | spam(ham[1], {eggs: 2} )
|
||||
27 | spam(ham[1], {eggs: 2} )
|
||||
|
|
||||
|
||||
E20.py:25:23: E202 Whitespace before ')'
|
||||
|
|
||||
25 | spam(ham[1 ], {eggs: 2})
|
||||
26 | #: E202:1:23
|
||||
27 | spam(ham[1], {eggs: 2} )
|
||||
27 | spam(ham[1], {eggs: 2} )
|
||||
| E202
|
||||
28 | #: E202:1:22
|
||||
29 | spam(ham[1], {eggs: 2 })
|
||||
29 | spam(ham[1], {eggs: 2 })
|
||||
|
|
||||
|
||||
E20.py:27:22: E202 Whitespace before ')'
|
||||
|
|
||||
27 | spam(ham[1], {eggs: 2} )
|
||||
27 | spam(ham[1], {eggs: 2} )
|
||||
28 | #: E202:1:22
|
||||
29 | spam(ham[1], {eggs: 2 })
|
||||
29 | spam(ham[1], {eggs: 2 })
|
||||
| E202
|
||||
30 | #: E202:1:11
|
||||
31 | spam(ham[1 ], {eggs: 2})
|
||||
31 | spam(ham[1 ], {eggs: 2})
|
||||
|
|
||||
|
||||
E20.py:29:11: E202 Whitespace before ')'
|
||||
|
|
||||
29 | spam(ham[1], {eggs: 2 })
|
||||
29 | spam(ham[1], {eggs: 2 })
|
||||
30 | #: E202:1:11
|
||||
31 | spam(ham[1 ], {eggs: 2})
|
||||
31 | spam(ham[1 ], {eggs: 2})
|
||||
| E202
|
||||
32 | #: Okay
|
||||
33 | spam(ham[1], {eggs: 2})
|
||||
|
||||
@@ -14,7 +14,7 @@ E20.py:55:10: E203 Whitespace before ',', ';', or ':'
|
||||
|
|
||||
55 | x, y = y, x
|
||||
56 | #: E203:1:10
|
||||
57 | if x == 4 :
|
||||
57 | if x == 4 :
|
||||
| E203
|
||||
58 | print x, y
|
||||
59 | x, y = y, x
|
||||
@@ -34,7 +34,7 @@ E20.py:63:15: E203 Whitespace before ',', ';', or ':'
|
||||
|
|
||||
63 | #: E203:2:15 E702:2:16
|
||||
64 | if x == 4:
|
||||
65 | print x, y ; x, y = y, x
|
||||
65 | print x, y ; x, y = y, x
|
||||
| E203
|
||||
66 | #: E203:3:13
|
||||
67 | if x == 4:
|
||||
@@ -54,7 +54,7 @@ E20.py:71:13: E203 Whitespace before ',', ';', or ':'
|
||||
|
|
||||
71 | if x == 4:
|
||||
72 | print x, y
|
||||
73 | x, y = y , x
|
||||
73 | x, y = y , x
|
||||
| E203
|
||||
74 | #: Okay
|
||||
75 | if x == 4:
|
||||
|
||||
@@ -5,7 +5,7 @@ E22.py:43:2: E223 Tab before operator
|
||||
|
|
||||
43 | #: E223
|
||||
44 | foobart = 4
|
||||
45 | a = 3 # aligned with tab
|
||||
45 | a = 3 # aligned with tab
|
||||
| E223
|
||||
46 | #:
|
||||
|
|
||||
|
||||
@@ -4,7 +4,7 @@ source: crates/ruff/src/rules/pycodestyle/mod.rs
|
||||
E22.py:48:5: E224 Tab after operator
|
||||
|
|
||||
48 | #: E224
|
||||
49 | a += 1
|
||||
49 | a += 1
|
||||
| E224
|
||||
50 | b += 1000
|
||||
51 | #:
|
||||
|
||||
@@ -10,6 +10,16 @@ E22.py:54:13: E225 Missing whitespace around operator
|
||||
57 | submitted+= 1
|
||||
|
|
||||
|
||||
E22.py:56:10: E225 Missing whitespace around operator
|
||||
|
|
||||
56 | submitted +=1
|
||||
57 | #: E225
|
||||
58 | submitted+= 1
|
||||
| E225
|
||||
59 | #: E225
|
||||
60 | c =-1
|
||||
|
|
||||
|
||||
E22.py:58:4: E225 Missing whitespace around operator
|
||||
|
|
||||
58 | submitted+= 1
|
||||
@@ -100,12 +110,12 @@ E22.py:74:12: E225 Missing whitespace around operator
|
||||
78 | i=i+ 1
|
||||
|
|
||||
|
||||
E22.py:76:3: E225 Missing whitespace around operator
|
||||
E22.py:76:2: E225 Missing whitespace around operator
|
||||
|
|
||||
76 | _1kB = _1MB>> 10
|
||||
77 | #: E225 E225
|
||||
78 | i=i+ 1
|
||||
| E225
|
||||
| E225
|
||||
79 | #: E225 E225
|
||||
80 | i=i +1
|
||||
|
|
||||
@@ -120,12 +130,12 @@ E22.py:76:4: E225 Missing whitespace around operator
|
||||
80 | i=i +1
|
||||
|
|
||||
|
||||
E22.py:78:3: E225 Missing whitespace around operator
|
||||
E22.py:78:2: E225 Missing whitespace around operator
|
||||
|
|
||||
78 | i=i+ 1
|
||||
79 | #: E225 E225
|
||||
80 | i=i +1
|
||||
| E225
|
||||
| E225
|
||||
81 | #: E225
|
||||
82 | i = 1and 1
|
||||
|
|
||||
@@ -140,6 +150,46 @@ E22.py:78:6: E225 Missing whitespace around operator
|
||||
82 | i = 1and 1
|
||||
|
|
||||
|
||||
E22.py:80:6: E225 Missing whitespace around operator
|
||||
|
|
||||
80 | i=i +1
|
||||
81 | #: E225
|
||||
82 | i = 1and 1
|
||||
| E225
|
||||
83 | #: E225
|
||||
84 | i = 1or 0
|
||||
|
|
||||
|
||||
E22.py:82:6: E225 Missing whitespace around operator
|
||||
|
|
||||
82 | i = 1and 1
|
||||
83 | #: E225
|
||||
84 | i = 1or 0
|
||||
| E225
|
||||
85 | #: E225
|
||||
86 | 1is 1
|
||||
|
|
||||
|
||||
E22.py:84:2: E225 Missing whitespace around operator
|
||||
|
|
||||
84 | i = 1or 0
|
||||
85 | #: E225
|
||||
86 | 1is 1
|
||||
| E225
|
||||
87 | #: E225
|
||||
88 | 1in []
|
||||
|
|
||||
|
||||
E22.py:86:2: E225 Missing whitespace around operator
|
||||
|
|
||||
86 | 1is 1
|
||||
87 | #: E225
|
||||
88 | 1in []
|
||||
| E225
|
||||
89 | #: E225
|
||||
90 | i = 1 @2
|
||||
|
|
||||
|
||||
E22.py:88:8: E225 Missing whitespace around operator
|
||||
|
|
||||
88 | 1in []
|
||||
@@ -160,12 +210,12 @@ E22.py:90:6: E225 Missing whitespace around operator
|
||||
94 | i=i+1
|
||||
|
|
||||
|
||||
E22.py:92:3: E225 Missing whitespace around operator
|
||||
E22.py:92:2: E225 Missing whitespace around operator
|
||||
|
|
||||
92 | i = 1@ 2
|
||||
93 | #: E225 E226
|
||||
94 | i=i+1
|
||||
| E225
|
||||
| E225
|
||||
95 | #: E225 E226
|
||||
96 | i =i+1
|
||||
|
|
||||
@@ -180,6 +230,16 @@ E22.py:94:4: E225 Missing whitespace around operator
|
||||
98 | i= i+1
|
||||
|
|
||||
|
||||
E22.py:96:2: E225 Missing whitespace around operator
|
||||
|
|
||||
96 | i =i+1
|
||||
97 | #: E225 E226
|
||||
98 | i= i+1
|
||||
| E225
|
||||
99 | #: E225 E226
|
||||
100 | c = (a +b)*(a - b)
|
||||
|
|
||||
|
||||
E22.py:98:9: E225 Missing whitespace around operator
|
||||
|
|
||||
98 | i= i+1
|
||||
|
||||
@@ -50,6 +50,15 @@ E22.py:100:11: E226 Missing whitespace around arithmetic operator
|
||||
103 | #:
|
||||
|
|
||||
|
||||
E22.py:104:6: E226 Missing whitespace around arithmetic operator
|
||||
|
|
||||
104 | #: E226
|
||||
105 | z = 2//30
|
||||
| E226
|
||||
106 | #: E226 E226
|
||||
107 | c = (a+b) * (a-b)
|
||||
|
|
||||
|
||||
E22.py:106:7: E226 Missing whitespace around arithmetic operator
|
||||
|
|
||||
106 | z = 2//30
|
||||
@@ -120,4 +129,14 @@ E22.py:116:12: E226 Missing whitespace around arithmetic operator
|
||||
120 | def halves(n):
|
||||
|
|
||||
|
||||
E22.py:119:14: E226 Missing whitespace around arithmetic operator
|
||||
|
|
||||
119 | #: E226
|
||||
120 | def halves(n):
|
||||
121 | return (i//2 for i in range(n))
|
||||
| E226
|
||||
122 | #: E227
|
||||
123 | _1kB = _1MB>>10
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@ E27.py:8:3: E271 Multiple spaces after keyword
|
||||
10 | if 1:
|
||||
| E271
|
||||
11 | #: E273
|
||||
12 | True and False
|
||||
12 | True and False
|
||||
|
|
||||
|
||||
E27.py:14:6: E271 Multiple spaces after keyword
|
||||
|
|
||||
14 | True and False
|
||||
14 | True and False
|
||||
15 | #: E271
|
||||
16 | a and b
|
||||
| E271
|
||||
|
||||
@@ -28,7 +28,7 @@ E27.py:24:5: E272 Multiple spaces before keyword
|
||||
26 | this and False
|
||||
| E272
|
||||
27 | #: E273
|
||||
28 | a and b
|
||||
28 | a and b
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -5,17 +5,17 @@ E27.py:10:9: E273 Tab after keyword
|
||||
|
|
||||
10 | if 1:
|
||||
11 | #: E273
|
||||
12 | True and False
|
||||
12 | True and False
|
||||
| E273
|
||||
13 | #: E273 E274
|
||||
14 | True and False
|
||||
14 | True and False
|
||||
|
|
||||
|
||||
E27.py:12:5: E273 Tab after keyword
|
||||
|
|
||||
12 | True and False
|
||||
12 | True and False
|
||||
13 | #: E273 E274
|
||||
14 | True and False
|
||||
14 | True and False
|
||||
| E273
|
||||
15 | #: E271
|
||||
16 | a and b
|
||||
@@ -23,10 +23,10 @@ E27.py:12:5: E273 Tab after keyword
|
||||
|
||||
E27.py:12:10: E273 Tab after keyword
|
||||
|
|
||||
12 | True and False
|
||||
12 | True and False
|
||||
13 | #: E273 E274
|
||||
14 | True and False
|
||||
| E273
|
||||
14 | True and False
|
||||
| E273
|
||||
15 | #: E271
|
||||
16 | a and b
|
||||
|
|
||||
@@ -35,18 +35,18 @@ E27.py:26:6: E273 Tab after keyword
|
||||
|
|
||||
26 | this and False
|
||||
27 | #: E273
|
||||
28 | a and b
|
||||
28 | a and b
|
||||
| E273
|
||||
29 | #: E274
|
||||
30 | a and b
|
||||
30 | a and b
|
||||
|
|
||||
|
||||
E27.py:30:10: E273 Tab after keyword
|
||||
|
|
||||
30 | a and b
|
||||
30 | a and b
|
||||
31 | #: E273 E274
|
||||
32 | this and False
|
||||
| E273
|
||||
32 | this and False
|
||||
| E273
|
||||
33 | #: Okay
|
||||
34 | from u import (a, b)
|
||||
|
|
||||
|
||||
@@ -3,20 +3,20 @@ source: crates/ruff/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E27.py:28:3: E274 Tab before keyword
|
||||
|
|
||||
28 | a and b
|
||||
28 | a and b
|
||||
29 | #: E274
|
||||
30 | a and b
|
||||
| E274
|
||||
30 | a and b
|
||||
| E274
|
||||
31 | #: E273 E274
|
||||
32 | this and False
|
||||
32 | this and False
|
||||
|
|
||||
|
||||
E27.py:30:6: E274 Tab before keyword
|
||||
|
|
||||
30 | a and b
|
||||
30 | a and b
|
||||
31 | #: E273 E274
|
||||
32 | this and False
|
||||
| E274
|
||||
32 | this and False
|
||||
| E274
|
||||
33 | #: Okay
|
||||
34 | from u import (a, b)
|
||||
|
|
||||
|
||||
@@ -5,8 +5,8 @@ W19.py:3:1: W191 Indentation contains tabs
|
||||
|
|
||||
3 | #: W191
|
||||
4 | if False:
|
||||
5 | print # indented with 1 tab
|
||||
| W191
|
||||
5 | print # indented with 1 tab
|
||||
| ^^^^ W191
|
||||
6 | #:
|
||||
|
|
||||
|
||||
@@ -14,8 +14,8 @@ W19.py:9:1: W191 Indentation contains tabs
|
||||
|
|
||||
9 | #: W191
|
||||
10 | y = x == 2 \
|
||||
11 | or x == 3
|
||||
| W191
|
||||
11 | or x == 3
|
||||
| ^^^^ W191
|
||||
12 | #: E101 W191 W504
|
||||
13 | if (
|
||||
|
|
||||
@@ -24,8 +24,8 @@ W19.py:16:1: W191 Indentation contains tabs
|
||||
|
|
||||
16 | ) or
|
||||
17 | y == 4):
|
||||
18 | pass
|
||||
| W191
|
||||
18 | pass
|
||||
| ^^^^ W191
|
||||
19 | #: E101 W191
|
||||
20 | if x == 2 \
|
||||
|
|
||||
@@ -34,8 +34,8 @@ W19.py:21:1: W191 Indentation contains tabs
|
||||
|
|
||||
21 | or y > 1 \
|
||||
22 | or x == 3:
|
||||
23 | pass
|
||||
| W191
|
||||
23 | pass
|
||||
| ^^^^ W191
|
||||
24 | #: E101 W191
|
||||
25 | if x == 2 \
|
||||
|
|
||||
@@ -44,8 +44,8 @@ W19.py:26:1: W191 Indentation contains tabs
|
||||
|
|
||||
26 | or y > 1 \
|
||||
27 | or x == 3:
|
||||
28 | pass
|
||||
| W191
|
||||
28 | pass
|
||||
| ^^^^ W191
|
||||
29 | #:
|
||||
|
|
||||
|
||||
@@ -53,8 +53,8 @@ W19.py:32:1: W191 Indentation contains tabs
|
||||
|
|
||||
32 | if (foo == bar and
|
||||
33 | baz == bop):
|
||||
34 | pass
|
||||
| W191
|
||||
34 | pass
|
||||
| ^^^^ W191
|
||||
35 | #: E101 W191 W504
|
||||
36 | if (
|
||||
|
|
||||
@@ -63,8 +63,8 @@ W19.py:38:1: W191 Indentation contains tabs
|
||||
|
|
||||
38 | baz == bop
|
||||
39 | ):
|
||||
40 | pass
|
||||
| W191
|
||||
40 | pass
|
||||
| ^^^^ W191
|
||||
41 | #:
|
||||
|
|
||||
|
||||
@@ -72,18 +72,18 @@ W19.py:44:1: W191 Indentation contains tabs
|
||||
|
|
||||
44 | if start[1] > end_col and not (
|
||||
45 | over_indent == 4 and indent_next):
|
||||
46 | return (0, "E121 continuation line over-"
|
||||
| W191
|
||||
47 | "indented for visual indent")
|
||||
46 | return (0, "E121 continuation line over-"
|
||||
| ^^^^ W191
|
||||
47 | "indented for visual indent")
|
||||
48 | #:
|
||||
|
|
||||
|
||||
W19.py:45:1: W191 Indentation contains tabs
|
||||
|
|
||||
45 | over_indent == 4 and indent_next):
|
||||
46 | return (0, "E121 continuation line over-"
|
||||
47 | "indented for visual indent")
|
||||
| ^^^^^^^^ W191
|
||||
46 | return (0, "E121 continuation line over-"
|
||||
47 | "indented for visual indent")
|
||||
| ^^^^^^^^^^^^ W191
|
||||
48 | #:
|
||||
|
|
||||
|
||||
@@ -91,8 +91,8 @@ W19.py:54:1: W191 Indentation contains tabs
|
||||
|
|
||||
54 | var_one, var_two, var_three,
|
||||
55 | var_four):
|
||||
56 | print(var_one)
|
||||
| W191
|
||||
56 | print(var_one)
|
||||
| ^^^^ W191
|
||||
57 | #: E101 W191 W504
|
||||
58 | if ((row < 0 or self.moduleCount <= row or
|
||||
|
|
||||
@@ -101,8 +101,8 @@ W19.py:58:1: W191 Indentation contains tabs
|
||||
|
|
||||
58 | if ((row < 0 or self.moduleCount <= row or
|
||||
59 | col < 0 or self.moduleCount <= col)):
|
||||
60 | raise Exception("%s,%s - %s" % (row, col, self.moduleCount))
|
||||
| W191
|
||||
60 | raise Exception("%s,%s - %s" % (row, col, self.moduleCount))
|
||||
| ^^^^ W191
|
||||
61 | #: E101 E101 E101 E101 W191 W191 W191 W191 W191 W191
|
||||
62 | if bar:
|
||||
|
|
||||
@@ -111,58 +111,58 @@ W19.py:61:1: W191 Indentation contains tabs
|
||||
|
|
||||
61 | #: E101 E101 E101 E101 W191 W191 W191 W191 W191 W191
|
||||
62 | if bar:
|
||||
63 | return (
|
||||
| W191
|
||||
64 | start, 'E121 lines starting with a '
|
||||
65 | 'closing bracket should be indented '
|
||||
63 | return (
|
||||
| ^^^^ W191
|
||||
64 | start, 'E121 lines starting with a '
|
||||
65 | 'closing bracket should be indented '
|
||||
|
|
||||
|
||||
W19.py:62:1: W191 Indentation contains tabs
|
||||
|
|
||||
62 | if bar:
|
||||
63 | return (
|
||||
64 | start, 'E121 lines starting with a '
|
||||
| ^^^^ W191
|
||||
65 | 'closing bracket should be indented '
|
||||
66 | "to match that of the opening "
|
||||
63 | return (
|
||||
64 | start, 'E121 lines starting with a '
|
||||
| ^^^^^^^^ W191
|
||||
65 | 'closing bracket should be indented '
|
||||
66 | "to match that of the opening "
|
||||
|
|
||||
|
||||
W19.py:63:1: W191 Indentation contains tabs
|
||||
|
|
||||
63 | return (
|
||||
64 | start, 'E121 lines starting with a '
|
||||
65 | 'closing bracket should be indented '
|
||||
| ^^^^ W191
|
||||
66 | "to match that of the opening "
|
||||
67 | "bracket's line"
|
||||
63 | return (
|
||||
64 | start, 'E121 lines starting with a '
|
||||
65 | 'closing bracket should be indented '
|
||||
| ^^^^^^^^ W191
|
||||
66 | "to match that of the opening "
|
||||
67 | "bracket's line"
|
||||
|
|
||||
|
||||
W19.py:64:1: W191 Indentation contains tabs
|
||||
|
|
||||
64 | start, 'E121 lines starting with a '
|
||||
65 | 'closing bracket should be indented '
|
||||
66 | "to match that of the opening "
|
||||
| ^^^^ W191
|
||||
67 | "bracket's line"
|
||||
68 | )
|
||||
64 | start, 'E121 lines starting with a '
|
||||
65 | 'closing bracket should be indented '
|
||||
66 | "to match that of the opening "
|
||||
| ^^^^^^^^ W191
|
||||
67 | "bracket's line"
|
||||
68 | )
|
||||
|
|
||||
|
||||
W19.py:65:1: W191 Indentation contains tabs
|
||||
|
|
||||
65 | 'closing bracket should be indented '
|
||||
66 | "to match that of the opening "
|
||||
67 | "bracket's line"
|
||||
| ^^^^ W191
|
||||
68 | )
|
||||
65 | 'closing bracket should be indented '
|
||||
66 | "to match that of the opening "
|
||||
67 | "bracket's line"
|
||||
| ^^^^^^^^ W191
|
||||
68 | )
|
||||
69 | #
|
||||
|
|
||||
|
||||
W19.py:66:1: W191 Indentation contains tabs
|
||||
|
|
||||
66 | "to match that of the opening "
|
||||
67 | "bracket's line"
|
||||
68 | )
|
||||
| W191
|
||||
66 | "to match that of the opening "
|
||||
67 | "bracket's line"
|
||||
68 | )
|
||||
| ^^^^ W191
|
||||
69 | #
|
||||
70 | #: E101 W191 W504
|
||||
|
|
||||
@@ -171,8 +171,8 @@ W19.py:73:1: W191 Indentation contains tabs
|
||||
|
|
||||
73 | foo.bar("bop")
|
||||
74 | )):
|
||||
75 | print "yes"
|
||||
| W191
|
||||
75 | print "yes"
|
||||
| ^^^^ W191
|
||||
76 | #: E101 W191 W504
|
||||
77 | # also ok, but starting to look like LISP
|
||||
|
|
||||
@@ -181,8 +181,8 @@ W19.py:78:1: W191 Indentation contains tabs
|
||||
|
|
||||
78 | if ((foo.bar("baz") and
|
||||
79 | foo.bar("bop"))):
|
||||
80 | print "yes"
|
||||
| W191
|
||||
80 | print "yes"
|
||||
| ^^^^ W191
|
||||
81 | #: E101 W191 W504
|
||||
82 | if (a == 2 or
|
||||
|
|
||||
@@ -191,8 +191,8 @@ W19.py:83:1: W191 Indentation contains tabs
|
||||
|
|
||||
83 | b == "abc def ghi"
|
||||
84 | "jkl mno"):
|
||||
85 | return True
|
||||
| W191
|
||||
85 | return True
|
||||
| ^^^^ W191
|
||||
86 | #: E101 W191 W504
|
||||
87 | if (a == 2 or
|
||||
|
|
||||
@@ -201,8 +201,8 @@ W19.py:88:1: W191 Indentation contains tabs
|
||||
|
|
||||
88 | b == """abc def ghi
|
||||
89 | jkl mno"""):
|
||||
90 | return True
|
||||
| W191
|
||||
90 | return True
|
||||
| ^^^^ W191
|
||||
91 | #: W191:2:1 W191:3:1 E101:3:2
|
||||
92 | if length > options.max_line_length:
|
||||
|
|
||||
@@ -211,35 +211,35 @@ W19.py:91:1: W191 Indentation contains tabs
|
||||
|
|
||||
91 | #: W191:2:1 W191:3:1 E101:3:2
|
||||
92 | if length > options.max_line_length:
|
||||
93 | return options.max_line_length, \
|
||||
| W191
|
||||
94 | "E501 line too long (%d characters)" % length
|
||||
93 | return options.max_line_length, \
|
||||
| ^^^^ W191
|
||||
94 | "E501 line too long (%d characters)" % length
|
||||
|
|
||||
|
||||
W19.py:92:1: W191 Indentation contains tabs
|
||||
|
|
||||
92 | if length > options.max_line_length:
|
||||
93 | return options.max_line_length, \
|
||||
94 | "E501 line too long (%d characters)" % length
|
||||
| ^^^^ W191
|
||||
93 | return options.max_line_length, \
|
||||
94 | "E501 line too long (%d characters)" % length
|
||||
| ^^^^^^^^ W191
|
||||
|
|
||||
|
||||
W19.py:98:1: W191 Indentation contains tabs
|
||||
|
|
||||
98 | #: E101 W191 W191 W504
|
||||
99 | if os.path.exists(os.path.join(path, PEP8_BIN)):
|
||||
100 | cmd = ([os.path.join(path, PEP8_BIN)] +
|
||||
| W191
|
||||
101 | self._pep8_options(targetfile))
|
||||
100 | cmd = ([os.path.join(path, PEP8_BIN)] +
|
||||
| ^^^^ W191
|
||||
101 | self._pep8_options(targetfile))
|
||||
102 | #: W191 - okay
|
||||
|
|
||||
|
||||
W19.py:99:1: W191 Indentation contains tabs
|
||||
|
|
||||
99 | if os.path.exists(os.path.join(path, PEP8_BIN)):
|
||||
100 | cmd = ([os.path.join(path, PEP8_BIN)] +
|
||||
101 | self._pep8_options(targetfile))
|
||||
| ^^^^^^^ W191
|
||||
100 | cmd = ([os.path.join(path, PEP8_BIN)] +
|
||||
101 | self._pep8_options(targetfile))
|
||||
| ^^^^^^^^^^^ W191
|
||||
102 | #: W191 - okay
|
||||
103 | '''
|
||||
|
|
||||
@@ -248,36 +248,36 @@ W19.py:125:1: W191 Indentation contains tabs
|
||||
|
|
||||
125 | if foo is None and bar is "bop" and \
|
||||
126 | blah == 'yeah':
|
||||
127 | blah = 'yeahnah'
|
||||
| W191
|
||||
127 | blah = 'yeahnah'
|
||||
| ^^^^ W191
|
||||
|
|
||||
|
||||
W19.py:131:1: W191 Indentation contains tabs
|
||||
|
|
||||
131 | #: W191 W191 W191
|
||||
132 | if True:
|
||||
133 | foo(
|
||||
| W191
|
||||
134 | 1,
|
||||
135 | 2)
|
||||
133 | foo(
|
||||
| ^^^^ W191
|
||||
134 | 1,
|
||||
135 | 2)
|
||||
|
|
||||
|
||||
W19.py:132:1: W191 Indentation contains tabs
|
||||
|
|
||||
132 | if True:
|
||||
133 | foo(
|
||||
134 | 1,
|
||||
| W191
|
||||
135 | 2)
|
||||
133 | foo(
|
||||
134 | 1,
|
||||
| ^^^^^^^^ W191
|
||||
135 | 2)
|
||||
136 | #: W191 W191 W191 W191 W191
|
||||
|
|
||||
|
||||
W19.py:133:1: W191 Indentation contains tabs
|
||||
|
|
||||
133 | foo(
|
||||
134 | 1,
|
||||
135 | 2)
|
||||
| W191
|
||||
133 | foo(
|
||||
134 | 1,
|
||||
135 | 2)
|
||||
| ^^^^^^^^ W191
|
||||
136 | #: W191 W191 W191 W191 W191
|
||||
137 | def test_keys(self):
|
||||
|
|
||||
@@ -286,48 +286,48 @@ W19.py:136:1: W191 Indentation contains tabs
|
||||
|
|
||||
136 | #: W191 W191 W191 W191 W191
|
||||
137 | def test_keys(self):
|
||||
138 | """areas.json - All regions are accounted for."""
|
||||
| W191
|
||||
139 | expected = set([
|
||||
140 | u'Norrbotten',
|
||||
138 | """areas.json - All regions are accounted for."""
|
||||
| ^^^^ W191
|
||||
139 | expected = set([
|
||||
140 | u'Norrbotten',
|
||||
|
|
||||
|
||||
W19.py:137:1: W191 Indentation contains tabs
|
||||
|
|
||||
137 | def test_keys(self):
|
||||
138 | """areas.json - All regions are accounted for."""
|
||||
139 | expected = set([
|
||||
| W191
|
||||
140 | u'Norrbotten',
|
||||
141 | u'V\xe4sterbotten',
|
||||
138 | """areas.json - All regions are accounted for."""
|
||||
139 | expected = set([
|
||||
| ^^^^ W191
|
||||
140 | u'Norrbotten',
|
||||
141 | u'V\xe4sterbotten',
|
||||
|
|
||||
|
||||
W19.py:138:1: W191 Indentation contains tabs
|
||||
|
|
||||
138 | """areas.json - All regions are accounted for."""
|
||||
139 | expected = set([
|
||||
140 | u'Norrbotten',
|
||||
| W191
|
||||
141 | u'V\xe4sterbotten',
|
||||
142 | ])
|
||||
138 | """areas.json - All regions are accounted for."""
|
||||
139 | expected = set([
|
||||
140 | u'Norrbotten',
|
||||
| ^^^^^^^^ W191
|
||||
141 | u'V\xe4sterbotten',
|
||||
142 | ])
|
||||
|
|
||||
|
||||
W19.py:139:1: W191 Indentation contains tabs
|
||||
|
|
||||
139 | expected = set([
|
||||
140 | u'Norrbotten',
|
||||
141 | u'V\xe4sterbotten',
|
||||
| W191
|
||||
142 | ])
|
||||
139 | expected = set([
|
||||
140 | u'Norrbotten',
|
||||
141 | u'V\xe4sterbotten',
|
||||
| ^^^^^^^^ W191
|
||||
142 | ])
|
||||
143 | #: W191
|
||||
|
|
||||
|
||||
W19.py:140:1: W191 Indentation contains tabs
|
||||
|
|
||||
140 | u'Norrbotten',
|
||||
141 | u'V\xe4sterbotten',
|
||||
142 | ])
|
||||
| W191
|
||||
140 | u'Norrbotten',
|
||||
141 | u'V\xe4sterbotten',
|
||||
142 | ])
|
||||
| ^^^^ W191
|
||||
143 | #: W191
|
||||
144 | x = [
|
||||
|
|
||||
@@ -336,8 +336,8 @@ W19.py:143:1: W191 Indentation contains tabs
|
||||
|
|
||||
143 | #: W191
|
||||
144 | x = [
|
||||
145 | 'abc'
|
||||
| W191
|
||||
145 | 'abc'
|
||||
| ^^^^ W191
|
||||
146 | ]
|
||||
147 | #: W191 - okay
|
||||
|
|
||||
|
||||
@@ -68,6 +68,7 @@ mod tests {
|
||||
#[test_case(Rule::UndocumentedPublicPackage, Path::new("D104/__init__.py"); "D104_1")]
|
||||
#[test_case(Rule::SectionNameEndsInColon, Path::new("D.py"); "D416")]
|
||||
#[test_case(Rule::SectionNotOverIndented, Path::new("sections.py"); "D214")]
|
||||
#[test_case(Rule::SectionNotOverIndented, Path::new("D214_module.py"); "D214_module")]
|
||||
#[test_case(Rule::SectionUnderlineAfterName, Path::new("sections.py"); "D408")]
|
||||
#[test_case(Rule::SectionUnderlineMatchesSectionLength, Path::new("sections.py"); "D409")]
|
||||
#[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("sections.py"); "D215")]
|
||||
|
||||
@@ -619,10 +619,14 @@ fn common_section(
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
// Replace the existing indentation with whitespace of the appropriate length.
|
||||
diagnostic.set_fix(Edit::range_replacement(
|
||||
whitespace::clean(docstring.indentation),
|
||||
TextRange::at(context.range().start(), leading_space.text_len()),
|
||||
));
|
||||
let content = whitespace::clean(docstring.indentation);
|
||||
let fix_range = TextRange::at(context.range().start(), leading_space.text_len());
|
||||
|
||||
diagnostic.set_fix(if content.is_empty() {
|
||||
Edit::range_deletion(fix_range)
|
||||
} else {
|
||||
Edit::range_replacement(content, fix_range)
|
||||
});
|
||||
};
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pydocstyle/mod.rs
|
||||
---
|
||||
D214_module.py:1:1: D214 [*] Section is over-indented ("Returns")
|
||||
|
|
||||
1 | / """A module docstring with D214 violations
|
||||
2 | |
|
||||
3 | | Returns
|
||||
4 | | -----
|
||||
5 | | valid returns
|
||||
6 | |
|
||||
7 | | Args
|
||||
8 | | -----
|
||||
9 | | valid args
|
||||
10 | | """
|
||||
| |___^ D214
|
||||
11 |
|
||||
12 | import os
|
||||
|
|
||||
= help: Remove over-indentation from "Returns"
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | """A module docstring with D214 violations
|
||||
2 2 |
|
||||
3 |- Returns
|
||||
3 |+Returns
|
||||
4 4 | -----
|
||||
5 5 | valid returns
|
||||
6 6 |
|
||||
|
||||
D214_module.py:1:1: D214 [*] Section is over-indented ("Args")
|
||||
|
|
||||
1 | / """A module docstring with D214 violations
|
||||
2 | |
|
||||
3 | | Returns
|
||||
4 | | -----
|
||||
5 | | valid returns
|
||||
6 | |
|
||||
7 | | Args
|
||||
8 | | -----
|
||||
9 | | valid args
|
||||
10 | | """
|
||||
| |___^ D214
|
||||
11 |
|
||||
12 | import os
|
||||
|
|
||||
= help: Remove over-indentation from "Args"
|
||||
|
||||
ℹ Suggested fix
|
||||
4 4 | -----
|
||||
5 5 | valid returns
|
||||
6 6 |
|
||||
7 |- Args
|
||||
7 |+Args
|
||||
8 8 | -----
|
||||
9 9 | valid args
|
||||
10 10 | """
|
||||
|
||||
|
||||
@@ -214,11 +214,7 @@ fn remove_unused_variable(
|
||||
))
|
||||
} else {
|
||||
// If (e.g.) assigning to a constant (`x = 1`), delete the entire statement.
|
||||
let parent = checker
|
||||
.ctx
|
||||
.child_to_parent
|
||||
.get(&RefEquality(stmt))
|
||||
.map(Into::into);
|
||||
let parent = checker.ctx.stmts.parent(stmt);
|
||||
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
|
||||
match delete_stmt(
|
||||
stmt,
|
||||
@@ -259,11 +255,7 @@ fn remove_unused_variable(
|
||||
))
|
||||
} else {
|
||||
// If assigning to a constant (`x = 1`), delete the entire statement.
|
||||
let parent = checker
|
||||
.ctx
|
||||
.child_to_parent
|
||||
.get(&RefEquality(stmt))
|
||||
.map(Into::into);
|
||||
let parent = checker.ctx.stmts.parent(stmt);
|
||||
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
|
||||
match delete_stmt(
|
||||
stmt,
|
||||
@@ -327,6 +319,7 @@ pub fn unused_variable(checker: &mut Checker, scope: ScopeId) {
|
||||
&& name != &"__tracebackhide__"
|
||||
&& name != &"__traceback_info__"
|
||||
&& name != &"__traceback_supplement__"
|
||||
&& name != &"__debuggerskip__"
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnusedVariable {
|
||||
@@ -335,7 +328,8 @@ pub fn unused_variable(checker: &mut Checker, scope: ScopeId) {
|
||||
binding.range,
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(stmt) = binding.source.as_ref().map(Into::into) {
|
||||
if let Some(source) = binding.source {
|
||||
let stmt = checker.ctx.stmts[source];
|
||||
if let Some((kind, fix)) = remove_unused_variable(stmt, binding.range, checker)
|
||||
{
|
||||
if matches!(kind, DeletionKind::Whole) {
|
||||
|
||||
@@ -37,9 +37,13 @@ mod tests {
|
||||
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_4.py"); "PLR1722_4")]
|
||||
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_5.py"); "PLR1722_5")]
|
||||
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_6.py"); "PLR1722_6")]
|
||||
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_7.py"); "PLR1722_7")]
|
||||
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_8.py"); "PLR1722_8")]
|
||||
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_9.py"); "PLR1722_9")]
|
||||
#[test_case(Rule::ContinueInFinally, Path::new("continue_in_finally.py"); "PLE0116")]
|
||||
#[test_case(Rule::GlobalStatement, Path::new("global_statement.py"); "PLW0603")]
|
||||
#[test_case(Rule::GlobalVariableNotAssigned, Path::new("global_variable_not_assigned.py"); "PLW0602")]
|
||||
#[test_case(Rule::ImportSelf, Path::new("import_self/module.py"); "PLW0406")]
|
||||
#[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::InvalidCharacterBackspace, Path::new("invalid_characters.py"); "PLE2510")]
|
||||
@@ -67,6 +71,7 @@ mod tests {
|
||||
#[test_case(Rule::UselessImportAlias, Path::new("import_aliasing.py"); "PLC0414")]
|
||||
#[test_case(Rule::UselessReturn, Path::new("useless_return.py"); "PLR1711")]
|
||||
#[test_case(Rule::YieldInInit, Path::new("yield_in_init.py"); "PLE0100")]
|
||||
#[test_case(Rule::NestedMinMax, Path::new("nested_min_max.py"); "PLW3301")]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -51,6 +51,30 @@ impl std::fmt::Display for EmptyStringCmpop {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for comparisons to empty strings.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// An empty string is falsy, so it is unnecessary to compare it to `""`. If
|
||||
/// the value can be something else Python considers falsy, such as `None` or
|
||||
/// `0` or another empty container, then the code is not equivalent.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def foo(x):
|
||||
/// if x == "":
|
||||
/// print("x is empty")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo(x):
|
||||
/// if not x:
|
||||
/// print("x is empty")
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#truth-value-testing)
|
||||
#[violation]
|
||||
pub struct CompareToEmptyString {
|
||||
pub existing: String,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use rustpython_parser::ast::Stmt;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
@@ -55,11 +53,9 @@ pub fn global_statement(checker: &mut Checker, name: &str) {
|
||||
if let Some(index) = scope.get(name) {
|
||||
let binding = &checker.ctx.bindings[*index];
|
||||
if binding.kind.is_global() {
|
||||
let source: &Stmt = binding
|
||||
let source = checker.ctx.stmts[binding
|
||||
.source
|
||||
.as_ref()
|
||||
.expect("`global` bindings should always have a `source`")
|
||||
.into();
|
||||
.expect("`global` bindings should always have a `source`")];
|
||||
let diagnostic = Diagnostic::new(
|
||||
GlobalStatement {
|
||||
name: name.to_string(),
|
||||
|
||||
70
crates/ruff/src/rules/pylint/rules/import_self.rs
Normal file
70
crates/ruff/src/rules/pylint/rules/import_self.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use rustpython_parser::ast::Alias;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::resolve_imported_module_path;
|
||||
|
||||
#[violation]
|
||||
pub struct ImportSelf {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Violation for ImportSelf {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self { name } = self;
|
||||
format!("Module `{name}` imports itself")
|
||||
}
|
||||
}
|
||||
|
||||
/// PLW0406
|
||||
pub fn import_self(alias: &Alias, module_path: Option<&[String]>) -> Option<Diagnostic> {
|
||||
let Some(module_path) = module_path else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if alias.node.name.split('.').eq(module_path) {
|
||||
return Some(Diagnostic::new(
|
||||
ImportSelf {
|
||||
name: alias.node.name.clone(),
|
||||
},
|
||||
alias.range(),
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// PLW0406
|
||||
pub fn import_from_self(
|
||||
level: Option<usize>,
|
||||
module: Option<&str>,
|
||||
names: &[Alias],
|
||||
module_path: Option<&[String]>,
|
||||
) -> Option<Diagnostic> {
|
||||
let Some(module_path) = module_path else {
|
||||
return None;
|
||||
};
|
||||
let Some(imported_module_path) = resolve_imported_module_path(level, module, Some(module_path)) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if imported_module_path
|
||||
.split('.')
|
||||
.eq(&module_path[..module_path.len() - 1])
|
||||
{
|
||||
if let Some(alias) = names
|
||||
.iter()
|
||||
.find(|alias| alias.node.name == module_path[module_path.len() - 1])
|
||||
{
|
||||
return Some(Diagnostic::new(
|
||||
ImportSelf {
|
||||
name: format!("{}.{}", imported_module_path, alias.node.name),
|
||||
},
|
||||
alias.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub use comparison_of_constant::{comparison_of_constant, ComparisonOfConstant};
|
||||
pub use continue_in_finally::{continue_in_finally, ContinueInFinally};
|
||||
pub use global_statement::{global_statement, GlobalStatement};
|
||||
pub use global_variable_not_assigned::GlobalVariableNotAssigned;
|
||||
pub use import_self::{import_from_self, import_self, ImportSelf};
|
||||
pub use invalid_all_format::{invalid_all_format, InvalidAllFormat};
|
||||
pub use invalid_all_object::{invalid_all_object, InvalidAllObject};
|
||||
pub use invalid_envvar_default::{invalid_envvar_default, InvalidEnvvarDefault};
|
||||
@@ -24,6 +25,7 @@ pub use load_before_global_declaration::{
|
||||
pub use logging::{logging_call, LoggingTooFewArgs, LoggingTooManyArgs};
|
||||
pub use magic_value_comparison::{magic_value_comparison, MagicValueComparison};
|
||||
pub use manual_import_from::{manual_from_import, ManualFromImport};
|
||||
pub use nested_min_max::{nested_min_max, NestedMinMax};
|
||||
pub use nonlocal_without_binding::NonlocalWithoutBinding;
|
||||
pub use property_with_parameters::{property_with_parameters, PropertyWithParameters};
|
||||
pub use redefined_loop_name::{redefined_loop_name, RedefinedLoopName};
|
||||
@@ -57,6 +59,7 @@ mod comparison_of_constant;
|
||||
mod continue_in_finally;
|
||||
mod global_statement;
|
||||
mod global_variable_not_assigned;
|
||||
mod import_self;
|
||||
mod invalid_all_format;
|
||||
mod invalid_all_object;
|
||||
mod invalid_envvar_default;
|
||||
@@ -66,6 +69,7 @@ mod load_before_global_declaration;
|
||||
mod logging;
|
||||
mod magic_value_comparison;
|
||||
mod manual_import_from;
|
||||
mod nested_min_max;
|
||||
mod nonlocal_without_binding;
|
||||
mod property_with_parameters;
|
||||
mod redefined_loop_name;
|
||||
|
||||
131
crates/ruff/src/rules/pylint/rules/nested_min_max.rs
Normal file
131
crates/ruff/src/rules/pylint/rules/nested_min_max.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use ruff_text_size::TextSize;
|
||||
use rustpython_parser::ast::{Expr, ExprKind, Keyword};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::{has_comments, unparse_expr};
|
||||
use ruff_python_semantic::context::Context;
|
||||
|
||||
use crate::{checkers::ast::Checker, registry::AsRule};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum MinMax {
|
||||
Min,
|
||||
Max,
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct NestedMinMax {
|
||||
func: MinMax,
|
||||
fixable: bool,
|
||||
}
|
||||
|
||||
impl Violation for NestedMinMax {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Nested `{}` calls can be flattened", self.func)
|
||||
}
|
||||
|
||||
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
|
||||
self.fixable
|
||||
.then_some(|NestedMinMax { func, .. }| format!("Flatten nested `{func}` calls"))
|
||||
}
|
||||
}
|
||||
|
||||
impl MinMax {
|
||||
/// Converts a function call [`Expr`] into a [`MinMax`] if it is a call to `min` or `max`.
|
||||
fn try_from_call(func: &Expr, keywords: &[Keyword], context: &Context) -> Option<MinMax> {
|
||||
if !keywords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let ExprKind::Name { id, .. } = func.node() else {
|
||||
return None;
|
||||
};
|
||||
if id == "min" && context.is_builtin("min") {
|
||||
Some(MinMax::Min)
|
||||
} else if id == "max" && context.is_builtin("max") {
|
||||
Some(MinMax::Max)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MinMax {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MinMax::Min => write!(f, "min"),
|
||||
MinMax::Max => write!(f, "max"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect a new set of arguments to by either accepting existing args as-is or
|
||||
/// collecting child arguments, if it's a call to the same function.
|
||||
fn collect_nested_args(context: &Context, min_max: MinMax, args: &[Expr]) -> Vec<Expr> {
|
||||
fn inner(context: &Context, min_max: MinMax, args: &[Expr], new_args: &mut Vec<Expr>) {
|
||||
for arg in args {
|
||||
if let ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
} = arg.node()
|
||||
{
|
||||
if MinMax::try_from_call(func, keywords, context) == Some(min_max) {
|
||||
inner(context, min_max, args, new_args);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
new_args.push(arg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_args = Vec::with_capacity(args.len());
|
||||
inner(context, min_max, args, &mut new_args);
|
||||
new_args
|
||||
}
|
||||
|
||||
/// W3301
|
||||
pub fn nested_min_max(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
let Some(min_max) = MinMax::try_from_call(func, keywords, &checker.ctx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if args.iter().any(|arg| {
|
||||
let ExprKind::Call { func, keywords, ..} = arg.node() else {
|
||||
return false;
|
||||
};
|
||||
MinMax::try_from_call(func, keywords, &checker.ctx) == Some(min_max)
|
||||
}) {
|
||||
let fixable = !has_comments(expr, checker.locator);
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
NestedMinMax {
|
||||
func: min_max,
|
||||
fixable,
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
if fixable && checker.patch(diagnostic.kind.rule()) {
|
||||
let flattened_expr = Expr::new(
|
||||
TextSize::default(),
|
||||
TextSize::default(),
|
||||
ExprKind::Call {
|
||||
func: Box::new(func.clone()),
|
||||
args: collect_nested_args(&checker.ctx, min_max, args),
|
||||
keywords: keywords.to_owned(),
|
||||
},
|
||||
);
|
||||
diagnostic.set_fix(Edit::range_replacement(
|
||||
unparse_expr(&flattened_expr, checker.stylist),
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ pub fn sys_exit_alias(checker: &mut Checker, func: &Expr) {
|
||||
let (import_edit, binding) = get_or_import_symbol(
|
||||
"sys",
|
||||
"exit",
|
||||
func.start(),
|
||||
&checker.ctx,
|
||||
&checker.importer,
|
||||
checker.locator,
|
||||
|
||||
@@ -5,6 +5,25 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary direct calls to lambda expressions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a lambda expression directly is unnecessary. The expression can be
|
||||
/// executed inline instead to improve readability.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// area = (lambda r: 3.14 * r ** 2)(radius)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// area = 3.14 * radius ** 2
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#lambda)
|
||||
#[violation]
|
||||
pub struct UnnecessaryDirectLambdaCall;
|
||||
|
||||
|
||||
@@ -6,6 +6,21 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for import aliases that do not rename the original package.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The import alias is redundant and should be removed to avoid confusion.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import numpy as numpy
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import numpy as np
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct UselessImportAlias;
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
sys_exit_alias_7.py:2:5: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
||||
|
|
||||
2 | def main():
|
||||
3 | exit(0)
|
||||
| ^^^^ PLR1722
|
||||
|
|
||||
= help: Replace `exit` with `sys.exit()`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 |+import sys
|
||||
1 2 | def main():
|
||||
2 |- exit(0)
|
||||
3 |+ sys.exit(0)
|
||||
3 4 |
|
||||
4 5 |
|
||||
5 6 | import functools
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
sys_exit_alias_8.py:5:5: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
||||
|
|
||||
5 | def main():
|
||||
6 | exit(0)
|
||||
| ^^^^ PLR1722
|
||||
|
|
||||
= help: Replace `exit` with `sys.exit()`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 |-from sys import argv
|
||||
1 |+from sys import argv, exit
|
||||
2 2 |
|
||||
3 3 |
|
||||
4 4 | def main():
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
sys_exit_alias_9.py:2:5: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
||||
|
|
||||
2 | def main():
|
||||
3 | exit(0)
|
||||
| ^^^^ PLR1722
|
||||
|
|
||||
= help: Replace `exit` with `sys.exit()`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 |+import sys
|
||||
1 2 | def main():
|
||||
2 |- exit(0)
|
||||
3 |+ sys.exit(0)
|
||||
3 4 |
|
||||
4 5 |
|
||||
5 6 | from sys import argv
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
module.py:1:8: PLW0406 Module `import_self.module` imports itself
|
||||
|
|
||||
1 | import import_self.module
|
||||
| ^^^^^^^^^^^^^^^^^^ PLW0406
|
||||
2 | from import_self import module
|
||||
3 | from . import module
|
||||
|
|
||||
|
||||
module.py:2:25: PLW0406 Module `import_self.module` imports itself
|
||||
|
|
||||
2 | import import_self.module
|
||||
3 | from import_self import module
|
||||
| ^^^^^^ PLW0406
|
||||
4 | from . import module
|
||||
|
|
||||
|
||||
module.py:3:15: PLW0406 Module `import_self.module` imports itself
|
||||
|
|
||||
3 | import import_self.module
|
||||
4 | from import_self import module
|
||||
5 | from . import module
|
||||
| ^^^^^^ PLW0406
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
nested_min_max.py:2:1: PLW3301 [*] Nested `min` calls can be flattened
|
||||
|
|
||||
2 | min(1, 2, 3)
|
||||
3 | min(1, min(2, 3))
|
||||
| ^^^^^^^^^^^^^^^^^ PLW3301
|
||||
4 | min(1, min(2, min(3, 4)))
|
||||
5 | min(1, foo("a", "b"), min(3, 4))
|
||||
|
|
||||
= help: Flatten nested `min` calls
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | min(1, 2, 3)
|
||||
2 |-min(1, min(2, 3))
|
||||
2 |+min(1, 2, 3)
|
||||
3 3 | min(1, min(2, min(3, 4)))
|
||||
4 4 | min(1, foo("a", "b"), min(3, 4))
|
||||
5 5 | min(1, max(2, 3))
|
||||
|
||||
nested_min_max.py:3:1: PLW3301 [*] Nested `min` calls can be flattened
|
||||
|
|
||||
3 | min(1, 2, 3)
|
||||
4 | min(1, min(2, 3))
|
||||
5 | min(1, min(2, min(3, 4)))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
|
||||
6 | min(1, foo("a", "b"), min(3, 4))
|
||||
7 | min(1, max(2, 3))
|
||||
|
|
||||
= help: Flatten nested `min` calls
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | min(1, 2, 3)
|
||||
2 2 | min(1, min(2, 3))
|
||||
3 |-min(1, min(2, min(3, 4)))
|
||||
3 |+min(1, 2, 3, 4)
|
||||
4 4 | min(1, foo("a", "b"), min(3, 4))
|
||||
5 5 | min(1, max(2, 3))
|
||||
6 6 | max(1, 2, 3)
|
||||
|
||||
nested_min_max.py:3:8: PLW3301 [*] Nested `min` calls can be flattened
|
||||
|
|
||||
3 | min(1, 2, 3)
|
||||
4 | min(1, min(2, 3))
|
||||
5 | min(1, min(2, min(3, 4)))
|
||||
| ^^^^^^^^^^^^^^^^^ PLW3301
|
||||
6 | min(1, foo("a", "b"), min(3, 4))
|
||||
7 | min(1, max(2, 3))
|
||||
|
|
||||
= help: Flatten nested `min` calls
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | min(1, 2, 3)
|
||||
2 2 | min(1, min(2, 3))
|
||||
3 |-min(1, min(2, min(3, 4)))
|
||||
3 |+min(1, min(2, 3, 4))
|
||||
4 4 | min(1, foo("a", "b"), min(3, 4))
|
||||
5 5 | min(1, max(2, 3))
|
||||
6 6 | max(1, 2, 3)
|
||||
|
||||
nested_min_max.py:4:1: PLW3301 [*] Nested `min` calls can be flattened
|
||||
|
|
||||
4 | min(1, min(2, 3))
|
||||
5 | min(1, min(2, min(3, 4)))
|
||||
6 | min(1, foo("a", "b"), min(3, 4))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
|
||||
7 | min(1, max(2, 3))
|
||||
8 | max(1, 2, 3)
|
||||
|
|
||||
= help: Flatten nested `min` calls
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | min(1, 2, 3)
|
||||
2 2 | min(1, min(2, 3))
|
||||
3 3 | min(1, min(2, min(3, 4)))
|
||||
4 |-min(1, foo("a", "b"), min(3, 4))
|
||||
4 |+min(1, foo("a", "b"), 3, 4)
|
||||
5 5 | min(1, max(2, 3))
|
||||
6 6 | max(1, 2, 3)
|
||||
7 7 | max(1, max(2, 3))
|
||||
|
||||
nested_min_max.py:7:1: PLW3301 [*] Nested `max` calls can be flattened
|
||||
|
|
||||
7 | min(1, max(2, 3))
|
||||
8 | max(1, 2, 3)
|
||||
9 | max(1, max(2, 3))
|
||||
| ^^^^^^^^^^^^^^^^^ PLW3301
|
||||
10 | max(1, max(2, max(3, 4)))
|
||||
11 | max(1, foo("a", "b"), max(3, 4))
|
||||
|
|
||||
= help: Flatten nested `max` calls
|
||||
|
||||
ℹ Suggested fix
|
||||
4 4 | min(1, foo("a", "b"), min(3, 4))
|
||||
5 5 | min(1, max(2, 3))
|
||||
6 6 | max(1, 2, 3)
|
||||
7 |-max(1, max(2, 3))
|
||||
7 |+max(1, 2, 3)
|
||||
8 8 | max(1, max(2, max(3, 4)))
|
||||
9 9 | max(1, foo("a", "b"), max(3, 4))
|
||||
10 10 |
|
||||
|
||||
nested_min_max.py:8:1: PLW3301 [*] Nested `max` calls can be flattened
|
||||
|
|
||||
8 | max(1, 2, 3)
|
||||
9 | max(1, max(2, 3))
|
||||
10 | max(1, max(2, max(3, 4)))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
|
||||
11 | max(1, foo("a", "b"), max(3, 4))
|
||||
|
|
||||
= help: Flatten nested `max` calls
|
||||
|
||||
ℹ Suggested fix
|
||||
5 5 | min(1, max(2, 3))
|
||||
6 6 | max(1, 2, 3)
|
||||
7 7 | max(1, max(2, 3))
|
||||
8 |-max(1, max(2, max(3, 4)))
|
||||
8 |+max(1, 2, 3, 4)
|
||||
9 9 | max(1, foo("a", "b"), max(3, 4))
|
||||
10 10 |
|
||||
11 11 | # These should not trigger; we do not flag cases with keyword args.
|
||||
|
||||
nested_min_max.py:8:8: PLW3301 [*] Nested `max` calls can be flattened
|
||||
|
|
||||
8 | max(1, 2, 3)
|
||||
9 | max(1, max(2, 3))
|
||||
10 | max(1, max(2, max(3, 4)))
|
||||
| ^^^^^^^^^^^^^^^^^ PLW3301
|
||||
11 | max(1, foo("a", "b"), max(3, 4))
|
||||
|
|
||||
= help: Flatten nested `max` calls
|
||||
|
||||
ℹ Suggested fix
|
||||
5 5 | min(1, max(2, 3))
|
||||
6 6 | max(1, 2, 3)
|
||||
7 7 | max(1, max(2, 3))
|
||||
8 |-max(1, max(2, max(3, 4)))
|
||||
8 |+max(1, max(2, 3, 4))
|
||||
9 9 | max(1, foo("a", "b"), max(3, 4))
|
||||
10 10 |
|
||||
11 11 | # These should not trigger; we do not flag cases with keyword args.
|
||||
|
||||
nested_min_max.py:9:1: PLW3301 [*] Nested `max` calls can be flattened
|
||||
|
|
||||
9 | max(1, max(2, 3))
|
||||
10 | max(1, max(2, max(3, 4)))
|
||||
11 | max(1, foo("a", "b"), max(3, 4))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
|
||||
12 |
|
||||
13 | # These should not trigger; we do not flag cases with keyword args.
|
||||
|
|
||||
= help: Flatten nested `max` calls
|
||||
|
||||
ℹ Suggested fix
|
||||
6 6 | max(1, 2, 3)
|
||||
7 7 | max(1, max(2, 3))
|
||||
8 8 | max(1, max(2, max(3, 4)))
|
||||
9 |-max(1, foo("a", "b"), max(3, 4))
|
||||
9 |+max(1, foo("a", "b"), 3, 4)
|
||||
10 10 |
|
||||
11 11 | # These should not trigger; we do not flag cases with keyword args.
|
||||
12 12 | min(1, min(2, 3), key=test)
|
||||
|
||||
nested_min_max.py:15:1: PLW3301 [*] Nested `min` calls can be flattened
|
||||
|
|
||||
15 | min(1, min(2, 3, key=test))
|
||||
16 | # This will still trigger, to merge the calls without keyword args.
|
||||
17 | min(1, min(2, 3, key=test), min(4, 5))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
|
||||
18 |
|
||||
19 | # Don't provide a fix if there are comments within the call.
|
||||
|
|
||||
= help: Flatten nested `min` calls
|
||||
|
||||
ℹ Suggested fix
|
||||
12 12 | min(1, min(2, 3), key=test)
|
||||
13 13 | min(1, min(2, 3, key=test))
|
||||
14 14 | # This will still trigger, to merge the calls without keyword args.
|
||||
15 |-min(1, min(2, 3, key=test), min(4, 5))
|
||||
15 |+min(1, min(2, 3, key=test), 4, 5)
|
||||
16 16 |
|
||||
17 17 | # Don't provide a fix if there are comments within the call.
|
||||
18 18 | min(
|
||||
|
||||
nested_min_max.py:18:1: PLW3301 Nested `min` calls can be flattened
|
||||
|
|
||||
18 | # Don't provide a fix if there are comments within the call.
|
||||
19 | / min(
|
||||
20 | | 1, # This is a comment.
|
||||
21 | | min(2, 3),
|
||||
22 | | )
|
||||
| |_^ PLW3301
|
||||
|
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ pub fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: &[Expr
|
||||
let (import_edit, binding) = get_or_import_symbol(
|
||||
"functools",
|
||||
"cache",
|
||||
expr.start(),
|
||||
&checker.ctx,
|
||||
&checker.importer,
|
||||
checker.locator,
|
||||
|
||||
@@ -164,9 +164,9 @@ fn fix_py2_block(
|
||||
let defined_by = checker.ctx.current_stmt();
|
||||
let defined_in = checker.ctx.current_stmt_parent();
|
||||
return match delete_stmt(
|
||||
defined_by.into(),
|
||||
defined_by,
|
||||
if block.starter == Tok::If {
|
||||
defined_in.map(Into::into)
|
||||
defined_in
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -176,7 +176,7 @@ fn fix_py2_block(
|
||||
checker.stylist,
|
||||
) {
|
||||
Ok(fix) => {
|
||||
checker.deletions.insert(RefEquality(defined_by.into()));
|
||||
checker.deletions.insert(RefEquality(defined_by));
|
||||
Some(fix)
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user