Compare commits

...

231 Commits

Author SHA1 Message Date
Charlie Marsh
1c3265ef98 Bump version to 0.0.231 2023-01-23 12:51:09 -05:00
Maksudul Haque
8001a1639c [flake8-bandit] Added Rule S612 (Use of insecure logging.config.listen) (#2108)
ref: https://github.com/charliermarsh/ruff/issues/1646
2023-01-23 12:37:33 -05:00
Charlie Marsh
7d9c1d7a5a Add a note on some isort incompatibilities 2023-01-23 12:32:35 -05:00
Thomas MK
c5cebb106e Fix outdated description of ruff's support of isort settings (#2106)
Ruff supports more than `known-first-party`, `known-third-party`, `extra-standard-library`, and `src` nowadays.

Not sure if this is the best wording. Suggestions welcome!
2023-01-23 12:29:44 -05:00
Martin Fischer
8c61e8a1ef Improve #[derive(RuleNamespace)] error handling 2023-01-23 12:20:10 -05:00
Martin Fischer
4f338273a5 refactor: Simplify test_ruff_black_compatibility 2023-01-23 12:20:10 -05:00
Martin Fischer
648191652d refactor: Collect into Result<Vec<_>, _> 2023-01-23 12:20:10 -05:00
Martin Fischer
90558609c3 refactor: Introduce parse_doc_attr helper function 2023-01-23 12:20:10 -05:00
Martin Fischer
991d3c1ef6 refactor: Move Linter::url and Linter::name generation to proc macro
This lets us get rid of the build.rs script and results
in more developer-friendly compile error messages.
2023-01-23 12:20:10 -05:00
Simon Brugman
f472fbc6d4 docs(readme): add pypa cibuildwheel (#2107) 2023-01-23 11:39:23 -05:00
Charlie Marsh
09b65a6449 Remove some usages of default format for expressions (#2100) 2023-01-22 23:15:43 -05:00
Charlie Marsh
9d2eced941 Add flake8-simplify to CONTRIBUTING.md 2023-01-22 21:46:52 -05:00
Charlie Marsh
be0f6acb40 Change contributing to point to tryceratops 2023-01-22 21:45:20 -05:00
Steve Dignam
0c624af036 Add flake8-pie PIE800: no-unnecessary-spread (#1881)
Checks for unnecessary spreads, like `{**foo, **{"bar": True}}`
rel: https://github.com/charliermarsh/ruff/issues/1879
rel: https://github.com/charliermarsh/ruff/issues/1543
2023-01-22 21:43:34 -05:00
Steve Dignam
4ca328f964 Add flake8-pie PIE804: no-unnecessary-dict-kwargs (#1884)
Warn about things like `foo(**{"bar": True})` which is equivalent to `foo(bar=True)`

rel: https://github.com/charliermarsh/ruff/issues/1879
rel: https://github.com/charliermarsh/ruff/issues/1543
2023-01-22 21:32:45 -05:00
Charlie Marsh
07b5bf7030 Remove misleading emoji comment 2023-01-22 21:23:55 -05:00
Charlie Marsh
f40ae943a7 Fix bad documentation message for init option 2023-01-22 19:25:23 -05:00
Charlie Marsh
8d46d3bfa6 Avoid nested-if violations when outer-if has else clause (#2095)
It looks like we need `do`-`while`-like semantics here with an additional outer check.

Closes #2094.
2023-01-22 17:40:56 -05:00
alm
4fb0c6e3ad feat: Implement TRY201 (#2073) 2023-01-22 17:08:57 -05:00
Simon Brugman
ebfdefd110 refactor: remove redundant enum (#2091) 2023-01-22 15:27:08 -05:00
Simon Brugman
11f06055a0 feat: flake8-use-pathlib PTH100-124 (#2090) 2023-01-22 15:17:25 -05:00
Ville Skyttä
6a6a792562 fix: issue D401 only for non-test/property functions and methods (#2071)
Extend test fixture to verify the targeting.

Includes two "attribute docstrings" which per PEP 257 are not recognized by the Python bytecode compiler or available as runtime object attributes. They are not available for us either at time of writing, but include them for completeness anyway in case they one day are.
2023-01-22 14:24:59 -05:00
Charlie Marsh
23b622943e Bump version to 0.0.230 2023-01-22 13:58:41 -05:00
Harutaka Kawamura
a7ce8621a9 Update RustPython to fix Dict.keys type (#2086)
This PR upgrades RustPython to fix the type of `Dict.keys` to `Vec<Option<Expr>>` (see https://github.com/RustPython/RustPython/pull/4449 for why this change was needed) and unblock #1884.
2023-01-22 13:24:00 -05:00
Shannon Rothe
36fb8f7a63 flake8_to_ruff: support isort options (#2082)
See: https://github.com/charliermarsh/ruff/issues/1749.
2023-01-22 13:18:01 -05:00
alm
e11cf1bf65 Update linters PyPI links to latest version (#2062) 2023-01-22 13:10:22 -05:00
Maksudul Haque
75e16c0ce5 [pep8-naming][N806] Don't mark TypeVar & NewType Assignment as Errors (#2085)
closes https://github.com/charliermarsh/ruff/issues/1985
2023-01-22 12:54:13 -05:00
Martin Fischer
1beedf20f9 fix: add_rule.py for --linter ruff 2023-01-22 11:51:29 -05:00
Martin Fischer
4758ee6ac4 refactor: Generate Linter -> RuleSelector mapping via macro
To enable ruff_dev to automatically generate the rule Markdown tables in
the README the ruff library contained the following function:

    Linter::codes() -> Vec<RuleSelector>

which was slightly changed to `fn prefixes(&self) -> Prefixes` in
9dc66b5a65 to enable ruff_dev to split
up the Markdown tables for linters that have multiple prefixes
(pycodestyle has E & W, Pylint has PLC, PLE, PLR & PLW).

The definition of this method was however largely redundant with the
#[prefix] macro attributes in the Linter enum, which are used to
derive the Linter::parse_code function, used by the --explain command.

This commit removes the redundant Linter::prefixes by introducing a
same-named method with a different signature to the RuleNamespace trait:

     fn prefixes(&self) -> &'static [&'static str];

As well as implementing IntoIterator<Rule> for &Linter. We extend the
extisting RuleNamespace proc macro to automatically derive both
implementations from the Linter enum definition.

To support the previously mentioned Markdown table splitting we
introduce a very simple hand-written method to the Linter impl:

    fn categories(&self) -> Option<&'static [LinterCategory]>;
2023-01-22 11:51:29 -05:00
Martin Fischer
c3dd1b0e3c refactor: Rename ParseCode trait to RuleNamespace
ParseCode was a fitting name since the trait only contained a single
parse_code method ... since we now however want to introduce an
additional `prefixes` method RuleNamespace is more fitting.
2023-01-22 11:51:29 -05:00
Martin Fischer
87443e6301 Support prefix "PL" to select all of Pylint 2023-01-22 11:51:29 -05:00
Martin Fischer
16d2ceba79 refactor: Avoid unnecessary Map indexing 2023-01-22 11:51:29 -05:00
Martin Fischer
aedee7294e refactor: Stop using Ident as BTreeMap key
Using Ident as the key type is inconvenient since creating an Ident
requires the specification of a Span, which isn't actually used by
the Hash implementation of Ident.
2023-01-22 11:51:29 -05:00
Martin Fischer
4f12b31dc8 refactor: Drop RuleSelector::codes in favor of IntoIterator impl 2023-01-22 11:51:29 -05:00
Martin Fischer
9f14e7c830 refactor: Update some variable/field/method names 2023-01-22 11:51:29 -05:00
Martin Fischer
4cc492a17a refactor: Encapsulate PerFileIgnore impl details 2023-01-22 11:51:29 -05:00
Martin Fischer
028436af81 refactor: Group Configuration struct fields 2023-01-22 11:51:29 -05:00
Martin Fischer
da4994aa73 refactor: impl From<&Configuration> for RuleTable 2023-01-22 11:51:29 -05:00
Martin Fischer
4dcb491bec refactor: Avoid some unnecessary allocations 2023-01-22 11:51:29 -05:00
Simon Brugman
6fc6bf0648 feat: enable autofix for TRY004 (#2084)
functionality was already implemented, just the trait needed to be added
2023-01-22 07:18:56 -05:00
Charlie Marsh
c1cb4796f8 Support decorators in source code generator (#2081) 2023-01-21 23:26:32 -05:00
Charlie Marsh
d81620397e Improve generator precedence operations (#2080) 2023-01-21 23:21:37 -05:00
Charlie Marsh
84b1490d03 Base INP check on package inference (#2079)
If a file doesn't have a `package`, then it must both be in a directory that lacks an `__init__.py`, and a directory that _isn't_ marked as a namespace package.

Closes #2075.
2023-01-21 19:49:56 -05:00
Simon Brugman
28f05aa6e7 feat: update scripts to new rules structure (#2078)
- optional `prefix` argument for `add_plugin.py`
- rules directory instead of `rules.rs`
- pathlib syntax
- fix test case where code was added instead of name

Example:
```
python scripts/add_plugin.py --url https://pypi.org/project/example/1.0.0/ example --prefix EXA
python scripts/add_rule.py --name SecondRule --code EXA002 --linter example
python scripts/add_rule.py --name FirstRule --code EXA001 --linter example
python scripts/add_rule.py --name ThirdRule --code EXA003 --linter example
 ```

Note that it breaks compatibility with 'old style' plugins (generation works fine, but namespaces need to be changed):
```
python scripts/add_rule.py --name DoTheThing --code PLC999 --linter pylint
```
2023-01-21 19:19:58 -05:00
Charlie Marsh
325faa8e18 Include package path in cache key (#2077)
Closes #2075.
2023-01-21 18:33:35 -05:00
Charlie Marsh
6bfa1804de Remove remaining ropey usages (#2076) 2023-01-21 18:24:10 -05:00
Charlie Marsh
4dcf284a04 Index source code upfront to power (row, column) lookups (#1990)
## Summary

The problem: given a (row, column) number (e.g., for a token in the AST), we need to be able to map it to a precise byte index in the source code. A while ago, we moved to `ropey` for this, since it was faster in practice (mostly, I think, because it's able to defer indexing). However, at some threshold of accesses, it becomes faster to index the string in advance, as we're doing here.

## Benchmark

It looks like this is ~3.6% slower for the default rule set, but ~9.3% faster for `--select ALL`.

**I suspect there's a strategy that would be strictly faster in both cases**, based on deferring even more computation (right now, we lazily compute these offsets, but we do it for the entire file at once, even if we only need some slice at the top), or caching the `ropey` lookups in some way.

Before:

![main](https://user-images.githubusercontent.com/1309177/213883581-8f73c61d-2979-4171-88a6-a88d7ff07e40.png)

After:

![48 all](https://user-images.githubusercontent.com/1309177/213883586-3e049680-9ef9-49e2-8f04-fd6ff402eba7.png)

## Alternatives

I tried tweaking the `Vec::with_capacity` hints, and even trying `Vec::with_capacity(str_indices::lines_crlf::count_breaks(contents))` to do a quick scan of the number of lines, but that turned out to be slower.
2023-01-21 17:56:11 -05:00
Zeddicus414
08fc9b8095 ICN001 check from imports that have no alias (#2072)
Add tests.

Ensure that these cases are caught by ICN001:
```python
from xml.dom import minidom
from xml.dom.minidom import parseString
```

with config:
```toml
[tool.ruff.flake8-import-conventions.extend-aliases]
"dask.dataframe" = "dd"
"xml.dom.minidom" = "md"
"xml.dom.minidom.parseString" = "pstr"
```
2023-01-21 17:47:08 -05:00
Cosmo
39aed6f11d Update link to Pylint parity tracking issue (#2074) 2023-01-21 17:46:55 -05:00
Zeddicus414
5726118cfe ICN001 import-alias-is-not-conventional should check "from" imports (#2070)
Closes https://github.com/charliermarsh/ruff/issues/2047.
2023-01-21 15:43:51 -05:00
Simon Brugman
67de8ac85e feat: implementation for TRY004 (#2066)
See: #2056.
2023-01-21 14:58:59 -05:00
figsoda
b1bda0de82 fix: pin rustpython to the same revision to fix cargo vendor (#2069)
I was trying to update ruff in nixpkgs and ran into this error when it was running `cargo vendor`
```
error: failed to sync

Caused by:
  found duplicate version of package `rustpython-ast v0.2.0` vendored from two sources:

        source 1: https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c#62aa942b
        source 2: https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52
```
2023-01-21 14:40:00 -05:00
Charlie Marsh
84300e00ff Bump version to 0.0.229 2023-01-21 13:18:06 -05:00
Charlie Marsh
fbee95a668 Avoid flagging redefined imports as unused in same-scope (#2065)
This is effectively a revert of #1173, to favor false-negatives over false-positives in the same-scope case.

Closes #2044.
2023-01-21 12:50:21 -05:00
Simon Brugman
afcf5c0ee0 feat: plugin scaffold for tryceratops with TRY300 (#2055)
Renamed to TRY to avoid conflicts, as proposed in https://github.com/guilatrova/tryceratops/pull/55

https://github.com/guilatrova/tryceratops/blob/main/docs/violations/TC300.md

See: #2056
2023-01-21 11:25:10 -05:00
Maksudul Haque
0c30768288 [flake8-builtins] Add builtins-ignorelist Option (#2061)
Closes #2053.
2023-01-21 11:09:04 -05:00
Colin Delahunty
80295f335b Pyupgrade: Printf string formatting (#1803) 2023-01-21 09:37:22 -05:00
Charlie Marsh
465943adf7 Revert "Upgrade to toml v0.5.11" (#2058)
This _did_ fix https://github.com/charliermarsh/ruff/issues/1894, but was a little premature. `toml` doesn't actually depend on `toml-edit` yet, and `v0.5.11` was mostly about deprecations AFAICT. So upgrading might solve that issue, but could introduce other incompatibilities, and I'd like to minimize churn. I expect that `toml` will have a new release soon, so we can revert this revert.

Reverts charliermarsh/ruff#2040.
2023-01-21 07:54:56 -05:00
Charlie Marsh
38eed292e4 Avoid removing comments in RUF005 (#2057)
Closes #2054.
2023-01-21 07:37:25 -05:00
Harutaka Kawamura
883e650a35 Fix S101 range to only highlight assert (#2052)
Fix:

```
resources/test/fixtures/flake8_bandit/S101.py:2:1: S101 Use of `assert` detected
  |
2 | assert True
  | ^^^^^^^^^^^ S101
  |

resources/test/fixtures/flake8_bandit/S101.py:8:5: S101 Use of `assert` detected
  |
8 |     assert x == 1
  |     ^^^^^^^^^^^^^ S101
  |

resources/test/fixtures/flake8_bandit/S101.py:11:5: S101 Use of `assert` detected
   |
11 |     assert x == 2
   |     ^^^^^^^^^^^^^ S101
   |

Found 3 error(s).
```

to:

```
resources/test/fixtures/flake8_bandit/S101.py:2:1: S101 Use of `assert` detected
  |
2 | assert True
  | ^^^^^^ S101
  |

resources/test/fixtures/flake8_bandit/S101.py:8:5: S101 Use of `assert` detected
  |
8 |     assert x == 1
  |     ^^^^^^ S101
  |

resources/test/fixtures/flake8_bandit/S101.py:11:5: S101 Use of `assert` detected
   |
11 |     assert x == 2
   |     ^^^^^^ S101
   |
```
2023-01-21 07:15:00 -05:00
Harutaka Kawamura
eb1b5e5454 De-duplicate SIM102 (#2050)
The idea is the same as #1867. Avoids emitting `SIM102` twice for the following code:

```python
if a:
    if b:
        if c:
            d
```

```
resources/test/fixtures/flake8_simplify/SIM102.py:1:1: SIM102 Use a single `if` statement instead of nested `if` statements
resources/test/fixtures/flake8_simplify/SIM102.py:2:5: SIM102 Use a single `if` statement instead of nested `if` statements
```
2023-01-20 23:38:52 -05:00
Charlie Marsh
8e558a3458 Add scaffolding for flake8-type-checking extension (#2048)
This PR adds the scaffolding files for `flake8-type-checking`, along with the simplest rule (`empty-type-checking-block`), just as an example to get us started.

See: #1785.
2023-01-20 22:41:36 -05:00
Martin Fischer
4e4643aa5d refactor: Decouple Rule from linter prefixes
543865c96b introduced
RuleCode::origin() -> RuleOrigin generation via a macro, while that
signature now has been renamed to Rule::origin() -> Linter we actually
want to get rid of it since rules and linters shouldn't be this tightly
coupled (since one rule can exist in multiple linters).

Another disadvantage of the previous approach was that the prefixes
had to be defined in ruff_macros/src/prefixes.rs, which was easy to
miss when defining new linters in src/*, case in point
INP001 => violations::ImplicitNamespacePackage has in the meantime been
added without ruff_macros/src/prefixes.rs being updated accordingly
which resulted in `ruff --explain INP001` mistakenly reporting that the
rule belongs to isort (since INP001 starts with the isort prefix "I").
The derive proc macro introduced in this commit requires every variant
to have at least one #[prefix = "..."], eliminating such mistakes.
2023-01-20 20:25:57 -05:00
Martin Fischer
b19258a243 refactor: Rename RuleCodePrefix to RuleSelector
More accurate since the enum also encompasses:

* ALL (which isn't a prefix at all)

* fully-qualified rule codes (which aren't prefixes unless you say
  they're a prefix to the empty string but that's not intuitive)
2023-01-20 20:25:57 -05:00
Martin Fischer
7fc42f8f85 refactor: Rename RuleOrigin to Linter
"origin" was accurate since ruff rules are currently always modeled
after one origin (except the Ruff-specific rules).

Since we however want to introduce a many-to-many mapping between codes
and rules, the term "origin" no longer makes much sense. Rules usually
don't have multiple origins but one linter implements a rule first and
then others implement it later (often inspired from another linter).
But we don't actually care much about where a rule originates from when
mapping multiple rule codes to one rule implementation, so renaming
RuleOrigin to Linter is less confusing with the many-to-many system.
2023-01-20 20:25:57 -05:00
Dmitry Dygalo
babe1eb7be perf: Reduce allocations (#2045)
I found a few places where some allocations could be avoided.
2023-01-20 20:06:48 -05:00
Simon Brugman
608b2191aa [flake8-executable] EXE003-005 (#2023)
Tracking issue: https://github.com/charliermarsh/ruff/issues/2024

Implementation for EXE003, EXE004 and EXE005 of `flake8-executable` 
(shebang should contain "python", not have whitespace before, and should be on the first line)

Please take in mind that this is my first rust contribution.

The remaining EXE-rules are a combination of shebang (`lines.rs`), file permissions (`fs.rs`) and if-conditions (`ast.rs`). I was not able to find other rules that have interactions/dependencies in them. Any advice on how this can be best implemented would be very welcome.

For autofixing `EXE005`, I had in mind to _move_  the shebang line to the top op the file. This could be achieved by a combination of `Fix::insert` and `Fix::delete` (multiple fixes per diagnostic), or by implementing a dedicated `Fix::move`, or perhaps in other ways. For now I've left it out, but keen on hearing what you think would be most consistent with the package, and pointer where to start (if at all).

---
If you care about another testimonial:
`ruff` not only helps staying on top of the many excellent flake8 plugins and other Python code quality tools that are available, it also applies them at baffling speed.
(Planning to implement it soon for github.com/pandas-profiling/pandas-profiling (as largest contributor) and github.com/ing-bank/popmon.)
2023-01-20 18:19:07 -05:00
Eric Roberts
3939c2dbf7 Add support for pycodestyle E101 (#2038)
Rule described here: https://www.flake8rules.com/rules/E101.html

I tried to follow contributing guidelines closely, I've never worked with Rust before. Stumbled across Ruff a few days ago and would like to use it in our project, but we use a bunch of flake8 rules that are not yet implemented in ruff, so I decided to give it a go.
2023-01-20 17:24:58 -05:00
Charlie Marsh
20a9252e92 Upgrade to toml v0.5.11 (#2040)
In #1680, we moved over to `toml_edit`. But it looks like `toml` now uses `toml_edit`, and has implemented some improvements (e.g., this closes #1894).
2023-01-20 17:20:45 -05:00
Hugo van Kemenade
a0e3347e43 README: --force-exclude is already set (#2042)
Re: https://github.com/charliermarsh/ruff-pre-commit/issues/19 / https://github.com/charliermarsh/ruff-pre-commit/pull/20

This is now always set, no need to include it in the README example.
2023-01-20 17:20:22 -05:00
Charlie Marsh
9e704a7c63 Only fix true-false returns for return-bool-condition-directly (#2037)
Closes #2035.
2023-01-20 13:17:19 -05:00
Zeddicus414
c9da98e0b7 Fix D404 NoThisPrefix not working with whitespace. (#2036)
D404 should trigger for """ This is a docstring."""

Add a few tests to ensure the fix worked.
2023-01-20 13:01:31 -05:00
Charlie Marsh
5377d24507 Bump version to 0.0.228 2023-01-20 09:58:56 -05:00
Florian Best
db8e4500ee fix(pydocstyle): Avoid trimming docstring if starts with leading quote (#2027)
Fixes: #2017

looks like the other way round is also possible to break:

```""" "foo"""`
2023-01-20 09:57:48 -05:00
Aarni Koskela
bd2de5624e Move readme dev details to CONTRIBUTING.md and fix contradictions (#2030)
Following up on #2018/#2019 discussion, this moves the readme's development-related bits to `CONTRIBUTING.md` to avoid duplication, and fixes up the commands accordingly 😄
2023-01-20 09:23:28 -05:00
Aarni Koskela
3a81f893cc Bump terminfo to remove a whole bunch of unnecessary dependencies (#2022)
See 6281c6b8f7

```
$ cargo update -p terminfo
    Updating crates.io index
    Removing cfg-if v0.1.10
    Removing dirs v2.0.2
    Removing getrandom v0.1.16
    Removing phf v0.8.0
    Updating phf_codegen v0.8.0 -> v0.11.1
    Updating phf_generator v0.8.0 -> v0.11.1
    Removing phf_shared v0.8.0
    Removing rand v0.7.3
    Removing rand_chacha v0.2.2
    Removing rand_core v0.5.1
    Removing rand_hc v0.2.0
    Removing rand_pcg v0.2.1
    Updating terminfo v0.7.3 -> v0.7.5
    Removing wasi v0.9.0+wasi-snapshot-preview1
```
2023-01-20 09:09:02 -05:00
Charlie Marsh
fd6dc2a343 Use platform-appropriate newline character for LibCST embedding (#2028)
Closes #2026.
2023-01-20 09:08:04 -05:00
Martin Fischer
8693236f9e Make CI test add_*.py scripts 2023-01-20 08:09:54 -05:00
Martin Fischer
44e2b6208a fix: Update add_rule.py to create new files for rules 2023-01-20 08:09:54 -05:00
Martin Fischer
16c81f75c2 fix: Update add_rule.py to account for 16e79c8d 2023-01-20 08:09:54 -05:00
Martin Fischer
e1d6ac3265 fix: Update add_plugin.py to account for 9dc66b5a 2023-01-20 08:09:54 -05:00
Martin Fischer
3aec1100f5 fix: Update add_plugin.py to account for b78b6f27 2023-01-20 08:09:54 -05:00
Martin Fischer
c00df647e1 fix: Update add_rule.py to account for 81996f1bc 2023-01-20 08:09:54 -05:00
Martin Fischer
f012877be1 Add scripts/pyproject.toml to use ruff for ruff :) 2023-01-20 08:09:54 -05:00
Martin Fischer
ff6defc988 refactor: Introduce get_indent helper for scripts 2023-01-20 08:09:54 -05:00
Martin Fischer
67ca50e9f2 refactor: Reduce code duplication in scripts/ 2023-01-20 08:09:54 -05:00
Martin Fischer
6cc160bc2b Mark scripts/add_*.py as executable 2023-01-20 08:09:54 -05:00
Ville Skyttä
4bdf506d80 Grammar fixes (#2014) 2023-01-20 07:44:23 -05:00
Charlie Marsh
4af2353ef9 Avoid trimming docstring if ends in trailing quote (#2025)
Closes #2017.
2023-01-20 07:41:58 -05:00
Ville Skyttä
6072edf5bf Note .astimezone() in call-datetime-strptime-without-zone message (#2015) 2023-01-20 07:40:34 -05:00
Martin Fischer
4061eeeb32 Update CI to use MSRV for cargo test and build
As per Cargo.toml our minimal supported Rust version is 1.65.0, so we
should be using that version in our CI for cargo test and cargo build.

This was apparently accidentally changed in
79ca66ace5.
2023-01-20 07:39:40 -05:00
Aarni Koskela
bea6deb0c3 Port pydocstyle code 401 (ImperativeMood) (#1999)
This adds support for pydocstyle code D401 using the `imperative` crate.
2023-01-20 07:18:27 -05:00
Colin Delahunty
81db00a3c4 Pyupgrade: Extraneous parenthesis (#1926) 2023-01-20 00:04:07 -05:00
Charlie Marsh
cf56955ba6 Bump version to 0.0.227 2023-01-19 23:24:52 -05:00
Charlie Marsh
8a8939afd8 Avoid checking row types for single-name @parametrize decorators (#2013)
Closes #2008.
2023-01-19 22:13:17 -05:00
Martin Fischer
6acf2accc6 Improve --explain output
Previous output for `ruff --explain E711`:

    E711 (pycodestyle): Comparison to `None` should be `cond is None`

New output:

    none-comparison

    Code: E711 (pycodestyle)

    Autofix is always available.

    Message formats:

    * Comparison to `None` should be `cond is None`
    * Comparison to `None` should be `cond is not None`
2023-01-19 22:08:00 -05:00
Charlie Marsh
ec0c7647ab Avoid SIM401 in elif blocks (#2012)
For now, we're just gonna avoid flagging this for `elif` blocks, following the same reasoning as for ternaries. We can handle all of these cases, but we'll knock out the TODOs as a pair, and this avoids broken code.

Closes #2007.
2023-01-19 21:57:18 -05:00
Charlie Marsh
045229630e Upgrade RustPython (#2011)
This lets us revert the "manual" fix introduced in #1944.
2023-01-19 21:49:12 -05:00
Martin Fischer
c600991905 Change AsRef<str> impl for Rule to kebab-case
As we surface rule names more to users we want
them to be easier to type than PascalCase.

Prior art:

Pylint and ESLint also use kebab-case for their rule names.
Clippy uses snake_case but only for syntactical reasons
(so that the argument to e.g. #![allow(clippy::some_lint)]
can be parsed as a path[1]).

[1]: https://doc.rust-lang.org/reference/paths.html
2023-01-19 21:37:11 -05:00
Charlie Marsh
f6a93a4c3d Enable autofix for FitsOnOneLine (D200) (#2006)
Closes #1965.
2023-01-19 19:24:50 -05:00
Aarni Koskela
de54ff114e Add RUF005 "unpack instead of concatenating" check (#1957)
This PR adds a new check that turns expressions such as `[1, 2, 3] + foo` into `[1, 2, 3, *foo]`, since the latter is easier to read and faster:

```
~ $ python3.11 -m timeit -s 'b = [6, 5, 4]' '[1, 2, 3] + b'
5000000 loops, best of 5: 81.4 nsec per loop
~ $ python3.11 -m timeit -s 'b = [6, 5, 4]' '[1, 2, 3, *b]'
5000000 loops, best of 5: 66.2 nsec per loop
```

However there's a couple of gotchas:

* This felt like a `simplify` rule, so I borrowed an unused `SIM` code even if the upstream `flake8-simplify` doesn't do this transform. If it should be assigned some other code, let me know 😄 
* **More importantly** this transform could be unsafe if the other operand of the `+` operation has overridden `__add__` to do something else. What's the `ruff` policy around potentially unsafe operations? (I think some of the suggestions other ported rules give could be semantically different from the original code, but I'm not sure.)
* I'm not a very established Rustacean, so there's no doubt my code isn't quite idiomatic. (For instance, is there a neater way to write that four-way `match` statement?)

Thanks for `ruff`, by the way! :)
2023-01-19 17:38:17 -05:00
Charlie Marsh
64b398c72b Tweak some instructions in CONTRIBUTING.md 2023-01-19 17:17:39 -05:00
Aarni Koskela
c99bd3fa60 Split up pydocstyle rules (#2003)
As per @not-mAs per @not-my-profile's [comment](https://github.com/charliermarsh/ruff/pull/1999#discussion_r1081579337):

> we actually want to break up such rules.rs files into smaller files

this breaks up `pydocstyle/rules.rs` into a directory.y-profile's [comment](https://github.com/charliermarsh/ruff/pull/1999#discussion_r1081579337):

> we actually want to break up such rules.rs files into smaller files

this breaks up `pydocstyle/rules.rs` into a directory.
2023-01-19 13:17:25 -05:00
Martin Fischer
8ac930f886 Fix that --explain panics
This commit fixes a bug accidentally introduced in
6cf770a692,
which resulted every `ruff --explain <code>` invocation to fail with:

    thread 'main' panicked at 'Mismatch between definition and access of `explain`.
    Could not downcast to ruff::registry::Rule, need to downcast to &ruff::registry::Rule',
    ruff_cli/src/cli.rs:184:18

We also add an integration test for --explain to prevent such bugs from
going by unnoticed in the future.
2023-01-19 12:58:44 -05:00
Charlie Marsh
ad80fdc2cd Avoid SIM201 and SIM202 errors in __ne__ et al (#2001)
Closes #1986.
2023-01-19 11:27:27 -05:00
Aarni Koskela
a0ea8fe22f Apply #[derive(Default)] fixes suggested by Clippy (#2000)
These were bugging me every time I ran `clippy` 😁
2023-01-19 11:04:43 -05:00
Martin Fischer
3c3da8a88c derive-msg-formats 5/5: Remove placeholder implementations
# This commit has been generated via the following Python script:
# (followed by `cargo +nightly fmt` and `cargo dev generate-all`)
# For the reasoning see the previous commit(s).

import re
import sys

for path in (
    'src/violations.rs',
    'src/rules/flake8_tidy_imports/banned_api.rs',
    'src/rules/flake8_tidy_imports/relative_imports.rs',
):
    with open(path) as f:
        text = ''

        while line := next(f, None):

            if line.strip() != 'fn message(&self) -> String {':
                text += line
                continue

            text += '    #[derive_message_formats]\n' + line

            body = next(f)
            while (line := next(f)) != '    }\n':
                body += line

            # body = re.sub(r'(?<!code\| |\.push\()format!', 'format!', body)
            body = re.sub(
                r'("[^"]+")\s*\.to_string\(\)', r'format!(\1)', body, re.DOTALL
            )
            body = re.sub(
                r'(r#".+?"#)\s*\.to_string\(\)', r'format!(\1)', body, re.DOTALL
            )

            text += body + '    }\n'

            while (line := next(f)).strip() != 'fn placeholder() -> Self {':
                text += line
            while (line := next(f)) != '    }\n':
                pass

    with open(path, 'w') as f:
        f.write(text)
2023-01-19 11:03:32 -05:00
Martin Fischer
16e79c8db6 derive-msg-formats 4/5: Implement #[derive_message_formats]
The idea is nice and simple we replace:

    fn placeholder() -> Self;

with

    fn message_formats() -> &'static [&'static str];

So e.g. if a Violation implementation defines:

    fn message(&self) -> String {
        format!("Local variable `{name}` is assigned to but never used")
    }

it would also have to define:

   fn message_formats() -> &'static [&'static str] {
       &["Local variable `{name}` is assigned to but never used"]
   }

Since we however obviously do not want to duplicate all of our format
strings we simply introduce a new procedural macro attribute
#[derive_message_formats] that can be added to the message method
declaration in order to automatically derive the message_formats
implementation.

This commit implements the macro. The following and final commit
updates violations.rs to use the macro. (The changes have been separated
because the next commit is autogenerated via a Python script.)
2023-01-19 11:03:32 -05:00
Martin Fischer
8f6d8e215c derive-msg-formats 3/5: Introduce Violation::AUTOFIX associated constant
ruff_dev::generate_rules_table previously documented which rules are
autofixable via DiagnosticKind::fixable ... since the DiagnosticKind was
obtained via Rule::kind (and Violation::placeholder) which we both want
to get rid of we have to obtain the autofixability via another way.

This commit implements such another way by adding an AUTOFIX
associated constant to the Violation trait. The constant is of the type
Option<AutoFixkind>, AutofixKind is a new struct containing an
Availability enum { Sometimes, Always}, letting us additionally document
that some autofixes are only available sometimes (which previously
wasn't documented). We intentionally introduce this information in a
struct so that we can easily introduce further autofix metadata in the
future such as autofix applicability[1].

[1]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_errors/enum.Applicability.html
2023-01-19 11:03:32 -05:00
Martin Fischer
8993baab01 derive-msg-formats 2/5: Remove DiagnosticKind::summary
While ruff displays the string returned by Violation::message in its
output for detected violations the messages displayed in the README
and in the `--explain <code>` output previously used the
DiagnosticKind::summary() function which for some verbose messages
provided shorter descriptions.

This commit removes DiagnosticKind::summary, and moves the more
extensive documentation into doc comments ... these are not displayed
yet to the user but doing that is very much planned.
2023-01-19 11:03:32 -05:00
Martin Fischer
2568627c4c derive-msg-formats 1/5: Remove unnecessary usages of Rule::kind
This commit series removes the following associated
function from the Violation trait:

    fn placeholder() -> Self;

ruff previously used this placeholder approach for the messages it
listed in the README and displayed when invoked with --explain <code>.

This approach is suboptimal for three reasons:

1. The placeholder implementations are completely boring code since they
   just initialize the struct with some dummy values.

2. Displaying concrete error messages with arbitrary interpolated values
   can be confusing for the user since they might not recognize that the
   values are interpolated.

3. Some violations have varying format strings depending on the
   violation which could not be documented with the previous approach
   (while we could have changed the signature to return Vec<Self> this
   would still very much suffer from the previous two points).

We therefore drop Violation::placeholder in favor of a new macro-based
approach, explained in commit 4/5.

Violation::placeholder is only invoked via Rule::kind, so we firstly
have to get rid of all Rule::kind invocations ... this commit starts
removing the trivial cases.
2023-01-19 11:03:32 -05:00
Martin Fischer
9603a024b3 refactor: Move a bunch of pandas-vet logic to rules::pandas_vet 2023-01-19 11:03:32 -05:00
Charlie Marsh
a122d95ef5 Preserve unmatched comparators in SIM109 (#1998)
Closes #1993.
2023-01-19 10:23:20 -05:00
Damien Allen
6ddfe50ac4 Added pylint formatter (#1995)
Fixes: #1953

@charliermarsh thank you for the tips in the issue.

I'm not very familiar with Rust, so please excuse if my string formatting syntax is messy.

In terms of testing, I compared output of `flake8 --format=pylint ` and `cargo run --format=pylint` on the same code and the output syntax seems to check out.
2023-01-19 08:01:27 -05:00
Martin Fischer
26901a78c9 Make define_rule_mapping! set rule code as doc comment of variants
Since the UI still relies on the rule codes this improves the developer
experience by letting developers view the code of a Rule enum variant by
hovering over it.
2023-01-19 07:37:16 -05:00
Martin Fischer
6649225167 rule 8/8: Automatically rewrite RuleCode to Rule
# This commit was automatically generated by running the following
# script (followed by `cargo +nightly fmt`):

import glob
import re
from typing import NamedTuple

class Rule(NamedTuple):
    code: str
    name: str
    path: str

def rules() -> list[Rule]:
    """Returns all the rules defined in `src/registry.rs`."""
    file = open('src/registry.rs')

    rules = []

    while next(file) != 'ruff_macros::define_rule_mapping!(\n':
        continue

    while (line := next(file)) != ');\n':
        line = line.strip().rstrip(',')
        if line.startswith('//'):
            continue
        code, path = line.split(' => ')
        name = path.rsplit('::')[-1]
        rules.append(Rule(code, name, path))

    return rules

code2name = {r.code: r.name for r in rules()}

for pattern in ('src/**/*.rs', 'ruff_cli/**/*.rs', 'ruff_dev/**/*.rs', 'scripts/add_*.py'):
    for name in glob.glob(pattern, recursive=True):
        with open(name) as f:
            text = f.read()

        text = re.sub('Rule(?:Code)?::([A-Z]\w+)', lambda m: 'Rule::' + code2name[m.group(1)], text)
        text = re.sub(r'(?<!"<FilePattern>:<)RuleCode\b', 'Rule', text)
        text = re.sub('(use crate::registry::{.*, Rule), Rule(.*)', r'\1\2', text) # fix duplicate import

        with open(name, 'w') as f:
            f.write(text)
2023-01-18 23:51:48 -05:00
Martin Fischer
9e3083aa2c rule 7/8: Change Rule enum definition 2023-01-18 23:51:48 -05:00
Martin Fischer
6d11ff3822 rule 6/8: Remove Serialize & Deserialize impls for Rule 2023-01-18 23:51:48 -05:00
Martin Fischer
6cf770a692 rule 5/8: Remove FromStr impl for Rule 2023-01-18 23:51:48 -05:00
Martin Fischer
3534e370e1 rule 4/8: Remove Display impl for Rule 2023-01-18 23:51:48 -05:00
Martin Fischer
dbcab5128c rule 3/8: Remove AsRef<str> impl for Rule 2023-01-18 23:51:48 -05:00
Martin Fischer
3810250bb6 rule 2/8: Rename DiagnosticKind::code to rule 2023-01-18 23:51:48 -05:00
Martin Fischer
3c1c1e1dd3 rule 1/8: Rename RuleCode to Rule (backwards-compatible for now)
This commit series refactors ruff to decouple "rules" from "rule codes",
in order to:

1. Make our code more readable by changing e.g.
   RuleCode::UP004 to Rule::UselessObjectInheritance.

2. Let us cleanly map multiple codes to one rule, for example:

   [UP004] in pyupgrade, [R0205] in pylint and [PIE792] in flake8-pie
   all refer to the rule UselessObjectInheritance but ruff currently
   only associates that rule with the UP004 code (since the
   implementation was initially modeled after pyupgrade).

3. Let us cleanly map one code to multiple rules, for example:

   [C0103] from pylint encompasses N801, N802 and N803 from pep8-naming.

The latter two steps are not yet implemented by this commit series
but this refactoring enables us to introduce such a mapping.  Such a
mapping would also let us expand flake8_to_ruff to support e.g. pylint.

After the next commit which just does some renaming the following four
commits remove all trait derivations from the Rule (previously RuleCode)
enum that depend on the variant names to guarantee that they are not
used anywhere anymore so that we can rename all of these variants in the
eigth and final commit without breaking anything.

While the plan very much is to also surface these human-friendly names
more in the user interface this is not yet done in this commit series,
which does not change anything about the UI: it's purely a refactor.

[UP004]: pyupgrade doesn't actually assign codes to its messages.
[R0205]: https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/useless-object-inheritance.html
[PIE792]: https://github.com/sbdchd/flake8-pie#pie792-no-inherit-object
[C0103]: https://pylint.pycqa.org/en/latest/user_guide/messages/convention/invalid-name.html
2023-01-18 23:51:48 -05:00
Martin Fischer
5b7bd93b91 refactor: Use Self:: in match arms 2023-01-18 23:51:48 -05:00
Martin Fischer
9e096b4a4c refactor: Make define_rule_mapping! generate RuleCodePrefix directly 2023-01-18 23:51:48 -05:00
Charlie Marsh
d8645acd1f Bump version to 0.0.226 2023-01-18 20:54:38 -05:00
Charlie Marsh
92dd073191 Add Pylint settings to lib_wasm.rs 2023-01-18 20:54:03 -05:00
Charlie Marsh
ff96219e62 Exclude None, Bool, and Ellipsis from ConstantType (#1988)
These have no effect, so it's confusing that they're even settable.
2023-01-18 20:50:09 -05:00
Charlie Marsh
d33424ec9d Enable suppression of magic values by type (#1987)
Closes #1949.
2023-01-18 20:44:24 -05:00
Charlie Marsh
34412a0a01 Avoid removing side effects for boolean simplifications (#1984)
Closes #1978.
2023-01-18 19:08:14 -05:00
Charlie Marsh
ceb48d3a32 Use relative paths for INP001 (#1981) 2023-01-18 18:45:47 -05:00
Charlie Marsh
969a6f0d53 Replace misplaced-comparison-constant with SIM300 (#1980)
Closes: #1954.
2023-01-18 18:42:49 -05:00
Charlie Marsh
7628876ff2 Invert order of yoda-conditions message (#1979)
The suggestion was wrong!
2023-01-18 18:27:36 -05:00
Charlie Marsh
ef355e5c2c Remove artificial wraps from GitHub messages (#1977) 2023-01-18 18:20:56 -05:00
Charlie Marsh
97f55b8e97 Convert remaining call path sites to use SmallVec (#1972) 2023-01-18 14:50:33 -05:00
Aarni Koskela
ff2be35f51 Run cargo fmt in pre-commit (#1968)
Since `cargo fmt` is a required CI check, we could just as well run it in `pre-commit`.
2023-01-18 13:14:51 -05:00
Anders Kaseorg
1e803f7108 README: Link “Flake8” for consistency with the rest of the list (#1969)
Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-01-18 13:07:21 -05:00
Charlie Marsh
1ab0273aa7 Strip whitespace when injecting D209 newline (#1967)
Closes #1963.
2023-01-18 12:09:17 -05:00
Charlie Marsh
5a7d8c25f4 Treat subscript accesses as unsafe effects for autofix (#1966)
See: #1809.
2023-01-18 11:46:12 -05:00
Charlie Marsh
26d6414558 Fix UP003 check from rebase 2023-01-18 11:39:39 -05:00
Charlie Marsh
dae95626ae Use smallvec for call path representation (#1960)
This provides a ~10% speed-up for large codebases with `--select ALL`:

![Screen Shot 2023-01-18 at 11 28 20 AM](https://user-images.githubusercontent.com/1309177/213236389-cff50840-6e55-47a3-9164-2e40cbc885f6.png)
2023-01-18 11:29:05 -05:00
Maksudul Haque
9a3e525930 [isort] Add no-lines-before Option (#1955)
Closes https://github.com/charliermarsh/ruff/issues/1916.
2023-01-18 11:09:47 -05:00
Anders Kaseorg
b9c6cfc0ab Autofix SIM117 (MultipleWithStatements) (#1961)
This is slightly buggy due to Instagram/LibCST#855; it will complain `[ERROR] Failed to fix nested with: Failed to extract CST from source` when trying to fix nested parenthesized `with` statements lacking trailing commas. But presumably people who write parenthesized `with` statements already knew that they don’t need to nest them.

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-01-18 11:06:04 -05:00
Charlie Marsh
b1f10c8339 Confine type-of-primitive checks to builtin type calls (#1962)
Closes #1958.
2023-01-18 10:53:50 -05:00
Anders Kaseorg
83346de6e0 Autofix SIM102 (NestedIfStatements)
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-01-18 07:37:27 -05:00
Anders Kaseorg
b23cc31863 Change ast::helpers::has_coments to accept a Range
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-01-18 07:37:27 -05:00
Anders Kaseorg
462d81beb7 Ensure ast::whitespace::indentation extracts whitespace
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-01-18 07:37:27 -05:00
Anders Kaseorg
715ea2d374 Accept a Locator for ast::whitespace::indentation
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-01-18 07:37:27 -05:00
skykasko
6c7e60b4f9 Fix bad link for flake8-no-pep420 (#1952)
See https://github.com/charliermarsh/ruff/pull/1942.
2023-01-18 07:36:05 -05:00
Maksudul Haque
868d0b3e29 [isort] Add constants and variables Options (#1951)
closes https://github.com/charliermarsh/ruff/issues/1819
2023-01-18 07:30:51 -05:00
Charlie Marsh
cdb4700813 Bump version to 0.0.225 2023-01-18 00:22:48 -05:00
Anders Kaseorg
ea4d54a90f Restrict SIM105 to try blocks with a body of one simple statement (#1948)
If a `try` block has multiple statements, a compound statement, or
control flow, rewriting it with `contextlib.suppress` would obfuscate
the fact that the exception still short-circuits further statements in
the block.

Fixes #1947.

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-01-18 00:22:22 -05:00
Charlie Marsh
51b917cfbf Exempt contextlib.ExitStack() for SIM115 rules (#1946)
Since our binding tracking is somewhat limited, I opted to favor false negatives over false positives. So, e.g., this won't trigger SIM115:

```py
with contextlib.ExitStack():
    f = exit_stack.enter_context(open("filename"))
```

(Notice that `exit_stack` is unbound.)

The alternative strategy required us to incorrectly trigger SIM115 on this:

```py
with contextlib.ExitStack() as exit_stack:
    exit_stack_ = exit_stack
    f = exit_stack_.enter_context(open("filename"))
```

Closes #1945.
2023-01-17 22:39:54 -05:00
Edgar R. M
c880d744fd Implement flake8-no-pep420 (#1942)
Closes https://github.com/charliermarsh/ruff/issues/1844.
2023-01-17 22:10:32 -05:00
Charlie Marsh
84d1df08be Avoid broken autofix for SIM103 with elif (#1944)
Also adjusts the generator to avoid the extra parentheses (and skips commented `if` statements).

Closes #1943.
2023-01-17 22:03:17 -05:00
Charlie Marsh
b9bb5acff8 Remove unnecessary setuptools block 2023-01-17 21:17:37 -05:00
Charlie Marsh
ca7c3c2175 Avoid autofixing comma rules when --fix is not set (#1940)
Closes #1939.
2023-01-17 20:09:01 -05:00
Charlie Marsh
8891e2e62b Fix name of ruff-pre-commit event 2023-01-17 17:25:02 -05:00
Martin Fischer
53265e0ed4 cli: Catch panics to tell the user to report them (#1928) 2023-01-17 17:17:09 -05:00
Charlie Marsh
072849a8a9 Move @functools.cache rewrites to their own rule (#1938)
Closes #1934.
2023-01-17 15:12:40 -05:00
Charlie Marsh
70ea4b25e8 Allow duplicate enum values for enum.auto() (#1933)
Closes #1932.
2023-01-17 11:14:11 -05:00
Martin Fischer
30e133f3d8 refactor: Declare defaults once in settings::defaults 2023-01-17 09:20:57 -05:00
Martin Fischer
aa812de07e refactor: Implement Default for Settings 2023-01-17 09:20:57 -05:00
Martin Fischer
57ac6a8444 refactor: Make resolve_codes take IntoIterator instead of Iterator 2023-01-17 09:20:57 -05:00
Martin Fischer
a6566b1b34 refactor: Merge Settings.enabled and Settings.fixable
The Settings struct previously contained the fields:

     pub enabled: HashableHashSet<RuleCode>,
     pub fixable: HashableHashSet<RuleCode>,

This commit merges both fields into one by introducing a new
RuleTable type, wrapping HashableHashMap<RuleCode, bool>,
which has the following benefits:

1. It makes the invalid state that a rule is
   disabled but fixable unrepresentable.

2. It encapsulates the implementation details of the table.
   (It currently uses an FxHashMap but that may change.)

3. It results in more readable code.

       settings.rules.enabled(rule)
       settings.rules.should_fix(rule)

   is more readable than:

       settings.enabled.contains(rule)
       settings.fixable.contains(rule)
2023-01-17 09:20:57 -05:00
Martin Fischer
580da1fa6b refactor: Group Settings fields 2023-01-17 09:20:57 -05:00
Martin Fischer
b78b6f275e refactor: Define origin names & URLs within doc comments 2023-01-17 07:44:40 -05:00
Martin Fischer
6868bb46f5 refactor: Get rid of Platform enum 2023-01-17 07:44:40 -05:00
Martin Fischer
601848d9a8 refactor: Rename RuleOrigin::title to RuleOrigin::name 2023-01-17 07:44:40 -05:00
Martin Fischer
f4da7635f0 Add missing url for flake8-import-conventions 2023-01-17 07:44:40 -05:00
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
1158 changed files with 27501 additions and 12750 deletions

View File

@@ -70,7 +70,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
toolchain: 1.65.0
override: true
- uses: Swatinem/rust-cache@v1
- run: cargo install cargo-insta
@@ -80,6 +80,26 @@ 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
scripts:
name: "test scripts"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/add_rule.py --name DoTheThing --code PLC999 --linter pylint
- run: cargo check
- run: |
./scripts/add_plugin.py test --url https://pypi.org/project/-test/0.1.0/ --prefix TST
./scripts/add_rule.py --name FirstRule --code TST001 --linter test
- run: cargo check
# TODO(charlie): Re-enable the `wasm-pack` tests.
# See: https://github.com/charliermarsh/ruff/issues/1425
@@ -119,7 +139,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
toolchain: 1.65.0
override: true
- uses: Swatinem/rust-cache@v1
- uses: actions/setup-python@v4

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": "pypi_release"}'

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.221
rev: v0.0.231
hooks:
- id: ruff
@@ -8,3 +8,11 @@ repos:
rev: v0.10.1
hooks:
- id: validate-pyproject
- repo: local
hooks:
- id: cargo-fmt
name: cargo fmt
entry: cargo fmt --
language: rust
types: [rust]

View File

@@ -1,5 +1,36 @@
# Breaking Changes
## 0.0.226
### `misplaced-comparison-constant` (`PLC2201`) was deprecated in favor of `SIM300` ([#1980](https://github.com/charliermarsh/ruff/pull/1980))
These two rules contain (nearly) identical logic. To deduplicate the rule set, we've upgraded
`SIM300` to handle a few more cases, and deprecated `PLC2201` in favor of `SIM300`.
## 0.0.225
### `@functools.cache` rewrites have been moved to a standalone rule (`UP033`) ([#1938](https://github.com/charliermarsh/ruff/pull/1938))
Previously, `UP011` handled both `@functools.lru_cache()`-to-`@functools.lru_cache` conversions,
_and_ `@functools.lru_cache(maxsize=None)`-to-`@functools.cache` conversions. The latter has been
moved out to its own rule (`UP033`). As such, some `# noqa: UP011` comments may need to be updated
to reflect the change in rule code.
## 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))

View File

@@ -14,9 +14,10 @@ If you're looking for a place to start, we recommend implementing a new lint rul
pattern-match against the examples in the existing codebase. Many lint rules are inspired by
existing Python plugins, which can be used as a reference implementation.
As a concrete example: consider taking on one of the rules in [`flake8-simplify`](https://github.com/charliermarsh/ruff/issues/998),
and looking to the originating [Python source](https://github.com/MartinThoma/flake8-simplify) for
guidance.
As a concrete example: consider taking on one of the rules from the [`tryceratops`](https://github.com/charliermarsh/ruff/issues/2056)
plugin, and looking to the originating [Python source](https://github.com/guilatrova/tryceratops)
for guidance. [`flake8-simplify`](https://github.com/charliermarsh/ruff/issues/998) has a few rules
left too.
### Prerequisites
@@ -37,13 +38,16 @@ After cloning the repository, run Ruff locally with:
cargo run resources/test/fixtures --no-cache
```
Prior to opening a pull request, ensure that your code has been auto-formatted, and that it passes
both the lint and test validation checks:
Prior to opening a pull request, ensure that your code has been auto-formatted,
and that it passes both the lint and test validation checks.
For rustfmt and Clippy, we use [nightly Rust][nightly], as it is stricter than stable Rust.
(However, tests and builds use stable Rust.)
```shell
cargo +nightly fmt --all # Auto-formatting...
cargo +nightly clippy --all # Linting...
cargo +nightly test --all # Testing...
cargo +nightly clippy --fix --workspace --all-targets --all-features -- -W clippy::pedantic # Linting...
cargo test --all # Testing...
```
These checks will run on GitHub Actions when you open your Pull Request, but running them locally
@@ -54,18 +58,22 @@ prior to merging.
### Example: Adding a new lint rule
There are four phases to adding a new lint rule:
At a high level, the steps involved in adding a new lint rule are as follows:
1. Define the violation struct in `src/violations.rs` (e.g., `ModuleImportNotAtTopOfFile`).
2. Map the violation struct to a rule code in `src/registry.rs` (e.g., `E402`).
3. Define the logic for triggering the violation in `src/checkers/ast.rs` (for AST-based checks),
`src/checkers/tokens.rs` (for token-based checks), or `src/checkers/lines.rs` (for text-based checks).
4. Add a test fixture.
5. Update the generated files (documentation and generated code).
1. Create a file for your rule (e.g., `src/rules/flake8_bugbear/rules/abstract_base_class.rs`).
2. In that file, define a violation struct. You can grep for `define_violation!` to see examples.
3. Map the violation struct to a rule code in `src/registry.rs` (e.g., `E402`).
4. Define the logic for triggering the violation in `src/checkers/ast.rs` (for AST-based checks),
`src/checkers/tokens.rs` (for token-based checks), `src/checkers/lines.rs` (for text-based
checks), or `src/checkers/filesystem.rs` (for filesystem-based checks).
5. Add a test fixture.
6. Update the generated files (documentation and generated code).
To define the violation, open up `src/violations.rs`, and define a new struct using the
`define_violation!` macro. There are plenty of examples in that file, so feel free to pattern-match
against the existing structs.
To define the violation, start by creating a dedicated file for your rule under the appropriate
rule linter (e.g., `src/rules/flake8_bugbear/rules/abstract_base_class.rs`). That file should
contain a struct defined via `define_violation!`, along with a function that creates the violation
based on any required inputs. (Many of the existing examples live in `src/violations.rs`, but we're
looking to place new rules in their own files.)
To trigger the violation, you'll likely want to augment the logic in `src/checkers/ast.rs`, which
defines the Python AST visitor, responsible for iterating over the abstract syntax tree and
@@ -74,7 +82,7 @@ collecting diagnostics as it goes.
If you need to inspect the AST, you can run `cargo +nightly dev print-ast` with a Python file. Grep
for the `Check::new` invocations to understand how other, similar rules are implemented.
To add a test fixture, create a file under `resources/test/fixtures/[origin]`, named to match
To add a test fixture, create a file under `resources/test/fixtures/[linter]`, named to match
the code you defined earlier (e.g., `resources/test/fixtures/pycodestyle/E402.py`). This file should
contain a variety of violations and non-violations designed to evaluate and demonstrate the behavior
of your lint rule.
@@ -83,7 +91,7 @@ Run `cargo +nightly dev generate-all` to generate the code for your new fixture.
locally with (e.g.) `cargo run resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
`test_case` macro in the relevant `src/[origin]/mod.rs` file. Then, run `cargo test --all`.
`test_case` macro in the relevant `src/[linter]/mod.rs` file. Then, run `cargo test --all`.
Your test will fail, but you'll be prompted to follow-up with `cargo insta review`. Accept the
generated snapshot, then commit the snapshot file alongside the rest of your changes.
@@ -123,3 +131,5 @@ them to [PyPI](https://pypi.org/project/ruff/).
Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).
[nightly]: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust

311
Cargo.lock generated
View File

@@ -14,7 +14,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.8",
"getrandom",
"once_cell",
"version_check",
]
@@ -197,12 +197,6 @@ version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -413,7 +407,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"wasm-bindgen",
]
@@ -439,7 +433,7 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -484,7 +478,7 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"crossbeam-utils",
]
@@ -494,7 +488,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
@@ -506,7 +500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"cfg-if",
"crossbeam-utils",
"memoffset",
"scopeguard",
@@ -518,7 +512,7 @@ version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -592,16 +586,6 @@ dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
dependencies = [
"cfg-if 0.1.10",
"dirs-sys",
]
[[package]]
name = "dirs"
version = "4.0.0"
@@ -617,7 +601,7 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"dirs-sys-next",
]
@@ -721,7 +705,7 @@ version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"redox_syscall",
"windows-sys",
@@ -735,7 +719,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.221-dev.0"
version = "0.0.231"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -786,24 +770,13 @@ dependencies = [
"libc",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
@@ -926,6 +899,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "imperative"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f92123bf2fe0d9f1b5df1964727b970ca3b2d0203d47cf97fb1f36d856b6398"
dependencies = [
"phf 0.11.1",
"rust-stemmers",
]
[[package]]
name = "indexmap"
version = "1.9.2"
@@ -976,7 +959,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -1186,7 +1169,7 @@ version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -1265,7 +1248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
"cfg-if",
"libc",
"static_assertions",
]
@@ -1364,6 +1347,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"
@@ -1398,7 +1402,7 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
@@ -1472,15 +1476,6 @@ dependencies = [
"indexmap",
]
[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_shared 0.8.0",
]
[[package]]
name = "phf"
version = "0.10.1"
@@ -1491,13 +1486,12 @@ dependencies = [
]
[[package]]
name = "phf_codegen"
version = "0.8.0"
name = "phf"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
dependencies = [
"phf_generator 0.8.0",
"phf_shared 0.8.0",
"phf_shared 0.11.1",
]
[[package]]
@@ -1511,13 +1505,13 @@ dependencies = [
]
[[package]]
name = "phf_generator"
version = "0.8.0"
name = "phf_codegen"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
dependencies = [
"phf_shared 0.8.0",
"rand 0.7.3",
"phf_generator 0.11.1",
"phf_shared 0.11.1",
]
[[package]]
@@ -1527,16 +1521,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared 0.10.0",
"rand 0.8.5",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
name = "phf_generator"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
dependencies = [
"siphasher",
"phf_shared 0.11.1",
"rand",
]
[[package]]
@@ -1548,6 +1543,15 @@ dependencies = [
"siphasher",
]
[[package]]
name = "phf_shared"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
dependencies = [
"siphasher",
]
[[package]]
name = "pico-args"
version = "0.4.2"
@@ -1621,6 +1625,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"
@@ -1692,20 +1707,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
"rand_pcg",
]
[[package]]
name = "rand"
version = "0.8.5"
@@ -1713,18 +1714,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
"rand_chacha",
"rand_core",
]
[[package]]
@@ -1734,16 +1725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
"rand_core",
]
[[package]]
@@ -1752,25 +1734,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.8",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
"rand_core 0.5.1",
"getrandom",
]
[[package]]
@@ -1810,7 +1774,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom 0.2.8",
"getrandom",
"redox_syscall",
"thiserror",
]
@@ -1862,35 +1826,26 @@ dependencies = [
"winapi",
]
[[package]]
name = "ropey"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd22239fafefc42138ca5da064f3c17726a80d2379d817a3521240e78dd0064"
dependencies = [
"smallvec",
"str_indices",
]
[[package]]
name = "ruff"
version = "0.0.221"
version = "0.0.231"
dependencies = [
"anyhow",
"bitflags",
"cfg-if 1.0.0",
"cfg-if",
"chrono",
"clap 4.0.32",
"colored",
"console_error_panic_hook",
"console_log",
"criterion",
"dirs 4.0.0",
"dirs",
"fern",
"getrandom 0.2.8",
"getrandom",
"glob",
"globset",
"ignore",
"imperative",
"insta",
"itertools",
"js-sys",
@@ -1903,7 +1858,6 @@ dependencies = [
"once_cell",
"path-absolutize",
"regex",
"ropey",
"ruff_macros",
"rustc-hash",
"rustpython-ast",
@@ -1914,10 +1868,12 @@ dependencies = [
"serde",
"serde-wasm-bindgen",
"shellexpand",
"smallvec",
"strum",
"strum_macros",
"test-case",
"textwrap",
"thiserror",
"titlecase",
"toml_edit",
"wasm-bindgen",
@@ -1926,7 +1882,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.221"
version = "0.0.231"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1963,7 +1919,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.221"
version = "0.0.231"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1984,7 +1940,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.221"
version = "0.0.231"
dependencies = [
"once_cell",
"proc-macro2",
@@ -1993,6 +1949,16 @@ dependencies = [
"textwrap",
]
[[package]]
name = "rust-stemmers"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54"
dependencies = [
"serde",
"serde_derive",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
@@ -2027,8 +1993,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=4f38cb68e4a97aeea9eb19673803a0bd5f655383#4f38cb68e4a97aeea9eb19673803a0bd5f655383"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -2037,12 +2003,12 @@ 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=4f38cb68e4a97aeea9eb19673803a0bd5f655383#4f38cb68e4a97aeea9eb19673803a0bd5f655383"
dependencies = [
"ascii",
"bitflags",
"cfg-if 1.0.0",
"cfg-if",
"hexf-parse",
"itertools",
"lexical-parse-float",
@@ -2053,7 +2019,7 @@ dependencies = [
"num-traits",
"once_cell",
"radium",
"rand 0.8.5",
"rand",
"siphasher",
"unic-ucd-category",
"volatile",
@@ -2062,8 +2028,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=4f38cb68e4a97aeea9eb19673803a0bd5f655383#4f38cb68e4a97aeea9eb19673803a0bd5f655383"
dependencies = [
"bincode",
"bitflags",
@@ -2072,15 +2038,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=4f38cb68e4a97aeea9eb19673803a0bd5f655383#4f38cb68e4a97aeea9eb19673803a0bd5f655383"
dependencies = [
"ahash",
"anyhow",
@@ -2240,7 +2206,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1c7ddea665294d484c39fd0c0d2b7e35bbfe10035c5fe1854741a57f6880e1"
dependencies = [
"dirs 4.0.0",
"dirs",
]
[[package]]
@@ -2279,12 +2245,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "str_indices"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd"
[[package]]
name = "string_cache"
version = "0.8.4"
@@ -2343,7 +2303,7 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"fastrand",
"libc",
"redox_syscall",
@@ -2383,15 +2343,15 @@ dependencies = [
[[package]]
name = "terminfo"
version = "0.7.3"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e"
checksum = "da31aef70da0f6352dbcb462683eb4dd2bfad01cf3fc96cf204547b9a839a585"
dependencies = [
"dirs 2.0.2",
"dirs",
"fnv",
"nom",
"phf 0.8.0",
"phf_codegen 0.8.0",
"phf 0.11.1",
"phf_codegen 0.11.1",
]
[[package]]
@@ -2415,7 +2375,7 @@ version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e45b7bf6e19353ddd832745c8fcf77a17a93171df7151187f26623f2b75b5b26"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"proc-macro-error",
"proc-macro2",
"quote",
@@ -2518,6 +2478,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"
@@ -2546,7 +2515,7 @@ version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"static_assertions",
]
@@ -2750,12 +2719,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@@ -2774,7 +2737,7 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"wasm-bindgen-macro",
]
@@ -2799,7 +2762,7 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",

View File

@@ -8,7 +8,7 @@ default-members = [".", "ruff_cli"]
[package]
name = "ruff"
version = "0.0.221"
version = "0.0.231"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -35,6 +35,7 @@ fern = { version = "0.6.1" }
glob = { version = "0.3.0" }
globset = { version = "0.4.9" }
ignore = { version = "0.4.18" }
imperative = { version = "1.0.3" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
log = { version = "0.4.17" }
@@ -45,19 +46,20 @@ num-traits = "0.2.15"
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.231", 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 = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
schemars = { version = "0.8.11" }
semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] }
shellexpand = { version = "3.0.0" }
smallvec = { version = "1.10.0" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
textwrap = { version = "0.16.0" }
thiserror = { version = "1.0" }
titlecase = { version = "2.2.1" }
toml_edit = { version = "0.17.1", features = ["easy"] }
@@ -95,7 +97,3 @@ opt-level = 3
# https://github.com/bytecodealliance/wasm-tools/blob/b5c3d98e40590512a3b12470ef358d5c7b983b15/crates/wasmparser/src/limits.rs#L29
[profile.dev.package.rustpython-parser]
opt-level = 1
[[bench]]
name = "source_code_locator"
harness = false

1279
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
use std::fs;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use ropey::Rope;
fn criterion_benchmark(c: &mut Criterion) {
let contents = fs::read_to_string(Path::new("resources/test/fixtures/D.py")).unwrap();
c.bench_function("rope", |b| {
b.iter(|| {
let rope = Rope::from_str(black_box(&contents));
rope.line_to_char(black_box(4));
});
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

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

View File

@@ -1,12 +1,8 @@
[package]
name = "flake8-to-ruff"
version = "0.0.221-dev.0"
version = "0.0.231"
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,32 +0,0 @@
//! Extract Black configuration settings from a pyproject.toml.
use std::path::Path;
use anyhow::Result;
use ruff::settings::types::PythonVersion;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Black {
#[serde(alias = "line-length", alias = "line_length")]
pub line_length: Option<usize>,
#[serde(alias = "target-version", alias = "target_version")]
pub target_version: Option<Vec<PythonVersion>>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Tools {
black: Option<Black>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Pyproject {
tool: Option<Tools>,
}
pub fn parse_black_options<P: AsRef<Path>>(path: P) -> Result<Option<Black>> {
let contents = std::fs::read_to_string(path)?;
Ok(toml_edit::easy::from_str::<Pyproject>(&contents)?
.tool
.and_then(|tool| tool.black))
}

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::{self, ExternalConfig};
#[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<()> {
@@ -50,14 +48,18 @@ fn main() -> Result<()> {
let config = ini.load(cli.file).map_err(|msg| anyhow::anyhow!(msg))?;
// Read the pyproject.toml file.
let black = cli
.pyproject
.map(parse_black_options)
.transpose()?
.flatten();
let pyproject = cli.pyproject.map(flake8_to_ruff::parse).transpose()?;
let external_config = pyproject
.as_ref()
.and_then(|pyproject| pyproject.tool.as_ref())
.map(|tool| ExternalConfig {
black: tool.black.as_ref(),
isort: tool.isort.as_ref(),
})
.unwrap_or_default();
// Create Ruff's pyproject.toml section.
let pyproject = converter::convert(&config, black.as_ref(), cli.plugin)?;
let pyproject = flake8_to_ruff::convert(&config, &external_config, 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

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2021 Rodolphe Pelloux-Prayer
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

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Adam Johnson
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.231"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },
@@ -41,9 +41,3 @@ bindings = "bin"
manifest-path = "ruff_cli/Cargo.toml"
python-source = "python"
strip = true
[tool.setuptools]
license-files = [
"LICENSE",
"licenses/*",
]

View File

@@ -0,0 +1,8 @@
import logging.config
t = logging.config.listen(9999)
def verify_func():
pass
l = logging.config.listen(9999, verify=verify_func)

View File

@@ -1,5 +1,6 @@
import some as sum
from some import other as int
from directory import new as dir
print = 1
copyright: 'annotation' = 2
@@ -7,6 +8,8 @@ copyright: 'annotation' = 2
float = object = 4
min, max = 5, 6
id = 4
def bytes():
pass

View File

@@ -5,5 +5,7 @@ def func1(str, /, type, *complex, Exception, **getattr):
async def func2(bytes):
pass
async def func3(id, dir):
pass
map([], lambda float: ...)

View File

@@ -1,8 +1,12 @@
class MyClass:
ImportError = 4
id = 5
dir = "/"
def __init__(self):
self.float = 5 # is fine
self.id = 10
self.dir = "."
def str(self):
pass

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 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,2 @@
#!/usr/bin/bash
print("hello world")

View File

@@ -0,0 +1 @@
#!/usr/bin/python

View File

@@ -0,0 +1,2 @@
print(" #!/usr/bin/python")
# shebang outside of comments should be ignored

View File

@@ -0,0 +1,2 @@
pass #!/usr/bin/env python

View File

@@ -0,0 +1,3 @@
# A python comment
#!/usr/bin/python

View File

@@ -0,0 +1,4 @@
print("code prior to shebang")
# A python comment
#!/usr/bin/python

View File

@@ -0,0 +1,6 @@
"""
With a docstring
print("commented out code")
"""
# A python comment
#!/usr/bin/python

View File

@@ -0,0 +1,21 @@
# Test absolute imports
# Violation cases
import xml.dom.minidom
import xml.dom.minidom as wrong
from xml.dom import minidom as wrong
from xml.dom import minidom
from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import.
from xml.dom.minidom import parseString
from xml.dom.minidom import parse, parseString
from xml.dom.minidom import parse as ps, parseString as wrong
# No ICN001 violations
import xml.dom.minidom as md
from xml.dom import minidom as md
from xml.dom.minidom import parseString as pstr
from xml.dom.minidom import parse, parseString as pstr
from xml.dom.minidom import parse as ps, parseString as pstr
# Test relative imports
from ..xml.dom import minidom as okay # Ensure config "xml.dom.minidom" doesn't catch relative imports

View File

@@ -0,0 +1 @@
print('hi')

View File

@@ -0,0 +1,2 @@
#!/bin/env/python
print('hi')

View File

@@ -0,0 +1 @@
import os # noqa: INP001

View File

@@ -0,0 +1,66 @@
import enum
from enum import Enum, unique
class FakeEnum1(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"
class FakeEnum10(enum.Enum):
A = enum.auto()
B = enum.auto()
C = enum.auto()

View File

@@ -0,0 +1,17 @@
{"foo": 1, **{"bar": 1}} # PIE800
foo({**foo, **{"bar": True}}) # PIE800
{**foo, **{"bar": 10}} # PIE800
{**foo, **buzz, **{bar: 10}} # PIE800
{**foo, "bar": True } # OK
{"foo": 1, "buzz": {"bar": 1}} # OK
{**foo, "bar": True } # OK
Table.objects.filter(inst=inst, **{f"foo__{bar}__exists": True}) # OK
buzz = {**foo, "bar": { 1: 2 }} # OK

View File

@@ -0,0 +1,19 @@
foo(**{"bar": True}) # PIE804
foo(**{"r2d2": True}) # PIE804
Foo.objects.create(**{"bar": True}) # PIE804
Foo.objects.create(**{"_id": some_id}) # PIE804
Foo.objects.create(**{**bar}) # PIE804
foo(**{**data, "foo": "buzz"})
foo(**buzz)
foo(**{"bar-foo": True})
foo(**{"bar foo": True})
foo(**{"1foo": True})
foo(**{buzz: True})
foo(**{"": True})
foo(**{f"buzz__{bar}": True})

View File

@@ -53,3 +53,25 @@ def test_list_of_tuples(param1, param2):
)
def test_list_of_lists(param1, param2):
...
@pytest.mark.parametrize(
"param1,param2",
[
[1, 2],
[3, 4],
],
)
def test_csv_name_list_of_lists(param1, param2):
...
@pytest.mark.parametrize(
"param",
[
[1, 2],
[3, 4],
],
)
def test_single_list_of_lists(param):
...

View File

@@ -1,25 +1,131 @@
if a: # SIM102
# SIM102
if a:
if b:
c
# SIM102
if a:
if b:
if c:
d
# SIM102
if a:
pass
elif b: # SIM102
elif b:
if c:
d
# SIM102
if a:
# Unfixable due to placement of this comment.
if b:
c
# SIM102
if a:
if b:
# Fixable due to placement of this comment.
c
# OK
if a:
if b:
c
else:
d
# OK
if __name__ == "__main__":
if foo():
...
# OK
if a:
d
if b:
c
while True:
# SIM102
if True:
if True:
"""this
is valid"""
"""the indentation on
this line is significant"""
"this is" \
"allowed too"
("so is"
"this for some reason")
# SIM102
if True:
if True:
"""this
is valid"""
"""the indentation on
this line is significant"""
"this is" \
"allowed too"
("so is"
"this for some reason")
while True:
# SIM102
if node.module:
if node.module == "multiprocessing" or node.module.startswith(
"multiprocessing."
):
print("Bad module!")
# SIM102
if node.module:
if node.module == "multiprocessing" or node.module.startswith(
"multiprocessing."
):
print("Bad module!")
# OK
if a:
if b:
print("foo")
else:
print("bar")
# OK
if a:
if b:
if c:
print("foo")
else:
print("bar")
else:
print("bar")
# OK
if a:
# SIM 102
if b:
if c:
print("foo")
else:
print("bar")
# OK
if a:
if b:
if c:
print("foo")
print("baz")
else:
print("bar")

View File

@@ -1,12 +1,35 @@
def f():
if a: # SIM103
# SIM103
if a:
return True
else:
return False
def f():
if a: # OK
# SIM103
if a:
return 1
elif b:
return True
else:
return False
def f():
# SIM103
if a:
return 1
else:
if b:
return True
else:
return False
def f():
# OK
if a:
foo()
return True
else:
@@ -14,7 +37,16 @@ def f():
def f():
if a: # OK
# OK
if a:
return "foo"
else:
return False
def f():
# SIM103 (but not fixable)
if a:
return False
else:
return True

View File

@@ -41,3 +41,21 @@ except ValueError:
pass
finally:
print('bar')
try:
foo()
foo()
except ValueError:
pass
try:
for i in range(3):
foo()
except ValueError:
pass
def bar():
try:
return foo()
except ValueError:
pass

View File

@@ -1,7 +1,31 @@
# Bad
# SIM109
if a == b or a == c:
d
# Good
# SIM109
if (a == b or a == c) and None:
d
# SIM109
if a == b or a == c or None:
d
# SIM109
if a == b or None or a == c:
d
# OK
if a in (b, c):
d
d
# OK
if a == b or a == c():
d
# OK
if (
a == b
# This comment prevents us from raising SIM109
or a == c
):
d

View File

@@ -1,6 +1,36 @@
f = open('foo.txt') # SIM115
import contextlib
# SIM115
f = open("foo.txt")
data = f.read()
f.close()
with open('foo.txt') as f: # OK
# OK
with open("foo.txt") as f:
data = f.read()
# OK
with contextlib.ExitStack() as exit_stack:
f = exit_stack.enter_context(open("filename"))
# OK
with contextlib.ExitStack() as stack:
files = [stack.enter_context(open(fname)) for fname in filenames]
close_files = stack.pop_all().close
# OK
with contextlib.AsyncExitStack() as exit_stack:
f = await exit_stack.enter_async_context(open("filename"))
# OK (false negative)
with contextlib.ExitStack():
f = exit_stack.enter_context(open("filename"))
# SIM115
with contextlib.ExitStack():
f = open("filename")
# OK
with contextlib.ExitStack() as exit_stack:
exit_stack_ = exit_stack
f = exit_stack_.enter_context(open("filename"))

View File

@@ -1,13 +1,92 @@
with A() as a: # SIM117
# SIM117
with A() as a:
with B() as b:
print("hello")
# SIM117
with A():
with B():
with C():
print("hello")
# SIM117
with A() as a:
# Unfixable due to placement of this comment.
with B() as b:
print("hello")
# SIM117
with A() as a:
with B() as b:
# Fixable due to placement of this comment.
print("hello")
# OK
with A() as a:
a()
with B() as b:
print("hello")
# OK
with A() as a:
with B() as b:
print("hello")
a()
# OK
async with A() as a:
with B() as b:
print("hello")
# OK
with A() as a:
async with B() as b:
print("hello")
# OK
async with A() as a:
async with B() as b:
print("hello")
while True:
# SIM117
with A() as a:
with B() as b:
"""this
is valid"""
"""the indentation on
this line is significant"""
"this is" \
"allowed too"
("so is"
"this for some reason")
# SIM117
with (
A() as a,
B() as b,
):
with C() as c:
print("hello")
# SIM117
with A() as a:
with (
B() as b,
C() as c,
):
print("hello")
# SIM117
with (
A() as a,
B() as b,
):
with (
C() as c,
D() as d,
):
print("hello")

View File

@@ -1,17 +1,27 @@
if not a == b: # SIM201
# SIM201
if not a == b:
pass
if not a == (b + c): # SIM201
# SIM201
if not a == (b + c):
pass
if not (a + b) == c: # SIM201
# SIM201
if not (a + b) == c:
pass
if not a != b: # OK
# OK
if not a != b:
pass
if a == b: # OK
# OK
if a == b:
pass
if not a == b: # OK
# OK
if not a == b:
raise ValueError()
# OK
def __ne__(self, other):
return not self == other

View File

@@ -1,14 +1,27 @@
if not a != b: # SIM202
# SIM202
if not a != b:
pass
if not a != (b + c): # SIM202
# SIM202
if not a != (b + c):
pass
if not (a + b) != c: # SIM202
# SIM202
if not (a + b) != c:
pass
if not a == b: # OK
# OK
if not a == b:
pass
if a != b: # OK
# OK
if a != b:
pass
# OK
if not a != b:
raise ValueError()
# OK
def __eq__(self, other):
return not self != other

View File

@@ -7,8 +7,12 @@ if (a or b) or True: # SIM223
if a or (b or True): # SIM223
pass
if a and True:
if a and True: # OK
pass
if True:
if True: # OK
pass
def validate(self, value):
return json.loads(value) or True # OK

View File

@@ -2,6 +2,9 @@
"yoda" == compare # SIM300
'yoda' == compare # SIM300
42 == age # SIM300
"yoda" <= compare # SIM300
'yoda' < compare # SIM300
42 > age # SIM300
# OK
compare == "yoda"

View File

@@ -85,3 +85,26 @@ if key in a_dict:
var = a_dict[key]
else:
var = foo()
# OK (complex default value)
if key in a_dict:
var = a_dict[key]
else:
var = a_dict["fallback"]
# OK (false negative for elif)
if foo():
pass
elif key in a_dict:
vars[idx] = a_dict[key]
else:
vars[idx] = "default"
# OK (false negative for nested else)
if foo():
pass
else:
if key in a_dict:
vars[idx] = a_dict[key]
else:
vars[idx] = "default"

View File

@@ -0,0 +1,25 @@
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
pass # TYP005
def example():
if TYPE_CHECKING:
pass # TYP005
return
class Test:
if TYPE_CHECKING:
pass # TYP005
x = 2
if TYPE_CHECKING:
if 2:
pass
if TYPE_CHECKING:
x: List

View File

@@ -0,0 +1,31 @@
import os
import os.path
p = "/foo"
a = os.path.abspath(p)
aa = os.chmod(p)
aaa = os.mkdir(p)
os.makedirs(p)
os.rename(p)
os.replace(p)
os.rmdir(p)
os.remove(p)
os.unlink(p)
os.getcwd(p)
b = os.path.exists(p)
bb = os.path.expanduser(p)
bbb = os.path.isdir(p)
bbbb = os.path.isfile(p)
bbbbb = os.path.islink(p)
os.readlink(p)
os.stat(p)
os.path.isabs(p)
os.path.join(p)
os.path.basename(p)
os.path.dirname(p)
os.path.samefile(p)
os.path.splitext(p)
with open(p) as fp:
fp.read()
open(p).close()

View File

@@ -0,0 +1,28 @@
import os as foo
import os.path as foo_p
p = "/foo"
a = foo_p.abspath(p)
aa = foo.chmod(p)
aaa = foo.mkdir(p)
foo.makedirs(p)
foo.rename(p)
foo.replace(p)
foo.rmdir(p)
foo.remove(p)
foo.unlink(p)
foo.getcwd(p)
b = foo_p.exists(p)
bb = foo_p.expanduser(p)
bbb = foo_p.isdir(p)
bbbb = foo_p.isfile(p)
bbbbb = foo_p.islink(p)
foo.readlink(p)
foo.stat(p)
foo_p.isabs(p)
foo_p.join(p)
foo_p.basename(p)
foo_p.dirname(p)
foo_p.samefile(p)
foo_p.splitext(p)

View File

@@ -0,0 +1,33 @@
from os import chmod, mkdir, makedirs, rename, replace, rmdir
from os import remove, unlink, getcwd, readlink, stat
from os.path import abspath, exists, expanduser, isdir, isfile, islink
from os.path import isabs, join, basename, dirname, samefile, splitext
p = "/foo"
a = abspath(p)
aa = chmod(p)
aaa = mkdir(p)
makedirs(p)
rename(p)
replace(p)
rmdir(p)
remove(p)
unlink(p)
getcwd(p)
b = exists(p)
bb = expanduser(p)
bbb = isdir(p)
bbbb = isfile(p)
bbbbb = islink(p)
readlink(p)
stat(p)
isabs(p)
join(p)
basename(p)
dirname(p)
samefile(p)
splitext(p)
with open(p) as fp:
fp.read()
open(p).close()

View File

@@ -0,0 +1,35 @@
from os import chmod as xchmod, mkdir as xmkdir
from os import makedirs as xmakedirs, rename as xrename, replace as xreplace
from os import rmdir as xrmdir, remove as xremove, unlink as xunlink
from os import getcwd as xgetcwd, readlink as xreadlink, stat as xstat
from os.path import abspath as xabspath, exists as xexists
from os.path import expanduser as xexpanduser, isdir as xisdir
from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
from os.path import join as xjoin, basename as xbasename, dirname as xdirname
from os.path import samefile as xsamefile, splitext as xsplitext
p = "/foo"
a = xabspath(p)
aa = xchmod(p)
aaa = xmkdir(p)
xmakedirs(p)
xrename(p)
xreplace(p)
xrmdir(p)
xremove(p)
xunlink(p)
xgetcwd(p)
b = xexists(p)
bb = xexpanduser(p)
bbb = xisdir(p)
bbbb = xisfile(p)
bbbbb = xislink(p)
xreadlink(p)
xstat(p)
xisabs(p)
xjoin(p)
xbasename(p)
xdirname(p)
xsamefile(p)
xsplitext(p)

View File

@@ -0,0 +1,3 @@
import py
p = py.path.local("../foo")

View File

@@ -0,0 +1,3 @@
from py.path import local as path
p = path("/foo")

View File

@@ -1 +0,0 @@
from long_module_name import member_one, member_two, member_three, member_four, member_five

View File

@@ -0,0 +1,9 @@
from __future__ import annotations
from typing import Any
from requests import Session
from my_first_party import my_first_party_object
from . import my_local_folder_object

View File

@@ -0,0 +1,2 @@
from sklearn.svm import XYZ, func, variable, Const, Klass, constant
from subprocess import First, var, func, Class, konst, A_constant, Last, STDOUT

View File

@@ -0,0 +1,2 @@
from sklearn.svm import VAR, Class, MyVar, CONST, abc
from subprocess import utils, var_ABC, Variable, Klass, CONSTANT, exe

View File

@@ -1,5 +1,7 @@
import collections
from collections import namedtuple
from typing import TypeVar
from typing import NewType
GLOBAL: str = "foo"
@@ -11,5 +13,9 @@ def f():
Camel = 0
CONSTANT = 0
_ = 0
MyObj1 = collections.namedtuple("MyObj1", ["a", "b"])
MyObj2 = namedtuple("MyObj12", ["a", "b"])
T = TypeVar("T")
UserId = NewType('UserId', int)

View File

@@ -0,0 +1,19 @@
def func_all_spaces():
# No error
print("spaces")
def func_tabs():
# No error
print("tabs")
def func_mixed_start_with_tab():
# E101
print("mixed starts with tab")
def func_mixed_start_with_space():
# E101
print("mixed starts with space")
def xyz():
# E101
print("xyz");

View File

@@ -579,3 +579,58 @@ def multiline_trailing_and_leading_space():
"or exclamation point (not '\"')")
def endswith_quote():
"""Whitespace at the end, but also a quote" """
@expect('D209: Multi-line docstring closing quotes should be on a separate '
'line')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfljdjgf24():
"""Summary.
Description. """
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def one_liner():
"""
Wrong."""
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def one_liner():
r"""Wrong.
"""
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def one_liner():
"""Wrong."
"""
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def one_liner():
"""
"Wrong."""
@expect('D404: First word of the docstring should not be "This"')
def starts_with_this():
"""This is a docstring."""
@expect('D404: First word of the docstring should not be "This"')
def starts_with_space_then_this():
""" This is a docstring that starts with a space.""" # noqa: D210

View File

@@ -0,0 +1,101 @@
"""This module docstring does not need to be written in imperative mood."""
from functools import cached_property
# Bad examples
def bad_liouiwnlkjl():
"""Returns foo."""
def bad_sdgfsdg23245():
"""Constructor for a foo."""
def bad_sdgfsdg23245777():
"""
Constructor for a boa.
"""
def bad_run_something():
"""Runs something"""
def bad_nested():
"""Runs other things, nested"""
bad_nested()
def multi_line():
"""Writes a logical line that
extends to two physical lines.
"""
# Good examples
def good_run_something():
"""Run away."""
def good_nested():
"""Run to the hills."""
good_nested()
def good_construct():
"""Construct a beautiful house."""
def good_multi_line():
"""Write a logical line that
extends to two physical lines.
"""
good_top_level_var = False
"""This top level assignment attribute docstring does not need to be written in imperative mood."""
# Classes and their members
class Thingy:
"""This class docstring does not need to be written in imperative mood."""
_beep = "boop"
"""This class attribute docstring does not need to be written in imperative mood."""
def bad_method(self):
"""This method docstring should be written in imperative mood."""
@property
def good_property(self):
"""This property method docstring does not need to be written in imperative mood."""
return self._beep
@cached_property
def good_cached_property(self):
"""This property method docstring does not need to be written in imperative mood."""
return 42 * 42
class NestedThingy:
"""This nested class docstring does not need to be written in imperative mood."""
# Test functions
def test_something():
"""This test function does not need to be written in imperative mood.
pydocstyle's rationale:
We exclude tests from the imperative mood check, because to phrase
their docstring in the imperative mood, they would have to start with
a highly redundant "Test that ..."
"""
def runTest():
"""This test function does not need to be written in imperative mood, either."""

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

@@ -1,51 +1,51 @@
if True:
import foo; x = 1
import foo; x = 1
import foo1; x = 1
import foo2; x = 1
if True:
import foo; \
import foo3; \
x = 1
if True:
import foo \
import foo4 \
; x = 1
if True:
x = 1; import foo
x = 1; import foo5
if True:
x = 1; \
import foo
import foo6
if True:
x = 1 \
; import foo
; import foo7
if True:
x = 1; import foo; x = 1
x = 1; import foo; x = 1
x = 1; import foo8; x = 1
x = 1; import foo9; x = 1
if True:
x = 1; \
import foo; \
import foo10; \
x = 1
if True:
x = 1 \
;import foo \
;import foo11 \
;x = 1
# Continuation, but not as the last content in the file.
x = 1; \
import foo
import foo12
# Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax
# error.)
x = 1; \
import foo
import foo13

View File

@@ -1,71 +1,69 @@
"""Check that magic values are not used in comparisons"""
import cmath
"""Check that magic values are not used in comparisons."""
user_input = 10
if 10 > user_input: # [magic-value-comparison]
if 10 > user_input: # [magic-value-comparison]
pass
if 10 == 100: # [comparison-of-constants] R0133
if 10 == 100: # [comparison-of-constants] R0133
pass
if 1 == 3: # [comparison-of-constants] R0133
if 1 == 3: # [comparison-of-constants] R0133
pass
x = 0
if 4 == 3 == x: # [comparison-of-constants] R0133
if 4 == 3 == x: # [comparison-of-constants] R0133
pass
time_delta = 7224
ONE_HOUR = 3600
if time_delta > ONE_HOUR: # correct
if time_delta > ONE_HOUR: # correct
pass
argc = 1
if argc != -1: # correct
if argc != -1: # correct
pass
if argc != 0: # correct
if argc != 0: # correct
pass
if argc != 1: # correct
if argc != 1: # correct
pass
if argc != 2: # [magic-value-comparison]
if argc != 2: # [magic-value-comparison]
pass
if __name__ == "__main__": # correct
if __name__ == "__main__": # correct
pass
ADMIN_PASSWORD = "SUPERSECRET"
input_password = "password"
if input_password == "": # correct
if input_password == "": # correct
pass
if input_password == ADMIN_PASSWORD: # correct
if input_password == ADMIN_PASSWORD: # correct
pass
if input_password == "Hunter2": # [magic-value-comparison]
if input_password == "Hunter2": # [magic-value-comparison]
pass
PI = 3.141592653589793238
pi_estimation = 3.14
if pi_estimation == 3.141592653589793238: # [magic-value-comparison]
if pi_estimation == 3.141592653589793238: # [magic-value-comparison]
pass
if pi_estimation == PI: # correct
if pi_estimation == PI: # correct
pass
HELLO_WORLD = b"Hello, World!"
user_input = b"Hello, There!"
if user_input == b"something": # [magic-value-comparison]
if user_input == b"something": # [magic-value-comparison]
pass
if user_input == HELLO_WORLD: # correct
if user_input == HELLO_WORLD: # correct
pass

View File

@@ -12,6 +12,9 @@ per-file-ignores = { "__init__.py" = ["F401"] }
[tool.ruff.flake8-bugbear]
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
[tool.ruff.flake8-builtins]
builtins-ignorelist = ["id", "dir"]
[tool.ruff.flake8-quotes]
inline-quotes = "single"
multiline-quotes = "double"

View File

@@ -1,5 +1,12 @@
type('')
type(b'')
type("")
type(b"")
type(0)
type(0.)
type(0.0)
type(0j)
# OK
y = x.dtype.type(0.0)
# OK
type = lambda *args, **kwargs: None
type("")

View File

@@ -0,0 +1,67 @@
import functools
from functools import lru_cache
@functools.lru_cache()
def fixme():
pass
@lru_cache()
def fixme():
pass
@other_decorator
@functools.lru_cache()
def fixme():
pass
@functools.lru_cache()
@other_decorator
def fixme():
pass
@functools.lru_cache(maxsize=None)
def ok():
pass
@lru_cache(maxsize=None)
def ok():
pass
@functools.lru_cache(maxsize=64)
def ok():
pass
@lru_cache(maxsize=64)
def ok():
pass
def user_func():
pass
@lru_cache(user_func)
def ok():
pass
@lru_cache(user_func, maxsize=None)
def ok():
pass
def lru_cache(maxsize=None):
pass
@lru_cache(maxsize=None)
def ok():
pass

View File

@@ -1,103 +0,0 @@
import functools
from functools import lru_cache
@lru_cache()
def fixme1():
pass
@other_deco_after
@functools.lru_cache()
def fixme2():
pass
@lru_cache(maxsize=None)
def fixme3():
pass
@functools.lru_cache(maxsize=None)
@other_deco_before
def fixme4():
pass
@lru_cache( # A
) # B
def fixme5():
pass
@lru_cache(
# A
) # B
def fixme6():
pass
@functools.lru_cache(
# A
maxsize = None) # B
def fixme7():
pass
@functools.lru_cache(
# A1
maxsize = None
# A2
) # B
def fixme8():
pass
@functools.lru_cache(
# A1
maxsize =
None
# A2
)
def fixme9():
pass
@functools.lru_cache(
# A1
maxsize =
None
# A2
)
def fixme10():
pass
@lru_cache
def correct1():
pass
@functools.lru_cache
def correct2():
pass
@functoools.lru_cache(maxsize=64)
def correct3():
pass
def user_func():
pass
@lru_cache(user_func)
def correct4():
pass
@lru_cache(user_func, maxsize=None)
def correct5():
pass

View File

@@ -1,15 +0,0 @@
import functools
def lru_cache(maxsize=None):
pass
@lru_cache()
def dont_fixme():
pass
@lru_cache(maxsize=None)
def dont_fixme():
pass

View File

@@ -31,3 +31,7 @@ MyType10 = TypedDict("MyType10", {"key": Literal["value"]})
# using namespace TypedDict
MyType11 = typing.TypedDict("MyType11", {"key": int})
# unpacking
c = {"c": float}
MyType12 = TypedDict("MyType1", {"a": int, "b": str, **c})

View File

@@ -0,0 +1,75 @@
a, b, x, y = 1, 2, 3, 4
# UP031
print('%s %s' % (a, b))
print('%s%s' % (a, b))
print("trivial" % ())
print("%s" % ("simple",))
print("%s" % ("%s" % ("nested",),))
print("%s%% percent" % (15,))
print("%f" % (15,))
print("%.f" % (15,))
print("%.3f" % (15,))
print("%3f" % (15,))
print("%-5f" % (5,))
print("%9f" % (5,))
print("%#o" % (123,))
print("brace {} %s" % (1,))
print(
"%s" % (
"trailing comma",
)
)
print("foo %s " % (x,))
print("%(k)s" % {"k": "v"})
print("%(k)s" % {
"k": "v",
"i": "j"
})
print("%(to_list)s" % {"to_list": []})
print("%(k)s" % {"k": "v", "i": 1, "j": []})
print("%(ab)s" % {"a" "b": 1})
print("%(a)s" % {"a" : 1})
print((
"foo %s "
"bar %s" % (x, y)
))
print(
"foo %(foo)s "
"bar %(bar)s" % {"foo": x, "bar": y}
)
bar = {"bar": y}
print(
"foo %(foo)s "
"bar %(bar)s" % {"foo": x, **bar}
)
print("%s \N{snowman}" % (a,))
print("%(foo)s \N{snowman}" % {"foo": 1})
print(("foo %s " "bar %s") % (x, y))

View File

@@ -0,0 +1,59 @@
# OK
"%s" % unknown_type
b"%s" % (b"bytestring",)
"%*s" % (5, "hi")
"%d" % (flt,)
"%c" % (some_string,)
"%4%" % ()
"%.2r" % (1.25)
i % 3
"%.*s" % (5, "hi")
"%i" % (flt,)
"%()s" % {"": "empty"}
"%s" % {"k": "v"}
"%(1)s" % {"1": "bar"}
"%(a)s" % {"a": 1, "a": 2}
pytest.param('"%8s" % (None,)', id="unsafe width-string conversion"),
"%()s" % {"": "bar"}
"%(1)s" % {1: 2, "1": 2}
"%(and)s" % {"and": 2}
# OK (arguably false negatives)
(
"foo %s "
"bar %s"
) % (x, y)
(
"foo %(foo)s "
"bar %(bar)s"
) % {"foo": x, "bar": y}
(
"""foo %s"""
% (x,)
)
(
"""
foo %s
"""
% (x,)
)

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

@@ -0,0 +1,67 @@
import functools
from functools import lru_cache
@functools.lru_cache(maxsize=None)
def fixme():
pass
@lru_cache(maxsize=None)
def fixme():
pass
@other_decorator
@functools.lru_cache(maxsize=None)
def fixme():
pass
@functools.lru_cache(maxsize=None)
@other_decorator
def fixme():
pass
@functools.lru_cache()
def ok():
pass
@lru_cache()
def ok():
pass
@functools.lru_cache(maxsize=64)
def ok():
pass
@lru_cache(maxsize=64)
def ok():
pass
def user_func():
pass
@lru_cache(user_func)
def ok():
pass
@lru_cache(user_func, maxsize=None)
def ok():
pass
def lru_cache(maxsize=None):
pass
@lru_cache(maxsize=None)
def ok():
pass

View File

@@ -0,0 +1,61 @@
# UP034
print(("foo"))
# UP034
print(("hell((goodybe))o"))
# UP034
print((("foo")))
# UP034
print((((1))))
# UP034
print(("foo{}".format(1)))
# UP034
print(
("foo{}".format(1))
)
# UP034
print(
(
"foo"
)
)
# UP034
def f():
x = int(((yield 1)))
# UP034
if True:
print(
("foo{}".format(1))
)
# UP034
print((x for x in range(3)))
# OK
print("foo")
# OK
print((1, 2, 3))
# OK
print(())
# OK
print((1,))
# OK
sum((block.code for block in blocks), [])
# OK
def f():
x = int((yield 1))
# OK
sum((i for i in range(3)), [])

39
resources/test/fixtures/ruff/RUF005.py vendored Normal file
View File

@@ -0,0 +1,39 @@
class Fun:
words = ("how", "fun!")
def yay(self):
return self.words
yay = Fun().yay
foo = [4, 5, 6]
bar = [1, 2, 3] + foo
zoob = tuple(bar)
quux = (7, 8, 9) + zoob
spam = quux + (10, 11, 12)
spom = list(spam)
eggs = spom + [13, 14, 15]
elatement = ("we all say", ) + yay()
excitement = ("we all think", ) + Fun().yay()
astonishment = ("we all feel", ) + Fun.words
chain = ['a', 'b', 'c'] + eggs + list(('yes', 'no', 'pants') + zoob)
baz = () + zoob
first = [
# The order
1, # here
2, # is
# extremely
3, # critical
# to preserve
]
second = first + [
# please
4,
# don't
5,
# touch
6,
]

View File

@@ -1,64 +1,89 @@
# This should ignore both errors.
from typing import ( # noqa: F401
List,
Sequence,
)
def f():
# This should ignore both errors.
from typing import ( # noqa: F401
List,
Sequence,
)
# This should ignore both errors.
from typing import ( # noqa
List,
Sequence,
)
# This should ignore both errors.
from typing import (
List, # noqa: F401
Sequence, # noqa: F401
)
def f():
# This should ignore both errors.
from typing import ( # noqa
List,
Sequence,
)
# This should ignore both errors.
from typing import (
List, # noqa
Sequence, # noqa
)
# This should ignore the first error.
from typing import (
Mapping, # noqa: F401
Union,
)
def f():
# This should ignore both errors.
from typing import (
List, # noqa: F401
Sequence, # noqa: F401
)
# This should ignore both errors.
from typing import ( # noqa
List,
Sequence,
)
# This should ignore the error, but the inner noqa should be marked as unused.
from typing import ( # noqa: F401
Optional, # noqa: F401
)
def f():
# This should ignore both errors.
from typing import (
List, # noqa
Sequence, # noqa
)
# This should ignore the error, but the inner noqa should be marked as unused.
from typing import ( # noqa
Optional, # noqa: F401
)
# This should ignore the error, but mark F501 as unused.
from typing import ( # noqa: F401
Dict, # noqa: F501
)
def f():
# This should ignore the first error.
from typing import (
Mapping, # noqa: F401
Union,
)
# This should ignore the error, but mark F501 as unused.
from typing import ( # noqa: F501
Tuple, # noqa: F401
)
# This should ignore both errors.
from typing import Any, AnyStr # noqa: F401
def f():
# This should ignore both errors.
from typing import ( # noqa
List,
Sequence,
)
# This should ignore both errors.
from typing import AsyncIterable, AsyncGenerator # noqa
# This should mark F501 as unused.
from typing import Awaitable, AwaitableGenerator # noqa: F501
def f():
# This should ignore the error, but the inner noqa should be marked as unused.
from typing import ( # noqa: F401
Optional, # noqa: F401
)
def f():
# This should ignore the error, but the inner noqa should be marked as unused.
from typing import ( # noqa
Optional, # noqa: F401
)
def f():
# This should ignore the error, but mark F501 as unused.
from typing import ( # noqa: F401
Dict, # noqa: F501
)
def f():
# This should ignore the error, but mark F501 as unused.
from typing import ( # noqa: F501
Tuple, # noqa: F401
)
def f():
# This should ignore both errors.
from typing import Any, AnyStr # noqa: F401
def f():
# This should ignore both errors.
from typing import AsyncIterable, AsyncGenerator # noqa
def f():
# This should mark F501 as unused.
from typing import Awaitable, AwaitableGenerator # noqa: F501

View File

@@ -0,0 +1,296 @@
"""
Violation:
Prefer TypeError when relevant.
"""
def incorrect_basic(some_arg):
if isinstance(some_arg, int):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_multiple_type_check(some_arg):
if isinstance(some_arg, (int, str)):
pass
else:
raise Exception("...") # should be typeerror
class MyClass:
pass
def incorrect_with_issubclass(some_arg):
if issubclass(some_arg, MyClass):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_with_callable(some_arg):
if callable(some_arg):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_ArithmeticError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise ArithmeticError("...") # should be typeerror
def incorrect_AssertionError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise AssertionError("...") # should be typeerror
def incorrect_AttributeError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise AttributeError("...") # should be typeerror
def incorrect_BufferError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise BufferError # should be typeerror
def incorrect_EOFError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise EOFError("...") # should be typeerror
def incorrect_ImportError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise ImportError("...") # should be typeerror
def incorrect_LookupError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise LookupError("...") # should be typeerror
def incorrect_MemoryError(some_arg):
if isinstance(some_arg, int):
pass
else:
# should be typeerror
# not multiline is on purpose for fix
raise MemoryError(
"..."
)
def incorrect_NameError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise NameError("...") # should be typeerror
def incorrect_ReferenceError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise ReferenceError("...") # should be typeerror
def incorrect_RuntimeError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise RuntimeError("...") # should be typeerror
def incorrect_SyntaxError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise SyntaxError("...") # should be typeerror
def incorrect_SystemError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise SystemError("...") # should be typeerror
def incorrect_ValueError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise ValueError("...") # should be typeerror
def incorrect_not_operator_isinstance(some_arg):
if not isinstance(some_arg):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_and_operator_isinstance(arg1, arg2):
if isinstance(some_arg) and isinstance(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_or_operator_isinstance(arg1, arg2):
if isinstance(some_arg) or isinstance(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_multiple_operators_isinstance(arg1, arg2, arg3):
if not isinstance(arg1) and isinstance(arg2) or isinstance(arg3):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_not_operator_callable(some_arg):
if not callable(some_arg):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_and_operator_callable(arg1, arg2):
if callable(some_arg) and callable(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_or_operator_callable(arg1, arg2):
if callable(some_arg) or callable(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_multiple_operators_callable(arg1, arg2, arg3):
if not callable(arg1) and callable(arg2) or callable(arg3):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_not_operator_issubclass(some_arg):
if not issubclass(some_arg):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_and_operator_issubclass(arg1, arg2):
if issubclass(some_arg) and issubclass(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_or_operator_issubclass(arg1, arg2):
if issubclass(some_arg) or issubclass(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_multiple_operators_issubclass(arg1, arg2, arg3):
if not issubclass(arg1) and issubclass(arg2) or issubclass(arg3):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_multi_conditional(arg1, arg2):
if isinstance(arg1, int):
pass
elif isinstance(arg2, int):
raise Exception("...") # should be typeerror
class MyCustomTypeValidation(Exception):
pass
def correct_custom_exception(some_arg):
if isinstance(some_arg, int):
pass
else:
raise MyCustomTypeValidation("...") # that's correct, because it's not vanilla
def correct_complex_conditional(val):
if val is not None and (not isinstance(val, int) or val < 0):
raise ValueError(...) # fine if this is not a TypeError
def correct_multi_conditional(some_arg):
if some_arg == 3:
pass
elif isinstance(some_arg, int):
pass
else:
raise Exception("...") # fine if this is not a TypeError
def correct_should_ignore(some_arg):
if isinstance(some_arg, int):
pass
else:
raise TypeError("...")
def check_body(some_args):
if isinstance(some_args, int):
raise ValueError("...") # should be typeerror
def check_body_correct(some_args):
if isinstance(some_args, int):
raise TypeError("...") # correct
def multiple_elifs(some_args):
if not isinstance(some_args, int):
raise ValueError("...") # should be typerror
elif some_args < 3:
raise ValueError("...") # this is ok
elif some_args > 10:
raise ValueError("...") # this is ok if we don't simplify
else:
pass
def multiple_ifs(some_args):
if not isinstance(some_args, int):
raise ValueError("...") # should be typerror
else:
if some_args < 3:
raise ValueError("...") # this is ok
else:
if some_args > 10:
raise ValueError("...") # this is ok if we don't simplify
else:
pass

View File

@@ -0,0 +1,54 @@
"""
Violation:
Raising an exception using its assigned name is verbose and unrequired
"""
import logging
logger = logging.getLogger(__name__)
class MyException(Exception):
pass
def bad():
try:
process()
except MyException as e:
logger.exception("process failed")
raise e
def good():
try:
process()
except MyException:
logger.exception("process failed")
raise
def still_good():
try:
process()
except MyException as e:
print(e)
raise
def bad_that_needs_recursion():
try:
process()
except MyException as e:
logger.exception("process failed")
if True:
raise e
def bad_that_needs_recursion_2():
try:
process()
except MyException as e:
logger.exception("process failed")
if True:
def foo():
raise e

View File

@@ -0,0 +1,47 @@
"""
Violation:
Returning a final value inside a try block may indicate you could use an else block
instead to outline the success scenario
"""
import logging
logger = logging.getLogger(__name__)
class MyException(Exception):
pass
def bad():
try:
a = 1
b = process()
return b
except MyException:
logger.exception("process failed")
def good():
try:
a = 1
b = process()
except MyException:
logger.exception("process failed")
else:
return b
def noreturn():
try:
a = 1
b = process()
except MyException:
logger.exception("process failed")
def still_good():
try:
return process()
except MyException:
logger.exception("process failed")

View File

@@ -73,7 +73,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"extend-select": {
@@ -83,7 +83,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"external": {
@@ -117,7 +117,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"flake8-annotations": {
@@ -153,6 +153,17 @@
}
]
},
"flake8-builtins": {
"description": "Options for the `flake8-builtins` plugin.",
"anyOf": [
{
"$ref": "#/definitions/Flake8BuiltinsOptions"
},
{
"type": "null"
}
]
},
"flake8-errmsg": {
"description": "Options for the `flake8-errmsg` plugin.",
"anyOf": [
@@ -227,7 +238,7 @@
]
},
"format": {
"description": "The style in which violation messages should be formatted: `\"text\"` (default), `\"grouped\"` (group messages by file), `\"json\"` (machine-readable), `\"junit\"` (machine-readable XML), `\"github\"` (GitHub Actions annotations) or `\"gitlab\"` (GitLab CI code quality report).",
"description": "The style in which violation messages should be formatted: `\"text\"` (default), `\"grouped\"` (group messages by file), `\"json\"` (machine-readable), `\"junit\"` (machine-readable XML), `\"github\"` (GitHub Actions annotations), `\"gitlab\"` (GitLab CI code quality report), or `\"pylint\"` (Pylint text format).",
"anyOf": [
{
"$ref": "#/definitions/SerializationFormat"
@@ -244,11 +255,11 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"ignore-init-module-imports": {
"description": "Avoid automatically removing unused imports in `__init__.py` files. Such imports will still be +flagged, but with a dedicated message suggesting that the import is either added to the module' +`__all__` symbol, or re-exported with a redundant alias (e.g., `import os as os`).",
"description": "Avoid automatically removing unused imports in `__init__.py` files. Such imports will still be flagged, but with a dedicated message suggesting that the import is either added to the module's `__all__` symbol, or re-exported with a redundant alias (e.g., `import os as os`).",
"type": [
"boolean",
"null"
@@ -285,6 +296,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": [
@@ -305,7 +326,7 @@
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
}
},
@@ -331,6 +352,17 @@
}
]
},
"pylint": {
"description": "Options for the `pylint` plugin.",
"anyOf": [
{
"$ref": "#/definitions/PylintOptions"
},
{
"type": "null"
}
]
},
"pyupgrade": {
"description": "Options for the `pyupgrade` plugin.",
"anyOf": [
@@ -367,7 +399,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"show-source": {
@@ -425,7 +457,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"update-check": {
@@ -438,7 +470,7 @@
},
"additionalProperties": false,
"definitions": {
"BannedApi": {
"ApiBan": {
"type": "object",
"required": [
"msg"
@@ -451,6 +483,17 @@
},
"additionalProperties": false
},
"ConstantType": {
"type": "string",
"enum": [
"bytes",
"complex",
"float",
"int",
"str",
"tuple"
]
},
"Convention": {
"oneOf": [
{
@@ -552,6 +595,22 @@
},
"additionalProperties": false
},
"Flake8BuiltinsOptions": {
"type": "object",
"properties": {
"builtins-ignorelist": {
"description": "Ignore list of builtins.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"Flake8ErrMsgOptions": {
"type": "object",
"properties": {
@@ -733,7 +792,7 @@
"null"
],
"additionalProperties": {
"$ref": "#/definitions/BannedApi"
"$ref": "#/definitions/ApiBan"
}
}
},
@@ -752,6 +811,16 @@
},
"additionalProperties": false
},
"ImportType": {
"type": "string",
"enum": [
"future",
"standard-library",
"third-party",
"first-party",
"local-folder"
]
},
"IsortOptions": {
"type": "object",
"properties": {
@@ -772,6 +841,16 @@
"null"
]
},
"constants": {
"description": "An override list of tokens to always recognize as a CONSTANT for `order-by-type` regardless of casing.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"extra-standard-library": {
"description": "A list of modules to consider standard-library, in addition to those known to Ruff in advance.",
"type": [
@@ -823,6 +902,16 @@
"type": "string"
}
},
"no-lines-before": {
"description": "A list of sections that should _not_ be delineated from the previous section via empty lines.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/ImportType"
}
},
"order-by-type": {
"description": "Order imports by type, which is determined by case, in addition to alphabetically.",
"type": [
@@ -867,6 +956,16 @@
"boolean",
"null"
]
},
"variables": {
"description": "An override list of tokens to always recognize as a var for `order-by-type` regardless of casing.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"additionalProperties": false
@@ -996,6 +1095,22 @@
},
"additionalProperties": false
},
"PylintOptions": {
"type": "object",
"properties": {
"allow-magic-value-types": {
"description": "Constant types to ignore when used as \"magic values\".",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/ConstantType"
}
}
},
"additionalProperties": false
},
"PythonVersion": {
"type": "string",
"enum": [
@@ -1046,7 +1161,7 @@
}
]
},
"RuleCodePrefix": {
"RuleSelector": {
"type": "string",
"enum": [
"A",
@@ -1146,6 +1261,12 @@
"C9",
"C90",
"C901",
"COM",
"COM8",
"COM81",
"COM812",
"COM818",
"COM819",
"D",
"D1",
"D10",
@@ -1183,6 +1304,7 @@
"D4",
"D40",
"D400",
"D401",
"D402",
"D403",
"D404",
@@ -1216,6 +1338,9 @@
"DTZ011",
"DTZ012",
"E",
"E1",
"E10",
"E101",
"E4",
"E40",
"E401",
@@ -1253,6 +1378,12 @@
"ERA0",
"ERA00",
"ERA001",
"EXE",
"EXE0",
"EXE00",
"EXE003",
"EXE004",
"EXE005",
"F",
"F4",
"F40",
@@ -1340,6 +1471,10 @@
"ICN0",
"ICN00",
"ICN001",
"INP",
"INP0",
"INP00",
"INP001",
"ISC",
"ISC0",
"ISC00",
@@ -1415,18 +1550,18 @@
"PIE79",
"PIE790",
"PIE794",
"PIE796",
"PIE8",
"PIE80",
"PIE800",
"PIE804",
"PIE807",
"PL",
"PLC",
"PLC0",
"PLC04",
"PLC041",
"PLC0414",
"PLC2",
"PLC22",
"PLC220",
"PLC2201",
"PLC3",
"PLC30",
"PLC300",
@@ -1500,6 +1635,36 @@
"PT024",
"PT025",
"PT026",
"PTH",
"PTH1",
"PTH10",
"PTH100",
"PTH101",
"PTH102",
"PTH103",
"PTH104",
"PTH105",
"PTH106",
"PTH107",
"PTH108",
"PTH109",
"PTH11",
"PTH110",
"PTH111",
"PTH112",
"PTH113",
"PTH114",
"PTH115",
"PTH116",
"PTH117",
"PTH118",
"PTH119",
"PTH12",
"PTH120",
"PTH121",
"PTH122",
"PTH123",
"PTH124",
"Q",
"Q0",
"Q00",
@@ -1536,6 +1701,7 @@
"RUF002",
"RUF003",
"RUF004",
"RUF005",
"RUF1",
"RUF10",
"RUF100",
@@ -1561,6 +1727,9 @@
"S506",
"S508",
"S509",
"S6",
"S61",
"S612",
"S7",
"S70",
"S701",
@@ -1614,6 +1783,20 @@
"TID25",
"TID251",
"TID252",
"TRY",
"TRY0",
"TRY00",
"TRY004",
"TRY2",
"TRY20",
"TRY201",
"TRY3",
"TRY30",
"TRY300",
"TYP",
"TYP0",
"TYP00",
"TYP005",
"U",
"U0",
"U00",
@@ -1670,6 +1853,10 @@
"UP029",
"UP03",
"UP030",
"UP031",
"UP032",
"UP033",
"UP034",
"W",
"W2",
"W29",
@@ -1707,7 +1894,8 @@
"junit",
"grouped",
"github",
"gitlab"
"gitlab",
"pylint"
]
},
"Strictness": {

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.221"
version = "0.0.231"
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");
@@ -35,10 +35,19 @@ fn content_dir() -> &'static Path {
Path::new("content")
}
fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: flags::Autofix) -> u64 {
fn cache_key<P: AsRef<Path>>(
path: P,
package: Option<&P>,
settings: &Settings,
autofix: flags::Autofix,
) -> u64 {
let mut hasher = DefaultHasher::new();
CARGO_PKG_VERSION.hash(&mut hasher);
path.as_ref().absolutize().unwrap().hash(&mut hasher);
package
.as_ref()
.map(|path| path.as_ref().absolutize().unwrap())
.hash(&mut hasher);
settings.hash(&mut hasher);
autofix.hash(&mut hasher);
hasher.finish()
@@ -79,11 +88,16 @@ fn read_sync(cache_dir: &Path, key: u64) -> Result<Vec<u8>, std::io::Error> {
/// Get a value from the cache.
pub fn get<P: AsRef<Path>>(
path: P,
package: Option<&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, package, &settings.lib, autofix),
)
.ok()?;
let (mtime, messages) = match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
metadata: CacheMetadata { mtime },
@@ -103,8 +117,9 @@ pub fn get<P: AsRef<Path>>(
/// Set a value in the cache.
pub fn set<P: AsRef<Path>>(
path: P,
package: Option<&P>,
metadata: &fs::Metadata,
settings: &Settings,
settings: &AllSettings,
autofix: flags::Autofix,
messages: &[Message],
) {
@@ -115,8 +130,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, package, &settings.lib, autofix),
&bincode::serialize(&check_result).unwrap(),
) {
error!("Failed to write to cache: {e:?}");

View File

@@ -1,14 +1,13 @@
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use clap::{command, Parser};
use regex::Regex;
use ruff::logging::LogLevel;
use ruff::registry::{RuleCode, RuleCodePrefix};
use ruff::registry::{Rule, RuleSelector};
use ruff::resolver::ConfigProcessor;
use ruff::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat,
};
use ruff::{fs, mccabe};
use rustc_hash::FxHashMap;
#[derive(Debug, Parser)]
@@ -66,18 +65,18 @@ pub struct Cli {
/// Comma-separated list of rule codes to enable (or ALL, to enable all
/// rules).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub select: Option<Vec<RuleCodePrefix>>,
pub select: Option<Vec<RuleSelector>>,
/// Like --select, but adds additional rule codes on top of the selected
/// ones.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub extend_select: Option<Vec<RuleCodePrefix>>,
pub extend_select: Option<Vec<RuleSelector>>,
/// Comma-separated list of rule codes to disable.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub ignore: Option<Vec<RuleCodePrefix>>,
pub ignore: Option<Vec<RuleSelector>>,
/// Like --ignore, but adds additional rule codes on top of the ignored
/// ones.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub extend_ignore: Option<Vec<RuleCodePrefix>>,
pub extend_ignore: Option<Vec<RuleSelector>>,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(long, value_delimiter = ',', value_name = "FILE_PATTERN")]
pub exclude: Option<Vec<FilePattern>>,
@@ -88,11 +87,11 @@ pub struct Cli {
/// List of rule codes to treat as eligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub fixable: Option<Vec<RuleCodePrefix>>,
pub fixable: Option<Vec<RuleSelector>>,
/// List of rule codes to treat as ineligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub unfixable: Option<Vec<RuleCodePrefix>>,
pub unfixable: Option<Vec<RuleSelector>>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
@@ -137,9 +136,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,
@@ -172,6 +168,7 @@ pub struct Cli {
/// Explain a rule.
#[arg(
long,
value_parser=Rule::from_code,
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
@@ -183,7 +180,7 @@ pub struct Cli {
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub explain: Option<RuleCode>,
pub explain: Option<&'static Rule>,
/// Generate shell completion
#[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,
@@ -306,7 +302,7 @@ pub struct Arguments {
pub config: Option<PathBuf>,
pub diff: bool,
pub exit_zero: bool,
pub explain: Option<RuleCode>,
pub explain: Option<&'static Rule>,
pub files: Vec<PathBuf>,
pub generate_shell_completion: Option<clap_complete_command::Shell>,
pub isolated: bool,
@@ -327,18 +323,17 @@ pub struct Overrides {
pub dummy_variable_rgx: Option<Regex>,
pub exclude: Option<Vec<FilePattern>>,
pub extend_exclude: Option<Vec<FilePattern>>,
pub extend_ignore: Option<Vec<RuleCodePrefix>>,
pub extend_select: Option<Vec<RuleCodePrefix>>,
pub fixable: Option<Vec<RuleCodePrefix>>,
pub ignore: Option<Vec<RuleCodePrefix>>,
pub extend_ignore: Option<Vec<RuleSelector>>,
pub extend_select: Option<Vec<RuleSelector>>,
pub fixable: Option<Vec<RuleSelector>>,
pub ignore: Option<Vec<RuleSelector>>,
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>>,
pub select: Option<Vec<RuleSelector>>,
pub show_source: Option<bool>,
pub target_version: Option<PythonVersion>,
pub unfixable: Option<Vec<RuleCodePrefix>>,
pub unfixable: Option<Vec<RuleSelector>>,
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
pub cache_dir: Option<PathBuf>,
pub fix: Option<bool>,
@@ -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()));
}
@@ -444,7 +434,7 @@ pub fn extract_log_level(cli: &Arguments) -> LogLevel {
/// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`.
pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgnore> {
let mut per_file_ignores: FxHashMap<String, Vec<RuleCodePrefix>> = FxHashMap::default();
let mut per_file_ignores: FxHashMap<String, Vec<RuleSelector>> = FxHashMap::default();
for pair in pairs {
per_file_ignores
.entry(pair.pattern)
@@ -453,9 +443,6 @@ pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgn
}
per_file_ignores
.into_iter()
.map(|(pattern, prefixes)| {
let absolute = fs::normalize_path(Path::new(&pattern));
PerFileIgnore::new(pattern, absolute, &prefixes)
})
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes, None))
.collect()
}

View File

@@ -15,11 +15,11 @@ use ruff::cache::CACHE_DIR_NAME;
use ruff::linter::add_noqa_to_path;
use ruff::logging::LogLevel;
use ruff::message::{Location, Message};
use ruff::registry::RuleCode;
use ruff::registry::{Linter, Rule, RuleNamespace};
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, AutofixAvailability, 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()))
}
@@ -112,9 +114,9 @@ pub fn run(
.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
let settings = resolver.resolve(path, pyproject_strategy);
if settings.enabled.contains(&RuleCode::E902) {
if settings.rules.enabled(&Rule::IOError) {
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)
}
@@ -283,28 +285,39 @@ pub fn show_files(
#[derive(Serialize)]
struct Explanation<'a> {
code: &'a str,
origin: &'a str,
linter: &'a str,
summary: &'a str,
}
/// Explain a `RuleCode` to the user.
pub fn explain(code: &RuleCode, format: SerializationFormat) -> Result<()> {
/// Explain a `Rule` to the user.
pub fn explain(rule: &Rule, format: SerializationFormat) -> Result<()> {
let (linter, _) = Linter::parse_code(rule.code()).unwrap();
match format {
SerializationFormat::Text | SerializationFormat::Grouped => {
println!(
"{} ({}): {}",
code.as_ref(),
code.origin().title(),
code.kind().summary()
);
println!("{}\n", rule.as_ref());
println!("Code: {} ({})\n", rule.code(), linter.name());
if let Some(autofix) = rule.autofixable() {
println!(
"{}",
match autofix.available {
AutofixAvailability::Sometimes => "Autofix is sometimes available.\n",
AutofixAvailability::Always => "Autofix is always available.\n",
}
);
}
println!("Message formats:\n");
for format in rule.message_formats() {
println!("* {format}");
}
}
SerializationFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&Explanation {
code: code.as_ref(),
origin: code.origin().title(),
summary: &code.kind().summary(),
code: rule.code(),
linter: linter.name(),
summary: rule.message_formats()[0],
})?
);
}
@@ -317,6 +330,9 @@ pub fn explain(code: &RuleCode, format: SerializationFormat) -> Result<()> {
SerializationFormat::Gitlab => {
bail!("`--explain` does not support GitLab format")
}
SerializationFormat::Pylint => {
bail!("`--explain` does not support pylint format")
}
};
Ok(())
}

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
@@ -55,7 +55,9 @@ pub fn lint_path(
&& matches!(autofix, fix::FixMode::None | fix::FixMode::Generate)
{
let metadata = path.metadata()?;
if let Some(messages) = cache::get(path, &metadata, settings, autofix.into()) {
if let Some(messages) =
cache::get(path, package.as_ref(), &metadata, settings, autofix.into())
{
debug!("Cache hit for: {}", path.to_string_lossy());
return Ok(Diagnostics::new(messages));
}
@@ -69,7 +71,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,14 +87,21 @@ 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)
};
// Re-populate the cache.
if let Some(metadata) = metadata {
cache::set(path, &metadata, settings, autofix.into(), &messages);
cache::set(
path,
package.as_ref(),
&metadata,
settings,
autofix.into(),
&messages,
);
}
Ok(Diagnostics { 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))
}
}
@@ -90,6 +91,22 @@ fn resolve(
pub fn main() -> Result<ExitCode> {
// Extract command-line arguments.
let (cli, overrides) = Cli::parse().partition();
let default_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
eprintln!(
r#"
{}: `ruff` crashed. This indicates a bug in `ruff`. If you could open an issue at:
https://github.com/charliermarsh/ruff/issues/new?title=%5BPanic%5D
quoting the executed command, along with the relevant file contents and `pyproject.toml` settings, we'd be very appreciative!
"#,
"error".red().bold(),
);
default_panic_hook(info);
}));
let log_level = extract_log_level(&cli);
set_up_logging(&log_level)?;
@@ -113,39 +130,35 @@ 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 {
commands::explain(&code, format)?;
if let Some(rule) = cli.explain {
commands::explain(rule, format)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_settings {
@@ -200,7 +213,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 +224,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 +243,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 +254,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};
@@ -9,7 +11,7 @@ use itertools::iterate;
use ruff::fs::relativize_path;
use ruff::logging::LogLevel;
use ruff::message::{Location, Message};
use ruff::registry::RuleCode;
use ruff::registry::Rule;
use ruff::settings::types::SerializationFormat;
use ruff::{fix, notify_user};
use serde::Serialize;
@@ -33,7 +35,7 @@ struct ExpandedFix<'a> {
#[derive(Serialize)]
struct ExpandedMessage<'a> {
code: &'a RuleCode,
code: SerializeRuleAsCode<'a>,
message: String,
fix: Option<ExpandedFix<'a>>,
location: Location,
@@ -41,6 +43,23 @@ struct ExpandedMessage<'a> {
filename: &'a str,
}
struct SerializeRuleAsCode<'a>(&'a Rule);
impl Serialize for SerializeRuleAsCode<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.code())
}
}
impl<'a> From<&'a Rule> for SerializeRuleAsCode<'a> {
fn from(rule: &'a Rule) -> Self {
Self(rule)
}
}
pub struct Printer<'a> {
format: &'a SerializationFormat,
log_level: &'a LogLevel,
@@ -69,7 +88,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 +96,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 +111,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 +122,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,25 +139,28 @@ 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
.messages
.iter()
.map(|message| ExpandedMessage {
code: message.kind.code(),
code: message.kind.rule().into(),
message: message.kind.body(),
fix: message.fix.as_ref().map(|fix| ExpandedFix {
content: &fix.content,
@@ -145,7 +174,7 @@ impl<'a> Printer<'a> {
})
.collect::<Vec<_>>()
)?
);
)?;
}
SerializationFormat::Junit => {
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
@@ -165,8 +194,10 @@ impl<'a> Printer<'a> {
message.location.column(),
message.kind.body()
));
let mut case =
TestCase::new(format!("org.ruff.{}", message.kind.code()), status);
let mut case = TestCase::new(
format!("org.ruff.{}", message.kind.rule().code()),
status,
);
let file_path = Path::new(filename);
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
let classname = file_path.parent().unwrap().join(file_stem);
@@ -180,14 +211,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 +240,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)),
@@ -232,26 +267,27 @@ impl<'a> Printer<'a> {
":",
message.location.column(),
":",
message.kind.code().as_ref(),
message.kind.rule().code(),
message.kind.body(),
);
println!(
writeln!(
stdout,
"::error title=Ruff \
({}),file={},line={},col={},endLine={},endColumn={}::{}",
message.kind.code(),
message.kind.rule().code(),
message.filename,
message.location.row(),
message.location.column(),
message.end_location.row(),
message.end_location.column(),
label,
);
});
)?;
}
}
SerializationFormat::Gitlab => {
// Generate JSON with errors in GitLab CI format
// Generate JSON with violations 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
@@ -259,9 +295,9 @@ impl<'a> Printer<'a> {
.iter()
.map(|message| {
json!({
"description": format!("({}) {}", message.kind.code(), message.kind.body()),
"description": format!("({}) {}", message.kind.rule().code(), message.kind.body()),
"severity": "major",
"fingerprint": message.kind.code(),
"fingerprint": message.kind.rule().code(),
"location": {
"path": message.filename,
"lines": {
@@ -274,16 +310,32 @@ impl<'a> Printer<'a> {
)
.collect::<Vec<_>>()
)?
);
)?;
}
SerializationFormat::Pylint => {
// Generate violations in Pylint format.
// See: https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter
for message in &diagnostics.messages {
let label = format!(
"{}:{}: [{}] {}",
relativize_path(Path::new(&message.filename)),
message.location.row(),
message.kind.rule().code(),
message.kind.body(),
);
writeln!(stdout, "{label}")?;
}
}
}
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 +345,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 +385,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(),
@@ -339,10 +394,10 @@ fn print_message(message: &Message) {
":".cyan(),
message.location.column(),
":".cyan(),
message.kind.code().as_ref().red().bold(),
message.kind.rule().code().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 +409,6 @@ fn print_message(message: &Message) {
} else {
vec![]
};
let snippet = Snippet {
title: Some(Annotation {
label: None,
@@ -367,7 +421,7 @@ fn print_message(message: &Message) {
source: &source.contents,
line_start: message.location.row(),
annotations: vec![SourceAnnotation {
label: message.kind.code().as_ref(),
label: message.kind.rule().code(),
annotation_type: AnnotationType::Error,
range: source.range,
}],
@@ -384,13 +438,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())),
@@ -398,10 +458,10 @@ fn print_grouped_message(message: &Message, row_length: usize, column_length: us
":".cyan(),
message.location.column(),
" ".repeat(column_length - num_digits(message.location.column())),
message.kind.code().as_ref().red().bold(),
message.kind.rule().code().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 +473,6 @@ fn print_grouped_message(message: &Message, row_length: usize, column_length: us
} else {
vec![]
};
let snippet = Snippet {
title: Some(Annotation {
label: None,
@@ -426,7 +485,7 @@ fn print_grouped_message(message: &Message, row_length: usize, column_length: us
source: &source.contents,
line_start: message.location.row(),
annotations: vec![SourceAnnotation {
label: message.kind.code().as_ref(),
label: message.kind.rule().code(),
annotation_type: AnnotationType::Error,
range: source.range,
}],
@@ -444,6 +503,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

@@ -10,11 +10,8 @@ use std::{fs, process, str};
use anyhow::{anyhow, Context, Result};
use assert_cmd::Command;
use itertools::Itertools;
use log::info;
use ruff::logging::{set_up_logging, LogLevel};
use ruff::registry::RuleOrigin;
use strum::IntoEnumIterator;
use walkdir::WalkDir;
/// Handles `blackd` process and allows submitting code to it for formatting.
@@ -175,13 +172,6 @@ fn test_ruff_black_compatibility() -> Result<()> {
.filter_map(Result::ok)
.collect();
let codes = RuleOrigin::iter()
// Exclude ruff codes, specifically RUF100, because it causes differences that are not a
// 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(","))
.join(",");
let ruff_args = [
"-",
"--silent",
@@ -189,8 +179,11 @@ fn test_ruff_black_compatibility() -> Result<()> {
"--fix",
"--line-length",
"88",
"--select",
&codes,
"--select ALL",
// Exclude ruff codes, specifically RUF100, because it causes differences that are not a
// problem. Ruff would add a `# noqa: W292` after the first run, black introduces a
// newline, and ruff removes the `# noqa: W292` again.
"--ignore RUF",
];
for entry in paths {

View File

@@ -151,3 +151,12 @@ fn test_show_source() -> Result<()> {
assert!(str::from_utf8(&output.get_output().stdout)?.contains("l = 1"));
Ok(())
}
#[test]
fn explain_status_codes() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
cmd.args(["-", "--explain", "F401"]).assert().success();
let mut cmd = Command::cargo_bin(BIN_NAME)?;
cmd.args(["-", "--explain", "RUF404"]).assert().failure();
Ok(())
}

View File

@@ -1,12 +1,8 @@
[package]
name = "ruff_dev"
version = "0.0.221"
version = "0.0.231"
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 = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
schemars = { version = "0.8.11" }
serde_json = {version="1.0.91"}
strum = { version = "0.24.1", features = ["strum_macros"] }

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