Compare commits

...

61 Commits

Author SHA1 Message Date
Charlie Marsh
74a8a218f3 Bump version to 0.0.224 2023-01-16 23:43:14 -05:00
Colin Delahunty
1730f2a603 [pyupgrade] Automatically rewrite format-strings to f-strings (#1905) 2023-01-16 23:06:39 -05:00
Charlie Marsh
a4862857de Update PIE796 fixture 2023-01-16 19:29:14 -05:00
Leonardo Esparis
6e88c60c46 Add flake8-pie PIE796: prefer-unique-enum (#1923)
I accept any suggestion. By the way, I have a doubt, I have checked and all flake8-pie plugins can be fixed by ruff, but is it necessary that this one is also fixed automatically ?

rel #1543
2023-01-16 19:27:34 -05:00
Charlie Marsh
2ed1f78873 Add benchmark scripts for no-IO (#1925) 2023-01-16 17:38:40 -05:00
Charlie Marsh
f3bf008aed Avoid removing statements that contain side-effects (#1920)
Closes #1917.
2023-01-16 14:45:02 -05:00
Charlie Marsh
3b4aaa53c1 Add some new testimonials (#1921) 2023-01-16 14:44:52 -05:00
Charlie Marsh
6abf71639f Avoid syntax errors when fixing parenthesized unused variables (#1919)
Closes #1917.
2023-01-16 14:27:41 -05:00
Charlie Marsh
c0845a8c28 Rewrite lru_cache to cache on Python 3.9+ (#1918)
Closes #1913.
2023-01-16 13:14:27 -05:00
Paul Barrett
019ecc4add Trigger update to pre-commit mirror after pypi publish (#1910) 2023-01-16 13:14:18 -05:00
Martin Fischer
f4cf48d885 refactor: Move rule-specific details out of mod.rs via type aliases 2023-01-16 11:27:24 -05:00
Martin Fischer
005f5d7911 refactor: Make flake8_tidy_imports::Settings derive Default 2023-01-16 11:27:24 -05:00
Martin Fischer
2fce580693 refactor: Move flake8_tidy_imports Settings to mod.rs 2023-01-16 11:27:24 -05:00
Martin Fischer
8862565a0f refactor: Split ruff::rules::flake8_tidy_imports::rules 2023-01-16 11:27:24 -05:00
Martin Fischer
5bf6da0db7 refactor: Rename BannedRelativeImport to RelativeImports
The idea is to follow the Rust naming convention for lints[1]:

> the lint name should make sense when read as
> "allow lint-name" or "allow lint-name items"

Following that convention prefixing "Banned" is
redundant as it could be prefixed to any lint name.

[1]: https://rust-lang.github.io/rfcs/0344-conventions-galore.html#lints
2023-01-16 11:27:24 -05:00
Martin Fischer
ee655c1a88 refactor: Rename BannedApi to ApiBan to distinguish it from the violation struct 2023-01-16 11:27:24 -05:00
Harutaka Kawamura
2236b4bd59 Add backticks to B904's message (#1914)
This PR adds backticks to B904's message to improve readability.


Without backticks:

<img width="1480" alt="image" src="https://user-images.githubusercontent.com/17039389/212682457-71f13de9-e3dd-4ead-a82b-98e5b60653c2.png">

With backticks:

<img width="1480" alt="image" src="https://user-images.githubusercontent.com/17039389/212682775-36868401-b63e-47d1-ae25-b43b61866b6c.png">
2023-01-16 11:12:43 -05:00
Charlie Marsh
fbf311f7d5 Add instructions for Pyupgrade benchmark 2023-01-16 03:21:31 -05:00
Martin Fischer
8c18b28bc4 Derive Hash instead of implementing it by hand
The caching mechanism of the CLI (ruff_cli::cache) relies on
ruff::settings::Settings implementing the Hash trait.

The ruff::settings::Settings struct previously couldn't automatically
derive the Hash implementation via the #[derive(Hash)] macro attribute
since some of its field types intentionally[1][2] don't implement Hash
(namely regex::Regex, globset::GlobMatcher and globset::GlobSet and
HashMap and HashSet from the standard library).

The code therefore previously implemented the Hash trait by hand for the
whole struct. Implementing Hash by hand for structs that are subject to
change is a bad idea since it's very easy to forget to update the Hash
implementation when adding a new field to the struct. And the Hash
implementation indeed was already incorrect by omitting several fields
from the hash.

This commit introduces wrapper types for Regex, GlobMatcher, GlobSet,
HashSet & HashMap that implement Hash so that we can still add
#[derive(Hash)] to the Settings struct, guaranteeing a correct hash
implementation.

[1]: https://github.com/rust-lang/regex/issues/364#issuecomment-301082076
[2]: The standard library doesn't impl<T: Hash + Ord> Hash for HashSet<T>
     presumably since sorted() requires an allocation and Hash
     implementations are generally expected to work without allocations.
2023-01-16 01:42:55 -05:00
Charlie Marsh
42031b8574 Re-run benchmark and update documentation (#1907)
Closes #269.
2023-01-16 01:38:58 -05:00
Charlie Marsh
3a3a5fcd81 Remove -dev suffix from flake8_to_ruff 2023-01-15 22:45:14 -05:00
Charlie Marsh
e8577d5e26 Bump version to 0.0.223 2023-01-15 22:44:01 -05:00
Charlie Marsh
bcb1e6ba20 Add flake8-commas to the README 2023-01-15 22:43:29 -05:00
Charlie Marsh
15403522c1 Avoid triggering SIM117 for async with statements (#1903)
Actually, it looks like _none_ of the existing rules should be triggered on async `with` statements.

Closes #1902.
2023-01-15 21:42:36 -05:00
messense
cb4f305ced Lock stdout once when printing diagnostics (#1901)
https://doc.rust-lang.org/stable/std/io/struct.Stdout.html

> Each handle shares a global buffer of data to be written to the standard output stream.
> Access is also synchronized via a lock and
> explicit control over locking is available via the [`lock`](https://doc.rust-lang.org/stable/std/io/struct.Stdout.html#method.lock) method.
2023-01-15 21:04:00 -05:00
Charlie Marsh
d71a615b18 Buffer diagnostic writes to stdout (#1900) 2023-01-15 19:34:15 -05:00
Charlie Marsh
dfc2a34878 Remove rogue println 2023-01-15 18:59:59 -05:00
Charlie Marsh
7608087776 Don't require docstrings for setters and deleters (#1899) 2023-01-15 18:57:38 -05:00
Charlie Marsh
228f033e15 Skip noqa checker if no diagnostics are found (#1898) 2023-01-15 18:53:00 -05:00
Martin Fischer
d75d6d7c7c refactor: Split CliSettings from Settings
We want to automatically derive Hash for the library settings, which
requires us to split off all the settings unused by the library
(since these shouldn't affect the hash used by ruff_cli::cache).
2023-01-15 15:19:42 -05:00
Martin Fischer
ef80ab205c Mark Settings::for_rule(s) as test-only 2023-01-15 15:19:42 -05:00
Ran Benita
d3041587ad Implement flake8-commas (#1872)
Implements [flake8-commas](https://github.com/PyCQA/flake8-commas). Fixes #1058.

The plugin is mostly redundant with Black (and also deprecated upstream), but very useful for projects which can't/won't use an auto-formatter. 

This linter works on tokens. Before porting to Rust, I cleaned up the Python code ([link](https://gist.github.com/bluetech/7c5dcbdec4a73dd5a74d4bc09c72b8b9)) and made sure the tests pass. In the Rust version I tried to add explanatory comments, to the best of my understanding of the original logic.

Some changes I did make:

- Got rid of rule C814 - "missing trailing comma in Python 2". Ruff doesn't support Python 2.
- Merged rules C815 - "missing trailing comma in Python 3.5+" and C816 - "missing trailing comma in Python 3.6+" into C812 - "missing trailing comma". These Python versions are outdated, didn't think it was worth the complication.
- Added autofixes for C812 and C819.

Autofix is missing for C818 - "trailing comma on bare tuple prohibited". It needs to turn e.g. `x = 1,` into `x = (1, )`, it's a bit difficult to do with tokens only, so I skipped it for now.

I ran the rules on cpython/Lib and on a big internal code base and it works as intended (though I only sampled the diffs).
2023-01-15 14:03:32 -05:00
Harutaka Kawamura
8d912404b7 Use more precise error ranges for RET505~508 (#1895) 2023-01-15 13:54:24 -05:00
Tom Fryers
85bdb45eca Improve magic value message wording (#1892)
The message previously specified 'number', but the error applies to more types.
2023-01-15 12:53:02 -05:00
messense
c7d0d26981 Update add plugin/rule scripts (#1889)
Adjusted some file locations and changed to use [`pathlib`](https://docs.python.org/3/library/pathlib.html) instead of `os.path`.
2023-01-15 12:49:42 -05:00
Charlie Marsh
5c6753e69e Remove some Clippy allows (#1888) 2023-01-15 02:32:36 -05:00
Charlie Marsh
3791ca721a Add a dedicated token indexer for continuations and comments (#1886)
The primary motivation is that we can now robustly detect `\` continuations due to the addition of `Tok::NonLogicalNewline`. This PR generalizes the approach we took to comments (track all lines that contain any comments), and applies it to continuations too.
2023-01-15 01:57:31 -05:00
Charlie Marsh
2c644619e0 Convert confusable violations to named fields (#1887)
See: #1871.
2023-01-15 01:56:18 -05:00
Martin Fischer
81996f1bcc Convert define_rule_mapping! to a procedural macro
define_rule_mapping! was previously implemented as a declarative macro,
which was however partially relying on an origin_by_code! proc macro
because declarative macros cannot match on substrings of identifiers.

Currently all define_rule_mapping! lines look like the following:

    TID251 => violations::BannedApi,
    TID252 => violations::BannedRelativeImport,

We want to break up violations.rs, moving the violation definitions to
the respective rule modules. To do this we want to change the previous
lines to:

    TID251 => rules::flake8_tidy_imports::banned_api::BannedApi,
    TID252 => rules::flake8_tidy_imports::relative_imports::RelativeImport,

This however doesn't work because the define_rule_mapping! macro is
currently defined as:

    ($($code:ident => $mod:ident::$name:ident,)+) => { ... }

That is it only supported $module::$name but not longer paths with
multiple modules. While we could define `=> $path:path`[1] then we
could no longer access the last path segment, which we need because
we use it for the DiagnosticKind variant names. And
`$path:path::$last:ident` doesn't work either because it would be
ambiguous (Rust wouldn't know where the path ends ... so path fragments
have to be followed by some punctuation/keyword that may not be part of
paths). And we also cannot just introduce a procedural macro like
path_basename!(...) because the following is not valid Rust code:

    enum Foo { foo!(...), }

(macros cannot be called in the place where you define variants.)

So we have to convert define_rule_mapping! into a proc macro in order to
support paths of arbitrary length and this commit implements that.

[1]: https://doc.rust-lang.org/reference/macros-by-example.html#metavariables
2023-01-15 01:54:57 -05:00
Charlie Marsh
e3cc918b93 Bump version to 0.0.222 2023-01-14 23:34:53 -05:00
Charlie Marsh
d864477876 Turn doc references into links (#1878) 2023-01-14 23:27:45 -05:00
Charlie Marsh
e1ced89624 Document breaking --max-complexity change 2023-01-14 23:22:38 -05:00
Martin Fischer
4470d7ba04 Make the CI check for broken links in the Rust docs (#1883) 2023-01-14 23:18:17 -05:00
Harutaka Kawamura
2a1601749f Fix range of SIM201, 202, and 208 (#1880)
Before

```
resources/test/fixtures/flake8_simplify/SIM208.py:1:13: SIM208 Use `a` instead of `not (not a)`
  |
1 | if not (not a):  # SIM208
  |             ^ SIM208
  |
  = help: Replace with `a`

resources/test/fixtures/flake8_simplify/SIM208.py:4:14: SIM208 Use `a == b` instead of `not (not a == b)`
  |
4 | if not (not (a == b)):  # SIM208
  |              ^^^^^^ SIM208
  |
  = help: Replace with `a == b`
```

After

```
resources/test/fixtures/flake8_simplify/SIM208.py:1:4: SIM208 Use `a` instead of `not (not a)`
  |
1 | if not (not a):  # SIM208
  |    ^^^^^^^^^^^ SIM208
  |
  = help: Replace with `a`

resources/test/fixtures/flake8_simplify/SIM208.py:4:4: SIM208 Use `a == b` instead of `not (not a == b)`
  |
4 | if not (not (a == b)):  # SIM208
  |    ^^^^^^^^^^^^^^^^^^ SIM208
  |
  = help: Replace with `a == b`
```
2023-01-14 21:17:32 -05:00
Charlie Marsh
a01edad1c4 Remove --max-complexity from the CLI (#1877) 2023-01-14 18:27:23 -05:00
Martin Fischer
a81ac6705d Make ruff::source_code::{Generator, Locator, Stylist} private 2023-01-14 18:23:59 -05:00
Martin Fischer
d77675f30d Make ruff::{ast, autofix, directives, rustpython_helpers} private 2023-01-14 18:23:59 -05:00
Martin Fischer
fe7658199d Make ruff::violations private 2023-01-14 18:23:59 -05:00
Martin Fischer
cfa25ea4b0 Make ruff::rules private 2023-01-14 18:23:59 -05:00
Martin Fischer
c7f0f3b237 Regenerate insta snapshots 2023-01-14 11:48:02 -05:00
Martin Fischer
3b36030461 Introduce ruff::rules module
Resolves #1547.
2023-01-14 11:48:02 -05:00
Charlie Marsh
812df77246 Add Dagster and SnowCLI 2023-01-14 10:45:16 -05:00
Martin Fischer
69b356e9b9 Add top-level doc comments for crates
Test by running:

    cargo doc --no-deps --all --open
2023-01-14 10:11:30 -05:00
Martin Fischer
033d7d7e91 Disable doc generation for the ruff_cli binary 2023-01-14 10:11:30 -05:00
Martin Fischer
a181ca7a3d Reduce the API of ruff_cli to ruff_cli::help() 2023-01-14 10:11:30 -05:00
Martin Fischer
92124001d5 Turn ruff_dev into a bin-only crate 2023-01-14 10:11:30 -05:00
Martin Fischer
06b389c5bc Turn flake8_to_ruff into a bin-only crate 2023-01-14 10:11:30 -05:00
Thomas MK
9dc66b5a65 Split up the table corresponding to the pylint rules (#1868)
This makes it easier to see which rules you're enabling when selecting
one of the pylint codes (like `PLC`). This also makes it clearer what
those abbreviations stand for. When I first saw the pylint section, I
was very confused by that, so other might be as well.

See it rendered here:
https://github.com/thomkeh/ruff/blob/patch-1/README.md#pylint-plc-ple-plr-plw
2023-01-14 08:07:02 -05:00
Ran Benita
3447dd3615 Bump RustPython (#1836)
This bumps RustPython so we can use the new `NonLogicalNewline` token.
A couple of rules needed a fix due to the new token. There might be more
that are not caught by tests (anything working with tokens directly with
lookaheads), I hope not.
2023-01-14 08:03:27 -05:00
Harutaka Kawamura
42cb106377 Improve SIM117 (#1867)
This PR makes the following changes to improve `SIM117`:

- Avoid emitting `SIM117` multiple times within the same `with`
statement:
- Adjust the error range.  


## Example

```python
with A() as a:  # SIM117
    with B() as b:
        with C() as c:
            print("hello")
```

### Current

```
resources/test/fixtures/flake8_simplify/SIM117.py:5:1: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
  |
5 | / with A() as a:  # SIM117
6 | |     with B() as b:
7 | |         with C() as c:
8 | |             print("hello")
  | |__________________________^ SIM117
  |

resources/test/fixtures/flake8_simplify/SIM117.py:6:5: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
  |
6 |       with B() as b:
  |  _____^
7 | |         with C() as c:
8 | |             print("hello")
  | |__________________________^ SIM117
  |
```

### Improved

```
resources/test/fixtures/flake8_simplify/SIM117.py:5:1: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
  |
5 | / with A() as a:  # SIM117
6 | |     with B() as b:
7 | |         with C() as c:
  | |______________________^ SIM117
  |
```

Signed-off-by: harupy <hkawamura0130@gmail.com>
2023-01-14 07:59:24 -05:00
Charlie Marsh
027382f891 Add support for namespace packages (#1859)
Closes #1817.
2023-01-14 07:31:57 -05:00
936 changed files with 8172 additions and 3346 deletions

View File

@@ -80,6 +80,9 @@ jobs:
cargo insta test --all --delete-unreferenced-snapshots
git diff --exit-code
- run: cargo test --package ruff_cli --test black_compatibility_test -- --ignored
# Check for broken links in the documentation.
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
- run: RUSTDOCFLAGS="-D warnings" cargo doc --all --no-deps
# TODO(charlie): Re-enable the `wasm-pack` tests.
# See: https://github.com/charliermarsh/ruff/issues/1425

View File

@@ -293,3 +293,6 @@ jobs:
run: |
pip install --upgrade twine
twine upload --skip-existing *
- name: Update pre-commit mirror
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.RUFF_PRE_COMMIT_PAT }}" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/charliermarsh/ruff-pre-commit/dispatches --data '{"event_type": "build_pypi_release"}'

View File

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

View File

@@ -1,5 +1,20 @@
# Breaking Changes
## 0.0.222
### `--max-complexity` has been removed from the CLI ([#1877](https://github.com/charliermarsh/ruff/pull/1877))
The McCabe plugin's `--max-complexity` setting has been removed from the CLI, for consistency with
the treatment of other, similar settings.
To set the maximum complexity, use the `max-complexity` property in your `pyproject.toml` file,
like so:
```toml
[tool.ruff.mccabe]
max-complexity = 10
```
## 0.0.181
### Files excluded by `.gitignore` are now ignored ([#1234](https://github.com/charliermarsh/ruff/pull/1234))

69
Cargo.lock generated
View File

@@ -735,7 +735,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.221-dev.0"
version = "0.0.224"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1364,6 +1364,27 @@ dependencies = [
"libc",
]
[[package]]
name = "num_enum"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "once_cell"
version = "1.17.0"
@@ -1621,6 +1642,17 @@ dependencies = [
"termtree",
]
[[package]]
name = "proc-macro-crate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9"
dependencies = [
"once_cell",
"thiserror",
"toml",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -1874,7 +1906,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.221"
version = "0.0.224"
dependencies = [
"anyhow",
"bitflags",
@@ -1926,7 +1958,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.221"
version = "0.0.224"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1963,7 +1995,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.221"
version = "0.0.224"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1984,7 +2016,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.221"
version = "0.0.224"
dependencies = [
"once_cell",
"proc-macro2",
@@ -2027,8 +2059,8 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=d532160333ffeb6dbeca2c2728c2391cd1e53b7f#d532160333ffeb6dbeca2c2728c2391cd1e53b7f"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=acbc517b55406c76da83d7b2711941d8d3f65b87#acbc517b55406c76da83d7b2711941d8d3f65b87"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -2037,8 +2069,8 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=d532160333ffeb6dbeca2c2728c2391cd1e53b7f#d532160333ffeb6dbeca2c2728c2391cd1e53b7f"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=acbc517b55406c76da83d7b2711941d8d3f65b87#acbc517b55406c76da83d7b2711941d8d3f65b87"
dependencies = [
"ascii",
"bitflags",
@@ -2062,8 +2094,8 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=d532160333ffeb6dbeca2c2728c2391cd1e53b7f#d532160333ffeb6dbeca2c2728c2391cd1e53b7f"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=acbc517b55406c76da83d7b2711941d8d3f65b87#acbc517b55406c76da83d7b2711941d8d3f65b87"
dependencies = [
"bincode",
"bitflags",
@@ -2072,15 +2104,15 @@ dependencies = [
"lz4_flex",
"num-bigint",
"num-complex",
"num_enum",
"serde",
"static_assertions",
"thiserror",
]
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=d532160333ffeb6dbeca2c2728c2391cd1e53b7f#d532160333ffeb6dbeca2c2728c2391cd1e53b7f"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=acbc517b55406c76da83d7b2711941d8d3f65b87#acbc517b55406c76da83d7b2711941d8d3f65b87"
dependencies = [
"ahash",
"anyhow",
@@ -2518,6 +2550,15 @@ dependencies = [
"regex",
]
[[package]]
name = "toml"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.5.0"

View File

@@ -8,7 +8,7 @@ default-members = [".", "ruff_cli"]
[package]
name = "ruff"
version = "0.0.221"
version = "0.0.224"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -46,11 +46,11 @@ once_cell = { version = "1.16.0" }
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.221", path = "ruff_macros" }
ruff_macros = { version = "0.0.224", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
schemars = { version = "0.8.11" }
semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] }

256
README.md
View File

@@ -10,9 +10,9 @@ An extremely fast Python linter, written in Rust.
<p align="center">
<picture align="center">
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1309177/210156880-a97c2a0d-2c03-4393-8695-36547935a94e.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1309177/210156881-a88fd142-5008-4695-9407-d028cec3eff7.svg">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/210156881-a88fd142-5008-4695-9407-d028cec3eff7.svg">
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1309177/212613422-7faaf278-706b-4294-ad92-236ffcab3430.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1309177/212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928.svg">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928.svg">
</picture>
</p>
@@ -51,16 +51,18 @@ Ruff is extremely actively developed and used in major open-source projects like
- [Apache Airflow](https://github.com/apache/airflow)
- [Bokeh](https://github.com/bokeh/bokeh)
- [Zulip](https://github.com/zulip/zulip)
- [Dagster](https://github.com/dagster-io/dagster)
- [Pydantic](https://github.com/pydantic/pydantic)
- [Sphinx](https://github.com/sphinx-doc/sphinx)
- [Hatch](https://github.com/pypa/hatch)
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Synapse](https://github.com/matrix-org/synapse)
- [Synapse (Matrix)](https://github.com/matrix-org/synapse)
- [Saleor](https://github.com/saleor/saleor)
- [Polars](https://github.com/pola-rs/polars)
- [Ibis](https://github.com/ibis-project/ibis)
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
- [`pyca/cryptography`](https://github.com/pyca/cryptography)
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- [SnowCLI (Snowflake)](https://github.com/Snowflake-Labs/snowcli)
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -72,6 +74,13 @@ of [FastAPI](https://github.com/tiangolo/fastapi):
> Ruff is so fast that sometimes I add an intentional bug in the code just to confirm it's actually
> running and checking the code.
[**Nick Schrock**](https://twitter.com/schrockn/status/1612615862904827904), founder of [Elementl](https://www.elementl.com/),
co-creator of [GraphQL](https://graphql.org/):
> Why is Ruff a gamechanger? Primarily because it is nearly 1000x faster. Literally. Not a typo. On
> our largest module (dagster itself, 250k LOC) pylint takes about 2.5 minutes, parallelized across 4
> cores on my M1. Running ruff against our *entire* codebase takes .4 seconds.
[**Bryan Van de Ven**](https://github.com/bokeh/bokeh/pull/12605), co-creator
of [Bokeh](https://github.com/bokeh/bokeh/), original author
of [Conda](https://docs.conda.io/en/latest/):
@@ -80,7 +89,13 @@ of [Conda](https://docs.conda.io/en/latest/):
> ~20s. This is an enormous quality of life improvement for local dev. It's fast enough that I added
> it as an actual commit hook, which is terrific.
[**Tim Abbott**](https://github.com/charliermarsh/ruff/issues/465#issuecomment-1317400028), lead developer of [Zulip](https://github.com/zulip/zulip):
[**Timothy Crosley**](https://twitter.com/timothycrosley/status/1606420868514877440),
creator of [isort](https://github.com/PyCQA/isort):
> Just switched my first project to Ruff. Only one downside so far: it's so fast I couldn't believe it was working till I intentionally introduced some errors.
[**Tim Abbott**](https://github.com/charliermarsh/ruff/issues/465#issuecomment-1317400028), lead
developer of [Zulip](https://github.com/zulip/zulip):
> This is just ridiculously fast... `ruff` is amazing.
@@ -121,6 +136,7 @@ of [Conda](https://docs.conda.io/en/latest/):
1. [pygrep-hooks (PGH)](#pygrep-hooks-pgh)
1. [Pylint (PLC, PLE, PLR, PLW)](#pylint-plc-ple-plr-plw)
1. [flake8-pie (PIE)](#flake8-pie-pie)
1. [flake8-commas (COM)](#flake8-commas-com)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf)<!-- End auto-generated table of contents. -->
1. [Editor Integrations](#editor-integrations)
1. [FAQ](#faq)
@@ -182,7 +198,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.221'
rev: 'v0.0.224'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
@@ -383,8 +399,6 @@ Options:
The minimum Python version that should be supported
--line-length <LINE_LENGTH>
Set the line-length for length-associated rules and automatic formatting
--max-complexity <MAX_COMPLEXITY>
Maximum McCabe complexity allowed for a given function
--add-noqa
Enable automatic additions of `noqa` directives to failing lines
--clean
@@ -581,6 +595,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI.
#### Error (E)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| E401 | MultipleImportsOnOneLine | Multiple imports on one line | |
@@ -598,6 +613,10 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | |
| E902 | IOError | IOError: `...` | |
| E999 | SyntaxError | SyntaxError: `...` | |
#### Warning (W)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| W292 | NoNewLineAtEndOfFile | No newline at end of file | 🛠 |
| W505 | DocLineTooLong | Doc line too long (89 > 88 characters) | |
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | 🛠 |
@@ -706,6 +725,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| UP028 | RewriteYieldFrom | Replace `yield` over `for` loop with `yield from` | 🛠 |
| UP029 | UnnecessaryBuiltinImport | Unnecessary builtin import: `...` | 🛠 |
| UP030 | FormatLiterals | Use implicit references for positional format fields | 🛠 |
| UP032 | FString | Use f-string instead of `format` call | 🛠 |
### pep8-naming (N)
@@ -836,7 +856,7 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged | |
| B027 | EmptyMethodWithoutAbstractDecorator | `...` is an empty method in an abstract base class, but has no abstract decorator | |
| B904 | RaiseWithoutFromInsideExcept | Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling | |
| B904 | RaiseWithoutFromInsideExcept | Within an except clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | |
| B905 | ZipWithoutExplicitStrict | `zip()` without an explicit `strict=` parameter | |
### flake8-builtins (A)
@@ -979,7 +999,6 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| SIM115 | OpenFileWithContextHandler | Use context handler for opening files | |
| SIM101 | DuplicateIsinstanceCall | Multiple `isinstance` calls for `...`, merge into a single call | 🛠 |
| SIM102 | NestedIfStatements | Use a single `if` statement instead of nested `if` statements | |
| SIM103 | ReturnBoolConditionDirectly | Return the condition `...` directly | 🛠 |
@@ -990,6 +1009,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
| SIM110 | ConvertLoopToAny | Use `return any(x for x in y)` instead of `for` loop | 🛠 |
| SIM111 | ConvertLoopToAll | Use `return all(x for x in y)` instead of `for` loop | 🛠 |
| SIM112 | UseCapitalEnvironmentVariables | Use capitalized environment variable `...` instead of `...` | 🛠 |
| SIM115 | OpenFileWithContextHandler | Use context handler for opening files | |
| SIM117 | MultipleWithStatements | Use a single `with` statement with multiple contexts instead of nested `with` statements | |
| SIM118 | KeyInDict | Use `key in dict` instead of `key in dict.keys()` | 🛠 |
| SIM201 | NegateEqualOp | Use `left != right` instead of `not left == right` | 🛠 |
@@ -1012,7 +1032,7 @@ For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TID251 | BannedApi | `...` is banned: ... | |
| TID252 | BannedRelativeImport | Relative imports are banned | |
| TID252 | RelativeImports | Relative imports are banned | |
### flake8-unused-arguments (ARG)
@@ -1084,20 +1104,33 @@ For more, see [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) on GitH
For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
#### Convention (PLC)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PLC0414 | UselessImportAlias | Import alias does not rename original package | 🛠 |
| PLC2201 | MisplacedComparisonConstant | Comparison should be ... | 🛠 |
| PLC3002 | UnnecessaryDirectLambdaCall | Lambda expression called directly. Execute the expression inline instead. | |
#### Error (PLE)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PLE0117 | NonlocalWithoutBinding | Nonlocal name `...` found without binding | |
| PLE0118 | UsedPriorGlobalDeclaration | Name `...` is used prior to global declaration on line 1 | |
| PLE1142 | AwaitOutsideAsync | `await` should be used within an async function | |
#### Refactor (PLR)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PLR0133 | ConstantComparison | Two constants compared in a comparison, consider replacing `0 == 0` | |
| PLR0206 | PropertyWithParameters | Cannot have defined parameters for properties | |
| PLR0402 | ConsiderUsingFromImport | Use `from ... import ...` in lieu of alias | |
| PLR0133 | ConstantComparison | Two constants compared in a comparison, consider replacing `0 == 0` | |
| PLR1701 | ConsiderMergingIsinstance | Merge these isinstance calls: `isinstance(..., (...))` | |
| PLR1722 | UseSysExit | Use `sys.exit()` instead of `exit` | 🛠 |
| PLR2004 | MagicValueComparison | Magic number used in comparison, consider replacing magic with a constant variable | |
| PLR2004 | MagicValueComparison | Magic value used in comparison, consider replacing magic with a constant variable | |
#### Warning (PLW)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PLW0120 | UselessElseOnLoop | Else clause on loop without a break statement, remove the else and de-indent all the code inside it | |
| PLW0602 | GlobalVariableNotAssigned | Using global for `...` but no assignment is done | |
@@ -1109,8 +1142,19 @@ For more, see [flake8-pie](https://pypi.org/project/flake8-pie/0.16.0/) on PyPI.
| ---- | ---- | ------- | --- |
| PIE790 | NoUnnecessaryPass | Unnecessary `pass` statement | 🛠 |
| PIE794 | DupeClassFieldDefinitions | Class field `...` is defined multiple times | 🛠 |
| PIE796 | PreferUniqueEnums | Enum contains duplicate value: `...` | |
| PIE807 | PreferListBuiltin | Prefer `list()` over useless lambda | 🛠 |
### flake8-commas (COM)
For more, see [flake8-commas](https://pypi.org/project/flake8-commas/2.1.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| COM812 | TrailingCommaMissing | Trailing comma missing | 🛠 |
| COM818 | TrailingCommaOnBareTupleProhibited | Trailing comma on bare tuple prohibited | |
| COM819 | TrailingCommaProhibited | Trailing comma prohibited | 🛠 |
### Ruff-specific rules (RUF)
| Code | Name | Message | Fix |
@@ -1395,6 +1439,7 @@ natively, including:
- [`flake8-boolean-trap`](https://pypi.org/project/flake8-boolean-trap/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-commas`](https://pypi.org/project/flake8-commas/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-datetimez`](https://pypi.org/project/flake8-datetimez/)
- [`flake8-debugger`](https://pypi.org/project/flake8-debugger/)
@@ -1412,6 +1457,7 @@ natively, including:
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/)
- [`isort`](https://pypi.org/project/isort/)
- [`mccabe`](https://pypi.org/project/mccabe/)
- [`pandas-vet`](https://pypi.org/project/pandas-vet/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) ([#980](https://github.com/charliermarsh/ruff/issues/980))
@@ -1460,6 +1506,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-boolean-trap`](https://pypi.org/project/flake8-boolean-trap/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-commas`](https://pypi.org/project/flake8-commas/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-datetimez`](https://pypi.org/project/flake8-datetimez/)
- [`flake8-debugger`](https://pypi.org/project/flake8-debugger/)
@@ -1476,6 +1523,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/)
- [`mccabe`](https://pypi.org/project/mccabe/)
- [`pandas-vet`](https://pypi.org/project/pandas-vet/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
@@ -1611,57 +1659,28 @@ which makes it a good target for benchmarking.
git clone --branch 3.10 https://github.com/python/cpython.git resources/test/cpython
```
Add this `pyproject.toml` to the CPython directory:
```toml
[tool.ruff]
line-length = 88
extend-exclude = [
"Lib/lib2to3/tests/data/bom.py",
"Lib/lib2to3/tests/data/crlf.py",
"Lib/lib2to3/tests/data/different_encoding.py",
"Lib/lib2to3/tests/data/false_encoding.py",
"Lib/lib2to3/tests/data/py2_test_grammar.py",
"Lib/test/bad_coding2.py",
"Lib/test/badsyntax_3131.py",
"Lib/test/badsyntax_pep3120.py",
"Lib/test/encoded_modules/module_iso_8859_1.py",
"Lib/test/encoded_modules/module_koi8_r.py",
"Lib/test/test_fstring.py",
"Lib/test/test_grammar.py",
"Lib/test/test_importlib/test_util.py",
"Lib/test/test_named_expressions.py",
"Lib/test/test_patma.py",
"Lib/test/test_source_encoding.py",
"Tools/c-analyzer/c_parser/parser/_delim.py",
"Tools/i18n/pygettext.py",
"Tools/test2to3/maintest.py",
"Tools/test2to3/setup.py",
"Tools/test2to3/test/test_foo.py",
"Tools/test2to3/test2to3/hello.py",
]
```
Next, to benchmark the release build:
To benchmark the release build:
```shell
cargo build --release
hyperfine --ignore-failure --warmup 10 --runs 100 \
cargo build --release && hyperfine --ignore-failure --warmup 10 \
"./target/release/ruff ./resources/test/cpython/ --no-cache" \
"./target/release/ruff ./resources/test/cpython/"
Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
Time (mean ± σ): 297.4 ms ± 4.9 ms [User: 2460.0 ms, System: 67.2 ms]
Range (min … max): 287.7 ms … 312.1 ms 100 runs
Time (mean ± σ): 293.8 ms ± 3.2 ms [User: 2384.6 ms, System: 90.3 ms]
Range (min … max): 289.9 ms … 301.6 ms 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: ./target/release/ruff ./resources/test/cpython/
Time (mean ± σ): 79.6 ms ± 7.3 ms [User: 59.7 ms, System: 356.1 ms]
Range (min … max): 62.4 ms … 111.2 ms 100 runs
Time (mean ± σ): 48.0 ms ± 3.1 ms [User: 65.2 ms, System: 124.7 ms]
Range (min … max): 45.0 ms … 66.7 ms 62 runs
Warning: Ignoring non-zero exit code.
Summary
'./target/release/ruff ./resources/test/cpython/' ran
6.12 ± 0.41 times faster than './target/release/ruff ./resources/test/cpython/ --no-cache'
```
To benchmark against the ecosystem's existing tools:
@@ -1669,73 +1688,89 @@ To benchmark against the ecosystem's existing tools:
```shell
hyperfine --ignore-failure --warmup 5 \
"./target/release/ruff ./resources/test/cpython/ --no-cache" \
"pylint --recursive=y resources/test/cpython/" \
"pyflakes resources/test/cpython" \
"autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython" \
"pycodestyle resources/test/cpython" \
"flake8 resources/test/cpython" \
"python -m scripts.run_flake8 resources/test/cpython"
```
"flake8 resources/test/cpython"
In order, these evaluate:
- Ruff
- Pylint
- Pyflakes
- autoflake
- pycodestyle
- Flake8
- Flake8, with a hack to enable multiprocessing on macOS
(You can `poetry install` from `./scripts` to create a working environment for the above.)
```shell
Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
Time (mean ± σ): 297.9 ms ± 7.0 ms [User: 2436.6 ms, System: 65.9 ms]
Range (min … max): 289.9 ms … 314.6 ms 10 runs
Time (mean ± σ): 294.3 ms ± 3.3 ms [User: 2467.5 ms, System: 89.6 ms]
Range (min … max): 291.1 ms … 302.8 ms 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: pylint --recursive=y resources/test/cpython/
Time (mean ± σ): 37.634 s ± 0.225 s [User: 36.728 s, System: 0.853 s]
Range (min … max): 37.201 s … 38.106 s 10 runs
Benchmark 2: pyflakes resources/test/cpython
Time (mean ± σ): 15.786 s ± 0.143 s [User: 15.560 s, System: 0.214 s]
Range (min … max): 15.640 s … 16.157 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 3: pyflakes resources/test/cpython
Time (mean ± σ): 40.950 s ± 0.449 s [User: 40.688 s, System: 0.229 s]
Range (min … max): 40.348 s … 41.671 s 10 runs
Benchmark 3: autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython
Time (mean ± σ): 6.175 s ± 0.169 s [User: 54.102 s, System: 1.057 s]
Range (min … max): 5.950 s … 6.391 s 10 runs
Benchmark 4: pycodestyle resources/test/cpython
Time (mean ± σ): 46.921 s ± 0.508 s [User: 46.699 s, System: 0.202 s]
Range (min … max): 46.171 s … 47.863 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 4: autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython
Time (mean ± σ): 11.562 s ± 0.160 s [User: 107.022 s, System: 1.143 s]
Range (min … max): 11.417 s … 11.917 s 10 runs
Benchmark 5: pycodestyle resources/test/cpython
Time (mean ± σ): 67.428 s ± 0.985 s [User: 67.199 s, System: 0.203 s]
Range (min … max): 65.313 s … 68.496 s 10 runs
Benchmark 5: flake8 resources/test/cpython
Time (mean ± σ): 12.260 s ± 0.321 s [User: 102.934 s, System: 1.230 s]
Range (min … max): 11.848 s … 12.933 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 6: flake8 resources/test/cpython
Time (mean ± σ): 116.099 s ± 1.178 s [User: 115.217 s, System: 0.845 s]
Range (min … max): 114.180 s … 117.724 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 7: python -m scripts.run_flake8 resources/test/cpython
Time (mean ± σ): 20.477 s ± 0.349 s [User: 142.372 s, System: 1.504 s]
Range (min … max): 20.107 s … 21.183 s 10 runs
Summary
'./target/release/ruff ./resources/test/cpython/ --no-cache' ran
38.81 ± 1.05 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
68.74 ± 1.99 times faster than 'python -m scripts.run_flake8 resources/test/cpython'
126.33 ± 3.05 times faster than 'pylint --recursive=y resources/test/cpython/'
137.46 ± 3.55 times faster than 'pyflakes resources/test/cpython'
226.35 ± 6.23 times faster than 'pycodestyle resources/test/cpython'
389.73 ± 9.92 times faster than 'flake8 resources/test/cpython'
20.98 ± 0.62 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
41.66 ± 1.18 times faster than 'flake8 resources/test/cpython'
53.64 ± 0.77 times faster than 'pyflakes resources/test/cpython'
159.43 ± 2.48 times faster than 'pycodestyle resources/test/cpython'
```
You can run `poetry install` from `./scripts` to create a working environment for the above. All
reported benchmarks were computed using the versions specified by `./scripts/pyproject.toml`
on Python 3.11.
To benchmark Pylint, remove the following files from the CPython repository:
```shell
rm Lib/test/bad_coding.py \
Lib/test/bad_coding2.py \
Lib/test/bad_getattr.py \
Lib/test/bad_getattr2.py \
Lib/test/bad_getattr3.py \
Lib/test/badcert.pem \
Lib/test/badkey.pem \
Lib/test/badsyntax_3131.py \
Lib/test/badsyntax_future10.py \
Lib/test/badsyntax_future3.py \
Lib/test/badsyntax_future4.py \
Lib/test/badsyntax_future5.py \
Lib/test/badsyntax_future6.py \
Lib/test/badsyntax_future7.py \
Lib/test/badsyntax_future8.py \
Lib/test/badsyntax_future9.py \
Lib/test/badsyntax_pep3120.py \
Lib/test/test_asyncio/test_runners.py \
Lib/test/test_copy.py \
Lib/test/test_inspect.py \
Lib/test/test_typing.py
```
Then, from `resources/test/cpython`, run: `time pylint -j 0 -E $(git ls-files '*.py')`. This
will execute Pylint with maximum parallelism and only report errors.
To benchmark Pyupgrade, run the following from `resources/test/cpython`:
```shell
hyperfine --ignore-failure --warmup 5 --prepare "git reset --hard HEAD" \
"find . -type f -name \"*.py\" | xargs -P 0 pyupgrade --py311-plus"
Benchmark 1: find . -type f -name "*.py" | xargs -P 0 pyupgrade --py311-plus
Time (mean ± σ): 30.119 s ± 0.195 s [User: 28.638 s, System: 0.390 s]
Range (min … max): 29.813 s … 30.356 s 10 runs
```
## Reference
@@ -2154,6 +2189,25 @@ line-length = 120
---
#### [`namespace-packages`](#namespace-packages)
Mark the specified directories as namespace packages. For the purpose of
module resolution, Ruff will treat those directories as if they
contained an `__init__.py` file.
**Default value**: `[]`
**Type**: `Vec<PathBuf>`
**Example usage**:
```toml
[tool.ruff]
namespace-packages = ["airflow/providers"]
```
---
#### [`per-file-ignores`](#per-file-ignores)
A list of mappings from file pattern to rule codes or prefixes to

View File

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

View File

@@ -1,12 +1,8 @@
[package]
name = "flake8-to-ruff"
version = "0.0.221-dev.0"
version = "0.0.224"
edition = "2021"
[lib]
name = "flake8_to_ruff"
doctest = false
[dependencies]
anyhow = { version = "1.0.66" }
clap = { version = "4.0.1", features = ["derive"] }

View File

@@ -1,18 +0,0 @@
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
clippy::implicit_hasher,
clippy::match_same_arms,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::similar_names,
clippy::too_many_lines
)]
#![forbid(unsafe_code)]
pub mod black;
pub mod converter;
mod parser;
pub mod plugin;

View File

@@ -1,4 +1,4 @@
//! Utility to generate Ruff's pyproject.toml section from a Flake8 INI file.
//! Utility to generate Ruff's `pyproject.toml` section from a Flake8 INI file.
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
@@ -18,9 +18,7 @@ use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use configparser::ini::Ini;
use flake8_to_ruff::black::parse_black_options;
use flake8_to_ruff::converter;
use flake8_to_ruff::plugin::Plugin;
use ruff::flake8_to_ruff;
#[derive(Parser)]
#[command(
@@ -38,7 +36,7 @@ struct Cli {
pyproject: Option<PathBuf>,
/// List of plugins to enable.
#[arg(long, value_delimiter = ',')]
plugin: Option<Vec<Plugin>>,
plugin: Option<Vec<flake8_to_ruff::Plugin>>,
}
fn main() -> Result<()> {
@@ -52,12 +50,12 @@ fn main() -> Result<()> {
// Read the pyproject.toml file.
let black = cli
.pyproject
.map(parse_black_options)
.map(flake8_to_ruff::parse_black_options)
.transpose()?
.flatten();
// Create Ruff's pyproject.toml section.
let pyproject = converter::convert(&config, black.as_ref(), cli.plugin)?;
let pyproject = flake8_to_ruff::convert(&config, black.as_ref(), cli.plugin)?;
println!("{}", toml_edit::easy::to_string_pretty(&pyproject)?);
Ok(())

View File

@@ -0,0 +1,45 @@
The MIT License (MIT)
Copyright (c) 2017 Thomas Grainger.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Portions of this flake8-commas Software may utilize the following
copyrighted material, the use of which is hereby acknowledged.
Original flake8-commas: https://github.com/trevorcreech/flake8-commas/commit/e8563b71b1d5442e102c8734c11cb5202284293d
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

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

View File

@@ -0,0 +1,628 @@
# ==> bad_function_call.py <==
bad_function_call(
param1='test',
param2='test'
)
# ==> bad_list.py <==
bad_list = [
1,
2,
3
]
bad_list_with_comment = [
1,
2,
3
# still needs a comma!
]
bad_list_with_extra_empty = [
1,
2,
3
]
# ==> bare.py <==
bar = 1, 2
foo = 1
foo = (1,)
foo = 1,
bar = 1; foo = bar,
foo = (
3,
4,
)
foo = 3,
class A(object):
foo = 3
bar = 10,
foo_bar = 2
a = ('a',)
from foo import bar, baz
group_by = function_call('arg'),
group_by = ('foobar' * 3),
def foo():
return False,
==> callable_before_parenth_form.py <==
def foo(
bar,
):
pass
{'foo': foo}['foo'](
bar
)
{'foo': foo}['foo'](
bar,
)
(foo)(
bar
)
(foo)[0](
bar,
)
[foo][0](
bar
)
[foo][0](
bar,
)
# ==> comment_good_dict.py <==
multiline_good_dict = {
"good": 123, # this is a good number
}
# ==> dict_comprehension.py <==
not_a_dict = {
x: y
for x, y in ((1, 2), (3, 4))
}
# ==> good_empty_comma_context.py <==
def func2(
):
pass
func2(
)
func2(
)
[
]
[
]
(
)
(
)
{
}
# ==> good_list.py <==
stuff = [
'a',
'b',
# more stuff will go here
]
more_stuff = [
'a',
'b',
]
# ==> keyword_before_parenth_form/base_bad.py <==
from x import (
y
)
assert(
SyntaxWarning,
ThrownHere,
Anyway
)
# async await is fine outside an async def
# ruff: RustPython tokenizer treats async/await as keywords, not applicable.
# def await(
# foo
# ):
# async(
# foo
# )
# def async(
# foo
# ):
# await(
# foo
# )
# ==> keyword_before_parenth_form/base.py <==
from x import (
y,
)
assert(
SyntaxWarning,
ThrownHere,
Anyway,
)
assert (
foo
)
assert (
foo and
bar
)
if(
foo and
bar
):
pass
elif(
foo and
bar
):
pass
for x in(
[1,2,3]
):
print(x)
(x for x in (
[1, 2, 3]
))
(
'foo'
) is (
'foo'
)
if (
foo and
bar
) or not (
foo
) or (
spam
):
pass
def xyz():
raise(
Exception()
)
def abc():
return(
3
)
while(
False
):
pass
with(
loop
):
pass
def foo():
yield (
"foo"
)
# async await is fine outside an async def
# ruff: RustPython tokenizer treats async/await as keywords, not applicable.
# def await(
# foo,
# ):
# async(
# foo,
# )
# def async(
# foo,
# ):
# await(
# foo,
# )
# ==> keyword_before_parenth_form/py3.py <==
# Syntax error in Py2
def foo():
yield from (
foo
)
# ==> list_comprehension.py <==
not_a_list = [
s.strip()
for s in 'foo, bar, baz'.split(',')
]
# ==> multiline_bad_dict.py <==
multiline_bad_dict = {
"bad": 123
}
# ==> multiline_bad_function_def.py <==
def func_good(
a = 3,
b = 2):
pass
def func_bad(
a = 3,
b = 2
):
pass
# ==> multiline_bad_function_one_param.py <==
def func(
a = 3
):
pass
func(
a = 3
)
# ==> multiline_bad_or_dict.py <==
multiline_bad_or_dict = {
"good": True or False,
"bad": 123
}
# ==> multiline_good_dict.py <==
multiline_good_dict = {
"good": 123,
}
# ==> multiline_good_single_keyed_for_dict.py <==
good_dict = {
"good": x for x in y
}
# ==> multiline_if.py <==
if (
foo
and bar
):
print("Baz")
# ==> multiline_index_access.py <==
multiline_index_access[
"good"
]
multiline_index_access_after_function()[
"good"
]
multiline_index_access_after_inline_index_access['first'][
"good"
]
multiline_index_access[
"probably fine",
]
[0, 1, 2][
"good"
]
[0, 1, 2][
"probably fine",
]
multiline_index_access[
"probably fine",
"not good"
]
multiline_index_access[
"fine",
"fine",
:
"not good"
]
# ==> multiline_string.py <==
s = (
'this' +
'is a string'
)
s2 = (
'this'
'is a also a string'
)
t = (
'this' +
'is a tuple',
)
t2 = (
'this'
'is also a tuple',
)
# ==> multiline_subscript_slice.py <==
multiline_index_access[
"fine",
"fine"
:
"not fine"
]
multiline_index_access[
"fine"
"fine"
:
"fine"
:
"fine"
]
multiline_index_access[
"fine"
"fine",
:
"fine",
:
"fine",
]
multiline_index_access[
"fine"
"fine",
:
"fine"
:
"fine",
"not fine"
]
multiline_index_access[
"fine"
"fine",
:
"fine",
"fine"
:
"fine",
]
multiline_index_access[
lambda fine,
fine,
fine: (0,)
:
lambda fine,
fine,
fine: (0,),
"fine"
:
"fine",
]
# ==> one_line_dict.py <==
one_line_dict = {"good": 123}
# ==> parenth_form.py <==
parenth_form = (
a +
b +
c
)
parenth_form_with_lambda = (
lambda x, y: 0
)
parenth_form_with_default_lambda = (
lambda x=(
lambda
x,
y,
:
0
),
y = {a: b},
:
0
)
# ==> prohibited.py <==
foo = ['a', 'b', 'c',]
bar = { a: b,}
def bah(ham, spam,):
pass
(0,)
(0, 1,)
foo = ['a', 'b', 'c', ]
bar = { a: b, }
def bah(ham, spam, ):
pass
(0, )
(0, 1, )
image[:, :, 0]
image[:,]
image[:,:,]
lambda x, :
# ==> unpack.py <==
def function(
foo,
bar,
**kwargs
):
pass
def function(
foo,
bar,
*args
):
pass
def function(
foo,
bar,
*args,
extra_kwarg
):
pass
result = function(
foo,
bar,
**kwargs
)
result = function(
foo,
bar,
**not_called_kwargs
)
def foo(
ham,
spam,
*args,
kwarg_only
):
pass
# In python 3.5 if it's not a function def, commas are mandatory.
foo(
**kwargs
)
{
**kwargs
}
(
*args
)
{
*args
}
[
*args
]
def foo(
ham,
spam,
*args
):
pass
def foo(
ham,
spam,
**kwargs
):
pass
def foo(
ham,
spam,
*args,
kwarg_only
):
pass
# In python 3.5 if it's not a function def, commas are mandatory.
foo(
**kwargs,
)
{
**kwargs,
}
(
*args,
)
{
*args,
}
[
*args,
]
result = function(
foo,
bar,
**{'ham': spam}
)

View File

@@ -0,0 +1,60 @@
import enum
from enum import Enum, unique
class FakeEnum(enum.Enum):
A = "A"
B = "B"
C = "B" # PIE796
class FakeEnum2(Enum):
A = 1
B = 2
C = 2 # PIE796
class FakeEnum3(str, Enum):
A = "1"
B = "2"
C = "2" # PIE796
class FakeEnum4(Enum):
A = 1.0
B = 2.5
C = 2.5 # PIE796
class FakeEnum5(Enum):
A = 1.0
B = True
C = False
D = False # PIE796
class FakeEnum6(Enum):
A = 1
B = 2
C = None
D = None # PIE796
@enum.unique
class FakeEnum7(enum.Enum):
A = "A"
B = "B"
C = "C"
@unique
class FakeEnum8(Enum):
A = 1
B = 2
C = 2 # PIE796
class FakeEnum9(enum.Enum):
A = "A"
B = "B"
C = "C"

View File

@@ -2,6 +2,11 @@ with A() as a: # SIM117
with B() as b:
print("hello")
with A(): # SIM117
with B():
with C():
print("hello")
with A() as a:
a()
with B() as b:
@@ -11,3 +16,15 @@ with A() as a:
with B() as b:
print("hello")
a()
async with A() as a:
with B() as b:
print("hello")
with A() as a:
async with B() as b:
print("hello")
async with A() as a:
async with B() as b:
print("hello")

View File

@@ -0,0 +1,17 @@
class PropertyWithSetter:
@property
def foo(self) -> str:
"""Docstring for foo."""
return "foo"
@foo.setter
def foo(self, value: str) -> None:
pass
@foo.deleter
def foo(self):
pass
@foo
def foo(self, value: str) -> None:
pass

View File

@@ -44,3 +44,25 @@ def f():
1 / 0
except (ValueError, ZeroDivisionError) as x2:
pass
def f(a, b):
x = (
a()
if a is not None
else b
)
y = \
a() if a is not None else b
def f(a, b):
x = (
a
if a is not None
else b
)
y = \
a if a is not None else b

View File

@@ -0,0 +1,86 @@
###
# Errors
###
"{} {}".format(a, b)
"{1} {0}".format(a, b)
"{x.y}".format(x=z)
"{.x} {.y}".format(a, b)
"{} {}".format(a.b, c.d)
"{}".format(a())
"{}".format(a.b())
"{}".format(a.b().c())
"hello {}!".format(name)
"{}{b}{}".format(a, c, b=b)
"{}".format(0x0)
"{} {}".format(a, b)
"""{} {}""".format(a, b)
"foo{}".format(1)
r"foo{}".format(1)
x = "{a}".format(a=1)
print("foo {} ".format(x))
"{a[b]}".format(a=a)
"{a.a[b]}".format(a=a)
"{}{{}}{}".format(escaped, y)
"{}".format(a)
###
# Non-errors
###
# False-negative: RustPython doesn't parse the `\N{snowman}`.
"\N{snowman} {}".format(a)
"{".format(a)
"}".format(a)
"{} {}".format(*a)
"{0} {0}".format(arg)
"{x} {x}".format(arg)
"{x.y} {x.z}".format(arg)
b"{} {}".format(a, b)
"{:{}}".format(x, y)
"{}{}".format(a)
"" "{}".format(a["\\"])
"{}".format(a["b"])
r'"\N{snowman} {}".format(a)'
"{a}" "{b}".format(a=1, b=1)
async def c():
return "{}".format(await 3)
async def c():
return "{}".format(1 + await 3)

View File

@@ -285,6 +285,16 @@
}
]
},
"namespace-packages": {
"description": "Mark the specified directories as namespace packages. For the purpose of module resolution, Ruff will treat those directories as if they contained an `__init__.py` file.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"pep8-naming": {
"description": "Options for the `pep8-naming` plugin.",
"anyOf": [
@@ -438,7 +448,7 @@
},
"additionalProperties": false,
"definitions": {
"BannedApi": {
"ApiBan": {
"type": "object",
"required": [
"msg"
@@ -733,7 +743,7 @@
"null"
],
"additionalProperties": {
"$ref": "#/definitions/BannedApi"
"$ref": "#/definitions/ApiBan"
}
}
},
@@ -1146,6 +1156,12 @@
"C9",
"C90",
"C901",
"COM",
"COM8",
"COM81",
"COM812",
"COM818",
"COM819",
"D",
"D1",
"D10",
@@ -1415,6 +1431,7 @@
"PIE79",
"PIE790",
"PIE794",
"PIE796",
"PIE8",
"PIE80",
"PIE807",
@@ -1670,6 +1687,7 @@
"UP029",
"UP03",
"UP030",
"UP032",
"W",
"W2",
"W29",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.221"
version = "0.0.224"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -17,6 +17,12 @@ name = "ruff"
path = "src/main.rs"
doctest = false
# Since the name of the binary is the same as the name of the `ruff` crate
# running `cargo doc --no-deps --all` results in an `output filename collision`
# See also https://github.com/rust-lang/cargo/issues/6313.
# We therefore disable the documentation generation for the binary.
doc = false
[dependencies]
ruff = { path = ".." }

View File

@@ -9,7 +9,7 @@ use filetime::FileTime;
use log::error;
use path_absolutize::Absolutize;
use ruff::message::Message;
use ruff::settings::{flags, Settings};
use ruff::settings::{flags, AllSettings, Settings};
use serde::{Deserialize, Serialize};
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -80,10 +80,14 @@ fn read_sync(cache_dir: &Path, key: u64) -> Result<Vec<u8>, std::io::Error> {
pub fn get<P: AsRef<Path>>(
path: P,
metadata: &fs::Metadata,
settings: &Settings,
settings: &AllSettings,
autofix: flags::Autofix,
) -> Option<Vec<Message>> {
let encoded = read_sync(&settings.cache_dir, cache_key(path, settings, autofix)).ok()?;
let encoded = read_sync(
&settings.cli.cache_dir,
cache_key(path, &settings.lib, autofix),
)
.ok()?;
let (mtime, messages) = match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
metadata: CacheMetadata { mtime },
@@ -104,7 +108,7 @@ pub fn get<P: AsRef<Path>>(
pub fn set<P: AsRef<Path>>(
path: P,
metadata: &fs::Metadata,
settings: &Settings,
settings: &AllSettings,
autofix: flags::Autofix,
messages: &[Message],
) {
@@ -115,8 +119,8 @@ pub fn set<P: AsRef<Path>>(
messages,
};
if let Err(e) = write_sync(
&settings.cache_dir,
cache_key(path, settings, autofix),
&settings.cli.cache_dir,
cache_key(path, &settings.lib, autofix),
&bincode::serialize(&check_result).unwrap(),
) {
error!("Failed to write to cache: {e:?}");

View File

@@ -2,13 +2,13 @@ use std::path::{Path, PathBuf};
use clap::{command, Parser};
use regex::Regex;
use ruff::fs;
use ruff::logging::LogLevel;
use ruff::registry::{RuleCode, RuleCodePrefix};
use ruff::resolver::ConfigProcessor;
use ruff::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat,
};
use ruff::{fs, mccabe};
use rustc_hash::FxHashMap;
#[derive(Debug, Parser)]
@@ -137,9 +137,6 @@ pub struct Cli {
/// formatting.
#[arg(long)]
pub line_length: Option<usize>,
/// Maximum McCabe complexity allowed for a given function.
#[arg(long)]
pub max_complexity: Option<usize>,
/// Enable automatic additions of `noqa` directives to failing lines.
#[arg(
long,
@@ -266,7 +263,6 @@ impl Cli {
fixable: self.fixable,
ignore: self.ignore,
line_length: self.line_length,
max_complexity: self.max_complexity,
per_file_ignores: self.per_file_ignores,
respect_gitignore: resolve_bool_arg(
self.respect_gitignore,
@@ -332,7 +328,6 @@ pub struct Overrides {
pub fixable: Option<Vec<RuleCodePrefix>>,
pub ignore: Option<Vec<RuleCodePrefix>>,
pub line_length: Option<usize>,
pub max_complexity: Option<usize>,
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
pub respect_gitignore: Option<bool>,
pub select: Option<Vec<RuleCodePrefix>>,
@@ -383,11 +378,6 @@ impl ConfigProcessor for &Overrides {
if let Some(line_length) = &self.line_length {
config.line_length = Some(*line_length);
}
if let Some(max_complexity) = &self.max_complexity {
config.mccabe = Some(mccabe::settings::Options {
max_complexity: Some(*max_complexity),
});
}
if let Some(per_file_ignores) = &self.per_file_ignores {
config.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone()));
}

View File

@@ -19,7 +19,7 @@ use ruff::registry::RuleCode;
use ruff::resolver::{FileDiscovery, PyprojectDiscovery};
use ruff::settings::flags;
use ruff::settings::types::SerializationFormat;
use ruff::{fix, fs, packaging, resolver, violations, warn_user_once};
use ruff::{fix, fs, packaging, resolver, warn_user_once, IOError};
use serde::Serialize;
use walkdir::WalkDir;
@@ -56,19 +56,19 @@ pub fn run(
if matches!(cache, flags::Cache::Enabled) {
match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => {
if let Err(e) = cache::init(&settings.cache_dir) {
if let Err(e) = cache::init(&settings.cli.cache_dir) {
error!(
"Failed to initialize cache at {}: {e:?}",
settings.cache_dir.to_string_lossy()
settings.cli.cache_dir.to_string_lossy()
);
}
}
PyprojectDiscovery::Hierarchical(default) => {
for settings in std::iter::once(default).chain(resolver.iter()) {
if let Err(e) = cache::init(&settings.cache_dir) {
if let Err(e) = cache::init(&settings.cli.cache_dir) {
error!(
"Failed to initialize cache at {}: {e:?}",
settings.cache_dir.to_string_lossy()
settings.cli.cache_dir.to_string_lossy()
);
}
}
@@ -83,6 +83,8 @@ pub fn run(
.flatten()
.map(ignore::DirEntry::path)
.collect::<Vec<_>>(),
&resolver,
pyproject_strategy,
);
let start = Instant::now();
@@ -95,7 +97,7 @@ pub fn run(
.parent()
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path, pyproject_strategy);
let settings = resolver.resolve_all(path, pyproject_strategy);
lint_path(path, package, settings, cache, autofix)
.map_err(|e| (Some(path.to_owned()), e.to_string()))
}
@@ -114,7 +116,7 @@ pub fn run(
let settings = resolver.resolve(path, pyproject_strategy);
if settings.enabled.contains(&RuleCode::E902) {
Diagnostics::new(vec![Message {
kind: violations::IOError(message).into(),
kind: IOError(message).into(),
location: Location::default(),
end_location: Location::default(),
fix: None,
@@ -169,9 +171,9 @@ pub fn run_stdin(
};
let package_root = filename
.and_then(Path::parent)
.and_then(packaging::detect_package_root);
.and_then(|path| packaging::detect_package_root(path, &settings.lib.namespace_packages));
let stdin = read_from_stdin()?;
let mut diagnostics = lint_stdin(filename, package_root, &stdin, settings, autofix)?;
let mut diagnostics = lint_stdin(filename, package_root, &stdin, &settings.lib, autofix)?;
diagnostics.messages.sort_unstable();
Ok(diagnostics)
}

View File

@@ -9,7 +9,7 @@ use anyhow::Result;
use log::debug;
use ruff::linter::{lint_fix, lint_only};
use ruff::message::Message;
use ruff::settings::{flags, Settings};
use ruff::settings::{flags, AllSettings, Settings};
use ruff::{fix, fs};
use similar::TextDiff;
@@ -38,12 +38,12 @@ impl AddAssign for Diagnostics {
pub fn lint_path(
path: &Path,
package: Option<&Path>,
settings: &Settings,
settings: &AllSettings,
cache: flags::Cache,
autofix: fix::FixMode,
) -> Result<Diagnostics> {
// Validate the `Settings` and return any errors.
settings.validate()?;
settings.lib.validate()?;
// Check the cache.
// TODO(charlie): `fixer::Mode::Apply` and `fixer::Mode::Diff` both have
@@ -69,7 +69,7 @@ pub fn lint_path(
// Lint the file.
let (messages, fixed) = if matches!(autofix, fix::FixMode::Apply | fix::FixMode::Diff) {
let (transformed, fixed, messages) = lint_fix(&contents, path, package, settings)?;
let (transformed, fixed, messages) = lint_fix(&contents, path, package, &settings.lib)?;
if fixed > 0 {
if matches!(autofix, fix::FixMode::Apply) {
write(path, transformed)?;
@@ -85,7 +85,7 @@ pub fn lint_path(
}
(messages, fixed)
} else {
let messages = lint_only(&contents, path, package, settings, autofix.into())?;
let messages = lint_only(&contents, path, package, &settings.lib, autofix.into())?;
let fixed = 0;
(messages, fixed)
};

View File

@@ -1,6 +1,14 @@
//! This library only exists to enable the Ruff internal tooling (`ruff_dev`)
//! to automatically update the `ruff --help` output in the `README.md`.
//!
//! For the actual Ruff library, see [`ruff`].
#![allow(clippy::must_use_candidate, dead_code)]
mod cli;
// used by ruff_dev::generate_cli_help
pub use cli::Cli;
use clap::CommandFactory;
/// Returns the output of `ruff --help`.
pub fn help() -> String {
cli::Cli::command().render_help().to_string()
}

View File

@@ -16,8 +16,8 @@ use ::ruff::resolver::{
resolve_settings_with_processor, ConfigProcessor, FileDiscovery, PyprojectDiscovery, Relativity,
};
use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::pyproject;
use ::ruff::settings::types::SerializationFormat;
use ::ruff::settings::{pyproject, Settings};
use ::ruff::{fix, fs, warn_user_once};
use anyhow::Result;
use clap::{CommandFactory, Parser};
@@ -26,6 +26,7 @@ use colored::Colorize;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use path_absolutize::path_dedot;
use printer::{Printer, Violations};
use ruff::settings::{AllSettings, CliSettings};
mod cache;
mod cli;
@@ -48,7 +49,7 @@ fn resolve(
// First priority: if we're running in isolated mode, use the default settings.
let mut config = Configuration::default();
overrides.process_config(&mut config);
let settings = Settings::from_configuration(config, &path_dedot::CWD)?;
let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Fixed(settings))
} else if let Some(pyproject) = config {
// Second priority: the user specified a `pyproject.toml` file. Use that
@@ -82,7 +83,7 @@ fn resolve(
// as the "default" settings.)
let mut config = Configuration::default();
overrides.process_config(&mut config);
let settings = Settings::from_configuration(config, &path_dedot::CWD)?;
let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
}
}
@@ -113,35 +114,31 @@ pub fn main() -> Result<ExitCode> {
// Validate the `Settings` and return any errors.
match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.validate()?,
PyprojectDiscovery::Hierarchical(settings) => settings.validate()?,
PyprojectDiscovery::Fixed(settings) => settings.lib.validate()?,
PyprojectDiscovery::Hierarchical(settings) => settings.lib.validate()?,
};
// Extract options that are included in `Settings`, but only apply at the top
// level.
let file_strategy = FileDiscovery {
force_exclude: match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.force_exclude,
PyprojectDiscovery::Hierarchical(settings) => settings.force_exclude,
PyprojectDiscovery::Fixed(settings) => settings.lib.force_exclude,
PyprojectDiscovery::Hierarchical(settings) => settings.lib.force_exclude,
},
respect_gitignore: match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.respect_gitignore,
PyprojectDiscovery::Hierarchical(settings) => settings.respect_gitignore,
PyprojectDiscovery::Fixed(settings) => settings.lib.respect_gitignore,
PyprojectDiscovery::Hierarchical(settings) => settings.lib.respect_gitignore,
},
};
let (fix, fix_only, format, update_check) = match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => (
settings.fix,
settings.fix_only,
settings.format,
settings.update_check,
),
PyprojectDiscovery::Hierarchical(settings) => (
settings.fix,
settings.fix_only,
settings.format,
settings.update_check,
),
let CliSettings {
fix,
fix_only,
format,
update_check,
..
} = match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.cli.clone(),
PyprojectDiscovery::Hierarchical(settings) => settings.cli.clone(),
};
if let Some(code) = cli.explain {
@@ -200,7 +197,7 @@ pub fn main() -> Result<ExitCode> {
}
// Perform an initial run instantly.
printer.clear_screen()?;
Printer::clear_screen()?;
printer.write_to_user("Starting linter in watch mode...\n");
let messages = commands::run(
@@ -211,7 +208,7 @@ pub fn main() -> Result<ExitCode> {
cache.into(),
fix::FixMode::None,
)?;
printer.write_continuously(&messages);
printer.write_continuously(&messages)?;
// Configure the file watcher.
let (tx, rx) = channel();
@@ -230,7 +227,7 @@ pub fn main() -> Result<ExitCode> {
.unwrap_or_default()
});
if py_changed {
printer.clear_screen()?;
Printer::clear_screen()?;
printer.write_to_user("File change detected...\n");
let messages = commands::run(
@@ -241,7 +238,7 @@ pub fn main() -> Result<ExitCode> {
cache.into(),
fix::FixMode::None,
)?;
printer.write_continuously(&messages);
printer.write_continuously(&messages)?;
}
}
Err(err) => return Err(err.into()),

View File

@@ -1,4 +1,6 @@
use std::collections::BTreeMap;
use std::io;
use std::io::{BufWriter, Write};
use std::path::Path;
use annotate_snippets::display_list::{DisplayList, FormatOptions};
@@ -69,7 +71,7 @@ impl<'a> Printer<'a> {
}
}
fn post_text(&self, diagnostics: &Diagnostics) {
fn post_text<T: Write>(&self, stdout: &mut T, diagnostics: &Diagnostics) -> Result<()> {
if self.log_level >= &LogLevel::Default {
match self.violations {
Violations::Show => {
@@ -77,9 +79,12 @@ impl<'a> Printer<'a> {
let remaining = diagnostics.messages.len();
let total = fixed + remaining;
if fixed > 0 {
println!("Found {total} error(s) ({fixed} fixed, {remaining} remaining).");
writeln!(
stdout,
"Found {total} error(s) ({fixed} fixed, {remaining} remaining)."
)?;
} else if remaining > 0 {
println!("Found {remaining} error(s).");
writeln!(stdout, "Found {remaining} error(s).")?;
}
if !matches!(self.autofix, fix::FixMode::Apply) {
@@ -89,7 +94,10 @@ impl<'a> Printer<'a> {
.filter(|message| message.kind.fixable())
.count();
if num_fixable > 0 {
println!("{num_fixable} potentially fixable with the --fix option.");
writeln!(
stdout,
"{num_fixable} potentially fixable with the --fix option."
)?;
}
}
}
@@ -97,14 +105,15 @@ impl<'a> Printer<'a> {
let fixed = diagnostics.fixed;
if fixed > 0 {
if matches!(self.autofix, fix::FixMode::Apply) {
println!("Fixed {fixed} error(s).");
writeln!(stdout, "Fixed {fixed} error(s).")?;
} else if matches!(self.autofix, fix::FixMode::Diff) {
println!("Would fix {fixed} error(s).");
writeln!(stdout, "Would fix {fixed} error(s).")?;
}
}
}
}
}
Ok(())
}
pub fn write_once(&self, diagnostics: &Diagnostics) -> Result<()> {
@@ -113,18 +122,21 @@ impl<'a> Printer<'a> {
}
if matches!(self.violations, Violations::Hide) {
let mut stdout = BufWriter::new(io::stdout().lock());
if matches!(
self.format,
SerializationFormat::Text | SerializationFormat::Grouped
) {
self.post_text(diagnostics);
self.post_text(&mut stdout, diagnostics)?;
}
return Ok(());
}
let mut stdout = BufWriter::new(io::stdout().lock());
match self.format {
SerializationFormat::Json => {
println!(
writeln!(
stdout,
"{}",
serde_json::to_string_pretty(
&diagnostics
@@ -145,7 +157,7 @@ impl<'a> Printer<'a> {
})
.collect::<Vec<_>>()
)?
);
)?;
}
SerializationFormat::Junit => {
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
@@ -180,14 +192,14 @@ impl<'a> Printer<'a> {
}
report.add_test_suite(test_suite);
}
println!("{}", report.to_string().unwrap());
writeln!(stdout, "{}", report.to_string().unwrap())?;
}
SerializationFormat::Text => {
for message in &diagnostics.messages {
print_message(message);
print_message(&mut stdout, message)?;
}
self.post_text(diagnostics);
self.post_text(&mut stdout, diagnostics)?;
}
SerializationFormat::Grouped => {
for (filename, messages) in group_messages_by_filename(&diagnostics.messages) {
@@ -209,21 +221,25 @@ impl<'a> Printer<'a> {
);
// Print the filename.
println!("{}:", relativize_path(Path::new(&filename)).underline());
writeln!(
stdout,
"{}:",
relativize_path(Path::new(&filename)).underline()
)?;
// Print each message.
for message in messages {
print_grouped_message(message, row_length, column_length);
print_grouped_message(&mut stdout, message, row_length, column_length)?;
}
println!();
writeln!(stdout)?;
}
self.post_text(diagnostics);
self.post_text(&mut stdout, diagnostics)?;
}
SerializationFormat::Github => {
// Generate error workflow command in GitHub Actions format.
// See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
diagnostics.messages.iter().for_each(|message| {
for message in &diagnostics.messages {
let label = format!(
"{}{}{}{}{}{} {} {}",
relativize_path(Path::new(&message.filename)),
@@ -235,7 +251,8 @@ impl<'a> Printer<'a> {
message.kind.code().as_ref(),
message.kind.body(),
);
println!(
writeln!(
stdout,
"::error title=Ruff \
({}),file={},line={},col={},endLine={},endColumn={}::{}",
message.kind.code(),
@@ -245,13 +262,13 @@ impl<'a> Printer<'a> {
message.end_location.row(),
message.end_location.column(),
label,
);
});
)?;
}
}
SerializationFormat::Gitlab => {
// Generate JSON with errors in GitLab CI format
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implementing-a-custom-tool
println!(
writeln!(stdout,
"{}",
serde_json::to_string_pretty(
&diagnostics
@@ -274,16 +291,18 @@ impl<'a> Printer<'a> {
)
.collect::<Vec<_>>()
)?
);
)?;
}
}
stdout.flush()?;
Ok(())
}
pub fn write_continuously(&self, diagnostics: &Diagnostics) {
pub fn write_continuously(&self, diagnostics: &Diagnostics) -> Result<()> {
if matches!(self.log_level, LogLevel::Silent) {
return;
return Ok(());
}
if self.log_level >= &LogLevel::Default {
@@ -293,18 +312,21 @@ impl<'a> Printer<'a> {
);
}
let mut stdout = BufWriter::new(io::stdout().lock());
if !diagnostics.messages.is_empty() {
if self.log_level >= &LogLevel::Default {
println!();
writeln!(stdout)?;
}
for message in &diagnostics.messages {
print_message(message);
print_message(&mut stdout, message)?;
}
}
stdout.flush()?;
Ok(())
}
#[allow(clippy::unused_self)]
pub fn clear_screen(&self) -> Result<()> {
pub fn clear_screen() -> Result<()> {
#[cfg(not(target_family = "wasm"))]
clearscreen::clear()?;
Ok(())
@@ -330,7 +352,7 @@ fn num_digits(n: usize) -> usize {
}
/// Print a single `Message` with full details.
fn print_message(message: &Message) {
fn print_message<T: Write>(stdout: &mut T, message: &Message) -> Result<()> {
let label = format!(
"{}{}{}{}{}{} {} {}",
relativize_path(Path::new(&message.filename)).bold(),
@@ -342,7 +364,7 @@ fn print_message(message: &Message) {
message.kind.code().as_ref().red().bold(),
message.kind.body(),
);
println!("{label}");
writeln!(stdout, "{label}")?;
if let Some(source) = &message.source {
let commit = message.kind.commit();
let footer = if commit.is_some() {
@@ -354,7 +376,6 @@ fn print_message(message: &Message) {
} else {
vec![]
};
let snippet = Snippet {
title: Some(Annotation {
label: None,
@@ -384,13 +405,19 @@ fn print_message(message: &Message) {
// Skip the first line, since we format the `label` ourselves.
let message = DisplayList::from(snippet).to_string();
let (_, message) = message.split_once('\n').unwrap();
println!("{message}\n");
writeln!(stdout, "{message}\n")?;
}
Ok(())
}
/// Print a grouped `Message`, assumed to be printed in a group with others from
/// the same file.
fn print_grouped_message(message: &Message, row_length: usize, column_length: usize) {
fn print_grouped_message<T: Write>(
stdout: &mut T,
message: &Message,
row_length: usize,
column_length: usize,
) -> Result<()> {
let label = format!(
" {}{}{}{}{} {} {}",
" ".repeat(row_length - num_digits(message.location.row())),
@@ -401,7 +428,7 @@ fn print_grouped_message(message: &Message, row_length: usize, column_length: us
message.kind.code().as_ref().red().bold(),
message.kind.body(),
);
println!("{label}");
writeln!(stdout, "{label}")?;
if let Some(source) = &message.source {
let commit = message.kind.commit();
let footer = if commit.is_some() {
@@ -413,7 +440,6 @@ fn print_grouped_message(message: &Message, row_length: usize, column_length: us
} else {
vec![]
};
let snippet = Snippet {
title: Some(Annotation {
label: None,
@@ -444,6 +470,7 @@ fn print_grouped_message(message: &Message, row_length: usize, column_length: us
let message = DisplayList::from(snippet).to_string();
let (_, message) = message.split_once('\n').unwrap();
let message = textwrap::indent(message, " ");
println!("{message}");
writeln!(stdout, "{message}")?;
}
Ok(())
}

View File

@@ -180,7 +180,7 @@ fn test_ruff_black_compatibility() -> Result<()> {
// problem. Ruff would add a `# noqa: W292` after the first run, black introduces a
// newline, and ruff removes the `# noqa: W292` again.
.filter(|origin| *origin != RuleOrigin::Ruff)
.map(|origin| origin.codes().iter().map(AsRef::as_ref).join(","))
.map(|origin| origin.prefixes().as_list(","))
.join(",");
let ruff_args = [
"-",

View File

@@ -1,12 +1,8 @@
[package]
name = "ruff_dev"
version = "0.0.221"
version = "0.0.224"
edition = "2021"
[lib]
name = "ruff_dev"
doctest = false
[dependencies]
anyhow = { version = "1.0.66" }
clap = { version = "4.0.1", features = ["derive"] }
@@ -15,9 +11,9 @@ libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a87
once_cell = { version = "1.16.0" }
ruff = { path = ".." }
ruff_cli = { path = "../ruff_cli" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
schemars = { version = "0.8.11" }
serde_json = {version="1.0.91"}
strum = { version = "0.24.1", features = ["strum_macros"] }

View File

@@ -1,8 +1,7 @@
//! Generate CLI help.
use anyhow::Result;
use clap::{Args, CommandFactory};
use ruff_cli::Cli as MainCli;
use clap::Args;
use crate::utils::replace_readme_section;
@@ -21,8 +20,7 @@ fn trim_lines(s: &str) -> String {
}
pub fn main(cli: &Cli) -> Result<()> {
let mut cmd = MainCli::command();
let output = trim_lines(cmd.render_help().to_string().trim());
let output = trim_lines(ruff_cli::help().trim());
if cli.dry_run {
print!("{output}");

View File

@@ -2,8 +2,7 @@
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::registry::{RuleCode, RuleOrigin};
use ruff::registry::{Prefixes, RuleCodePrefix, RuleOrigin};
use strum::IntoEnumIterator;
use crate::utils::replace_readme_section;
@@ -21,12 +20,33 @@ pub struct Cli {
pub(crate) dry_run: bool,
}
fn generate_table(table_out: &mut String, prefix: &RuleCodePrefix) {
table_out.push_str("| Code | Name | Message | Fix |");
table_out.push('\n');
table_out.push_str("| ---- | ---- | ------- | --- |");
table_out.push('\n');
for rule_code in prefix.codes() {
let kind = rule_code.kind();
let fix_token = if kind.fixable() { "🛠" } else { "" };
table_out.push_str(&format!(
"| {} | {} | {} | {} |",
kind.code().as_ref(),
kind.as_ref(),
kind.summary().replace('|', r"\|"),
fix_token
));
table_out.push('\n');
}
table_out.push('\n');
}
pub fn main(cli: &Cli) -> Result<()> {
// Generate the table string.
let mut table_out = String::new();
let mut toc_out = String::new();
for origin in RuleOrigin::iter() {
let codes_csv: String = origin.codes().iter().map(AsRef::as_ref).join(", ");
let prefixes = origin.prefixes();
let codes_csv: String = prefixes.as_list(", ");
table_out.push_str(&format!("### {} ({codes_csv})", origin.title()));
table_out.push('\n');
table_out.push('\n');
@@ -50,26 +70,16 @@ pub fn main(cli: &Cli) -> Result<()> {
table_out.push('\n');
}
table_out.push_str("| Code | Name | Message | Fix |");
table_out.push('\n');
table_out.push_str("| ---- | ---- | ------- | --- |");
table_out.push('\n');
for rule_code in RuleCode::iter() {
if rule_code.origin() == origin {
let kind = rule_code.kind();
let fix_token = if kind.fixable() { "🛠" } else { "" };
table_out.push_str(&format!(
"| {} | {} | {} | {} |",
kind.code().as_ref(),
kind.as_ref(),
kind.summary().replace('|', r"\|"),
fix_token
));
table_out.push('\n');
match prefixes {
Prefixes::Single(prefix) => generate_table(&mut table_out, &prefix),
Prefixes::Multiple(entries) => {
for (prefix, category) in entries {
table_out.push_str(&format!("#### {category} ({})", prefix.as_ref()));
table_out.push('\n');
generate_table(&mut table_out, &prefix);
}
}
}
table_out.push('\n');
}
if cli.dry_run {

View File

@@ -1,24 +0,0 @@
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
clippy::implicit_hasher,
clippy::match_same_arms,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::similar_names,
clippy::too_many_lines
)]
#![forbid(unsafe_code)]
pub mod generate_all;
pub mod generate_cli_help;
pub mod generate_json_schema;
pub mod generate_options;
pub mod generate_rules_table;
pub mod print_ast;
pub mod print_cst;
pub mod print_tokens;
pub mod round_trip;
mod utils;

View File

@@ -1,3 +1,6 @@
//! This crate implements an internal CLI for developers of Ruff.
//!
//! Within the ruff repository you can run it with `cargo dev`.
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
@@ -12,12 +15,19 @@
)]
#![forbid(unsafe_code)]
mod generate_all;
mod generate_cli_help;
mod generate_json_schema;
mod generate_options;
mod generate_rules_table;
mod print_ast;
mod print_cst;
mod print_tokens;
mod round_trip;
mod utils;
use anyhow::Result;
use clap::{Parser, Subcommand};
use ruff_dev::{
generate_all, generate_cli_help, generate_json_schema, generate_options, generate_rules_table,
print_ast, print_cst, print_tokens, round_trip,
};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]

View File

@@ -5,8 +5,7 @@ use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use ruff::source_code::{Generator, Locator, Stylist};
use rustpython_parser::parser;
use ruff::source_code::round_trip;
#[derive(Args)]
pub struct Cli {
@@ -17,11 +16,6 @@ pub struct Cli {
pub fn main(cli: &Cli) -> Result<()> {
let contents = fs::read_to_string(&cli.file)?;
let python_ast = parser::parse_program(&contents, &cli.file.to_string_lossy())?;
let locator = Locator::new(&contents);
let stylist = Stylist::from_contents(&contents, &locator);
let mut generator: Generator = (&stylist).into();
generator.unparse_suite(&python_ast);
println!("{}", generator.generate());
println!("{}", round_trip(&contents, &cli.file.to_string_lossy())?);
Ok(())
}

View File

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

View File

@@ -0,0 +1,133 @@
use proc_macro2::Span;
use quote::quote;
use syn::parse::Parse;
use syn::{Ident, Path, Token};
pub fn define_rule_mapping(mapping: Mapping) -> proc_macro2::TokenStream {
let mut rulecode_variants = quote!();
let mut diagkind_variants = quote!();
let mut rulecode_kind_match_arms = quote!();
let mut rulecode_origin_match_arms = quote!();
let mut diagkind_code_match_arms = quote!();
let mut diagkind_body_match_arms = quote!();
let mut diagkind_fixable_match_arms = quote!();
let mut diagkind_commit_match_arms = quote!();
let mut from_impls_for_diagkind = quote!();
for (code, path, name) in mapping.entries {
rulecode_variants.extend(quote! {#code,});
diagkind_variants.extend(quote! {#name(#path),});
rulecode_kind_match_arms.extend(
quote! {RuleCode::#code => DiagnosticKind::#name(<#path as Violation>::placeholder()),},
);
let origin = get_origin(&code);
rulecode_origin_match_arms.extend(quote! {RuleCode::#code => RuleOrigin::#origin,});
diagkind_code_match_arms.extend(quote! {DiagnosticKind::#name(..) => &RuleCode::#code, });
diagkind_body_match_arms
.extend(quote! {DiagnosticKind::#name(x) => Violation::message(x), });
diagkind_fixable_match_arms
.extend(quote! {DiagnosticKind::#name(x) => x.autofix_title_formatter().is_some(),});
diagkind_commit_match_arms.extend(
quote! {DiagnosticKind::#name(x) => x.autofix_title_formatter().map(|f| f(x)), },
);
from_impls_for_diagkind.extend(quote! {
impl From<#path> for DiagnosticKind {
fn from(x: #path) -> Self {
DiagnosticKind::#name(x)
}
}
});
}
quote! {
#[derive(
AsRefStr,
RuleCodePrefix,
EnumIter,
EnumString,
Debug,
Display,
PartialEq,
Eq,
Clone,
Serialize,
Deserialize,
Hash,
PartialOrd,
Ord,
)]
pub enum RuleCode { #rulecode_variants }
#[derive(AsRefStr, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiagnosticKind { #diagkind_variants }
impl RuleCode {
/// A placeholder representation of the `DiagnosticKind` for the diagnostic.
pub fn kind(&self) -> DiagnosticKind {
match self { #rulecode_kind_match_arms }
}
pub fn origin(&self) -> RuleOrigin {
match self { #rulecode_origin_match_arms }
}
}
impl DiagnosticKind {
/// A four-letter shorthand code for the diagnostic.
pub fn code(&self) -> &'static RuleCode {
match self { #diagkind_code_match_arms }
}
/// The body text for the diagnostic.
pub fn body(&self) -> String {
match self { #diagkind_body_match_arms }
}
/// Whether the diagnostic is (potentially) fixable.
pub fn fixable(&self) -> bool {
match self { #diagkind_fixable_match_arms }
}
/// The message used to describe the fix action for a given `DiagnosticKind`.
pub fn commit(&self) -> Option<String> {
match self { #diagkind_commit_match_arms }
}
}
#from_impls_for_diagkind
}
}
fn get_origin(ident: &Ident) -> Ident {
let ident = ident.to_string();
let mut iter = crate::prefixes::PREFIX_TO_ORIGIN.iter();
let origin = loop {
let (prefix, origin) = iter
.next()
.unwrap_or_else(|| panic!("code doesn't start with any recognized prefix: {ident}"));
if ident.starts_with(prefix) {
break origin;
}
};
Ident::new(origin, Span::call_site())
}
pub struct Mapping {
entries: Vec<(Ident, Path, Ident)>,
}
impl Parse for Mapping {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut entries = Vec::new();
while !input.is_empty() {
let code: Ident = input.parse()?;
let _: Token![=>] = input.parse()?;
let path: Path = input.parse()?;
let name = path.segments.last().unwrap().ident.clone();
let _: Token![,] = input.parse()?;
entries.push((code, path, name));
}
Ok(Mapping { entries })
}
}

View File

@@ -1,3 +1,4 @@
//! This crate implements internal macros for the `ruff` library.
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
@@ -12,11 +13,10 @@
)]
#![forbid(unsafe_code)]
use proc_macro2::Span;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Ident};
use syn::{parse_macro_input, DeriveInput};
mod config;
mod define_rule_mapping;
mod prefixes;
mod rule_code_prefix;
@@ -39,21 +39,7 @@ pub fn derive_rule_code_prefix(input: proc_macro::TokenStream) -> proc_macro::To
}
#[proc_macro]
pub fn origin_by_code(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ident = parse_macro_input!(item as Ident).to_string();
let mut iter = prefixes::PREFIX_TO_ORIGIN.iter();
let origin = loop {
let (prefix, origin) = iter
.next()
.unwrap_or_else(|| panic!("code doesn't start with any recognized prefix: {ident}"));
if ident.starts_with(prefix) {
break origin;
}
};
let prefix = Ident::new(origin, Span::call_site());
quote! {
RuleOrigin::#prefix
}
.into()
pub fn define_rule_mapping(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let mapping = parse_macro_input!(item as define_rule_mapping::Mapping);
define_rule_mapping::define_rule_mapping(mapping).into()
}

View File

@@ -9,6 +9,7 @@ pub const PREFIX_TO_ORIGIN: &[(&str, &str)] = &[
("B", "Flake8Bugbear"),
("C4", "Flake8Comprehensions"),
("C9", "McCabe"),
("COM", "Flake8Commas"),
("DTZ", "Flake8Datetimez"),
("D", "Pydocstyle"),
("ERA", "Eradicate"),

View File

@@ -10,8 +10,9 @@ Example usage:
import argparse
import os
from pathlib import Path
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def dir_name(plugin: str) -> str:
@@ -25,15 +26,16 @@ def pascal_case(plugin: str) -> str:
def main(*, plugin: str, url: str) -> None:
# Create the test fixture folder.
os.makedirs(
os.path.join(ROOT_DIR, f"resources/test/fixtures/{dir_name(plugin)}"),
ROOT_DIR / "resources/test/fixtures" / dir_name(plugin),
exist_ok=True,
)
# Create the Rust module.
os.makedirs(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}"), exist_ok=True)
with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/rules.rs"), "w+") as fp:
rust_module = ROOT_DIR / "src/rules" / dir_name(plugin)
os.makedirs(rust_module, exist_ok=True)
with open(rust_module / "rules.rs", "w+") as fp:
fp.write("use crate::checkers::ast::Checker;\n")
with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/mod.rs"), "w+") as fp:
with open(rust_module / "mod.rs", "w+") as fp:
fp.write("pub(crate) mod rules;\n")
fp.write("\n")
fp.write(
@@ -65,15 +67,14 @@ mod tests {
% dir_name(plugin)
)
# Add the plugin to `lib.rs`.
with open(os.path.join(ROOT_DIR, "src/lib.rs"), "a") as fp:
fp.write(f"mod {dir_name(plugin)};")
# Add the plugin to `rules/mod.rs`.
with open(ROOT_DIR / "src/rules/mod.rs", "a") as fp:
fp.write(f"pub mod {dir_name(plugin)};")
# Add the relevant sections to `src/registry.rs`.
with open(os.path.join(ROOT_DIR, "src/registry.rs")) as fp:
content = fp.read()
content = (ROOT_DIR / "src/registry.rs").read_text()
with open(os.path.join(ROOT_DIR, "src/registry.rs"), "w") as fp:
with open(ROOT_DIR / "src/registry.rs", "w") as fp:
for line in content.splitlines():
if line.strip() == "// Ruff":
indent = line.split("// Ruff")[0]
@@ -108,10 +109,9 @@ mod tests {
fp.write("\n")
# Add the relevant section to `src/violations.rs`.
with open(os.path.join(ROOT_DIR, "src/violations.rs")) as fp:
content = fp.read()
content = (ROOT_DIR / "src/violations.rs").read_text()
with open(os.path.join(ROOT_DIR, "src/violations.rs"), "w") as fp:
with open(ROOT_DIR / "src/violations.rs", "w") as fp:
for line in content.splitlines():
if line.strip() == "// Ruff":
indent = line.split("// Ruff")[0]

View File

@@ -11,8 +11,9 @@ Example usage:
import argparse
import os
from pathlib import Path
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def dir_name(origin: str) -> str:
@@ -32,16 +33,16 @@ def snake_case(name: str) -> str:
def main(*, name: str, code: str, origin: str) -> None:
# Create a test fixture.
with open(
os.path.join(ROOT_DIR, f"resources/test/fixtures/{dir_name(origin)}/{code}.py"),
ROOT_DIR / "resources/test/fixtures" / dir_name(origin) / f"{code}.py",
"a",
):
pass
# Add the relevant `#testcase` macro.
with open(os.path.join(ROOT_DIR, f"src/{dir_name(origin)}/mod.rs")) as fp:
content = fp.read()
mod_rs = ROOT_DIR / "src/rules" / dir_name(origin) / "mod.rs"
content = mod_rs.read_text()
with open(os.path.join(ROOT_DIR, f"src/{dir_name(origin)}/mod.rs"), "w") as fp:
with open(mod_rs, "w") as fp:
for line in content.splitlines():
if line.strip() == "fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {":
indent = line.split("fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {")[0]
@@ -52,7 +53,7 @@ def main(*, name: str, code: str, origin: str) -> None:
fp.write("\n")
# Add the relevant rule function.
with open(os.path.join(ROOT_DIR, f"src/{dir_name(origin)}/rules.rs"), "a") as fp:
with open(ROOT_DIR / "src/rules" / dir_name(origin) / "rules.rs", "a") as fp:
fp.write(
f"""
/// {code}
@@ -62,10 +63,9 @@ pub fn {snake_case(name)}(checker: &mut Checker) {{}}
fp.write("\n")
# Add the relevant struct to `src/violations.rs`.
with open(os.path.join(ROOT_DIR, "src/violations.rs")) as fp:
content = fp.read()
content = (ROOT_DIR / "src/violations.rs").read_text()
with open(os.path.join(ROOT_DIR, "src/violations.rs"), "w") as fp:
with open(ROOT_DIR / "src/violations.rs", "w") as fp:
for line in content.splitlines():
fp.write(line)
fp.write("\n")
@@ -90,12 +90,11 @@ impl Violation for %s {
fp.write("\n")
# Add the relevant code-to-violation pair to `src/registry.rs`.
with open(os.path.join(ROOT_DIR, "src/registry.rs")) as fp:
content = fp.read()
content = (ROOT_DIR / "src/registry.rs").read_text()
seen_macro = False
has_written = False
with open(os.path.join(ROOT_DIR, "src/registry.rs"), "w") as fp:
with open(ROOT_DIR / "src/registry.rs", "w") as fp:
for line in content.splitlines():
fp.write(line)
fp.write("\n")

View File

@@ -0,0 +1,25 @@
# benchmarks
Utilities for benchmarking Ruff.
## Getting Started
Run `./scripts/benchmarks/run.sh` to clone the benchmarking target (CPython).
If you're looking to benchmark Ruff against other tools, you'll also need to run `poetry
install` to create a virtual environment with the required dependencies.
## Running Benchmarks
Run `./scripts/benchmarks/run.sh` to run Ruff over the target repo (CPython). The
`./scripts/benchmarks` folder contains a few other benchmarks (e.g., `scripts/benchmarks/run_comparisons.sh`
compares Ruff to a variety of other tools).
## Generating Plots
The Vega specification for the benchmark plot depicted in the root README can be found at
`scripts/benchmarks/graph-spec.json`. You can render this JSON spec in the [Vega Editor](https://vega.github.io/editor/#/edited).
The images seen in the README are generated by exporting the rendered Vega spec as SVG (at around
688px wide) and manually bolding the Ruff title and benchmark time. The dark mode variant is
generated by changing the fill from `fill="#333333"` to `fill="#C9D1D9"`.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,209 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"values": [
{
"tool": "Ruff",
"time": 0.2943,
"timeFormat": "0.29s"
},
{
"tool": "Autoflake",
"time": 6.175,
"timeFormat": "6.18s"
},
{
"tool": "Flake8",
"time": 12.26,
"timeFormat": "12.26s"
},
{
"tool": "Pyflakes",
"time": 15.786,
"timeFormat": "15.79s"
},
{
"tool": "Pycodestyle",
"time": 46.921,
"timeFormat": "46.92s"
},
{
"tool": "Pylint",
"time": 62.0,
"timeFormat": "> 60s"
}
]
},
"config": {
"params": [
{
"name": "defaultFont",
"value": "-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\""
},
{
"name": "titleColor",
"value": "#333333"
},
{
"name": "labelColor",
"value": "#333333"
}
],
"header": {
"labelFont": {
"expr": "defaultFont"
},
"titleFont": {
"expr": "defaultFont"
},
"titleFontWeight": 500
},
"text": {
"font": {
"expr": "defaultFont"
},
"color": {
"expr": "labelColor"
}
},
"mark": {
"font": {
"expr": "defaultFont"
},
"color": {
"expr": "labelColor"
}
},
"title": {
"font": {
"expr": "defaultFont"
},
"subtitleFont": {
"expr": "defaultFont"
},
"fontWeight": 500
},
"axis": {
"labelColor": {
"expr": "labelColor"
},
"labelFont": {
"expr": "defaultFont"
},
"titleFont": {
"expr": "defaultFont"
},
"titleFontWeight": 500,
"titleColor": {
"expr": "titleColor"
},
"titleFontSize": 12
},
"legend": {
"titleFontWeight": 500,
"titleColor": {
"expr": "titleColor"
},
"titleFontSize": 12,
"labelColor": {
"expr": "labelColor"
},
"labelFont": {
"expr": "defaultFont"
},
"titleFont": {
"expr": "defaultFont"
}
},
"view": {
"stroke": null
},
"background": "transparent"
},
"background": "transparent",
"encoding": {
"y": {
"field": "tool",
"type": "nominal",
"axis": {
"grid": false,
"title": null,
"labelFontSize": 12,
"ticks": false,
"labelPadding": 10,
"domain": false
},
"sort": null
},
"x": {
"field": "time",
"type": "quantitative",
"axis": {
"title": null,
"labelExpr": "datum.value + 's'",
"tickCount": 3,
"tickSize": 0,
"labelPadding": 6,
"labelAlign": "center",
"labelFontSize": 12,
"tickColor": "rgba(127,127,127,0.25)",
"gridColor": "rgba(127,127,127,0.25)",
"domain": false
}
}
},
"height": 140,
"width": "container",
"layer": [
{
"mark": "bar",
"encoding": {
"size": {
"value": 13
},
"color": {
"value": "#E15759"
}
}
},
{
"transform": [
{
"filter": "datum.tool !== 'ruff'"
}
],
"mark": {
"type": "text",
"align": "left",
"baseline": "middle",
"dx": 6,
"fontSize": 12
},
"encoding": {
"text": {
"field": "timeFormat"
}
}
},
{
"transform": [
{
"filter": "datum.tool === 'ruff'"
}
],
"mark": {
"type": "text",
"align": "left",
"baseline": "middle",
"dx": 6,
"fontSize": 12,
"fontWeight": "bold"
},
"encoding": {
"text": {
"field": "timeFormat"
}
}
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

1005
scripts/benchmarks/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
[tool.poetry]
name = "scripts"
version = "0.1.0"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
[tool.poetry.dependencies]
python = ">=3.10,<3.12"
autoflake = "^2.0.0"
flake8 = "^6.0.0"
pycodestyle = "^2.10.0"
pyflakes = "^3.0.1"
pylint = "^2.15.10"
black = "^22.12.0"
isort = "^5.11.4"
flake8-2020 = { version = "*", optional = true }
flake8-annotations = { version = "*", optional = true }
flake8-bandit = { version = "*", optional = true }
flake8-blind-except = { version = "*", optional = true }
# flake8-boolean-trap = { version = "*", optional = true }
flake8-bugbear = { version = "*", optional = true }
flake8-builtins = { version = "*", optional = true }
flake8-commas = { version = "*", optional = true }
flake8-comprehensions = { version = "*", optional = true }
flake8-datetimez = { version = "*", optional = true }
flake8-debugger = { version = "*", optional = true }
flake8-docstrings = { version = "*", optional = true }
# flake8-eradicate = { version = "*", optional = true }
flake8-errmsg = { version = "*", optional = true }
flake8-implicit-str-concat = { version = "*", optional = true }
# flake8-import-conventions = { version = "*", optional = true }
flake8-isort = { version = "*", optional = true }
flake8-pie = { version = "*", optional = true }
flake8-print = { version = "*", optional = true }
flake8-quotes = { version = "*", optional = true }
flake8-return = { version = "*", optional = true }
flake8-simplify = { version = "*", optional = true }
flake8-super = { version = "*", optional = true }
flake8-tidy-imports = { version = "*", optional = true }
pandas-vet = { version = "*", optional = true }
pep8-naming = { version = "*", optional = true }
[tool.poetry.dev-dependencies]
[tool.poetry.extras]
plugins = [
"flake8-2020",
"flake8-annotations",
"flake8-bandit",
"flake8-blind-except",
# "flake8-boolean-trap",
"flake8-bugbear",
"flake8-builtins",
"flake8-commas",
"flake8-comprehensions",
"flake8-datetimez",
"flake8-debugger",
"flake8-docstrings",
# "flake8-eradicate",
"flake8-errmsg",
"flake8-implicit-str-concat",
# "flake8-import-conventions",
"flake8-isort",
"flake8-pie",
"flake8-print",
"flake8-quotes",
"flake8-return",
"flake8-simplify",
"flake8-super",
"flake8-tidy-imports",
"pandas-vet",
"pep8-naming",
]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

8
scripts/benchmarks/run.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env sh
###
# Benchmark Ruff on the CPython codebase.
###
cargo build --release && hyperfine --ignore-failure --warmup 10 \
"./target/release/ruff ./resources/test/cpython/ --no-cache"

26
scripts/benchmarks/run_all.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env sh
###
# Benchmark Ruff's performance against a variety of similar tools, suppressing output as much as
# possible (so as to reduce I/O overhead).
###
# Note: Flake8's `checker.py` requires the following variant of `mp_run`:
# def _mp_run(filename: str) -> tuple[str, Results, dict[str, int]]:
# try:
# return FileChecker(
# filename=filename, plugins=_mp_plugins, options=_mp_options
# ).run_checks()
# except:
# return (filename, [], {
# "files": 0,
# "logical lines": 0,
# "physical lines": 0,
# "tokens": 0,
# })
hyperfine --ignore-failure --warmup 5 \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --select ALL" \
"flake8 resources/test/cpython -qq --docstring-convention=all" \
"pycodestyle resources/test/cpython -qq" \
"pylint resources/test/cpython -j 0 --recursive=y --disable=E,W,C,R"

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env sh
###
# Benchmark Ruff's performance against a variety of similar tools.
###
hyperfine --ignore-failure --warmup 5 \
"./target/release/ruff ./resources/test/cpython/ --no-cache" \
"pyflakes resources/test/cpython" \
"autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython" \
"pycodestyle resources/test/cpython" \
"flake8 resources/test/cpython"

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env sh
###
# Benchmark the incremental performance of each subsequent plugin.
###
cargo build --release && hyperfine --ignore-failure --warmup 10 \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select C90" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select I" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select D" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select UP" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select N" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select YTT" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select ANN" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select S" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select BLE" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select FBT" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select B" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select A" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select C4" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select T10" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select EM" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select ISC" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select ICN" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select T20" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select PT" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select Q" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select RET" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select SIM" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select TID" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select ARG" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select DTZ" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select ERA" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select PD" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select PGH" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select PLC" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select PLE" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select PLR" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select PLW" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select PIE" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select COM" \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent --extend-select RUF"

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env sh
###
# Benchmark Ruff's performance against a variety of similar tools, suppressing output as much as
# possible (so as to reduce I/O overhead).
###
hyperfine --ignore-failure --warmup 5 \
"./target/release/ruff ./resources/test/cpython/ --no-cache --silent" \
"pycodestyle resources/test/cpython -qq" \
"flake8 resources/test/cpython -qq" \
"pylint resources/test/cpython -j 0 --recursive=y --disable=E,W,C,R"

7
scripts/benchmarks/setup.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env sh
###
# Setup the CPython repository to enable benchmarking.
###
git clone --branch 3.10 https://github.com/python/cpython.git resources/test/cpython

305
scripts/poetry.lock generated
View File

@@ -1,305 +0,0 @@
# This file is automatically @generated by Poetry and should not be changed by hand.
[[package]]
name = "astroid"
version = "2.12.13"
description = "An abstract syntax tree for Python with inference support."
category = "main"
optional = false
python-versions = ">=3.7.2"
files = [
{file = "astroid-2.12.13-py3-none-any.whl", hash = "sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907"},
{file = "astroid-2.12.13.tar.gz", hash = "sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"},
]
[package.dependencies]
lazy-object-proxy = ">=1.4.0"
wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""}
[[package]]
name = "autoflake"
version = "1.7.8"
description = "Removes unused imports and unused variables"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "autoflake-1.7.8-py3-none-any.whl", hash = "sha256:46373ef69b6714f5064c923bb28bd797c4f8a9497f557d87fc36665c6d956b39"},
{file = "autoflake-1.7.8.tar.gz", hash = "sha256:e7e46372dee46fa1c97acf310d99d922b63d369718a270809d7c278d34a194cf"},
]
[package.dependencies]
pyflakes = ">=1.1.0,<3"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "dill"
version = "0.3.6"
description = "serialize all of python"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"},
{file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"},
]
[package.extras]
graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "flake8"
version = "5.0.4"
description = "the modular source code checker: pep8 pyflakes and co"
category = "main"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.9.0,<2.10.0"
pyflakes = ">=2.5.0,<2.6.0"
[[package]]
name = "isort"
version = "5.11.4"
description = "A Python utility / library to sort Python imports."
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"},
{file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"},
]
[package.extras]
colors = ["colorama (>=0.4.3,<0.5.0)"]
pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "lazy-object-proxy"
version = "1.8.0"
description = "A fast and thorough lazy object proxy."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"},
{file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"},
{file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"},
{file = "lazy_object_proxy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"},
{file = "lazy_object_proxy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7"},
{file = "lazy_object_proxy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e"},
{file = "lazy_object_proxy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d"},
{file = "lazy_object_proxy-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c"},
{file = "lazy_object_proxy-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd"},
{file = "lazy_object_proxy-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858"},
{file = "lazy_object_proxy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada"},
{file = "lazy_object_proxy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f"},
{file = "lazy_object_proxy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c"},
{file = "lazy_object_proxy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288"},
{file = "lazy_object_proxy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f"},
{file = "lazy_object_proxy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0"},
{file = "lazy_object_proxy-1.8.0-pp37-pypy37_pp73-any.whl", hash = "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891"},
{file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"},
{file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"},
]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "platformdirs"
version = "2.6.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"},
{file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"},
]
[package.extras]
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
[[package]]
name = "pycodestyle"
version = "2.9.1"
description = "Python style guide checker"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
]
[[package]]
name = "pyflakes"
version = "2.5.0"
description = "passive checker of Python programs"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
{file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
]
[[package]]
name = "pylint"
version = "2.15.9"
description = "python code static checker"
category = "main"
optional = false
python-versions = ">=3.7.2"
files = [
{file = "pylint-2.15.9-py3-none-any.whl", hash = "sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb"},
{file = "pylint-2.15.9.tar.gz", hash = "sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4"},
]
[package.dependencies]
astroid = ">=2.12.13,<=2.14.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = {version = ">=0.2", markers = "python_version < \"3.11\""}
isort = ">=4.2.5,<6"
mccabe = ">=0.6,<0.8"
platformdirs = ">=2.2.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
tomlkit = ">=0.10.1"
[package.extras]
spelling = ["pyenchant (>=3.2,<4.0)"]
testutils = ["gitpython (>3)"]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "tomlkit"
version = "0.11.6"
description = "Style preserving TOML library"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"},
{file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"},
]
[[package]]
name = "wrapt"
version = "1.14.1"
description = "Module for decorators, wrappers and monkey patching."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
files = [
{file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"},
{file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"},
{file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"},
{file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"},
{file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"},
{file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"},
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"},
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"},
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"},
{file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"},
{file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"},
{file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"},
{file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"},
{file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"},
{file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"},
{file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"},
{file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"},
{file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"},
{file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"},
{file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"},
{file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"},
{file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"},
{file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"},
{file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"},
{file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"},
{file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"},
{file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"},
{file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"},
{file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"},
{file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"},
{file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"},
{file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"},
{file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"},
{file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"},
{file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"},
{file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"},
{file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"},
{file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"},
{file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"},
{file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"},
{file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"},
{file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"},
{file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"},
{file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"},
{file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"},
{file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"},
{file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"},
{file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"},
{file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"},
{file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"},
{file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"},
{file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"},
]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.11"
content-hash = "959633dfe6335ab3f943a95c5fdd12ff1bb66cd03c5917704f10ae38d3a5009c"

View File

@@ -1,27 +0,0 @@
[tool.black]
line-length = 120
[tool.ruff]
line-length = 120
select = ["E", "F", "W", "I", "C", "RET", "ANN", "UP"]
target-version = "py310"
[tool.poetry]
name = "scripts"
version = "0.1.0"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
[tool.poetry.dependencies]
python = ">=3.10,<3.11"
autoflake = "^1.4"
flake8 = "^5.0.4"
pycodestyle = "^2.9.1"
pyflakes = "^2.5.0"
pylint = "^2.15.0"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,29 +0,0 @@
#!/usr/bin/env python3
"""Wrapper around Flake8 to enable multiprocessing on all operating systems.
As of Python 3.8, macOS's default "start method" for multiprocessing is `spawn`. Flake8
requires a "start method" of `fork`, and disables multiprocessing if it detects `spawn`
or some other "start method". This script enables the `fork` start method before passing
along any command-line arguments to `flake8`.
This has never caused me any problems, but note that they disabled this for a reason:
Flake8's plugin interface doesn't work with `spawn`, and the maintainer says that `fork`
is "pretty broken" on macOS.
See:
- https://github.com/pycqa/flake8/issues/955
- https://github.com/PyCQA/flake8/issues/1337
- https://github.com/PyCQA/flake8/issues/342
- https://github.com/PyCQA/flake8/pull/1621
Example usage: python -m run_flake8 --select=E501 .
"""
import multiprocessing
import sys
from flake8.main import cli
if __name__ == "__main__":
multiprocessing.set_start_method("fork", force=True)
cli.main(sys.argv[1:])

View File

@@ -1,5 +1,5 @@
//! An equivalent object hierarchy to the `Expr` hierarchy, but with the ability
//! to compare expressions for equality (via `Eq` and `Hash`).
//! An equivalent object hierarchy to the [`Expr`] hierarchy, but with the
//! ability to compare expressions for equality (via [`Eq`] and [`Hash`]).
use num_bigint::BigInt;
use rustpython_ast::{

View File

@@ -13,7 +13,7 @@ use rustpython_parser::token::StringKind;
use crate::ast::types::{Binding, BindingKind, Range};
use crate::checkers::ast::Checker;
use crate::source_code::{Generator, Locator, Stylist};
use crate::source_code::{Generator, Indexer, Locator, Stylist};
/// Create an `Expr` with default location from an `ExprKind`.
pub fn create_expr(node: ExprKind) -> Expr {
@@ -94,6 +94,45 @@ pub fn contains_call_path(checker: &Checker, expr: &Expr, target: &[&str]) -> bo
})
}
/// Return `true` if the `Expr` contains an expression that appears to include a
/// side-effect (like a function call).
pub fn contains_effect(checker: &Checker, expr: &Expr) -> bool {
any_over_expr(expr, &|expr| {
// Accept empty initializers.
if let ExprKind::Call {
func,
args,
keywords,
} = &expr.node
{
if args.is_empty() && keywords.is_empty() {
if let ExprKind::Name { id, .. } = &func.node {
let is_empty_initializer = (id == "set"
|| id == "list"
|| id == "tuple"
|| id == "dict"
|| id == "frozenset")
&& checker.is_builtin(id);
return !is_empty_initializer;
}
}
}
// Otherwise, avoid all complex expressions.
matches!(
expr.node,
ExprKind::Call { .. }
| ExprKind::Await { .. }
| ExprKind::GeneratorExp { .. }
| ExprKind::ListComp { .. }
| ExprKind::SetComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::Yield { .. }
| ExprKind::YieldFrom { .. }
)
})
}
/// Call `func` over every `Expr` in `expr`, returning `true` if any expression
/// returns `true`..
pub fn any_over_expr<F>(expr: &Expr, func: &F) -> bool
@@ -197,7 +236,7 @@ where
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
/// Return `true` if the `Stmt` is an assignment to a dunder (like `__all__`).
/// Return `true` if the [`Stmt`] is an assignment to a dunder (like `__all__`).
pub fn is_assignment_to_a_dunder(stmt: &Stmt) -> bool {
// Check whether it's an assignment to a dunder, with or without a type
// annotation. This is what pycodestyle (as of 2.9.1) does.
@@ -219,7 +258,7 @@ pub fn is_assignment_to_a_dunder(stmt: &Stmt) -> bool {
}
}
/// Return `true` if the `Expr` is a singleton (`None`, `True`, `False`, or
/// Return `true` if the [`Expr`] is a singleton (`None`, `True`, `False`, or
/// `...`).
pub fn is_singleton(expr: &Expr) -> bool {
matches!(
@@ -231,7 +270,7 @@ pub fn is_singleton(expr: &Expr) -> bool {
)
}
/// Return `true` if the `Expr` is a constant or tuple of constants.
/// Return `true` if the [`Expr`] is a constant or tuple of constants.
pub fn is_constant(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Constant { .. } => true,
@@ -240,13 +279,13 @@ pub fn is_constant(expr: &Expr) -> bool {
}
}
/// Return `true` if the `Expr` is a non-singleton constant.
/// Return `true` if the [`Expr`] is a non-singleton constant.
pub fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// Return the `Keyword` with the given name, if it's present in the list of
/// `Keyword` arguments.
/// 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> {
keywords.iter().find(|keyword| {
let KeywordData { arg, .. } = &keyword.node;
@@ -254,7 +293,7 @@ pub fn find_keyword<'a>(keywords: &'a [Keyword], keyword_name: &str) -> Option<&
})
}
/// Return `true` if an `Expr` is `None`.
/// Return `true` if an [`Expr`] is `None`.
pub fn is_const_none(expr: &Expr) -> bool {
matches!(
&expr.node,
@@ -426,7 +465,7 @@ pub fn match_trailing_content(stmt: &Stmt, locator: &Locator) -> bool {
/// Return the number of trailing empty lines following a statement.
pub fn count_trailing_lines(stmt: &Stmt, locator: &Locator) -> usize {
let suffix =
locator.slice_source_code_at(&Location::new(stmt.end_location.unwrap().row() + 1, 0));
locator.slice_source_code_at(Location::new(stmt.end_location.unwrap().row() + 1, 0));
suffix
.lines()
.take_while(|line| line.trim().is_empty())
@@ -601,31 +640,62 @@ pub fn else_range(stmt: &Stmt, locator: &Locator) -> Option<Range> {
}
}
/// Return `true` if a `Stmt` appears to be part of a multi-statement line, with
/// other statements preceding it.
pub fn preceded_by_continuation(stmt: &Stmt, locator: &Locator) -> bool {
// Does the previous line end in a continuation? This will have a specific
// false-positive, which is that if the previous line ends in a comment, it
// will be treated as a continuation. So we should only use this information to
// make conservative choices.
// TODO(charlie): Come up with a more robust strategy.
if stmt.location.row() > 1 {
let range = Range::new(
Location::new(stmt.location.row() - 1, 0),
Location::new(stmt.location.row(), 0),
);
let line = locator.slice_source_code_range(&range);
if line.trim_end().ends_with('\\') {
return true;
}
}
false
/// Return the `Range` of the first `Tok::Colon` token in a `Range`.
pub fn first_colon_range(range: Range, locator: &Locator) -> Option<Range> {
let contents = locator.slice_source_code_range(&range);
let range = lexer::make_tokenizer_located(&contents, range.location)
.flatten()
.find(|(_, kind, _)| matches!(kind, Tok::Colon))
.map(|(location, _, end_location)| Range {
location,
end_location,
});
range
}
/// Return the `Range` of the first `Elif` or `Else` token in an `If` statement.
pub fn elif_else_range(stmt: &Stmt, locator: &Locator) -> Option<Range> {
let StmtKind::If { body, orelse, .. } = &stmt.node else {
return None;
};
let start = body
.last()
.expect("Expected body to be non-empty")
.end_location
.unwrap();
let end = match &orelse[..] {
[Stmt {
node: StmtKind::If { test, .. },
..
}] => test.location,
[stmt, ..] => stmt.location,
_ => return None,
};
let contents = locator.slice_source_code_range(&Range::new(start, end));
let range = lexer::make_tokenizer_located(&contents, start)
.flatten()
.find(|(_, kind, _)| matches!(kind, Tok::Elif | Tok::Else))
.map(|(location, _, end_location)| Range {
location,
end_location,
});
range
}
/// Return `true` if a `Stmt` appears to be part of a multi-statement line, with
/// other statements preceding it.
pub fn preceded_by_multi_statement_line(stmt: &Stmt, locator: &Locator) -> bool {
match_leading_content(stmt, locator) || preceded_by_continuation(stmt, locator)
pub fn preceded_by_continuation(stmt: &Stmt, indexer: &Indexer) -> bool {
stmt.location.row() > 1
&& indexer
.continuation_lines()
.contains(&(stmt.location.row() - 1))
}
/// Return `true` if a `Stmt` appears to be part of a multi-statement line, with
/// other statements preceding it.
pub fn preceded_by_multi_statement_line(stmt: &Stmt, locator: &Locator, indexer: &Indexer) -> bool {
match_leading_content(stmt, locator) || preceded_by_continuation(stmt, indexer)
}
/// Return `true` if a `Stmt` appears to be part of a multi-statement line, with
@@ -696,7 +766,6 @@ impl<'a> SimpleCallArgs<'a> {
}
/// Get the number of positional and keyword arguments used.
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.args.len() + self.kwargs.len()
}
@@ -708,7 +777,9 @@ mod tests {
use rustpython_ast::Location;
use rustpython_parser::parser;
use crate::ast::helpers::{else_range, identifier_range, match_trailing_content};
use crate::ast::helpers::{
elif_else_range, else_range, first_colon_range, identifier_range, match_trailing_content,
};
use crate::ast::types::Range;
use crate::source_code::Locator;
@@ -839,4 +910,54 @@ else:
assert_eq!(range.end_location.column(), 4);
Ok(())
}
#[test]
fn test_first_colon_range() {
let contents = "with a: pass";
let locator = Locator::new(contents);
let range = first_colon_range(
Range::new(Location::new(1, 0), Location::new(1, contents.len())),
&locator,
)
.unwrap();
assert_eq!(range.location.row(), 1);
assert_eq!(range.location.column(), 6);
assert_eq!(range.end_location.row(), 1);
assert_eq!(range.end_location.column(), 7);
}
#[test]
fn test_elif_else_range() -> Result<()> {
let contents = "
if a:
...
elif b:
...
"
.trim_start();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
let range = elif_else_range(stmt, &locator).unwrap();
assert_eq!(range.location.row(), 3);
assert_eq!(range.location.column(), 0);
assert_eq!(range.end_location.row(), 3);
assert_eq!(range.end_location.column(), 4);
let contents = "
if a:
...
else:
...
"
.trim_start();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
let range = elif_else_range(stmt, &locator).unwrap();
assert_eq!(range.location.row(), 3);
assert_eq!(range.location.column(), 0);
assert_eq!(range.end_location.row(), 3);
assert_eq!(range.end_location.column(), 4);
Ok(())
}
}

View File

@@ -96,7 +96,7 @@ impl<'a> Visitor<'a> for GlobalVisitor<'a> {
}
}
/// Extract a map from global name to its last-defining `Stmt`.
/// Extract a map from global name to its last-defining [`Stmt`].
pub fn extract_globals(body: &[Stmt]) -> FxHashMap<&str, &Stmt> {
let mut visitor = GlobalVisitor::default();
for stmt in body {
@@ -197,12 +197,12 @@ pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool {
pub type LocatedCmpop<U = ()> = Located<Cmpop, U>;
/// Extract all `Cmpop` operators from a source code snippet, with appropriate
/// Extract all [`Cmpop`] operators from a source code snippet, with appropriate
/// ranges.
///
/// `RustPython` doesn't include line and column information on `Cmpop` nodes.
/// `RustPython` doesn't include line and column information on [`Cmpop`] nodes.
/// `CPython` doesn't either. This method iterates over the token stream and
/// re-identifies `Cmpop` nodes, annotating them with valid ranges.
/// re-identifies [`Cmpop`] nodes, annotating them with valid ranges.
pub fn locate_cmpops(contents: &str) -> Vec<LocatedCmpop> {
let mut tok_iter = lexer::make_tokenizer(contents).flatten().peekable();
let mut ops: Vec<LocatedCmpop> = vec![];

View File

@@ -127,7 +127,7 @@ pub enum BindingKind<'a> {
pub struct Binding<'a> {
pub kind: BindingKind<'a>,
pub range: Range,
/// The statement in which the `Binding` was defined.
/// The statement in which the [`Binding`] was defined.
pub source: Option<RefEquality<'a, Stmt>>,
/// Tuple of (scope index, range) indicating the scope and range at which
/// the binding was last used.

View File

@@ -12,7 +12,7 @@ use crate::ast::whitespace::LinesWithTrailingNewline;
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_module;
use crate::fix::Fix;
use crate::source_code::Locator;
use crate::source_code::{Indexer, Locator};
/// Determine if a body contains only a single statement, taking into account
/// deleted.
@@ -79,7 +79,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
/// Return the location of a trailing semicolon following a `Stmt`, if it's part
/// of a multi-statement line.
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
let contents = locator.slice_source_code_at(&stmt.end_location.unwrap());
let contents = locator.slice_source_code_at(stmt.end_location.unwrap());
for (row, line) in LinesWithTrailingNewline::from(&contents).enumerate() {
let trimmed = line.trim();
if trimmed.starts_with(';') {
@@ -102,7 +102,7 @@ fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
/// Find the next valid break for a `Stmt` after a semicolon.
fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location {
let start_location = Location::new(semicolon.row(), semicolon.column() + 1);
let contents = locator.slice_source_code_at(&start_location);
let contents = locator.slice_source_code_at(start_location);
for (row, line) in LinesWithTrailingNewline::from(&contents).enumerate() {
let trimmed = line.trim();
// Skip past any continuations.
@@ -134,7 +134,7 @@ fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location {
/// Return `true` if a `Stmt` occurs at the end of a file.
fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool {
let contents = locator.slice_source_code_at(&stmt.end_location.unwrap());
let contents = locator.slice_source_code_at(stmt.end_location.unwrap());
contents.is_empty()
}
@@ -156,6 +156,7 @@ pub fn delete_stmt(
parent: Option<&Stmt>,
deleted: &[&Stmt],
locator: &Locator,
indexer: &Indexer,
) -> Result<Fix> {
if parent
.map(|parent| is_lone_child(stmt, parent, deleted))
@@ -175,7 +176,7 @@ pub fn delete_stmt(
Fix::deletion(stmt.location, next)
} else if helpers::match_leading_content(stmt, locator) {
Fix::deletion(stmt.location, stmt.end_location.unwrap())
} else if helpers::preceded_by_continuation(stmt, locator) {
} else if helpers::preceded_by_continuation(stmt, indexer) {
if is_end_of_file(stmt, locator) && stmt.location.column() == 0 {
// Special-case: a file can't end in a continuation.
Fix::replacement("\n".to_string(), stmt.location, stmt.end_location.unwrap())
@@ -198,6 +199,7 @@ pub fn remove_unused_imports<'a>(
parent: Option<&Stmt>,
deleted: &[&Stmt],
locator: &Locator,
indexer: &Indexer,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let mut tree = match_module(&module_text)?;
@@ -235,7 +237,7 @@ pub fn remove_unused_imports<'a>(
if !found_star {
bail!("Expected \'*\' for unused import");
}
return delete_stmt(stmt, parent, deleted, locator);
return delete_stmt(stmt, parent, deleted, locator, indexer);
} else {
bail!("Expected: ImportNames::Aliases | ImportNames::Star");
}
@@ -296,7 +298,7 @@ pub fn remove_unused_imports<'a>(
}
if aliases.is_empty() {
delete_stmt(stmt, parent, deleted, locator)
delete_stmt(stmt, parent, deleted, locator, indexer)
} else {
let mut state = CodegenState::default();
tree.codegen(&mut state);

View File

@@ -66,7 +66,7 @@ fn apply_fixes<'a>(
}
// Add the remaining content.
let slice = locator.slice_source_code_at(&last_pos);
let slice = locator.slice_source_code_at(last_pos);
output.append(&slice);
(Cow::from(output.finish()), num_fixed)

View File

@@ -29,20 +29,20 @@ use crate::python::future::ALL_FEATURE_NAMES;
use crate::python::typing;
use crate::python::typing::SubscriptKind;
use crate::registry::{Diagnostic, RuleCode};
use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_pie, flake8_print,
flake8_pytest_style, flake8_return, flake8_simplify, flake8_tidy_imports,
flake8_unused_arguments, mccabe, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes,
pygrep_hooks, pylint, pyupgrade, ruff,
};
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
use crate::source_code::{Locator, Stylist};
use crate::source_code::{Indexer, Locator, Stylist};
use crate::violations::DeferralKeyword;
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
use crate::{
autofix, docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez,
flake8_debugger, flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions,
flake8_pie, flake8_print, flake8_pytest_style, flake8_return, flake8_simplify,
flake8_tidy_imports, flake8_unused_arguments, mccabe, noqa, pandas_vet, pep8_naming,
pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, violations,
visibility,
};
use crate::{autofix, docstrings, noqa, violations, visibility};
const GLOBAL_SCOPE_INDEX: usize = 0;
@@ -57,7 +57,8 @@ pub struct Checker<'a> {
pub(crate) settings: &'a Settings,
pub(crate) noqa_line_for: &'a IntMap<usize, usize>,
pub(crate) locator: &'a Locator<'a>,
pub(crate) style: &'a Stylist<'a>,
pub(crate) stylist: &'a Stylist<'a>,
pub(crate) indexer: &'a Indexer,
// Computed diagnostics.
pub(crate) diagnostics: Vec<Diagnostic>,
// Function and class definition tracking (e.g., for docstring enforcement).
@@ -98,6 +99,7 @@ pub struct Checker<'a> {
}
impl<'a> Checker<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
settings: &'a Settings,
noqa_line_for: &'a IntMap<usize, usize>,
@@ -106,6 +108,7 @@ impl<'a> Checker<'a> {
path: &'a Path,
locator: &'a Locator,
style: &'a Stylist,
indexer: &'a Indexer,
) -> Checker<'a> {
Checker {
settings,
@@ -114,7 +117,8 @@ impl<'a> Checker<'a> {
noqa,
path,
locator,
style,
stylist: style,
indexer,
diagnostics: vec![],
definitions: vec![],
deletions: FxHashSet::default(),
@@ -697,6 +701,10 @@ where
flake8_pie::rules::dupe_class_field_definitions(self, stmt, body);
}
if self.settings.enabled.contains(&RuleCode::PIE796) {
flake8_pie::rules::prefer_unique_enums(self, stmt, body);
}
self.check_builtin_shadowing(name, stmt, false);
for expr in bases {
@@ -800,7 +808,7 @@ where
// flake8_tidy_imports
if self.settings.enabled.contains(&RuleCode::TID251) {
if let Some(diagnostic) =
flake8_tidy_imports::rules::name_or_parent_is_banned(
flake8_tidy_imports::banned_api::name_or_parent_is_banned(
alias,
&alias.node.name,
&self.settings.flake8_tidy_imports.banned_api,
@@ -944,16 +952,18 @@ where
if self.settings.enabled.contains(&RuleCode::TID251) {
if let Some(module) = module {
for name in names {
if let Some(diagnostic) = flake8_tidy_imports::rules::name_is_banned(
module,
name,
&self.settings.flake8_tidy_imports.banned_api,
) {
if let Some(diagnostic) =
flake8_tidy_imports::banned_api::name_is_banned(
module,
name,
&self.settings.flake8_tidy_imports.banned_api,
)
{
self.diagnostics.push(diagnostic);
}
}
if let Some(diagnostic) =
flake8_tidy_imports::rules::name_or_parent_is_banned(
flake8_tidy_imports::banned_api::name_or_parent_is_banned(
stmt,
module,
&self.settings.flake8_tidy_imports.banned_api,
@@ -1102,11 +1112,13 @@ where
}
if self.settings.enabled.contains(&RuleCode::TID252) {
if let Some(diagnostic) = flake8_tidy_imports::rules::banned_relative_import(
stmt,
level.as_ref(),
&self.settings.flake8_tidy_imports.ban_relative_imports,
) {
if let Some(diagnostic) =
flake8_tidy_imports::relative_imports::banned_relative_import(
stmt,
level.as_ref(),
&self.settings.flake8_tidy_imports.ban_relative_imports,
)
{
self.diagnostics.push(diagnostic);
}
}
@@ -1275,7 +1287,7 @@ where
}
}
}
StmtKind::With { items, body, .. } | StmtKind::AsyncWith { items, body, .. } => {
StmtKind::With { items, body, .. } => {
if self.settings.enabled.contains(&RuleCode::B017) {
flake8_bugbear::rules::assert_raises_exception(self, stmt, items);
}
@@ -1283,7 +1295,12 @@ where
flake8_pytest_style::rules::complex_raises(self, stmt, items, body);
}
if self.settings.enabled.contains(&RuleCode::SIM117) {
flake8_simplify::rules::multiple_with_statements(self, stmt);
flake8_simplify::rules::multiple_with_statements(
self,
stmt,
body,
self.current_stmt_parent().map(Into::into),
);
}
}
StmtKind::While { body, orelse, .. } => {
@@ -1834,7 +1851,7 @@ where
}
if self.settings.enabled.contains(&RuleCode::TID251) {
flake8_tidy_imports::rules::banned_attribute_access(self, expr);
flake8_tidy_imports::banned_api::banned_attribute_access(self, expr);
}
}
ExprKind::Call {
@@ -1850,6 +1867,7 @@ where
|| self.settings.enabled.contains(&RuleCode::F525)
// pyupgrade
|| self.settings.enabled.contains(&RuleCode::UP030)
|| self.settings.enabled.contains(&RuleCode::UP032)
{
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if let ExprKind::Constant {
@@ -1873,8 +1891,8 @@ where
}
Ok(summary) => {
if self.settings.enabled.contains(&RuleCode::F522) {
pyflakes::rules::string_dot_format_extra_named_arguments(self,
&summary, keywords, location,
pyflakes::rules::string_dot_format_extra_named_arguments(
self, &summary, keywords, location,
);
}
@@ -1900,6 +1918,10 @@ where
if self.settings.enabled.contains(&RuleCode::UP030) {
pyupgrade::rules::format_literals(self, &summary, expr);
}
if self.settings.enabled.contains(&RuleCode::UP032) {
pyupgrade::rules::f_strings(self, &summary, expr);
}
}
}
}
@@ -3996,6 +4018,7 @@ impl<'a> Checker<'a> {
parent,
&deleted,
self.locator,
self.indexer,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
@@ -4291,6 +4314,7 @@ pub fn check_ast(
python_ast: &Suite,
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
noqa_line_for: &IntMap<usize, usize>,
settings: &Settings,
autofix: flags::Autofix,
@@ -4305,6 +4329,7 @@ pub fn check_ast(
path,
locator,
stylist,
indexer,
);
checker.push_scope(Scope::new(ScopeKind::Module));
checker.bind_builtins();

View File

@@ -6,16 +6,17 @@ use rustpython_parser::ast::Suite;
use crate::ast::visitor::Visitor;
use crate::directives::IsortDirectives;
use crate::isort;
use crate::isort::track::{Block, ImportTracker};
use crate::registry::{Diagnostic, RuleCode};
use crate::rules::isort;
use crate::rules::isort::track::{Block, ImportTracker};
use crate::settings::{flags, Settings};
use crate::source_code::{Locator, Stylist};
use crate::source_code::{Indexer, Locator, Stylist};
#[allow(clippy::too_many_arguments)]
pub fn check_imports(
python_ast: &Suite,
locator: &Locator,
indexer: &Indexer,
directives: &IsortDirectives,
settings: &Settings,
stylist: &Stylist,
@@ -39,7 +40,7 @@ pub fn check_imports(
for block in &blocks {
if !block.imports.is_empty() {
if let Some(diagnostic) = isort::rules::organize_imports(
block, locator, settings, stylist, autofix, package,
block, locator, indexer, settings, stylist, autofix, package,
) {
diagnostics.push(diagnostic);
}

View File

@@ -1,9 +1,11 @@
//! Lint rules based on checking raw physical lines.
use crate::pycodestyle::rules::{doc_line_too_long, line_too_long, no_newline_at_end_of_file};
use crate::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
use crate::pyupgrade::rules::unnecessary_coding_comment;
use crate::registry::{Diagnostic, RuleCode};
use crate::rules::pycodestyle::rules::{
doc_line_too_long, line_too_long, no_newline_at_end_of_file,
};
use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
use crate::settings::{flags, Settings};
pub fn check_lines(

View File

@@ -4,10 +4,12 @@ use rustpython_parser::lexer::{LexResult, Tok};
use crate::lex::docstring_detection::StateMachine;
use crate::registry::{Diagnostic, RuleCode};
use crate::ruff::rules::Context;
use crate::rules::ruff::rules::Context;
use crate::rules::{
eradicate, flake8_commas, flake8_implicit_str_concat, flake8_quotes, pycodestyle, ruff,
};
use crate::settings::{flags, Settings};
use crate::source_code::Locator;
use crate::{eradicate, flake8_implicit_str_concat, flake8_quotes, pycodestyle, ruff};
pub fn check_tokens(
locator: &Locator,
@@ -28,6 +30,9 @@ pub fn check_tokens(
let enforce_invalid_escape_sequence = settings.enabled.contains(&RuleCode::W605);
let enforce_implicit_string_concatenation = settings.enabled.contains(&RuleCode::ISC001)
|| settings.enabled.contains(&RuleCode::ISC002);
let enforce_trailing_comma = settings.enabled.contains(&RuleCode::COM812)
|| settings.enabled.contains(&RuleCode::COM818)
|| settings.enabled.contains(&RuleCode::COM819);
let mut state_machine = StateMachine::default();
for &(start, ref tok, end) in tokens.iter().flatten() {
@@ -105,7 +110,16 @@ pub fn check_tokens(
// ISC001, ISC002
if enforce_implicit_string_concatenation {
diagnostics.extend(
flake8_implicit_str_concat::rules::implicit(tokens, locator)
flake8_implicit_str_concat::rules::implicit(tokens)
.into_iter()
.filter(|diagnostic| settings.enabled.contains(diagnostic.kind.code())),
);
}
// COM812, COM818, COM819
if enforce_trailing_comma {
diagnostics.extend(
flake8_commas::rules::trailing_commas(tokens, locator)
.into_iter()
.filter(|diagnostic| settings.enabled.contains(diagnostic.kind.code())),
);

View File

@@ -37,14 +37,12 @@ pub struct IsortDirectives {
}
pub struct Directives {
pub commented_lines: Vec<usize>,
pub noqa_line_for: IntMap<usize, usize>,
pub isort: IsortDirectives,
}
pub fn extract_directives(lxr: &[LexResult], flags: Flags) -> Directives {
Directives {
commented_lines: extract_commented_lines(lxr),
noqa_line_for: if flags.contains(Flags::NOQA) {
extract_noqa_line_for(lxr)
} else {
@@ -58,16 +56,6 @@ pub fn extract_directives(lxr: &[LexResult], flags: Flags) -> Directives {
}
}
pub fn extract_commented_lines(lxr: &[LexResult]) -> Vec<usize> {
let mut commented_lines = Vec::new();
for (start, tok, ..) in lxr.iter().flatten() {
if matches!(tok, Tok::Comment(_)) {
commented_lines.push(start.row());
}
}
commented_lines
}
/// Extract a mapping from logical line to noqa line.
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
let mut noqa_line_for: IntMap<usize, usize> = IntMap::default();

View File

@@ -7,3 +7,7 @@ pub const TRIPLE_QUOTE_PREFIXES: &[&str] = &[
pub const SINGLE_QUOTE_PREFIXES: &[&str] = &[
"u\"", "u'", "r\"", "r'", "u\"", "u'", "r\"", "r'", "U\"", "U'", "R\"", "R'", "\"", "'",
];
pub const TRIPLE_QUOTE_SUFFIXES: &[&str] = &["\"\"\"", "'''"];
pub const SINGLE_QUOTE_SUFFIXES: &[&str] = &["\"", "'"];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
use rustpython_ast::{Stmt, StmtKind};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violations;
/// SIM117
pub fn multiple_with_statements(checker: &mut Checker, stmt: &Stmt) {
let StmtKind::With { body, .. } = &stmt.node else {
return;
};
if body.len() != 1 {
return;
}
if matches!(body[0].node, StmtKind::With { .. }) {
checker.diagnostics.push(Diagnostic::new(
violations::MultipleWithStatements,
Range::from_located(stmt),
));
}
}

View File

@@ -1,15 +0,0 @@
---
source: src/flake8_simplify/mod.rs
expression: checks
---
- kind:
MultipleWithStatements: ~
location:
row: 1
column: 0
end_location:
row: 3
column: 22
fix: ~
parent: ~

View File

@@ -1,77 +0,0 @@
pub(crate) mod rules;
pub mod settings;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use rustc_hash::FxHashMap;
use crate::flake8_tidy_imports;
use crate::flake8_tidy_imports::settings::{BannedApi, Strictness};
use crate::linter::test_path;
use crate::registry::RuleCode;
use crate::settings::Settings;
#[test]
fn ban_parent_imports() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_tidy_imports/TID252.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
ban_relative_imports: Strictness::Parents,
..Default::default()
},
..Settings::for_rules(vec![RuleCode::TID252])
},
)?;
insta::assert_yaml_snapshot!(diagnostics);
Ok(())
}
#[test]
fn ban_all_imports() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_tidy_imports/TID252.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
ban_relative_imports: Strictness::All,
..Default::default()
},
..Settings::for_rules(vec![RuleCode::TID252])
},
)?;
insta::assert_yaml_snapshot!(diagnostics);
Ok(())
}
#[test]
fn banned_api_true_positives() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_tidy_imports/TID251.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
banned_api: FxHashMap::from_iter([
(
"cgi".to_string(),
BannedApi {
msg: "The cgi module is deprecated.".to_string(),
},
),
(
"typing.TypedDict".to_string(),
BannedApi {
msg: "Use typing_extensions.TypedDict instead.".to_string(),
},
),
]),
..Default::default()
},
..Settings::for_rules(vec![RuleCode::TID251])
},
)?;
insta::assert_yaml_snapshot!(diagnostics);
Ok(())
}
}

View File

@@ -1,92 +0,0 @@
use rustc_hash::FxHashMap;
use rustpython_ast::{Alias, Expr, Located, Stmt};
use super::settings::BannedApi;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::flake8_tidy_imports::settings::Strictness;
use crate::registry::Diagnostic;
use crate::violations;
/// TID252
pub fn banned_relative_import(
stmt: &Stmt,
level: Option<&usize>,
strictness: &Strictness,
) -> Option<Diagnostic> {
let strictness_level = match strictness {
Strictness::All => 0,
Strictness::Parents => 1,
};
if level? > &strictness_level {
Some(Diagnostic::new(
violations::BannedRelativeImport(strictness.clone()),
Range::from_located(stmt),
))
} else {
None
}
}
/// TID251
pub fn name_is_banned(
module: &str,
name: &Alias,
banned_apis: &FxHashMap<String, BannedApi>,
) -> Option<Diagnostic> {
let full_name = format!("{module}.{}", &name.node.name);
if let Some(ban) = banned_apis.get(&full_name) {
return Some(Diagnostic::new(
violations::BannedApi {
name: full_name,
message: ban.msg.to_string(),
},
Range::from_located(name),
));
}
None
}
/// TID251
pub fn name_or_parent_is_banned<T>(
located: &Located<T>,
name: &str,
banned_apis: &FxHashMap<String, BannedApi>,
) -> Option<Diagnostic> {
let mut name = name;
loop {
if let Some(ban) = banned_apis.get(name) {
return Some(Diagnostic::new(
violations::BannedApi {
name: name.to_string(),
message: ban.msg.to_string(),
},
Range::from_located(located),
));
}
match name.rfind('.') {
Some(idx) => {
name = &name[..idx];
}
None => return None,
}
}
}
/// TID251
pub fn banned_attribute_access(checker: &mut Checker, expr: &Expr) {
if let Some(call_path) = checker.resolve_call_path(expr) {
for (banned_path, ban) in &checker.settings.flake8_tidy_imports.banned_api {
if call_path == banned_path.split('.').collect::<Vec<_>>() {
checker.diagnostics.push(Diagnostic::new(
violations::BannedApi {
name: banned_path.to_string(),
message: ban.msg.to_string(),
},
Range::from_located(expr),
));
return;
}
}
}
}

View File

@@ -3,9 +3,10 @@
use std::path::Path;
use anyhow::Result;
use ruff::settings::types::PythonVersion;
use serde::{Deserialize, Serialize};
use crate::settings::types::PythonVersion;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Black {
#[serde(alias = "line-length", alias = "line_length")]

View File

@@ -2,23 +2,24 @@ use std::collections::{BTreeSet, HashMap};
use anyhow::Result;
use colored::Colorize;
use ruff::flake8_pytest_style::types::{
use super::black::Black;
use super::plugin::Plugin;
use super::{parser, plugin};
use crate::registry::RuleCodePrefix;
use crate::rules::flake8_pytest_style::types::{
ParametrizeNameType, ParametrizeValuesRowType, ParametrizeValuesType,
};
use ruff::flake8_quotes::settings::Quote;
use ruff::flake8_tidy_imports::settings::Strictness;
use ruff::pydocstyle::settings::Convention;
use ruff::registry::RuleCodePrefix;
use ruff::settings::options::Options;
use ruff::settings::pyproject::Pyproject;
use ruff::{
use crate::rules::flake8_quotes::settings::Quote;
use crate::rules::flake8_tidy_imports::relative_imports::Strictness;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_pytest_style, flake8_quotes,
flake8_tidy_imports, mccabe, pep8_naming, pydocstyle, warn_user,
flake8_tidy_imports, mccabe, pep8_naming, pydocstyle,
};
use crate::black::Black;
use crate::plugin::Plugin;
use crate::{parser, plugin};
use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject;
use crate::warn_user;
pub fn convert(
config: &HashMap<String, HashMap<String, Option<String>>>,
@@ -92,7 +93,7 @@ pub fn convert(
let mut flake8_errmsg = flake8_errmsg::settings::Options::default();
let mut flake8_pytest_style = flake8_pytest_style::settings::Options::default();
let mut flake8_quotes = flake8_quotes::settings::Options::default();
let mut flake8_tidy_imports = flake8_tidy_imports::settings::Options::default();
let mut flake8_tidy_imports = flake8_tidy_imports::options::Options::default();
let mut mccabe = mccabe::settings::Options::default();
let mut pep8_naming = pep8_naming::settings::Options::default();
let mut pydocstyle = pydocstyle::settings::Options::default();
@@ -270,7 +271,7 @@ pub fn convert(
match value.trim() {
"csv" => {
flake8_pytest_style.parametrize_names_type =
Some(ParametrizeNameType::CSV);
Some(ParametrizeNameType::Csv);
}
"tuple" => {
flake8_pytest_style.parametrize_names_type =
@@ -353,7 +354,7 @@ pub fn convert(
if flake8_quotes != flake8_quotes::settings::Options::default() {
options.flake8_quotes = Some(flake8_quotes);
}
if flake8_tidy_imports != flake8_tidy_imports::settings::Options::default() {
if flake8_tidy_imports != flake8_tidy_imports::options::Options::default() {
options.flake8_tidy_imports = Some(flake8_tidy_imports);
}
if mccabe != mccabe::settings::Options::default() {
@@ -388,14 +389,14 @@ mod tests {
use std::collections::HashMap;
use anyhow::Result;
use ruff::pydocstyle::settings::Convention;
use ruff::registry::RuleCodePrefix;
use ruff::settings::options::Options;
use ruff::settings::pyproject::Pyproject;
use ruff::{flake8_quotes, pydocstyle};
use crate::converter::convert;
use crate::plugin::Plugin;
use super::super::plugin::Plugin;
use super::convert;
use crate::registry::RuleCodePrefix;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{flake8_quotes, pydocstyle};
use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject;
#[test]
fn it_converts_empty() -> Result<()> {
@@ -423,6 +424,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
@@ -488,6 +490,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: Some(100),
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
@@ -553,6 +556,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: Some(100),
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
@@ -618,6 +622,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
@@ -683,6 +688,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
@@ -756,6 +762,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
@@ -824,6 +831,7 @@ mod tests {
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,

View File

@@ -0,0 +1,8 @@
mod black;
mod converter;
mod parser;
mod plugin;
pub use black::parse_black_options;
pub use converter::convert;
pub use plugin::Plugin;

View File

@@ -4,11 +4,12 @@ use anyhow::{bail, Result};
use colored::Colorize;
use once_cell::sync::Lazy;
use regex::Regex;
use ruff::registry::{RuleCodePrefix, PREFIX_REDIRECTS};
use ruff::settings::types::PatternPrefixPair;
use ruff::warn_user;
use rustc_hash::FxHashMap;
use crate::registry::{RuleCodePrefix, PREFIX_REDIRECTS};
use crate::settings::types::PatternPrefixPair;
use crate::warn_user;
static COMMA_SEPARATED_LIST_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
/// Parse a comma-separated list of `RuleCodePrefix` values (e.g.,
@@ -203,10 +204,10 @@ pub fn collect_per_file_ignores(
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff::registry::RuleCodePrefix;
use ruff::settings::types::PatternPrefixPair;
use crate::parser::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
use super::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
use crate::registry::RuleCodePrefix;
use crate::settings::types::PatternPrefixPair;
#[test]
fn it_parses_prefix_codes() {

View File

@@ -3,7 +3,8 @@ use std::fmt;
use std::str::FromStr;
use anyhow::anyhow;
use ruff::registry::RuleCodePrefix;
use crate::registry::RuleCodePrefix;
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Plugin {
@@ -298,7 +299,7 @@ pub fn resolve_select(plugins: &[Plugin]) -> BTreeSet<RuleCodePrefix> {
mod tests {
use std::collections::HashMap;
use crate::plugin::{infer_plugins_from_options, Plugin};
use super::{infer_plugins_from_options, Plugin};
#[test]
fn it_infers_plugins() {

View File

@@ -4,11 +4,11 @@ use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use globset::GlobMatcher;
use path_absolutize::{path_dedot, Absolutize};
use rustc_hash::FxHashSet;
use crate::registry::RuleCode;
use crate::settings::hashable::{HashableGlobMatcher, HashableHashSet};
/// Extract the absolute path and basename (as strings) from a Path.
pub fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
@@ -26,7 +26,11 @@ pub fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
/// Create a set with codes matching the pattern/code pairs.
pub(crate) fn ignores_from_path<'a>(
path: &Path,
pattern_code_pairs: &'a [(GlobMatcher, GlobMatcher, FxHashSet<RuleCode>)],
pattern_code_pairs: &'a [(
HashableGlobMatcher,
HashableGlobMatcher,
HashableHashSet<RuleCode>,
)],
) -> Result<FxHashSet<&'a RuleCode>> {
let (file_path, file_basename) = extract_path_names(path)?;
Ok(pattern_code_pairs
@@ -34,7 +38,7 @@ pub(crate) fn ignores_from_path<'a>(
.filter(|(absolute, basename, _)| {
basename.is_match(file_basename) || absolute.is_match(file_path)
})
.flat_map(|(_, _, codes)| codes)
.flat_map(|(_, _, codes)| codes.iter())
.collect())
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,7 +43,10 @@ impl StateMachine {
}
pub fn consume(&mut self, tok: &Tok) -> bool {
if matches!(tok, Tok::Newline | Tok::Indent | Tok::Dedent) {
if matches!(
tok,
Tok::NonLogicalNewline | Tok::Newline | Tok::Indent | Tok::Dedent | Tok::Comment(..)
) {
return false;
}

View File

@@ -1,3 +1,9 @@
//! This is the library for the [Ruff] Python linter.
//!
//! **The API is currently completely unstable**
//! and subject to change drastically.
//!
//! [Ruff]: https://github.com/charliermarsh/ruff
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
@@ -14,66 +20,36 @@
extern crate core;
pub mod ast;
pub mod autofix;
mod ast;
mod autofix;
pub mod cache;
mod checkers;
mod cst;
pub mod directives;
mod directives;
mod doc_lines;
mod docstrings;
mod eradicate;
pub mod fix;
mod flake8_2020;
pub mod flake8_annotations;
pub mod flake8_bandit;
mod flake8_blind_except;
pub mod flake8_boolean_trap;
pub mod flake8_bugbear;
mod flake8_builtins;
mod flake8_comprehensions;
mod flake8_datetimez;
mod flake8_debugger;
pub mod flake8_errmsg;
mod flake8_implicit_str_concat;
mod flake8_import_conventions;
pub mod flake8_pie;
mod flake8_print;
pub mod flake8_pytest_style;
pub mod flake8_quotes;
mod flake8_return;
mod flake8_simplify;
pub mod flake8_tidy_imports;
mod flake8_unused_arguments;
pub mod flake8_to_ruff;
pub mod fs;
mod isort;
mod lex;
pub mod linter;
pub mod logging;
pub mod mccabe;
pub mod message;
mod noqa;
mod pandas_vet;
pub mod pep8_naming;
mod pycodestyle;
pub mod pydocstyle;
mod pyflakes;
mod pygrep_hooks;
mod pylint;
mod python;
mod pyupgrade;
pub mod registry;
pub mod resolver;
mod ruff;
pub mod rustpython_helpers;
mod rules;
mod rustpython_helpers;
pub mod settings;
pub mod source_code;
mod vendor;
mod violation;
pub mod violations;
mod violations;
mod visibility;
use cfg_if::cfg_if;
pub use violations::IOError;
cfg_if! {
if #[cfg(not(target_family = "wasm"))] {

View File

@@ -10,17 +10,17 @@ use crate::resolver::Relativity;
use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
use crate::settings::{flags, pyproject, Settings};
use crate::source_code::{Locator, Stylist};
use crate::source_code::{Indexer, Locator, Stylist};
use crate::{directives, packaging, resolver};
/// Load the relevant `Settings` for a given `Path`.
fn resolve(path: &Path) -> Result<Settings> {
if let Some(pyproject) = pyproject::find_settings_toml(path)? {
// First priority: `pyproject.toml` in the current `Path`.
resolver::resolve_settings(&pyproject, &Relativity::Parent)
Ok(resolver::resolve_settings(&pyproject, &Relativity::Parent)?.lib)
} else if let Some(pyproject) = pyproject::find_user_settings_toml() {
// Second priority: user-specific `pyproject.toml`.
resolver::resolve_settings(&pyproject, &Relativity::Cwd)
Ok(resolver::resolve_settings(&pyproject, &Relativity::Cwd)?.lib)
} else {
// Fallback: default settings.
Settings::from_configuration(Configuration::default(), &path_dedot::CWD)
@@ -44,6 +44,9 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Diagnosti
// Detect the current code style (lazily).
let stylist = Stylist::from_contents(contents, &locator);
// Extra indices from the code.
let indexer: Indexer = tokens.as_slice().into();
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(&settings));
@@ -51,11 +54,12 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Diagnosti
// Generate diagnostics.
let diagnostics = check_path(
path,
packaging::detect_package_root(path),
packaging::detect_package_root(path, &settings.namespace_packages),
contents,
tokens,
&locator,
&stylist,
&indexer,
&directives,
&settings,
autofix.into(),

View File

@@ -5,19 +5,20 @@ use rustpython_parser::lexer::LexResult;
use serde::Serialize;
use wasm_bindgen::prelude::*;
use crate::directives;
use crate::linter::check_path;
use crate::registry::{RuleCode, RuleCodePrefix};
use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_errmsg, flake8_import_conventions,
flake8_pytest_style, flake8_quotes, flake8_tidy_imports, flake8_unused_arguments, isort,
mccabe, pep8_naming, pycodestyle, pydocstyle, pyupgrade,
};
use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
use crate::settings::options::Options;
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
use crate::source_code::{Locator, Stylist};
use crate::{
directives, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_errmsg,
flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_tidy_imports,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyupgrade,
};
use crate::source_code::{Indexer, Locator, Stylist};
const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -105,14 +106,15 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
force_exclude: None,
format: None,
ignore_init_module_imports: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
show_source: None,
src: None,
unfixable: None,
typing_modules: None,
task_tags: None,
typing_modules: None,
unfixable: None,
update_check: None,
// Use default options for all plugins.
flake8_annotations: Some(flake8_annotations::settings::Settings::default().into()),
@@ -121,7 +123,7 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()),
flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()),
flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()),
flake8_tidy_imports: Some(flake8_tidy_imports::settings::Settings::default().into()),
flake8_tidy_imports: Some(flake8_tidy_imports::Settings::default().into()),
flake8_import_conventions: Some(
flake8_import_conventions::settings::Settings::default().into(),
),
@@ -155,6 +157,9 @@ pub fn check(contents: &str, options: JsValue) -> Result<JsValue, JsValue> {
// Detect the current code style (lazily).
let stylist = Stylist::from_contents(contents, &locator);
// Extra indices from the code.
let indexer: Indexer = tokens.as_slice().into();
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = directives::extract_directives(&tokens, directives::Flags::empty());
@@ -166,6 +171,7 @@ pub fn check(contents: &str, options: JsValue) -> Result<JsValue, JsValue> {
tokens,
&locator,
&stylist,
&indexer,
&directives,
&settings,
flags::Autofix::Enabled,

View File

@@ -17,7 +17,7 @@ use crate::message::{Message, Source};
use crate::noqa::add_noqa;
use crate::registry::{Diagnostic, LintSource, RuleCode};
use crate::settings::{flags, Settings};
use crate::source_code::{Locator, Stylist};
use crate::source_code::{Indexer, Locator, Stylist};
use crate::{directives, fs, rustpython_helpers, violations};
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
@@ -33,6 +33,7 @@ pub fn check_path(
tokens: Vec<LexResult>,
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
directives: &Directives,
settings: &Settings,
autofix: flags::Autofix,
@@ -65,7 +66,7 @@ pub fn check_path(
let use_ast = settings
.enabled
.iter()
.any(|rule_code| matches!(rule_code.lint_source(), LintSource::AST));
.any(|rule_code| matches!(rule_code.lint_source(), LintSource::Ast));
let use_imports = !directives.isort.skip_file
&& settings
.enabled
@@ -79,6 +80,7 @@ pub fn check_path(
&python_ast,
locator,
stylist,
indexer,
&directives.noqa_line_for,
settings,
autofix,
@@ -90,6 +92,7 @@ pub fn check_path(
diagnostics.extend(check_imports(
&python_ast,
locator,
indexer,
&directives.isort,
settings,
stylist,
@@ -127,7 +130,7 @@ pub fn check_path(
{
diagnostics.extend(check_lines(
contents,
&directives.commented_lines,
indexer.commented_lines(),
&doc_lines,
settings,
autofix,
@@ -135,16 +138,16 @@ pub fn check_path(
}
// Enforce `noqa` directives.
if matches!(noqa, flags::Noqa::Enabled)
if (matches!(noqa, flags::Noqa::Enabled) && !diagnostics.is_empty())
|| settings
.enabled
.iter()
.any(|rule_code| matches!(rule_code.lint_source(), LintSource::NoQA))
.any(|rule_code| matches!(rule_code.lint_source(), LintSource::NoQa))
{
check_noqa(
&mut diagnostics,
contents,
&directives.commented_lines,
indexer.commented_lines(),
&directives.noqa_line_for,
settings,
autofix,
@@ -184,6 +187,9 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
// Detect the current code style (lazily).
let stylist = Stylist::from_contents(&contents, &locator);
// Extra indices from the code.
let indexer: Indexer = tokens.as_slice().into();
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
@@ -196,6 +202,7 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
tokens,
&locator,
&stylist,
&indexer,
&directives,
settings,
flags::Autofix::Disabled,
@@ -230,6 +237,9 @@ pub fn lint_only(
// Detect the current code style (lazily).
let stylist = Stylist::from_contents(contents, &locator);
// Extra indices from the code.
let indexer: Indexer = tokens.as_slice().into();
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
@@ -242,6 +252,7 @@ pub fn lint_only(
tokens,
&locator,
&stylist,
&indexer,
&directives,
settings,
autofix,
@@ -290,6 +301,9 @@ pub fn lint_fix(
// Detect the current code style (lazily).
let stylist = Stylist::from_contents(&contents, &locator);
// Extra indices from the code.
let indexer: Indexer = tokens.as_slice().into();
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
@@ -302,6 +316,7 @@ pub fn lint_fix(
tokens,
&locator,
&stylist,
&indexer,
&directives,
settings,
flags::Autofix::Enabled,
@@ -366,6 +381,7 @@ pub fn test_path(path: &Path, settings: &Settings) -> Result<Vec<Diagnostic>> {
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
let locator = Locator::new(&contents);
let stylist = Stylist::from_contents(&contents, &locator);
let indexer: Indexer = tokens.as_slice().into();
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
let mut diagnostics = check_path(
@@ -375,6 +391,7 @@ pub fn test_path(path: &Path, settings: &Settings) -> Result<Vec<Diagnostic>> {
tokens,
&locator,
&stylist,
&indexer,
&directives,
settings,
flags::Autofix::Enabled,
@@ -395,6 +412,7 @@ pub fn test_path(path: &Path, settings: &Settings) -> Result<Vec<Diagnostic>> {
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
let locator = Locator::new(&contents);
let stylist = Stylist::from_contents(&contents, &locator);
let indexer: Indexer = tokens.as_slice().into();
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
let diagnostics = check_path(
@@ -404,6 +422,7 @@ pub fn test_path(path: &Path, settings: &Settings) -> Result<Vec<Diagnostic>> {
tokens,
&locator,
&stylist,
&indexer,
&directives,
settings,
flags::Autofix::Enabled,

View File

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

View File

@@ -9,6 +9,7 @@ use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::registry::{Diagnostic, RuleCode, CODE_REDIRECTS};
use crate::settings::hashable::HashableHashSet;
use crate::source_code::LineEnding;
static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
@@ -84,7 +85,7 @@ pub fn add_noqa(
diagnostics: &[Diagnostic],
contents: &str,
noqa_line_for: &IntMap<usize, usize>,
external: &FxHashSet<String>,
external: &HashableHashSet<String>,
line_ending: &LineEnding,
) -> Result<usize> {
let (count, output) =
@@ -97,7 +98,7 @@ fn add_noqa_inner(
diagnostics: &[Diagnostic],
contents: &str,
noqa_line_for: &IntMap<usize, usize>,
external: &FxHashSet<String>,
external: &HashableHashSet<String>,
line_ending: &LineEnding,
) -> (usize, String) {
let mut matches_by_line: FxHashMap<usize, FxHashSet<&RuleCode>> = FxHashMap::default();
@@ -208,12 +209,12 @@ fn add_noqa_inner(
#[cfg(test)]
mod tests {
use nohash_hasher::IntMap;
use rustc_hash::FxHashSet;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::noqa::{add_noqa_inner, NOQA_LINE_REGEX};
use crate::registry::Diagnostic;
use crate::settings::hashable::HashableHashSet;
use crate::source_code::LineEnding;
use crate::violations;
@@ -236,7 +237,7 @@ mod tests {
let diagnostics = vec![];
let contents = "x = 1";
let noqa_line_for = IntMap::default();
let external = FxHashSet::default();
let external = HashableHashSet::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
@@ -253,7 +254,7 @@ mod tests {
)];
let contents = "x = 1";
let noqa_line_for = IntMap::default();
let external = FxHashSet::default();
let external = HashableHashSet::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
@@ -276,7 +277,7 @@ mod tests {
];
let contents = "x = 1 # noqa: E741\n";
let noqa_line_for = IntMap::default();
let external = FxHashSet::default();
let external = HashableHashSet::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
@@ -299,7 +300,7 @@ mod tests {
];
let contents = "x = 1 # noqa";
let noqa_line_for = IntMap::default();
let external = FxHashSet::default();
let external = HashableHashSet::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,

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