Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c89a19f76 | ||
|
|
82fea36bb3 | ||
|
|
63d63e8c12 | ||
|
|
9d136de55a | ||
|
|
1821c07367 | ||
|
|
1fe90ef7f4 | ||
|
|
b5cb9485f6 | ||
|
|
4d798512b1 | ||
|
|
5f9815b103 | ||
|
|
0d3fac1bf9 | ||
|
|
ff0e5f5cb4 | ||
|
|
374d57d822 | ||
|
|
85b2a9920f | ||
|
|
3c22913470 | ||
|
|
ea03a59b72 |
5
.github/workflows/ci.yaml
vendored
5
.github/workflows/ci.yaml
vendored
@@ -6,6 +6,11 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
|
||||
jobs:
|
||||
cargo_build:
|
||||
name: "cargo build"
|
||||
|
||||
3
.github/workflows/flake8-to-ruff.yaml
vendored
3
.github/workflows/flake8-to-ruff.yaml
vendored
@@ -10,6 +10,9 @@ env:
|
||||
PACKAGE_NAME: flake8-to-ruff
|
||||
CRATE_NAME: flake8_to_ruff
|
||||
PYTHON_VERSION: "3.7" # to build abi3 wheels
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
|
||||
jobs:
|
||||
macos-x86_64:
|
||||
|
||||
3
.github/workflows/ruff.yaml
vendored
3
.github/workflows/ruff.yaml
vendored
@@ -12,6 +12,9 @@ concurrency:
|
||||
env:
|
||||
PACKAGE_NAME: ruff
|
||||
PYTHON_VERSION: "3.7" # to build abi3 wheels
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
|
||||
jobs:
|
||||
macos-x86_64:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.119
|
||||
rev: v0.0.122
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -930,7 +930,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.119-dev.0"
|
||||
version = "0.0.122-dev.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.22",
|
||||
@@ -2238,10 +2238,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.119"
|
||||
version = "0.0.122"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"atty",
|
||||
"bincode",
|
||||
"bitflags",
|
||||
"cacache",
|
||||
@@ -2286,7 +2287,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.119"
|
||||
version = "0.0.122"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.22",
|
||||
|
||||
@@ -6,7 +6,7 @@ members = [
|
||||
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.119"
|
||||
version = "0.0.122"
|
||||
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"] }
|
||||
|
||||
24
README.md
24
README.md
@@ -52,15 +52,16 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
|
||||
4. [pydocstyle](#pydocstyle)
|
||||
5. [pyupgrade](#pyupgrade)
|
||||
6. [pep8-naming](#pep8-naming)
|
||||
7. [flake8-comprehensions](#flake8-comprehensions)
|
||||
8. [flake8-bugbear](#flake8-bugbear)
|
||||
9. [flake8-builtins](#flake8-builtins)
|
||||
10. [flake8-print](#flake8-print)
|
||||
11. [flake8-quotes](#flake8-quotes)
|
||||
12. [flake8-annotations](#flake8-annotations)
|
||||
13. [flake8-2020](#flake8-2020)
|
||||
14. [Ruff-specific rules](#ruff-specific-rules)
|
||||
15. [Meta rules](#meta-rules)
|
||||
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)
|
||||
@@ -236,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
|
||||
@@ -528,6 +531,7 @@ 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 | |
|
||||
@@ -731,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/) (25/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),
|
||||
|
||||
4
flake8_to_ruff/Cargo.lock
generated
4
flake8_to_ruff/Cargo.lock
generated
@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8_to_ruff"
|
||||
version = "0.0.119"
|
||||
version = "0.0.122"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1975,7 +1975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.119"
|
||||
version = "0.0.122"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.119-dev.0"
|
||||
version = "0.0.122-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
40
resources/test/fixtures/B020.py
vendored
Normal file
40
resources/test/fixtures/B020.py
vendored
Normal 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)
|
||||
14
resources/test/fixtures/F821_5.py
vendored
Normal file
14
resources/test/fixtures/F821_5.py
vendored
Normal 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()
|
||||
7
resources/test/fixtures/isort/add_newline_before_comments.py
vendored
Normal file
7
resources/test/fixtures/isort/add_newline_before_comments.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
# This is a comment in the same section, so we need to add one newline.
|
||||
import sys
|
||||
import numpy as np
|
||||
# This is a comment, but it starts a new section, so we don't need to add a newline
|
||||
# before it.
|
||||
import leading_prefix
|
||||
25
resources/test/fixtures/isort/comments.py
vendored
Normal file
25
resources/test/fixtures/isort/comments.py
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Comment 1
|
||||
# Comment 2
|
||||
import D
|
||||
|
||||
# Comment 3a
|
||||
import C
|
||||
|
||||
# Comment 3b
|
||||
import C
|
||||
|
||||
import B # Comment 4
|
||||
|
||||
# Comment 5
|
||||
|
||||
# Comment 6
|
||||
from A import (
|
||||
a, # Comment 7
|
||||
b,
|
||||
c, # Comment 8
|
||||
)
|
||||
from A import (
|
||||
a, # Comment 9
|
||||
b, # Comment 10
|
||||
c, # Comment 11
|
||||
)
|
||||
4
resources/test/fixtures/isort/fit_line_length_comment.py
vendored
Normal file
4
resources/test/fixtures/isort/fit_line_length_comment.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import a
|
||||
# Don't take this comment into account when determining whether the next import can fit on one line.
|
||||
from b import c
|
||||
from d import e # Do take this comment into account when determining whether the next import can fit on one line.
|
||||
11
resources/test/fixtures/isort/preserve_comment_order.py
vendored
Normal file
11
resources/test/fixtures/isort/preserve_comment_order.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import io
|
||||
# Old MacDonald had a farm,
|
||||
# EIEIO
|
||||
# And on his farm he had a cow,
|
||||
# EIEIO
|
||||
# With a moo-moo here and a moo-moo there
|
||||
# Here a moo, there a moo, everywhere moo-moo
|
||||
# Old MacDonald had a farm,
|
||||
# EIEIO
|
||||
from errno import EIO
|
||||
import abc
|
||||
2
resources/test/fixtures/isort/type_comments.py
vendored
Normal file
2
resources/test/fixtures/isort/type_comments.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import A # type: ignore
|
||||
from B import C # type: ignore
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.119"
|
||||
version = "0.0.122"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,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>)>,
|
||||
@@ -159,7 +159,13 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
|
||||
/// Return `true` if the `Expr` is a reference to `typing.${target}`.
|
||||
pub fn match_typing_module(&self, call_path: &[&str], target: &str) -> bool {
|
||||
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_call_path(call_path, "typing_extensions", target, &self.from_imports))
|
||||
@@ -867,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) {
|
||||
@@ -1036,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,
|
||||
@@ -1053,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(&collect_call_paths(value), "Literal") {
|
||||
if self.match_typing_expr(value, "Literal") {
|
||||
self.in_literal = true;
|
||||
}
|
||||
|
||||
@@ -1563,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(
|
||||
@@ -1646,12 +1665,12 @@ where
|
||||
keywords,
|
||||
} => {
|
||||
let call_path = dealias_call_path(collect_call_paths(func), &self.import_aliases);
|
||||
if self.match_typing_module(&call_path, "ForwardRef") {
|
||||
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(&call_path, "cast") {
|
||||
} else if self.match_typing_call_path(&call_path, "cast") {
|
||||
self.visit_expr(func);
|
||||
if !args.is_empty() {
|
||||
self.visit_annotation(&args[0]);
|
||||
@@ -1659,12 +1678,12 @@ where
|
||||
for expr in args.iter().skip(1) {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
} else if self.match_typing_module(&call_path, "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(&call_path, "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);
|
||||
@@ -1681,7 +1700,7 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.match_typing_module(&call_path, "NamedTuple") {
|
||||
} else if self.match_typing_call_path(&call_path, "NamedTuple") {
|
||||
self.visit_expr(func);
|
||||
|
||||
// Ex) NamedTuple("a", [("a", int)])
|
||||
@@ -1713,7 +1732,7 @@ where
|
||||
let KeywordData { value, .. } = &keyword.node;
|
||||
self.visit_annotation(value);
|
||||
}
|
||||
} else if self.match_typing_module(&call_path, "TypedDict") {
|
||||
} else if self.match_typing_call_path(&call_path, "TypedDict") {
|
||||
self.visit_expr(func);
|
||||
|
||||
// Ex) TypedDict("a", {"a": int})
|
||||
@@ -2122,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;
|
||||
@@ -2338,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);
|
||||
}
|
||||
}
|
||||
@@ -2348,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(
|
||||
@@ -2361,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ pub enum CheckCode {
|
||||
B017,
|
||||
B018,
|
||||
B019,
|
||||
B020,
|
||||
B021,
|
||||
B022,
|
||||
B024,
|
||||
@@ -399,6 +400,7 @@ pub enum CheckKind {
|
||||
NoAssertRaisesException,
|
||||
UselessExpression,
|
||||
CachedInstanceMethod,
|
||||
LoopVariableOverridesIterator(String),
|
||||
FStringDocstring,
|
||||
UselessContextlibSuppress,
|
||||
AbstractBaseClassWithoutAbstractMethod(String),
|
||||
@@ -645,6 +647,7 @@ 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()),
|
||||
@@ -890,6 +893,7 @@ 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,
|
||||
@@ -1098,6 +1102,7 @@ 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,
|
||||
@@ -1472,6 +1477,9 @@ 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(),
|
||||
|
||||
@@ -56,6 +56,7 @@ pub enum CheckCodePrefix {
|
||||
B018,
|
||||
B019,
|
||||
B02,
|
||||
B020,
|
||||
B021,
|
||||
B022,
|
||||
B024,
|
||||
@@ -386,6 +387,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::B017,
|
||||
CheckCode::B018,
|
||||
CheckCode::B019,
|
||||
CheckCode::B020,
|
||||
CheckCode::B021,
|
||||
CheckCode::B022,
|
||||
CheckCode::B024,
|
||||
@@ -412,6 +414,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::B017,
|
||||
CheckCode::B018,
|
||||
CheckCode::B019,
|
||||
CheckCode::B020,
|
||||
CheckCode::B021,
|
||||
CheckCode::B022,
|
||||
CheckCode::B024,
|
||||
@@ -460,6 +463,7 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::B018 => vec![CheckCode::B018],
|
||||
CheckCodePrefix::B019 => vec![CheckCode::B019],
|
||||
CheckCodePrefix::B02 => vec![
|
||||
CheckCode::B020,
|
||||
CheckCode::B021,
|
||||
CheckCode::B022,
|
||||
CheckCode::B024,
|
||||
@@ -467,6 +471,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::B026,
|
||||
CheckCode::B027,
|
||||
],
|
||||
CheckCodePrefix::B020 => vec![CheckCode::B020],
|
||||
CheckCodePrefix::B021 => vec![CheckCode::B021],
|
||||
CheckCodePrefix::B022 => vec![CheckCode::B022],
|
||||
CheckCodePrefix::B024 => vec![CheckCode::B024],
|
||||
@@ -1213,6 +1218,7 @@ 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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use rustpython_ast::{Arguments, Constant, Expr, ExprKind, Stmt, StmtKind};
|
||||
|
||||
use crate::ast::helpers::collect_call_paths;
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::visitor;
|
||||
use crate::ast::visitor::Visitor;
|
||||
@@ -54,7 +53,7 @@ fn check_dynamically_typed<F>(checker: &mut Checker, annotation: &Expr, func: F)
|
||||
where
|
||||
F: FnOnce() -> String,
|
||||
{
|
||||
if checker.match_typing_module(&collect_call_paths(annotation), "Any") {
|
||||
if checker.match_typing_expr(annotation, "Any") {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::DynamicallyTypedExpression(func()),
|
||||
Range::from_located(annotation),
|
||||
|
||||
@@ -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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ 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;
|
||||
@@ -32,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;
|
||||
|
||||
40
src/isort/comments.rs
Normal file
40
src/isort/comments.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rustpython_ast::Location;
|
||||
use rustpython_parser::lexer;
|
||||
use rustpython_parser::lexer::Tok;
|
||||
|
||||
use crate::ast::helpers;
|
||||
use crate::ast::types::Range;
|
||||
use crate::SourceCodeLocator;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Comment<'a> {
|
||||
pub value: Cow<'a, str>,
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
}
|
||||
|
||||
/// Collect all comments in an import block.
|
||||
pub fn collect_comments<'a>(range: &Range, locator: &'a SourceCodeLocator) -> Vec<Comment<'a>> {
|
||||
let contents = locator.slice_source_code_range(range);
|
||||
lexer::make_tokenizer(&contents)
|
||||
.flatten()
|
||||
.filter_map(|(start, tok, end)| {
|
||||
if matches!(tok, Tok::Comment) {
|
||||
let start = helpers::to_absolute(&start, &range.location);
|
||||
let end = helpers::to_absolute(&end, &range.location);
|
||||
Some(Comment {
|
||||
value: locator.slice_source_code_range(&Range {
|
||||
location: start,
|
||||
end_location: end,
|
||||
}),
|
||||
location: start,
|
||||
end_location: end,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
182
src/isort/format.rs
Normal file
182
src/isort/format.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use crate::isort::types::{AliasData, CommentSet, ImportFromData, Importable};
|
||||
|
||||
// Hard-code four-space indentation for the imports themselves, to match Black.
|
||||
const INDENT: &str = " ";
|
||||
|
||||
// Guess a capacity to use for string allocation.
|
||||
const CAPACITY: usize = 200;
|
||||
|
||||
/// Add a plain import statement to the `RopeBuilder`.
|
||||
pub fn format_import(alias: &AliasData, comments: &CommentSet, is_first: bool) -> String {
|
||||
let mut output = String::with_capacity(CAPACITY);
|
||||
if !is_first && !comments.atop.is_empty() {
|
||||
output.push('\n');
|
||||
}
|
||||
for comment in &comments.atop {
|
||||
output.push_str(comment);
|
||||
output.push('\n');
|
||||
}
|
||||
if let Some(asname) = alias.asname {
|
||||
output.push_str("import ");
|
||||
output.push_str(alias.name);
|
||||
output.push_str(" as ");
|
||||
output.push_str(asname);
|
||||
} else {
|
||||
output.push_str("import ");
|
||||
output.push_str(alias.name);
|
||||
}
|
||||
for comment in &comments.inline {
|
||||
output.push_str(" ");
|
||||
output.push_str(comment);
|
||||
}
|
||||
output.push('\n');
|
||||
output
|
||||
}
|
||||
|
||||
/// Add an import-from statement to the `RopeBuilder`.
|
||||
pub fn format_import_from(
|
||||
import_from: &ImportFromData,
|
||||
comments: &CommentSet,
|
||||
aliases: &[(AliasData, CommentSet)],
|
||||
line_length: &usize,
|
||||
is_first: bool,
|
||||
) -> String {
|
||||
// We can only inline if: (1) none of the aliases have atop comments, and (3)
|
||||
// only the last alias (if any) has inline comments.
|
||||
if aliases
|
||||
.iter()
|
||||
.all(|(_, CommentSet { atop, .. })| atop.is_empty())
|
||||
&& aliases
|
||||
.iter()
|
||||
.rev()
|
||||
.skip(1)
|
||||
.all(|(_, CommentSet { inline, .. })| inline.is_empty())
|
||||
{
|
||||
let (single_line, import_length) =
|
||||
format_single_line(import_from, comments, aliases, is_first);
|
||||
if import_length <= *line_length {
|
||||
return single_line;
|
||||
}
|
||||
}
|
||||
|
||||
format_multi_line(import_from, comments, aliases, is_first)
|
||||
}
|
||||
|
||||
/// Format an import-from statement in single-line format.
|
||||
///
|
||||
/// This method assumes that the output source code is syntactically valid.
|
||||
fn format_single_line(
|
||||
import_from: &ImportFromData,
|
||||
comments: &CommentSet,
|
||||
aliases: &[(AliasData, CommentSet)],
|
||||
is_first: bool,
|
||||
) -> (String, usize) {
|
||||
let mut output = String::with_capacity(CAPACITY);
|
||||
let mut line_length = 0;
|
||||
|
||||
if !is_first && !comments.atop.is_empty() {
|
||||
output.push('\n');
|
||||
}
|
||||
for comment in &comments.atop {
|
||||
output.push_str(comment);
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
let module_name = import_from.module_name();
|
||||
output.push_str("from ");
|
||||
output.push_str(&module_name);
|
||||
output.push_str(" import ");
|
||||
line_length += 5 + module_name.len() + 8;
|
||||
|
||||
for (index, (AliasData { name, asname }, comments)) in aliases.iter().enumerate() {
|
||||
if let Some(asname) = asname {
|
||||
output.push_str(name);
|
||||
output.push_str(" as ");
|
||||
output.push_str(asname);
|
||||
line_length += name.len() + 4 + asname.len();
|
||||
} else {
|
||||
output.push_str(name);
|
||||
line_length += name.len();
|
||||
}
|
||||
if index < aliases.len() - 1 {
|
||||
output.push_str(", ");
|
||||
line_length += 2;
|
||||
}
|
||||
|
||||
for comment in &comments.inline {
|
||||
output.push(' ');
|
||||
output.push(' ');
|
||||
output.push_str(comment);
|
||||
line_length += 2 + comment.len();
|
||||
}
|
||||
}
|
||||
|
||||
for comment in &comments.inline {
|
||||
output.push(' ');
|
||||
output.push(' ');
|
||||
output.push_str(comment);
|
||||
line_length += 2 + comment.len();
|
||||
}
|
||||
|
||||
output.push('\n');
|
||||
|
||||
(output, line_length)
|
||||
}
|
||||
|
||||
/// Format an import-from statement in multi-line format.
|
||||
fn format_multi_line(
|
||||
import_from: &ImportFromData,
|
||||
comments: &CommentSet,
|
||||
aliases: &[(AliasData, CommentSet)],
|
||||
is_first: bool,
|
||||
) -> String {
|
||||
let mut output = String::with_capacity(CAPACITY);
|
||||
|
||||
if !is_first && !comments.atop.is_empty() {
|
||||
output.push('\n');
|
||||
}
|
||||
for comment in &comments.atop {
|
||||
output.push_str(comment);
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
output.push_str("from ");
|
||||
output.push_str(&import_from.module_name());
|
||||
output.push_str(" import ");
|
||||
output.push('(');
|
||||
for comment in &comments.inline {
|
||||
output.push(' ');
|
||||
output.push(' ');
|
||||
output.push_str(comment);
|
||||
}
|
||||
output.push('\n');
|
||||
|
||||
for (AliasData { name, asname }, comments) in aliases {
|
||||
for comment in &comments.atop {
|
||||
output.push_str(INDENT);
|
||||
output.push_str(comment);
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(INDENT);
|
||||
if let Some(asname) = asname {
|
||||
output.push_str(name);
|
||||
output.push_str(" as ");
|
||||
output.push_str(asname);
|
||||
} else {
|
||||
output.push_str(name);
|
||||
}
|
||||
output.push(',');
|
||||
|
||||
for comment in &comments.inline {
|
||||
output.push(' ');
|
||||
output.push(' ');
|
||||
output.push_str(comment);
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
output.push(')');
|
||||
output.push('\n');
|
||||
|
||||
output
|
||||
}
|
||||
387
src/isort/mod.rs
387
src/isort/mod.rs
@@ -1,64 +1,266 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use fnv::FnvHashSet;
|
||||
use fnv::FnvHashMap;
|
||||
use itertools::Itertools;
|
||||
use ropey::RopeBuilder;
|
||||
use rustpython_ast::{Stmt, StmtKind};
|
||||
|
||||
use crate::isort::categorize::{categorize, ImportType};
|
||||
use crate::isort::comments::Comment;
|
||||
use crate::isort::sorting::{member_key, module_key};
|
||||
use crate::isort::types::{AliasData, ImportBlock, ImportFromData, Importable, OrderedImportBlock};
|
||||
use crate::isort::types::{
|
||||
AliasData, CommentSet, ImportBlock, ImportFromData, Importable, OrderedImportBlock,
|
||||
};
|
||||
|
||||
mod categorize;
|
||||
mod comments;
|
||||
pub mod format;
|
||||
pub mod plugins;
|
||||
pub mod settings;
|
||||
mod sorting;
|
||||
pub mod track;
|
||||
mod types;
|
||||
|
||||
// Hard-code four-space indentation for the imports themselves, to match Black.
|
||||
const INDENT: &str = " ";
|
||||
#[derive(Debug)]
|
||||
pub struct AnnotatedAliasData<'a> {
|
||||
pub name: &'a str,
|
||||
pub asname: &'a Option<String>,
|
||||
pub atop: Vec<Comment<'a>>,
|
||||
pub inline: Vec<Comment<'a>>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum AnnotatedImport<'a> {
|
||||
Import {
|
||||
names: Vec<AliasData<'a>>,
|
||||
atop: Vec<Comment<'a>>,
|
||||
inline: Vec<Comment<'a>>,
|
||||
},
|
||||
ImportFrom {
|
||||
module: &'a Option<String>,
|
||||
names: Vec<AnnotatedAliasData<'a>>,
|
||||
level: &'a Option<usize>,
|
||||
atop: Vec<Comment<'a>>,
|
||||
inline: Vec<Comment<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
fn normalize_imports<'a>(imports: &'a [&'a Stmt]) -> ImportBlock<'a> {
|
||||
let mut block: ImportBlock = Default::default();
|
||||
fn annotate_imports<'a>(
|
||||
imports: &'a [&'a Stmt],
|
||||
comments: Vec<Comment<'a>>,
|
||||
) -> Vec<AnnotatedImport<'a>> {
|
||||
let mut annotated = vec![];
|
||||
let mut comments_iter = comments.into_iter().peekable();
|
||||
for import in imports {
|
||||
match &import.node {
|
||||
StmtKind::Import { names } => {
|
||||
for name in names {
|
||||
block.import.insert(AliasData {
|
||||
name: &name.node.name,
|
||||
asname: &name.node.asname,
|
||||
});
|
||||
// Find comments above.
|
||||
let mut atop = vec![];
|
||||
while let Some(comment) =
|
||||
comments_iter.next_if(|comment| comment.location.row() < import.location.row())
|
||||
{
|
||||
atop.push(comment);
|
||||
}
|
||||
|
||||
// Find comments inline.
|
||||
let mut inline = vec![];
|
||||
while let Some(comment) = comments_iter.next_if(|comment| {
|
||||
comment.end_location.row() == import.end_location.unwrap().row()
|
||||
}) {
|
||||
inline.push(comment);
|
||||
}
|
||||
|
||||
annotated.push(AnnotatedImport::Import {
|
||||
names: names
|
||||
.iter()
|
||||
.map(|alias| AliasData {
|
||||
name: &alias.node.name,
|
||||
asname: &alias.node.asname,
|
||||
})
|
||||
.collect(),
|
||||
atop,
|
||||
inline,
|
||||
});
|
||||
}
|
||||
StmtKind::ImportFrom {
|
||||
module,
|
||||
names,
|
||||
level,
|
||||
} => {
|
||||
for name in names {
|
||||
if name.node.asname.is_none() {
|
||||
block
|
||||
// Find comments above.
|
||||
let mut atop = vec![];
|
||||
while let Some(comment) =
|
||||
comments_iter.next_if(|comment| comment.location.row() < import.location.row())
|
||||
{
|
||||
atop.push(comment);
|
||||
}
|
||||
|
||||
// Find comments inline.
|
||||
let mut inline = vec![];
|
||||
while let Some(comment) =
|
||||
comments_iter.next_if(|comment| comment.location.row() == import.location.row())
|
||||
{
|
||||
inline.push(comment);
|
||||
}
|
||||
|
||||
// Capture names.
|
||||
let mut aliases = vec![];
|
||||
for alias in names {
|
||||
// Find comments above.
|
||||
let mut alias_atop = vec![];
|
||||
while let Some(comment) = comments_iter
|
||||
.next_if(|comment| comment.location.row() < alias.location.row())
|
||||
{
|
||||
alias_atop.push(comment);
|
||||
}
|
||||
|
||||
// Find comments inline.
|
||||
let mut alias_inline = vec![];
|
||||
while let Some(comment) = comments_iter.next_if(|comment| {
|
||||
comment.end_location.row() == alias.end_location.unwrap().row()
|
||||
}) {
|
||||
alias_inline.push(comment);
|
||||
}
|
||||
|
||||
aliases.push(AnnotatedAliasData {
|
||||
name: &alias.node.name,
|
||||
asname: &alias.node.asname,
|
||||
atop: alias_atop,
|
||||
inline: alias_inline,
|
||||
})
|
||||
}
|
||||
|
||||
annotated.push(AnnotatedImport::ImportFrom {
|
||||
module,
|
||||
names: aliases,
|
||||
level,
|
||||
atop,
|
||||
inline,
|
||||
});
|
||||
}
|
||||
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
|
||||
}
|
||||
}
|
||||
annotated
|
||||
}
|
||||
|
||||
fn normalize_imports(imports: Vec<AnnotatedImport>) -> ImportBlock {
|
||||
let mut block: ImportBlock = Default::default();
|
||||
for import in imports {
|
||||
match import {
|
||||
AnnotatedImport::Import {
|
||||
names,
|
||||
atop,
|
||||
inline,
|
||||
} => {
|
||||
// Associate the comments with the first alias (best effort).
|
||||
if let Some(name) = names.first() {
|
||||
let entry = block
|
||||
.import
|
||||
.entry(AliasData {
|
||||
name: name.name,
|
||||
asname: name.asname,
|
||||
})
|
||||
.or_default();
|
||||
for comment in atop {
|
||||
entry.atop.push(comment.value);
|
||||
}
|
||||
for comment in inline {
|
||||
entry.inline.push(comment.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Create an entry for every alias.
|
||||
for name in &names {
|
||||
block
|
||||
.import
|
||||
.entry(AliasData {
|
||||
name: name.name,
|
||||
asname: name.asname,
|
||||
})
|
||||
.or_default();
|
||||
}
|
||||
}
|
||||
AnnotatedImport::ImportFrom {
|
||||
module,
|
||||
names,
|
||||
level,
|
||||
atop,
|
||||
inline,
|
||||
} => {
|
||||
// Associate the comments with the first alias (best effort).
|
||||
if let Some(alias) = names.first() {
|
||||
if alias.asname.is_none() {
|
||||
let entry = &mut block
|
||||
.import_from
|
||||
.entry(ImportFromData { module, level })
|
||||
.or_default()
|
||||
.insert(AliasData {
|
||||
name: &name.node.name,
|
||||
asname: &name.node.asname,
|
||||
});
|
||||
.0;
|
||||
for comment in atop {
|
||||
entry.atop.push(comment.value);
|
||||
}
|
||||
for comment in inline {
|
||||
entry.inline.push(comment.value);
|
||||
}
|
||||
} else {
|
||||
block.import_from_as.insert((
|
||||
ImportFromData { module, level },
|
||||
AliasData {
|
||||
name: &name.node.name,
|
||||
asname: &name.node.asname,
|
||||
},
|
||||
));
|
||||
let entry = &mut block
|
||||
.import_from_as
|
||||
.entry((
|
||||
ImportFromData { module, level },
|
||||
AliasData {
|
||||
name: alias.name,
|
||||
asname: alias.asname,
|
||||
},
|
||||
))
|
||||
.or_default();
|
||||
for comment in atop {
|
||||
entry.atop.push(comment.value);
|
||||
}
|
||||
for comment in inline {
|
||||
entry.inline.push(comment.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create an entry for every alias.
|
||||
for alias in names {
|
||||
if alias.asname.is_none() {
|
||||
let entry = block
|
||||
.import_from
|
||||
.entry(ImportFromData { module, level })
|
||||
.or_default()
|
||||
.1
|
||||
.entry(AliasData {
|
||||
name: alias.name,
|
||||
asname: alias.asname,
|
||||
})
|
||||
.or_default();
|
||||
for comment in alias.atop {
|
||||
entry.atop.push(comment.value);
|
||||
}
|
||||
for comment in alias.inline {
|
||||
entry.inline.push(comment.value);
|
||||
}
|
||||
} else {
|
||||
let entry = block
|
||||
.import_from_as
|
||||
.entry((
|
||||
ImportFromData { module, level },
|
||||
AliasData {
|
||||
name: alias.name,
|
||||
asname: alias.asname,
|
||||
},
|
||||
))
|
||||
.or_default();
|
||||
entry
|
||||
.atop
|
||||
.extend(alias.atop.into_iter().map(|comment| comment.value));
|
||||
for comment in alias.inline {
|
||||
entry.inline.push(comment.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
|
||||
}
|
||||
}
|
||||
block
|
||||
@@ -73,7 +275,7 @@ fn categorize_imports<'a>(
|
||||
) -> BTreeMap<ImportType, ImportBlock<'a>> {
|
||||
let mut block_by_type: BTreeMap<ImportType, ImportBlock> = Default::default();
|
||||
// Categorize `StmtKind::Import`.
|
||||
for alias in block.import {
|
||||
for (alias, comments) in block.import {
|
||||
let import_type = categorize(
|
||||
&alias.module_base(),
|
||||
&None,
|
||||
@@ -86,7 +288,7 @@ fn categorize_imports<'a>(
|
||||
.entry(import_type)
|
||||
.or_default()
|
||||
.import
|
||||
.insert(alias);
|
||||
.insert(alias, comments);
|
||||
}
|
||||
// Categorize `StmtKind::ImportFrom` (without re-export).
|
||||
for (import_from, aliases) in block.import_from {
|
||||
@@ -105,7 +307,7 @@ fn categorize_imports<'a>(
|
||||
.insert(import_from, aliases);
|
||||
}
|
||||
// Categorize `StmtKind::ImportFrom` (with re-export).
|
||||
for (import_from, alias) in block.import_from_as {
|
||||
for ((import_from, alias), comments) in block.import_from_as {
|
||||
let classification = categorize(
|
||||
&import_from.module_base(),
|
||||
import_from.level,
|
||||
@@ -118,7 +320,7 @@ fn categorize_imports<'a>(
|
||||
.entry(classification)
|
||||
.or_default()
|
||||
.import_from_as
|
||||
.insert((import_from, alias));
|
||||
.insert((import_from, alias), comments);
|
||||
}
|
||||
block_by_type
|
||||
}
|
||||
@@ -131,7 +333,7 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
|
||||
block
|
||||
.import
|
||||
.into_iter()
|
||||
.sorted_by_cached_key(|alias| module_key(alias.name, alias.asname)),
|
||||
.sorted_by_cached_key(|(alias, _)| module_key(alias.name, alias.asname)),
|
||||
);
|
||||
|
||||
// Sort `StmtKind::ImportFrom`.
|
||||
@@ -145,19 +347,37 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
|
||||
block
|
||||
.import_from_as
|
||||
.into_iter()
|
||||
.map(|(import_from, alias)| (import_from, FnvHashSet::from_iter([alias]))),
|
||||
.map(|((import_from, alias), comments)| {
|
||||
(
|
||||
import_from,
|
||||
(
|
||||
CommentSet {
|
||||
atop: comments.atop,
|
||||
inline: Default::default(),
|
||||
},
|
||||
FnvHashMap::from_iter([(
|
||||
alias,
|
||||
CommentSet {
|
||||
atop: Default::default(),
|
||||
inline: comments.inline,
|
||||
},
|
||||
)]),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.map(|(import_from, aliases)| {
|
||||
.map(|(import_from, (comments, aliases))| {
|
||||
// Within each `StmtKind::ImportFrom`, sort the members.
|
||||
(
|
||||
import_from,
|
||||
comments,
|
||||
aliases
|
||||
.into_iter()
|
||||
.sorted_by_cached_key(|alias| member_key(alias.name, alias.asname))
|
||||
.collect::<Vec<AliasData>>(),
|
||||
.sorted_by_cached_key(|(alias, _)| member_key(alias.name, alias.asname))
|
||||
.collect::<Vec<(AliasData, CommentSet)>>(),
|
||||
)
|
||||
})
|
||||
.sorted_by_cached_key(|(import_from, aliases)| {
|
||||
.sorted_by_cached_key(|(import_from, _, aliases)| {
|
||||
// Sort each `StmtKind::ImportFrom` by module key, breaking ties based on
|
||||
// members.
|
||||
(
|
||||
@@ -167,7 +387,7 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
|
||||
.map(|module| module_key(module, &None)),
|
||||
aliases
|
||||
.first()
|
||||
.map(|alias| member_key(alias.name, alias.asname)),
|
||||
.map(|(alias, _)| member_key(alias.name, alias.asname)),
|
||||
)
|
||||
}),
|
||||
);
|
||||
@@ -176,15 +396,18 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
|
||||
}
|
||||
|
||||
pub fn format_imports(
|
||||
block: Vec<&Stmt>,
|
||||
block: &[&Stmt],
|
||||
comments: Vec<Comment>,
|
||||
line_length: &usize,
|
||||
src: &[PathBuf],
|
||||
known_first_party: &BTreeSet<String>,
|
||||
known_third_party: &BTreeSet<String>,
|
||||
extra_standard_library: &BTreeSet<String>,
|
||||
) -> String {
|
||||
let block = annotate_imports(block, comments);
|
||||
|
||||
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
|
||||
let block = normalize_imports(&block);
|
||||
let block = normalize_imports(block);
|
||||
|
||||
// Categorize by type (e.g., first-party vs. third-party).
|
||||
let block_by_type = categorize_imports(
|
||||
@@ -195,81 +418,38 @@ pub fn format_imports(
|
||||
extra_standard_library,
|
||||
);
|
||||
|
||||
// Generate replacement source code.
|
||||
let mut output = RopeBuilder::new();
|
||||
let mut first_block = true;
|
||||
|
||||
// Generate replacement source code.
|
||||
let mut is_first_block = true;
|
||||
for import_block in block_by_type.into_values() {
|
||||
let import_block = sort_imports(import_block);
|
||||
|
||||
// Add a blank line between every section.
|
||||
if !first_block {
|
||||
if !is_first_block {
|
||||
output.append("\n");
|
||||
} else {
|
||||
first_block = false;
|
||||
is_first_block = false;
|
||||
}
|
||||
|
||||
let mut is_first_statement = true;
|
||||
|
||||
// Format `StmtKind::Import` statements.
|
||||
for AliasData { name, asname } in import_block.import.iter() {
|
||||
if let Some(asname) = asname {
|
||||
output.append(&format!("import {} as {}\n", name, asname));
|
||||
} else {
|
||||
output.append(&format!("import {}\n", name));
|
||||
}
|
||||
for (alias, comments) in import_block.import.iter() {
|
||||
output.append(&format::format_import(alias, comments, is_first_statement));
|
||||
is_first_statement = false;
|
||||
}
|
||||
|
||||
// Format `StmtKind::ImportFrom` statements.
|
||||
for (import_from, aliases) in import_block.import_from.iter() {
|
||||
let prelude: String = format!("from {} import ", import_from.module_name());
|
||||
let members: Vec<String> = aliases
|
||||
.iter()
|
||||
.map(|AliasData { name, asname }| {
|
||||
if let Some(asname) = asname {
|
||||
format!("{} as {}", name, asname)
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Can we fit the import on a single line?
|
||||
let expected_len: usize =
|
||||
// `from base import `
|
||||
prelude.len()
|
||||
// `member( as alias)?`
|
||||
+ members.iter().map(|part| part.len()).sum::<usize>()
|
||||
// `, `
|
||||
+ 2 * (members.len() - 1);
|
||||
|
||||
if expected_len <= *line_length {
|
||||
// `from base import `
|
||||
output.append(&prelude);
|
||||
// `member( as alias)?(, )?`
|
||||
for (index, part) in members.into_iter().enumerate() {
|
||||
if index > 0 {
|
||||
output.append(", ");
|
||||
}
|
||||
output.append(&part);
|
||||
}
|
||||
// `\n`
|
||||
output.append("\n");
|
||||
} else {
|
||||
// `from base import (\n`
|
||||
output.append(&prelude);
|
||||
output.append("(");
|
||||
output.append("\n");
|
||||
|
||||
// ` member( as alias)?,\n`
|
||||
for part in members {
|
||||
output.append(INDENT);
|
||||
output.append(&part);
|
||||
output.append(",");
|
||||
output.append("\n");
|
||||
}
|
||||
|
||||
// `)\n`
|
||||
output.append(")");
|
||||
output.append("\n");
|
||||
}
|
||||
for (import_from, comments, aliases) in import_block.import_from.iter() {
|
||||
output.append(&format::format_import_from(
|
||||
import_from,
|
||||
comments,
|
||||
aliases,
|
||||
line_length,
|
||||
is_first_statement,
|
||||
));
|
||||
is_first_statement = false;
|
||||
}
|
||||
}
|
||||
output.finish().to_string()
|
||||
@@ -287,13 +467,17 @@ mod tests {
|
||||
use crate::linter::test_path;
|
||||
use crate::Settings;
|
||||
|
||||
#[test_case(Path::new("add_newline_before_comments.py"))]
|
||||
#[test_case(Path::new("combine_import_froms.py"))]
|
||||
#[test_case(Path::new("comments.py"))]
|
||||
#[test_case(Path::new("deduplicate_imports.py"))]
|
||||
#[test_case(Path::new("fit_line_length.py"))]
|
||||
#[test_case(Path::new("fit_line_length_comment.py"))]
|
||||
#[test_case(Path::new("import_from_after_import.py"))]
|
||||
#[test_case(Path::new("leading_prefix.py"))]
|
||||
#[test_case(Path::new("no_reorder_within_section.py"))]
|
||||
#[test_case(Path::new("order_by_type.py"))]
|
||||
#[test_case(Path::new("preserve_comment_order.py"))]
|
||||
#[test_case(Path::new("preserve_indentation.py"))]
|
||||
#[test_case(Path::new("reorder_within_section.py"))]
|
||||
#[test_case(Path::new("separate_first_party_imports.py"))]
|
||||
@@ -303,6 +487,7 @@ mod tests {
|
||||
#[test_case(Path::new("skip.py"))]
|
||||
#[test_case(Path::new("sort_similar_imports.py"))]
|
||||
#[test_case(Path::new("trailing_suffix.py"))]
|
||||
#[test_case(Path::new("type_comments.py"))]
|
||||
fn isort(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}", path.to_string_lossy());
|
||||
let mut checks = test_path(
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::ast::types::Range;
|
||||
use crate::autofix::{fixer, Fix};
|
||||
use crate::checks::CheckKind;
|
||||
use crate::docstrings::helpers::leading_space;
|
||||
use crate::isort::format_imports;
|
||||
use crate::isort::{comments, format_imports};
|
||||
use crate::{Check, Settings, SourceCodeLocator};
|
||||
|
||||
fn extract_range(body: &[&Stmt]) -> Range {
|
||||
@@ -44,7 +44,15 @@ fn match_trailing_content(body: &[&Stmt], locator: &SourceCodeLocator) -> bool {
|
||||
end_location: Location::new(end_location.row() + 1, 0),
|
||||
};
|
||||
let suffix = locator.slice_source_code_range(&range);
|
||||
suffix.chars().any(|char| !char.is_whitespace())
|
||||
for char in suffix.chars() {
|
||||
if char == '#' {
|
||||
return false;
|
||||
}
|
||||
if !char.is_whitespace() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// I001
|
||||
@@ -57,13 +65,23 @@ pub fn check_imports(
|
||||
let range = extract_range(&body);
|
||||
let indentation = extract_indentation(&body, locator);
|
||||
|
||||
// Extract comments. Take care to grab any inline comments from the last line.
|
||||
let comments = comments::collect_comments(
|
||||
&Range {
|
||||
location: range.location,
|
||||
end_location: Location::new(range.end_location.row() + 1, 0),
|
||||
},
|
||||
locator,
|
||||
);
|
||||
|
||||
// Special-cases: there's leading or trailing content in the import block.
|
||||
let has_leading_content = match_leading_content(&body, locator);
|
||||
let has_trailing_content = match_trailing_content(&body, locator);
|
||||
|
||||
// Generate the sorted import block.
|
||||
let expected = format_imports(
|
||||
body,
|
||||
&body,
|
||||
comments,
|
||||
&(settings.line_length - indentation.len()),
|
||||
&settings.src,
|
||||
&settings.isort.known_first_party,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: src/isort/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: UnsortedImports
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 8
|
||||
column: 0
|
||||
fix:
|
||||
patch:
|
||||
content: "import os\n\n# This is a comment in the same section, so we need to add one newline.\nimport sys\n\nimport numpy as np\n\n# This is a comment, but it starts a new section, so we don't need to add a newline\n# before it.\nimport leading_prefix\n"
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 8
|
||||
column: 0
|
||||
applied: false
|
||||
|
||||
22
src/isort/snapshots/ruff__isort__tests__comments.py.snap
Normal file
22
src/isort/snapshots/ruff__isort__tests__comments.py.snap
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: src/isort/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: UnsortedImports
|
||||
location:
|
||||
row: 3
|
||||
column: 0
|
||||
end_location:
|
||||
row: 26
|
||||
column: 0
|
||||
fix:
|
||||
patch:
|
||||
content: "import B # Comment 4\n\n# Comment 3a\n# Comment 3b\nimport C\nimport D\n\n# Comment 5\n# Comment 6\nfrom A import (\n a, # Comment 7 # Comment 9\n b, # Comment 10\n c, # Comment 8 # Comment 11\n)\n"
|
||||
location:
|
||||
row: 3
|
||||
column: 0
|
||||
end_location:
|
||||
row: 26
|
||||
column: 0
|
||||
applied: false
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: src/isort/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: UnsortedImports
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 5
|
||||
column: 0
|
||||
fix:
|
||||
patch:
|
||||
content: "import a\n\n# Don't take this comment into account when determining whether the next import can fit on one line.\nfrom b import c\nfrom d import ( # Do take this comment into account when determining whether the next import can fit on one line.\n e,\n)\n"
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 5
|
||||
column: 0
|
||||
applied: false
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: src/isort/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: UnsortedImports
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 12
|
||||
column: 0
|
||||
fix:
|
||||
patch:
|
||||
content: "import abc\nimport io\n\n# Old MacDonald had a farm,\n# EIEIO\n# And on his farm he had a cow,\n# EIEIO\n# With a moo-moo here and a moo-moo there\n# Here a moo, there a moo, everywhere moo-moo\n# Old MacDonald had a farm,\n# EIEIO\nfrom errno import EIO\n"
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 12
|
||||
column: 0
|
||||
applied: false
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/isort/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use fnv::{FnvHashMap, FnvHashSet};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use fnv::FnvHashMap;
|
||||
|
||||
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub struct ImportFromData<'a> {
|
||||
@@ -12,6 +14,12 @@ pub struct AliasData<'a> {
|
||||
pub asname: &'a Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CommentSet<'a> {
|
||||
pub atop: Vec<Cow<'a, str>>,
|
||||
pub inline: Vec<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
pub trait Importable {
|
||||
fn module_name(&self) -> String;
|
||||
fn module_base(&self) -> String;
|
||||
@@ -50,17 +58,24 @@ impl Importable for ImportFromData<'_> {
|
||||
pub struct ImportBlock<'a> {
|
||||
// Set of (name, asname), used to track regular imports.
|
||||
// Ex) `import module`
|
||||
pub import: FnvHashSet<AliasData<'a>>,
|
||||
pub import: FnvHashMap<AliasData<'a>, CommentSet<'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>>>,
|
||||
pub import_from:
|
||||
FnvHashMap<ImportFromData<'a>, (CommentSet<'a>, FnvHashMap<AliasData<'a>, CommentSet<'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>)>,
|
||||
pub import_from_as: FnvHashMap<(ImportFromData<'a>, AliasData<'a>), CommentSet<'a>>,
|
||||
}
|
||||
|
||||
type AliasDataWithComments<'a> = (AliasData<'a>, CommentSet<'a>);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct OrderedImportBlock<'a> {
|
||||
pub import: Vec<AliasData<'a>>,
|
||||
pub import_from: Vec<(ImportFromData<'a>, Vec<AliasData<'a>>)>,
|
||||
pub import: Vec<AliasDataWithComments<'a>>,
|
||||
pub import_from: Vec<(
|
||||
ImportFromData<'a>,
|
||||
CommentSet<'a>,
|
||||
Vec<AliasDataWithComments<'a>>,
|
||||
)>,
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -344,6 +344,7 @@ 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")]
|
||||
@@ -456,6 +457,7 @@ mod tests {
|
||||
#[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")]
|
||||
|
||||
38
src/main.rs
38
src/main.rs
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use rustpython_ast::{Constant, Expr, ExprKind, Operator};
|
||||
|
||||
use crate::ast::helpers::collect_call_paths;
|
||||
use crate::ast::helpers::{collect_call_paths, dealias_call_path};
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
@@ -44,8 +44,8 @@ fn union(elts: &[Expr]) -> Expr {
|
||||
|
||||
/// U007
|
||||
pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, slice: &Expr) {
|
||||
let call_path = collect_call_paths(value);
|
||||
if checker.match_typing_module(&call_path, "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();
|
||||
@@ -60,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(&call_path, "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 {
|
||||
|
||||
32
src/snapshots/ruff__linter__tests__B020_B020.py.snap
Normal file
32
src/snapshots/ruff__linter__tests__B020_B020.py.snap
Normal 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: ~
|
||||
|
||||
14
src/snapshots/ruff__linter__tests__F821_F821_5.py.snap
Normal file
14
src/snapshots/ruff__linter__tests__F821_F821_5.py.snap
Normal 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: ~
|
||||
|
||||
75
src/updates.rs
Normal file
75
src/updates.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user