Compare commits

...

27 Commits

Author SHA1 Message Date
Charlie Marsh
9d136de55a Bump version to 0.0.121 2022-11-15 16:18:39 -05:00
Harutaka Kawamura
1821c07367 Implement B020 (#753) 2022-11-15 16:17:03 -05:00
Charlie Marsh
1fe90ef7f4 Only notify once for each app update (#762) 2022-11-15 16:14:10 -05:00
Charlie Marsh
b5cb9485f6 Move updater to its own module 2022-11-15 15:51:24 -05:00
Charlie Marsh
4d798512b1 Only print version checks on tty (#761) 2022-11-15 15:36:06 -05:00
Charlie Marsh
5f9815b103 Disable auto-updates in JSON mode (#760) 2022-11-15 15:29:10 -05:00
Charlie Marsh
0d3fac1bf9 Add --line-length command line argument (#759) 2022-11-15 12:23:38 -05:00
Charlie Marsh
ff0e5f5cb4 Preserve scopes when checking deferred strings (#758) 2022-11-15 12:19:22 -05:00
Charlie Marsh
374d57d822 Limit PEP 604 checks to Python 3.10+ (#757) 2022-11-15 11:52:12 -05:00
Edgar R. M
85b2a9920f docs: Add flake8-bandit to ToC (#750) 2022-11-15 00:11:39 -05:00
Charlie Marsh
3c22913470 Bump version to 0.0.120 2022-11-14 22:53:36 -05:00
Charlie Marsh
ea03a59b72 De-alias Literal checks (#748) 2022-11-14 22:53:23 -05:00
Charlie Marsh
058a5276b0 Bump version to 0.0.119 2022-11-14 21:45:41 -05:00
Charlie Marsh
62d4096be3 Move bindings to FNV map (#747) 2022-11-14 21:42:57 -05:00
Charlie Marsh
8961da7b89 Add support for import alias tracking (#746) 2022-11-14 21:29:30 -05:00
Brett Cannon
58bcffbe2d Add isort to the README's ToC (#745) 2022-11-14 18:51:39 -05:00
Charlie Marsh
f67727b13c Improve performance of import matching code (#744) 2022-11-14 17:14:22 -05:00
Charlie Marsh
fea029ae35 Bump version to 0.0.118 2022-11-14 13:21:27 -05:00
Harutaka Kawamura
3e3c3c7421 Ignore namedtuple assignment in N806, N815, and N816 (#735) 2022-11-14 13:21:04 -05:00
Harutaka Kawamura
9047bf680d Implement B024 and B027 (#738) 2022-11-14 13:12:23 -05:00
Charlie Marsh
d170388b7b Allow second line as 'first line' for punctuation (#741) 2022-11-14 13:07:27 -05:00
Charlie Marsh
502d3316f9 Add flake8-bugbear settings to hash (#739) 2022-11-14 12:29:47 -05:00
Harutaka Kawamura
a8159f9893 Implement B022 (#734) 2022-11-14 09:24:05 -05:00
Charlie Marsh
71f727c380 Use FNV hasher in more places (#732) 2022-11-13 23:44:16 -05:00
Charlie Marsh
ce3c45a361 Make combine-as-imports the default import sorting behavior (#731) 2022-11-13 23:07:13 -05:00
Charlie Marsh
29ae6c159d Add FastAPI to README (#730) 2022-11-13 22:27:35 -05:00
Charlie Marsh
1ae07b4c70 Allow explicit re-export of straight imports (#729) 2022-11-13 22:26:48 -05:00
82 changed files with 2063 additions and 745 deletions

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.117
rev: v0.0.121
hooks:
- id: ruff

8
Cargo.lock generated
View File

@@ -930,11 +930,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.117-dev.0"
version = "0.0.121-dev.0"
dependencies = [
"anyhow",
"clap 4.0.22",
"configparser",
"fnv",
"once_cell",
"regex",
"ruff",
@@ -2237,10 +2238,11 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.117"
version = "0.0.121"
dependencies = [
"anyhow",
"assert_cmd",
"atty",
"bincode",
"bitflags",
"cacache",
@@ -2285,7 +2287,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.117"
version = "0.0.121"
dependencies = [
"anyhow",
"clap 4.0.22",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.117"
version = "0.0.121"
edition = "2021"
[lib]
@@ -14,6 +14,7 @@ name = "ruff"
[dependencies]
anyhow = { version = "1.0.66" }
atty = { version = "0.2.14" }
bincode = { version = "1.3.3" }
bitflags = { version = "1.3.2" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }

View File

@@ -36,8 +36,8 @@ faster than any individual tool.
automatically convert your existing configuration.)
Ruff is actively developed and used in major open-source projects
like [Zulip](https://github.com/zulip/zulip), [pydantic](https://github.com/pydantic/pydantic),
and [Saleor](https://github.com/saleor/saleor).
like [FastAPI](https://github.com/tiangolo/fastapi), [Zulip](https://github.com/zulip/zulip),
[pydantic](https://github.com/pydantic/pydantic), and [Saleor](https://github.com/saleor/saleor).
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -48,18 +48,20 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
3. [Supported Rules](#supported-rules)
1. [Pyflakes](#pyflakes)
2. [pycodestyle](#pycodestyle)
3. [pydocstyle](#pydocstyle)
4. [pyupgrade](#pyupgrade)
5. [pep8-naming](#pep8-naming)
6. [flake8-comprehensions](#flake8-comprehensions)
7. [flake8-bugbear](#flake8-bugbear)
8. [flake8-builtins](#flake8-builtins)
9. [flake8-print](#flake8-print)
10. [flake8-quotes](#flake8-quotes)
11. [flake8-annotations](#flake8-annotations)
12. [flake8-2020](#flake8-2020)
13. [Ruff-specific rules](#ruff-specific-rules)
14. [Meta rules](#meta-rules)
3. [isort](#isort)
4. [pydocstyle](#pydocstyle)
5. [pyupgrade](#pyupgrade)
6. [pep8-naming](#pep8-naming)
7. [flake8-bandit](#flake8-bandit)
8. [flake8-comprehensions](#flake8-comprehensions)
9. [flake8-bugbear](#flake8-bugbear)
10. [flake8-builtins](#flake8-builtins)
11. [flake8-print](#flake8-print)
12. [flake8-quotes](#flake8-quotes)
13. [flake8-annotations](#flake8-annotations)
14. [flake8-2020](#flake8-2020)
15. [Ruff-specific rules](#ruff-specific-rules)
16. [Meta rules](#meta-rules)
5. [Editor Integrations](#editor-integrations)
6. [FAQ](#faq)
7. [Development](#development)
@@ -235,6 +237,8 @@ Options:
Regular expression matching the name of dummy variables
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
--line-length <LINE_LENGTH>
Set the line-length for length-associated checks and automatic formatting
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
-h, --help
@@ -527,9 +531,13 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
| B017 | NoAssertRaisesException | `assertRaises(Exception)` should be considered evil | |
| B018 | UselessExpression | Found useless expression. Either assign it to a variable or remove it. | |
| B019 | CachedInstanceMethod | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks | |
| B020 | LoopVariableOverridesIterator | Loop control variable `...` overrides iterable it iterates | |
| B021 | FStringDocstring | f-string used as docstring. This will be interpreted by python as a joined string rather than a docstring. | |
| B022 | UselessContextlibSuppress | No arguments passed to `contextlib.suppress`. No exceptions will be suppressed and therefore this context manager is redundant | |
| B024 | AbstractBaseClassWithoutAbstractMethod | `...` is an abstract base class, but it has no abstract methods | |
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged | |
| B027 | EmptyMethodWithoutAbstractDecorator | `...` is an empty method in an abstract base class, but has no abstract decorator | |
### flake8-builtins
@@ -702,7 +710,7 @@ including:
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (22/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (25/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (15/34)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
@@ -727,7 +735,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (22/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (26/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
Ruff can also replace [`isort`](https://pypi.org/project/isort/), [`yesqa`](https://github.com/asottile/yesqa),
@@ -753,9 +761,8 @@ project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
### How does Ruff's import sorting compare to [`isort`](https://pypi.org/project/isort/)?
Ruff's import sorting is intended to be equivalent to `isort` when used `profile = "black"`, and a
few other settings (`combine_as_imports = true`, `order_by_type = false`, and
`case_sensitive = true`).
Ruff's import sorting is intended to be nearly equivalent to `isort` when used `profile = "black"`.
(There are some minor differences in how Ruff and isort break ties between similar imports.)
Like `isort`, Ruff's import sorting is compatible with Black.

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.117"
version = "0.0.121"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.117"
version = "0.0.121"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.117-dev.0"
version = "0.0.121-dev.0"
edition = "2021"
[lib]
@@ -10,6 +10,7 @@ name = "flake8_to_ruff"
anyhow = { version = "1.0.66" }
clap = { version = "4.0.1", features = ["derive"] }
configparser = { version = "3.0.2" }
fnv = { version = "1.0.7" }
once_cell = { version = "1.16.0" }
regex = { version = "1.6.0" }
ruff = { path = "..", default-features = false }

View File

@@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use std::str::FromStr;
use anyhow::Result;
use fnv::FnvHashMap;
use once_cell::sync::Lazy;
use regex::Regex;
use ruff::checks_gen::CheckCodePrefix;
@@ -179,8 +179,8 @@ pub fn parse_files_to_codes_mapping(value: &str) -> Result<Vec<PatternPrefixPair
/// Collect a list of `PatternPrefixPair` structs as a `BTreeMap`.
pub fn collect_per_file_ignores(
pairs: Vec<PatternPrefixPair>,
) -> BTreeMap<String, Vec<CheckCodePrefix>> {
let mut per_file_ignores: BTreeMap<String, Vec<CheckCodePrefix>> = BTreeMap::new();
) -> FnvHashMap<String, Vec<CheckCodePrefix>> {
let mut per_file_ignores: FnvHashMap<String, Vec<CheckCodePrefix>> = FnvHashMap::default();
for pair in pairs {
per_file_ignores
.entry(pair.pattern)

40
resources/test/fixtures/B020.py vendored Normal file
View File

@@ -0,0 +1,40 @@
"""
Should emit:
B020 - on lines 8, 21, and 36
"""
items = [1, 2, 3]
for items in items:
print(items)
items = [1, 2, 3]
for item in items:
print(item)
values = {"secret": 123}
for key, value in values.items():
print(f"{key}, {value}")
for key, values in values.items():
print(f"{key}, {values}")
# Variables defined in a comprehension are local in scope
# to that comprehension and are therefore allowed.
for var in [var for var in range(10)]:
print(var)
for var in (var for var in range(10)):
print(var)
for k, v in {k: v for k, v in zip(range(10), range(10, 20))}.items():
print(k, v)
# However we still call out reassigning the iterable in the comprehension.
for vars in [i for i in vars]:
print(vars)
for var in sorted(range(10), key=lambda var: var.real):
print(var)

23
resources/test/fixtures/B022.py vendored Normal file
View File

@@ -0,0 +1,23 @@
"""
Should emit:
B022 - on lines 8
"""
import contextlib
from contextlib import suppress
with contextlib.suppress():
raise ValueError
with suppress():
raise ValueError
with contextlib.suppress(ValueError):
raise ValueError
exceptions_to_suppress = []
if True:
exceptions_to_suppress.append(ValueError)
with contextlib.suppress(*exceptions_to_suppress):
raise

129
resources/test/fixtures/B024.py vendored Normal file
View File

@@ -0,0 +1,129 @@
"""
Should emit:
B024 - on lines 17, 34, 52, 58, 69, 74, 79, 84, 89
"""
import abc
import abc as notabc
from abc import ABC, ABCMeta
from abc import abstractmethod
from abc import abstractmethod as abstract
from abc import abstractmethod as abstractaoeuaoeuaoeu
from abc import abstractmethod as notabstract
import foo
class Base_1(ABC): # error
def method(self):
foo()
class Base_2(ABC):
@abstractmethod
def method(self):
foo()
class Base_3(ABC):
@abc.abstractmethod
def method(self):
foo()
class Base_4(ABC):
@notabc.abstractmethod
def method(self):
foo()
class Base_5(ABC):
@abstract
def method(self):
foo()
class Base_6(ABC):
@abstractaoeuaoeuaoeu
def method(self):
foo()
class Base_7(ABC): # error
@notabstract
def method(self):
foo()
class MetaBase_1(metaclass=ABCMeta): # error
def method(self):
foo()
class MetaBase_2(metaclass=ABCMeta):
@abstractmethod
def method(self):
foo()
class abc_Base_1(abc.ABC): # error
def method(self):
foo()
class abc_Base_2(metaclass=abc.ABCMeta): # error
def method(self):
foo()
class notabc_Base_1(notabc.ABC): # error
def method(self):
foo()
class multi_super_1(notabc.ABC, abc.ABCMeta): # safe
def method(self):
foo()
class multi_super_2(notabc.ABC, metaclass=abc.ABCMeta): # safe
def method(self):
foo()
class non_keyword_abcmeta_1(ABCMeta): # safe
def method(self):
foo()
class non_keyword_abcmeta_2(abc.ABCMeta): # safe
def method(self):
foo()
# very invalid code, but that's up to mypy et al to check
class keyword_abc_1(metaclass=ABC): # safe
def method(self):
foo()
class keyword_abc_2(metaclass=abc.ABC): # safe
def method(self):
foo()
class abc_set_class_variable_1(ABC): # safe
foo: int
class abc_set_class_variable_2(ABC): # safe
foo = 2
class abc_set_class_variable_3(ABC): # safe
foo: int = 2
# this doesn't actually declare a class variable, it's just an expression
class abc_set_class_variable_4(ABC): # error
foo

88
resources/test/fixtures/B027.py vendored Normal file
View File

@@ -0,0 +1,88 @@
"""
Should emit:
B027 - on lines 12, 15, 18, 22, 30
"""
import abc
from abc import ABC
from abc import abstractmethod
from abc import abstractmethod as notabstract
class AbstractClass(ABC):
def empty_1(self): # error
...
def empty_2(self): # error
pass
def empty_3(self): # error
"""docstring"""
...
def empty_4(self): # error
"""multiple ellipsis/pass"""
...
pass
...
pass
@notabstract
def abstract_0(self):
...
@abstractmethod
def abstract_1(self):
...
@abstractmethod
def abstract_2(self):
pass
@abc.abstractmethod
def abstract_3(self):
...
def body_1(self):
print("foo")
...
def body_2(self):
self.body_1()
class NonAbstractClass:
def empty_1(self): # safe
...
def empty_2(self): # safe
pass
# ignore @overload, fixes issue #304
# ignore overload with other imports, fixes #308
import typing
import typing as t
import typing as anything
from typing import Union, overload
class AbstractClass(ABC):
@overload
def empty_1(self, foo: str):
...
@typing.overload
def empty_1(self, foo: int):
...
@t.overload
def empty_1(self, foo: list):
...
@anything.overload
def empty_1(self, foo: float):
...
@abstractmethod
def empty_1(self, foo: Union[str, int, list, float]):
...

View File

@@ -8,3 +8,13 @@ from .background import BackgroundTasks
# F401 `datastructures.UploadFile` imported but unused
from .datastructures import UploadFile as FileUpload
# OK
import applications as applications
# F401 `background` imported but unused
import background
# F401 `datastructures` imported but unused
import datastructures as structures

24
resources/test/fixtures/F821_4.py vendored Normal file
View File

@@ -0,0 +1,24 @@
"""Test: import alias tracking."""
from typing import List
_ = List["Model"]
from typing import List as IList
_ = IList["Model"]
from collections.abc import ItemsView
_ = ItemsView["Model"]
import collections.abc
_ = collections.abc.ItemsView["Model"]
from collections import abc
_ = abc.ItemsView["Model"]

14
resources/test/fixtures/F821_5.py vendored Normal file
View File

@@ -0,0 +1,14 @@
"""Test: inner class annotation."""
class RandomClass:
def random_func(self) -> "InnerClass":
pass
class OuterClass:
class InnerClass:
pass
def failing_func(self) -> "InnerClass":
return self.InnerClass()

View File

@@ -1,5 +1,11 @@
import collections
from collections import namedtuple
def f():
lower = 0
Camel = 0
CONSTANT = 0
_ = 0
MyObj1 = collections.namedtuple("MyObj1", ["a", "b"])
MyObj2 = namedtuple("MyObj12", ["a", "b"])

View File

@@ -1,6 +1,12 @@
import collections
from collections import namedtuple
class C:
lower = 0
CONSTANT = 0
mixedCase = 0
_mixedCase = 0
mixed_Case = 0
myObj1 = collections.namedtuple("MyObj1", ["a", "b"])
myObj2 = namedtuple("MyObj2", ["a", "b"])

View File

@@ -1,5 +1,10 @@
import collections
from collections import namedtuple
lower = 0
CONSTANT = 0
mixedCase = 0
_mixedCase = 0
mixed_Case = 0
myObj1 = collections.namedtuple("MyObj1", ["a", "b"])
myObj2 = namedtuple("MyObj2", ["a", "b"])

View File

@@ -1,3 +1,10 @@
import typing
def f(x: typing.List[str]) -> None:
...
from typing import List
@@ -5,8 +12,15 @@ def f(x: List[str]) -> None:
...
import typing
import typing as t
def f(x: typing.List[str]) -> None:
def f(x: t.List[str]) -> None:
...
from typing import List as IList
def f(x: IList[str]) -> None:
...

View File

@@ -5,9 +5,8 @@ print(sys.version)
print(sys.version[:3])
print(version[:3])
# ignore from imports with aliases, patches welcome
print(v[:3])
# the tool is timid and only flags certain numeric slices
i = 3
print(sys.version[:i])

View File

@@ -0,0 +1,26 @@
from a import b
from a import BAD as DEF
from a import B
from a import Boo as DEF
from a import B as Abc
from a import B as A
from a import B as DEF
from a import b as a
from a import b as x
from a import b as c
from b import c
from a import b as d
from a import b as y
from b import C
from b import c as d
import A
import a
import b
import B
import x as y
import x as A
import x as Y
import x
import x as a

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.117"
version = "0.0.121"
edition = "2021"
[dependencies]

View File

@@ -3,13 +3,14 @@ use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, StmtKind};
fn compose_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
#[inline(always)]
fn collect_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
match &expr.node {
ExprKind::Call { func, .. } => {
compose_call_path_inner(func, parts);
collect_call_path_inner(func, parts);
}
ExprKind::Attribute { value, attr, .. } => {
compose_call_path_inner(value, parts);
collect_call_path_inner(value, parts);
parts.push(attr);
}
ExprKind::Name { id, .. } => {
@@ -20,9 +21,9 @@ fn compose_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
}
/// Convert an `Expr` to its call path (like `List`, or `typing.List`).
#[inline(always)]
pub fn compose_call_path(expr: &Expr) -> Option<String> {
let mut segments = vec![];
compose_call_path_inner(expr, &mut segments);
let segments = collect_call_paths(expr);
if segments.is_empty() {
None
} else {
@@ -30,6 +31,34 @@ pub fn compose_call_path(expr: &Expr) -> Option<String> {
}
}
/// Convert an `Expr` to its call path segments (like ["typing", "List"]).
#[inline(always)]
pub fn collect_call_paths(expr: &Expr) -> Vec<&str> {
let mut segments = vec![];
collect_call_path_inner(expr, &mut segments);
segments
}
/// Rewrite any import aliases on a call path.
pub fn dealias_call_path<'a>(
call_path: Vec<&'a str>,
import_aliases: &FnvHashMap<&str, &'a str>,
) -> Vec<&'a str> {
if let Some(head) = call_path.first() {
if let Some(origin) = import_aliases.get(head) {
let tail = &call_path[1..];
let mut call_path: Vec<&str> = vec![];
call_path.extend(origin.split('.'));
call_path.extend(tail);
call_path
} else {
call_path
}
} else {
call_path
}
}
/// Return `true` if the `Expr` is a name or attribute reference to `${target}`.
pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
match &expr.node {
@@ -39,6 +68,94 @@ pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
}
}
/// Return `true` if the `Expr` is a reference to `${module}.${target}`.
///
/// Useful for, e.g., ensuring that a `Union` reference represents
/// `typing.Union`.
pub fn match_module_member(
expr: &Expr,
module: &str,
member: &str,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
match_call_path(
&dealias_call_path(collect_call_paths(expr), import_aliases),
module,
member,
from_imports,
)
}
/// Return `true` if the `call_path` is a reference to `${module}.${target}`.
///
/// Optimized version of `match_module_member` for pre-computed call paths.
pub fn match_call_path(
call_path: &[&str],
module: &str,
member: &str,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> bool {
// If we have no segments, we can't ever match.
let num_segments = call_path.len();
if num_segments == 0 {
return false;
}
// If the last segment doesn't match the member, we can't ever match.
if call_path[num_segments - 1] != member {
return false;
}
// We now only need the module path, so throw out the member name.
let call_path = &call_path[..num_segments - 1];
let num_segments = call_path.len();
// Case (1): It's a builtin (like `list`).
// Case (2a): We imported from the parent (`from typing.re import Match`,
// `Match`).
// Case (2b): We imported star from the parent (`from typing.re import *`,
// `Match`).
if num_segments == 0 {
module.is_empty()
|| from_imports
.get(module)
.map(|imports| imports.contains(member) || imports.contains("*"))
.unwrap_or(false)
} else {
let components: Vec<&str> = module.split('.').collect();
// Case (3a): it's a fully qualified call path (`import typing`,
// `typing.re.Match`). Case (3b): it's a fully qualified call path (`import
// typing.re`, `typing.re.Match`).
if components == call_path {
return true;
}
// Case (4): We imported from the grandparent (`from typing import re`,
// `re.Match`)
let num_matches = (0..components.len())
.take(num_segments)
.take_while(|i| components[components.len() - 1 - i] == call_path[num_segments - 1 - i])
.count();
if num_matches > 0 {
let cut = components.len() - num_matches;
// TODO(charlie): Rewrite to avoid this allocation.
let module = components[..cut].join(".");
let member = components[cut];
if from_imports
.get(&module.as_str())
.map(|imports| imports.contains(member))
.unwrap_or(false)
{
return true;
}
}
false
}
}
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
@@ -72,7 +189,7 @@ pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
}
/// Extract the names of all handled exceptions.
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<String> {
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<Vec<&str>> {
let mut handler_names = vec![];
for handler in handlers {
match &handler.node {
@@ -80,12 +197,16 @@ pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<String> {
if let Some(type_) = type_ {
if let ExprKind::Tuple { elts, .. } = &type_.node {
for type_ in elts {
if let Some(name) = compose_call_path(type_) {
handler_names.push(name);
let call_path = collect_call_paths(type_);
if !call_path.is_empty() {
handler_names.push(call_path);
}
}
} else if let Some(name) = compose_call_path(type_) {
handler_names.push(name);
} else {
let call_path = collect_call_paths(type_);
if !call_path.is_empty() {
handler_names.push(call_path);
}
}
}
}
@@ -117,78 +238,6 @@ pub fn to_absolute(relative: &Location, base: &Location) -> Location {
}
}
/// Return `true` if the `Expr` is a reference to `${module}.${target}`.
///
/// Useful for, e.g., ensuring that a `Union` reference represents
/// `typing.Union`.
pub fn match_module_member(
expr: &Expr,
target: &str,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> bool {
compose_call_path(expr)
.map(|expr| match_call_path(&expr, target, from_imports))
.unwrap_or(false)
}
/// Return `true` if the `call_path` is a reference to `${module}.${target}`.
///
/// Optimized version of `match_module_member` for pre-computed call paths.
pub fn match_call_path(
call_path: &str,
target: &str,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> bool {
// Case (1a): it's the same call path (`import typing`, `typing.re.Match`).
// Case (1b): it's the same call path (`import typing.re`, `typing.re.Match`).
if call_path == target {
return true;
}
if let Some((parent, member)) = target.rsplit_once('.') {
// Case (2): We imported star from the parent (`from typing.re import *`,
// `Match`).
if call_path == member
&& from_imports
.get(parent)
.map(|imports| imports.contains("*"))
.unwrap_or(false)
{
return true;
}
// Case (3): We imported from the parent (`from typing.re import Match`,
// `Match`)
if call_path == member
&& from_imports
.get(parent)
.map(|imports| imports.contains(member))
.unwrap_or(false)
{
return true;
}
}
// Case (4): We imported from the grandparent (`from typing import re`,
// `re.Match`)
let mut parts = target.rsplitn(3, '.');
let member = parts.next();
let parent = parts.next();
let grandparent = parts.next();
if let (Some(member), Some(parent), Some(grandparent)) = (member, parent, grandparent) {
if call_path == format!("{parent}.{member}")
&& from_imports
.get(grandparent)
.map(|imports| imports.contains(parent))
.unwrap_or(false)
{
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use anyhow::Result;
@@ -197,13 +246,28 @@ mod tests {
use crate::ast::helpers::match_module_member;
#[test]
fn builtin() -> Result<()> {
let expr = parser::parse_expression("list", "<filename>")?;
assert!(match_module_member(
&expr,
"",
"list",
&FnvHashMap::default(),
&FnvHashMap::default(),
));
Ok(())
}
#[test]
fn fully_qualified() -> Result<()> {
let expr = parser::parse_expression("typing.re.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.Match",
&FnvHashMap::default()
"typing.re",
"Match",
&FnvHashMap::default(),
&FnvHashMap::default(),
));
Ok(())
}
@@ -213,13 +277,17 @@ mod tests {
let expr = parser::parse_expression("Match", "<filename>")?;
assert!(!match_module_member(
&expr,
"typing.re.Match",
"typing.re",
"Match",
&FnvHashMap::default(),
&FnvHashMap::default(),
));
let expr = parser::parse_expression("re.Match", "<filename>")?;
assert!(!match_module_member(
&expr,
"typing.re.Match",
"typing.re",
"Match",
&FnvHashMap::default(),
&FnvHashMap::default(),
));
Ok(())
@@ -230,8 +298,10 @@ mod tests {
let expr = parser::parse_expression("Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["*"]))])
"typing.re",
"Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["*"]))]),
&FnvHashMap::default()
));
Ok(())
}
@@ -241,8 +311,10 @@ mod tests {
let expr = parser::parse_expression("Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["Match"]))])
"typing.re",
"Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["Match"]))]),
&FnvHashMap::default()
));
Ok(())
}
@@ -252,8 +324,67 @@ mod tests {
let expr = parser::parse_expression("re.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.Match",
&FnvHashMap::from_iter([("typing", FnvHashSet::from_iter(["re"]))])
"typing.re",
"Match",
&FnvHashMap::from_iter([("typing", FnvHashSet::from_iter(["re"]))]),
&FnvHashMap::default()
));
let expr = parser::parse_expression("match.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.match",
"Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["match"]))]),
&FnvHashMap::default()
));
let expr = parser::parse_expression("re.match.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.match",
"Match",
&FnvHashMap::from_iter([("typing", FnvHashSet::from_iter(["re"]))]),
&FnvHashMap::default()
));
Ok(())
}
#[test]
fn from_alias() -> Result<()> {
let expr = parser::parse_expression("IMatch", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["Match"]))]),
&FnvHashMap::from_iter([("IMatch", "Match")]),
));
Ok(())
}
#[test]
fn from_aliased_parent() -> Result<()> {
let expr = parser::parse_expression("t.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::default(),
&FnvHashMap::from_iter([("t", "typing.re")]),
));
Ok(())
}
#[test]
fn from_aliased_grandparent() -> Result<()> {
let expr = parser::parse_expression("t.re.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::default(),
&FnvHashMap::from_iter([("t", "typing")]),
));
Ok(())
}

View File

@@ -1,6 +1,6 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use fnv::FnvHashMap;
use rustpython_ast::{Expr, Keyword};
use rustpython_parser::ast::{Located, Location};
@@ -54,7 +54,7 @@ pub struct Scope<'a> {
pub id: usize,
pub kind: ScopeKind<'a>,
pub import_starred: bool,
pub values: BTreeMap<String, Binding>,
pub values: FnvHashMap<String, Binding>,
}
impl<'a> Scope<'a> {
@@ -63,7 +63,7 @@ impl<'a> Scope<'a> {
id: id(),
kind,
import_starred: false,
values: BTreeMap::new(),
values: FnvHashMap::default(),
}
}
}

View File

@@ -61,7 +61,7 @@ fn apply_fixes<'a>(
) -> Cow<'a, str> {
let mut output = RopeBuilder::new();
let mut last_pos: Location = Location::new(1, 0);
let mut applied: BTreeSet<&Patch> = BTreeSet::new();
let mut applied: BTreeSet<&Patch> = BTreeSet::default();
for fix in fixes.sorted_by_key(|fix| fix.patch.location) {
// If we already applied an identical fix as part of another correction, skip

View File

@@ -23,7 +23,7 @@ use crate::autofix::fixer;
use crate::message::Message;
use crate::settings::Settings;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Serialize, Deserialize)]
struct CacheMetadata {
@@ -89,7 +89,7 @@ fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String
format!(
"{}@{}@{}",
path.absolutize().unwrap().to_string_lossy(),
VERSION,
CARGO_PKG_VERSION,
hasher.finish()
)
}

View File

@@ -5,6 +5,7 @@ use std::ops::Deref;
use std::path::Path;
use fnv::{FnvHashMap, FnvHashSet};
use itertools::Itertools;
use log::error;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
@@ -12,7 +13,9 @@ use rustpython_parser::ast::{
};
use rustpython_parser::parser;
use crate::ast::helpers::{extract_handler_names, match_module_member};
use crate::ast::helpers::{
collect_call_paths, dealias_call_path, extract_handler_names, match_call_path,
};
use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
@@ -54,6 +57,7 @@ pub struct Checker<'a> {
pub(crate) deletions: FnvHashSet<usize>,
// Import tracking.
pub(crate) from_imports: FnvHashMap<&'a str, FnvHashSet<&'a str>>,
pub(crate) import_aliases: FnvHashMap<&'a str, &'a str>,
// Retain all scopes and parent nodes, along with a stack of indexes to track which are active
// at various points in time.
pub(crate) parents: Vec<&'a Stmt>,
@@ -61,7 +65,7 @@ pub struct Checker<'a> {
scopes: Vec<Scope<'a>>,
scope_stack: Vec<usize>,
dead_scopes: Vec<usize>,
deferred_string_annotations: Vec<(Range, &'a str)>,
deferred_string_annotations: Vec<(Range, &'a str, Vec<usize>, Vec<usize>)>,
deferred_annotations: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
deferred_functions: Vec<(&'a Stmt, Vec<usize>, Vec<usize>, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
@@ -75,7 +79,7 @@ pub struct Checker<'a> {
seen_import_boundary: bool,
futures_allowed: bool,
annotations_future_enabled: bool,
except_handlers: Vec<Vec<String>>,
except_handlers: Vec<Vec<Vec<&'a str>>>,
}
impl<'a> Checker<'a> {
@@ -94,6 +98,7 @@ impl<'a> Checker<'a> {
definitions: Default::default(),
deletions: Default::default(),
from_imports: Default::default(),
import_aliases: Default::default(),
parents: Default::default(),
parent_stack: Default::default(),
scopes: Default::default(),
@@ -154,14 +159,16 @@ impl<'a> Checker<'a> {
}
/// Return `true` if the `Expr` is a reference to `typing.${target}`.
pub fn match_typing_module(&self, expr: &Expr, target: &str) -> bool {
match_module_member(expr, &format!("typing.{target}"), &self.from_imports)
pub fn match_typing_expr(&self, expr: &Expr, target: &str) -> bool {
let call_path = dealias_call_path(collect_call_paths(expr), &self.import_aliases);
self.match_typing_call_path(&call_path, target)
}
/// Return `true` if the call path is a reference to `typing.${target}`.
pub fn match_typing_call_path(&self, call_path: &[&str], target: &str) -> bool {
match_call_path(call_path, "typing", target, &self.from_imports)
|| (typing::in_extensions(target)
&& match_module_member(
expr,
&format!("typing_extensions.{target}"),
&self.from_imports,
))
&& match_call_path(call_path, "typing_extensions", target, &self.from_imports))
}
}
@@ -452,6 +459,14 @@ where
flake8_bugbear::plugins::useless_expression(self, body);
}
if self.settings.enabled.contains(&CheckCode::B024)
|| self.settings.enabled.contains(&CheckCode::B027)
{
flake8_bugbear::plugins::abstract_base_class(
self, stmt, name, bases, keywords, body,
);
}
self.check_builtin_shadowing(name, Range::from_located(stmt), false);
for expr in bases {
@@ -516,13 +531,38 @@ where
full_name.to_string(),
self.binding_context(),
),
used: None,
// Treat explicit re-export as usage (e.g., `import applications
// as applications`).
used: if alias
.node
.asname
.as_ref()
.map(|asname| asname == &alias.node.name)
.unwrap_or(false)
{
Some((
self.scopes[*(self
.scope_stack
.last()
.expect("No current scope found."))]
.id,
Range::from_located(stmt),
))
} else {
None
},
range: Range::from_located(stmt),
},
)
}
if let Some(asname) = &alias.node.asname {
for alias in names {
if let Some(asname) = &alias.node.asname {
self.import_aliases.insert(asname, &alias.node.name);
}
}
let name = alias.node.name.split('.').last().unwrap();
if self.settings.enabled.contains(&CheckCode::N811) {
if let Some(check) =
@@ -591,6 +631,11 @@ where
.map(|alias| alias.node.name.as_str()),
)
}
for alias in names {
if let Some(asname) = &alias.node.asname {
self.import_aliases.insert(asname, &alias.node.name);
}
}
}
if self.settings.enabled.contains(&CheckCode::E402) {
@@ -828,10 +873,15 @@ where
flake8_bugbear::plugins::assert_raises_exception(self, stmt, items);
}
}
StmtKind::For { target, body, .. } => {
StmtKind::For {
target, body, iter, ..
} => {
if self.settings.enabled.contains(&CheckCode::B007) {
flake8_bugbear::plugins::unused_loop_control_variable(self, target, body);
}
if self.settings.enabled.contains(&CheckCode::B020) {
flake8_bugbear::plugins::loop_variable_overrides_iterator(self, target, iter);
}
}
StmtKind::Try { handlers, .. } => {
if self.settings.enabled.contains(&CheckCode::F707) {
@@ -997,8 +1047,12 @@ where
..
} = &expr.node
{
self.deferred_string_annotations
.push((Range::from_located(expr), value));
self.deferred_string_annotations.push((
Range::from_located(expr),
value,
self.scope_stack.clone(),
self.parent_stack.clone(),
));
} else {
self.deferred_annotations.push((
expr,
@@ -1014,12 +1068,12 @@ where
ExprKind::Subscript { value, slice, .. } => {
// Ex) typing.List[...]
if self.settings.enabled.contains(&CheckCode::U007)
&& self.settings.target_version >= PythonVersion::Py39
&& self.settings.target_version >= PythonVersion::Py310
{
pyupgrade::plugins::use_pep604_annotation(self, expr, value, slice);
}
if self.match_typing_module(value, "Literal") {
if self.match_typing_expr(value, "Literal") {
self.in_literal = true;
}
@@ -1053,7 +1107,11 @@ where
// Ex) List[...]
if self.settings.enabled.contains(&CheckCode::U006)
&& self.settings.target_version >= PythonVersion::Py39
&& typing::is_pep585_builtin(expr, &self.from_imports)
&& typing::is_pep585_builtin(
expr,
&self.from_imports,
&self.import_aliases,
)
{
pyupgrade::plugins::use_pep585_annotation(self, expr, id);
}
@@ -1085,7 +1143,7 @@ where
// Ex) typing.List[...]
if self.settings.enabled.contains(&CheckCode::U006)
&& self.settings.target_version >= PythonVersion::Py39
&& typing::is_pep585_builtin(expr, &self.from_imports)
&& typing::is_pep585_builtin(expr, &self.from_imports, &self.import_aliases)
{
pyupgrade::plugins::use_pep585_annotation(self, expr, attr);
}
@@ -1138,6 +1196,9 @@ where
flake8_bugbear::plugins::setattr_with_constant(self, expr, func, args);
}
}
if self.settings.enabled.contains(&CheckCode::B022) {
flake8_bugbear::plugins::useless_contextlib_suppress(self, expr, args);
}
if self.settings.enabled.contains(&CheckCode::B026) {
flake8_bugbear::plugins::star_arg_unpacking_after_keyword_arg(
self, args, keywords,
@@ -1517,8 +1578,12 @@ where
..
} => {
if self.in_annotation && !self.in_literal {
self.deferred_string_annotations
.push((Range::from_located(expr), value));
self.deferred_string_annotations.push((
Range::from_located(expr),
value,
self.scope_stack.clone(),
self.parent_stack.clone(),
));
}
if self.settings.enabled.contains(&CheckCode::S104) {
if let Some(check) = flake8_bandit::plugins::hardcoded_bind_all_interfaces(
@@ -1599,12 +1664,13 @@ where
args,
keywords,
} => {
if self.match_typing_module(func, "ForwardRef") {
let call_path = dealias_call_path(collect_call_paths(func), &self.import_aliases);
if self.match_typing_call_path(&call_path, "ForwardRef") {
self.visit_expr(func);
for expr in args {
self.visit_annotation(expr);
}
} else if self.match_typing_module(func, "cast") {
} else if self.match_typing_call_path(&call_path, "cast") {
self.visit_expr(func);
if !args.is_empty() {
self.visit_annotation(&args[0]);
@@ -1612,12 +1678,12 @@ where
for expr in args.iter().skip(1) {
self.visit_expr(expr);
}
} else if self.match_typing_module(func, "NewType") {
} else if self.match_typing_call_path(&call_path, "NewType") {
self.visit_expr(func);
for expr in args.iter().skip(1) {
self.visit_annotation(expr);
}
} else if self.match_typing_module(func, "TypeVar") {
} else if self.match_typing_call_path(&call_path, "TypeVar") {
self.visit_expr(func);
for expr in args.iter().skip(1) {
self.visit_annotation(expr);
@@ -1634,7 +1700,7 @@ where
}
}
}
} else if self.match_typing_module(func, "NamedTuple") {
} else if self.match_typing_call_path(&call_path, "NamedTuple") {
self.visit_expr(func);
// Ex) NamedTuple("a", [("a", int)])
@@ -1666,7 +1732,7 @@ where
let KeywordData { value, .. } = &keyword.node;
self.visit_annotation(value);
}
} else if self.match_typing_module(func, "TypedDict") {
} else if self.match_typing_call_path(&call_path, "TypedDict") {
self.visit_expr(func);
// Ex) TypedDict("a", {"a": int})
@@ -1705,7 +1771,11 @@ where
visitor::walk_expr(self, expr);
} else {
self.in_subscript = true;
match typing::match_annotated_subscript(value, &self.from_imports) {
match typing::match_annotated_subscript(
value,
&self.from_imports,
&self.import_aliases,
) {
Some(subscript) => {
match subscript {
// Ex) Optional[int]
@@ -2071,6 +2141,7 @@ impl<'a> Checker<'a> {
let mut import_starred = false;
for scope_index in self.scope_stack.iter().rev() {
let scope = &mut self.scopes[*scope_index];
if matches!(scope.kind, ScopeKind::Class(_)) {
if id == "__class__" {
return;
@@ -2117,7 +2188,10 @@ impl<'a> Checker<'a> {
// Avoid flagging if NameError is handled.
if let Some(handler_names) = self.except_handlers.last() {
if handler_names.contains(&"NameError".to_string()) {
if handler_names
.iter()
.any(|call_path| call_path.len() == 1 && call_path[0] == "NameError")
{
return;
}
}
@@ -2144,32 +2218,22 @@ impl<'a> Checker<'a> {
}
if self.settings.enabled.contains(&CheckCode::N806) {
let current =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if let Some(check) =
pep8_naming::checks::non_lowercase_variable_in_function(current, expr, id)
{
self.add_check(check);
if matches!(self.current_scope().kind, ScopeKind::Function(..)) {
pep8_naming::plugins::non_lowercase_variable_in_function(self, expr, parent, id)
}
}
if self.settings.enabled.contains(&CheckCode::N815) {
let current =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if let Some(check) =
pep8_naming::checks::mixed_case_variable_in_class_scope(current, expr, id)
{
self.add_check(check);
if matches!(self.current_scope().kind, ScopeKind::Class(..)) {
pep8_naming::plugins::mixed_case_variable_in_class_scope(self, expr, parent, id)
}
}
if self.settings.enabled.contains(&CheckCode::N816) {
let current =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if let Some(check) =
pep8_naming::checks::mixed_case_variable_in_global_scope(current, expr, id)
{
self.add_check(check);
if matches!(self.current_scope().kind, ScopeKind::Module) {
pep8_naming::plugins::mixed_case_variable_in_global_scope(
self, expr, parent, id,
)
}
}
@@ -2294,8 +2358,8 @@ impl<'a> Checker<'a> {
fn check_deferred_annotations(&mut self) {
while let Some((expr, scopes, parents)) = self.deferred_annotations.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.parent_stack = parents;
self.visit_expr(expr);
}
}
@@ -2304,10 +2368,14 @@ impl<'a> Checker<'a> {
where
'b: 'a,
{
while let Some((range, expression)) = self.deferred_string_annotations.pop() {
let mut stacks = vec![];
while let Some((range, expression, scopes, parents)) =
self.deferred_string_annotations.pop()
{
if let Ok(mut expr) = parser::parse_expression(expression, "<filename>") {
relocate_expr(&mut expr, range);
allocator.push(expr);
stacks.push((scopes, parents));
} else {
if self.settings.enabled.contains(&CheckCode::F722) {
self.add_check(Check::new(
@@ -2317,7 +2385,9 @@ impl<'a> Checker<'a> {
}
}
}
for expr in allocator {
for (expr, (scopes, parents)) in allocator.iter().zip(stacks) {
self.scope_stack = scopes;
self.parent_stack = parents;
self.visit_expr(expr);
}
}
@@ -2443,7 +2513,7 @@ impl<'a> Checker<'a> {
let mut unused: BTreeMap<(ImportKind, usize, Option<usize>), Vec<&str>> =
BTreeMap::new();
for (name, binding) in scope.values.iter().rev() {
for (name, binding) in scope.values.iter() {
let used = binding.used.is_some()
|| all_names
.map(|names| names.contains(name))
@@ -2452,25 +2522,25 @@ impl<'a> Checker<'a> {
if !used {
match &binding.kind {
BindingKind::FromImportation(_, full_name, context) => {
let full_names = unused
unused
.entry((
ImportKind::ImportFrom,
context.defined_by,
context.defined_in,
))
.or_default();
full_names.push(full_name);
.or_default()
.push(full_name);
}
BindingKind::Importation(_, full_name, context)
| BindingKind::SubmoduleImportation(_, full_name, context) => {
let full_names = unused
unused
.entry((
ImportKind::Import,
context.defined_by,
context.defined_in,
))
.or_default();
full_names.push(full_name);
.or_default()
.push(full_name);
}
_ => {}
}
@@ -2511,7 +2581,7 @@ impl<'a> Checker<'a> {
if self.path.ends_with("__init__.py") {
checks.push(Check::new(
CheckKind::UnusedImport(
full_names.into_iter().map(String::from).collect(),
full_names.into_iter().sorted().map(String::from).collect(),
true,
),
Range::from_located(child),
@@ -2519,7 +2589,7 @@ impl<'a> Checker<'a> {
} else {
let mut check = Check::new(
CheckKind::UnusedImport(
full_names.into_iter().map(String::from).collect(),
full_names.into_iter().sorted().map(String::from).collect(),
false,
),
Range::from_located(child),
@@ -2712,8 +2782,5 @@ pub fn check_ast(
// Check docstrings.
checker.check_definitions();
// Check import blocks.
// checker.check_import_blocks();
checker.checks
}

View File

@@ -95,9 +95,13 @@ pub enum CheckCode {
B017,
B018,
B019,
B020,
B021,
B022,
B024,
B025,
B026,
B027,
// flake8-comprehensions
C400,
C401,
@@ -396,9 +400,13 @@ pub enum CheckKind {
NoAssertRaisesException,
UselessExpression,
CachedInstanceMethod,
LoopVariableOverridesIterator(String),
FStringDocstring,
UselessContextlibSuppress,
AbstractBaseClassWithoutAbstractMethod(String),
DuplicateTryBlockException(String),
StarArgUnpackingAfterKeywordArg,
EmptyMethodWithoutAbstractDecorator(String),
// flake8-comprehensions
UnnecessaryGeneratorList,
UnnecessaryGeneratorSet,
@@ -639,9 +647,13 @@ impl CheckCode {
CheckCode::B017 => CheckKind::NoAssertRaisesException,
CheckCode::B018 => CheckKind::UselessExpression,
CheckCode::B019 => CheckKind::CachedInstanceMethod,
CheckCode::B020 => CheckKind::LoopVariableOverridesIterator("...".to_string()),
CheckCode::B021 => CheckKind::FStringDocstring,
CheckCode::B022 => CheckKind::UselessContextlibSuppress,
CheckCode::B024 => CheckKind::AbstractBaseClassWithoutAbstractMethod("...".to_string()),
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg,
CheckCode::B027 => CheckKind::EmptyMethodWithoutAbstractDecorator("...".to_string()),
// flake8-comprehensions
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
CheckCode::C401 => CheckKind::UnnecessaryGeneratorSet,
@@ -881,9 +893,13 @@ impl CheckCode {
CheckCode::B017 => CheckCategory::Flake8Bugbear,
CheckCode::B018 => CheckCategory::Flake8Bugbear,
CheckCode::B019 => CheckCategory::Flake8Bugbear,
CheckCode::B020 => CheckCategory::Flake8Bugbear,
CheckCode::B021 => CheckCategory::Flake8Bugbear,
CheckCode::B022 => CheckCategory::Flake8Bugbear,
CheckCode::B024 => CheckCategory::Flake8Bugbear,
CheckCode::B025 => CheckCategory::Flake8Bugbear,
CheckCode::B026 => CheckCategory::Flake8Bugbear,
CheckCode::B027 => CheckCategory::Flake8Bugbear,
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
CheckCode::C402 => CheckCategory::Flake8Comprehensions,
@@ -1086,9 +1102,13 @@ impl CheckKind {
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::UselessExpression => &CheckCode::B018,
CheckKind::CachedInstanceMethod => &CheckCode::B019,
CheckKind::LoopVariableOverridesIterator(_) => &CheckCode::B020,
CheckKind::FStringDocstring => &CheckCode::B021,
CheckKind::UselessContextlibSuppress => &CheckCode::B022,
CheckKind::AbstractBaseClassWithoutAbstractMethod(_) => &CheckCode::B024,
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
CheckKind::EmptyMethodWithoutAbstractDecorator(_) => &CheckCode::B027,
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
CheckKind::UnnecessaryGeneratorSet => &CheckCode::C401,
@@ -1457,9 +1477,20 @@ impl CheckKind {
CheckKind::CachedInstanceMethod => "Use of `functools.lru_cache` or `functools.cache` \
on methods can lead to memory leaks"
.to_string(),
CheckKind::LoopVariableOverridesIterator(name) => {
format!("Loop control variable `{name}` overrides iterable it iterates")
}
CheckKind::FStringDocstring => "f-string used as docstring. This will be interpreted \
by python as a joined string rather than a docstring."
.to_string(),
CheckKind::UselessContextlibSuppress => {
"No arguments passed to `contextlib.suppress`. No exceptions will be suppressed \
and therefore this context manager is redundant"
.to_string()
}
CheckKind::AbstractBaseClassWithoutAbstractMethod(name) => {
format!("`{name}` is an abstract base class, but it has no abstract methods")
}
CheckKind::DuplicateTryBlockException(name) => {
format!("try-except block with duplicate exception `{name}`")
}
@@ -1469,6 +1500,12 @@ impl CheckKind {
unpacked sequence, and this change of ordering can surprise and mislead readers."
.to_string()
}
CheckKind::EmptyMethodWithoutAbstractDecorator(name) => {
format!(
"`{name}` is an empty method in an abstract base class, but has no abstract \
decorator"
)
}
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => {
"Unnecessary generator (rewrite as a `list` comprehension)".to_string()

View File

@@ -56,9 +56,13 @@ pub enum CheckCodePrefix {
B018,
B019,
B02,
B020,
B021,
B022,
B024,
B025,
B026,
B027,
C,
C4,
C40,
@@ -383,9 +387,13 @@ impl CheckCodePrefix {
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
],
CheckCodePrefix::B0 => vec![
CheckCode::B002,
@@ -406,9 +414,13 @@ impl CheckCodePrefix {
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
],
CheckCodePrefix::B00 => vec![
CheckCode::B002,
@@ -450,10 +462,22 @@ impl CheckCodePrefix {
CheckCodePrefix::B017 => vec![CheckCode::B017],
CheckCodePrefix::B018 => vec![CheckCode::B018],
CheckCodePrefix::B019 => vec![CheckCode::B019],
CheckCodePrefix::B02 => vec![CheckCode::B021, CheckCode::B025, CheckCode::B026],
CheckCodePrefix::B02 => vec![
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
],
CheckCodePrefix::B020 => vec![CheckCode::B020],
CheckCodePrefix::B021 => vec![CheckCode::B021],
CheckCodePrefix::B022 => vec![CheckCode::B022],
CheckCodePrefix::B024 => vec![CheckCode::B024],
CheckCodePrefix::B025 => vec![CheckCode::B025],
CheckCodePrefix::B026 => vec![CheckCode::B026],
CheckCodePrefix::B027 => vec![CheckCode::B027],
CheckCodePrefix::C => vec![
CheckCode::C400,
CheckCode::C401,
@@ -1194,9 +1218,13 @@ impl CheckCodePrefix {
CheckCodePrefix::B018 => PrefixSpecificity::Explicit,
CheckCodePrefix::B019 => PrefixSpecificity::Explicit,
CheckCodePrefix::B02 => PrefixSpecificity::Tens,
CheckCodePrefix::B020 => PrefixSpecificity::Explicit,
CheckCodePrefix::B021 => PrefixSpecificity::Explicit,
CheckCodePrefix::B022 => PrefixSpecificity::Explicit,
CheckCodePrefix::B024 => PrefixSpecificity::Explicit,
CheckCodePrefix::B025 => PrefixSpecificity::Explicit,
CheckCodePrefix::B026 => PrefixSpecificity::Explicit,
CheckCodePrefix::B027 => PrefixSpecificity::Explicit,
CheckCodePrefix::C => PrefixSpecificity::Category,
CheckCodePrefix::C4 => PrefixSpecificity::Hundreds,
CheckCodePrefix::C40 => PrefixSpecificity::Tens,

View File

@@ -1,8 +1,8 @@
use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
use clap::{command, Parser};
use fnv::FnvHashMap;
use log::warn;
use regex::Regex;
@@ -87,6 +87,10 @@ pub struct Cli {
/// The minimum Python version that should be supported.
#[arg(long)]
pub target_version: Option<PythonVersion>,
/// Set the line-length for length-associated checks and automatic
/// formatting.
#[arg(long)]
pub line_length: Option<usize>,
/// Round-trip auto-formatting.
// TODO(charlie): This should be a sub-command.
#[arg(long, hide = true)]
@@ -120,6 +124,8 @@ pub fn extract_log_level(cli: &Cli) -> LogLevel {
LogLevel::Quiet
} else if cli.verbose {
LogLevel::Verbose
} else if matches!(cli.format, SerializationFormat::Json) {
LogLevel::Quiet
} else {
LogLevel::Default
}
@@ -188,7 +194,7 @@ pub fn collect_per_file_ignores(
pairs: Vec<PatternPrefixPair>,
project_root: &Option<PathBuf>,
) -> Vec<PerFileIgnore> {
let mut per_file_ignores: BTreeMap<String, Vec<CheckCodePrefix>> = BTreeMap::new();
let mut per_file_ignores: FnvHashMap<String, Vec<CheckCodePrefix>> = FnvHashMap::default();
for pair in pairs {
per_file_ignores
.entry(pair.pattern)

View File

@@ -1,11 +1,10 @@
//! Abstractions for Google-style docstrings.
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use once_cell::sync::Lazy;
pub(crate) static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
pub(crate) static GOOGLE_SECTION_NAMES: Lazy<FnvHashSet<&'static str>> = Lazy::new(|| {
FnvHashSet::from_iter([
"Args",
"Arguments",
"Attention",
@@ -37,35 +36,36 @@ pub(crate) static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new
])
});
pub(crate) static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"args",
"arguments",
"attention",
"attributes",
"caution",
"danger",
"error",
"example",
"examples",
"hint",
"important",
"keyword args",
"keyword arguments",
"methods",
"note",
"notes",
"return",
"returns",
"raises",
"references",
"see also",
"tip",
"todo",
"warning",
"warnings",
"warns",
"yield",
"yields",
])
});
pub(crate) static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy<FnvHashSet<&'static str>> =
Lazy::new(|| {
FnvHashSet::from_iter([
"args",
"arguments",
"attention",
"attributes",
"caution",
"danger",
"error",
"example",
"examples",
"hint",
"important",
"keyword args",
"keyword arguments",
"methods",
"note",
"notes",
"return",
"returns",
"raises",
"references",
"see also",
"tip",
"todo",
"warning",
"warnings",
"warns",
"yield",
"yields",
])
});

View File

@@ -1,11 +1,10 @@
//! Abstractions for NumPy-style docstrings.
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use once_cell::sync::Lazy;
pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<FnvHashSet<&'static str>> = Lazy::new(|| {
FnvHashSet::from_iter([
"short summary",
"extended summary",
"parameters",
@@ -22,8 +21,8 @@ pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> =
])
});
pub(crate) static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
pub(crate) static NUMPY_SECTION_NAMES: Lazy<FnvHashSet<&'static str>> = Lazy::new(|| {
FnvHashSet::from_iter([
"Short Summary",
"Extended Summary",
"Parameters",

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use once_cell::sync::Lazy;
use crate::docstrings::google::{GOOGLE_SECTION_NAMES, LOWERCASE_GOOGLE_SECTION_NAMES};
@@ -11,14 +10,14 @@ pub(crate) enum SectionStyle {
}
impl SectionStyle {
pub(crate) fn section_names(&self) -> &Lazy<BTreeSet<&'static str>> {
pub(crate) fn section_names(&self) -> &Lazy<FnvHashSet<&'static str>> {
match self {
SectionStyle::NumPy => &NUMPY_SECTION_NAMES,
SectionStyle::Google => &GOOGLE_SECTION_NAMES,
}
}
pub(crate) fn lowercase_section_names(&self) -> &Lazy<BTreeSet<&'static str>> {
pub(crate) fn lowercase_section_names(&self) -> &Lazy<FnvHashSet<&'static str>> {
match self {
SectionStyle::NumPy => &LOWERCASE_NUMPY_SECTION_NAMES,
SectionStyle::Google => &LOWERCASE_GOOGLE_SECTION_NAMES,

View File

@@ -7,7 +7,13 @@ use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
fn is_sys(checker: &Checker, expr: &Expr, target: &str) -> bool {
match_module_member(expr, &format!("sys.{target}"), &checker.from_imports)
match_module_member(
expr,
"sys",
target,
&checker.from_imports,
&checker.import_aliases,
)
}
/// YTT101, YTT102, YTT301, YTT303
@@ -181,7 +187,13 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
/// YTT202
pub fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
if match_module_member(expr, "six.PY3", &checker.from_imports) {
if match_module_member(
expr,
"six",
"PY3",
&checker.from_imports,
&checker.import_aliases,
) {
checker.add_check(Check::new(
CheckKind::SixPY3Referenced,
Range::from_located(expr),

View File

@@ -49,10 +49,13 @@ fn is_none_returning(body: &[Stmt]) -> bool {
}
/// ANN401
fn check_dynamically_typed(checker: &mut Checker, annotation: &Expr, name: &str) {
if checker.match_typing_module(annotation, "Any") {
fn check_dynamically_typed<F>(checker: &mut Checker, annotation: &Expr, func: F)
where
F: FnOnce() -> String,
{
if checker.match_typing_expr(annotation, "Any") {
checker.add_check(Check::new(
CheckKind::DynamicallyTypedExpression(name.to_string()),
CheckKind::DynamicallyTypedExpression(func()),
Range::from_located(annotation),
));
};
@@ -100,7 +103,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
{
if let Some(expr) = &arg.node.annotation {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, expr, &arg.node.arg);
check_dynamically_typed(checker, expr, || arg.node.arg.to_string());
};
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
@@ -122,7 +125,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("*{name}"));
check_dynamically_typed(checker, expr, || format!("*{name}"));
}
}
} else {
@@ -145,7 +148,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("**{name}"));
check_dynamically_typed(checker, expr, || format!("**{name}"));
}
}
} else {
@@ -165,7 +168,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, expr, name);
check_dynamically_typed(checker, expr, || name.to_string());
};
} else {
// Allow omission of return annotation in `__init__` functions, if the function
@@ -215,7 +218,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if let Some(annotation) = &arg.node.annotation {
has_any_typed_arg = true;
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, annotation, &arg.node.arg);
check_dynamically_typed(checker, annotation, || arg.node.arg.to_string());
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
@@ -238,7 +241,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("*{name}"));
check_dynamically_typed(checker, expr, || format!("*{name}"));
}
}
} else {
@@ -262,7 +265,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("**{name}"));
check_dynamically_typed(checker, expr, || format!("**{name}"));
}
}
} else {
@@ -305,7 +308,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
// ANN201, ANN202
if let Some(expr) = &returns {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, expr, name);
check_dynamically_typed(checker, expr, || name.to_string());
}
} else {
// Allow omission of return annotation in `__init__` functions, if the function

View File

@@ -0,0 +1,129 @@
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::helpers::match_module_member;
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
fn is_abc_class(
bases: &[Expr],
keywords: &[Keyword],
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
keywords.iter().any(|keyword| {
keyword
.node
.arg
.as_ref()
.map(|a| a == "metaclass")
.unwrap_or(false)
&& match_module_member(
&keyword.node.value,
"abc",
"ABCMeta",
from_imports,
import_aliases,
)
}) || bases
.iter()
.any(|base| match_module_member(base, "abc", "ABC", from_imports, import_aliases))
}
fn is_empty_body(body: &[Stmt]) -> bool {
body.iter().all(|stmt| match &stmt.node {
StmtKind::Pass => true,
StmtKind::Expr { value } => match &value.node {
ExprKind::Constant { value, .. } => {
matches!(value, Constant::Str(..) | Constant::Ellipsis)
}
_ => false,
},
_ => false,
})
}
fn is_abstractmethod(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
match_module_member(expr, "abc", "abstractmethod", from_imports, import_aliases)
}
fn is_overload(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
match_module_member(expr, "typing", "overload", from_imports, import_aliases)
}
pub fn abstract_base_class(
checker: &mut Checker,
stmt: &Stmt,
name: &str,
bases: &[Expr],
keywords: &[Keyword],
body: &[Stmt],
) {
if bases.len() + keywords.len() == 1
&& is_abc_class(
bases,
keywords,
&checker.from_imports,
&checker.import_aliases,
)
{
let mut has_abstract_method = false;
for stmt in body {
// https://github.com/PyCQA/flake8-bugbear/issues/293
// Ignore abc's that declares a class attribute that must be set
if let StmtKind::AnnAssign { .. } | StmtKind::Assign { .. } = &stmt.node {
has_abstract_method = true;
continue;
}
if let StmtKind::FunctionDef {
decorator_list,
body,
..
}
| StmtKind::AsyncFunctionDef {
decorator_list,
body,
..
} = &stmt.node
{
let has_abstract_decorator = decorator_list
.iter()
.any(|d| is_abstractmethod(d, &checker.from_imports, &checker.import_aliases));
has_abstract_method |= has_abstract_decorator;
if checker.settings.enabled.contains(&CheckCode::B027) {
if !has_abstract_decorator
&& is_empty_body(body)
&& !decorator_list
.iter()
.any(|d| is_overload(d, &checker.from_imports, &checker.import_aliases))
{
checker.add_check(Check::new(
CheckKind::EmptyMethodWithoutAbstractDecorator(name.to_string()),
Range::from_located(stmt),
));
}
}
}
}
if checker.settings.enabled.contains(&CheckCode::B024) {
if !has_abstract_method {
checker.add_check(Check::new(
CheckKind::AbstractBaseClassWithoutAbstractMethod(name.to_string()),
Range::from_located(stmt),
));
}
}
}
}

View File

@@ -10,10 +10,10 @@ pub fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[With
if let Some(item) = items.first() {
let item_context = &item.context_expr;
if let ExprKind::Call { func, args, .. } = &item_context.node {
if match_name_or_attr(func, "assertRaises")
&& args.len() == 1
&& match_name_or_attr(args.first().unwrap(), "Exception")
if args.len() == 1
&& item.optional_vars.is_none()
&& match_name_or_attr(func, "assertRaises")
&& match_name_or_attr(args.first().unwrap(), "Exception")
{
checker.add_check(Check::new(
CheckKind::NoAssertRaisesException,

View File

@@ -1,13 +1,14 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::helpers::{compose_call_path, match_module_member};
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path};
use crate::ast::types::{Range, ScopeKind};
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
fn is_cache_func(checker: &Checker, expr: &Expr) -> bool {
match_module_member(expr, "functools.lru_cache", &checker.from_imports)
|| match_module_member(expr, "functools.cache", &checker.from_imports)
let call_path = dealias_call_path(collect_call_paths(expr), &checker.import_aliases);
match_call_path(&call_path, "functools", "lru_cache", &checker.from_imports)
|| match_call_path(&call_path, "functools", "cache", &checker.from_imports)
}
/// B019
@@ -16,8 +17,8 @@ pub fn cached_instance_method(checker: &mut Checker, decorator_list: &[Expr]) {
for decorator in decorator_list {
// TODO(charlie): This should take into account `classmethod-decorators` and
// `staticmethod-decorators`.
if let Some(decorator_path) = compose_call_path(decorator) {
if decorator_path == "classmethod" || decorator_path == "staticmethod" {
if let ExprKind::Name { id, .. } = &decorator.node {
if id == "classmethod" || id == "staticmethod" {
return;
}
}

View File

@@ -21,20 +21,21 @@ fn type_pattern(elts: Vec<&Expr>) -> Expr {
)
}
pub fn duplicate_handler_exceptions(
fn duplicate_handler_exceptions<'a>(
checker: &mut Checker,
expr: &Expr,
elts: &[Expr],
) -> BTreeSet<String> {
let mut seen: BTreeSet<String> = Default::default();
let mut duplicates: BTreeSet<String> = Default::default();
expr: &'a Expr,
elts: &'a [Expr],
) -> BTreeSet<Vec<&'a str>> {
let mut seen: BTreeSet<Vec<&str>> = Default::default();
let mut duplicates: BTreeSet<Vec<&str>> = Default::default();
let mut unique_elts: Vec<&Expr> = Default::default();
for type_ in elts {
if let Some(name) = helpers::compose_call_path(type_) {
if seen.contains(&name) {
duplicates.insert(name);
let call_path = helpers::collect_call_paths(type_);
if !call_path.is_empty() {
if seen.contains(&call_path) {
duplicates.insert(call_path);
} else {
seen.insert(name);
seen.insert(call_path);
unique_elts.push(type_);
}
}
@@ -45,7 +46,11 @@ pub fn duplicate_handler_exceptions(
if !duplicates.is_empty() {
let mut check = Check::new(
CheckKind::DuplicateHandlerException(
duplicates.into_iter().sorted().collect::<Vec<String>>(),
duplicates
.into_iter()
.map(|call_path| call_path.join("."))
.sorted()
.collect::<Vec<String>>(),
),
Range::from_located(expr),
);
@@ -70,19 +75,20 @@ pub fn duplicate_handler_exceptions(
}
pub fn duplicate_exceptions(checker: &mut Checker, stmt: &Stmt, handlers: &[Excepthandler]) {
let mut seen: BTreeSet<String> = Default::default();
let mut duplicates: BTreeSet<String> = Default::default();
let mut seen: BTreeSet<Vec<&str>> = Default::default();
let mut duplicates: BTreeSet<Vec<&str>> = Default::default();
for handler in handlers {
match &handler.node {
ExcepthandlerKind::ExceptHandler { type_, .. } => {
if let Some(type_) = type_ {
match &type_.node {
ExprKind::Attribute { .. } | ExprKind::Name { .. } => {
if let Some(name) = helpers::compose_call_path(type_) {
if seen.contains(&name) {
duplicates.insert(name);
let call_path = helpers::collect_call_paths(type_);
if !call_path.is_empty() {
if seen.contains(&call_path) {
duplicates.insert(call_path);
} else {
seen.insert(name);
seen.insert(call_path);
}
}
}
@@ -105,7 +111,7 @@ pub fn duplicate_exceptions(checker: &mut Checker, stmt: &Stmt, handlers: &[Exce
if checker.settings.enabled.contains(&CheckCode::B025) {
for duplicate in duplicates.into_iter().sorted() {
checker.add_check(Check::new(
CheckKind::DuplicateTryBlockException(duplicate),
CheckKind::DuplicateTryBlockException(duplicate.join(".")),
Range::from_located(stmt),
));
}

View File

@@ -1,7 +1,9 @@
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
use crate::ast::helpers::{compose_call_path, match_call_path};
use crate::ast::helpers::{
collect_call_paths, compose_call_path, dealias_call_path, match_call_path,
};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
@@ -9,35 +11,34 @@ use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::flake8_bugbear::plugins::mutable_argument_default::is_mutable_func;
const IMMUTABLE_FUNCS: [&str; 7] = [
"tuple",
"frozenset",
"operator.attrgetter",
"operator.itemgetter",
"operator.methodcaller",
"types.MappingProxyType",
"re.compile",
const IMMUTABLE_FUNCS: [(&str, &str); 7] = [
("", "tuple"),
("", "frozenset"),
("operator", "attrgetter"),
("operator", "itemgetter"),
("operator", "methodcaller"),
("types", "MappingProxyType"),
("re", "compile"),
];
fn is_immutable_func(
expr: &Expr,
extend_immutable_calls: &[&str],
extend_immutable_calls: &[(&str, &str)],
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
compose_call_path(expr)
.map(|call_path| {
IMMUTABLE_FUNCS
.iter()
.chain(extend_immutable_calls)
.any(|target| match_call_path(&call_path, target, from_imports))
})
.unwrap_or(false)
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
IMMUTABLE_FUNCS
.iter()
.chain(extend_immutable_calls)
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
}
struct ArgumentDefaultVisitor<'a> {
checks: Vec<(CheckKind, Range)>,
extend_immutable_calls: &'a [&'a str],
extend_immutable_calls: &'a [(&'a str, &'a str)],
from_imports: &'a FnvHashMap<&'a str, FnvHashSet<&'a str>>,
import_aliases: &'a FnvHashMap<&'a str, &'a str>,
}
impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor<'b>
@@ -47,8 +48,13 @@ where
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::Call { func, args, .. } => {
if !is_mutable_func(func, self.from_imports)
&& !is_immutable_func(func, self.extend_immutable_calls, self.from_imports)
if !is_mutable_func(func, self.from_imports, self.import_aliases)
&& !is_immutable_func(
func,
self.extend_immutable_calls,
self.from_imports,
self.import_aliases,
)
&& !is_nan_or_infinity(func, args)
{
self.checks.push((
@@ -92,17 +98,26 @@ fn is_nan_or_infinity(expr: &Expr, args: &[Expr]) -> bool {
/// B008
pub fn function_call_argument_default(checker: &mut Checker, arguments: &Arguments) {
let extend_immutable_cells: Vec<&str> = checker
// Map immutable calls to (module, member) format.
let extend_immutable_cells: Vec<(&str, &str)> = checker
.settings
.flake8_bugbear
.extend_immutable_calls
.iter()
.map(|s| s.as_str())
.map(|s| {
let s = s.as_str();
if let Some(index) = s.rfind('.') {
(&s[..index], &s[index + 1..])
} else {
("", s)
}
})
.collect();
let mut visitor = ArgumentDefaultVisitor {
checks: vec![],
extend_immutable_calls: &extend_immutable_cells,
from_imports: &checker.from_imports,
import_aliases: &checker.import_aliases,
};
for expr in arguments
.defaults

View File

@@ -0,0 +1,64 @@
use fnv::FnvHashMap;
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
#[derive(Default)]
struct NameFinder<'a> {
names: FnvHashMap<&'a str, &'a Expr>,
}
impl<'a, 'b> Visitor<'b> for NameFinder<'a>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::Name { id, .. } => {
self.names.insert(id, expr);
}
ExprKind::ListComp { generators, .. }
| ExprKind::DictComp { generators, .. }
| ExprKind::SetComp { generators, .. }
| ExprKind::GeneratorExp { generators, .. } => {
for comp in generators {
self.visit_expr(&comp.iter);
}
}
ExprKind::Lambda { args, body } => {
visitor::walk_expr(self, body);
for arg in args.args.iter() {
self.names.remove(arg.node.arg.as_str());
}
}
_ => visitor::walk_expr(self, expr),
}
}
}
/// B020
pub fn loop_variable_overrides_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) {
let target_names = {
let mut target_finder = NameFinder::default();
target_finder.visit_expr(target);
target_finder.names
};
let iter_names = {
let mut iter_finder = NameFinder::default();
iter_finder.visit_expr(iter);
iter_finder.names
};
for (name, expr) in target_names {
if iter_names.contains_key(name) {
checker.add_check(Check::new(
CheckKind::LoopVariableOverridesIterator(name.to_string()),
Range::from_located(expr),
));
}
}
}

View File

@@ -1,13 +1,15 @@
pub use abstract_base_class::abstract_base_class;
pub use assert_false::assert_false;
pub use assert_raises_exception::assert_raises_exception;
pub use assignment_to_os_environ::assignment_to_os_environ;
pub use cached_instance_method::cached_instance_method;
pub use cannot_raise_literal::cannot_raise_literal;
pub use duplicate_exceptions::{duplicate_exceptions, duplicate_handler_exceptions};
pub use duplicate_exceptions::duplicate_exceptions;
pub use f_string_docstring::f_string_docstring;
pub use function_call_argument_default::function_call_argument_default;
pub use getattr_with_constant::getattr_with_constant;
pub use jump_statement_in_finally::jump_statement_in_finally;
pub use loop_variable_overrides_iterator::loop_variable_overrides_iterator;
pub use mutable_argument_default::mutable_argument_default;
pub use redundant_tuple_in_exception_handler::redundant_tuple_in_exception_handler;
pub use setattr_with_constant::setattr_with_constant;
@@ -17,8 +19,10 @@ pub use unary_prefix_increment::unary_prefix_increment;
pub use unreliable_callable_check::unreliable_callable_check;
pub use unused_loop_control_variable::unused_loop_control_variable;
pub use useless_comparison::useless_comparison;
pub use useless_contextlib_suppress::useless_contextlib_suppress;
pub use useless_expression::useless_expression;
mod abstract_base_class;
mod assert_false;
mod assert_raises_exception;
mod assignment_to_os_environ;
@@ -29,6 +33,7 @@ mod f_string_docstring;
mod function_call_argument_default;
mod getattr_with_constant;
mod jump_statement_in_finally;
mod loop_variable_overrides_iterator;
mod mutable_argument_default;
mod redundant_tuple_in_exception_handler;
mod setattr_with_constant;
@@ -38,4 +43,5 @@ mod unary_prefix_increment;
mod unreliable_callable_check;
mod unused_loop_control_variable;
mod useless_comparison;
mod useless_contextlib_suppress;
mod useless_expression;

View File

@@ -1,29 +1,30 @@
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Arguments, Expr, ExprKind};
use crate::ast::helpers::{compose_call_path, match_call_path};
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
const MUTABLE_FUNCS: [&str; 7] = [
"dict",
"list",
"set",
"collections.Counter",
"collections.OrderedDict",
"collections.defaultdict",
"collections.deque",
const MUTABLE_FUNCS: [(&str, &str); 7] = [
("", "dict"),
("", "list"),
("", "set"),
("collections", "Counter"),
("collections", "OrderedDict"),
("collections", "defaultdict"),
("collections", "deque"),
];
pub fn is_mutable_func(expr: &Expr, from_imports: &FnvHashMap<&str, FnvHashSet<&str>>) -> bool {
compose_call_path(expr)
.map(|call_path| {
MUTABLE_FUNCS
.iter()
.any(|target| match_call_path(&call_path, target, from_imports))
})
.unwrap_or(false)
pub fn is_mutable_func(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
MUTABLE_FUNCS
.iter()
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
}
/// B006
@@ -46,7 +47,7 @@ pub fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) {
));
}
ExprKind::Call { func, .. } => {
if is_mutable_func(func, &checker.from_imports) {
if is_mutable_func(func, &checker.from_imports, &checker.import_aliases) {
checker.add_check(Check::new(
CheckKind::MutableArgumentDefault,
Range::from_located(expr),

View File

@@ -0,0 +1,22 @@
use rustpython_ast::Expr;
use crate::ast::helpers::{collect_call_paths, match_call_path};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// B005
pub fn useless_contextlib_suppress(checker: &mut Checker, expr: &Expr, args: &[Expr]) {
if match_call_path(
&collect_call_paths(expr),
"contextlib",
"suppress",
&checker.from_imports,
) && args.is_empty()
{
checker.add_check(Check::new(
CheckKind::UselessContextlibSuppress,
Range::from_located(expr),
));
}
}

View File

@@ -1,11 +1,11 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fs::File;
use std::io::{BufReader, Read};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use fnv::FnvHashSet;
use log::debug;
use path_absolutize::{path_dedot, Absolutize};
use walkdir::{DirEntry, WalkDir};
@@ -121,7 +121,7 @@ pub fn iter_python_files<'a>(
pub(crate) fn ignores_from_path<'a>(
path: &Path,
pattern_code_pairs: &'a [PerFileIgnore],
) -> Result<BTreeSet<&'a CheckCode>> {
) -> Result<FnvHashSet<&'a CheckCode>> {
let (file_path, file_basename) = extract_path_names(path)?;
Ok(pattern_code_pairs
.iter()

View File

@@ -1,9 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeSet;
use std::fs;
use std::path::PathBuf;
use once_cell::sync::Lazy;
use crate::python::sys::KNOWN_STANDARD_LIBRARY;
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone)]
@@ -31,8 +29,8 @@ pub fn categorize(
ImportType::ThirdParty
} else if extra_standard_library.contains(module_base) {
ImportType::StandardLibrary
} else if let Some(import_type) = STATIC_CLASSIFICATIONS.get(module_base) {
import_type.clone()
} else if module_base == "__future__" {
ImportType::Future
} else if KNOWN_STANDARD_LIBRARY.contains(module_base) {
ImportType::StandardLibrary
} else if find_local(src, module_base) {
@@ -42,14 +40,6 @@ pub fn categorize(
}
}
static STATIC_CLASSIFICATIONS: Lazy<BTreeMap<&'static str, ImportType>> = Lazy::new(|| {
BTreeMap::from([
("__future__", ImportType::Future),
// Relative imports (e.g., `from . import module`).
("", ImportType::FirstParty),
])
});
fn find_local(paths: &[PathBuf], base: &str) -> bool {
for path in paths {
if let Ok(metadata) = fs::metadata(path.join(base)) {

View File

@@ -1,6 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use fnv::FnvHashSet;
use itertools::Itertools;
use ropey::RopeBuilder;
use rustpython_ast::{Stmt, StmtKind};
@@ -36,15 +37,25 @@ fn normalize_imports<'a>(imports: &'a [&'a Stmt]) -> ImportBlock<'a> {
names,
level,
} => {
let targets = block
.import_from
.entry(ImportFromData { module, level })
.or_default();
for name in names {
targets.insert(AliasData {
name: &name.node.name,
asname: &name.node.asname,
});
if name.node.asname.is_none() {
block
.import_from
.entry(ImportFromData { module, level })
.or_default()
.insert(AliasData {
name: &name.node.name,
asname: &name.node.asname,
});
} else {
block.import_from_as.insert((
ImportFromData { module, level },
AliasData {
name: &name.node.name,
asname: &name.node.asname,
},
));
}
}
}
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
@@ -77,7 +88,7 @@ fn categorize_imports<'a>(
.import
.insert(alias);
}
// Categorize `StmtKind::ImportFrom`.
// Categorize `StmtKind::ImportFrom` (without re-export).
for (import_from, aliases) in block.import_from {
let classification = categorize(
&import_from.module_base(),
@@ -93,36 +104,74 @@ fn categorize_imports<'a>(
.import_from
.insert(import_from, aliases);
}
// Categorize `StmtKind::ImportFrom` (with re-export).
for (import_from, alias) in block.import_from_as {
let classification = categorize(
&import_from.module_base(),
import_from.level,
src,
known_first_party,
known_third_party,
extra_standard_library,
);
block_by_type
.entry(classification)
.or_default()
.import_from_as
.insert((import_from, alias));
}
block_by_type
}
fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
let mut ordered: OrderedImportBlock = Default::default();
// Sort `StmtKind::Import`.
for import in block
.import
.into_iter()
.sorted_by_cached_key(|alias| module_key(alias.name))
{
ordered.import.push(import);
}
ordered.import.extend(
block
.import
.into_iter()
.sorted_by_cached_key(|alias| module_key(alias.name, alias.asname)),
);
// Sort `StmtKind::ImportFrom`.
for (import_from, aliases) in
ordered.import_from.extend(
// Include all non-re-exports.
block
.import_from
.into_iter()
.sorted_by_cached_key(|(import_from, _)| {
import_from.module.as_ref().map(|module| module_key(module))
.chain(
// Include all re-exports.
block
.import_from_as
.into_iter()
.map(|(import_from, alias)| (import_from, FnvHashSet::from_iter([alias]))),
)
.map(|(import_from, aliases)| {
// Within each `StmtKind::ImportFrom`, sort the members.
(
import_from,
aliases
.into_iter()
.sorted_by_cached_key(|alias| member_key(alias.name, alias.asname))
.collect::<Vec<AliasData>>(),
)
})
{
ordered.import_from.push((
import_from,
aliases
.into_iter()
.sorted_by_cached_key(|alias| member_key(alias.name))
.collect(),
));
}
.sorted_by_cached_key(|(import_from, aliases)| {
// Sort each `StmtKind::ImportFrom` by module key, breaking ties based on
// members.
(
import_from
.module
.as_ref()
.map(|module| module_key(module, &None)),
aliases
.first()
.map(|alias| member_key(alias.name, alias.asname)),
)
}),
);
ordered
}
@@ -252,6 +301,7 @@ mod tests {
#[test_case(Path::new("separate_local_folder_imports.py"))]
#[test_case(Path::new("separate_third_party_imports.py"))]
#[test_case(Path::new("skip.py"))]
#[test_case(Path::new("sort_similar_imports.py"))]
#[test_case(Path::new("trailing_suffix.py"))]
fn isort(path: &Path) -> Result<()> {
let snapshot = format!("{}", path.to_string_lossy());

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 27
column: 0
fix:
patch:
content: "import A\nimport a\nimport B\nimport b\nimport x\nimport x as A\nimport x as Y\nimport x as a\nimport x as y\nfrom a import BAD as DEF\nfrom a import B, b\nfrom a import B as A\nfrom a import B as Abc\nfrom a import B as DEF\nfrom a import Boo as DEF\nfrom a import b as a\nfrom a import b as c\nfrom a import b as d\nfrom a import b as x\nfrom a import b as y\nfrom b import C, c\nfrom b import c as d\n"
location:
row: 1
column: 0
end_location:
row: 27
column: 0
applied: false

View File

@@ -8,16 +8,22 @@ pub enum Prefix {
Variables,
}
pub fn module_key(module_name: &str) -> String {
module_name.to_lowercase()
pub fn module_key<'a>(
name: &'a str,
asname: &'a Option<String>,
) -> (String, &'a str, &'a Option<String>) {
(name.to_lowercase(), name, asname)
}
pub fn member_key(member_name: &str) -> (Prefix, String) {
pub fn member_key<'a>(
name: &'a str,
asname: &'a Option<String>,
) -> (Prefix, String, &'a Option<String>) {
(
if member_name.len() > 1 && string::is_upper(member_name) {
if name.len() > 1 && string::is_upper(name) {
// Ex) `CONSTANT`
Prefix::Constants
} else if member_name
} else if name
.chars()
.next()
.map(|char| char.is_uppercase())
@@ -29,6 +35,7 @@ pub fn member_key(member_name: &str) -> (Prefix, String) {
// Ex) `variable`
Prefix::Variables
},
member_name.to_lowercase(),
name.to_lowercase(),
asname,
)
}

View File

@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet};
use fnv::{FnvHashMap, FnvHashSet};
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct ImportFromData<'a> {
@@ -48,16 +48,19 @@ impl Importable for ImportFromData<'_> {
#[derive(Debug, Default)]
pub struct ImportBlock<'a> {
// Map from (module, level) to `AliasData`.
pub import_from: BTreeMap<ImportFromData<'a>, BTreeSet<AliasData<'a>>>,
// Set of (name, asname).
pub import: BTreeSet<AliasData<'a>>,
// Set of (name, asname), used to track regular imports.
// Ex) `import module`
pub import: FnvHashSet<AliasData<'a>>,
// Map from (module, level) to `AliasData`, used to track 'from' imports.
// Ex) `from module import member`
pub import_from: FnvHashMap<ImportFromData<'a>, FnvHashSet<AliasData<'a>>>,
// Set of (module, level, name, asname), used to track re-exported 'from' imports.
// Ex) `from module import member as member`
pub import_from_as: FnvHashSet<(ImportFromData<'a>, AliasData<'a>)>,
}
#[derive(Debug, Default)]
pub struct OrderedImportBlock<'a> {
// Map from (module, level) to `AliasData`.
pub import_from: Vec<(ImportFromData<'a>, Vec<AliasData<'a>>)>,
// Set of (name, asname).
pub import: Vec<AliasData<'a>>,
pub import_from: Vec<(ImportFromData<'a>, Vec<AliasData<'a>>)>,
}

View File

@@ -52,6 +52,8 @@ mod pyupgrade;
mod rules;
pub mod settings;
pub mod source_code_locator;
#[cfg(feature = "update-informer")]
pub mod updates;
pub mod visibility;
/// Run Ruff over Python source code directly.

View File

@@ -344,9 +344,13 @@ mod tests {
#[test_case(CheckCode::B017, Path::new("B017.py"); "B017")]
#[test_case(CheckCode::B018, Path::new("B018.py"); "B018")]
#[test_case(CheckCode::B019, Path::new("B019.py"); "B019")]
#[test_case(CheckCode::B020, Path::new("B020.py"); "B020")]
#[test_case(CheckCode::B021, Path::new("B021.py"); "B021")]
#[test_case(CheckCode::B022, Path::new("B022.py"); "B022")]
#[test_case(CheckCode::B024, Path::new("B024.py"); "B024")]
#[test_case(CheckCode::B025, Path::new("B025.py"); "B025")]
#[test_case(CheckCode::B026, Path::new("B026.py"); "B026")]
#[test_case(CheckCode::B027, Path::new("B027.py"); "B027")]
#[test_case(CheckCode::C400, Path::new("C400.py"); "C400")]
#[test_case(CheckCode::C401, Path::new("C401.py"); "C401")]
#[test_case(CheckCode::C402, Path::new("C402.py"); "C402")]
@@ -452,6 +456,8 @@ mod tests {
#[test_case(CheckCode::F821, Path::new("F821_1.py"); "F821_1")]
#[test_case(CheckCode::F821, Path::new("F821_2.py"); "F821_2")]
#[test_case(CheckCode::F821, Path::new("F821_3.py"); "F821_3")]
#[test_case(CheckCode::F821, Path::new("F821_4.py"); "F821_4")]
#[test_case(CheckCode::F821, Path::new("F821_5.py"); "F821_5")]
#[test_case(CheckCode::F822, Path::new("F822.py"); "F822")]
#[test_case(CheckCode::F823, Path::new("F823.py"); "F823")]
#[test_case(CheckCode::F831, Path::new("F831.py"); "F831")]

View File

@@ -17,6 +17,8 @@ use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::types::FilePattern;
use ::ruff::settings::user::UserConfiguration;
use ::ruff::settings::{pyproject, Settings};
#[cfg(feature = "update-informer")]
use ::ruff::updates;
use anyhow::Result;
use clap::Parser;
use colored::Colorize;
@@ -26,11 +28,6 @@ use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use walkdir::DirEntry;
#[cfg(feature = "update-informer")]
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
#[cfg(feature = "update-informer")]
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Shim that calls par_iter except for wasm because there's no wasm support in
/// rayon yet (there is a shim to be used for the web, but it requires js
/// cooperation) Unfortunately, ParallelIterator does not implement Iterator so
@@ -45,30 +42,6 @@ fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl Iterator<Item = &T> {
iterable.iter()
}
#[cfg(feature = "update-informer")]
fn check_for_updates() {
use update_informer::{registry, Check};
let informer = update_informer::new(registry::PyPI, CARGO_PKG_NAME, CARGO_PKG_VERSION);
if let Some(new_version) = informer.check_version().ok().flatten() {
let msg = format!(
"A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",
pkg_name = CARGO_PKG_NAME.italic().cyan(),
pkg_version = CARGO_PKG_VERSION,
new_version = new_version.to_string().green()
);
let cmd = format!(
"Run to update: {cmd} {pkg_name}",
cmd = "pip3 install --upgrade".green(),
pkg_name = CARGO_PKG_NAME.green()
);
println!("\n{msg}\n{cmd}");
}
}
fn show_settings(
configuration: Configuration,
project_root: Option<PathBuf>,
@@ -292,6 +265,9 @@ fn inner_main() -> Result<ExitCode> {
if !cli.extend_ignore.is_empty() {
configuration.extend_ignore = cli.extend_ignore;
}
if let Some(line_length) = cli.line_length {
configuration.line_length = line_length;
}
if let Some(target_version) = cli.target_version {
configuration.target_version = target_version;
}
@@ -402,8 +378,8 @@ fn inner_main() -> Result<ExitCode> {
// Check for updates if we're in a non-silent log level.
#[cfg(feature = "update-informer")]
if !is_stdin && log_level >= LogLevel::Default {
check_for_updates();
if !is_stdin && log_level >= LogLevel::Default && atty::is(atty::Stream::Stdout) {
let _ = updates::check_for_updates();
}
if messages.iter().any(|message| !message.fixed) && !cli.exit_zero {

View File

@@ -1,11 +1,11 @@
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use crate::ast::types::{FunctionScope, Range, Scope, ScopeKind};
use crate::ast::types::{Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pep8_naming::helpers;
use crate::pep8_naming::helpers::FunctionType;
use crate::pep8_naming::settings::Settings;
use crate::python::string;
use crate::python::string::{self};
/// N801
pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option<Check> {
@@ -100,20 +100,6 @@ pub fn invalid_first_argument_name_for_method(
None
}
/// N806
pub fn non_lowercase_variable_in_function(scope: &Scope, expr: &Expr, name: &str) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Function(FunctionScope { .. })) {
return None;
}
if name.to_lowercase() != name {
return Some(Check::new(
CheckKind::NonLowercaseVariableInFunction(name.to_string()),
Range::from_located(expr),
));
}
None
}
/// N807
pub fn dunder_function_name(scope: &Scope, stmt: &Stmt, name: &str) -> Option<Check> {
if matches!(scope.kind, ScopeKind::Class(_)) {
@@ -192,38 +178,6 @@ pub fn camelcase_imported_as_constant(
None
}
/// N815
pub fn mixed_case_variable_in_class_scope(scope: &Scope, expr: &Expr, name: &str) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Class(_)) {
return None;
}
if helpers::is_mixed_case(name) {
return Some(Check::new(
CheckKind::MixedCaseVariableInClassScope(name.to_string()),
Range::from_located(expr),
));
}
None
}
/// N816
pub fn mixed_case_variable_in_global_scope(
scope: &Scope,
expr: &Expr,
name: &str,
) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Module) {
return None;
}
if helpers::is_mixed_case(name) {
return Some(Check::new(
CheckKind::MixedCaseVariableInGlobalScope(name.to_string()),
Range::from_located(expr),
));
}
None
}
/// N817
pub fn camelcase_imported_as_acronym(
import_from: &Stmt,

View File

@@ -1,7 +1,8 @@
use fnv::{FnvHashMap, FnvHashSet};
use itertools::Itertools;
use rustpython_ast::{Expr, ExprKind};
use rustpython_ast::{Expr, ExprKind, Stmt, StmtKind};
use crate::ast::helpers::match_name_or_attr;
use crate::ast::helpers::{collect_call_paths, match_call_path, match_name_or_attr};
use crate::ast::types::{Scope, ScopeKind};
use crate::pep8_naming::settings::Settings;
use crate::python::string::{is_lower, is_upper};
@@ -78,6 +79,22 @@ pub fn is_acronym(name: &str, asname: &str) -> bool {
name.chars().filter(|c| c.is_uppercase()).join("") == asname
}
pub fn is_namedtuple_assignment(
stmt: &Stmt,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> bool {
if let StmtKind::Assign { value, .. } = &stmt.node {
match_call_path(
&collect_call_paths(value),
"collections",
"namedtuple",
from_imports,
)
} else {
false
}
}
#[cfg(test)]
mod tests {
use crate::pep8_naming::helpers::{is_acronym, is_camelcase, is_mixed_case};

View File

@@ -1,3 +1,4 @@
pub mod checks;
mod helpers;
pub mod plugins;
pub mod settings;

View File

@@ -0,0 +1,58 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::CheckKind;
use crate::pep8_naming::helpers;
use crate::Check;
/// N806
pub fn non_lowercase_variable_in_function(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
) {
if name.to_lowercase() != name
&& !helpers::is_namedtuple_assignment(stmt, &checker.from_imports)
{
checker.add_check(Check::new(
CheckKind::NonLowercaseVariableInFunction(name.to_string()),
Range::from_located(expr),
));
}
}
/// N815
pub fn mixed_case_variable_in_class_scope(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
) {
if helpers::is_mixed_case(name)
&& !helpers::is_namedtuple_assignment(stmt, &checker.from_imports)
{
checker.add_check(Check::new(
CheckKind::MixedCaseVariableInClassScope(name.to_string()),
Range::from_located(expr),
));
}
}
/// N816
pub fn mixed_case_variable_in_global_scope(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
) {
if helpers::is_mixed_case(name)
&& !helpers::is_namedtuple_assignment(stmt, &checker.from_imports)
{
checker.add_check(Check::new(
CheckKind::MixedCaseVariableInGlobalScope(name.to_string()),
Range::from_located(expr),
));
}
}

View File

@@ -1,5 +1,6 @@
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
@@ -696,7 +697,7 @@ pub fn ends_with_period(checker: &mut Checker, definition: &Definition) {
..
} = &docstring.node
{
if let Some(string) = string.lines().next() {
if let Some(string) = string.trim().lines().next() {
if !string.ends_with('.') {
checker.add_check(Check::new(
CheckKind::EndsInPeriod,
@@ -806,7 +807,7 @@ pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) {
..
} = &docstring.node
{
if let Some(string) = string.lines().next() {
if let Some(string) = string.trim().lines().next() {
if !(string.ends_with('.') || string.ends_with('!') || string.ends_with('?')) {
checker.add_check(Check::new(
CheckKind::EndsInPunctuation,
@@ -1287,7 +1288,11 @@ fn common_section(
blanks_and_section_underline(checker, definition, context);
}
fn missing_args(checker: &mut Checker, definition: &Definition, docstrings_args: &BTreeSet<&str>) {
fn missing_args(
checker: &mut Checker,
definition: &Definition,
docstrings_args: &FnvHashSet<&str>,
) {
if let DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent)
| DefinitionKind::Method(parent) = definition.kind
@@ -1377,7 +1382,7 @@ fn args_section(checker: &mut Checker, definition: &Definition, context: &Sectio
checker,
definition,
// Collect the list of arguments documented in the docstring.
&BTreeSet::from_iter(args_sections.iter().filter_map(|section| {
&FnvHashSet::from_iter(args_sections.iter().filter_map(|section| {
match GOOGLE_ARGS_REGEX.captures(section.as_str()) {
Some(caps) => caps.get(1).map(|arg_name| arg_name.as_str()),
None => None,
@@ -1388,7 +1393,7 @@ fn args_section(checker: &mut Checker, definition: &Definition, context: &Sectio
fn parameters_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) {
// Collect the list of arguments documented in the docstring.
let mut docstring_args: BTreeSet<&str> = Default::default();
let mut docstring_args: FnvHashSet<&str> = FnvHashSet::default();
let section_level_indent = helpers::leading_space(context.line);
for i in 1..context.following_lines.len() {
let current_line = context.following_lines[i - 1];

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use regex::Regex;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind,
@@ -113,7 +112,7 @@ pub fn duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
}
// Search for duplicates.
let mut idents: BTreeSet<&str> = BTreeSet::new();
let mut idents: FnvHashSet<&str> = FnvHashSet::default();
for arg in all_arguments {
let ident = &arg.node.arg;
if idents.contains(ident.as_str()) {

View File

@@ -1,10 +1,9 @@
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use once_cell::sync::Lazy;
// See: https://pycqa.github.io/isort/docs/configuration/options.html#known-standard-library
pub static KNOWN_STANDARD_LIBRARY: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
pub static KNOWN_STANDARD_LIBRARY: Lazy<FnvHashSet<&'static str>> = Lazy::new(|| {
FnvHashSet::from_iter([
"_ast",
"_dummy_thread",
"_thread",

View File

@@ -1,6 +1,8 @@
use fnv::{FnvHashMap, FnvHashSet};
use once_cell::sync::Lazy;
use rustpython_ast::{Expr, ExprKind};
use rustpython_ast::Expr;
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path};
// See: https://pypi.org/project/typing-extensions/
static TYPING_EXTENSIONS: Lazy<FnvHashSet<&'static str>> = Lazy::new(|| {
@@ -63,144 +65,144 @@ pub fn in_extensions(name: &str) -> bool {
}
// See: https://docs.python.org/3/library/typing.html
static IMPORTED_SUBSCRIPTS: Lazy<FnvHashMap<&'static str, FnvHashSet<&'static str>>> =
Lazy::new(|| {
let mut import_map = FnvHashMap::default();
for (name, module) in [
// `collections`
("ChainMap", "collections"),
("Counter", "collections"),
("OrderedDict", "collections"),
("defaultdict", "collections"),
("deque", "collections"),
// `collections.abc`
("AsyncGenerator", "collections.abc"),
("AsyncIterable", "collections.abc"),
("AsyncIterator", "collections.abc"),
("Awaitable", "collections.abc"),
("ByteString", "collections.abc"),
("Callable", "collections.abc"),
("Collection", "collections.abc"),
("Container", "collections.abc"),
("Coroutine", "collections.abc"),
("Generator", "collections.abc"),
("ItemsView", "collections.abc"),
("Iterable", "collections.abc"),
("Iterator", "collections.abc"),
("KeysView", "collections.abc"),
("Mapping", "collections.abc"),
("MappingView", "collections.abc"),
("MutableMapping", "collections.abc"),
("MutableSequence", "collections.abc"),
("MutableSet", "collections.abc"),
("Reversible", "collections.abc"),
("Sequence", "collections.abc"),
("Set", "collections.abc"),
("ValuesView", "collections.abc"),
// `contextlib`
("AbstractAsyncContextManager", "contextlib"),
("AbstractContextManager", "contextlib"),
// `re`
("Match", "re"),
("Pattern", "re"),
// `typing`
("AbstractSet", "typing"),
("Annotated", "typing"),
("AsyncContextManager", "typing"),
("AsyncGenerator", "typing"),
("AsyncIterator", "typing"),
("Awaitable", "typing"),
("BinaryIO", "typing"),
("ByteString", "typing"),
("Callable", "typing"),
("ChainMap", "typing"),
("ClassVar", "typing"),
("Collection", "typing"),
("Concatenate", "typing"),
("Container", "typing"),
("ContextManager", "typing"),
("Coroutine", "typing"),
("Counter", "typing"),
("DefaultDict", "typing"),
("Deque", "typing"),
("Dict", "typing"),
("Final", "typing"),
("FrozenSet", "typing"),
("Generator", "typing"),
("Generic", "typing"),
("IO", "typing"),
("ItemsView", "typing"),
("Iterable", "typing"),
("Iterator", "typing"),
("KeysView", "typing"),
("List", "typing"),
("Mapping", "typing"),
("Match", "typing"),
("MutableMapping", "typing"),
("MutableSequence", "typing"),
("MutableSet", "typing"),
("Optional", "typing"),
("OrderedDict", "typing"),
("Pattern", "typing"),
("Reversible", "typing"),
("Sequence", "typing"),
("Set", "typing"),
("TextIO", "typing"),
("Tuple", "typing"),
("Type", "typing"),
("TypeGuard", "typing"),
("Union", "typing"),
("Unpack", "typing"),
("ValuesView", "typing"),
// `typing.io`
("BinaryIO", "typing.io"),
("IO", "typing.io"),
("TextIO", "typing.io"),
// `typing.re`
("Match", "typing.re"),
("Pattern", "typing.re"),
// `typing_extensions`
("Annotated", "typing_extensions"),
("AsyncContextManager", "typing_extensions"),
("AsyncGenerator", "typing_extensions"),
("AsyncIterable", "typing_extensions"),
("AsyncIterator", "typing_extensions"),
("Awaitable", "typing_extensions"),
("ChainMap", "typing_extensions"),
("ClassVar", "typing_extensions"),
("Concatenate", "typing_extensions"),
("ContextManager", "typing_extensions"),
("Coroutine", "typing_extensions"),
("Counter", "typing_extensions"),
("DefaultDict", "typing_extensions"),
("Deque", "typing_extensions"),
("Type", "typing_extensions"),
// `weakref`
("WeakKeyDictionary", "weakref"),
("WeakSet", "weakref"),
("WeakValueDictionary", "weakref"),
] {
import_map
.entry(name)
.or_insert_with(FnvHashSet::default)
.insert(module);
}
import_map
});
const SUBSCRIPTS: &[(&str, &str)] = &[
// builtins
("", "dict"),
("", "frozenset"),
("", "list"),
("", "set"),
("", "tuple"),
("", "type"),
// `collections`
("collections", "ChainMap"),
("collections", "Counter"),
("collections", "OrderedDict"),
("collections", "defaultdict"),
("collections", "deque"),
// `collections.abc`
("collections.abc", "AsyncGenerator"),
("collections.abc", "AsyncIterable"),
("collections.abc", "AsyncIterator"),
("collections.abc", "Awaitable"),
("collections.abc", "ByteString"),
("collections.abc", "Callable"),
("collections.abc", "Collection"),
("collections.abc", "Container"),
("collections.abc", "Coroutine"),
("collections.abc", "Generator"),
("collections.abc", "ItemsView"),
("collections.abc", "Iterable"),
("collections.abc", "Iterator"),
("collections.abc", "KeysView"),
("collections.abc", "Mapping"),
("collections.abc", "MappingView"),
("collections.abc", "MutableMapping"),
("collections.abc", "MutableSequence"),
("collections.abc", "MutableSet"),
("collections.abc", "Reversible"),
("collections.abc", "Sequence"),
("collections.abc", "Set"),
("collections.abc", "ValuesView"),
// `contextlib`
("contextlib", "AbstractAsyncContextManager"),
("contextlib", "AbstractContextManager"),
// `re`
("re", "Match"),
("re", "Pattern"),
// `typing`
("typing", "AbstractSet"),
("typing", "AsyncContextManager"),
("typing", "AsyncGenerator"),
("typing", "AsyncIterator"),
("typing", "Awaitable"),
("typing", "BinaryIO"),
("typing", "ByteString"),
("typing", "Callable"),
("typing", "ChainMap"),
("typing", "ClassVar"),
("typing", "Collection"),
("typing", "Concatenate"),
("typing", "Container"),
("typing", "ContextManager"),
("typing", "Coroutine"),
("typing", "Counter"),
("typing", "DefaultDict"),
("typing", "Deque"),
("typing", "Dict"),
("typing", "Final"),
("typing", "FrozenSet"),
("typing", "Generator"),
("typing", "Generic"),
("typing", "IO"),
("typing", "ItemsView"),
("typing", "Iterable"),
("typing", "Iterator"),
("typing", "KeysView"),
("typing", "List"),
("typing", "Mapping"),
("typing", "Match"),
("typing", "MutableMapping"),
("typing", "MutableSequence"),
("typing", "MutableSet"),
("typing", "Optional"),
("typing", "OrderedDict"),
("typing", "Pattern"),
("typing", "Reversible"),
("typing", "Sequence"),
("typing", "Set"),
("typing", "TextIO"),
("typing", "Tuple"),
("typing", "Type"),
("typing", "TypeGuard"),
("typing", "Union"),
("typing", "Unpack"),
("typing", "ValuesView"),
// `typing.io`
("typing.io", "BinaryIO"),
("typing.io", "IO"),
("typing.io", "TextIO"),
// `typing.re`
("typing.re", "Match"),
("typing.re", "Pattern"),
// `typing_extensions`
("typing_extensions", "AsyncContextManager"),
("typing_extensions", "AsyncGenerator"),
("typing_extensions", "AsyncIterable"),
("typing_extensions", "AsyncIterator"),
("typing_extensions", "Awaitable"),
("typing_extensions", "ChainMap"),
("typing_extensions", "ClassVar"),
("typing_extensions", "Concatenate"),
("typing_extensions", "ContextManager"),
("typing_extensions", "Coroutine"),
("typing_extensions", "Counter"),
("typing_extensions", "DefaultDict"),
("typing_extensions", "Deque"),
("typing_extensions", "Type"),
// `weakref`
("weakref", "WeakKeyDictionary"),
("weakref", "WeakSet"),
("weakref", "WeakValueDictionary"),
];
// See: https://docs.python.org/3/library/typing.html
const PEP_583_SUBSCRIPTS: &[(&str, &str)] = &[
// `typing`
("typing", "Annotated"),
// `typing_extensions`
("typing_extensions", "Annotated"),
];
// These are all assumed to come from the `typing` module.
// See: https://peps.python.org/pep-0585/
static PEP_585_BUILTINS_ELIGIBLE: Lazy<FnvHashSet<&'static str>> =
Lazy::new(|| FnvHashSet::from_iter(["Dict", "FrozenSet", "List", "Set", "Tuple", "Type"]));
// These are all assumed to come from the `typing` module.
// See: https://peps.python.org/pep-0585/
static PEP_585_BUILTINS: Lazy<FnvHashSet<&'static str>> =
Lazy::new(|| FnvHashSet::from_iter(["dict", "frozenset", "list", "set", "tuple", "type"]));
fn is_pep593_annotated_subscript(name: &str) -> bool {
name == "Annotated"
}
const PEP_585_BUILTINS_ELIGIBLE: &[(&str, &str)] = &[
("typing", "Dict"),
("typing", "FrozenSet"),
("typing", "List"),
("typing", "Set"),
("typing", "Tuple"),
("typing", "Type"),
("typing_extensions", "Type"),
];
pub enum SubscriptKind {
AnnotatedSubscript,
@@ -210,72 +212,38 @@ pub enum SubscriptKind {
pub fn match_annotated_subscript(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> Option<SubscriptKind> {
match &expr.node {
ExprKind::Attribute { attr, value, .. } => {
if let ExprKind::Name { id, .. } = &value.node {
// If `id` is `typing` and `attr` is `Union`, verify that `typing.Union` is an
// annotated subscript.
if IMPORTED_SUBSCRIPTS
.get(&attr.as_str())
.map(|imports| imports.contains(&id.as_str()))
.unwrap_or_default()
{
return if is_pep593_annotated_subscript(attr) {
Some(SubscriptKind::PEP593AnnotatedSubscript)
} else {
Some(SubscriptKind::AnnotatedSubscript)
};
}
}
}
ExprKind::Name { id, .. } => {
// Built-ins (no import necessary).
if PEP_585_BUILTINS.contains(&id.as_str()) {
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
if !call_path.is_empty() {
for (module, member) in SUBSCRIPTS {
if match_call_path(&call_path, module, member, from_imports) {
return Some(SubscriptKind::AnnotatedSubscript);
}
// Verify that, e.g., `Union` is a reference to `typing.Union`.
if let Some(modules) = IMPORTED_SUBSCRIPTS.get(&id.as_str()) {
for module in modules {
if from_imports
.get(module)
.map(|imports| imports.contains(&id.as_str()) || imports.contains("*"))
.unwrap_or_default()
{
return if is_pep593_annotated_subscript(id) {
Some(SubscriptKind::PEP593AnnotatedSubscript)
} else {
Some(SubscriptKind::AnnotatedSubscript)
};
}
}
}
for (module, member) in PEP_583_SUBSCRIPTS {
if match_call_path(&call_path, module, member, from_imports) {
return Some(SubscriptKind::PEP593AnnotatedSubscript);
}
}
_ => {}
}
None
}
/// Returns `true` if `Expr` represents a reference to a typing object with a
/// PEP 585 built-in. Note that none of the PEP 585 built-ins are in
/// `typing_extensions`.
pub fn is_pep585_builtin(expr: &Expr, from_imports: &FnvHashMap<&str, FnvHashSet<&str>>) -> bool {
match &expr.node {
ExprKind::Attribute { attr, value, .. } => {
if let ExprKind::Name { id, .. } = &value.node {
id == "typing" && PEP_585_BUILTINS_ELIGIBLE.contains(&attr.as_str())
} else {
false
/// PEP 585 built-in.
pub fn is_pep585_builtin(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
if !call_path.is_empty() {
for (module, member) in PEP_585_BUILTINS_ELIGIBLE {
if match_call_path(&call_path, module, member, from_imports) {
return true;
}
}
ExprKind::Name { id, .. } => {
from_imports
.get("typing")
.map(|imports| imports.contains(&id.as_str()) || imports.contains("*"))
.unwrap_or_default()
&& PEP_585_BUILTINS_ELIGIBLE.contains(&id.as_str())
}
_ => false,
}
false
}

View File

@@ -163,7 +163,8 @@ pub fn type_of_primitive(func: &Expr, args: &[Expr], location: Range) -> Option<
pub fn unnecessary_lru_cache_params(
decorator_list: &[Expr],
target_version: PythonVersion,
imports: &FnvHashMap<&str, FnvHashSet<&str>>,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> Option<Check> {
for expr in decorator_list.iter() {
if let ExprKind::Call {
@@ -172,7 +173,14 @@ pub fn unnecessary_lru_cache_params(
keywords,
} = &expr.node
{
if args.is_empty() && helpers::match_module_member(func, "functools.lru_cache", imports)
if args.is_empty()
&& helpers::match_module_member(
func,
"functools",
"lru_cache",
from_imports,
import_aliases,
)
{
// Ex) `functools.lru_cache()`
if keywords.is_empty() {

View File

@@ -9,6 +9,7 @@ pub fn unnecessary_lru_cache_params(checker: &mut Checker, decorator_list: &[Exp
decorator_list,
checker.settings.target_version,
&checker.from_imports,
&checker.import_aliases,
) {
if checker.patch() {
if let Some(fix) =

View File

@@ -7,13 +7,14 @@ use crate::checks::{Check, CheckKind};
/// U006
pub fn use_pep585_annotation(checker: &mut Checker, expr: &Expr, id: &str) {
let replacement = checker.import_aliases.get(id).unwrap_or(&id);
let mut check = Check::new(
CheckKind::UsePEP585Annotation(id.to_string()),
CheckKind::UsePEP585Annotation(replacement.to_string()),
Range::from_located(expr),
);
if checker.patch() {
check.amend(Fix::replacement(
id.to_lowercase(),
replacement.to_lowercase(),
expr.location,
expr.end_location.unwrap(),
));

View File

@@ -1,5 +1,6 @@
use rustpython_ast::{Constant, Expr, ExprKind, Operator};
use crate::ast::helpers::{collect_call_paths, dealias_call_path};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::check_ast::Checker;
@@ -43,7 +44,8 @@ fn union(elts: &[Expr]) -> Expr {
/// U007
pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, slice: &Expr) {
if checker.match_typing_module(value, "Optional") {
let call_path = dealias_call_path(collect_call_paths(value), &checker.import_aliases);
if checker.match_typing_call_path(&call_path, "Optional") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if checker.patch() {
let mut generator = SourceGenerator::new();
@@ -58,7 +60,7 @@ pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, s
}
}
checker.add_check(check);
} else if checker.match_typing_module(value, "Union") {
} else if checker.match_typing_call_path(&call_path, "Union") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if checker.patch() {
match &slice.node {

View File

@@ -114,6 +114,7 @@ impl Hash for Settings {
self.target_version.hash(state);
// Add plugin properties in alphabetical order.
self.flake8_annotations.hash(state);
self.flake8_bugbear.hash(state);
self.flake8_quotes.hash(state);
self.isort.hash(state);
self.pep8_naming.hash(state);

View File

@@ -1,7 +1,6 @@
//! Options that the user can provide via pyproject.toml.
use std::collections::BTreeMap;
use fnv::FnvHashMap;
use serde::{Deserialize, Serialize};
use crate::checks_gen::CheckCodePrefix;
@@ -29,5 +28,5 @@ pub struct Options {
pub isort: Option<isort::settings::Options>,
pub pep8_naming: Option<pep8_naming::settings::Options>,
// Tables are required to go last.
pub per_file_ignores: Option<BTreeMap<String, Vec<CheckCodePrefix>>>,
pub per_file_ignores: Option<FnvHashMap<String, Vec<CheckCodePrefix>>>,
}

View File

@@ -96,12 +96,12 @@ pub fn load_options(pyproject: &Option<PathBuf>) -> Result<Options> {
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::env::current_dir;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::Result;
use fnv::FnvHashMap;
use crate::checks_gen::CheckCodePrefix;
use crate::flake8_quotes::settings::Quote;
@@ -346,7 +346,7 @@ other-attribute = 1
extend_select: None,
ignore: None,
extend_ignore: None,
per_file_ignores: Some(BTreeMap::from([(
per_file_ignores: Some(FnvHashMap::from_iter([(
"__init__.py".to_string(),
vec![CheckCodePrefix::F401]
),])),

View File

@@ -0,0 +1,32 @@
---
source: src/linter.rs
expression: checks
---
- kind:
LoopVariableOverridesIterator: items
location:
row: 8
column: 4
end_location:
row: 8
column: 9
fix: ~
- kind:
LoopVariableOverridesIterator: values
location:
row: 21
column: 9
end_location:
row: 21
column: 15
fix: ~
- kind:
LoopVariableOverridesIterator: vars
location:
row: 36
column: 4
end_location:
row: 36
column: 8
fix: ~

View File

@@ -0,0 +1,21 @@
---
source: src/linter.r
expression: checks
---
- kind: UselessContextlibSuppress
location:
row: 9
column: 5
end_location:
row: 9
column: 26
fix: ~
- kind: UselessContextlibSuppress
location:
row: 12
column: 5
end_location:
row: 12
column: 15
fix: ~

View File

@@ -0,0 +1,59 @@
---
source: src/linter.rs
expression: checks
---
- kind:
AbstractBaseClassWithoutAbstractMethod: Base_1
location:
row: 17
column: 0
end_location:
row: 22
column: 0
fix: ~
- kind:
AbstractBaseClassWithoutAbstractMethod: MetaBase_1
location:
row: 58
column: 0
end_location:
row: 63
column: 0
fix: ~
- kind:
AbstractBaseClassWithoutAbstractMethod: abc_Base_1
location:
row: 69
column: 0
end_location:
row: 74
column: 0
fix: ~
- kind:
AbstractBaseClassWithoutAbstractMethod: abc_Base_2
location:
row: 74
column: 0
end_location:
row: 79
column: 0
fix: ~
- kind:
AbstractBaseClassWithoutAbstractMethod: notabc_Base_1
location:
row: 79
column: 0
end_location:
row: 84
column: 0
fix: ~
- kind:
AbstractBaseClassWithoutAbstractMethod: abc_set_class_variable_4
location:
row: 128
column: 0
end_location:
row: 130
column: 0
fix: ~

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
EmptyMethodWithoutAbstractDecorator: AbstractClass
location:
row: 12
column: 4
end_location:
row: 15
column: 4
fix: ~
- kind:
EmptyMethodWithoutAbstractDecorator: AbstractClass
location:
row: 15
column: 4
end_location:
row: 18
column: 4
fix: ~
- kind:
EmptyMethodWithoutAbstractDecorator: AbstractClass
location:
row: 18
column: 4
end_location:
row: 22
column: 4
fix: ~
- kind:
EmptyMethodWithoutAbstractDecorator: AbstractClass
location:
row: 22
column: 4
end_location:
row: 29
column: 4
fix: ~

View File

@@ -2,30 +2,6 @@
source: src/linter.rs
expression: checks
---
- kind: EndsInPeriod
location:
row: 124
column: 4
end_location:
row: 126
column: 7
fix: ~
- kind: EndsInPeriod
location:
row: 283
column: 4
end_location:
row: 283
column: 33
fix: ~
- kind: EndsInPeriod
location:
row: 288
column: 4
end_location:
row: 288
column: 37
fix: ~
- kind: EndsInPeriod
location:
row: 350

View File

@@ -2,30 +2,6 @@
source: src/linter.rs
expression: checks
---
- kind: EndsInPunctuation
location:
row: 124
column: 4
end_location:
row: 126
column: 7
fix: ~
- kind: EndsInPunctuation
location:
row: 283
column: 4
end_location:
row: 283
column: 33
fix: ~
- kind: EndsInPunctuation
location:
row: 288
column: 4
end_location:
row: 288
column: 37
fix: ~
- kind: EndsInPunctuation
location:
row: 350

View File

@@ -42,4 +42,44 @@ expression: checks
row: 11
column: 0
applied: false
- kind:
UnusedImport:
- - background
- false
location:
row: 17
column: 0
end_location:
row: 17
column: 17
fix:
patch:
content: ""
location:
row: 17
column: 0
end_location:
row: 18
column: 0
applied: false
- kind:
UnusedImport:
- - datastructures
- false
location:
row: 20
column: 0
end_location:
row: 20
column: 35
fix:
patch:
content: ""
location:
row: 20
column: 0
end_location:
row: 21
column: 0
applied: false

View File

@@ -0,0 +1,50 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UndefinedName: Model
location:
row: 4
column: 9
end_location:
row: 4
column: 16
fix: ~
- kind:
UndefinedName: Model
location:
row: 9
column: 10
end_location:
row: 9
column: 17
fix: ~
- kind:
UndefinedName: Model
location:
row: 14
column: 14
end_location:
row: 14
column: 21
fix: ~
- kind:
UndefinedName: Model
location:
row: 19
column: 30
end_location:
row: 19
column: 37
fix: ~
- kind:
UndefinedName: Model
location:
row: 24
column: 18
end_location:
row: 24
column: 25
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UndefinedName: InnerClass
location:
row: 5
column: 29
end_location:
row: 5
column: 41
fix: ~

View File

@@ -5,19 +5,19 @@ expression: checks
- kind:
NonLowercaseVariableInFunction: Camel
location:
row: 3
row: 7
column: 4
end_location:
row: 3
row: 7
column: 9
fix: ~
- kind:
NonLowercaseVariableInFunction: CONSTANT
location:
row: 4
row: 8
column: 4
end_location:
row: 4
row: 8
column: 12
fix: ~

View File

@@ -5,28 +5,28 @@ expression: checks
- kind:
MixedCaseVariableInClassScope: mixedCase
location:
row: 4
row: 8
column: 4
end_location:
row: 4
row: 8
column: 13
fix: ~
- kind:
MixedCaseVariableInClassScope: _mixedCase
location:
row: 5
row: 9
column: 4
end_location:
row: 5
row: 9
column: 14
fix: ~
- kind:
MixedCaseVariableInClassScope: mixed_Case
location:
row: 6
row: 10
column: 4
end_location:
row: 6
row: 10
column: 14
fix: ~

View File

@@ -5,28 +5,28 @@ expression: checks
- kind:
MixedCaseVariableInGlobalScope: mixedCase
location:
row: 3
row: 6
column: 0
end_location:
row: 3
row: 6
column: 9
fix: ~
- kind:
MixedCaseVariableInGlobalScope: _mixedCase
location:
row: 4
row: 7
column: 0
end_location:
row: 4
row: 7
column: 10
fix: ~
- kind:
MixedCaseVariableInGlobalScope: mixed_Case
location:
row: 5
row: 8
column: 0
end_location:
row: 5
row: 8
column: 10
fix: ~

View File

@@ -9,7 +9,7 @@ expression: checks
column: 9
end_location:
row: 4
column: 13
column: 20
fix:
patch:
content: list
@@ -18,7 +18,7 @@ expression: checks
column: 9
end_location:
row: 4
column: 13
column: 20
applied: false
- kind:
UsePEP585Annotation: List
@@ -27,7 +27,7 @@ expression: checks
column: 9
end_location:
row: 11
column: 20
column: 13
fix:
patch:
content: list
@@ -36,6 +36,42 @@ expression: checks
column: 9
end_location:
row: 11
column: 20
column: 13
applied: false
- kind:
UsePEP585Annotation: List
location:
row: 18
column: 9
end_location:
row: 18
column: 15
fix:
patch:
content: list
location:
row: 18
column: 9
end_location:
row: 18
column: 15
applied: false
- kind:
UsePEP585Annotation: List
location:
row: 25
column: 9
end_location:
row: 25
column: 14
fix:
patch:
content: list
location:
row: 25
column: 9
end_location:
row: 25
column: 14
applied: false

View File

@@ -18,4 +18,12 @@ expression: checks
row: 7
column: 13
fix: ~
- kind: SysVersionSlice3Referenced
location:
row: 8
column: 6
end_location:
row: 8
column: 7
fix: ~

75
src/updates.rs Normal file
View File

@@ -0,0 +1,75 @@
use std::fs::{create_dir_all, read_to_string, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::Result;
use colored::Colorize;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
fn cache_dir() -> &'static str {
"./.ruff_cache"
}
fn file_path() -> PathBuf {
Path::new(cache_dir()).join(".update-informer")
}
/// Get the "latest" version for which the user has been informed.
fn get_latest() -> Result<Option<String>> {
let path = file_path();
if path.exists() {
Ok(Some(read_to_string(path)?.trim().to_string()))
} else {
Ok(None)
}
}
/// Set the "latest" version for which the user has been informed.
fn set_latest(version: &str) -> Result<()> {
create_dir_all(cache_dir())?;
let path = file_path();
let mut file = File::create(path)?;
file.write_all(version.trim().as_bytes())
.map_err(|e| e.into())
}
/// Update the user if a newer version is available.
pub fn check_for_updates() -> Result<()> {
use update_informer::{registry, Check};
let informer = update_informer::new(registry::PyPI, CARGO_PKG_NAME, CARGO_PKG_VERSION);
if let Some(new_version) = informer
.check_version()
.ok()
.flatten()
.map(|version| version.to_string())
{
// If we've already notified the user about this version, return early.
if let Some(latest_version) = get_latest()? {
if latest_version == new_version {
return Ok(());
}
}
set_latest(&new_version)?;
let msg = format!(
"A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",
pkg_name = CARGO_PKG_NAME.italic().cyan(),
pkg_version = CARGO_PKG_VERSION,
new_version = new_version.green()
);
let cmd = format!(
"Run to update: {cmd} {pkg_name}",
cmd = "pip3 install --upgrade".green(),
pkg_name = CARGO_PKG_NAME.green()
);
println!("\n{msg}\n{cmd}");
}
Ok(())
}