Compare commits

...

12 Commits

Author SHA1 Message Date
Charlie Marsh
f74050e5b1 Bump version to 0.0.207 2023-01-02 14:39:32 -05:00
Martin Fischer
90b2d85c85 Fix __init__.py being private (#1556)
Previously visibility::module_visibility() returned Private
for any module name starting with an underscore, resulting in
__init__.py being categorized as private, which in turn resulted
in D104 (Missing docstring in public package) never being reported
for __init__.py files.
2023-01-02 14:39:23 -05:00
Charlie Marsh
ccf848705d Detect unpacking assignments in eradicate (#1559) 2023-01-02 14:05:58 -05:00
Charlie Marsh
3b535fcc74 Add explicit new-rule recommendation in CONTRIBUTING.md (#1558) 2023-01-02 13:50:11 -05:00
Víctor
06321fd240 Add usage clarification to README (#1557) 2023-01-02 13:40:16 -05:00
Martin Fischer
cdae2f0e67 Fix typing::match_annotated_subscript matching ExprKind::Call (#1554) 2023-01-02 12:13:45 -05:00
Martin Fischer
f52691a90a Print warning when running debug builds without --no-cache (#1549) 2023-01-02 12:12:04 -05:00
Pedram Navid
07e47bef4b Add flake8-simplify SIM300 check for Yoda Conditions (#1539) 2023-01-01 18:37:40 -05:00
Anders Kaseorg
86b61806a5 Correct UP027 message to “generator expression” (#1540) 2023-01-01 18:30:58 -05:00
Charlie Marsh
31ce37dd8e Avoid PD false positives on some non-DataFrame expressions (#1538) 2023-01-01 17:05:57 -05:00
Charlie Marsh
2cf6d05586 Avoid triggering PD errors on method calls (#1537) 2023-01-01 17:00:17 -05:00
Colin Delahunty
65c34c56d6 Implement list-to-tuple comprehension unpacking (#1534) 2023-01-01 16:53:26 -05:00
39 changed files with 524 additions and 55 deletions

View File

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

View File

@@ -9,6 +9,15 @@ free to submit a PR. For larger changes (e.g., new lint rules, new functionality
options), consider submitting an [Issue](https://github.com/charliermarsh/ruff/issues) outlining
your proposed change.
If you're looking for a place to start, we recommend implementing a new lint rule (see:
[_Adding a new lint rule_](#example-adding-a-new-lint-rule), which will allow you to learn from and
pattern-match against the examples in the existing codebase. Many lint rules are inspired by
existing Python plugins, which can be used as a reference implementation.
As a concrete example: consider taking on one of the rules in [`flake8-simplify`](https://github.com/charliermarsh/ruff/issues/998),
and looking to the originating [Python source](https://github.com/MartinThoma/flake8-simplify) for
guidance.
### Prerequisites
Ruff is written in Rust. You'll need to install the
@@ -65,21 +74,15 @@ understand how other, similar rules are implemented.
To add a test fixture, create a file under `resources/test/fixtures`, named to match the `CheckCode`
you defined earlier (e.g., `E402.py`). This file should contain a variety of violations and
non-violations designed to evaluate and demonstrate the behavior of your lint rule. Run Ruff locally
with (e.g.) `cargo run resources/test/fixtures/E402.py --no-cache --select E402`. Once you're satisfied with the
output, codify the behavior as a snapshot test by adding a new `testcase` macro to the `mod tests`
section of `src/linter.rs`, like so:
non-violations designed to evaluate and demonstrate the behavior of your lint rule.
```rust
use test_case::test_case;
Run `cargo +nightly dev generate-all` to generate the code for your new fixture. Then run Ruff
locally with (e.g.) `cargo run resources/test/fixtures/E402.py --no-cache --select E402`.
#[test_case(CheckCode::A001, Path::new("A001.py"); "A001")]
...
```
Then, run `cargo test`. Your test will fail, but you'll be prompted to follow-up with
`cargo insta review`. Accept the generated snapshot, then commit the snapshot file alongside the
rest of your changes.
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
`test_case` macro in the relevant `src/[test-suite-name]/mod.rs` file. Then, run `cargo test`. Your
test will fail, but you'll be prompted to follow-up with `cargo insta review`. Accept the generated
snapshot, then commit the snapshot file alongside the rest of your changes.
Finally, regenerate the documentation and generated code with `cargo +nightly dev generate-all`.

8
Cargo.lock generated
View File

@@ -750,7 +750,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.206-dev.0"
version = "0.0.207-dev.0"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1878,7 +1878,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.206"
version = "0.0.207"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1946,7 +1946,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.206"
version = "0.0.207"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1967,7 +1967,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.206"
version = "0.0.207"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.206"
version = "0.0.207"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -51,7 +51,7 @@ path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix
quick-junit = { version = "0.3.2" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.206", path = "ruff_macros" }
ruff_macros = { version = "0.0.207", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }

View File

@@ -157,9 +157,9 @@ pacman -S ruff
To run Ruff, try any of the following:
```shell
ruff path/to/code/to/check.py
ruff path/to/code/
ruff path/to/code/*.py
ruff path/to/code/to/check.py # Run Ruff over `check.py`
ruff path/to/code/ # Run Ruff over all files in `/path/to/code` (and any subdirectories)
ruff path/to/code/*.py # Run Ruff over all `.py` files in `/path/to/code`
```
You can run Ruff in `--watch` mode to automatically re-run on-change:
@@ -173,7 +173,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.206'
rev: 'v0.0.207'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
@@ -689,6 +689,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| UP024 | OSErrorAlias | Replace aliased errors with `OSError` | 🛠 |
| UP025 | RewriteUnicodeLiteral | Remove unicode literals from strings | 🛠 |
| UP026 | RewriteMockImport | `mock` is deprecated, use `unittest.mock` | 🛠 |
| UP027 | RewriteListComprehension | Replace unpacked list comprehension with a generator expression | 🛠 |
### pep8-naming (N)
@@ -922,6 +923,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| SIM118 | KeyInDict | Use `key in dict` instead of `key in dict.keys()` | 🛠 |
| SIM300 | YodaConditions | Use `left == right` instead of `right == left (Yoda-conditions)` | |
### flake8-tidy-imports (TID)

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.206-dev.0"
version = "0.0.207-dev.0"
edition = "2021"
[lib]

3
foo.py
View File

@@ -1,3 +0,0 @@
import mock.mock
x = mock.mock.Mock()

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.206"
version = "0.0.207"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },

View File

@@ -0,0 +1,11 @@
# Errors
"yoda" == compare # SIM300
42 == age # SIM300
# OK
compare == "yoda"
age == 42
x == y
"yoda" == compare == 1
"yoda" == compare == someothervar
"yoda" == "yoda"

View File

View File

@@ -0,0 +1,6 @@
from __future__ import annotations
# test case for https://github.com/charliermarsh/ruff/issues/1552
def _():
x = 0
list()[x:]

View File

@@ -0,0 +1,18 @@
# Should change
foo, bar, baz = [fn(x) for x in items]
foo, bar, baz =[fn(x) for x in items]
foo, bar, baz = [fn(x) for x in items]
foo, bar, baz = [[i for i in fn(x)] for x in items]
foo, bar, baz = [
fn(x)
for x in items
]
# Should not change
foo = [fn(x) for x in items]
x, = [await foo for foo in bar]

View File

@@ -848,6 +848,9 @@
"SIM1",
"SIM11",
"SIM118",
"SIM3",
"SIM30",
"SIM300",
"T",
"T1",
"T10",
@@ -912,6 +915,7 @@
"UP024",
"UP025",
"UP026",
"UP027",
"W",
"W2",
"W29",

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.206"
version = "0.0.207"
edition = "2021"
[lib]

View File

@@ -212,6 +212,23 @@ pub fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// Return `true` if an `Expr` is not a reference to a variable (or something
/// that could resolve to a variable, like a function call).
pub fn is_non_variable(expr: &Expr) -> bool {
matches!(
expr.node,
ExprKind::Constant { .. }
| ExprKind::Tuple { .. }
| ExprKind::List { .. }
| ExprKind::Set { .. }
| ExprKind::Dict { .. }
| ExprKind::SetComp { .. }
| ExprKind::ListComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::GeneratorExp { .. }
)
}
/// Return the `Keyword` with the given name, if it's present in the list of
/// `Keyword` arguments.
pub fn find_keyword<'a>(keywords: &'a [Keyword], keyword_name: &str) -> Option<&'a Keyword> {

View File

@@ -1210,12 +1210,11 @@ where
pycodestyle::plugins::do_not_assign_lambda(self, target, value, stmt);
}
}
if self.settings.enabled.contains(&CheckCode::UP001) {
pyupgrade::plugins::useless_metaclass_type(self, stmt, value, targets);
}
if self.settings.enabled.contains(&CheckCode::B003) {
flake8_bugbear::plugins::assignment_to_os_environ(self, targets);
}
if self.settings.enabled.contains(&CheckCode::S105) {
if let Some(check) =
flake8_bandit::plugins::assign_hardcoded_password_string(value, targets)
@@ -1223,6 +1222,10 @@ where
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::UP001) {
pyupgrade::plugins::useless_metaclass_type(self, stmt, value, targets);
}
if self.settings.enabled.contains(&CheckCode::UP013) {
pyupgrade::plugins::convert_typed_dict_functional_to_class(
self, stmt, targets, value,
@@ -1233,6 +1236,10 @@ where
self, stmt, targets, value,
);
}
if self.settings.enabled.contains(&CheckCode::UP027) {
pyupgrade::plugins::unpack_list_comprehension(self, targets, value);
}
if self.settings.enabled.contains(&CheckCode::PD901) {
if let Some(check) = pandas_vet::checks::assignment_to_df(targets) {
self.add_check(check);
@@ -1581,7 +1588,7 @@ where
pylint::plugins::used_prior_global_declaration(self, id, expr);
}
}
ExprKind::Attribute { attr, .. } => {
ExprKind::Attribute { attr, value, .. } => {
// Ex) typing.List[...]
if !self.in_deferred_string_type_definition
&& self.settings.enabled.contains(&CheckCode::UP006)
@@ -1622,6 +1629,16 @@ where
] {
if self.settings.enabled.contains(&code) {
if attr == name {
// Avoid flagging on function calls (e.g., `df.values()`).
if let Some(parent) = self.current_expr_parent() {
if matches!(parent.0.node, ExprKind::Call { .. }) {
continue;
}
}
// Avoid flagging on non-DataFrames (e.g., `{"a": 1}.values`).
if helpers::is_non_variable(value) {
continue;
}
self.add_check(Check::new(code.kind(), Range::from_located(expr)));
};
}
@@ -2385,6 +2402,10 @@ where
comparators,
);
}
if self.settings.enabled.contains(&CheckCode::SIM300) {
flake8_simplify::plugins::yoda_conditions(self, expr, left, ops, comparators);
}
}
ExprKind::Constant {
value: Constant::Str(value),

View File

@@ -213,6 +213,7 @@ pub enum CheckCode {
YTT303,
// flake8-simplify
SIM118,
SIM300,
// pyupgrade
UP001,
UP003,
@@ -239,6 +240,7 @@ pub enum CheckCode {
UP024,
UP025,
UP026,
UP027,
// pydocstyle
D100,
D101,
@@ -892,6 +894,7 @@ pub enum CheckKind {
SysVersionSlice1Referenced,
// flake8-simplify
KeyInDict(String, String),
YodaConditions(String, String),
// pyupgrade
TypeOfPrimitive(Primitive),
UselessMetaclassType,
@@ -918,6 +921,7 @@ pub enum CheckKind {
OSErrorAlias(Option<String>),
RewriteUnicodeLiteral,
RewriteMockImport(MockReference),
RewriteListComprehension,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -1285,6 +1289,7 @@ impl CheckCode {
CheckCode::BLE001 => CheckKind::BlindExcept("Exception".to_string()),
// flake8-simplify
CheckCode::SIM118 => CheckKind::KeyInDict("key".to_string(), "dict".to_string()),
CheckCode::SIM300 => CheckKind::YodaConditions("left".to_string(), "right".to_string()),
// pyupgrade
CheckCode::UP001 => CheckKind::UselessMetaclassType,
CheckCode::UP003 => CheckKind::TypeOfPrimitive(Primitive::Str),
@@ -1314,6 +1319,7 @@ impl CheckCode {
CheckCode::UP024 => CheckKind::OSErrorAlias(None),
CheckCode::UP025 => CheckKind::RewriteUnicodeLiteral,
CheckCode::UP026 => CheckKind::RewriteMockImport(MockReference::Import),
CheckCode::UP027 => CheckKind::RewriteListComprehension,
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -1718,6 +1724,7 @@ impl CheckCode {
CheckCode::S106 => CheckCategory::Flake8Bandit,
CheckCode::S107 => CheckCategory::Flake8Bandit,
CheckCode::SIM118 => CheckCategory::Flake8Simplify,
CheckCode::SIM300 => CheckCategory::Flake8Simplify,
CheckCode::T100 => CheckCategory::Flake8Debugger,
CheckCode::T201 => CheckCategory::Flake8Print,
CheckCode::T203 => CheckCategory::Flake8Print,
@@ -1748,6 +1755,7 @@ impl CheckCode {
CheckCode::UP024 => CheckCategory::Pyupgrade,
CheckCode::UP025 => CheckCategory::Pyupgrade,
CheckCode::UP026 => CheckCategory::Pyupgrade,
CheckCode::UP027 => CheckCategory::Pyupgrade,
CheckCode::W292 => CheckCategory::Pycodestyle,
CheckCode::W605 => CheckCategory::Pycodestyle,
CheckCode::YTT101 => CheckCategory::Flake82020,
@@ -1946,6 +1954,7 @@ impl CheckKind {
CheckKind::SysVersionSlice1Referenced => &CheckCode::YTT303,
// flake8-simplify
CheckKind::KeyInDict(..) => &CheckCode::SIM118,
CheckKind::YodaConditions(..) => &CheckCode::SIM300,
// pyupgrade
CheckKind::TypeOfPrimitive(..) => &CheckCode::UP003,
CheckKind::UselessMetaclassType => &CheckCode::UP001,
@@ -1972,6 +1981,7 @@ impl CheckKind {
CheckKind::OSErrorAlias(..) => &CheckCode::UP024,
CheckKind::RewriteUnicodeLiteral => &CheckCode::UP025,
CheckKind::RewriteMockImport(..) => &CheckCode::UP026,
CheckKind::RewriteListComprehension => &CheckCode::UP027,
// pydocstyle
CheckKind::BlankLineAfterLastSection(..) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(..) => &CheckCode::D410,
@@ -2668,6 +2678,9 @@ impl CheckKind {
CheckKind::KeyInDict(key, dict) => {
format!("Use `{key} in {dict}` instead of `{key} in {dict}.keys()`")
}
CheckKind::YodaConditions(left, right) => {
format!("Use `{left} == {right}` instead of `{right} == {left} (Yoda-conditions)`")
}
// pyupgrade
CheckKind::TypeOfPrimitive(primitive) => {
format!("Use `{}` instead of `type(...)`", primitive.builtin())
@@ -2736,6 +2749,9 @@ impl CheckKind {
CheckKind::RewriteMockImport(..) => {
"`mock` is deprecated, use `unittest.mock`".to_string()
}
CheckKind::RewriteListComprehension => {
"Replace unpacked list comprehension with a generator expression".to_string()
}
// pydocstyle
CheckKind::FitsOnOneLine => "One-line docstring should fit on one line".to_string(),
CheckKind::BlankLineAfterSummary => {
@@ -3206,6 +3222,7 @@ impl CheckKind {
| CheckKind::RewriteCElementTree
| CheckKind::RewriteMockImport(..)
| CheckKind::RewriteUnicodeLiteral
| CheckKind::RewriteListComprehension
| CheckKind::SectionNameEndsInColon(..)
| CheckKind::SectionNotOverIndented(..)
| CheckKind::SectionUnderlineAfterName(..)
@@ -3321,6 +3338,9 @@ impl CheckKind {
MockReference::Import => "Import from `unittest.mock` instead".to_string(),
MockReference::Attribute => "Replace `mock.mock` with `mock`".to_string(),
}),
CheckKind::RewriteListComprehension => {
Some("Replace with generator expression".to_string())
}
CheckKind::NewLineAfterSectionName(name) => {
Some(format!("Add newline after \"{name}\""))
}

View File

@@ -479,6 +479,9 @@ pub enum CheckCodePrefix {
SIM1,
SIM11,
SIM118,
SIM3,
SIM30,
SIM300,
T,
T1,
T10,
@@ -543,6 +546,7 @@ pub enum CheckCodePrefix {
UP024,
UP025,
UP026,
UP027,
W,
W2,
W29,
@@ -753,6 +757,7 @@ impl CheckCodePrefix {
CheckCode::YTT302,
CheckCode::YTT303,
CheckCode::SIM118,
CheckCode::SIM300,
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
@@ -778,6 +783,7 @@ impl CheckCodePrefix {
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
CheckCode::UP027,
CheckCode::D100,
CheckCode::D101,
CheckCode::D102,
@@ -2409,10 +2415,13 @@ impl CheckCodePrefix {
CheckCodePrefix::S105 => vec![CheckCode::S105],
CheckCodePrefix::S106 => vec![CheckCode::S106],
CheckCodePrefix::S107 => vec![CheckCode::S107],
CheckCodePrefix::SIM => vec![CheckCode::SIM118],
CheckCodePrefix::SIM => vec![CheckCode::SIM118, CheckCode::SIM300],
CheckCodePrefix::SIM1 => vec![CheckCode::SIM118],
CheckCodePrefix::SIM11 => vec![CheckCode::SIM118],
CheckCodePrefix::SIM118 => vec![CheckCode::SIM118],
CheckCodePrefix::SIM3 => vec![CheckCode::SIM300],
CheckCodePrefix::SIM30 => vec![CheckCode::SIM300],
CheckCodePrefix::SIM300 => vec![CheckCode::SIM300],
CheckCodePrefix::T => vec![CheckCode::T100, CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T1 => vec![CheckCode::T100],
CheckCodePrefix::T10 => vec![CheckCode::T100],
@@ -2459,6 +2468,7 @@ impl CheckCodePrefix {
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
CheckCode::UP027,
]
}
CheckCodePrefix::U0 => {
@@ -2494,6 +2504,7 @@ impl CheckCodePrefix {
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
CheckCode::UP027,
]
}
CheckCodePrefix::U00 => {
@@ -2713,6 +2724,7 @@ impl CheckCodePrefix {
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
CheckCode::UP027,
],
CheckCodePrefix::UP0 => vec![
CheckCode::UP001,
@@ -2740,6 +2752,7 @@ impl CheckCodePrefix {
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
CheckCode::UP027,
],
CheckCodePrefix::UP00 => vec![
CheckCode::UP001,
@@ -2789,6 +2802,7 @@ impl CheckCodePrefix {
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
CheckCode::UP027,
],
CheckCodePrefix::UP020 => vec![CheckCode::UP020],
CheckCodePrefix::UP021 => vec![CheckCode::UP021],
@@ -2797,6 +2811,7 @@ impl CheckCodePrefix {
CheckCodePrefix::UP024 => vec![CheckCode::UP024],
CheckCodePrefix::UP025 => vec![CheckCode::UP025],
CheckCodePrefix::UP026 => vec![CheckCode::UP026],
CheckCodePrefix::UP027 => vec![CheckCode::UP027],
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
CheckCodePrefix::W2 => vec![CheckCode::W292],
CheckCodePrefix::W29 => vec![CheckCode::W292],
@@ -3307,6 +3322,9 @@ impl CheckCodePrefix {
CheckCodePrefix::SIM1 => SuffixLength::One,
CheckCodePrefix::SIM11 => SuffixLength::Two,
CheckCodePrefix::SIM118 => SuffixLength::Three,
CheckCodePrefix::SIM3 => SuffixLength::One,
CheckCodePrefix::SIM30 => SuffixLength::Two,
CheckCodePrefix::SIM300 => SuffixLength::Three,
CheckCodePrefix::T => SuffixLength::Zero,
CheckCodePrefix::T1 => SuffixLength::One,
CheckCodePrefix::T10 => SuffixLength::Two,
@@ -3371,6 +3389,7 @@ impl CheckCodePrefix {
CheckCodePrefix::UP024 => SuffixLength::Three,
CheckCodePrefix::UP025 => SuffixLength::Three,
CheckCodePrefix::UP026 => SuffixLength::Three,
CheckCodePrefix::UP027 => SuffixLength::Three,
CheckCodePrefix::W => SuffixLength::Zero,
CheckCodePrefix::W2 => SuffixLength::One,
CheckCodePrefix::W29 => SuffixLength::Two,

View File

@@ -46,8 +46,8 @@ pub fn run(
if paths.is_empty() {
one_time_warning!(
"{} {}",
"warning:".yellow().bold(),
"{}: {}",
"warning".yellow().bold(),
"No Python files found under the given path(s)"
);
return Ok(Diagnostics::default());
@@ -196,8 +196,8 @@ pub fn add_noqa(
if paths.is_empty() {
one_time_warning!(
"{} {}",
"warning:".yellow().bold(),
"{}: {}",
"warning".yellow().bold(),
"No Python files found under the given path(s)"
);
return Ok(0);
@@ -270,8 +270,8 @@ pub fn show_files(
if paths.is_empty() {
one_time_warning!(
"{} {}",
"warning:".yellow().bold(),
"{}: {}",
"warning".yellow().bold(),
"No Python files found under the given path(s)"
);
return Ok(());

View File

@@ -24,7 +24,7 @@ static CODING_COMMENT_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)").unwrap());
static HASH_NUMBER: Lazy<Regex> = Lazy::new(|| Regex::new(r"#\d").unwrap());
static MULTILINE_ASSIGNMENT_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s*\w+\s*=.*[(\[{]$").unwrap());
Lazy::new(|| Regex::new(r"^\s*([(\[]\s*)?(\w+\s*,\s*)*\w+\s*([)\]]\s*)?=.*[(\[{]$").unwrap());
static PARTIAL_DICTIONARY_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^\s*['"]\w+['"]\s*:.+[,{]\s*$"#).unwrap());
static PRINT_RETURN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(print|return)\b\s*").unwrap());
@@ -153,6 +153,21 @@ mod tests {
assert!(!comment_contains_code("#or else:"));
assert!(!comment_contains_code("#else True:"));
// Unpacking assignments
assert!(comment_contains_code(
"# user_content_type, _ = TimelineEvent.objects.using(db_alias).get_or_create("
));
assert!(comment_contains_code(
"# (user_content_type, _) = TimelineEvent.objects.using(db_alias).get_or_create("
));
assert!(comment_contains_code(
"# ( user_content_type , _ )= TimelineEvent.objects.using(db_alias).get_or_create("
));
assert!(comment_contains_code(
"# app_label=\"core\", model=\"user\""
));
assert!(comment_contains_code("# )"));
// TODO(charlie): This should be `true` under aggressive mode.
assert!(!comment_contains_code("#def foo():"));
}

View File

@@ -13,6 +13,7 @@ mod tests {
use crate::settings;
#[test_case(CheckCode::SIM118, Path::new("SIM118.py"); "SIM118")]
#[test_case(CheckCode::SIM300, Path::new("SIM300.py"); "SIM300")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let checks = test_path(

View File

@@ -1,3 +1,5 @@
pub use key_in_dict::{key_in_dict_compare, key_in_dict_for};
pub use yoda_conditions::yoda_conditions;
mod key_in_dict;
mod yoda_conditions;

View File

@@ -0,0 +1,40 @@
use rustpython_ast::{Cmpop, Expr, ExprKind};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
/// SIM300
pub fn yoda_conditions(
checker: &mut Checker,
expr: &Expr,
left: &Expr,
ops: &[Cmpop],
comparators: &[Expr],
) {
if !matches!(ops[..], [Cmpop::Eq]) {
return;
}
if comparators.len() != 1 {
return;
}
if !matches!(left.node, ExprKind::Constant { .. }) {
return;
}
let right = comparators.first().unwrap();
if matches!(left.node, ExprKind::Constant { .. })
& matches!(right.node, ExprKind::Constant { .. })
{
return;
}
let check = Check::new(
CheckKind::YodaConditions(left.to_string(), right.to_string()),
Range::from_located(expr),
);
checker.add_check(check);
}

View File

@@ -0,0 +1,29 @@
---
source: src/flake8_simplify/mod.rs
expression: checks
---
- kind:
YodaConditions:
- "'yoda'"
- compare
location:
row: 2
column: 0
end_location:
row: 2
column: 17
fix: ~
parent: ~
- kind:
YodaConditions:
- "42"
- age
location:
row: 3
column: 0
end_location:
row: 3
column: 9
fix: ~
parent: ~

View File

@@ -29,6 +29,7 @@ use ::ruff::settings::{pyproject, Settings};
use ::ruff::updates;
use anyhow::Result;
use clap::{CommandFactory, Parser};
use colored::Colorize;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use path_absolutize::path_dedot;
@@ -172,13 +173,29 @@ pub(crate) fn inner_main() -> Result<ExitCode> {
};
let cache = !cli.no_cache;
#[cfg(debug_assertions)]
if cache {
// `--no-cache` doesn't respect code changes, and so is often confusing during
// development.
eprintln!(
"{}: debug build without --no-cache.",
"warning".yellow().bold()
);
}
let printer = Printer::new(&format, &log_level, &autofix, &violations);
if cli.watch {
if !matches!(autofix, fixer::Mode::None) {
eprintln!("Warning: --fix is not enabled in watch mode.");
eprintln!(
"{}: --fix is not enabled in watch mode.",
"warning".yellow().bold()
);
}
if format != SerializationFormat::Text {
eprintln!("Warning: --format 'text' is used in watch mode.");
eprintln!(
"{}: --format 'text' is used in watch mode.",
"warning".yellow().bold()
);
}
// Perform an initial run instantly.

View File

@@ -81,8 +81,8 @@ mod tests {
#[test_case("result = df.to_array()", &[]; "PD011_pass_to_array")]
#[test_case("result = df.array", &[]; "PD011_pass_array")]
#[test_case("result = df.values", &[CheckCode::PD011]; "PD011_fail_values")]
// TODO(edgarrmondragon): Check that the attribute access is NOT a method call.
// #[test_case("result = {}.values()", &[]; "PD011_pass_values_call")]
#[test_case("result = df.values()", &[]; "PD011_pass_values_call")]
#[test_case("result = {}.values", &[]; "PD011_pass_values_dict")]
#[test_case("result = values", &[]; "PD011_pass_node_name")]
#[test_case("employees = pd.read_csv(input_file)", &[]; "PD012_pass_read_csv")]
#[test_case("employees = pd.read_table(input_file)", &[CheckCode::PD012]; "PD012_fail_read_table")]

View File

@@ -62,6 +62,7 @@ mod tests {
#[test_case(CheckCode::D417, Path::new("sections.py"); "D417_0")]
#[test_case(CheckCode::D418, Path::new("D.py"); "D418")]
#[test_case(CheckCode::D419, Path::new("D.py"); "D419")]
#[test_case(CheckCode::D104, Path::new("D104/__init__.py"); "D104_1")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let checks = test_path(

View File

@@ -0,0 +1,14 @@
---
source: src/pydocstyle/mod.rs
expression: checks
---
- kind: PublicPackage
location:
row: 1
column: 0
end_location:
row: 1
column: 0
fix: ~
parent: ~

View File

@@ -102,6 +102,7 @@ mod tests {
#[test_case(CheckCode::F823, Path::new("F823.py"); "F823")]
#[test_case(CheckCode::F841, Path::new("F841_0.py"); "F841_0")]
#[test_case(CheckCode::F841, Path::new("F841_1.py"); "F841_1")]
#[test_case(CheckCode::F841, Path::new("F841_2.py"); "F841_2")]
#[test_case(CheckCode::F842, Path::new("F842.py"); "F842")]
#[test_case(CheckCode::F901, Path::new("F901.py"); "F901")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {

View File

@@ -0,0 +1,6 @@
---
source: src/pyflakes/mod.rs
expression: checks
---
[]

View File

@@ -1,6 +1,6 @@
use once_cell::sync::Lazy;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Expr;
use rustpython_ast::{Expr, ExprKind};
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path};
@@ -219,6 +219,12 @@ pub fn match_annotated_subscript<F>(
where
F: Fn(&str) -> bool,
{
if !matches!(
expr.node,
ExprKind::Name { .. } | ExprKind::Attribute { .. }
) {
return None;
}
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
if !call_path.is_empty() {
for (module, member) in SUBSCRIPTS {

View File

@@ -47,6 +47,7 @@ mod tests {
#[test_case(CheckCode::UP024, Path::new("UP024_2.py"); "UP024_2")]
#[test_case(CheckCode::UP025, Path::new("UP025.py"); "UP025")]
#[test_case(CheckCode::UP026, Path::new("UP026.py"); "UP026")]
#[test_case(CheckCode::UP027, Path::new("UP027.py"); "UP027")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let checks = test_path(

View File

@@ -18,6 +18,7 @@ pub use typing_text_str_alias::typing_text_str_alias;
pub use unnecessary_encode_utf8::unnecessary_encode_utf8;
pub use unnecessary_future_import::unnecessary_future_import;
pub use unnecessary_lru_cache_params::unnecessary_lru_cache_params;
pub use unpack_list_comprehension::unpack_list_comprehension;
pub use use_pep585_annotation::use_pep585_annotation;
pub use use_pep604_annotation::use_pep604_annotation;
pub use useless_metaclass_type::useless_metaclass_type;
@@ -43,6 +44,7 @@ mod typing_text_str_alias;
mod unnecessary_encode_utf8;
mod unnecessary_future_import;
mod unnecessary_lru_cache_params;
mod unpack_list_comprehension;
mod use_pep585_annotation;
mod use_pep604_annotation;
mod useless_metaclass_type;

View File

@@ -5,6 +5,7 @@ use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
/// UP025
pub fn rewrite_unicode_literal(checker: &mut Checker, expr: &Expr, kind: &Option<String>) {
if let Some(const_kind) = kind {
if const_kind.to_lowercase() == "u" {

View File

@@ -0,0 +1,99 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
/// Returns `true` if `expr` contains an `ExprKind::Await`.
fn contains_await(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Await { .. } => true,
ExprKind::BoolOp { values, .. } => values.iter().any(contains_await),
ExprKind::NamedExpr { target, value } => contains_await(target) || contains_await(value),
ExprKind::BinOp { left, right, .. } => contains_await(left) || contains_await(right),
ExprKind::UnaryOp { operand, .. } => contains_await(operand),
ExprKind::Lambda { body, .. } => contains_await(body),
ExprKind::IfExp { test, body, orelse } => {
contains_await(test) || contains_await(body) || contains_await(orelse)
}
ExprKind::Dict { keys, values } => keys.iter().chain(values.iter()).any(contains_await),
ExprKind::Set { elts } => elts.iter().any(contains_await),
ExprKind::ListComp { elt, .. } => contains_await(elt),
ExprKind::SetComp { elt, .. } => contains_await(elt),
ExprKind::DictComp { key, value, .. } => contains_await(key) || contains_await(value),
ExprKind::GeneratorExp { elt, .. } => contains_await(elt),
ExprKind::Yield { value } => value.as_ref().map_or(false, |value| contains_await(value)),
ExprKind::YieldFrom { value } => contains_await(value),
ExprKind::Compare {
left, comparators, ..
} => contains_await(left) || comparators.iter().any(contains_await),
ExprKind::Call {
func,
args,
keywords,
} => {
contains_await(func)
|| args.iter().any(contains_await)
|| keywords
.iter()
.any(|keyword| contains_await(&keyword.node.value))
}
ExprKind::FormattedValue {
value, format_spec, ..
} => {
contains_await(value)
|| format_spec
.as_ref()
.map_or(false, |value| contains_await(value))
}
ExprKind::JoinedStr { values } => values.iter().any(contains_await),
ExprKind::Constant { .. } => false,
ExprKind::Attribute { value, .. } => contains_await(value),
ExprKind::Subscript { value, slice, .. } => contains_await(value) || contains_await(slice),
ExprKind::Starred { value, .. } => contains_await(value),
ExprKind::Name { .. } => false,
ExprKind::List { elts, .. } => elts.iter().any(contains_await),
ExprKind::Tuple { elts, .. } => elts.iter().any(contains_await),
ExprKind::Slice { lower, upper, step } => {
lower.as_ref().map_or(false, |value| contains_await(value))
|| upper.as_ref().map_or(false, |value| contains_await(value))
|| step.as_ref().map_or(false, |value| contains_await(value))
}
}
}
/// UP027
pub fn unpack_list_comprehension(checker: &mut Checker, targets: &[Expr], value: &Expr) {
let Some(target) = targets.get(0) else {
return;
};
if let ExprKind::Tuple { .. } = target.node {
if let ExprKind::ListComp { elt, generators } = &value.node {
if generators.iter().any(|generator| generator.is_async > 0) || contains_await(elt) {
return;
}
let mut check = Check::new(
CheckKind::RewriteListComprehension,
Range::from_located(value),
);
if checker.patch(&CheckCode::UP027) {
let existing = checker
.locator
.slice_source_code_range(&Range::from_located(value));
let mut content = String::with_capacity(existing.len());
content.push('(');
content.push_str(&existing[1..existing.len() - 1]);
content.push(')');
check.amend(Fix::replacement(
content,
value.location,
value.end_location.unwrap(),
));
}
checker.add_check(check);
}
}
}

View File

@@ -0,0 +1,85 @@
---
source: src/pyupgrade/mod.rs
expression: checks
---
- kind: RewriteListComprehension
location:
row: 2
column: 16
end_location:
row: 2
column: 38
fix:
content: (fn(x) for x in items)
location:
row: 2
column: 16
end_location:
row: 2
column: 38
parent: ~
- kind: RewriteListComprehension
location:
row: 4
column: 15
end_location:
row: 4
column: 37
fix:
content: (fn(x) for x in items)
location:
row: 4
column: 15
end_location:
row: 4
column: 37
parent: ~
- kind: RewriteListComprehension
location:
row: 6
column: 25
end_location:
row: 6
column: 47
fix:
content: (fn(x) for x in items)
location:
row: 6
column: 25
end_location:
row: 6
column: 47
parent: ~
- kind: RewriteListComprehension
location:
row: 8
column: 16
end_location:
row: 8
column: 51
fix:
content: "([i for i in fn(x)] for x in items)"
location:
row: 8
column: 16
end_location:
row: 8
column: 51
parent: ~
- kind: RewriteListComprehension
location:
row: 10
column: 16
end_location:
row: 13
column: 1
fix:
content: "(\n fn(x)\n for x in items\n)"
location:
row: 10
column: 16
end_location:
row: 13
column: 1
parent: ~

View File

@@ -105,17 +105,48 @@ pub fn is_init(stmt: &Stmt) -> bool {
}
}
/// Returns `true` if a module name indicates private visibility.
fn is_private_module(module_name: &str) -> bool {
module_name.starts_with('_') || (module_name.starts_with("__") && module_name.ends_with("__"))
/// Returns `true` if a module name indicates public visibility.
fn is_public_module(module_name: &str) -> bool {
!module_name.starts_with('_') || (module_name.starts_with("__") && module_name.ends_with("__"))
}
/// Returns `true` if a module name indicates private visibility.
fn is_private_module(module_name: &str) -> bool {
!is_public_module(module_name)
}
/// Return the stem of a module name (everything preceding the last dot).
fn stem(path: &str) -> &str {
if let Some(index) = path.rfind('.') {
&path[..index]
} else {
path
}
}
/// Return the `Visibility` of the Python file at `Path` based on its name.
pub fn module_visibility(path: &Path) -> Visibility {
for component in path.iter().rev() {
if is_private_module(&component.to_string_lossy()) {
let mut components = path.iter().rev();
// Is the module itself private?
// Ex) `_foo.py` (but not `__init__.py`)
if let Some(filename) = components.next() {
let module_name = filename.to_string_lossy();
let module_name = stem(&module_name);
if is_private_module(module_name) {
return Visibility::Private;
}
}
// Is the module in a private parent?
// Ex) `_foo/bar.py`
for component in components {
let module_name = component.to_string_lossy();
if is_private_module(&module_name) {
return Visibility::Private;
}
}
Visibility::Public
}